Здравствуйте! Меня зовут Александр, и я работаю программистом микроконтроллеров.
Наверное, любой разработчик встраиваемых систем время от времени подумывает написать свою собственную ось. Да такую, чтобы другим неповадно было!
И ваш автор не исключение.
Как по мне - дело не то чтобы запредельно сложное, сколько кропотливое. Если у вас, как и у меня, увлечение или карьера крутится вокруг Arm Cortex-M серии, то вооружаемся стволами (раз, два и три) и выдвигаемся за Джеффом.
Но, написав и запустив ядро своей "best of the best" оси около года назад, я вскоре забросил разработку. Ибо как я ни креативил, вместо Сокола Тысячелетия у меня получался крепенький, но банальный и скучный велосипед.
А ведь хотелось оригинальности и бесстыдного выпендрёжа.
И тут в 20-й стандарт завезли корутины.
Вот это вот всё:
#include <coroutine> Coro task(){ foo(); co_await awaitable_1(); bar(); auto res = co_await awaitable_2(); func(res); }
Тут ваша чуйка эмбеддера должна триггернуть: "А если то же самое, но с перламутровыми пуговицами?" :
#include <coroutine> Coro task1() { while (true) { // ожидаем некое событие co_await event.get(); // по его наступлению блинкаем toggle_led(); // запускаем таймер на 250 мс/с/мин и ждем co_await timer.get(250); // по истечению времени задержки снова блинкаем toggle_led(); // и все по новой } }
Действительно, получилась сигнатура типичной задачи в РТОС. Причем в случае с корутинами компилятор возьмет на себя расчет требуемой памяти под задачу. Вероятно, эти данные будет несложно получить и учесть. Нам останется только контролировать объем памяти, выделенной суммарно под все задачи. Уже неплохо.
Фантазируем дальше. Будет удобно, если оператор co_await сможет выступить единым окном обмена данными между корутиной, диспетчером и примитивами синхронизации (эвенты, мьютексы, таймеры, очереди etc.). Тогда мы сможем выиграть в композиции и читабельности кода.
Хорошо, а что можно сделать с приоритетами задач? А можно дерзнуть, вывернуть все наизнанку и внезапно получить задачу с динамическим приоритетом времени выполнения в зависимости от ожидаемого события :
#include <coroutine> Coro task2(){ while(true){ // ждем сигнала от очереди с нормальным приоритетом co_await queue.get<CoPrio::normal>(); // выгружаем значение в режиме нормального приоритета auto payload = queue.unload(); // пробуем захватить мютекс. Если успешно, то продолжаем // выполнение сразу. Если нет - ждем его освобождения co_await mutex.get<CoPrio::low>(); // работаем с неким общим ресурсом в режиме низкого приоритета shared_bus_send(res); // свобождаем мьютекс mutex.give(); // ждем событие с высоким приоритетом co_await event.get<CoPrio::high>(); // выполняем срочную работу в режиме высокого приоритета very_urgent_func(); } }
Выглядит заманчиво.
Останется навесить сюда диспетчер, жонглирующий нашими корутинами, плюс context switcher, и может получиться нечто любопытное. Похоже, у нас есть материал, с которым интересно поработать. Ну и хайпануть на горяченькой еще теме корутин - как же без этого :)
Для заинтересовавшихся этой темой читателей дам несколько вводных, которых буду придерживаться далее по ходу статьи:
я предполагаю, что вы более-менее знакомы с инструментарием корутин, предоставляемым языком на данный момент. Если необходимо освежить представления, рекомендую к прочтению отличную статью. Также забуриться в тему поглубже можно здесь.
чтобы упростить восприятие примеров с кодом и сэкономить вам время прочтения, я буду опускать квалификаторы и ключевые слова из описаний методов классов/функций. Ссылки на рабочую реализацию я дам в конце статьи.
важным элементом статьи являются комментарии в примерах кода.
Далее - вдумчивый лонгрид. Все-таки ось пишем, а не моргалку ардуиновскую.
Перво-наперво нам понадобится некий синхро-объект, с помощью которого мы будем обмениваться данными между корутиной и внешним миром. Определим его:
#include "co_types.hpp" struct CoSync{ co_mutex_t mutex; // объект с параметрами мьютекса // рассмотрим его подробнее в разделе о CoMutex void* co_addr; // адрес coroutine_handle CoState state; // состояние корутины (выполняется, приостановлена etc.) CoPrio prio; // приоритет base_t id; // уникальный идентификатор base_t size; // размер выделенной для корутины памяти co_act_t expected; // ожидаемое корутиной событие }; // также зададим алиас на указатель объекта CoSync using co_sync_t = CoSync*;
Объект кастомизируемый; мы вольны при развитии проекта расширить его новыми полями данных.
Тогда promise_type корутины может быть определен следующим образом. Опять же, для упрощения чтения я приведу только методы, содержащие логику нашей программы. Минимальный требуемый стандартом набор методов объекта Promise всегда можно посмотреть здесь.
#include <coroutine> #include <limits> #include "co_proxy.hpp" #include "co_alloc.hpp" struct Coro { using promise_type = Coro; CoSync sync { .mutex{.ptr = nullptr, .is_taken = false}, .co_addr{nullptr}, .state{CoState::stopped}, .prio{CoPrio::lowest}, .id{ indexer_t{}() }, // присваиваем уникальный id // в момент создания корутины .size{0}, .expected{std::numeric_limits<co_act_t>::max()}, }; auto get_return_object() { return Coro{}; } std::suspend_never initial_suspend() { // корутина создана, // меняем состояние на "готова" sync.state = CoState::ready; // сохраняем размер выделенной корутине памяти sync.size = CoAlloc::get_current_size(); return {}; } template<co_act_t ID, CoPrio P> auto yield_value ( co_proxy_t<ID, P> p) { struct Awaitable{ /* см. определение ниже */ }; return Awaitable{p}; } template<co_act_t ID, CoPrio P> auto await_transform(co_proxy_t<ID, P> p) { return yield_value<ID, P>(p); } void* operator new(std::size_t sz){ // переопределяем для корутины стандартный // оператор new, стобы аллоцировать память // кастомным аллокатором. Как и где мы хотим. return CoAlloc::allocate(sz); } void operator delete( void* p){ CoAlloc::deallocate(p); } };
Пробежимся сверху вниз и разберем новые типы и функции, встретившиеся в promise_type.
В момент создания корутины мы присваиваем ей идентификатор. Это просто число от 0 до суммарного количества задач, запущенных в программе. Его основное назначение - служить индексом массива, в котором будут храниться указатели на синхро-объекты CoSync. Индексируем мы корутин�� объектом типа indexer_t :
using indexer_t = decltype( []{ static base_t i; return ++i - 1; } );
Извращенство? Возможно, ведь нужный результат можно получить через обычную функцию. Но я сейчас нездорОво фанатею по выражению логики через типы. Типы можно инстанцировать по месту использования, не замусоривая код глобальными переменными. Типы можно пихать в шаблоны, помогая компилятору инлайнить код, перетаскивать часть функционала программы в компайл тайм. Поэтому потерпите чутка:)
Структура Awaitable:
#include <coroutine> struct Awaitable{ // объект proxy при инстанцировании Awaitable в // методе yield_value сохранит значение аргумента p, // переданного ему примитивом синхронизации (эвент, очередь etc.). // proxy - легковесный объект шаблонного типа co_proxy_t(см. ниже), // параметризованный индексом ожидаемого события и приоритетом // он служит каналом передачи инфо между объектом синхронизации // и корутиной. co_proxy_t<ID, P> proxy; // проверяем в объекте proxy параметры мьютекса. // по результатам приостанавливаем корутину или продолжаем // выполнение текущей задачи bool await_ready () { // если мьютекс вообще не захватывался - по умолчанию // приостанавливаемся. if (not proxy.mutex.ptr) return false; // иначе возвращаем флаг захвата мьютекса и действуем по // его значению return proxy.mutex.is_taken; } void await_suspend (std::coroutine_handle<promise_type> coro) { // получаем адрес поля sync из объекта promise корутины co_sync_t sync = &coro.promise().sync; // при первом вызове co_await сохраняем указатель // на sync в диспетчере. co_proxy_t знает о типе // диспетчера, поэтому имеет доступ к его статическим // методам if (CoState::ready == sync->state) decltype(proxy)::store_sync(sync); // последовательно сохраняем в sync: адрес cooutine_handle, // новые параметры мьютекса, новые приоритет и ожидаемое событие. // также меняем состояние корутины на приостановленное sync->co_addr = coro.address(); sync->mutex = proxy.mutex; sync->state = CoState::suspended; sync->prio = P; sync->expected = ID; } // в данной версии оси я пока не решил, что можно и нужно // возвращать через оператор co_yield, поэтому возвращаем пока 0 auto await_resume () { return 0; } };
Как известно, корутины динамически аллоцируют память в куче. Для встроенных решений слово "куча" почти ругательство. Хороший разработчик встроенного ПО, как правило, сам планирует кому, где и сколько выделить памяти. Мы хотим быть хорошими, поэтому реализуем собственный аллокатор. Воспользуемся готовым инструментом из стандарта и заюзаем std::pmr::monotonic_buffer_resource. Он быстрый, принимает в конструкторе указатель на определенный нами фрагмент памяти и имеет необходимые методы (de)allocate() :
// in co_alloc.cpp #include <memory_resource> #include "co_alloc.hpp" byte_t raw_buf[CoParam::CORO_STORAGE_SIZE]; std::pmr::monotonic_buffer_resource mbr{raw_buf, CoParam::CORO_STORAGE_SIZE}; // переменная current_size содержит кэшированное значение // кол-ва байт последней аллокации. // max_size - суммарный размер памяти, аллоцированной всеми // корутинами в программе std::size_t current_size, max_size;
Реализация CoAlloc тривиальна(второстепенные детали опущены):
// in co_alloc.hpp struct CoAlloc{ static void* allocate (std::size_t size); static void deallocate (void *p); static std::size_t get_current_size(); static std::size_t check_memory(); }; // in co_alloc.cpp void* CoAlloc::allocate(std::size_t size){ current_size = size; max_size += size; return mbr.allocate(size); } void CoAlloc::deallocate([[maybe_unused]] void *p){ // метод release() класса monotonic_buffer_resource // высвобождает сразу всю аллоцированную объектом mbr // память. Но так как наши задачи крутятся // в infinite loop, мы вообще не должны сюда попасть. // но если попали, то значит сушим весла. mbr.release(); std::abort(); } std::size_t CoAlloc::get_current_size(){ return current_size; } std::size_t CoAlloc::check_memory(){ return max_size; }
Теперь рассмотрим подробнее механизм взаимодействия корутины с объектами синхронизации.
Точка контакта корутины и события(или таймера, очереди, мьютекса etc.) - это вызов оператора co_await. Через него объект синхронизации передает уже знакомый нам аргумент типа co_proxy_t. Это алиас на следующую структуру:
// in co_proxy.hpp template<co_act_t ID, CoPrio P> struct CoProxyData final : public CoManager { co_mutex_t mutex{ .ptr = nullptr, .is_taken = false }; }; template<co_act_t ID, CoPrio P> using co_proxy_t = CoProxyData<ID, P>;
Как и указывалось ранее, тип co_proxy_t аккумулирует знание о типе диспетчера (структура CoManager, о ней чуть позже), параметрах мьютекса, а также идентификаторе события (шаблонный параметр А) и приоритете (шаблонный параметр P). Но как может выглядить единый интерфейс для всех примитивов синхронизации?
Реализуем базовый класс CoProxy. От него в дальнейшем, используя паттерн CRTP, в компайл тайме унаследуем классы конкретных примитивов.
// in co_proxy.hpp #include "co_util.hpp" #include "co_manager.hpp" template<typename T> class CoProxy : public CoManager { public: using derived_ptr = T*; // алиас указателя на наследуемый класс // метод give() будет передавать в диспетчер идентификатор // наступившего события. метод обеспечивает двусторонний // канал связи - при необходим��сти мы передадим через pack // аргументов необходимые данные источнику события. Пример увидим // в реализации таймера template<typename ...Args> void give(Args&& ...args) { // получаем от класса-наследника id события co_act_t action = derived()->give_impl(co_detail::forward<Args>(args) ...); // обрабатываем его в методе диспетчера set_action(action); } // метод get() формирует из данных контекста и сведений // объекта синхронизации объект типа co_proxy_t, // передаваемый корутине при каждом вызове оператора co_await. template<CoPrio P, typename ...Args> auto get (Args&& ...args) { return derived()->template get_impl<P>(co_detail::forward<Args>(args) ...); } // метод ready() сигнализирует о готовности объекта // синхронизации к определенному действию. // Пример увидим в реализации очереди. bool ready() { return derived()->ready_impl(); } // если объект синхронизации несет полезную нагрузку, // выгружаем ее методом unload(). Смотрим в примере очереди ниже. auto unload() { return derived()->unload_impl(); } private: derived_ptr derived() { return static_cast<derived_ptr>(this); } };
Здесь вы наверняка обратили на конструкцию co_detail::forward<Args>(args). Действительно, пока наша ОС в зачаточном состянии, мы не знаем всех направлений ее развития. Поэтому разумно на этом этапе заложить в ключевом интерфейсе максимум вариативности. Исполним это через инструментарий шаблонов и perfect forwarding. Ну а чтобы не инклюдить сквозь весь проект нехилый такой хедер <utility>, я определил move(), forward() в компактном заголовочнике"co_util.hpp", в нэймспейсе co_detail, благо их реализации рассмотрены во многих источниках(пример).
В принципе я и далее по возможности буду избегать включения в свои заголовочники "тяжелых" хедеров стандартной библиотеки (буду подключать их только в .cpp файлах или использовать в качестве альтернативы свою легковесную имплементацию требуемых инструментов). Цель проста и благородна - сэкономить время себе и потенциальному пользователю на сборку проекта. Понятно, что речь в данном случае идет о секундах, но все-таки...
Настало время разработать примитивы синхронизации. Начнем с очереди. Класс CoQueue может быть определен так:
// in "co_queue.hpp" #include "co_variant.hpp" #include "co_queue_impl.hpp" #include "co_proxy.hpp" template<co_act_t A> class CoQueue final: public CoProxy<CoQueue<A>>{ public: // шаблонный класс CoQueueImpl реализует собственно логику // очереди. Параметризуем его типом полезной нагрузки // размером и типом отвечающим за атомарность операций using co_queue_t = CoQueueImpl<co_payload_t, CoParam::CORO_QUEUE_SIZE, co_critical_t>; // в методе give_impl() помещаем данные в очередь и // возвращаем id данной конкретной очереди. // как помните, в методе give() базового класса этот // id передается диспетчеру для сигнала возобновления // целевой корутины co_act_t give_impl(const co_payload_t& payload) { instance().push(payload); return A; } // конструируем и передаем корутине сведения о // событии, приоритете, параметрах мьютекса (по умолчанию - пустые) // и типе диспетчера template<CoPrio P> co_proxy_t<A, P> get_impl() { return {}; } // проверяем, содержит ли очередь данные bool ready_impl() { return !instance().is_empty(); } // выгружаем очередь co_payload_t unload_impl() { return instance().pop(); } private: // приватным методом instance() при первом конструировании // объекта co_queue создаем статический объект queue_impl // и возвращаем ссылку на него при всех последующих операциях. [[gnu::always_inline]] co_queue_t& instance() { static co_queue_t queue_impl; return queue_impl; } }; // дефайн упрощащющий задание пользовательских типов очередей #define CO_QUEUE(q) using q = CoProxy<CoQueue<__COUNTER__>> /* USER SECTION START */ // в пользовательской секции задаем типы очередей и // и далее инстанцируем и пользуем где необходимо. При этом // каждый вновь созданный объект этого же типа будет // помнить историю операций с ним. CO_QUEUE(spi_queue_t); CO_QUEUE(uart_queue_t); /* USER SECTION end */
С классом CoQueueImpl я вас ничем не удивлю, его реализация на данном этапе разработки ОС элементарна:
// in "co_queue_impl.hpp" #include "critical_section.hpp" #include "co_types.hpp" template<typename P, CoParam D, typename CS> class CoQueueImpl{ public: void push (const P& payload) { CS critical_section; queue[head] = payload; ++head; if (D == head) head = 0; } P pop () { CS critical_section; base_t current = tail; ++tail; if (D == tail) tail = 0; return queue[current]; } P back() { return queue[tail]; } bool is_empty() { return head == tail; } auto& get_instance() { return queue; } private: P queue[D]; base_t head{0}, tail{0}; };
В рассмотренном выше классе CoQueue в качестве элемента очереди задан некий тип co_payload_t. Это алиас на облегченный (отсылка к моему бзику об экономии времени компиляции) аналог std::variant - класс CoVariant. В его основе использован т.н. tagged union. Если вы не знакомы с этой конструкцией, то продемонстрирую основную идею урезанной имплементацей CoVariant ниже. Полную реализацию сможете найти в примере в конце статьи. Пока наш вариант готов принимать только типы uint32_t и void*. Расширение его новыми типами - вопрос аккуратного копипаста. Ну а если вас не тревожит время сборки проекта, его легк�� можно заменить на std::variant.
// in "co_variant.hpp" class CoVariant{ public: CoVariant(const CoVariant& other) : tag(other.tag){ switch(tag){ case Tag::NONE: val = 0; break; case Tag::BASE_T: val = other.val; break; case Tag::VOID_PTR: ptr = other.ptr; break; } } private: enum class Tag{NONE, VOID_PTR, BASE_T}; Tag tag{Tag::NONE}; union{ void* ptr; base_t val; }; };
Интерфейс класса CoMutex следует той же логике, что и рассмотренный ранее класс CoQueue. Существенные детали реализации, связанные именно с функционалом мьютекса, прокомментированы в примере кода:
// in "co_types.hpp" // структура параметров мьютекса struct CoMutexData{ bool* ptr; // указатель на мьютекс bool is_taken; // флаг успешности взятия мьютекса }; using co_mutex_t = CoMutexData; // in "co_mutex.hpp" #include "critical_section.hpp" #include "co_proxy.hpp" template<typename CS> class CoMutexImpl{ public: co_mutex_t get_mutex() { CS critical_section; // если мьютекс свободен, флаг is_taken = true bool is_taken = !mutex; // забираем мьтекс if (is_taken) mutex = true; return {&mutex, is_taken}; } void give_mutex() { mutex = false; } private: bool mutex{false}; }; template<co_act_t A> class CoMutex final : public CoProxy<CoMutex<A>>{ public: co_act_t give_impl() { instance().give_mutex(); return A; }; template<CoPrio P> co_proxy_t<A, P> get_impl() { // передаем корутине параметры мьютекса return {.mutex = instance().get_mutex(),}; } private: using mutex_impl_t = CoMutexImpl<co_critical_t>; [[gnu::always_inline]] mutex_impl_t& instance() { static mutex_impl_t mutex_impl; return mutex_impl; } }; #define CO_MUTEX(n) using n = CoProxy<CoMutex<__COUNTER__>> /* USER SECTION START */ CO_MUTEX(dma_mutex_t); /* USER SECTION END */
Для имплементации таймера мы будем использовать стандартный инструментарий из std::chrono. Но сначала определим наш ресурс локального времени:
// in "co_chrono.сpp" #include <chrono> #include <tuple> struct PlatformClock{ using duration = std::chrono::duration<base_t, std::milli>; using rep = duration::rep; using period = duration::period; using time_point = std::chrono::time_point<PlatformClock, duration>; static constexpr bool is_steady = false; static time_point now() { // пример к статье будет реализован на stm-ке, // поэтому, не мудрствуя лукаво, воспользуемся халовской функцией auto millisecond_tick = HAL_GetTick(); return time_point(duration(millisecond_tick)); } };
Далее определим вспомогательный класс CoChrono:
// in "co_chrono.hpp" struct CoChrono{ // заводим и регистрируем таймер static void set_timer (co_act_t A, base_t delay); // проверяем зарегистрированные таймеры static void check_if_expired(); }; // in "co_chrono.cpp" #include <chrono> #include <tuple> #include "co_proxy.hpp" #include "co_queue_impl.hpp" #include "co_chrono.hpp" using namespace std::chrono; // наследуемся от CoProxy, чтобы иметь возможность // сигналить о наступлении заданного времени class CoChronoImpl final: public CoProxy<CoChronoImpl>{ public: co_act_t give_impl(co_act_t A) { return A; } }; using co_chrono_t = CoProxy<CoChronoImpl>; // задаем тип и удобоваримый алиас регистрационной записи таймера. // она будет содержать id таймера, стартовое время и величину задержки using chrono_entry_t = std::tuple<co_act_t, PlatformClock::time_point, base_t>; // хранить записи будем в очереди; задаем ее тип и алиас using chrono_queue_t = CoQueueImpl<chrono_entry_t, CoParam::CORO_TIMER_NUM, co_critical_t>; // инстанцируем очередь chrono_queue_t chrono_queue; void CoChrono::set_timer (co_act_t A, base_t delay) { // сохраняем запись с установкой времени момента регистрации chrono_queue.push( {A, PlatformClock::now(), delay} ); } // этот метод вызываем в обработчике прерывания // таймера, назначенного в микроконтроллере void CoChrono::check_if_expired() { auto& q = chrono_queue.get_instance(); co_chrono_t chrono; // пробегаемся по очереди for (auto& [act, start_point, delay] : q){ // если задержка не установлена, пропускаем итерацию if (not delay) continue; // считаем пройденное время с момента регистрации таймера auto res = duration_cast<milliseconds>(PlatformClock::now() - start_point).count(); // если время вышло, сигналим диспетчеру и зачищаем поле delay, // чтобы избежать повторного срабатывания if (res > delay) { chrono.give(act); delay = 0; } } }
Теперь мы готовы дать определение класса CoTimer:
// in "co_timer.hpp" #include "co_proxy.hpp" #include "co_chrono.hpp" template<co_act_t A> class CoTimer final : public CoChrono, public CoProxy<CoTimer<A>>{ public: template<CoPrio P> co_proxy_t<A, P> get_impl(base_t delay) { // регистрируем и запускаем таймер set_timer(A, delay); return {}; } }; #define CO_TIMER(n) using n = CoProxy<CoTimer<__COUNTER__>> /* USER SECTION START */ CO_TIMER(app_timer_t); /* USER SECTION END */
Класс CoEvent здесь приводить не буду, он не несет ничего нового к рассмотренному. Его реализацию вы сможете посмотреть по ссылке на пример в конце статьи.
Теперь рассмотрим подробнее диспетчер нашей ОС - класс CoManager и его интерфейс:
// in "co_manager.hpp" struct CoManager{ static void set_action(co_act_t act); static void store_sync(co_sync_t s); static void run(); }; // in "co_manager.cpp" #include <coroutine> #include "critical_section.hpp" #include "co_queue_impl.hpp" #include "co_manager.hpp" // если событие, возобновляющее корутину, это сигнал от мьютекса, // то обрабатываем параметры мьютекса, сохраненные в объекте CoSync корутины // локальной функцией mutex_take() static bool mutex_take (co_sync_t sync); //объявляем локальную функцию, ответственную за возобновление корутины static void co_resume (co_sync_t sync); // на базе разработанного ранее класса создаем очередь // в которую будем складывать указатели на синхрообъекты // готовых к возобновлению корутин using sync_queue_t = CoQueueImpl<co_sync_t, CoParam::CORO_TASK_NUM, co_critical_t>; // создаем массив указателей на синхрообъекты всех корутин программы co_sync_t co_repo[CoParam::CORO_TASK_NUM]; // создаем массив очередей указателей синхрообъектов корутин // получивших сигнал к возобновлению. // наименьший индекс массива соотвтетствует наивысшему приоритету sync_queue_t co_queue_repo[CoPrio::num]; // указатель на синхрообъект корутины, выполняемой в данный момент времени co_sync_t current; // кэшированное значение памяти, выделенной аллокатором для корутин base_t current_memory; void CoManager::set_action(co_act_t act) { // пробегаемся по массиву указателей на синхрообъекты for (auto sync : co_repo){ // если корутина с таким индексом не создана - // пропускаем итерацию if (not sync) continue; // если id наступившего события совпадает с id ожидаемого // события, то помещаем в очередь готовых к возобновлению // в соответствии с назначенным событию приоритетом if(act == sync->expected) co_queue_repo[sync->prio].push(sync); } }; // в методе co_yield() сохраняем указатель // на синхрообъект корутины void CoManager::store_sync(co_sync_t s) { co_repo[s->id] = s; } void CoManager::run(){ // пробегаемся по массиву очередей готовых к выполнению // корутин for (auto& queue : co_queue_repo){ // обрабатываем очередь пока она не опустеет while ( not queue.is_empty() ) { co_sync_t sync = queue.back(); // если в данный момент нет выполняемых корутин // сохраняем указатель из очереди в переменную current // и возобновляем корутину if ( not current || CoState::suspended == current->state ){ current = sync; co_resume(current); // если в данный момент выполняется какая-то корутина // и ее приоритет ниже, чем у данной, то вытесняем ее, // сохранив ее указатель в переменную preemted. // по завершению более срочной корутины, продолжаем выполнение // вытесненной } else if (CoState::running == current->state && sync->prio < current->prio ){ current->state = CoState::blocked; co_sync_t preemted = current; current = sync; co_resume(current); preemted->state = CoState::running; current = preemted; } else { return; } } } } static void co_resume (co_sync_t sync){ // если корутина ждала сигнала от корутины, // но он пока захвачен, то не возобновляемся if( not mutex_take(sync) ) return; // меняем состояние на "выполняется" sync->state = CoState::running; // сбрасываем id ожидаемого события sync->expected = std::numeric_limits<co_act_t>::max(); // в период выполнения метода CoManager::run() прерывания // запрещены, поэтому при возобновлении корутины мы их разрешаем // (чтобы корутина могла быть вытеснена более приоритетной)... co_detail::enable_irq(); std::coroutine_handle<>::from_address(sync->co_addr).resume(); // ... а по завершении - вновь запрещаем co_detail::disable_irq(); } static bool mutex_take (co_sync_t sync){ bool *mutex_ptr = sync->mutex.ptr; // если mutex_ptr != nullptr и мьютекс захвачен // корутину не возобновляем if ( mutex_ptr && (*mutex_ptr) ) return false; // иначе захватываем мьютекс и возобновляем if (mutex_ptr) *mutex_ptr = true; return true; }
Настало время поговорить о переключении контекста в arm cortex-m. На мой взгляд эту тему практически полностью закрыл замечательный материал уважаемого @lamerok. Я сам по ней закрывал белые пятна в своем понимании темы
Если вы не очень разбираетесь в этом вопросе, настоятельно рекомендую проштудировать сначала указанную статью.
Здесь же я ог��аничусь схематичным описанием процедуры пререключения контекста через призму взаимодействия с ОС:
в МК назначаем таймер - источник тиков для ОС (обычно 1 или 10 мс)
в обработчике прерываний этого таймера генерим PendSV request
из обработчика PendSV IRQ, предварительно запретив прерывания и сохранив на стеке "снимок" значений системных регистров вытесненного контекста, вызываем в thread mode метод CoManager::run()
из метода CoManager::run() последовательно, в соответствии с заданным приоритетом, возобновляем корутины. Они могут быть вновь прерваны системным таймером и тогда мы по методу матрешки вновь пробегаемся по п.1 - 4.
из метода CoManager::run() возвращаемся в промежуточную функцию ManagerReturn() в которой генерируем NMI request.
в обработчике NMI IRQ восстанавливаем кадр вытесненного контекста
возвращаемся в вытесненный контекст в thread mode
Ну что ж, вся концепция и теория позади, переходим к примерам. Онлайн можно посмотреть здесь. На трех задачах потестированы все рассмотренные примитивы синхронизации. В демонстрационно-образовательных целях сделал отладочный вывод из корутин; можно понаблюдать порядок вызова методов при их приостановке/возобновлении. Как именно работает пример можно уточнить из комментариев в коде.
Рабочий пример на STM32F412 Discovery можно забрать отсюда. Там сделан акцент на вытеснение задач более приоритетными. task_1 стартует и ожидает event из обработчика прерывания TIM14, запущенного в режиме one pulse mode. Получив event, task_1 возобновляет работу и через 1 секунду загружает данные в очередь task_2. Получив сигнал от очереди, task_2 запускается и вытесняет task_1, так как имеет более высокий приоритет. Также отработав одну секунду, task_2 выгружает значение из очереди, инкрементирует его и загружает в очередь task_3. Последняя по схожему сценарию вытесняет task_2. По завершению работы task_3, возобновляется task_2, а следом и task_1. В конце task_1 рестартует TIM14 и описанный цикл повторяется. Работа задач демонстрируется через светодиоды и отладочный вывод через SWO.
И несколько слов в заключение. В статье описан именно концепт ОС на корутинах. Он работает, но пока опробован на самых простых задачах. Требуется обкатка на разных сценариях, наверняка я что-то зевнул и потребуется существенная доработка и модификация кода. К примеру, в некоторых ситуациях будет полезен механизм наследования приоритетов. Также логика диспетчера сейчас самая примитивная, точно по ходу тестов она будет оттачиваться и усложняться. Буду потихоньку допиливать в свободное от работы время.
Тем не менее, буду рад, если идеи и подходы, изложенные в этой статье вам показались небезынтересными.
Как и всегда, очень рассчитываю на конструктивную критику и встречные идеи.
Спасибо за внимание!