Содержание статьи
В те недалекие времена, когда мы были молодыми и здоровыми, все ламеры сидели на Windows 98, а тру-хакеры пили пиво и дико напрягались, устанавливая на свои машины седьмую слакварь. Поставил слакварь — стал мужчиной. Сегодня установить линукс на свою машину может каждая блондинка, поэтому нам, хакерам, приходится искать для себя новые испытания. Как насчет установки операционной системы реального времени scmRTOS на Arduino?
Немного теории
Похоже, что с логической точки зрения без сакраментального вопроса «что же такое операционная система?» нам не обойтись, как бы банально этот вопрос ни звучал. По сути, ОС — это некий набор программ, который позволяет другим программистам не думать о железе, не заморачиваться над разделением ресурсов физической системы и обеспечением многозадачности. Более умно и подробно это описано в Википедии.
Теперь коснемся термина операционная система реального времени. Если кратко, то ось такого типа гарантирует реакцию программы на «внешний раздражитель» (аппаратное или программное прерывание) не более, чем через оговоренное время. Чуть копнув, можно выяснить, что существуют понятия «жесткого реалтайма» и «мягкого реалтайма», но настолько глубоко в вопрос вдаваться не буду, на эту тему в интернетах достаточно материала. Отмечу только, что, выставив в настольной операционной системе приоритет для процесса «максимальный», ты все равно не получишь «истинного» реалтайма.
Попробую «смочить» сухие слова. Операционная система общего назначения все события от внешней периферии кеширует и передает управление коду, обслуживающему данное событие только тогда, когда придет очередь этого обработчика. Исходя из этого, получаем порой немаленькие задержки в обработке событий. Для ПК это не трагично, так как твой ПК не управляет никакими критически важными процессами. Теперь представь, что твой код выполняется на каком-нибудь устройстве нефтеперерабатывающего завода. Задержка реакции на аварийное событие может привести к экологической катастрофе.
Но отвлечемся от катастроф. Проще говоря, ОС реального времени обеспечит твоему коду передачу управления за известное время (от величины этого времени зачастую зависит и выбор самой ОС) при возникновении какого-либо события.
Ну и еще одна плюшка от ОС — это многопоточность, способность ОС выполнять код нескольких процессов на одном физическом ядре процессора (или на нескольких ядрах), осуществляя переключение между процессами и обеспечивая межпроцессное взаимодействие.
Многопоточность
И как же все это должно работать, ведь ядро у микроконтроллера Arduino одно, а процессов, которые надо выполнить «одновременно», много? Для решения этой задачи придумали планировщик, в его обязанности входят следующие действия:
- решать, какой код выполнять в текущий момент;
- при переключении процессов отвечать за сохранение стеков текущего процесса и восстановление стеков переключаемого процесса.
Решение о переключении между процессами принимается на основе таких факторов, как истечение кванта операционной системы (при организации равноприоритетного планирования или round robin), появление необходимости выполнить код более высокоприоритетного процесса, передать управление процессу, который ожидал аппаратное событие (и это событие наступило), принудительный вызов переключения процессов. На самом деле факторов больше, я перечислил только основные.
Из сказанного может сложиться впечатление, что процессы всегда будут работать по нисходящим приоритетам (сначала завершается более высокоприоритетный процесс, передается управление менее приоритетному и так до Idle). Но это впечатление не совсем верно. Зачастую процессы привлекают периферию, от которой требуется дождаться ответа, и тогда процесс «впадает в спячку» (если код написан аккуратно), а планировщик передает управление менее приоритетному процессу. Как только будет получен ответ от периферии, обработчик прерываний периферии должен взвести соответствующий флаг (семафор, например), по которому и будет разбужен уснувший процесс.
Пациента на стол
Принцип работы всех ОС реального времени практически одинаков (не зря они объединены названием ;)), но есть и различия: разная реализация планировщиков, разное обеспечение межпроцессного взаимодействия, реализация таймеров, набор плюшек в виде поддержки из коробки периферийного оборудования и файловых систем и прочее. Чтобы разобраться в самих принципах работы ОС реального времени, надо остановиться на чем-то одном. Выбор пал на scmRTOS, написанной на С++ (если до этого ты писал только на си, плюсов не бойся — их тут немного :)). scmRTOS — минималистичная операционная система, авторы дают нам только планировщик и базовые механизмы взаимодействия между процессами, остальное отдается на откуп пользователю ОС. scmRTOS имеет простой планировщик с вытесняющей многозадачностью, то есть в этой ОС нельзя создать несколько равноприоритетных процессов.
В проекте scmRTOS уже есть порт для нашего микроконтроллера (точнее, для всего семейства AVR, у которых ресурсов хватает на запуск этой ОС), чем мы и воспользуемся. Но, конечно же, мы покопаемся во внутренностях, так как цель данного материала — не просто запустить ОС, а разобраться, как это все работает.
В качестве базового проекта я возьму проект из материала прошлой статьи. Напомню, что в этом проекте реализована сигнализация вскрытия холодильника (мониторинг размыкания контактного датчика) — моргание светодиода с задаваемым из консоли интервалом. В рамках данной статьи мы всего лишь прикрутим ОС к этому проекту и добавим одну маленькую примочку — при возникновении события тебе в консоль будет выдаваться сообщение об этом. Функционально, конечно, ничего особо не поменяется, но на чем-то тренироваться надо.
Начинаем операцию
Скачивай исходники ОС с SourceForge. В комплекте идут несколько примеров, сами исходники операционки и порт для AVR.
Для начала работы надо создать конфигурационный файл, файл порта и файл расширений. Я недолго думая скопировал их из примеров, подкрутив только файл конфигурации scmRTOS_CONFIG.h
и отключив вызовы со стороны ядра ненужных мне хуков, следующим образом:
#define scmRTOS_SYSTEM_TICKS_ENABLE 0
#define scmRTOS_SYSTIMER_HOOK_ENABLE 0
// Выбор схемы переключения контекстов
#define scmRTOS_CONTEXT_SWITCH_SCHEME 1
// Использовать хук переключения контекстов
#define scmRTOS_CONTEXT_SWITCH_USER_HOOK_ENABLE 1
Отмечу отдельно scmRTOS_CONTEXT_SWITCH_SCHEME
и scmRTOS_CONTEXT_SWITCH_USER_HOOK_ENABLE
. Существует два способа переключения контекста процессов: прямое и асинхронное переключение с помощью программного прерывания. Я решил пойти путем асинхронного переключения (хотя в нашем случае это непринципиально). Если ты внимательно смотрел даташит на микроконтроллер ATmega2560 (а если не смотрел, то я подскажу), то заметил, что ATmega не имеет программных прерываний. В таком случае авторы операционки рекомендуют использовать какое-нибудь низкоприоритетное аппаратное прерывание и заставлять микроконтроллер вызывать его обработчик. В порте для AVR выбрано прерывание контроллера самопрограммирования SPM. Собственно, для «стимулирования» прерывания и необходимо, чтобы операционная система вызывала пользовательский хук переключения контекстов, так как это уже платформозависимая часть.
Следующим шагом необходимо включить таймер, отсчитывающий «квант» операционной системы.
TIMER0_CS_REG = (1 << CS01) | (1 << CS00); // clk/64
UNLOCK_SYSTEM_TIMER();
Для того чтобы создавать меньше каши и писать в «терминах» операционной системы, я использовал макросы для таймера, определенные в scmRTOS_TARGET_CFG.h
.
Под завершение добавим в функцию main вызов перехода в операционную систему и выкинем из этой функции все лишнее.
int main(void)
{
// Настраиваем железо
init_hw();
printf_P(PSTR("\r\nStarted...\r\n"));
OS::run();
}
Процессы
Каждый процесс — это небольшая программа, которая выполняется в бесконечном цикле, выход из функции-процесса запрещен. По большей части все процессы обычно «спят» или выполняют код мониторинга внешней периферии (а может, даже и внешнего оборудования). Спящие процессы ожидают какого-то события, для того чтобы его обработать и уснуть до следующего. Такими событиями могут быть сообщения от других процессов, сигналы из прерываний об изменении состояния периферии.
В scmRTOS процессы создаются как экземпляры классов на основе базового шаблонного класса OS::process<TPriority pr, size_t stack_size>
. pr
— это приоритет процесса, в зависимости от настроек ОС приоритеты распределяются от младшего к более приоритетному или наоборот. scmRTOS не позволяет создавать несколько равноприоритетных процессов. stack_size
задает размер стека, который будет зарезервирован для процесса, значение не подлежит какой-то математической оценке и чаще всего задается «на глаз», а потом эмпирически подгоняется под реальность.
В нашем проекте я сделал два процесса:
- процесс консоли команд;
- процесс сигнализации.
Процесс консоли является менее приоритетным и ожидает команды по UART. Процесс сигнализации ожидает сообщения из прерывания об изменении состояния дискретного входа, контролирующего взлом твоего холодильника, а при получении такого сообщения грязно ругается в консоль (выводит сообщение в UART).
Для красоты объявлю typedef для каждого класса-процесса:
// Объявляем процесс обработки команд консоли
typedef OS::process<OS::pr1, 500> TCommandTask;
TCommandTask CommandTask;// экземпляр процесса консоли
// Объявляем процесс обработки сигнализации
typedef OS::process<OS::pr0, 500> TAlarmTask;
TAlarmTask AlarmTask; // экземпляр процесса сигнализации
Теперь в пространстве имен namespace OS
необходимо описать тело функций-процессов, приведу код для консоли:
namespace OS
{
template<> OS_PROCESS void TCommandTask::exec()
{
for(;;)
{
process_command();
}
}//TCommandTask::exec()
}// namespace OS
Как видишь, ничего сложного, просто мы в бесконечном цикле крутим вызов обработчика команд.
Межпроцессное взаимодействие
Поговорим о сигнализации. Как уже сказано выше, мы хотим, чтобы наша железка выводила сообщение в UART при изменении состояния контактного датчика сигнализации холодильника. В прошлый раз мы сделали функции по смене режимов индикации устройства, теперь добавим новых плюшек.
Код обработки изменения состояния датчика находится в прерывании, и необходимо каким-то образом сообщить потоку, что произошло событие. Есть несколько вариантов: использовать флаги-события (Event), «почтовый ящик» MessageBox или межпроцессный канал channel. Я рекомендую использовать channel, так как он позволяет «кешировать» изменения состояния, в то время как процесс занимается обработкой «вытащенного» из канала события.
Для объекта, который мы будем передавать через канал, я объявил простенький класс
struct TAlarmMessage {
enum TAlarmSrc
{
DI_ALARAM, // Сигнализация об изменении дискретного входа
AI_ALARM // Сигнализация об изменении аналогового датчика (резерв для примера)
}src;
uint8_t state;
};
Теперь объявляй экземпляр канала
OS::channel<TAlarmMessage, ALARM_MSG_BOX_CAPACITY> AlarmMessageBox;
Для того чтобы получить из канала данные, необходимо вызвать функцию AlarmMessageBox.pop()
. Фишка этой функции в том, что если в канале нет данных, то выполнение текущего процесса остановится и процесс будет переведен в спящий режим, а управление будет передано другому процессу. Как только в канале появятся данные, процесс будет разбужен (в порядке очереди по приоритетам готовых к выполнению процессов) и данные будут возвращены через аргумент.
TAlarmMessage msg;
// Тут мы уснем до получения аварийного сообщения
AlarmMessageBox.pop(msg);
// Получено сообщение, обработаем его
if (msg.state == 1)
printf_P(PSTR("\r\nAlarm: raised\r\n"));
else
printf_P(PSTR("\r\nAlarm: failed\r\n"));
Вообще, эта функция может ожидать данные с тайм-аутом. То есть если за отведенный тайм-аут данные не поступят в канал, то процесс будет разбужен и продолжит свое выполнение. В таком случае необходимо проверять булев результат возврата функции. В нашей программе это не нужно, поэтому будем ждать, пока данные не поступят в канал.
Для отправки сообщения об изменении состояния датчика в код обработки этого самого изменения добавь следующее:
TAlarmMessage msg;
msg.src = TAlarmMessage::DI_ALARAM;
msg.state = 1; // 0 для перехода в режим «норма»
AlarmMessageBox.push(msg);
ch_blink_mode(wm_alarm); // wm_normal для перехода в режим «норма»
Таким образом мы наладили канал между прерыванием и процессом. Теперь при изменении состояния контактного датчика в процесс будет передано сообщение об этом.
Безопасность превыше всего
Как ты можешь заметить, наша программа из двух процессов сразу пытается записать данные в UART. Это плохо. А может, и не так, как кажется на первый взгляд. В прошлый раз мы сделали простенькие FIFO-буферы для отправки и приема данных. Проблема заключается в следующих моментах:
- в то время, пока один процесс пишет данные в FIFO, второй процесс может начать делать то же самое, и сообщение от одного потока будет «разорвано» сообщением другого потока, а то и вовсе испорчено;
- в то время, пока процесс читает данные из FIFO, может возникнуть прерывание от UART, которое кладет данные в FIFO на прием;
- два потока попытаются прочитать данные из FIFO на прием;
- в то время, пока процесс читает данные из FIFO, может возникнуть прерывание от UART, которое захочет положить данные в FIFO на прием.
Во всех перечисленных случаях возникают проблемы совместного доступа к внутренним переменным FIFO. Для разрешения этих конфликтных ситуаций я организовал канал channal
с элементами uint8_t
. Дополнительные меры, которые пришлось применить, — это проверка на пустой канал перед чтением из него данных на отправку в UART и запись данных в канал при приеме из UART.
// Объявляем FIFO-буферы для считывания и отправки данных в UART
#define TXFIFO_SIZE 128
uint8_t tx_buf[TXFIFO_SIZE];
static OS::TChannel uart_tx_fifo = OS::TChannel(tx_buf, TXFIFO_SIZE);
// Аналогично объявляется буфер на прием
ISR( USART0_RX_vect )
{
OS::TISRW tisrw;
unsigned char rxbyte = UDR0;
// Необходима проверка на заполненность входящего буфера, так как поведение самого класса в этой ситуации нас немного не устраивает
if (uart_rx_fifo.get_count() < RXFIFO_SIZE)
uart_rx_fifo.push(rxbyte);
}
Еще одна неприятность — это когда процесс останавливается прерыванием и при работе обработчика прерывания возникнет необходимость перепланировать задачи (например, получены данные из UART, которые ожидаются через объект синхронизации более высокоприоритетным процессом, чем тот, что сейчас прерван). В этой ситуации объект синхронизации дает сигнал ОС о переключении контекста и надо как-то объяснить товарищу планировщику, что перепланирование стоит отложить до лучших времен, так как в данный момент идет выполнение контекста прерывания, а не процесса. В scmRTOS делается это достаточно просто, необходимо объявить экземпляр класса-враппера прерываний OS::TISRW tisrw;
.
Копнув в исходники ОС, ты увидишь, что при создании экземпляра класса-враппера ядру сообщается, что сейчас выполняется код прерывания, а при вызове планировщика проверяется, не в прерывании ли мы сейчас. Если выполняется код обработчика прерывания, то переключение контекста не произойдет (либо будет отправлен запрос на программное прерывание, отвечающее за переключение контекстов, которое будет обработано после всех аппаратных прерываний).
Еще один класс, служащий мерами безопасности, — это TCritSect
, или критическая секция. Экземпляры этого класса необходимо создавать в тех ситуациях, когда ты пытаешься изменить или прочитать объект, используемый в другом процессе или прерывании. С объектами класса TCritSect
необходимо быть осторожным и ограничивать время его жизни только на доступ к общему объекту. Сделать это можно, ограничив область видимости объекта операторными скобками, например:
void foo()
{
// много кода
...
{// операторные скобки, ограничивающие критическую секцию
TCritSect cs;
// небезопасный код работы с общими ресурсами
...
}// закрывающая скобка критической секции
// еще много-много кода
...
}
К пуску готов
Итак, теперь код готов к тому, чтобы его прошить в микроконтроллер и посмотреть, что же получилось. Прошивай микроконтроллер, открывай терминалку и для пущего эффекта нажми на кнопку RESET на плате. Как и в прошлый раз, в терминал выводится сообщение о старте и приглашение консоли:
Started...
Arduino>
Теперь попробуй разомкнуть контактный датчик, и в терминал тут же выведется сообщение об этом:
Alarm: raised
Если после этого замкнуть датчик, то в терминале сразу же появится уведомление:
Alarm: failed
Заключение
На этом я заканчиваю очень и очень краткое знакомство с операционными системами реального времени на микроконтроллерах. scmRTOS достаточно маленькая и мощная операционная система для такого малыша, как ATmega2560. В ней нет поддержки файловых систем, работы с периферией и еще всяких вкусностей, но в рамках ее применения на микроконтроллерах с очень ограниченными ресурсами это разумно возложено на плечи разработчика — пользователя ОС. Операционные системы дают огромный простор для более красивого, понятного и функционального кода. Надеюсь, что данный материал подогрел твой интерес к программированию микроконтроллеров. Железный привет, RESET :).
![Экран терминала при размыкании/замыкании контактного датчика](https://xakep.ru/wp-content/uploads/2015/01/term.png)
![](https://xakep.ru/wp-content/uploads/2014/12/cover_12_2014_mid-217x280.jpg)
Хакер #191. Анализ безопасности паркоматов
Переключение контекстов
Под контекстом процесса подразумевают стек возвратов, программный стек, значения регистров, то есть все то, что требуется программе для выполнения. При переключении контекстов процессов происходит сохранение состояния процессора для выполняемого в текущий момент процесса и воссоздание состояния процессора для нового. Фактически процесс никоим образом не может заподозрить, выполняется он в многозадачной среде или же в однозадачной, так как управление у него забирается бесцеремонно, а при возврате управления окружающая среда оказывается восстановлена. Единственное, о чем необходимо заботиться процессам, — это разделение ресурсов между ними, ведь, когда процесс начинает пользоваться разделяемым ресурсом, управление у него может быть отобрано и передано другому процессу, который неожиданно может захотеть попользоваться тем же ресурсом. Как пример, разделяемыми ресурсами является периферия процессора, если какой-то процесс захотел пообщаться с чем-то по шине SPI, то рекомендуется использовать объекты синхронизации для блокировки доступа к шине другим процессам.
Программистам, работающим в Windows
Волею судеб материал для этой статьи я готовил в ОС Windows и узнал в связи с этим много, хм, интересного. Во-первых, драйверы для Arduino придется скачивать либо вместе с Arduino IDE (в виде ZIP-архива и оттуда выдергивать драйверы) либо искать на просторах интернета. Я скачал Arduino IDE. Второй сюрприз меня поджидал при попытке прошить микроконтроллер с помощью AVRDUDE. Он упорно не хотел прошиваться, но я заметил, что при перезагрузке иногда он «цепляется». Выяснилось, что AVRDUDE не очень-то хочет работать с аппаратным управлением потоком порта, если ему не указать явно тип программатора через параметр -cwiring вместо -c STK500.