React для вбудованої розробки

text
Це дивно, адже веб-розробка та програмування мікроконтролерів здаються зовсім різними. Перше пов’язане з розробкою елегантних інтерфейсів користувача у зручному веб-браузері, з безліччю простору для маневру — динамічна пам’ять, високорівневі абстракції, ресурси, що здаються майже безмежними. А інше? Тут ви працюєте на рівні апаратного забезпечення, де правила диктують реальні строки виконання, а кожен байт пам’яті на вагу золота.

Але якщо придивитись до цього ближче, обидва напрямки мають одну основну проблему: все стає занадто складним.

Якщо ви коли-небудь працювали з прошивкою для мікроконтролера, то, ймовірно, відчули біль від цієї ситуації. Коли ваша кодова база росте, вона починає нагадувати тарілку з спагеті, яку хтось випадково впустив на підлогу. Частини логіки для керування периферією розкидані по всьому коду. Обробники переривань та зміни стану з’являються в найнесподіваніших місцях. Як ми любимо створювати хаос, чи не так? І це може нагадати вам про ваші кошмари з налагодженням та інтеграцією нової функції в систему. Потягнути одну нитку, не порвавши весь светр, — це схоже на те.

А що якщо — і це може звучати божевільно — ми використаємо деякі чудові ідеї з React, цієї революційної бібліотеки для веб-розробки, і адаптуємо їх до програмування мікроконтролерів? React пропагує модульність, зосереджуючись на що робити, а не на тому, як це робити, та керує станом у чіткий, передбачуваний спосіб.

Ця стаття саме про це: як архітектурні патерни React, такі як компоненти, оновлення стану та керування життєвим циклом, можуть виглядати та відчуватися в суворому середовищі з обмеженими ресурсами мікроконтролерів, і як ці ідеї можна реалізувати за допомогою таких трюків, як макроси C та оптимізації на етапі компіляції, щоб це мало сенс для апаратного забезпечення в розробці прошивок.

Чому React для мікроконтролерів?

Не дивно, що основні принципи React вирішують багато універсальних проблем у програмній архітектурі та можуть бути застосовані далеко за межі їх первісного призначення. Суть React полягає у впорядкуванні складності. У цьому хаотичному світі мікроконтролерів, де кожна операція — від керування світлодіодами до управління UART-зв'язком — здається, пов’язана з важкими маніпуляціями з реєстрами та змінними стану, акцент на простоті має величезну привабливість.

Це означає, що нам не потрібно переносити сам React, а можна скористатися основними принципами React за допомогою рідних засобів для вбудованої розробки. Використовуючи макроси C разом з обережним генеруванням коду на етапі компіляції, можна отримати архітектуру компонентів, подібну до React, без накладних витрат на час виконання. Це не просто теоретичне міркування; я зміг застосувати це в реальних виробничих системах, від простих контролерів світлодіодів до складного обладнання для промислової автоматизації.

Адаптація основних концепцій

Обмежена пам'ять, відсутність збору сміття, вимоги до реального часу, безпосереднє спілкування з апаратним забезпеченням — це деякі з характеристик середовища з обмеженими ресурсами MCU. Основні ідеї React — компонування, керування станом і односпрямований потік даних — можна успішно застосувати, незважаючи на ці обмеження.

Компонентний дизайн
Перезапускові компоненти — це одна з привабливих рис використання React для створення інтерфейсів користувача. Вони є окремими одиницями з власними станами, логікою та поведінкою. Тепер уявіть, що ви використовуєте цю концепцію для розробки MCU. Що, якби ми сприймали апаратні компоненти, такі як двигуни, таймери, кнопки та світлодіоди, як окремі «компоненти»? Приберіть складні елементи низького рівня, щоб забезпечити модульність та повторне використання в різних проектах.

У React компоненти є динамічними та інтерактивними, зазвичай реагують на дії користувача через віртуальний DOM.
text
У мікроконтролерах, з іншого боку, наші компоненти є статичними та повністю детермінованими, і їх можна моделювати за допомогою структурованих блоків на C, що зберігають стан і пропси, де пропси представляють чітко визначені інтерфейси для взаємодії.

/* Компонент кнопки */  

typedef struct {  
 eer_gpio_handler_t *io;  
 void * pin;  

 enum {  
 BUTTON_PUSH, /* Кнопка натискається лише при натисканні */  
 BUTTON_TOGGLE /* Зміна стану після кожного натискання */  
 } type;  


 struct Clock_time *clock; /* Часова мітка для фільтрації відскакування */  
 int bounce_delay_ms; /* Затримка для фільтрації відскакування в мілісекундах */  

 struct {  
 void (*press)(eer_t *instance);  
 void (*release)(eer_t *instance);  
 void (*toggle)(eer_t *instance); /* Коли натискається, стан змінюється */  
 } on;  
} Button_props_t;  

typedef struct {  
 union {  
 struct {  
 bool level : 1;  
 bool pressed : 1; /* Чи натиснута кнопка */  
 };  
 uint8_t flags;  
 };  
 uint16_t tick; /* Часова мітка, коли змінено стан піну */  
} Button_state_t;

Наприклад, існують прості реалізації компонентів IO та Button.

Розглядаючи вбудовані периферійні пристрої через призму «компонентів», розробка для мікроконтролерів стає не лише більш керованою, але й більш елегантною — додаючи штрихи програмної інженерії до часто складного світу апаратного забезпечення.

Керування станом та синхронізація

React наголошує на єдиному джерелі правди для стану, що забезпечує передбачуване та декларативне оновлення системи. У мікроконтролері стан може представляти конфігурацію чи поведінку апаратних периферій — наприклад, чи працює двигун, чи ввімкнений світлодіод, чи досяг сенсор температури порогу. Організовуючи ці стани в центральні чи локальні «контейнери стану», ми можемо систематично відстежувати стани периферії, уникаючи помилок від розкиданих і не синхронізованих змін стану.

