В предыдущей статье я рассказывал о том, как хорошо работать с микроконтроллерами STM32 без операционной системы. Однако может наступить момент, когда вам понадобится наладить взаимодействие с вашим МК по сети. Например, что-нибудь типа умного дома или передача отладочной и диагностической информации на web страницу. Наличие Ethernet интерфейса — это конечно сильный аргумент для того, чтобы бросить STM32 и перейти на полноценный ARM процессор под управлением Linux. Однако, этот аргумент парируется очень просто: на рынке есть компактные Ethernet модули, которые можно задействовать в наших STM32 проектах. Речь идет о чипах семейства W5500 WIZnet, на основе которых эти модули и собраны. Добавляем к нашему проекту этот модуль (хвала Али Экспрессу, как обычно!), и получаем функциональность Ethernet в МК STM32.
Поскольку в этом эксперименте я использовал макетную плату без пайки, все стало выглядеть очень симпатично (по сравнению с предыдущими экспериментами). Поэтому не стыдно выложить фото этой сборки:
Работать с модулем W5500 — это сущий ад. В нем куча регистров, в которые нужно заносить и считывать информацию. Когда я раскрыл мануал этого чипа, то первое что я сделал — это сразу закрыл его и начал искать библиотеку, в которой вся эта регистровая часть уже реализована и которая предоставляет такие привычные и понятные интерфейсы: socket(), listen() и так далее. И я ее нашел.
Как работать с библиотекой я расскажу дальше, а пока поработаем с нашим фото.
Пройдемся по макетке слева направо и посмотрим, что я там разместил.
Макетная сборка
Модуль W5500
Слева, как вы уже догадались, Ethernet модуль W5500. В этом варианте исполнения его пины смотрят вниз, что удобно для размещения на макетной плате. Есть вариант и пинами вверх, что будет удобно если вы решите просто соединять контакты напрямую кабелями. Собственно чип W5500 распаян под зеленой платкой, из-за чего создается впечатление что кроме Ethernet разъема больше ничего нет. На самом деле это не так. Чип берет на себя всю работу по поддержке сетевых протоколов, а работать с ним нужно по интерфейсу SPI. Поэтому красные и темные проводки — это и есть соединения SPI с микроконтроллером.
Кто уже работал с SPI, сейчас испытывают небольшой диссонанс. Этот протокол вообще-то говоря принято использовать для внутриплатных соединений, и выводить его наружу считается моветоном. Пришлось поступиться принципами ради демонстрационных целей, потому что нужна скорость, которую другие популярные интерфейсы — I2C и UART обеспечивать не могут. Если же вы распаяете модуль W5500 на своей собственной плате, то правила поведения в приличном обществе будут соблюдены.
UART
Смещаем взгляд правее и видим вертикально воткнутую платку. Это адаптер UART-USB, с которым мы познакомились раньше. Его мы используем исключительно для отладочных целей, для того чтобы выводить сообщения нашей программы на экран PC.
От МК этой плате требуется только одна линия: Tx (отдаем строчки), которая на адаптере будет обозначаться уже как Rx (принимаем строчки). Ну и само собой кабель, который вы видите на торце платы, подключен к USB разъему PC.
Переключатель на плате устанавливаем в положение 3.3В: это будут уровни линий передачи данных, с которыми работает адаптер.
МК STM32F103 Blue Pill
По центру — наш микроконтроллер, плата МК STM32F103C8T6. Здесь неожиданностей нет, единственное я использовал компактную версию под названием Blue Pill. Есть еще Black Pill, Red Pill но честно говоря не знаю чем все они принципиально отличаются друг от друга. Работать будут все.
В комплекте с этими платами идут линейки пинов, которые я распаял снизу. Это позволяет вставить пины контроллера в макетную плату. Если вы используете Pill’ы в своих устройствах, запаивать пины нет необходимости — можно сразу паять соединения на ламели по краям платы. В нашей конфигурации используются контакты соответствующие UART1 и SPI2.
К разъему JTAG в торце платы МК как обычно подключен программатор/отладчик ST-LINK V2. Его мы видим сверху. Его место — также в USB разъеме PC.
USB разъем для электропитания не используется — напряжение подается непосредственно на пины МК.
Электропитание
На правой стороне макетной платы расположен весьма удобный модуль электропитания, который обеспечивает независимые напряжения 5В или 3.3 В по обеим линиям питания макетной платы (синие и красные линии). Своими пинами модуль ложится в аккурат на эти линии, плюс к этому есть возможность подключиться еще сверху. Входное напряжение для модуля — 12В.
Модуль питания сконфигурирован джамперами на выходное напряжение 3.3В, отбор питания идет по внешним линиям модулями W5500 и МК STM32, которым требуется 3.3В. На фото видны эти желтые и коричневые короткие перемычки, которые идут рядом.
Нюанс: вы пожете подключить к МК вместо 3.3В 5В к соответствующему (другому естественно!) контакту. Эти 5В будут преобразованы в 3.3В.
Внимание! Не подключайте к плате внешнее питание 3.3В и 5В от USB одновременно. Можете потерять МК ) Также по этой причине провод +5В для JTAG от USB PC болтается в воздухе, как может заметить внимательный читатель )
Модуль UART требует электропитание 5В, которое мы обеспечиваем внешним проводком который подключается к пину +5В модуля питания. Есть подозрение, что он прекрасно будет работать и без этого проводка от USB, когда тот подключен (и работает на самом деле).
Ситуация когда к одной цепи питания модуля подключаются два источника питания — не совсем здоровая. Картинку портит поступающие 5В от USB, которые преобразуются в 3.3В и эта цепь потенциально может конфликтовать с внешней линией питания 3.3В. В таких случаях обычно ставят диод Шоттки, который защищает цепи питания 3.3В от реверсного тока. Будем надеяться, что так оно и есть )
Зачем вообще нужен модуль питания, если раньше мы прекрасно запитывали микроконтроллер от USB? Не забываем, что теперь у нас появился серьезный потребитель по напряжению 3.3В — это чип W5500, который не в лучшие для нас моменты готов принять до 185мА. Внутренний преобразователь уровня 5/3.3В микроконтроллера на такой подвиг не способен, если нам в голову придет мысль взять напряжение 3.3В оттуда.
Конфигурация
Как и раньше, для создания проекта воспользуемся услугами STM32CubeMX. По традиции создаем проект на основе Makefile, чтобы не городить огород с визуальными системами разработки (помните наш проект hardcore? Только Makefile и vim!). Как обычно, включаем пункт Debug:Serial Wire в меню SYS, чтобы иметь возможность прошивки и отладки. Сразу включатся пины PA13, PA14: через них STLINK будет общаться с МК.
Осталось сконфигурировать интерфейсы и все. Включаем USART1: Mode Asynchronous (Куб задействует пины PA9, PA10) и включаем SPI2: Mode Full-Duplex Master. На вкладке конфигурации для SPI2 нужно будет подправить параметр Prescaler: установить его в значение 4. Напоминаю еще раз, что UART нужен только для отладки, чтобы получать на нашем компе логи из программы контроллера.
Почему SPI2 а не SPI1? Так было в библиотеке поддержки чипа W5500, я не стал менять интерфейс. Видимо, разработчикам было удобно работать с пинами именно с этой стороны модуля микроконтроллера ) В нашем случае Куб выдаст для SPI пины PB13 — PB15.
Нам еще понадобится управлять пином PB12 как выходным (забегая вперед — библиотека использует его для включения чипа W5500). Поэтому кликаем на него и делаем GPIO_Output. И точно также, сразу делаем выходным пин PC13: к нему подключен светодиод МК, и грех не воспользоваться возможностью поморгать светодиодом в нужных местах.
Создаем проект и переходим к следующему шагу.
Соединения
Поскольку распиновка модулей W5500 и UART-USB и так известна, а распиновку Blue Pill мы уже получили с помощью Куба, займемся соединениями. Работа приятная, медитативная, навевает мысли о тщете всего сущего, хорошо заниматься этим перед сном, глубокое погружение гарантировано 🙂
Начнем с модуля W5500. Все, что нам от него нужно — это цепи SPI и питания. С подключением питания все понятно — задействуем цепи 3.3В и GND, самое главное — правильно соединить линии интерфейса SPI. Поскольку на модуле нет нумерации пинов, на схеме соединений слева будем указывать SPI обозначение пина по версии W5500, справа — номер пина и SPI обозначение по версии Куба:
|
Памятка по SPI:
- MISO — Master In, Slave Out: если мы в режиме мастер — принимаем данные, в режиме периферии — передаем;
- MOSI — Master Out, Slave In: в режиме мастер передаем данные, в режиме периферии — принимаем.
В нашей конфигурации W5500 будет периферийным модулем, который будет выбираться сигналом SCNn с МК. Поэтому по линии MISO данные будут идти от W5500 к МК, по линии MOSI — в противоположном направлении. Обратите внимание, что нет необходимости переполюсовки Tx/Rx как в UART: назначение вывода (вход или выход) меняется в зависимости от того, в каком режиме — Master или Slave работает модуль.
С модулем UART-USB все просто. Нужна только одна линия передачи данных:
1 2 |
UART-USB STM32 2 Rx ---------- Tx PA9 |
И в заключение — подключаем программатор/отладчик ST-LINK V2:
1 2 3 4 |
STLINK STM32 2 ----- SWDIO ----- 7 4 ----- SWCLK ----- 9 |
Цепь 3.3В остается неподключенной!
Не забудьте соединить «земли» всех модулей с землей модуля питания и подать 3.3В на модули W5500 и Blue Pill. Модуль UART-USB, как я говорил до этого, подключается к пину 5В модуля питания.
W5500 Library
Наконец наступил долгожданный момент — подключение библиотеки модуля W5500, чтобы с ним можно было работать вменяемым образом. Саму библиотеку можно скачать здесь. На самом деле, это не совсем библиотека, а набор файлов которые нужно включить в наш проект. Они точно также будут компилиться и собираться, как и наши собственные файлы. Поэтому по структуре это не library, а дополнительные к нашему проекту кусочки. Но я все равно буду называть это библиотекой — по существу. Кому нравится — могут называть это также драйвером; сама WizNet вообще использует наименование Library_Driver.
Подобные продукты третьих сторон я храню отдельно от своего проекта, потому как не нужно дублировать исходники в каждом отдельном проекте и тем более включать их в систему контроля версий. Для этого у меня специальная директория — /home/user/bin, куда и скачаем библиотеку WizNet она же драйвер W5500.
Распаковываем архив и меням имя на ioLibrary_Driver. Далее, нужно настроить библиотеку на определенную версию чипа. Для этого в файле Ethernet/wizchip_conf.h ищем строчку вида
1 |
#define _WIZCHIP_ W5500 // W5100, W5100S, W5200, W5300, W5500 |
И меняем W5500 на модель чипа, которая у вас в наличии. Само собой у меня менять ничего не пришлось.
Теперь нужно как-то интегрировать исходники библиотеки в наш проект. Это делается через Makefile. Добавляем в него строки следующим образом (будьте внимательны: показаны также существующие строки, чтобы вам было проще ориентироваться по make-файлу):
1 2 3 |
BUILD_DIR = build # путь к библиотеке пишем один раз IOLIB = /home/user/bin/ioLibrary_Driver/Ethernet |
1 2 3 4 5 6 7 8 |
Drivers/STM32F1xx_HAL_Driver/Src/stm32f1xx_hal_flash_ex.c \ Src/system_stm32f1xx.c \ # все что требуется включить в проект - эти три исходных файла $(IOLIB)/socket.c \ $(IOLIB)/wizchip_conf.c \ $(IOLIB)/W5500/w5500.c \ # наша оболочка к библиотеке, о ней - позже Src/tcp.c |
1 2 3 4 |
-IDrivers/CMSIS/Include \ # позаботимся о том, чтобы компилятор нашел header файлы библиотеки -I$(IOLIB) \ -I$(IOLIB)/W5500 |
Все достаточно просто, не правда ли? Наступил момент запуска библиотеки. Для этого ее нужно инициализировать и выполнить тестовый пример — запустить TCP сервер который будет принимать входящее соединение и отвечать чем-то в стиле «Hello World».
Software
Как вы наверное уже догадались, все это будет жить в файле tcp.c. Но вначале библиотеку нужно инициализировать. Код инициализации разместим в main.c, поскольку так ему будет проще доступаться к интерфейсным переменным.
Первым делом нужно приготовить для библиотеки наши callback функции, которые она будет дергать когда ей понадобится доступ к SPI для обмена с чипом WZ5500:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/* USER CODE BEGIN PFP */ /* Private function prototypes -----------------------------------------------*/ // предоставляем библиотеке возможности: // включить модуль W5500 сигналом SCNn=0 void cs_sel() { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); //CS LOW } // выключить модуль W5500 сигналом SCNn=1 void cs_desel() { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); //CS HIGH } // принять байт через SPI uint8_t spi_rb(void) { uint8_t rbuf; HAL_SPI_Receive(&hspi2, &rbuf, 1, 0xFFFFFFFF); return rbuf; } // передать байт через SPI void spi_wb(uint8_t b) { HAL_SPI_Transmit(&hspi2, &b, 1, 0xFFFFFFFF); } |
Теперь библиотеке нужно как-то сообщить о нашей проделанной работе. Регистрация наших callback-функций в библиотеке выполняется следующей парой строк:
1 2 |
reg_wizchip_cs_cbfunc(cs_sel, cs_desel); reg_wizchip_spi_cbfunc(spi_rb, spi_wb); |
Само собой, это уже вызовы функций библиотеки. Дальнейшая инициализация:
1 2 3 4 5 6 7 8 |
uint8_t bufSize[] = {2, 2, 2, 2}; wizchip_init(bufSize, bufSize); wiz_NetInfo netInfo = { .mac = {0x00, 0x08, 0xdc, 0xab, 0xcd, 0xef}, // MAC адрес .ip = {192, 168, 77, 6}, // IP адрес .sn = {255, 255, 255, 0}, // маска сети .gw = {192, 168, 77, 1}}; // адрес шлюза wizchip_setnetinfo(&netInfo); wizchip_getnetinfo(&netInfo); |
Обращаем внимание на то, что прямо здесь идут сетевые настройки модуля. В результате в структуре netInfo будет храниться вся сетевая информация, которую вы можете вывести на экран вашего компа через модуль UART-USB сразу после инициализации.
Ну а теперь можно вызывать нашу функцию tcp_server(), которая будет делать всю полезную работу. Функцию разместим в файле Src/tcp.c. Не забудем объявить ее в заголовке main.h:
1 2 |
/* USER CODE BEGIN Private defines */ void tcp_server(); |
Так уж и быть, выложу полное содержание tcp.c. Как все это работает, описал в комментах. Для тех, кто знаком с логикой создания входящих tcp соединений, трудностей не будет никаких.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// Copyright nazim.ru // BeerWare License #include "main.h" #include "stm32f1xx_hal.h" // внимание: это никакой не системный файл, а входит в состав библиотеки W5500 #include "socket.h" #include <string.h> char msg[60]; // это будем посылать tcp клиенту, когда он к нам приконнектится const char MSG[] = "Hello World"; // простой tcp сервер. Данные не принимаем, только посылаем строчку Hello World. // вы можете самостоятельно дополнить функцию для получения данных от клиента void tcp_server() { uint8_t retVal, sockStatus; // а вот это как раз моя функция вывода сообщений через модуль UART-USB // если вместо -1 поставить число, она и его отобразит. // в недрах этой функции работающая строка будет // HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 0xFFFF); trace(-1,"Try open socket\r\n"); // открываем сокет 0 как TCP_SOCKET, порт 5000 */ if((retVal = socket(0, Sn_MR_TCP, 5000, 0)) != 0) { trace(-1, "Error open socket\r\n"); return; } trace(-1,"Socket opened, try listen\r\n"); // устанавливаем сокет в режим LISTEN. Так будет создан tcp сервер if((retVal = listen(0)) != SOCK_OK) { trace(-1, "Error listen socket\r\n"); return; } trace(-1,"Socked listened, wait for input connection\r\n"); // ждем входящих соединений. здесь мы немножко крутимся в бесконечном цикле // и чтобы не заскучать одновременно мигаем светодиодом while((sockStatus = getSn_SR(0)) == SOCK_LISTEN) { HAL_Delay(200); HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); } // раз мы попали сюда, значит выскочили из цикла. входящее соединение! trace(-1,"Input connection\r\n"); if((sockStatus = getSn_SR(0)) != SOCK_ESTABLISHED) { trace(-1, "Error socket status\r\n"); return; } // из сокета вытаскиваем информацию: кто к нам пришел, откуда // можете также отобразить инфу в трассировке uint8_t remoteIP[4]; uint16_t remotePort; getsockopt(0, SO_DESTIP, remoteIP); getsockopt(0, SO_DESTPORT, (uint8_t*)&remotePort); // посылаем клиенту приветствие и закрываем сокет if((retVal = send(0, (uint8_t*)MSG, strlen(MSG))) == (int16_t)strlen(MSG)) // нехорошо так писать код. TODO: добавить фигурные скобки даже для одной строчки ) trace(-1, "Msg sent\r\n"); else trace(-1, "Error socket send\r\n"); // закрываемся. когда нас снова вызовут, мы всегда готовы кработе disconnect(0); close(0); } |
Проверяем все это так. Соединяем патчкордом модуль W5500 и наш комп. Конфигурируем проводное соединение, у меня на PC Ubuntu это адрес 192.168.77.5 (заметьте, что это также адрес шлюза для W5500). С нашей машины пингуем модуль: ping 192.168.77.6, убеждаемся что пинги проходят.
Дальше, достаем швейцарский нож хакера — программу netcat, коннектимся к модулю:
1 |
nc -vv 192.168.77.6 5000 |
и получаем в ответ долгожданное «Hello World», а в экране minicom наблюдаем логи, которые посылает нам trace через модуль UART-USB.
Некоторые размышления по структуре программы. В ожидании входящих соединений, МК видимо будет выполнять полезную работу. Например обрабатывать поток радиолокационных данных ) Возникает проблема одновременной поддержки двух процессов. В Linux среде это решается просто — системным вызовом select(), который может зависать на нескольких событиях. В STM32 Линукса нет, но зато для STM32 есть простая операционная система реального времени FreeRtos. И она умеет работать с потоками, то есть содержит простейший планировщик заданий. Даю наводку, а как вы ей распорядитесь — ваше дело )
Про прошивку МК и отладку я подробно написал в предыдущей статье. Поэтому здесь описание этих процессов опущено.
Спасибо за материал! Долго не мог найти простой пример первого шага с STM32 и W5500.
PS: также любим обычный текстовый редактор + make.
Пожалуйста )
Да, стремился делать такие статьи по принципу — какой инфы собранной в одном месте не хватало для начала. Детали можно найти в инете уже потом
Это наверное самое короткое и почти подробное описание старта W5500 на STM32. Настоящий Hello World! Спасибо.
Спасибо за отзыв!
Интерестная стаття по поводу мультипоточности freeRTOS с использованием очереди сообщений и мютексов:
http://easyelectronics.ru/freertos-primer.html
Мне кажется, что в строке
.gw = {192, 168, 1, 77}}; // адрес шлюза
перепутаны местами два последних байта
Да, точно.
Поправил, спасибо )
так и не понял,где вызывается функция tcp_server
она коллбек на что-то, тогда на что?
или она отрабатывает один раз и тогда её вызвать один раз в main?
не понятно
Функция tcp_server() вызывается один раз, после чего в теле функции работает бесконечный цикл while в котором ведется прослушивание входного сокета. При входящем соединении оно обрабатывается, после чего сокет закрывается.
Чтобы продолжить прослушивание сокета, функцию нужно вызывать еще раз и так далее.
Модель соединений простая и использована в демонстрационных целях. Но работает )
Все на пальцах объяснил с нуля.
Осталось добавить обработку прерывания наличия tcp соединения(w5500 такое вроди бы выставляет).
Чтобы не крутится в бесконечных циклах и все будет готово.
Нужно организовать минимальный обмен данными между контроллером и ПК.
Заказал W5500 буду стартовать по этому руководству.
Спасибо тебе мил человек.
Да, конечно, это только каркас. Далее лучше наладить обработку по прерыванию.
Успехов! )
Огромное спасибо , начал осваивать IDE Keil+cube . Хотелось бы увидеть WiFi модуль и простенький пример терминала с ним.
Да, был один небольшой проект с WiFi модулем ESP8266. Надо бы написать статью