Как стать автором
Обновить

Ethernet поверх USB на STM32F4

Время на прочтение9 мин
Количество просмотров58K


Недавно возникла идея заставить плату на базе МК STM32F4 работать по сети. Поскольку на борту отсутствовал Ethernet PHY контроллер, то единственным вариантом было использовать USB FullSpeed интерфейс для эмуляции Ethernet устройства. Распространённый стандарт USB-класса, реализующий данную функцию, называется RNDIS.
К своему огорчению, поиск RNDIS драйвера для STM32 не увенчался успехом. Впрочем, это не удивило, т.к. открытые примеры использования USB порта у STM32 ограничиваются только теми, что предоставил нам производитель.
Захотелось исправить сию несправедливость. А заодно и поиметь нужные исходники, благо в будущем они пригодятся.
Сейчас, когда демонстрационная версия библиотеки готова, выкладываю её в свет на правах MIT-лицензии. Поэтому, все кому библиотека интересна — пользуйтесь «на здоровье». Библиотека имеет название LRNDIS, первая буква которого означает использование сетевого стека для встраиваемых систем «LwIP».
Для демонстрации возможностей библиотеки был создан пример на плате stm32f4discovery. Его работа заключается в поддержке основных сервисов (DHCP и DNS сервера) и передаче usb-хосту запрашиваемых WEB-страниц. Таким образом, наш discovery превратился в почти полноценный WEB-сервер, работающий по порту USB!
Пару слов о том, где это применимо.
В быту RNDIS устройства обычно являются USB-модемами для доступа в Интернет. Возможно, такое применение, действительно, окажется полезным, если разработчик выберет STM32 в роли связующей цепочки между ПК и радиочастотным (или другим) трансивером. Или, может быть, захочет расширить собственную сеть на Ethernet-сегмент?
Другое применение, в котором нахожу основную пользу для себя, — это интерфейс управления сложными устройствами. Типовое решение в этой области — создание терминального ПО. При этом приходится заниматься его поддержкой вместе с поддержкой устройства, что бывает неудобным. Собственно, в отказе от такой схемы в пользу управляющего Web-интерфейса и заключается смысл возможного применения библиотеки. Вспомните Web-интерфейсы настройки роутеров. Удобно. Красиво. Без лишнего ПО.

Итак, если Вы заинтересовались, читайте далее…

1. Драйвер RNDIS


