Подписаться YouTube

Многозадачность в микроконтроллерах Arduino, Wemos, ECP8266

Текст видео

Всем привет. В этом видео я хочу рассказать вам об одном паттерне программирования, который мне бывает очень часто нужен при разработке на платах WeMos и Arduino. Используя его, вы сможете писать сложные программы более легко. Программируя эти микроконтроллеры, я очень часто сталкиваюсь с тем, что нужно организовать свой код так, чтобы разные функции программы выполнялись с разной периодичность и не мешали друг-другу при этом. Т.е. получить некое подобие многозадачности на этих скромных девайсах и при этом сильно не усложнять код. В качестве примера я возьму свою прошлую самоделку с монитором температуры для датчиков умного дома и внесу в код некоторые дополнения. В частности, чтобы сделать эту задачу более интересной, давайте добавим на экран часы, которые будут показывать точное время. Секунды на этих часах должны обновляться с интервалом в одну секунду без задержек. В то же время это устройство должен выполнять и другие функции: опрашивать состояние датчиков температуры, получать информацию о погоде из интернета, получать точное время с серверов точного времени в интернете, обновлять информацию на экране.

В прошлый раз я решил, что информацию с датчиков погоды достаточно обновлять на дисплее раз в полчаса. Поэтому чтобы не усложнять программу я добавил задержку в полчаса в основной цикл программы. Очевидно, что в этот раз она нам не понадобится. Наши часы должны обновлять информацию с частотой в одну секунду ровно, все остальные части программы обновляют информацию реже. Поэтому есть смысл модифицировать главный цикл программы так, чтобы он повторился именно с такой частотой, а все остальные части программы должны быть написаны так, чтобы они успевали сделать свою работу, пока секундная стрелка часов не двигается.

Для начала нужно модифицировать главный цикл программы. Раньше он выглядел вот так, теперь я хочу убрать из него задержку в полчаса и дополнить его другим кодом.

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

В переменных startMillis и endMillis будет храниться значения этой временной метки в начале цикла и в конце. Кажется что для того чтобы посчитать время выполнения нужно из endMillis вычесть startMillis, но это не всегда так. Дело в том, что самое большое беззнаковое целочисленное число которое можно сохранить в нашем микроконтроллере занимает 4 байта. Давайте проверим, какое максимальное значение можно сохранить в переменную типа unsigned long, и что будет при его переполнении.

Объявим константу макс ю л и сохраним в нее максимальное значение unsigned long. Префикс 0x означает, что дальше за ним будет значение байт в шестнадцатеричной кодировке.

Без префикса десятичная система исчисления

В – двоичная система исчисления

0 – восьмеричная система исчисления

(07 в десятичной системе счисления это 7, а 016 это 14) O_O будьте внимательны

0x – шестнадцатеричная система исчисления

Восемь F следом это значение этих байт. По два символа на каждый байт. Т.е. в двоичной кодировке все 4 байта окажутся заполненные единицами. UL в конце означает что эти байты объявлены как тип данных unsigned long.

Если вывести это значение в консоль, то оно выведется в привычной для нас десятичной форме. Таким образом, мы выяснили, что максимальное значение unsigned long это то число, которое вы видите на экране.

Прибавим к этому числу одну тысячу, чтобы байты переполнились и выведем результат в консоль.

Получилось 999. Очевидно, что когда значение переполнилось, то оно вначале стало нулем, а потом к нему добавилось еще 999.

Получается, что наш счетчик времени через 4 миллиарда миллисекунд переполнится и начнет считать сначала. Это произойдет примерно через 49 дней. Я не хочу, чтобы это устройство начало сбоить через 49 дней. Поэтому чтобы убедиться, что эта программа адекватно отреагирует на переполнение счетчика миллисекунд, я предлагаю приблизить эту дату и посмотреть, что будет, когда она настанет. Для этого я написал специальную функцию, которая возвращает временную метку в миллисекундах, которая переполнится через 30 секунд. Везде где нужно получить время в миллисекундах я буду использовать эту функцию. Таким образом, если в программе не будет ошибки через 30 секунд, то и через 49 дней ее не будет то же.

В этом блоке кода я как раз обрабатываю ситуацию, когда в течение одного цикла программы таймер переполнился. При этом время выполнения нужно считать не как разность между временем конца и начала, а так как написано в программе.

В этом блоке кода я добавляю задержку такого размера, чтобы цикл программы длился ровно одну секунду.

Выведу на экран значение миллисекунд, посмотрим, как оно меняется. Немного модифицирую код программы. Добавлю вывод логов и отключу отображение основной информации. Если все работает корректно, то разница между значениями должна быть примерно 1000 и они должны появляться раз в секунду.

Что же, все работает как надо. Погрешность всего несколько миллисекунд. Это вполне приемлемо.

На этом модификация основного цикла программы завершена.

Теперь я добавлю код для получения точного времени из интернета. Для этого придется подключить еще одну библиотеку к нашему проекту. Она называется, так как показано на скриншоте. Время, отображаемое на часах нужно конечно брать, опираясь на внутренний счетчик времени микроконтроллера. Библиотека, которую мы подключили как раз занимается тем, что ведет счет реального времени, опираясь на время прошедшее с начала запуска программы и позволяет вести его счет в привычном для нас формате.

Получение точного времени из интернета в целом работает так. В интернете есть специальные сервера которые ведут очень точный счет времени. К ним можно отправить короткий бинарный запрос длинной в 48 байт на UDP сокет с портом 8888. В ответ они пришлют другие 48 байт с точным временем. Задержки при передаче данных при этом учитываются. Этот протокол называется NTP, т.е. Network Time Protocol. Я не буду сейчас рассматривать его более подробно. Код для получения точного времени из интернета я взял из примеров этой библиотеки.

И так я хочу, чтобы время, отображаемое на часах синхронизировалось по протоколу NTP один раз в 10 минут.

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

Метод timeString() возвращает время в понятном для человека формате. Методы hour(), minute(), second(), day(), month() это как раз и есть то, новое что появилось в нашей программе после подключения библиотеки TimeLib.

Аналогичным образом модифицируем код для получения информации с метеодатчиков. При этом запрос на получение информации я буду отправлять один раз в пять минут, а ответ буду проверять в каждом цикле. 5 минут это 300 секунд, но я выберу значение 301.

Так же модифицируем код для получения информации о погоде из интернета. Я хочу, чтобы он обновлялся один раз в пять минут. Но я выберу значение 307.

Код для восстановления соединения с Wi-Fi и код для обновления информации на экране я модифицировать не буду. Они по-прежнему будут выполняться каждый цикл, т.е. один раз в секунду.

Выведем показания времени вместо названия города на дисплее. Я оставлю его таким же мелким шрифтом. Цель его отображения в том, чтобы продемонстрировать вам как эмитировать многозадачность в микроконтроллерах. А еще оно, может быть, служить индикатором того, что наш микроконтроллер не завис. Для этих целей такого мелкого шрифта достаточно.

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

Ссылки на исходный код, я оставлю в описании под видео.