На пути к Ultimate Data Exchange Framework. Часть 1 — Введение

Рано или поздно, начинающий программист микроконтроллеров сталкивается с задачей организовать управление девайсом через управляющую программу на ПК. Самым простым и надежным решением является использование интерфейса RS232: в любом микроконтроллере есть хотя бы один UART, добавьте к нему какой-нибудь MAX232 с щепоткой кондеров и вуаля, обмен готов! Либо, если нет в компе аппаратного RS232 интерфейса, на помощь приходит USB. Ставим какой-нибудь мост USB<->UART (например от FTDI FT232RL) и получаем все то же самое, только по USB. И девайс современней выглядит, и в системе отображается как старый добрый COM-порт, и готовые библиотеки в Visual Studio есть. Осталось только набросать какой-нибудь простенький протокол обмена, и вперед! Однако, с протоколом может быть не все так просто, как кажется на первый взгляд. В этой части мы составим небольшое ТЗ на устройство, рассмотрим его функционал, и рассмотрим один из вариантов протокола обмена данными, который я использую в своей практике.

ТЗ и концепт протокола

Для большей конкретики набросаем ТЗ нашего концептуального устройства.

Состав девайса: один датчик температуры, две кнопки, две лампочки, один мотор.

Как это должно работать. Раз в 30 секунд девайс отсылает в ПК текущую температуру. Если юзер нажал на одну из кнопок, то отослать это событие в ПК. Организовать возможность включать и выключать с компа лампочки и управлять мотором.

Логика работы с лампочками следующая: пользователь из управляющей программы на ПК отсылает команду вида «включить N-ю лампочку». После этого, девайс должен включить указанную лампочку и вернуть в ПК некий ответ, который подтверждает успешное выполнение операции. То же самое для выключения лампочки.

Логика работы мотора следующая: пользователь из управляющей программы на ПК отсылает команду вида «включить мотор на XXX секунд». После этого, мотор должен включиться на XXX секунд, и после выключения должно вернуться в ПК некое подтверждение завершения операции.

Дополнительное требование: при выполнении относительно долгой операции с мотором, контроллер должен продолжать принимать команды на включение лампочек, отсылать события нажатия на кнопки и текущую температуру. При получении неверной команды, девайс не должен ломаться ( 😀 ) и должен вернуть ответ, который говорит об ошибочной команде.

Вот такой некислый функционал у нашего девайса.

Посмотрим еще раз на ТЗ. Наш девайс логически разбит на 4 функциональных блока:

  1. термометр
  2. кнопки
  3. лампочки
  4. мотор

Термометр и кнопки только передают данные о своем состоянии в ПК, лампочки и мотор получают некие данные и отправляют ответ.

Так как выполнение длительной операции (ожидание остановки мотора) не должно блокировать другие запросы, нам надо как-то различать данные, которые пришли от мотора, лампочки, кнопки и термометра. Намечается примерно такой формат команды и ответа:

mod_id data

mod_id — идентификатор блока: 1-термометр, 2-кнопки, 3-лампочки, 4-мотор;

data — полезные данные, в которых содержится запрос к данному блоку, либо ответ от блока.

Обмен данными у нас получается пакетный, и в начале пакета у нас идет значение mod_id, с помощью которого мы сможем различать от кого пришел ответ.

Перейдем к обработке ошибок. Ошибки могут быть 2х типов:

  1. неверный аргумент запроса (в области data)
  2. неверный mod_id

Рассмотрим первый тип ошибок. Для того, чтобы была возможность определить коды ошибок запросов, перед областью data ответа девайса добавим один байт кода ошибки (resp). Итого, получаем следующий протокол обмена:

  • Запрос (от ПК в девайс): mod_id data
  • Ответ (из девайса в ПК): mod_id resp data

resp — код ответа: 0x00 — ошибок нет, 0x01 — ошибка запроса

Итак, с первым типом ошибок разобрались. Перейдет к 2-му. В данном случае у нас mod_id могут принимать значения от 1 до 4. Если mod_id неверный, то давайте сделаем вот так:

  • Запрос (от ПК в девайс): mod_id data
  • Ответ (из девайса в ПК): 0x00 0x01

Т.е. при неправильном значении mod_id у нас приходит ответ как-бы от модуля с mod_id = 0x00, и в качестве кода ошибки он присылает значение 0x01, а область ответа data отсутствует. Такое решение выглядит довольно органично.

Вернемся немного назад к ТЗ. Некоторые блоки девайса (термометр и кнопки) могут присылать какие-либо данные самопроизвольно, без запроса от ПК. Однако, наша концепция пока не позволяет делать такие вещи: ответ может быть только на запрос. Значит давайте добавим еще один тип пакета, который идет от девайса в ПК, который назовем «Событие». Мы будем топить за универсальность, и поэтому предположим, что один и тот же блок с одним и тем же mod_id может обрабатывать как и обычные запросы, так и генерировать какие-либо события. Вопрос: как отличать пакет типа «Ответ» от пакета «Событие»? А давайте это делать с помощью resp:

  • Событие (из девайса в ПК): mod_id 0xFF data

По внешнему виду пакет похож на «Ответ», однако resp в нем равен 0xFF. И теперь мы сможем отличить такой пакет от ответа на запрос.

Ну и можно добавить еще одну вещь. Пусть область data в «Запросе», «Ответе», и «Событии» будет необязательной.

