Микроконтроллеры STM32: выходим в сеть с Wiznet W5500
3 декабря 2018
Wiznet W5500 — это Ethernet-контроллер с интерфейсом SPI. Чип поддерживает стандарты 10baseT и 100baseT. Характерной особенностью контроллера является то, что он имеет аппаратную реализацию TCP/IPv4. Это позволяет существенно разгрузить работающий с ним микроконтроллер. Wiznet W5500 поддерживает до 8 сокетов и имеет суммарно 16 Кб памяти на прием и еще 16 Кб на передачу. Эта память может быть распределена между сокетами произвольным образом. Давайте же разберемся, как работать с этим чипом на примере МК STM32.
Примечание: В этом контексте вас может заинтересовать статья Изучаем Ethernet-фреймы с помощью осциллографа, если вдруг вы ее пропустили.
В свои экспериментах я использовал такой модуль:
Цена устройства на eBay составляет около 4$. На модуле есть стабилизатор напряжения, благодаря чему его можно питать как от 5 В, так и от 3.3 В. Сам же чип прекрасно работает как с 3.3-вольтовой, так и 5-иволтовой логикой. Таким образом, его можно без труда использовать с любым другим микроконтроллером, не обязательно STM32, или даже с FPGA. В частности, для Arduino существует библиотека Ethernet2 от Adafruit.
Fun fact! На момент написания этих строк, Sigrok не имел декодера протокола для Wiznet W5500. Между тем, протокол этот довольно несложен, и описан в даташите W5500 [PDF]. Отличная возможность законтрибьютить в опенсорс ;)
Существует также официальная библиотека от самого производителя под названием ioLibrary_Driver, которой мы и воспользуемся. Библиотека имеет лицензию MIT. Она не завязана на конкретный микроконтроллер, и может быть с тем же успехом использована с STM8, AVR, 8051 или PIC.
Код библиотеки был скопирован в каталог Lib/ioLibrary_Driver проекта. В Makefile было дописано следующее:
C_SOURCES = \
Lib/ioLibrary_Driver/Ethernet/wizchip_conf.c \
Lib/ioLibrary_Driver/Ethernet/socket.c \
Lib/ioLibrary_Driver/Ethernet/W5500/w5500.c \
Lib/ioLibrary_Driver/Internet/DHCP/dhcp.c \
Lib/ioLibrary_Driver/Internet/DNS/dns.c \
# ... прочие файлы были здесь и раньше ...
# ...
C_INCLUDES = \
-ILib/ioLibrary_Driver/Ethernet \
-ILib/ioLibrary_Driver/Internet/DHCP \
-ILib/ioLibrary_Driver/Internet/DNS \
# ... прочие каталоги были здесь и раньше ...
# ...
Правим Lib/ioLibrary_Driver/Ethernet/wizchip_conf.h:
Основной код проекта рассмотрим по частям. Первым делом добавляем необходимые инклудники:
#include "dhcp.h"
#include "dns.h"
Мы будем использовать три сокета — один для DHCP, один для DNS, и еще один для хождения по HTTP:
#define DNS_SOCKET 1
#define HTTP_SOCKET 2
Для работы DHCP и DNS нам понадобятся временные буферы:
uint8_t dhcp_buffer[1024];
// 1K seems to be enough for this buffer as well
uint8_t dns_buffer[1024];
Также объявим процедуры, через которые библиотека будет ходить в SPI:
HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin,
GPIO_PIN_RESET);
}
void W5500_Unselect(void) {
HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin,
GPIO_PIN_SET);
}
void W5500_ReadBuff(uint8_t* buff, uint16_t len) {
HAL_SPI_Receive(&hspi1, buff, len, HAL_MAX_DELAY);
}
void W5500_WriteBuff(uint8_t* buff, uint16_t len) {
HAL_SPI_Transmit(&hspi1, buff, len, HAL_MAX_DELAY);
}
uint8_t W5500_ReadByte(void) {
uint8_t byte;
W5500_ReadBuff(&byte, sizeof(byte));
return byte;
}
void W5500_WriteByte(uint8_t byte) {
W5500_WriteBuff(&byte, sizeof(byte));
}
Эти процедуры должны быть зарегистрированы таким образом:
reg_wizchip_cs_cbfunc(W5500_Select, W5500_Unselect);
reg_wizchip_spi_cbfunc(W5500_ReadByte, W5500_WriteByte);
reg_wizchip_spiburst_cbfunc(W5500_ReadBuff, W5500_WriteBuff);
Далее распределяем память между сокетами. Пусть у каждого сокета будет по 2 Кб на прием и передачу:
uint8_t rx_tx_buff_sizes[] = {2, 2, 2, 2, 2, 2, 2, 2};
wizchip_init(rx_tx_buff_sizes, rx_tx_buff_sizes);
Инициализируем клиент DHCP:
wiz_NetInfo net_info = {
.mac = { 0xEA, 0x11, 0x22, 0x33, 0x44, 0xEA },
.dhcp = NETINFO_DHCP
};
// set MAC address before using DHCP
setSHAR(net_info.mac);
DHCP_init(DHCP_SOCKET, dhcp_buffer);
Ему также нужно несколько колбэков, роль которых очевидна из названия:
void Callback_IPAssigned(void) {
UART_Printf("Callback: IP assigned! Leased time: %d sec\r\n",
getDHCPLeasetime());
ip_assigned = true;
}
void Callback_IPConflict(void) {
UART_Printf("Callback: IP conflict!\r\n");
}
Эти колбэки регистрируются следующим образом:
reg_dhcp_cbfunc(
Callback_IPAssigned,
Callback_IPAssigned,
Callback_IPConflict
);
Согласно документации, мы также должны раз в секунду вызывать процедуру DHCP_time_handler
. Для этого проще всего отредактировать Src/stm32f4xx_it.c:
#include "dhcp.h"
# ...
void SysTick_Handler(void) {
/* USER CODE BEGIN SysTick_IRQn 0 */
static uint16_t ticks = 0;
ticks++;
if(ticks == 1000) {
DHCP_time_handler();
ticks = 0;
}
/* USER CODE END SysTick_IRQn 0 */
/* ... прочий код был здесь и раньше ... */
}
Возвращаемся к основному коду программы. Получаем IP-адрес и информацию о сети по DHCP:
// actually should be called in a loop, e.g. by timer
uint32_t ctr = 10000;
while((!ip_assigned) && (ctr > 0)) {
DHCP_run();
ctr--;
}
if(!ip_assigned) {
UART_Printf("\r\nIP was not assigned :(\r\n");
return;
}
В общем случае, DHCP_run
должна вызываться регулярно, например, по таймеру.
Выводим полученную информацию, а также передаем ее самой библиотеке:
getGWfromDHCP(net_info.gw);
getSNfromDHCP(net_info.sn);
uint8_t dns[4];
getDNSfromDHCP(dns);
UART_Printf(
"IP: %d.%d.%d.%d\r\n"
"GW: %d.%d.%d.%d\r\n"
"Net: %d.%d.%d.%d\r\n"
"DNS: %d.%d.%d.%d\r\n",
net_info.ip[0], net_info.ip[1], net_info.ip[2], net_info.ip[3],
net_info.gw[0], net_info.gw[1], net_info.gw[2], net_info.gw[3],
net_info.sn[0], net_info.sn[1], net_info.sn[2], net_info.sn[3],
dns[0], dns[1], dns[2], dns[3]
);
UART_Printf("Calling wizchip_setnetinfo()...\r\n");
wizchip_setnetinfo(&net_info);
Далее резолвим доменное имя «eax.me»:
DNS_init(DNS_SOCKET, dns_buffer);
uint8_t addr[4];
{
char domain_name[] = "eax.me";
UART_Printf("Resolving domain name \"%s\"...\r\n", domain_name);
int8_t res = DNS_run(dns, (uint8_t*)&domain_name, addr);
if(res != 1) {
UART_Printf("DNS_run() failed, res = %d", res);
return;
}
UART_Printf("Result: %d.%d.%d.%d\r\n",
addr[0], addr[1], addr[2], addr[3]);
}
Создаем сокет и коннектимся на 80-ый порт:
uint8_t http_socket = HTTP_SOCKET;
uint8_t code = socket(http_socket, Sn_MR_TCP, 10888, 0);
if(code != http_socket) {
UART_Printf("socket() failed, code = %d\r\n", code);
return;
}
UART_Printf("Socket created, connecting...\r\n");
code = connect(http_socket, addr, 80);
if(code != SOCK_OK) {
UART_Printf("connect() failed, code = %d\r\n", code);
close(http_socket);
return;
}
Посылаем HTTP-запрос:
uint16_t len = sizeof(req) - 1;
uint8_t* buff = (uint8_t*)&req;
while(len > 0) {
UART_Printf("Sending %d bytes...\r\n", len);
int32_t nbytes = send(http_socket, buff, len);
if(nbytes <= 0) {
UART_Printf("send() failed, %d returned\r\n", nbytes);
close(http_socket);
return;
}
UART_Printf("%d bytes sent!\r\n", nbytes);
len -= nbytes;
}
Принимаем ответ:
for(;;) {
int32_t nbytes = recv(http_socket,
(uint8_t*)&buff, sizeof(buff)-1);
if(nbytes == SOCKERR_SOCKSTATUS) {
UART_Printf("\r\nConnection closed.\r\n");
break;
}
if(nbytes <= 0) {
UART_Printf("\r\nrecv() failed, %d returned\r\n", nbytes);
break;
}
buff[nbytes] = '\0';
UART_Printf("%s", buff);
}
Наконец, закрываем сокет:
close(http_socket);
Прошиваем и, если все было сделано правило, в UART мы увидим:
Request sent. Reading response...
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sun, 31 Dec 2018 23:59:59 GMT
Content-Type: text/html
Content-Length: 178
Connection: close
Location: https://eax.me/
<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
Connection closed.
В целом, основной API библиотеки похож на API обычных сокетов:
int8_t socket(uint8_t sn, uint8_t protocol, uint16_t port,
uint8_t flag);
// close socket
int8_t close(int8_t sn);
int8_t connect(uint8_t sn, uint8_t * addr, uint16_t port);
int8_t listen(uint8_t sn);
int8_t disconnect(uint8_t sn);
// TCP
int32_t send(uint8_t sn, uint8_t * buf, uint16_t len);
int32_t recv(uint8_t sn, uint8_t * buf, uint16_t len);
// UDP
int32_t sendto(uint8_t sn, uint8_t * buf, uint16_t len,
uint8_t * addr, uint16_t port);
int32_t recvfrom(uint8_t sn, uint8_t * buf, uint16_t len,
uint8_t * addr, uint16_t *port);
Помимо рассмотренных возможностей, в ioLibrary_Driver также есть реализации HTTP-сервера, клиента и сервера FTP, клиента TFTP, агента SNMP, клиента SNTP и клиента MQTT. Библиотека активно развивается. Так что, если вы читаете этот пост из далекого будущего, возможно, в ней уже есть готовый HTTP-клиент, и вам не придется писать его на сокетах, как это делал я. (Впрочем, не убежден, что библиотека для работы с Ethernet-контроллером — подходящее место для реализаций всех этих протоколов.)
За дополнительной информацией обращайтесь к документации библиотеки, она довольно неплоха. Документация генерируется при помощи Doxygen, но зачем-то упаковывается в формат chm. Для его просмотра в Linux можно воспользоваться программой Kchmviewer. Также у Wiznet есть вики-сайт, и даже форум. На вики можно узнать о ряде других продуктов Wiznet, не исключая модулей Wi-Fi.
Полную версию исходников к этому посту, как обычно, вы найдете на GitHub.
Метки: STM32, Сети, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.