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

3 декабря 2018

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.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.