Случайно наткнулся на библиотеку, реализующую кооперативную операционную систему. Называется она quarkTS. Ссылки давать не буду, ибо очень просто ищется на GitHub. Помимо классических задач, библиотека может реализовывать задачи в виде конечного автомата. Также есть различная функциональность в виде таймеров, очереди сообщений, ко-рутин для временного и/или условного переключения контекста, утилит для работы с командной строкой, памятью. И всё это может уместиться в небольшом размере флэш памяти 8-ми, 16-ти, 32-х битных МК. Список протестированных платформ есть в мануале, но фактически из-за отсутствия аппаратной привязки библиотеки к какой-либо платформе, её можно запустить на любом МК.
Чтобы далеко не ходить, давайте решим такую задачу прямо из мануала: нажатие на кнопку зажигает светодиод, повторное нажатие на кнопку заставляет его моргать. Если ещё раз нажать, то светодиод гаснет. Мало того, через определённое время бездействия светодиод также должен гаснуть автоматически. У меня под рукой есть плата Explained Mini на Attiny817. На ней как раз расположены один светодиод и одна кнопка. К портам МК они подключены вот так: Код (C++): /* | PC0| - LED | PC5| - Button ---/ --- | | --- */ Я начну с написания макроопределений для светодиода: Код (C++): #define LED_TOGGLE() PORTC.OUTTGL = PIN0_bm #define LED_ON() PORTC.OUTSET = PIN0_bm #define LED_OFF() PORTC.OUTCLR = PIN0_bm В новых AVR и Attiny есть специальные регистры, которые непосредственно управляют пинами без выполнения цепочки операций "чтение-модификация-запись". Ими я и воспользовался выше. Кнопку я заведу в обработчик прерывания: Код (C++): ISR (PORTC_PORT_vect) { BUTTON_PRESS = 1; PORTC.INTFLAGS = 0xFF; } Дребезг кнопки меня здесь никак не волнует, так как флаг Код (C++): volatile uint8_t BUTTON_PRESS = 0; я буду опускать в теле обработки задачи. Прерывание не сбрасывает автоматически флаги, поэтому делаем это вручную. Рассматриваемая ОСРВ не имеет абсолютно никакой аппаратной привязки за исключением одного: системног тика часов. Для его передачи в ОСРВ предусмотрено два варианта: с помощью HAL функции целевой платформы или вызова функции Код (C++): qClock_SysTick(); Я воспользуюсь вторым вариантом, т..к у меня нет системных часов: Код (C++): ISR (RTC_CNT_vect) { RTC.INTFLAGS = 1; qClock_SysTick(); } Это прерывание RTC таймера по достижению им определённого значения. Данное прерывание не сбрасывает флаг, поэтому его сбрасываем вручную. Для инициализации RTC таймера воспользуюсь такой функцией: Код (C++): void RTC_Init (void) { RTC.CLKSEL = RTC_CLKSEL_INT32K_gc; // Подключаем источник тактирования 32 кГц RTC.PER = 32; // В регистр переполнения 32, чтобы получить прерывание раз в 1 мс RTC.INTCTRL = RTC_OVF_bm; RTC.CTRLA = RTC_RUNSTDBY_bm | RTC_PRESCALER_DIV1_gc | RTC_RTCEN_bm; } RTC таймер настраиваю так, чтобы получить системный тик с периодом 1 мс. Что делают биты регистров не сложно догадаться по их названию. Тактируется таймер от внутреннего генератора 32 кГц. Мне для поморгать какая-то точность не нужна: как моргает, так и моргает. Ну и в завершении подготовительных процедур запишем инициализацию портов: Код (C++): void GPIO_Init (void) { PORTC.DIRSET = PIN0_bm; PORTC.PIN5CTRL = PORT_ISC_FALLING_gc; } Подтяжку к шине питания я не включаю - она есть на плате в виде резистора.
Для решения нашей задачи так и напрашивается конечный автомат. В качестве состояний автомата возьмём состояния светодиода. Тогда граф состояний и переходов будет выглядеть так (картинка из мануала): Здесь: S1 - светодиод выключен S2 - светодиод включен S3 - светодиод мигает t1 - нажатие кнопки оператором t2 - завершение таймаута Прежде, чем начать работу с конечным автоматом, необходимо произвести инициализацию ОСРВ. Делается это с помощью следующей функции: Код (C++): void qOS_Setup( const qGetTickFcn_t TickProvider, const qTimingBase_t BaseTimming, qTaskFcn_t IdleCallback ) Первый аргумент - это функция системного тика, второй - это уведомление ОСРВ о периоде системного тика для правильного расчёта временных отсчётов. Ну и последний аргумент - это обратный вызов функции для обработки режима ожидания. Вызывается она тогда, когда в операционной системе отсутствуют задачи для текущего выполнения. Время в функции инициализации задаётся в формате float. Для систем, не поддерживающих данный формат аппаратно, работа с float весьма накладна. Поэтому ОСРВ QuarkTS поддерживает работу с целочисленными значениями времени. Для этого в файле qconfig.h необходимо включить опцию: Код (C++): #define Q_SETUP_TIME_CANONICAL ( 1 ) В таком случае системный тик жёстко задан и равен 1 мс, а все временные отсчёты также указываются в мс. Тогда наша функция сокращается до вида: Код (C++): void qOS_Setup( const qGetTickFcn_t TickProvider, qTaskFcn_t IdleCallback ) В режиме простоя операционной системы я буду спать. Применённый мною таймер RTC умеет тикать в режимах сна вплоть до StandBy, поэтому разрешаем его: Код (C++): SLPCTRL.CTRLA = SLEEP_MODE_STANDBY | SLPCTRL_SEN_bm; В режиме простоя уходим спать: Код (C++): void IdleTask_Callback (qEvent_t e) { sleep_cpu(); } Теперь запишем функцию инициализации операционной системы: Код (C++): qOS_Setup(NULL, IdleTask_Callback);
Опишем функции состояний нашего автомата. Состояние, когда светодиод выключен: Код (C++): qSM_Status_t State_LED_Off (qSM_Handler_t m) { switch (m->Signal) { case QSM_SIGNAL_ENTRY: LED_OFF (); break; default: if (BUTTON_PRESS) { m->NextState = State_LED_On; BUTTON_PRESS = 0; } break; } return qSM_EXIT_SUCCESS; } Из всех полей структуры qSM_Handler_t нам сейчас интересны два: Signal и NextState. Поле Signal содержит признак выполнения функции (задачи): QSM_SIGNAL_ENTRY - этот признак указывает на то, что задача выполняется первый раз после смены состояния автомата. Для уведомления конечного автомата о том, что необходимо сменить его состояние, существует поле NextState. При нажатии на кнопку мы просим наш автомат включить светодиод: Код (C++): qSM_Status_t State_LED_On (qSM_Handler_t m) { static qSTimer_t timeout; switch (m->Signal) { case QSM_SIGNAL_ENTRY: qSTimer_Set (&timeout, 5000); LED_ON (); break; default: if (qSTimer_Expired(&timeout)) { m->NextState = State_LED_Off; } if (BUTTON_PRESS) { m->NextState = State_LED_Blink; BUTTON_PRESS = 0; } } return qSM_EXIT_SUCCESS; } Т.к. QSM_SIGNAL_ENTRY вызывается один раз после изменения состояния автомата, то здесь мы инициализируем таймер, по сигналу которого мы выйдем из данного состояния. Отведённая пауза - 5000 мс или 5 с. Вышло время или нет мы проверяем с помощью функции qSTimer_Expired. Если до истечения этого времени оператор нажмёт кнопку, то светодиод начнёт моргать: Код (C++): qSM_Status_t State_LED_Blink (qSM_Handler_t m) { static qSTimer_t timeout, blinktime; switch (m->Signal) { case QSM_SIGNAL_ENTRY: qSTimer_Set(&timeout, 10000); break; default: if (qSTimer_Expired(&timeout) || (BUTTON_PRESS)) { m->NextState = State_LED_Off; BUTTON_PRESS = 0; } if (qSTimer_FreeRun(&blinktime, 100)) { LED_TOGGLE(); } break; } return qSM_EXIT_SUCCESS; } Здесь с помощью функции qSTimer_FreeRun() мы организуем мигание светодиода с частотой 5 Гц. Функция qSTimer_FreeRun проверяет, вышло ли указанное в её аргументе время. Если вышло, она возвращает истину и перезапускает указанный таймер на указанный период. Все наши состояния мы описали, теперь необходимо в ОСРВ добавить задачу для выполнения. Но прежде инициализируем необходимые объекты: Код (C++): qTask_t LED_Task; qSM_t LED_FSM; LED_FSM - это переменная, которая описывает конечный автомат, LED_Task описывает нашу задачу. В ОСРВ задача с функциями конечного автомата добавляется так: Код (C++): qOS_Add_StateMachineTask(&LED_Task, qHigh_Priority, 20, &LED_FSM, State_LED_Off, NULL, NULL, NULL, NULL, qEnabled, NULL); qHigh_Priority - это приоритет нашей задачи. Она у нас всего одна (не считая простаивающей задачи), поэтому приоритет здесь не важен; 20 - это период вызова нашей задачи. Напомню, что все временные интервалы у нас указываются в мс. Период 20 мс выбран с целью устранения дребезга контактов кнопки; State_LED_Off - это состояние, в которое должен перейти наш автомат сразу после инициализации; qEnabled - разрешение выполнения нашей задачи. Осталось разрешить прерывания и запустить выполнение операционной системы: Код (C++): sei(); qOS_Run(); Цикл Код (C++): /* while (1) { }*/ при нормальном выполнении операционной системы здесь не нужен. Enjoy!
Мне так лень резисторы паять, я подтяжку обычно включаю. Очень интересно. Попробуйте на Хабре публиковаться.
Интересная штука, но разработчики говорят, что это не ОСРВ. Паровозз, уже не первый раз пытаешься за ОСРВ выдать то, что к ОСРВ ни какого отношения не имеет. Если уже так хочется "потыкать палочкой" в ОСРВ, то возьми туже FreeRTOS (в описании к quarkTS на Github есть ссылочка в последнем абзаце). Тебя ждёт очень занятный квест -- запуск FreeRTOS на ATtiny817. Хотя этот квест может оказаться не проходимым. Но не отчаивайся характеристик Arduino UNO 3-ей ревизии хватает для запуска FreeRTOS (по крайней мере так пишут). А что касается данной ОС, то кооперативность -- это скорее недостаток. Могли бы вытесняющую многозадачность завести.... Тем более что:
следующее Я уже взял uOS. После неё FreeRTOS мне не интересна. Как и не интересны старые МК. Очень популярна ещё RIOT OS. Но список поддерживаемых платформ скуден.