В уроке поговорим о необходимости и способах выполнения задач параллельными процессами. Текст не насыщен строгой информацией, но программы всех дальнейшие уроках будут строиться по принципу описанному здесь.
Предыдущий урок Список уроков Следующий урок
Большей частью программы для Ардуино разрабатываются, как последовательность действий в цикле loop(). Почти все библиотеки Ардуино рассчитаны на работу именно в таком режиме. Существует мнение, что так и должны строиться программы.
Я неоднократно писал об этом. И сейчас опять возвращаюсь к теме параллельных процессов уже применительно к системе STM32.
Мне непонятно, как создавать работоспособные программы без использования параллельных процессов. Вспомним традиционный подход в Ардуино.
Надо нам, например, узнать состояние кнопки. Вызываем функцию, она возвращает результат. Но для обработки сигнала кнопки требуется значительное время, несколько миллисекунд. В течение этого времени основная программа не работает, висит. А если нам постоянно необходимо знать состояние кнопки? Например, мы ожидаем, когда ее нажмут. А если таких кнопок несколько. Нам вызывать их опрос по очереди? Большую часть времени программа будет висеть, пропускать другие события.
Кроме кнопок надо еще что-то делать. А если это работа со сложными узлами, сложными алгоритмами? Для работы с ними есть свои библиотеки-функции, и они тоже подвешивают программу. Стройно написанный код, с четко выделенными последовательными блоками, превращается в алгоритмически-временной мусорник. Необходимо предсказать все варианты, когда что может сработать или не сработать, где программа подвиснет, на какое время, и как это все отразится на работе алгоритма в целом.
Сложная система, созданная по такому принципу, будет выглядеть, как школьник, который делает домашнее задание и смотрит по телевизору кинофильм. Фильм периодически подвисает из-за плохого уровня сигнала, прерывается рекламой. Школьнику приходят СМС, он отвечает. В результате он не способен выполнить все действия. Если какие-то из них можно отложить на время, например, домашнее задание, то просмотр фильма по телевизору остановить нельзя. В общем, он что-то обязательно пропустит, что-то сделает не вовремя, что-то сделает неправильно.
Выход – обработка задач параллельными процессами.
Выполнение задач параллельными процессами в прерывании от таймера.
Параллельные процессы – это действия, которые выполняются одновременно.
Конечно, строго одновременно задачи можно выполнять только на мультипроцессорных системах. Но реальные задачи не требуют для работы всего процессорного времени.
В предыдущем уроке мы обрабатывали сигнал кнопки. Вызывали функцию scanAverage(), она считывала состояние входа, делала какие-то операции и возвращала управление в программу. Все это занимало совсем немного времени. Дальше нам надо было подождать в течение 1 мс. Для этого мы вызывали функцию задержки, в которой программа и висела большую часть времени.
while (1) {
// выполнение алгоритма программы
button.scanAverage(); // метод обработки сигнала
HAL_Delay(1);
}
Почему бы не использовать время ожидания для выполнения основного алгоритма.
Сделать это можно с помощью следующей организации программы.
while (1) {
// выполнение алгоритма программы
}
// прерывание по таймеру 1 мс
void TIM1_UP_IRQHandler(void) {
button.scanAverage(); // метод обработки сигнала
}
К основному циклу мы добавили обработчик прерывания по таймеру с периодом вызова 1 мс.
Таймер - это аппаратный узел микроконтроллера. Он вырабатывает аппаратные прерывания.
Аппаратное прерывание – это сигнал о каком-то событии. При появлении сигнала выполнение программы останавливается, и управление переходит к обработчику прерывания. После завершения его работы управление возвращается в код программы, на то самое место, где она была прервана.
С точки зрения программы прерывание – это вызов функции по внешнему, не связанному непосредственно с программным кодом, событию.
Таймер может быть запрограммирован так, что сигнал прерывания с него приходит циклически, с заданным периодом. Значит, циклически будут происходить его прерывания.
Т.е. мы добавили в программу функцию, которая сама по себе вызывается каждую 1 мс. В эту функцию поместили метод обработки сигнала scanAverage(). Таким образом, в основной программе мы можем постоянно выполнять необходимые действия, которые с периодом 1 мс будут прерываться на короткое время для обработки сигнала кнопки. А о состоянии кнопки можно судить по программным признакам, например button.flagLow.
Обработка сигнала кнопки в этом случае происходит в фоновом режиме, т.е. незаметно для основной программы.
Если нам требуется работать с несколькими кнопками, то в одном обработчике прерывания разместим несколько функций scanAverage() для разных объектов.
// прерывание по таймеру 1 мс
void TIM1_UP_IRQHandler(void) {
Button1.scanAverage();
Button2.scanAverage();
Button3.scanAverage();
}
В основной программе будем работать с признаками объектов button1.flagLow - button3.flagLow.
Я привел самый простой пример – работа с кнопкой.
При использовании более сложных объектов эффективность выполнения задач в фоновом режиме может быть гораздо значительнее.
Примеры из Уроков Ардуино.
Для управления шаговыми двигателями есть стандартная библиотека Stepper. Вызываешь функцию step, в которой указываешь нужное число шагов, и шаговый двигатель отрабатывает задание. Только программа на это время зависает. Ничего больше делать невозможно. Невозможно даже остановить двигатель по нажатию кнопки. Как определить, что кнопка нажата, если программа занята переключением фаз.
У меня есть Ардуино-библиотека StepMotor. Она работает параллельным процессом. В прерывании по таймеру с заданным периодом вызывается функция control(). В основной программе достаточно только сформировать команду на отработку нужного числа шагов и заниматься другими задачами. Двигатель будет вращаться сам по себе.
//--------------------------------- обработчик прерывания 1 мс
void timerInterrupt() {
myMotor.control(); // управление двигателем
}
Можно управлять нескольким двигателями. Все они будут работать независимо.
Как с помощью библиотеки Stepper разработать систему управления нескольким шаговыми двигателями – ума не приложу. Особенно если еще требуется анализировать состояние датчиков, отрабатывать временные задержки, выводить информацию на дисплей и т.п.
Не удержусь еще от одного примера – достаточно сложной библиотеки для работы с сетевым протоколом ModBus. Данных для обмена бывает много, не исключены ошибки сети, отсутствие подтверждения пакета и т.п. Процесс передачи или приема данных может затянуться, непредсказуемо оборваться.
Я разработал Ардуино-библиотеки Tiny_ModBusRTU_Slave и Tiny_ModBusRTU_Master для работы с ModBus-сетями. Библиотеки работают параллельными процессами. В обработчике прерывания вызывается функция.
// обработчик прерывания 500 мкс
void timerInterrupt() {
slave.update();
}
На этом разработка ведомого ModBus устройства заканчивается. В основной программе создается массив данных. Он доступен по локальной сети для ведущего устройства. В распределенной системе появляется общий массив данных, с которым могут работать как ведомое, так и ведущее устройства. Обмен происходит в фоновом режиме, незаметно для основной программы.
У меня есть библиотека для управления семисегментными светодиодными индикаторами Led4Digits. Писали, что это единственная подобная библиотека Ардуино, при использовании которой сегменты разрядов светятся равномерно. Она управляет индикаторами, подключенными непосредственно к микроконтроллеру, без сдвиговых регистров и других дополнительных микросхем. Управление динамическое, регенерация должна происходить в строго равные промежутки времени. Любые диспропорции времени переключения разрядов приводят к неравномерности свечения.
Временной стабильности удается добиться только вызовом функции регенерации в прерывании по таймеру.
Это еще одно достоинство организации параллельного выполнения задач таким образом. А именно, вызов обработчика происходит в строго заданные промежутки времени и не зависит от хода выполнения основного алгоритма программы.
Программные таймеры.
Если мы разместим в обработчике прерывания команду инкремента переменной.
// прерывание по таймеру 1 мс
void TIM1_UP_IRQHandler(void) {
timer++;
}
То получим программный таймер. В нашем случае его значение будет увеличиваться каждую миллисекунду, он будет отсчитывать миллисекунды. Значение таймера мы всегда можем считать в основной программе, установить или сбросить.
Таким образом, с помощью одного аппаратного таймера можно создать несколько программных.
Ничего не мешает от таймеров, в том числе программных, синхронизировать и части программы в основном цикле. Т.е. параллельные процессы можно организовывать не только в обработчике прерывания, но и в основном цикле.
// основной цикл программы
while(1) {
// блок вызывается каждые 10 мс
if(timer >= 10 ) {
timer=0;
// выполнение кода блока
}
}
// прерывание по таймеру 1 мс
void TIM1_UP_IRQHandler(void) {
timer++;
}
Иногда такой вариант предпочтительнее, особенно если обработка задачи занимает длительное время. В этом случае не блокируются остальные аппаратные прерывания. Длительность выполнения обработки прерывания должна быть минимальной.
Можно сложным образом разбить основную программу по отдельным временным интервалам, как это сделано в уроке 38 Ардуино при разработке контроллера элемента Пельте.
//------------------------------- основной цикл -------------------------------
loop() {
//------------------------------- цикл 20 мс ( регулятор мощности )
if (flagReady == true) {
flagReady= false;
// операции цикла 20 мс
//--------------- выполнение распределенных операций в цикле 1 сек
cycle20mcCount++; // счетчик циклов 20 мс
if (cycle20mcCount >= 50) cycle20mcCount= 0; // время цикла 1 сек
if (cycle20mcCount == 0) {
//операции интервала 0
}
if (cycle20mcCount == 1) {
//операции интервала 1
//инициализации измерения температуры
}
if (cycle20mcCount == 25) {
//операции интервала 25
}
if (cycle20mcCount == 48) {
//операции интервала 48
//чтение температуры
}
}
}
В программе существует одно прерывание по таймеру с периодом 2 мс. От него формируется признак flagReady с периодом 20 мс. По этому признаку синхронизируется программа в основном цикле. Добавляется счетчик до 50, т.е. программный таймер с периодом 1 сек. И внутри этого цикла распределены разные задачи по 20 миллисекундным интервалам.
Хороший стиль использовать в программе только одно прерывание от аппаратного таймера и синхронизировать от него все регулярные процессы. В этом случае программа получается полностью синхронной, предсказуемой. Программные блоки не конфликтуют между собой, как это может случиться при нескольких аппаратных прерываниях.
Этой теме посвящены Уроки Ардуино 10 и 11. Лучше просмотреть их. Хотя, я практически в каждом уроке Ардуино подчеркиваю, что было реализовано параллельным процессом.
Квалификатор Volatile.
Еще хочу напомнить о существовании квалификатора volatile. Я не буду подробно рассказывать о нем. Если не знаете, что это такое, обязательно посмотрите в уроке 10 Ардуино.
Очень коротко. Если переменная изменяет свое состояние в обработчике прерывания, то компилятор не способен предсказать эту операцию. Он может оптимизировать программу с учетом того, что переменная, по его мнению, никогда не изменяется. Например, выбросить в коде сравнение с переменной. Зачем ее с чем-то сравнивать, если она всегда постоянна.
Для того чтобы сообщить компилятору о том, что код работы с переменной оптимизировать не надо, существует специальный квалификатор. Он используется при объявлении переменных.
volatile uint32_t timer; // программный таймер
Теперь компилятор не будет пытаться оптимизировать вычисления, связанные с переменной timer.
В следующем уроке научимся устанавливать конфигурацию таймеров STM32 в режиме счетчиков, узнаем, как работать с их прерываниями.
// прерывание по таймеру 1 мс
void TIM1_UP_IRQHandler(void) {
Button1.scanAverage();
Button2.scanAverage();
Button3.scanAverage();
}
но всё-равно же сначала опросится Button1, потом Button2, затем Button3? по очереди же?
—————————
«Можно управлять нескольким двигателями. Все они будут работать независимо.»
так что ли?:
/——————————— обработчик прерывания 1 мс
void timerInterrupt() {
myMotor1.control(); // управление двигателем1
myMotor2.control(); // управление двигателем2
myMotor3.control(); // управление двигателем3
}
три двигателя начнут одновременную работу каждый со своими параметрами? или будет задержка (на время вызова функции) в старте второго и третьего?
Здравствуйте!
Ничего в мире не бывает одновременно. Конечно, задержка будет. Но всего несколько микросекунд. Ее невозможно будет увидеть на реальных двигателях.
Здравствуйте, спасибо за ваши уроки. Есть у вас примеры программ управления несколькими шаговыми двигателями?
Здравствуйте!
Задаете 2 объекта StepDirDriver. В прерывании вызываете 2 метода control для обоих объектов. И все.
Например, 2 двигателя используются в этом проекте
http://mypractic-forum.ru/viewtopic.php?t=131
Я на стм32 только перехожу. Нужно сюда установить вашу библиотеку для шаговика от ардуино? где её взять, как подключить? Спасибо.
Где взять библиотеку StepDirDriver нашёл. Остался вопрос, она также и на стм32?
Здравствуйте!
Нет, библиотека StepDirDriver только для Ардуино. Сейчас я возобновил писать уроки STM32. Собираюсь подключать к нему и шаговые двигатели, но когда до этого дойдет — не знаю.
Остается ждать. Думаю я не один, кого интересует эта тема. Пока буду сам пытаться чтото сделать. Надо управление по трём осям, на чпу. Спасибо.
Здравствуйте,
Из примера Павела, что будет если функция TIM1_UP_IRQHandler(void) не закончит свою работу за 1 мс?
Здравствуйте!
Либо прерывание пропустится, если флаг переполнения таймера будет сброшен при выходе из обработки прерывания. Либо после выхода из прерывания тут же возникнет новое, если флаг таймера сброшен в начале обработки прерывания.
Здравствуйте!
А сформулируйте задачу формально. Например:
— есть дискретный датчик;
— выход такой-то;
— еще один выход такой-то.
Устройство ждет срабатывания датчика — выходы отключены.
Датчик сработал — активный выход 1 в течение заданного времени. И т.д.
volatile uint23_t timer; // программный таймер
Поправьте, пожалуйста;)
Да, конечно. спасибо.
Всегда, когда идёт речь о (квази)параллелизме, нужно вспоминать о разделяемых ресурсах и задаче взаимного исключения. В данном простом примере каких-то смертельных последствий от изменения переменной timer сразу в двух местах не наступит, конечно (возможен, разве что, небольшой «дрифт времени»), но в общем случае так лучше не делать.