Привет.
Традиционным уникальным преимуществом платформы Arduino называлось (да и сейчас иногда называется, хотя это уже неверно — и мы поговорим, почему) опускание порога входа в микроконтроллерную разработку до уровня базовых знаний C/C++ и электроники в маштабе «подключить светодиод в нужной полярности».
Спросите примерно у любого активного сторонника Arduino — и вам быстро объяснят, что можно, конечно, писать под STM32 или nRF52, но выгоды в том реальной никакой, зато вас ждут бессонные ночи над сотнями страниц даташитов и бесконечные простыни функций с длинными непонятными названиями.
Заслуги Arduino в снижении порога вхождения действительно трудно переоценить — эта платформа появилась на свет в середине нулевых годов, а после 2010 завоевала серьёзную популярность среди любителей. Особых альтернатив на тот момент ей не было — процессоры на ядрах Cortex-M только появились, по сравнению с AVR они были довольно сложны даже для профессиональных разработчиков, а отладочные платы у большинства вендоров стоили от сотни долларов и выше (и в общем в индустрии ценник за отладку на 5-долларовом контроллере в $500 никого сильно не удивлял).
Однако большая проблема Arduino в том, что её развитие за минувшие 10+ лет более всего напоминает некоторые модели АвтоВАЗа:
Так как дальше я планирую длинное вступление, то сейчас, чтобы вы представляли, в чём будет заключаться практическая часть, я приведу полный текст программы, включающий инициализацию процессора STM32 и мигание светодиодом. Программа написана для ОС ARM Mbed:
#include "mbed.h"
DigitalOut myled(LED1);
int main() {
while(1) {
myled = 1; // LED is ON
wait(0.2); // 200 ms
myled = 0; // LED is OFF
wait(1.0); // 1 sec
}
}
Похоже ли это на высокий входной порог? На функции с непонятными названиями? Бессонные ночи над даташитами? Нет? Ладно, давайте не будем забегать вперёд.
Дело в том, что в мире встраиваемой разработки с 2010 года произошло… многое. AVR, как и вообще 8-битные контроллеры, практически умерли — на 2017 год суммарная доля последних в разработках составляла 12 % (данные опроса разработчиков Aspencore), причём она делилась как минимум на три семейства: AVR, PIC и STM8. Фактически, основное их применение сейчас — замена мелкой логики, периферийные контроллеры с минимальным объёмом мозгов и т.п.
Для серии ATMega в этих 12 % места совсем мало — они избыточны в качестве вспомогательных и не могут конкурировать с 32-битными Cortex-M по соотношению цена/характеристики (STM32F030 с 16-64 КБ флэша и 4-8 КБ ОЗУ стоит в России мелким оптом 30-50 рублей). По сути, атмеги в каких-то проектах остались только по историческим причинам.
64-битные процессоры — это, очевидно, старшие Cortex-A и интелы в тяжёлых проектах
Случилось много нового и в средах разработки. Помните, я выше писал, что на старте Cortex-M отпугивали многих разработчиков своей сложностью? Помимо самой по себе развесистой программной инициализации железа (одна только корректная инициализация тактирования, чтобы процессор вообще запустился — это полстраницы кода), главной проблемой стал низкий уровень совместимости разных моделей друг с другом. Если один AVR менялся на другой иногда вообще без правки кода, то для смены STM32F1 даже на STM32L1 придётся поправить изрядно, а уж на какой-нибудь Infineon или NXP…
В пределах одного вендора проблема решалась выпуском набора библиотек, абстрагирующих софт от железа — так, STMicro на данный момент сделала их аж три штуки: SPL, HAL и LL. Однако популярность таких библиотек не всегда была так высока, как хотелось бы вендорам, а кроме того, они с очевидностью были привязаны к конкретному вендору.
Эта проблема начала решаться с появление микроконтроллерных операционных систем.
При всех словах о необходимости экономии ресурсов ОС оказались настолько удобны в разработке, что сейчас большинство проектов делается с их использованием. Впрочем, это вполне ожидаемо — при работе над крупным проектом, даже если вы не будете использовать готовую ОС, вы в итоге напишете для себя набор подсистем, фактически эту ОС составляющих. Просто потому, что они нужны (нет, по опыту людей, пишущих статьи о том, как сделать многозадачность средствами цикла loop(), я в курсе, что кому-то и не нужны — но это будет дискуссия о том, что кому-то и велосипед с круглыми обрезиненными колёсами не нужен, когда и на деревянных квадратных в принципе как-то можно ездить).
Микроконтроллерные ОС и что они дают
С точки зрения программиста ОС — это не только большой полосатый мух, но и набор сервисов «из коробки», серьёзно облегчающих ему жизнь:
- HAL — абстрагирование от железа. В ОС очень чётко и недвусмысленно разделено взаимодействие с железом и пользовательский код. Это позволяет, во-первых, легко переносить проекты между разными контроллерами (например, у нас большая, увесистая прошивка без каких-либо модификаций собирается под STM32L072, STM32L151 и STM32L451 — достаточно просто указать, какая плата нужна сейчас), во-вторых, при работе над проектом нескольких человек разделять между ними обязанности в соответствии с навыками и квалификацией (логику приложения, например, может писать человек, имеющий крайне смутное понятие о работе с регистрами в STM32, при этом низкоуровневая часть проекта будет развиваться другим человеком параллельно, и они не будут друг другу мешать). В некоторых ОС абстрагируется даже доступ к внешним устройствам — например, в RIOT есть API под названием SAUL, которым можно пользоваться для доступа к сенсорами; в этом случае автору кода верхнего уровня будет всё равно, какой конкретно сенсор там подключён где-то внизу.
- Многозадачность — любой серьёзный проект представляет собой набор конкурирующих и кооперирующихся задач, которые исполняются в разные периоды времени, периодически или по каким-либо событиям. Современные ОС позволяют легко выделять такие задачи в отдельные потоки, не зависящие от других потоков и обладающие собственными стеками, назначать им приоритет выполнения и отслеживать их работу.
- Таймеры — события часто привязываются к конкретному времени, и поэтому обязательно нужны часы. Аппаратные таймеры контроллера для регистрации событий в многозадачной ОС подходят плохо в силу ограниченности своего числа, поэтому ОС предоставляют собственные таймеры. Это подсистемы, работающие на базе одного из аппаратных таймеров, и позволяющие запрограммировать практически неограниченное количество событий с точностью, равной дискретности используемого таймера. Так как подсистема работает на базе прерываний аппаратного таймера, то взаимное влияние событий ограничено собственно только пересечением обработчиков прерываний.
- IPC — межпроцессное сообщение. Так как различные задачи работают не в вакууме, а общаются между собой, то ОС предоставляет средства такого общения, от простых семафоров и мьютексов, позволяющих, например, притормозить один поток в ожидании, пока другой освободит нужную периферию или получит нужные данные, до сообщений, в которых можно передавать данные и которые сами по себе могут являться триггером для переключения ОС из потока-отправителя в поток-получатель (так, например, делается выход из обработчика прерывания: если у вас есть тяжёлая процедура, которая должна запускаться по прерыванию, то из прерывания вы просто отправляете сообщение в собственно поток с этой процедурой, который выполняется уже в обычном контексте, не мешая работе системы).
- Наборы стандартных библиотек и функций — помимо API работы с самим микроконтроллером, ОС может предоставлять вам доступ к стандартным библиотекам, в том числе сторонней разработки. Это могут быть простые, но востребованные процедуры типа форматирования чисел между разными представлениями, процедуры шифрования и расчёта контрольных сумм, а также большие сторонние библиотеки, например, сетевые стеки, файловые системы и так далее, адаптированные под API данной ОС.
- Наборы драйверов — многие ОС предоставляют также «из коробки» наборы драйверов для внешних по отношению к контроллеру датчиков и систем.
В целом, благодаря ОС программирование микроконтроллеров становится всё ближе к написанию софта под большие ПК — даже API местами очень похоже на старый добрый POSIX.
Операционных систем сейчас уже довольно много, и наборы функционала у них разные. Ну, например:
- FreeRTOS — строго говоря, вообще не ОС, а только ядро ОС. Обвес к нему — на усмотрение разработчика. Самая популярная на данный момент микроконтроллерная ОС (но это не значит, что вам надо использовать именно её)
- Contiki OS — старая универстетская разработка, получившая известность благодаря сетевым возможностям, в частности, стеку 6LoWPAN. По нынешним меркам тяжеловесна, а многие концепции давно устарели (например, там нет нормальной по современным понятиям реализаци многозадачности). Сейчас переписывается под названием Contiki NG — из проекта выкидывают разный хлам, но общая концепция не меняется.
- RIOT OS — молодая университетская разработка, претендующая на место Contiki. Поддерживает кучу процессоров разных архитектур (включая даже старшие ATMega) и бурно развивается. Легковесна и понятна. Написана на C. Многого местами не хватает, но проста в освоении, поддержке и доработке под свои нужды. Оптимальна, на мой взгляд, в качестве учебной для профильных студентов.
- ARM Mbed — коммерческая (но опенсорсная, Apache 2.0) ОС разработки самой ARM Holdings. Была немного печальна в версии 2.0, но стремительно рванула ввысь в новой ветке 5.x. Написана на C++ и имеет достаточно понятный и простой API. Поддерживает гору различных плат, и в общем и целом делает это хорошо. У части вендоров есть собственные команды, занимающиеся поддержкой отладочных плат этого вендора в Mbed.
- TI-RTOS — собственная разработка Texas Instruments, работает примерно со всем, что TI когда-либо выпустила, ставится из коробки сразу с поддержкой примерно всего, но установка занимает пару часов и уменьшает ваш диск на несколько гигабайт. На мой вкус, API чрезмерно тяжеловесен.
Во многих случаях ОС не привязана к какой-либо среде разработки, а в качестве тулчейна обычно используется типовой arm-none-eabi-gcc. Так, RIOT изначально сделан на Makefile'ах, а потому с ним можно работать хоть из Visual Studio, хоть из командной строки. ARM Mbed имеет собственную систему сборки на питоне (mbed-cli), а также может быть быстро и практически автоматически настроен в PlatformIO, равно как и экспортирован через mbed-cli в проекты для Keil uVision или те же Makefiles.
То же самое касается и отладчиков — слова, не слышанного в мире Arduino. Как правило, любая платформа позволяет использовать старый добрый gdb в связке с любым нравящимся вам JTAG/SWD-отладчиком.
Этот страшный входной порог
А что же с входным порогом, с необходимостью долгими бессонными ночами читать бесконечные даташиты на регистры процессора и учить ассемблер? Всеми теми неприятными вещами, про которые вам обязательно упомянут?
А ничего. Его уже давно нет. Все перечисленные выше плюшки, сервисы и возможности с точки зрения кривой обучения получаются практически бесплатно.
Например, с RIOT OS «высокий входной порог» от нулевого знакомства с микроконтроллерами до запуска первой программы выглядит так:
- Скачать и поставить MinGW, arm-none-eabi-gcc и GNU make (для пользователей Windows 10 первый пункт не нужен, достаточно из Магазина поставить свежую Ubuntu)
- Скачать и распаковать последний релиз RIOT с Гитхаба
- cd RIOT/examples/hello-world
- make BOARD=nucleo-l152re
- залить полученный HEX-файл любым способом на Nucleo (в свежих версиях ST-Link это можно сделать просто киданием его на виртуальный диск)
Вся программа, если вы заглянете в main.c, выглядит так:
#include <stdio.h>
int main(void)
{
puts("Hello World!");
printf("You are running RIOT on a(n) %s board.n", RIOT_BOARD);
printf("This board features a(n) %s MCU.n", RIOT_MCU);
return 0;
}
Видите эту страшную инициализацию процессора? Бессонные ночи, проведённые над даташитами? Бесконечной длины вызовы функций SPL? Ассемблер, наконец?
Вот и я не вижу.
В принципе, это был сложный путь. На более простом вообще ничего локально устанавливать не надо — ARM Mbed предоставляет возможность компилировать всё прямо в онлайне.
Давайте сделаем метеостанцию
(не то чтобы я любил метеостанции, просто я сейчас сижу дома с температурой, и помимо всяких сугубо специализированных вещей под разные b2b-проекты, у меня тут из дженерика под рукой только Nucleo-L152RE да несколько плат с BME280 и OPT3001)
1) Регистрируемся на mbed.com.
2) Жмём там кнопочку «Compiler» и попадаем в онлайн-среду разработки. В правом верхнем углу в ней есть кнопочка выбора платы, с которой мы будем работать. Жмём её, во всплывшем окошке жмём «Add board» и выбираем ту версию Nucleo, которая у нас имеется (у меня это L152RE, у вас может быть и другая, а может быть и вообще не Nucleo, это неважно). Вернувшись в окно с проектами, выбираем в той же менюшке нашу Nucleo как текущую плату.
3) Жмём слева вверху New → New Program. Mbed предлагает нам сразу какую-то гору образцов, но по крайней мере для Nucleo-L152 все они притащат с собой старую версию ОС. Мы же хотим новую, свежую, вышедшую три дня назад 5.9.5, поэтому выберем самое простое — «Empty program».
Для дальнейшей работы нам потребуются две вещи — во-первых, подключить собственно исходники mbed, во-вторых, создать и чем-нибудь наполнить файл main.cpp. Для подключения исходников надо не только указать #include, но и собственно импортировать в проект библиотеку mbed-os.
Для этого:
- Жмём «Import», в открывшемся разделе справа вверху жмём малозаметную ссылку «Click here to import from URL», суём ей вот эту ссылку (при всём страшном виде, это просто ссылка на наиболее свежий релиз Mbed, которую мы взяли со страницы релизов)
- Тыкаем радиобаттон «Library»
- В качестве целевого проекта уже сам подставился наш проект
- Тыкаем «Import»
Теперь жмём на имени проекта правой кнопкой → «New file» → создаём файл main.cpp. Заполняем его тут же самым тривиальным образом:
#include "mbed.h"
DigitalOut led(LED1);
int main()
{
printf("Hello World !n");
while(1) {
wait(1); // 1 second
led = !led; // Toggle LED
}
}
(LED1 уже определён в описании платы — это собственно единственный имеющийся на Nucleo управляемый пользователем светодиод)
4) Жмём Ctrl-D (или кнопку «Compile»), ждём секунд десять-пятнадцать, скачиваем получившийся файл. Подключаем Nucleo к USB-порту (мощная засада: на Nucleo стоят разъёмы mini-USB, шнурок для которых есть уже не у всех и не всегда), обнаруживаем на компьютере новый диск размером 540 КБ с названием «Node_L152RE». Внутри него лежит файл MBED.HTM, недвусмысленно намекающий, зачем этот диск нужен.
На самом деле, конечно, любой кинутый на него BIN- или HEX-файл автоматически прошивается в контроллер, с Mbed это напрямую не связано, просто такой интерфейс программирования.
Кидаем туда скачанный файл. Nucleo моргает светодиодиком программатора, а потом начинает размеренно моргать светодиодом на плате — тем самым LED1.
5) Теперь нам надо добавить датчик BME280, ибо метеостанция у нас или что? Пятнадцать секунд поиска приводят нас к библиотеке с его поддержкой, на странице которой надо жамкнуть «Import Library» (библиотеки, конечно, бывают разные, и всегда стоит смотреть внутрь — но пока опустим эти детали). При импорте не забываем отметить, что это Library, и импортировать её надо в наш текущий проект («Target path»).
Тут же лежит пример использования библиотеки, в котором можно вкратце посмотреть, как ей пользоваться.
6) Добавляем обращение к библиотеке в свой код, заодно убирая из него всякий хеллоуворлд:
#include "mbed.h"
#include "BME280.h"
DigitalOut led(LED1);
BME280 sensor_bme(D14, D15);
int main()
{
while(1) {
wait(1); // 1 second
led = !led; // Toggle LED
printf("%2.2f degC, %04.2f hPa, %2.2f %%rn", sensor_bme.getTemperature(), sensor_bme.getPressure(), sensor_bme.getHumidity());
}
}
В качестве ножек я, ничтоже сумняшеся, напрямую указал пины, на которые мне в моей Nucleo будет удобно ткнуть I2C-датчик. Вообще, т.к. они прямо на плате подписаны как SDA и SCL (хотя вообще у STM32L1 несколько портов I2C, и каждый можно повесить на несколько вариантов ножек), то, скорее всего, дефайны I2C_SDA и I2C_SCL указали бы на них же. Но мне сейчас тяжело думать о настолько сложных материях.
7) Подключаем датчик. Четыре провода — +3,3 В, земля, SDA, SCL. Так как и STM32, и датчик 3,3-вольтовые, думать о согласовании уровней не надо.
8) Снова жмём «Compile», скачиваем новый BIN-файл и кидаем его в Nucleo.
8) ??????
(на самом деле на этом пункте мы открываем свою любимую терминалку — я обычно использую Termite, и цепляем её на виртуальный порт, соответствующий Nucleo; в современном компьютере это с большой вероятностью будет единственный наличествующий COM-порт. Скорость выставляем 9600 бит/с)
9) PROFIT!!!
Работает. Но тут мы, как хорошие программисты, не удовлетворяемся идеей «если индусский код работает, то не так уж он и плох», и смотрим, что мы хотим улучшить.
Глобально — наверное, две вещи. Во-первых, код, крутящийся в while(1) с 1-секундной задержкой — это очень плохая концепция, так как и задержка там не 1 секунда, а 1 секунда плюс время выполнения кода, и мало ли что с другими периодами мы захотим тут ещё запустить.
Во-вторых, 9600 бит/с — это как-то грустно, 2018 год на дворе, давайте хотя бы 115200.
С портом всё просто: надо создать свой экземпляр класса Serial, указать ему нужную скорость, а потом печатать через него.
Serial pc(SERIAL_TX, SERIAL_RX);
pc.baud(115200);
С избавлением от while(1) тоже, впрочем, больших проблем не возникает. В Mbed есть такая штука, как очередь событий (EventQueue), события в которой могут вызываться через заданный промежуток времени однократно (метод call) или постоянно (метод call_every), а результатом их вызова будет выполнение той или иной функции.
Оформляем в код:
#include "mbed.h"
#include "BME280.h"
DigitalOut led(LED1);
BME280 sensor_bme(D14, D15);
EventQueue eventQueue(/* event count */ 50 * EVENTS_EVENT_SIZE);
void printTemperature(void)
{
printf("%2.2f degC, %04.2f hPa, %2.2f %%rn", sensor_bme.getTemperature(), sensor_bme.getPressure(), sensor_bme.getHumidity());
led = !led; // Toggle LED
}
int main()
{
eventQueue.call_every(1000, printTemperature); // run every 1000 ms
eventQueue.dispatch_forever();
return 0;
}
Собираем, запускаем. Работает точно так же, как и раньше (что уже неплохо), но — теперь мы получили решение, которое можно практически неограниченно и без каких-либо проблем масштабировать. Мы совершенно спокойно можем накидать в eventQueue других событий с другими периодами (главное — следить, чтобы одномоментно у нас более 50 событий не жило), и до тех пор, пока их время выполнения не начнёт тупо пересекаться, нам вообще не надо беспокоиться о том, как они взаимодействуют.
Почему?
Потому что, в отличие от запихивания всего в один while(1) с тщательно выверенными задержками, они не взаимодействуют.
Ладно, теперь давайте добавим сюда OPT3001. Не то чтобы измерение освещённости было обязательной чертой метеостанции, но раз уж у меня есть на столе этот датчик…
С OPT3001 мы имеем некоторую проблему — это очень хороший (и точностью, и особенно энергопотреблением), но не сильно популярный по сравнению с тем же TSL2561 среди самодельщиков датчик, поэтому готовую библиотечку для него прямо в Mbed найти не получается. Я бы сказал, что драйвер для него и самому написать несложно (я писал), но давайте сунемся в гугль — и первой ссылкой обнаружим https://github.com/ashok-rao/mbed-OPT3001.
Снова кликаем Import, выбираем в заголовке малозаметную ссылку «Click here to import from URL», вставляем URL проекта на гитхабе, не забываем тыцнуть пункт «Library» — ну и собственно «Import».
На самом деле нет. Астанавитес.
На самом деле этот драйвер написан настолько на коленке, что его использовать вообще не стоит ни в каком случае. Как однажды сказал великий писатель, «открыв OPT3001.cpp, кровавые слёзы потекли из глаз моих», поэтому я отложил ненадолго все важные дела и быстренько набросал более пристойный драйвер (то есть, теперь я уже дважды писатель драйвера OPT3001).
Далее по известному уже пути: Import → Click here to import from URL → github.com/olegart/mbed_opt3001 → Library → Target Path = наш проект → Import.
Добавляем в проект остатки кода:
#include "mbed.h"
#include "BME280.h"
#include "OPT3001.h"
DigitalOut led(LED1);
BME280 sensor_bme(D14, D15);
OPT3001 sensor_opt(D14, D15);
EventQueue eventQueue(/* event count */ 50 * EVENTS_EVENT_SIZE);
Serial pc(SERIAL_TX, SERIAL_RX);
void printTemperature(void)
{
pc.printf("%2.2f degC, %04.2f hPa, %2.2f %%rn", sensor_bme.getTemperature(), sensor_bme.getPressure(), sensor_bme.getHumidity());
pc.printf("%ld luxrn", sensor_opt.readSensor());
led = !led; // Toggle LED
}
int main()
{
pc.baud(115200);
eventQueue.call_every(1000, printTemperature); // run every 1000 ms
eventQueue.dispatch_forever();
return 0;
}
Compile, сохраняем файл, кидаем его в Nucleo, не забываем переключить терминалку на 115200…
PROFIT!!!
Бонус: энергосбережение
Всё это мы написали, по большому счёту, левой задней пяткой, особо не задумываясь ни о чём — ну, кроме разве что выкидывания дурацкого и неудобного бесконечного цикла.
Среди вещей, о которых мы не задумывались, есть такая штука, как энергопотребление. Иногда о нём задумываться, однако, приходится.
Специально для его измерения на Nucleo есть джампер «IDD», сняв который, можно воткнуться туда мультиметром и посмотреть, что же у нас происходит (хотя вообще говоря, мультиметр — плохой инструмент для измерения энергопотребления микроконтроллеров, при быстрых скачках потребления он фиксирует в основном погоду на Марсе).
На моём L152 ток получается в районе 10 мА, немного гуляя туда-сюда в зависимости от свечения LED1, который мы по привычке всё ещё дёргаем (он потребляет 2-3 мА). Как-то довольно негуманно для системы, которая большую часть времени ничего не делает, вы не находите?..
В данном случае нам мешает та самая очередь сообщений, которой мы недавно радовались. Дело в том, что она позволяет задавать тайминги с миллисекундной точностью, а потому работает на быстром аппаратном таймере процессора, выключающемся при уходе последнего в сон — говоря иначе, если процессор заснёт, наш eventQueue.call_every заснёт тоже, причём навсегда. На STM32L151 нет как такового специального быстрого таймера, тикающего во сне — точнее, на некоторых моделях процессора его можно вытащить из обычных часов RTC (начиная с L151CB-A и выше, регистр RTC_SSR), но в общем случае — нет.
Кроме того, у очереди сообщений есть метод dispatch_forever(), крутящийся в конце нашей прошивки и тоже не дающий поспать — он занимается тем, что, собственно, принимает, обрабатывает и выполняет сообщения.
Но ведь нам же здесь и не нужны миллисекундные интервалы, у нас всё равно данные снимаются раз в секунду? И ведь сообщения нам надо обрабатывать, только когда процессор проснулся, потому что если не проснулся — откуда там сообщения?
Поэтому мы смело берём штуку под названием LowPowerTicker, работающую на RTC, и начинаем переписывать код на ней — благо поменять надо всего несколько строчек. Запустив LowPowerTicker, можно сказать системе sleep() — и она уснёт глубоким сном (ну или, точнее, тем, каким позволяют другие модули — здесь есть нюансы, например, какой-то модуль, ломающийся во сне, может сон на время своей работы запретить).
Но есть одна проблема: вызовы тикера выполняются в прерывании, а в прерывании нельзя делать какие-то долгие вещи — у нас же чтение датчиков очень, очень долгое (у OPT3001, например, от запроса до ответа проходит 100 мс). С printf всё ещё хуже — он ещё и громоздкий, так что в прерывании может тупо не хватить стека. Поэтому нам надо из прерывания послать сигнал функции, собственно читающей датчики и печатающей значение так, чтобы она выполнилась уже в обычном контексте.
Грубым методом было бы устанавливать в прерывании переменную, а в основном контексте читать значение этой переменной в while(1), и если вдруг она взвелась — вызывать чтение датчиков. Но мы же ведь с вами не ардуинщики какие, чтобы в while(1) запихивать осмысленный код?
Внезапно, поможет нам в этом тот же самый EventQueue, который мы только что отбросили.
#include "mbed.h"
#include "BME280.h"
#include "OPT3001.h"
DigitalOut led(LED1);
BME280 sensor_bme(D14, D15);
OPT3001 sensor_opt(D14, D15);
EventQueue eventQueue(/* event count */ 50 * EVENTS_EVENT_SIZE);
Serial pc(SERIAL_TX, SERIAL_RX);
LowPowerTicker lpTicker;
void printTemperature(void)
{
pc.printf("%2.2f degC, %04.2f hPa, %2.2f %%rn", sensor_bme.getTemperature(), sensor_bme.getPressure(), sensor_bme.getHumidity());
pc.printf("%ld luxrn", sensor_opt.readSensor());
led = !led; // Toggle LED
}
void tickerIRQ (void)
{
eventQueue.call(printTemperature);
}
int main()
{
pc.baud(115200);
lpTicker.attach(tickerIRQ, 1); // every second
while(1)
{
eventQueue.dispatch(0);
sleep();
}
}
Что мы здесь сделали? Да в общем ничего сложного: добавили Low Power Ticker, который раз в секунду выдаёт прерывание. По прерыванию контроллер просыпается, обработчик прерывания зовёт eventQueue.call, который должен вытащить собственно тяжёлую работу из обработчика во внешнюю функцию (сонливый процессор тут ему не мешает, т.к. у него нет никакой задержки, в течение которой процессор мог бы заснуть — call подразумевает немедленное выполнение). Сам по себе .call ничего бы не сделал, но в основном теле программы у нас снова появился while(1), в котором всего две строки — метод dispatch для очереди, обрабатывающий сообщения в ней, и sleep(), уводящий процессор в сон до следующего прерывания.
Всё.
Заснули → прерывание → проснулись → cal → вышли из прерывания → dispatch → сняли показания с датчиков → sleep() до следующего щелчка тикера.
PROFIT???
PROFIT!!!
(на самом деле, 0,57 мА потребления — это очень много, хотя и в пару десятков раз меньше прежнего. Но в Mbed, скорее всего, не учтён неприятный нюанс с процессорами STM32L1: у них по умолчанию ножки стоят в режиме Digital In без подтяжки, а надо бы Analog In или подтяжку включить, иначе триггеры Шмитта на входе этих ножек довольно прилично жрут из-за спонтанных переключений из-за наводок. Кроме того, в Nucleo ток может утекать через ножки JTAG в сторону встроенного на плату программатор-отладчика)
И всё это мы получили в общем-то без особых усилий. На самом деле, даже ещё не добравшись до «настоящей RTOS» с её процессами, семафорами, сообщениями и прочими вкусностями.
Единственной платой стало разве что потребление ресурсов — 55,1 КБ флэша и 9,2 КБ ОЗУ (можно посмотреть, жамкнув «Build details» в логе с сообщением об успешной сборке проекта), но и это для современных контроллеров — не то чтобы гигантские объёмы, даже совершенно типовой 2-долларовый STM32F103CB имеет 128 КБ первого и 20 КБ второго. В принципе же минимальный проект на микроконтроллере и ОС из тех, с которыми работал я, жил на STM32F030F4P6 с 16 КБ флэша и 4 КБ ОЗУ — на RIOT OS со слегка подрезанными крыльями.
Кроме того, в данном случае мы вообще никак не экономили ресурсы, в то время как в приличном обществе за использование float внутри printf на микроконтроллере разработчика просто молча выводят за дверь и расстреливают в коридоре. Ничего при этом не говорят — все и так понимают, за что. Даже malloc лучше, malloc в некоторых обстоятельствах всё же можно простить.
P.S. Для ленивых — написанный здесь код сразу в репозитарии Mbed.
P.P.S. Детали для сборки: Nucleo-L152RE (любая другая Nucleo / Discovery тоже подойдёт, проверьте только, что она есть в списке плат — но там, похоже, все есть), BME280 (внимание: на Али эти платки дешевле стоимости самого чипа не только потому, что китайцы щедрые, но и потому, что на них лепят втрое более дешёвый BMP280, который внешне неотличим). Где вам взять OPT3001 ближе Али — не знаю, если не считать родной девкит TI за три тысячи рублей.
Заключение
Я не хочу сказать, что в мире микроконтроллерных RTOS всё безоблачно, а каждая строчка кода сама собой сочится благодатью — тот же Mbed до приемлемого, с моей точки зрения, состояния дошёл где-то в последнюю пару лет, в RIOT только в релизе 2018.07 исправили часть старых болячек (а часть так и не исправили), и так далее.
Однако идею о том, что только Arduino «обеспечивает минимальный порог входа», а все остальные платформы — это обязательное ковыряние в странных библиотеках, сотнях страниц даташитов и ассемблере, может разделять только человек, последнее десятилетие пробывший где-то в очень удалённых от цивилизации местах (в не столь удалённых Интернет, говорят, и то уже провели).
Абсолютно ничего сложного в том, чтобы начать работать с современными контроллерами в современной среде разработки, нет. Более того, при этом вы получаете массу вкусных, полезных, и главное — грамотно реализованных вещей, радикально ускоряющих и упрощающих разработку. Нет, я не про библиотеки «мигания светодиодом», хотя и их тоже достаточно — я про таймеры, многозадачность, сообщения, сервисы и всё остальное, что не имеет никакого смысла в 2018 году нашей эры писать руками, потому что всё это уже написано до вас и, скорее всего, сильно лучше, чем когда-либо напишете вы.
Фактически, мы с вами буквально за полчаса с нуля сейчас сделали вполне пристойный проект — не просто работающий, а минимально нормальный с архитектурной точки зрения. Масштабируемый без особых усилий, работающий на прерываниях, поддерживающий энергосбережение. Не напрягаясь.
Да, можно извернуться и за несколько дней сваять на ардуине кособокое граблеобразное подобие того, что пользователи современных платформ получают из коробки в элегантном отглаженном виде. Можно даже написать на этом какой-то осмысленный проект, который рано или поздно ценой серьёзных затраченных усилий удастся заставить работать и при этом не вываливаться из ресурсов типового AVR.
Но…
Зачем?!
Посмотрите на календарь, 2018 год на дворе! Вам настолько себя не жалко?
Автор: Олег Артамонов