На этапе написания решалось две задачи: подписать наше устройство и поддержать стандарт RNDIS.
Подпись устройства сводится к составлению верных USB-дескрипторов. Значение VID выбрано 0x0483 (STMicroelectronics), значение PID — 0x0123 (произвольное). Само собой, в коммерческом применении так делать не следует.
Просмотреть дескрипторы
Device Descriptor
Offset Field Size Value Description
0 bLength 1 12h
1 bDescriptorType 1 01h Device
2 bcdUSB 2 0200h USB Spec 2.0
4 bDeviceClass 1 02h CDC Control
5 bDeviceSubClass 1 00h
6 bDeviceProtocol 1 00h
7 bMaxPacketSize0 1 40h 64 bytes
8 idVendor 2 0483h SGS Thomson Microelectronics
10 idProduct 2 0123h
12 bcdDevice 2 0001h 0.01
14 iManufacturer 1 01h «Fetisov Sergey»
15 iProduct 1 02h «STM32F4 RNDIS»
16 iSerialNumber 1 03h «00000000123C»
17 bNumConfigurations 1 01h
Configuration Descriptor 1
Offset Field Size Value Description
0 bLength 1 09h
1 bDescriptorType 1 02h Configuration
2 wTotalLength 2 0043h
4 bNumInterfaces 1 02h
5 bConfigurationValue 1 01h
6 iConfiguration 1 00h
7 bmAttributes 1 40h Self Powered
8 bMaxPower 1 01h 2 mA
Interface Descriptor 0/0 CDC Control, 1 Endpoint
Offset Field Size Value Description
0 bLength 1 09h
1 bDescriptorType 1 04h Interface
2 bInterfaceNumber 1 00h
3 bAlternateSetting 1 00h
4 bNumEndpoints 1 01h
5 bInterfaceClass 1 02h CDC Control
6 bInterfaceSubClass 1 02h Abstract Control Model
7 bInterfaceProtocol 1 FFh Vendor-Specific
8 iInterface 1 00h
Header Functional Descriptor
Offset Field Size Value Description
0 bFunctionLength 1 05h
1 bDescriptorType 1 24h CS Interface
2 bDescriptorSubtype 1 00h Header
3 bcdCDC 2 0110h 1.10
Call Management Functional Descriptor
Offset Field Size Value Description
0 bFunctionLength 1 05h
1 bDescriptorType 1 24h CS Interface
2 bDescriptorSubtype 1 01h Call Management
3 bmCapabilities 1 00h
4 bDataInterface 1 01h
Abstract Control Management Functional Descriptor
Offset Field Size Value Description
0 bFunctionLength 1 04h
1 bDescriptorType 1 24h CS Interface
2 bDescriptorSubtype 1 02h Abstract Control Management
3 bmCapabilities 1 00h Requests/notifications not supported
Union Functional Descriptor
Offset Field Size Value Description
0 bFunctionLength 1 05h
1 bDescriptorType 1 24h CS Interface
2 bDescriptorSubtype 1 06h Union
3 bControlInterface 1 00h
4 bSubordinateInterface0 1 01h CDC Data
Endpoint Descriptor 81 1 In, Interrupt, 80 ms
Offset Field Size Value Description
0 bLength 1 07h
1 bDescriptorType 1 05h Endpoint
2 bEndpointAddress 1 81h 1 In
3 bmAttributes 1 03h Interrupt
4 wMaxPacketSize 2 0008h 8 bytes
6 bInterval 1 50h 80 ms
Interface Descriptor 1/0 CDC Data, 2 Endpoints
Offset Field Size Value Description
0 bLength 1 09h
1 bDescriptorType 1 04h Interface
2 bInterfaceNumber 1 01h
3 bAlternateSetting 1 00h
4 bNumEndpoints 1 02h
5 bInterfaceClass 1 0Ah CDC Data
6 bInterfaceSubClass 1 00h
7 bInterfaceProtocol 1 00h
8 iInterface 1 00h
Endpoint Descriptor 82 2 In, Bulk, 64 bytes
Offset Field Size Value Description
0 bLength 1 07h
1 bDescriptorType 1 05h Endpoint
2 bEndpointAddress 1 82h 2 In
3 bmAttributes 1 02h Bulk
4 wMaxPacketSize 2 0040h 64 bytes
6 bInterval 1 00h
Endpoint Descriptor 03 3 Out, Bulk, 64 bytes
Offset Field Size Value Description
0 bLength 1 07h
1 bDescriptorType 1 05h Endpoint
2 bEndpointAddress 1 03h 3 Out
3 bmAttributes 1 02h Bulk
4 wMaxPacketSize 2 0040h 64 bytes
6 bInterval 1 00h


Поддержка стандарта осуществлена в соответствии с документацией.

Также в сети присутствует множество RNDIS-драйверов для других платформ (1, 2, 3), что существенно упростило разработку.
В части обмена драйвер повторяет особенность CDC-класса за исключением того, что передаваемые пакеты оборачиваются служебной информацией. Также существует специальный интерфейс для передачи запросов от usb-хоста с целью настройки устройства или получения его статуса. Детально код драйвера можно изучить в модуле usbd_rndis_core.c.
Минимальная настройка драйвера при встраивании заключается в изменении определений в файле usbd_rndis_core.h.
— MAC-адрес устройства (PERMANENT_HWADDR, STATION_HWADDR)
— Идентификатор производителя (RNDIS_VENDOR)
— Значение MTU (ETH_MTU):

#define ETH_MTU          1500                           // MTU value
#define ETH_LINK_SPEED   250000                         // bits per sec
#define RNDIS_VENDOR     "fetisov"                      // NIC vendor name
#define STATION_HWADDR   0x20,0x89,0x84,0x6A,0x96,0xAA  // station MAC
#define PERMANENT_HWADDR 0x20,0x89,0x84,0x6A,0x96,0xAA  // permanent MAC

Также в файле usbd_desc.h следует изменить название продукта (USBD_PRODUCT_STRING) и производителя (USBD_MANUFACTURER_STRING).

Процесс установки драйвера в ОС Windows:



1. Пункт меню «обновить драйвер»;
2. Выполнить поиск драйвера;
3. Выбрать драйвер из списка;
4. В списке классов устройств выбрать «Сетевые адаптеры»;
5. В списке производителе выбрать «Microsoft Corporation»;
6. И продукт «Remote NDIS based Internet Sharing Device».