Подводим промежуточный итог. Протокол обмена между ПК и электронным блоком может работать поверх любого асинхронного дуплексного (если немного напрячься, то можно и по half-duplex) протокола передачи данных, поддерживает параллельные запросы к одному электронному блоку, работает в режиме «Запрос-ответ» и «Событие».

Формат режима «Запрос-ответ»:

  • Запрос: mod_id, [data]
  • Ответ: mod_id, resp, [data]

Формат режима «Событие»:

  • Событие: mod_id, 0xFF, [data]

где:

  • mod_id — id программного модуля электронного блока
  • resp — код ответа: 0x00 — ошибок нет, 0x01 — ошибка запроса, 0xFF — пакет «Событие»
  • data — необязательная область, содержащая полезные данные

При запросе с неправильным mod_id ответ придет от mod_id=0x00, c resp = 0x01.

Размышления насчет параллельных запросов. Если отойти на расстояние 5и метров, и очень сильно прищуриться, то можно увидеть очень странный протокол TCP/IP с двумя компами, без IP-адресов, и в качестве TCP-порта используется mod_id. В принципе, вдохновение было черпано как раз отсюда.

Команды модулей

Лампочки

Как несложно догадаться, команды самих модулей будут скрываться в области data запроса. Рассмотрим модуль лампочек. По функционалу мы должны уметь включать, либо выключать N-ю лампочку. В этом случае, пусть 0й байт в data будет говорить о том, что нужно сделать (1-включить, 0-выключить), а 1й байт будет указывать номер лампочки (1-первая, 2-вторая). В этом случае запрос будет выглядеть следующим образом:

  • Запрос: 0x03, cmd, lamp
  • Ответ: 0x03, 0x00

где cmd-действие (1-включить, 0-выключить), lamp-номер лампочки.

Пример. Включаем 2ю лампочку:

  • Запрос: 0x03, 0x01, 0x02
  • Ответ: 0x03, 0x00

Идея, думаю, ясна.

 

Мотор

Перейдем к мотору. Из ТЗ следует, что нужно сделать команду, которая на вход принимает время в секундах, в течение которого мотор находится во включенном состоянии. После выключения мотора будет отправлять ответ на запрос. Для простоты установим диапазон времени включения мотора от 0 до 255 секунд. В этом случае запрос будет иметь следующий вид:

  • Запрос: 0x04, motor, time
  • Ответ (через time секунд): 0x04, 0x00

где motor — номер мотора, в нашем случае может принимать только значение 1, time — время в секундах.

В запрос добавил параметр motor на всякий случай, так как говорится, аппетит приходит во время еды, и заказчик может захотеть добавить еще моторов.

 

Термометр

Этот блок (или модуль) не принимает ничего на вход, а только самопроизвольно каждые 30 секунд генерирует событие, в котором содержится текущая температура. Пусть температура у нас будет типа int16_t temper, причем это число с фиксированной точкой, и 4 младших бита отданы под значение после запятой. Именно в таком формате выдает температуру самый распространенный цифровой термодатчик DS18B20. Давайте немного усложним задачу, и добавим 2 типа события. 1й тип отсылается в случае успешного чтения температуры. 2й тип в случае ошибки чтения (термодатчик не подключен либо неисправен). События будут иметь следующий формат:

  • Событие Датчик подключен, текущая температура: 0x01, 0xFF, 0x00 t_0, t_1

где t_0 — младший байт temper, t_1 — старший байт temper

  • Событие Датчик не подключен: 0x01, 0xFF, 0x01

Чем отличаются события при подключенном и неподключенном датчике, думаю, видно и без пояснений 🙂

 

Кнопки

Тут все просто. У нас 2 кнопки. При нажатии на одну из них будет возникать следующее событие:

  • Событие: 0x02, 0xFF, Button

где Button — номер кнопки, 1 или 2.

 

Заключение

Итак, на этом пока все. Может показаться, что для данного функционала девайса слишком много заморочек. Может это и так, однако, если будут готовы все необходимые библиотеки как для микроконтроллера, так и для ПК, данный подход позволяет реализовывать довольно сложные вещи без особого напряга, и что самое важное, легко добавлять новый функционал в существующий проект. Кроме того, библиотеки данного протокола для МК показали себя с лучшей стороны при переносе проекта с одного микроконтроллера на другой, единственное, что приходилось менять, так это настройки ножек, к которым подключен UART.

Немного спойлеров. До этого я говорил, что кому-то может показаться данный функционал простым. Но кому-то может показаться совершенно противоположное, и в голове могут возникнуть мысли типа «как? что? почему? как это сделать? все сложно(((«. Все эти вещи очень легко реализуются с использованием конечных автоматов. Вещь супер, как только разобрался с ними несколько лет назад, так и до сих пор пишу все прошивки для МК на конечных автоматах. Тут тебе и многозадачность, и ни какая RTOS не нужна, и отлаживать их легко, все перед глазами! Поэтому неподготовленных читателей отправляю штудировать следующие статьи: Применение Switch-технологии при разработке прикладного программного обеспечения для микроконтроллеров. Часть 1. Там целый цикл статей. Наперед скажу, что оттуда взял только концепт самих конечных автоматов на  SWITCH-технологии, и программные таймеры. Mutex-ы и событийную системы как-то ни разу еще не использовал, но вещи тоже классные.

На этом пока все, ждите продолжения!

Литература

[1] Татарчевский Владимир: «Применение Switch-технологии при разработке прикладного программного обеспечения для микроконтроллеров. Часть 1» // Компоненты и технологии, №2’2007

Закладка Постоянная ссылка.

Добавить комментарий