Глобальний розподіл стану: На відміну від динамічних об'єктів стану в React, ми виділяємо стан глобально на етапі компіляції. Це гарантує повний контроль над використанням пам'яті.

Детерміновані оновлення: Добре спроектовані цикли оновлень керують змінами стану. Це забезпечує, щоб компоненти залишались у відповідності з їх апаратною основою, запобігаючи проблемам, таким як умови гонки або непослідовні стани.

Декларативне програмування

Традиційне програмування для вбудованих систем сильно залежить від імперативного «як це зробити» коду: явного прописування кожної конфігурації піну, оновлення стану та взаємодії з периферією. Підхід, натхненний React, дозволяє розробникам зосередитись на «що» система має робити, залишаючи «як» абстракціям.

Clock(clk, &hw(timer), TIMESTAMP);  

/* Компонент кнопки з повним життєвим циклом */  
pin_t enter_pin = hw_pin(ENTER_PORT, ENTER_PIN);  
Button(enter, _({  
 .io = &hw(gpio),  
 .pin = &enter_pin,  
 .clock = &clk.state.time,  
 .type = BUTTON_PUSH_PULLUP,  
 .bounce_delay_ms = 100,  
 .on = {  
 .press = handle_press,  
 .release = handle_release  
 }  
}));  

/* Основний цикл програми */  
/* Компоненти автоматично керують своїм життєвим циклом */  
loop(clk, enter) {  
 /* Реагуємо на зміни стану */  
 if (Button_is_pressed(enter)) {  
 /* Обробляємо натискання кнопки */  
 }  
}

Більш складний приклад реалізації простого shell.

Односторонній потік даних

React нав'язує односпрямований потік даних, забезпечуючи, щоб зміни передавались передбачувано від джерела правди (стану) до відображеного інтерфейсу.
text
У мікроконтролерах цей принцип може керувати взаємодією між входами (наприклад, даними з сенсорів) і виходами (наприклад, двигунами або дисплеями), запобігаючи хаотичним зворотним зв'язкам або умовам гонки.

Життєвий цикл компонента

Аксесуари апаратного забезпечення, сенсори або логічні одиниці функціонують подібно до компонентів React у цьому підході; кожен має визначений життєвий цикл, починаючи від ініціалізації (монтаж) і змін стану (оновлення) до очищення (демонтаж). Ми відтворюємо елегантність React, зберігаючи ефективність та мінімальний обсяг коду, що є необхідним для вбудованих систем, використовуючи макроси C для створення декларативного API.

#include "MyComponent.h"  

WILL_MOUNT(MyComponent) {  
 state->initialized = true;  
}  

SHOULD_UPDATE(MyComponent) {  
 return props->value != next_props->value;  
}  

WILL_UPDATE(MyComponent) {  
 state->updated = false;  
}  

RELEASE(MyComponent) {  
 state->value = props->value;  
}  

DID_UPDATE(MyComponent) {  
 state->updated = true;  
 printf("MyComponent updated with value %d\n", state->value);  
}  

DID_MOUNT(MyComponent) {  
 printf("MyComponent mounted with value %d\n", state->value);  
}

Цей шаблон дозволяє зберігати код організованим, багаторазовим і зручним для налагодження завдяки чітким станам, таким як DEFINED → RELEASED → PREPARED → RELEASED → … і хукам для кожного кроку.

Фаза ініціалізації

DEFINED: Компонент оголошений з початковими властивостями

  • WILL_MOUNT: Підготовка компонента до першого використання (наприклад, ініціалізація апаратного забезпечення)
  • RELEASE: Застосування початкового стану
  • DID_MOUNT: Компонент готовий до роботи

Фаза оновлення

RELEASED: Компонент готовий до нового циклу оновлення

  • SHOULD_UPDATE: Визначення, чи потрібно оновити компонент
  • WILL_MOUNT: Підготовка до змін стану/властивостей

PREPARED: Компонент готовий до застосування змін

  • RELEASE: Застосування змін
  • DID_UPDATE: Операції після оновлення

Це означає, що управління станами компонентів і оновленнями у вбудованих пристроях може бути справді простим за допомогою шаблонів життєвого циклу React для програмування на MCU. Чудова філософія React, поєднана з унікальними викликами вбудованого програмування, призведе до коду, який буде легшим для підтримки, але головне — набагато надійнішим.

Імплементація показує, що складні методології веб-розробки можна успішно адаптувати для ресурсно обмежених середовищ, зберігаючи код чистим, а систему надійною.

Джерела

  1. Вбудований Event-based React: EER / Компоненти / Застосунки
  2. React бібліотека для веб-розробки: Офіційний сайт React

Продовження буде

Я маю намір зануритися глибше у розробку цього фреймворку в наступних статтях:

  • Реалізація EER фреймворку — Занурення у проектування фреймворку для розробки MCU, натхненного React
  • Архітектура на основі подій — Розгляд того, як абстрагувати низькорівневу логіку переривань за допомогою шаблонів, орієнтованих на події.
  • Реальні обмеження часу — Як можна гарантувати, щоб система, натхнена React, працювала передбачувано і швидко?
  • Написання застосунків — Цей розділ показує, як застосувати концепції та техніки з попередніх статей до реальних сценаріїв.
  • Інструменти та налагодження — Розгляд інструментів та технік для візуалізації змін станів, станів апаратного забезпечення, симуляцій компонентів і покращень робочих процесів.

Перекладено з: React for Embedded Development

Leave a Reply

Your email address will not be published. Required fields are marked *