Более подробно процесс установки стандартного RNDIS устройства описан по ссылке.

2. Прикручиваем LwIP




Драйвер RNDIS обеспечивает лишь транспортную функцию для кадров стандарта 802.3 (Ethernet). Очевидно, что поддержку всего многообразия пакетов и стандартов требуется возложить на сетевой стек. В его роли было принято решение использовать популярный стек для встраиваемых систем lwip последней (на текущий момент) версии 1.4.1. Надо отдать должное авторам стека, тот получился весьма просто интегрируемым. При всём при этом, код стека богат полезными комментариями и инструкциями.

lwIP is a small independent implementation of the TCP/IP protocol suite that has been initially developed by Adam Dunkels and is now continued here.
The focus of the lwIP TCP/IP implementation is to reduce resource usage while still having a full scale TCP. This makes lwIP suitable for use in embedded systems with tens of kilobytes of free RAM and room for around 40 kilobytes of code ROM.
Main features include:
— Protocols: IP, ICMP, UDP, TCP, IGMP, ARP, PPPoS, PPPoE
— DHCP client, DNS client, AutoIP/APIPA (Zeroconf), SNMP agent (private MIB support)
— APIs: specialized APIs for enhanced performance, optional Berkeley-alike socket API
— Extended features: IP forwarding over multiple network interfaces, TCP congestion control, RTT estimation and fast recovery/fast retransmit
— Addon applications: HTTP server, SNTP client, SMTP client, ping, NetBIOS nameserver

Запуск стека под stm32 ограничился включением в процесс сборки конкретного набора исходных файлов и вводом определений в файле lwipopts.h:

#define NO_SYS                          1
#define LWIP_RAW                        1
#define LWIP_NETCONN                    0
#define LWIP_SOCKET                     0
#define LWIP_DHCP                       0
#define LWIP_ICMP                       1
#define LWIP_UDP                        1
#define LWIP_TCP                        1
#define ETH_PAD_SIZE                    0
#define LWIP_IP_ACCEPT_UDP_PORT(p)      ((p) == PP_NTOHS(67))

#define MEM_SIZE                        10000
#define TCP_MSS                         (1500 /*mtu*/ - 14 /*ethhdr*/ - 20 /*iphdr*/ - 20 /*tcphhr*/)

#define ETHARP_SUPPORT_STATIC_ENTRIES   1

Нужно отметить, работа по портированию стека на STM32F4 производилась и раньше фирмой STMicroelectronics (приложение STSW-STM32070).

Запускаем DHCP-сервер




Добавление DHCP-сервера в состав библиотеки связано с необходимостью инициализировать сетевой интерфейс на стороне host-а. По умолчанию, создаваемый при подключении устройства интерфейс настроен на автоматическое получение ip-адреса. Библиотека также успешно работает и при статической адресации, но это является не совсем удобным.

К сожалению, среди поставляемых lwip средств, DHCP-сервер отсутствует.

Однако, это не является существенной проблемой, т.к. в своей минимальной реализации DHCP-сервер весьма минималистичен.

В сети присутствует, пожалуй, единственный пример DHCP-сервера, работающего со стеком lwip. Данный источник оказался весьма полезным для изучения, хоть и не пригодным для встраивания по принципу «as-is» по причине отсутствия возможности конфигурирования и использования socket-api.

Поэтому было принято решение написать DHCP сервер.

И вот его скромные возможности:
— выдача адресов на произвольное время
— резервирование адресов по MAC адресу
— настройка DNS-сервера

Подключение сервера в тестовом проекте:

#define NUM_DHCP_ENTRY 3

static dhcp_entry_t entries[NUM_DHCP_ENTRY] =
{
	// mac    ip address        subnet mask        lease time
	{ {0}, {192, 168, 7, 2}, {255, 255, 255, 0}, 24 * 60 * 60 },
	{ {0}, {192, 168, 7, 3}, {255, 255, 255, 0}, 24 * 60 * 60 },
	{ {0}, {192, 168, 7, 4}, {255, 255, 255, 0}, 24 * 60 * 60 }
};

static dhcp_config_t dhcp_config =
{
	{192, 168, 7, 1}, 67, // server address, port
	{192, 168, 7, 1},     // dns server
	"stm",                // dns suffix
	NUM_DHCP_ENTRY,       // num entry
	entries               // entries
};

