Розробка

Швидкий старт з ARM Mbed: розробка на сучасних мікроконтролерах для початківців

Традиційною унікальною перевагою платформи 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 «високий вхідний поріг» від нульового знайомства з мікроконтролерами до запуску першої програми виглядає так:

  1. Завантажити і поставити MinGW, arm-none-eabi-gcc і GNU make (для користувачів Windows 10 перший пункт не потрібен, достатньо з Магазину поставити свіжу Ubuntu)
  2. Завантажити і розпакувати останній реліз RIOT з Гітхаба
  3. cd RIOT/examples/hello-world
  4. make BOARD=nucleo-l152re
  5. залити отриманий 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) Тиснемо там кнопочку «Компілятор» і потрапляємо в онлайн-середовище розробки. У правому верхньому кутку в ній є кнопочка вибору плати, з якою ми будемо працювати. Тиснемо її, у спливаючому віконці тиснемо «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.2 f degC, %04.2 f hPa, %2.2 f %%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.2 f degC, %04.2 f hPa, %2.2 f %%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 → Бібліотека → 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.2 f degC, %04.2 f hPa, %2.2 f %%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.2 f degC, %04.2 f hPa, %2.2 f %%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 рік на дворі! Вам настільки себе не шкода?

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Check Also

Close
Close