Введение
Как быть, когда надо делать много разных задач в одном контроллере? Отрисовать меню на экране, моргать лампочкой, управлять каким-то механизмом, проверять датчики, и всё это одновременно. Дел море, а контроллер один!
Обучение часто начинается с линейных бесконечных однозадачных программ: моргание светодиодом или что-то такое. Поменяли состояние, подождали, снова поменяли, снова подождали. И так далее, довольно однообразно и сложно сюда добавить что-то ещё. Добавим опрос датчика — и светодиод станет моргать уже не так. Было 500 мс выключенного и 500 мс включенного, а станет 530 мс выключенного и 500 мс включенного, например. Или и того хуже.
Эта схема не годится для многозадачных применений. Есть исключение, конечно, — использование операционных систем. Там можно сделать несколько таких циклов с паузами, и задержки в одном не будут влиять на другой. Но это потом.
Хорошим наглядным примером скорости работы контроллера являются всякие индикаторы, состоящие из множества светодиодов: семисегментные индикаторы, светодиодные информационные панели, табло, вывески. Очень часто в них применяется динамическая индикация: кажется, что они все светятся целиком и полностью, но по факту это не так. Там могут быстро-быстро управляться сегменты по-отдельности: сначала одна цифра посветится, потом вторая, потом третья, потом четвёртая и дальше по кругу. Из-за инерционности глаза этого не видно, но индикаторы мерцают. Если подвигать ладонью перед глазами, глядя на индикатор или панель, бывает видно, что она светится частями.
Скорость электроники позволяет прокручивать такие фишки. Управление яркостью освещения часто работает на том же принципе: светодиоды не инерционны и включаются и выключаются вместе с током, проходящим через них. Можем регулировать не ток напрямую, а соотношение состояний включено-выключено. Всё время включено — полная яркость. Половина времени включено, половина выключено — условно половинная яркость, Всё время выключено — света нет. Хотя, конечно, зависимость там не линейная, но принцип понятен. Если такие переключения будут происходить очень часто, 50 раз в секунду или выше, то колебаний видно не будет, свет будет восприниматься равномерно-приглушённым. Происходит интегрирование, так сказать.
У нагревателей инерционных частота может быть и ещё ниже, даже меньше 1 герца, и он всё равно будет воспринимать это как плавное регулирование.
Многозадачность
В чём суть многозадачности? Контроллер очень быстрый и может по очереди обработать все задачи так, что будет казаться, что они работают одновременно. Например, опрос датчика занимает 10 мс, переключение светодиода 10 мкс, отрисовка экрана 20 мс, проверка кнопок 50 мкс, какая-то логика при нажатии кнопок ещё сколько-то мс. В сумме немного.
Цель: разнести задачи по времени, чтобы все успевали выполняться и не мешали другим.
Есть много способов разрулить время выполнения между задачами.
Первый способ, очевидный: просто последовательно прописать все задачи в одном главном цикле и они будут выполняться безусловно по очереди. Для ряда задач можно сделать так, но минус: задачи все выполняются с одинаковой частотой, надо ли это или не надо. Можно, конечно, понавертеть счётчики, чтоб выполняться каждый второй-третий-пятый-десятый раз, но выйдет в итоге сложно и непонятно.
while(true) {
send_spi(); // Отправляем и получаем данные от датчика
process_data(); // Обрабатываем данные
check_button(); // Проверим кнопки
update_screen(); // Обновим экран
}
Второй способ, один из простых и понятных, — использование программных таймеров. Идея такая: берётся аппаратный таймер, счётчик, что угодно, и по нему отсчитывается реальное время с отсчётами со сравнительно высокой точно известной частотой: 1-2-4 кГц. И есть таблица задач, составляемая или статически, или по запросам от кода: какую функцию как часто надо вызывать. Раз в секунду или два, или двадцать, или двести...
Вычисляем при добавлении в таблицу, сколько для какой задачи надо пропускать срабатываний счётчика, чтоб частота вызовов соответствовала заданной. И каждый раз при отсчёте таймера пробегаемся по всей таблице и выполняем те задачи, чей срок пришёл.
Тогда вся логика разделится на функции, вызываемые с нужной частотой. И неважно, сколько там занимают по времени остальные задачи, если это адекватное время, то примерно частота вызова будет удерживаться. Захотели уменьшить частоту обновления экрана или моргания светодиода — достаточно поменять одно число при добавлении в таблицу таймеров. Задачи стали независимыми и их стало возможно добавлять при инициализации или по ходу дела. И удалять тоже. Удобно!
void LedBlink() {
// Меняем состояние светодиода на обратное (вкл - выкл)
Led.Toggle();
}
...
// Добавим в таблицу программных таймеров задачу на моргание светодиодом с частотой 5 мыргов в секунду (10: 5 вкл и 5 выкл)
timer_Add(10, LedBlink);
// Добавим в таблицу программных таймеров задачу на обновление экрана 2 раза в секунду
timer_Add(2, UpdateScreen);
...
Реализаций программных таймеров в интернете достаточно много для любых контроллеров.
Можно совместить эти методы.
Это были чисто алгоритмические подходы с минимальным влезанием в модули контроллера. Есть и другие методы, возможно, вы найдёте какой-то более удобный для вас или придумаете новый.
Прерывания
Но у контроллеров и процессоров, любых, есть специальная система параллелизации для важных задач — прерывания. То есть при наступлении какого-то события: таймер досчитал, случилась ошибка выполнения, принят был байт, на ножке изменился логический уровень и т.д. сразу же происходит вызов специальной функции, а основной поток выполнения на это время останавливается. Функции эти обычно короткие, для реагирования на события реального времени, которые нельзя пропустить: изменилась полярность фазы в сети 220 и надо бы переключать сигнал управления тиристором, сработал концевой датчик или защита и надо бы останавливать двигатель... примеров можно привести море, да и не такие важные задачи можно на них повесить, просто для удобства.
// Прерывание. Функция вызывается каждый раз, когда на определённом выводе микросхемы меняется логический уровень, используется для синхронизации
void Exti_Interrupt() {
// Запустим таймер управления нагрузкой
StartPWMTimer();
}
Оптимизация
Некоторые задачи часто выполнять смысла нет: изменения происходят медленнее. Например, нет смысла менять данные на экране чаще, чем несколько раз в секунду. Для видео нужен высокий FPS, но вот для чисел и текста он вреден, всё просто смажется.
Некоторые длинные задачи можно разбить на этапы: сделал первый кусок, дал поработать остальным, второй кусок, отдохнул, третий, и так далее.
Можно пропускать выполнение задачи, если ничего не поменялось.
Но! Начинаем оптимизацию с логики и планирования алгоритмов. Лезть в оптимизацию кода вроде "это сэкономит мне 2 такта на итерацию цикла, если я запишу это как-то хитровывернуто" не стоит. Сначала бьёмся за логику и понятность, и если уж будет не хватать скорости, то тогда уже думать, что там не так. При нормальном планировании программы у контроллера остаётся обычно достаточно свободного времени и ресурсов (не занятого срочными делами), чтоб можно было расширяться по функционалу.
Спихивание задач на спецмодули
В контроллерах существуют специальные модули, некоторые из них никак себя снаружи даже и не проявляют и работают только с памятью или вообще сами в себе. Но зато они заточены на ряд специализированных задач, которые могут выполняться параллельно основному коду, иногда даже и быстрее, чем если бы этот алгоритм был реализован программно.
Модули эти могут заниматься шифрованием и хешированием данных, вычислением случайных чисел, копированием памяти, ускорять графику, выполняя какие-то специфические для графики задачи: заливка цветом, копирование изображений со сменой цветового пространства, и всякое такое.
И если в маленьких контроллерах выгода будет не очень большая, то крупных процессорах на таких модулях ускорения крутится много что: обработка, сжатие и разжатие видео и аудио, ускорение графики, ускорение и параллелизация вычислений, выполнение нейросетей и всякого такого, что на процессоре будет работать очень медленно. Такие ускорители есть и в планшетах/телефонах, и в настольных компьютерах и ноутбуках, почти везде. Их нет разве что в самых маленьких и простых контроллерах.
В некоторых контроллерах есть блоки с программируемой логикой, где можно реализовать в некоторой мере модули со своей архитектурой (серии PSoC от Cypress, например, но не только), спрятав часть цифровой схемы внутрь контроллера. Это помогает уменьшать размеры плат и упрощать изменение функционала прибора. Проще поменять всё же программно схему, чем делать новую печатную плату...
Это очень мощный функционал, который не стоит оставлять в стороне.
И получается, что от задачи остаются управляющие и регулирующие функции: поставить задачу модулю и проверить её выполнение. Всё. Это экономит время и процессор может заниматься другими делами.
Заключение
Комбинацией программных и аппаратных средств и алгоритмов можно реализовать одновременное исполнение самых разных задач на одном устройстве.
Когда отдельные операции занимают мало времени, в одну секунду их можно впихнуть сотни и тысячи.
Начать, кстати, можно с динамической индикации и семисегментных индикаторов и кнопок. По нажатию кнопки меняем числа на экране. Если получится сделать подобное устройство — вы молодцы!
Если вспомнить, что кнопки и датчики наличия события — всё же очень близкие по интерфейсу вещи.
Это же уже можно собрать и счётчик событий (оборотов колеса, измеритель расстояния, витков, проходов через дверь), и генератор случайных чисел для игр, и таймер, и часы, и много чего ещё. Казалось бы, самое начало пути, а сколько всего можно сделать!