int main(void)
{
...
	while (dhserv_init(&dhcp_config) != ERR_OK) ;
...
}


Запускаем DNS-сервер




Желаемый нами результат работы — отображение web-страницы при вводе в браузере некоторого имени ресурса. Однако, это возможно только при наличии DNS-сервера, который будет «знать» о нашем хосте. Конечно, этот результат доступен, если в адресной строке напрямую ввести ip-адрес: 192.168.7.1. Такой адрес имеет по умолчанию наше устройство. Однако, будем более искусны и запустим DNS-сервер.

В отличии от DHCP, текущая реализация DNS-сервера ещё «тоньше». На данный момент она позволяет обработать только стандартные DNS-запросы на одну запись.

Запуск сервера в проекте:

bool dns_query_proc(const char *name, ip_addr_t *addr)
{
	if (strcmp(name, "run.stm") == 0 || strcmp(name, "www.run.stm") == 0)
	{
		addr->addr = *(uint32_t *)ipaddr;
		return true;
	}
	return false;
}

int main(void)
{
...
	while (dnserv_init(PADDR(ipaddr), 53, dns_query_proc) != ERR_OK) ;
...
}


Запускаем HTTP-сервер


Долго и безрезультатно пришлось бороться с запуском известного сервера из пакета lwip «contrib-1.4.1». До сих пор для меня остаётся загадкой таинственный HardFault, возникающий на, казалось бы, ровном месте. Были проверены все настройки, адреса чтения и записи, глубина стека… Но, увы.

Позже я начал писать рабочий http-сервер и HardFault повторился тогда, когда я обращался к области памяти, выделенной функцией mem_malloc. При этом адреса были валидны и относились к внутренней RAM. В общем, с радостью попрощался с динамической аллокацией и принялся использовать статику в сервере. Однако, вопрос о причине HardFault остался открытым, и поэтому нужно иметь в виду, что пользоваться lwip функциями mem_* в текущей версии библиотеки небезопасно.

Итак, результат был всё таки получен.

Код включения HTTP-сервера:

static const my_page_t my_pages[] =
{
	{ "/",          200, MIME_TEXT_HTML, page1_html,     page1_html_size      },
	{ "/page2.htm", 200, MIME_TEXT_HTML, page2_html,     page2_html_size      },
	{ "/page3.htm", 200, MIME_TEXT_HTML, page3_html,     page3_html_size      },
	{ "/check.gif", 200, MIME_IMAGE_GIF, check_png,      check_png_size       },
	{ NULL,         404, MIME_TEXT_HTML, page_not_found, page_not_found_size  }
};

bool on_http_req(const http_req_t *req, http_resp_t *resp, void **arg)
{
	const my_page_t *page;
	for (page = my_pages; page->uri != NULL; page++)
		if (strcmp(page->uri, req->uri) == 0) break;
	resp->code = page->code;
	resp->cont_len = page->size;
	resp->mime = page->mime;
	resp->conn_type = CT_CLOSE;
	*arg = (void *)page;
	return true;
}

void http_write_data()
{
	for (int i = 0; i < HTTP_SERVER_MAX_CON; i++)
	{
		int n;
		const htcon_t *con;
		my_page_t *page;
		con = htcon(i);
		if (con == NULL) continue;
		page = (my_page_t *)con->arg;
		if (con->state == CON_CLOSED)
		{
			htcon_free(i);
			continue;
		}
		if (con->state != CON_ACTIVE) continue;
		n = page->size - con->writed;
		htcon_write(i, (char *)page->data + con->writed, n);
	}
}

int main(void)
{
	...

	htserv_on_req = on_http_req;
	while (htserv_init(80) != ERR_OK) ;

	while (1)
	{
		stmr();            // call software timers
		usb_polling();     // usb device polling
		http_write_data(); // writes http response
	}
}


Нерешённые вопросы


1. Ньюансы релицензирования стека lwip, который может иметь свои условия включения в состав другого ПО;
2. Проблема с работой под ОС Linux;
3. Добавление обработки POST-запросов;
4. Доработка DNS-сервера для обработки «многовопросных» пакетов;
5. Проблема с mem_malloc.
Теги:
Хабы:
Всего голосов 28: ↑26 и ↓2+24
Комментарии17

Публикации

Истории

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань