В современном эмбеде от программы зависит даже больше, чем от железа. Хорошо написанная программа может вообще не зависать и ни разу не вызвать срабатывание вотчдога (но это совсем не повод его не использовать!).

Хорошее подспорье при написании правильных программ — стандарт MISRA C. Однако нужно понимать, что он — всего лишь свод правил непосредственно кодинга. Он не подскажет вам, какой алгоритм лучше использовать и как его реализовать.

Я же хочу привести список неких good practice для эмбед-программиста, следование которым лишит вас некоторой головной боли при разработке алгоритмов.

Контрольные суммы

Все критически важные куски данных должны сопровождаться контрольной суммой. Причём желательно использовать такие суммы, которые:

  1. не обращаются в ноль на входном потоке с нулевыми байтами, т.е. 0 не является особым значением и не вырождает эту сумму в 0.
  2. не дают одинаковых значений для «данные» и «данные + 0x00″, т.е. чувствительны к длине данных.
  3. не дают одинаковых значений для «0x01 0x00″ и «0x00 0x01″, т.е. чувствительны к перестановке байт.

Обычные «суммы» — такие как простая сумма, или последовательный xor всех байт, к сожалению, не обладают ни одним из этих свойств. Поэтому применять их нельзя, несмотря на всю простоту их вычисления. Собственно, потому и нельзя, потому что они слишком просты :)

Однако, давно существуют алгоритмы, удовлетворяющие всем этим требованиям — например, семейство CRC-функций. Математические институты нашли среди них очень хорошие функции, но в принципе использовать можно любую. Единственная их проблема (вырождение в ноль) решается ненулевым начальным значением (например, 0xFFFF…).

К тому же, есть хороший способ здорово усложнить жизнь злоумышленнику, решившему вскрыть вашу CRC — какое-то особое нетривиальное начальное значение, не 0 и не 0xFFFF — а ваше собственное.

Дополнительное пожелание — как можно больший диапазон возможных значений суммы. Если сумма имеет длину 1 байт, это значит что есть всего 256 различных значений суммы, и вероятность совпадения сумм у разных кусков данных равна 0,4%. Это  довольно много, поэтому лучше увеличить количество различных сумм, увеличив её длину например до 2 байт. Это — CRC-16, и вероятность снижается до 0,002% — уже вполне приемлемо. Есть ещё и CRC-32, но думаю что можно ограничиться и 2 байтами.

Конечно, избыточное кодирование и коды с коррекцией ошибок — ещё более классная штука, но это уже следующий шаг развития.

Конечные автоматы

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

Применим научный подход. Хороший способ анализа таких заданий — разложение всей совокупности на отдельные «атомарные» состояния. К примеру, «нажали кнопку», «едем вниз», «сработал датчик этажа» и так далее. Также нужно найти все связи между этими состояниями.

Теперь переходим к синтезу алгоритма. Самый хороший способ — применение конечных автоматов. Они дадут вам возможность буквально напрямую перенести список состояний и связей в программу. Получится что-то похожее на блок-схему.

Кстати, стек TCP/IP в большинстве случаев реализуется на конечных автоматах.

Время ожидания

Не полагайтесь всецело на то, что вам ответят на запрос. Если внешняя система занята, не отвечает или вообще отвалилась — это не должно приводить к зависанию вашей программы. Любые запросы с ожиданием ответа делайте либо совсем неблокирующими, либо ставьте на них таймаут с помощью таймера. Под блокированием я имею в виду не настоящее блокирование процессора пустым циклом (понятно, что так делать нельзя ни при каких обстоятельствах), а «зависание» вашего конечного автомата или другого алгоритма в определенном состоянии, из которого уже нельзя вылезти.

Пример из моего недавнего опыта: периодически по таймеру отправляю запрос, и останавливаю таймер на время ожидания ответа. Адресат любит подумать, и может не сразу ответить — поэтому я не хотел зафлудить размышляющего адресата запросами (а то потом мне же разбираться с пришедшей ахинеей). После получения ответа снова запускаю таймер.

Если же адресат случайно не ответит на один из запросов (даже не из-за зависания, а из-за какого-то случайного сбоя связи) — таймер никогда не запустится снова. Исправил программу так: не останавливаю таймер, а настраиваю его на некое время ожидания ответа. Если ответа за это время нет — возвращаюсь в состояние IDLE.

Event-driven

Хороший стиль заключается в том, чтобы следовать методологии event-driven, то есть выполнять какие-то действия как реакцию на события. Это даст возможность не проверять постоянно одно и тоже или регулярно запрашивать неизменные значения — а высвободить время на действительно полезные дела. Или на сон, что тоже неплохо. Значительно помогут вам в этом прерывания, в том числе от таймеров.

Не принимать мусор

Интересная идея кроется в общении «запрос»-«ответ». Если адресат не может сам инициировать общение — зачем нам вообще слушать канал связи в промежутках между сеансами? Захотел чего-то спросить — включил связь, спросил, получил ответ, выключил связь. Включение UART-контролера — дело 1 миллисекунды, а возможность не получать всякий незапрошенный мусор и не думать, что с ним сделать — бесценна :)

Используйте RTOS

Очень полезным подспорьем становится использование разнообразных RTOS. Благодаря им вы не думаете о синхронизации асинхронных процессов, а просто пишете код. Я написал гайд по установке FreeRTOS на STM32F100 — использование FreeRTOS здорово помогло мне в том числе при разработке проекта с Ethernet.

 

Ну и чтобы закончить на весёлой ноте: помните, что одно из свойств машины Тьюринга — её потенциальная способность к зависанию :)