← На главную

Микроконтроллеры STM32: выходим в сеть с Wiznet W5500

Wiznet W5500 – это Ethernet-контроллер с интерфейсом SPI. Чип поддерживает стандарты 10baseT и 100baseT. Характерной особенностью контроллера является то, что он имеет аппаратную реализацию TCP/IPv4. Это позволяет существенно разгрузить работающий с ним микроконтроллер. Wiznet W5500 поддерживает до 8 сокетов и имеет суммарно 16 Кб памяти на прием и еще 16 Кб на передачу. Эта память может быть распределена между сокетами произвольным образом. Давайте же разберемся, как работать с этим чипом на примере МК STM32.

Примечание: В этом контексте вас может заинтересовать статья Изучаем Ethernet-фреймы с помощью осциллографа, если вдруг вы ее пропустили.

В свои экспериментах я использовал такой модуль:

Ethernet-модуль на базе Wiznet W5500

Цена устройства на 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:

#define _WIZCHIP_ W5500

Основной код проекта рассмотрим по частям. Первым делом добавляем необходимые инклудники:

#include "socket.h" #include "dhcp.h" #include "dns.h"

Мы будем использовать три сокета – один для DHCP, один для DNS, и еще один для хождения по HTTP:

#define DHCP_SOCKET 0 #define DNS_SOCKET 1 #define HTTP_SOCKET 2

Для работы DHCP и DNS нам понадобятся временные буферы:

// 1K should be enough, see https://forum.wiznet.io/t/topic/1612/2 uint8_t dhcp_buffer[1024]; // 1K seems to be enough for this buffer as well uint8_t dns_buffer[1024];

Также объявим процедуры, через которые библиотека будет ходить в SPI:

void W5500_Select(void) { 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)); }

Эти процедуры должны быть зарегистрированы таким образом:

UART_Printf("Registering W5500 callbacks...\r\n"); 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 Кб на прием и передачу:

UART_Printf("Calling wizchip_init()...\r\n"); 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:

UART_Printf("Calling DHCP_init()...\r\n"); 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);

Ему также нужно несколько колбэков, роль которых очевидна из названия:

volatile bool ip_assigned = false; 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"); }

Эти колбэки регистрируются следующим образом:

UART_Printf("Registering DHCP callbacks...\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:

UART_Printf("Calling DHCP_run()...\r\n"); // 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 должна вызываться регулярно, например, по таймеру.

Выводим полученную информацию, а также передаем ее самой библиотеке:

getIPfromDHCP(net_info.ip); 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»:

UART_Printf("Calling DNS_init()...\r\n"); 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-ый порт:

UART_Printf("Creating socket...\r\n"); 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-запрос:

char req[] = "GET / HTTP/1.0\r\nHost: eax.me\r\n\r\n"; 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; }

Принимаем ответ:

char buff[32]; 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); }

Наконец, закрываем сокет:

UART_Printf("Closing socket.\r\n"); 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 обычных сокетов:

// create a socket: TCP, UDP or RAW 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.