37. Приложения IoT
Прибор, который отправляет показания на сервер, — самая частая современная задача, и у неё есть два уровня: как подключиться к сети и что по ней передавать. Раздел о том, какие способы подключения существуют для F103 и F407, чем они отличаются в цене и сложности, и почему протокол обмена с сервером почти всегда MQTT.
Как это называется в документации
Оригинал Перевод network stack / TCP/IP stack сетевой стек AT command команда модулю в текстовом виде hardwired TCP/IP стек, реализованный в контроллере аппаратно MAC / PHY контроллер канального уровня / физический приёмопередатчик socket сокет, точка соединения broker брокер, сервер-посредник MQTT topic топик, имя канала публикации publish / subscribe опубликовать / подписаться QoS (quality of service) уровень гарантии доставки keep-alive интервал контроля соединения retained message сохранённое сообщение last will «завещание» — сообщение при обрыве remaining length длина остатка пакета
37.1 Три способа подключиться
У F103 сетевого контроллера нет вовсе. У F407 есть контроллер Ethernet, но ему нужны внешняя микросхема физического уровня и стек TCP/IP в прошивке. Отсюда три пути.
| Модуль по UART | Модуль по SPI | Встроенный Ethernet | |
|---|---|---|---|
| камень | любой | любой | только F407, F107 |
| интерфейс | UART, 2 провода | SPI, 4 провода + INT |
RMII, 9 проводов + PHY |
| стек TCP/IP | в модуле | в модуле, аппаратно | в вашей прошивке |
| память на STM32 | буферы, ~1 КБ | буферы, ~1 КБ | десятки КБ |
| скорость | до ~1 Мбит/с реально | до ~10 Мбит/с | 100 Мбит/с |
| Wi‑Fi | да | обычно провод | нет |
| сложность | разбор строк | регистры и сокеты | полноценный стек |
| «зависает и не отвечает» | часто | редко | ваша ответственность |
⚠️ Заголовок не скажет, есть ли блок. Файл
stm32f10x.hедин на всё семейство F1 и объявляетETH_BASE,ETH_TypeDefи указательETHдля всех, включая F103, у которого контроллера нет. ОбращениеETH->MACCR = …на F103 соберётся без предупреждения и даст BusFault по несуществующему адресу (раздел 35). Под условиемSTM32F10X_CLтам только векторETH_IRQnи два регистра RCC. У F4 заголовки раздельные по камню:stm32f407xx.hобъявляет Ethernet,stm32f401xe.h— нет. Наличие блока проверяют по даташиту, а не по тому, что компилируется.
Модуль по UART: AT-команды
Самый дешёвый и самый частый путь — Wi‑Fi модуль, который принимает текстовые команды и отвечает текстом. Обычно это ESP8266 или ESP32 с прошивкой AT-команд — команды ниже из её набора:
AT+CWJAP="имя_сети","пароль" → OK (секунды)
AT+CIPSTART="TCP","192.168.1.10",1883
AT+CIPSEND=14 → > (ждёт данные)
<14 байт> → SEND OK
Просто на вид и коварно в реализации. Три вещи, на которых спотыкаются.
Ответ приходит потоком, а не сразу. На 115 200 бод один байт —
87 мкс, строка в 30 байт — почти 3 мс. Ждать в цикле нельзя: приём
через потоковый буфер (раздел 32) в задачу-разборщик, которая собирает
строки и распознаёт OK и ERROR.
Таймауты разного порядка. Простая команда отвечает за десятки миллисекунд, подключение к сети — за секунды, до пятнадцати. Единый таймаут «на всё» либо мал для подключения, либо велик для всего остального. У каждой команды свой.
Модуль — отдельный компьютер со своими сбоями. Он может зависнуть, перезагрузиться, потерять сеть, выдать незапрошенное сообщение посреди вашего ответа. Программа обязана переживать всё это: сторожевой таймер связи (раздел 29), сброс модуля по выводу, повторное подключение по расписанию.
⚠️ Незапрошенные сообщения. Модуль присылает
WIFI DISCONNECT,+IPD,n:с входящими данными и подобное в любой момент — в том числе между вашей командой и ответом на неё. Разборщик, который ждёт строго «команда → ответ», сломается на первом же. Разбирать нужно каждую строку по её началу, а не по ожиданию.
Модуль по SPI: стек в кремнии
Микросхема с контроллером Ethernet и стеком TCP/IP внутри, типовая — W5500: наружу отдаёт восемь сокетов через регистры по SPI. Ни строк, ни разбора — записал адрес и порт, выставил бит «открыть», проверил статус, положил байты в буфер отправки.
| Плюс | Минус |
|---|---|
| детерминировано: регистры, а не текст | только провод, без Wi‑Fi |
| не зависает так, как модули с прошивкой | нужен разъём и кабель |
| 32 КБ буферов в самой микросхеме | пропускная ограничена SPI |
| прерывание по событию | сокетов восемь и не больше |
При 10 МГц SPI — 1,25 МБ/с сырых, реально вдвое меньше: для датчиков избыток, для видео мало. Из 32 КБ буферов 16 — на передачу и 16 — на приём; при равном делении каждому сокету достаётся 2 КБ на передачу и 2 КБ на приём.
Правильный выбор для стационарного прибора с проводом: ничего не зависает, ничего не разбирается, вся сложность в кремнии.
Бывают и модули Wi‑Fi по SPI со стеком внутри, но типовой модуль по SPI — проводной. И есть четвёртый путь, между вторым и третьим: микросхема Ethernet без стека, например ENC28J60. В ней по SPI доступны только контроллер канального уровня и физический уровень, 10 Мбит/с, а стек TCP/IP — в прошивке, как у F407, только на любом камне. На F103C8 помещается лишь урезанный стек (37.3).
Встроенный Ethernet на F407
Контроллер канального уровня на кристалле, физический уровень — внешней микросхемой по девяти проводам RMII, стек TCP/IP — в прошивке. Обычно это lwIP, и он весит десятки килобайт оперативной памяти — больше, чем есть у F103C8 целиком.
| Байт | |
|---|---|
| буферы приёма и передачи с дескрипторами | 8–16 КБ |
| стек и его память под соединения | 16–24 КБ |
| задача стека | стек 1–2 КБ |
Зато 100 Мбит/с, любое число соединений, полный контроль. И полная ответственность: стек в вашей прошивке, значит ваша задача его конфигурировать, обслуживать и отлаживать. Путь для приборов, где сеть — основная функция, а не довесок.
Под ядром стек живёт в отдельной задаче, а прикладной код работает через сокеты с блокирующими вызовами — как на компьютере. Без ядра стек тоже работает, но в режиме опроса, и это заметно неудобнее.
37.2 Что передавать: почему MQTT
Подключились. Дальше нужен протокол, по которому прибор говорит с сервером. Кандидатов два.
| HTTP | MQTT | |
|---|---|---|
| модель | запрос — ответ | публикация — подписка через брокера |
| соединение | обычно новое на запрос; HTTP/1.1 умеет держать открытым | одно, постоянное |
| служебных байт на сообщение | сотни | около десятка |
| сервер → прибор | только по запросу прибора | в любой момент |
| несколько получателей | нет | да, по подписке |
| обрыв связи | не замечает | брокер видит, шлёт «завещание» |
| разбор на приборе | текст, заголовки | двоичный, десяток байт |
MQTT выбирают за первые три строки: постоянное соединение, минимум байт и возможность серверу говорить первым. Для прибора на батарейке главное из них — не устанавливать соединение заново: рукопожатие TCP — три пакета, а TLS добавляет ещё несколько обменов и килобайты сертификатов. Заголовки TCP/IP — 40 байт на пакет — платятся в обоих протоколах одинаково, и на их фоне сотня служебных байт против десятка заметна, но не решает.
Устройство пакета
Всего два поля в заголовке: тип с флагами в одном байте и длина остатка.
байт 0: тип << 4 | флаги
байт 1…4: длина остатка, по 7 бит на байт, старший бит — «есть ещё»
далее: остаток — зависит от типа
| Тип | Байт | Назначение |
|---|---|---|
CONNECT |
0x10 |
представиться брокеру |
CONNACK |
0x20 |
брокер принял |
PUBLISH |
0x30 + флаги |
опубликовать в топик |
PUBACK |
0x40 |
подтверждение при QoS1 |
SUBSCRIBE |
0x82 |
подписаться; флаг 2 обязателен |
SUBACK |
0x90 |
подписка принята |
PINGREQ |
0xC0 |
«я жив» |
PINGRESP |
0xD0 |
«и я» |
DISCONNECT |
0xE0 |
отключаюсь |
Флаги PUBLISH: бит 0 — сохранить на брокере, биты 1–2 — QoS,
бит 3 — повтор.
Длина остатка кодируется переменным числом байт: до 127 — один, до 16 383 — два, до 2 097 151 — три, до 268 435 455 — четыре; больше четырёх не бывает. Для приборных сообщений почти всегда один.
Пример целиком — публикация «21.5» в топик «home/t»:
30 PUBLISH, QoS0
0C длина остатка: 12
00 06 h o m e / t топик: длина 6, шесть байт
2 1 . 5 тело: 4 байта, без длины — до конца пакета
Четырнадцать байт, из них десять служебных. Тот же HTTP-запрос — двести с лишним.
⚠️ Топик и тело — не строки. Ни в топике, ни в теле нет завершающего нуля: топик предваряется длиной в двух байтах, тело занимает всё до конца пакета.
strlenна теле — мусор при первом двоичном байте. Длину берут из заголовка, а не ищут ноль.
QoS: три уровня и два полезных
| QoS | Гарантия | Цена |
|---|---|---|
| 0 | «отправил и забыл» | ничего |
| 1 | «дойдёт хотя бы раз» | +2 байта идентификатора, ожидание PUBACK, возможен дубль |
| 2 | «дойдёт ровно раз» | четыре пакета на сообщение |
Для показаний датчика — QoS0: пропало одно, придёт следующее. Для команд прибору — QoS1 и готовность к дублю. QoS2 на микроконтроллере почти не применяют: цена не окупается.
Keep-alive: обязательство, а не опция
В CONNECT прибор называет интервал в секундах. Брокер ждёт полтора
интервала молчания и рвёт соединение. При 60 секундах — 90 секунд
тишины, и прибор отключён без предупреждения.
Значит, прибор обязан слать PINGREQ чаще интервала — с запасом,
раз в 45 секунд при интервале 60. Программный таймер (раздел 29)
с перезапуском при каждой отправке: если что-то ушло, пинг не нужен;
замолчали — пинг сработает сам.
Ответ ждут и в обратную сторону. Если на PINGREQ за несколько секунд
не пришёл PINGRESP, соединение считают потерянным: закрывают сокет
и переходят к переподключению. Сам TCP заметит обрыв нескоро — в разделе
106.6 до отказа по повторам проходит 924,6 с, — и всё это время прибор
копит показания для брокера, которого уже нет.
⚠️ Обрыв связи брокер замечает по keep-alive, а не по TCP. Радио пропало, TCP-соединение висит — брокер узнает об этом только через полтора интервала. Тогда он публикует «завещание» — сообщение, которое прибор оставил в
CONNECTна случай обрыва. Так сервер узнаёт, что прибор пропал, не опрашивая его.
Библиотека или своими руками
Разбирать и собирать пакеты самому — работа на вечер: формат
из десятка байт. Готовые клиенты для микроконтроллеров есть, и они
берут на себя keep-alive, QoS1 и переподключение. Брать стоит готовый,
но понимать формат — обязательно: отладка по байтам в канале, когда
«брокер отвечает CONNACK с кодом 5», без понимания невозможна.
37.3 Схема прибора
Типовая структура под ядром, с модулем по UART:
| Задача | Приоритет | Что делает |
|---|---|---|
| приём UART → потоковый буфер | прерывание | байты в буфер |
| разборщик модуля | 3 | строки, OK/ERROR, незапрошенные сообщения |
| сетевой автомат | 2 | подключение, переподключение, keep-alive, отправка |
| датчики | 2 | измерения → очередь |
| основная логика | 1 | что показывать, что слать |
Сетевой автомат — конечный автомат с состояниями «нет сети», «подключаюсь к точке», «подключаюсь к брокеру», «работаю», «жду перед повтором». Каждое состояние — со своим таймаутом; каждый обрыв возвращает в начало с задержкой, растущей по экспоненте: секунда, две, четыре, до минуты, — причём внутри этого предела задержку выбирают случайно (раздел 88.6). Без растущей задержки прибор, потерявший сервер, будет долбить его каждую секунду, а без разброса тысяча приборов, потерявших сервер одновременно, будет стучаться в него тоже одновременно.
Показания от датчиков не идут в сеть напрямую: они ложатся в очередь,
сетевой автомат забирает их, когда соединение живо. Пока сети нет,
очередь копит; переполнилась — теряем старые, не новые. Очередь ядра
при переполнении отвергает новое значение, поэтому старые теряют явно: если
xQueueSend с нулевым ожиданием вернул errQUEUE_FULL, вынимают одно
значение xQueueReceive(q, &old, 0) и отправляют новое ещё раз. Писатель
в очередь при этом один — задача датчиков.
Память на F103C8
| Байт | |
|---|---|
| ядро с кучей под задачи | ≈ 6 КБ |
| буферы связи | 768 |
| очередь показаний на 32 записи | ≈ 400 |
| стеки пяти задач | 3–5 КБ |
| итого | ≈ 12 КБ из 20 |
Помещается — с внешним стеком. Полный стек в прошивке, как у F407, сюда не влезет, поэтому Ethernet к F103 обычно подключают модулем со стеком внутри. Урезанный lwIP — одно соединение TCP, DHCP и DNS, режим без ОС — в опыте добавил к прошивке 6,1 КБ ОЗУ и 33 КБ Flash: вместе с таблицей выше это 18 КБ из 20, впритык. Так работает четвёртый путь из 37.1 — микросхема Ethernet без стека.
37.4 Безопасность: то, что нельзя пропустить
Пароль сети и брокера — не в исходниках. Исходники попадают в репозитории. Пароль записывают при производстве в отдельную страницу внутренней флеш-памяти (раздел 11.5), а камень закрывают защитой чтения (раздел 11.6). Внешняя флеш-память (раздел 41) убирает пароль из репозитория, но не защищает: её читают программатором, прицепившись к выводам. Резервные регистры малы — 20 байт у F103C8, 80 у F407, а пароль Wi‑Fi бывает до 63 символов — и стираются без батарейки.
Шифрование — TLS поверх TCP. На модуле по UART его делает модуль командой; на F407 с собственным стеком — библиотека, и она весит ещё десятки килобайт и заметно нагружает процессор при установлении соединения. F103 с TLS в прошивке — нереалистично.
Брокер без пароля и без шифрования — только в лаборатории, никогда в изделии. Открытый брокер с приборами на нём — первое, что находят сканеры.
Обновление прошивки по сети — отдельная тема с загрузчиком, проверкой подписи и защитой от полуобновления при обрыве. Без неё прибор, ушедший в поле, не исправить.
37.5 Грабли
Модуль отвечает ERROR на всё. Не та скорость UART, либо
предыдущая команда не завершена, либо модуль в другом режиме.
Разборщик путается. Незапрошенное сообщение пришло между командой и ответом. Разбирать каждую строку по началу.
Подключение к сети — ERROR через полсекунды. Таймаут слишком
короткий; подключение занимает секунды.
Брокер отключает раз в полторы минуты. Нет PINGREQ чаще keep-alive.
Прибор минутами «на связи» с пропавшим брокером. На PINGREQ не ждут
PINGRESP, а TCP сам сдастся нескоро.
CONNACK с кодом не ноль. 1 — версия протокола, 2 — плохой
идентификатор, 4 — логин, 5 — не авторизован.
Сообщение доходит обрезанным. strlen на теле с двоичным байтом.
Сервер получает дубли. QoS1 работает как задумано; обрабатывать идемпотентно.
После обрыва прибор долбит сервер каждую секунду. Нет растущей задержки между попытками.
Поднятый сервер снова падает под своими же приборами. Все повторяют попытки разом: у задержки нет случайного разброса.
Модуль по UART теряет байты входящих данных. Приём в цикле вместо потокового буфера с прерыванием.
lwIP не помещается. Полный на F103C8 и не поместится: внешний стек или урезанный lwIP на одно соединение (37.3).
Работает в лаборатории, не работает на объекте. Другая сеть, другой брокер, другой брандмауэр, порт 1883 закрыт.
37.6 Итоги
- Три пути: модуль по UART (Wi‑Fi, разбор строк), модуль по SPI (провод, стек в кремнии), встроенный Ethernet (только F407, стек в прошивке); четвёртый — микросхема Ethernet без стека и стек в прошивке на любом камне.
- Ethernet на кристалле есть у F407 и F107, нет у F103 и F401 — но заголовок F1 объявляет его регистры для всех; наличие блока проверяют по даташиту.
- AT-команды: приём через потоковый буфер, таймаут на каждую команду свой, незапрошенные сообщения разбираются по началу строки.
- Полный стек в прошивке весит десятки килобайт — больше памяти F103C8; урезанный lwIP на одно соединение — 6,1 КБ ОЗУ и 33 КБ Flash, впритык.
- MQTT: постоянное соединение, десяток служебных байт против сотен у HTTP, сервер может говорить первым.
- Пакет — тип с флагами плюс длина остатка по 7 бит на байт; топик с длиной впереди, тело без длины и без нуля.
- QoS0 для показаний, QoS1 для команд с готовностью к дублю.
- Keep-alive — обязательство: пинговать чаще интервала, брокер рвёт
через полтора; не пришёл
PINGRESP— переподключаться. - Между попытками подключения — растущая задержка со случайным разбросом.
- Пароли не в исходниках, а во внутренней флеш-памяти под защитой чтения; брокер без шифрования — только в лаборатории.
Дальше: раздел 37а «Сколько стоит TLS на микроконтроллере: Mbed TLS по флешу и ОЗУ» — из чего складывается цена клиента TLS во флеш-памяти, куче и времени.