Comments 24
и выполняться _ строго определенное заранее запланированное время — две большие разницы.
Здесь именно _в_ строго определенное заранее запланированное время. Т.е. процедура прерывания будет вызываться точно каждые X микросекунд независимо от того, сколько будет выполняться код обработчика по времени (конечно, если код обработчика уместится в период, если он не умещается, то поведение не определено, ну или нужно подробно изучить документацию на контроллер, чтобы посмотреть, что он делает в такой ситуации). По этой же причине замерять время выполнения Serial.print здесь не обязательно — главное, чтобы он вместился в период (в 1 секунду из примера он вмещается наверняка, для более высоких частот уже может не уместиться).
>К тому же — о смене приоритетов выполнения процедур, вызванных прерываниями, насколько я понимаю, не может идти и речи.
В описанном случае у нас получается всего 2 процедуры: это главный поток, который пополняется в главном цикле с низким приоритетом, и «прерывистый» поток обработчика прерывания с высоким приоритетом — он обрывает главный поток точно по расписанию, но не должен занимать слишком много времени за каждый вызов. Вообще, можно запустить и несколько таймеров одновременно на платах, где их несколько (передать в timer_init_ISR другой ID таймера), но что будет происходить, если их обработчики начнут пересекаться по времени, я пока не разбирался.
>И ещё вопрос — мы замыкаем цепь enable с микроконтроллера. В то же время сигнал enable обязан приходить на драйвера в случае безопасной эксплуатации устройства, в котором вращается двигатель. Не ошибка ли это? И почему En=low to enable?
Так устроен драйвер step-dir: EN=LOW двигатель вращается по сигналу, EN=HIGH — не вращается в любом случае, проверено экспериментально. Вероятно, это сделано для того, чтобы можно было подключать этот пин к концевым кнопкам на станке, которые при нажатии замыкают цепь (хотя, никто не мешает поставить кнопку, работающую на разрыв, не знаю).
вот, нашел в даташите:
> nENBL: Logic high to disable device outputs and indexer operation, logic low to enable. Internal pulldown.
www.pololu.com/product/2133
www.pololu.com/file/download/drv8825.pdf?file_id=0J590
Как правило все разрешающие сигналы — низкого уровня, это пошло ещё с тех времён когда логика была TTL и неподключенный вход имел положительный потенциал по конструктивным причинам.
— независимые процессы с возможностью задать приоритет для каждого
— таймеры (произвольное количество)
— средства межпроцессного взаимодействия: очереди, семафоры и т.д.
Например, FreeRTOS есть как под Arduino (https://github.com/greiman/FreeRTOS-Arduino), так и под голый AVR (http://www.freertos.org/AVR_IAR.html).
Ищется в менеджере библиотек по словам task, scheduler, thread. Например, Tasker, он наверняка менее гибок, зато более, хм, Arduino-style и user-frendly.
— В статье и примерах речь идёт об одном таймере. Что насчёт работы одновременно нескольких таймеров? Можно добавить пример кода в библиотеку?
— Что произойдёт, если код не успеет выполниться до следующего прерывания? Насколько катастрофическими будут последствия?
— Планируется ли поддержка других платформ, например, ESP8266?
Можно. Написал на скорую руку скетчик, пожалуй, добавлю его в библиотеку
#include"timer-api.h"
void setup() {
Serial.begin(9600);
timer_init_ISR_1Hz(_TIMER2_32BIT);
timer_init_ISR_2Hz(_TIMER4_32BIT);
}
void loop() {
Serial.println("Hello from loop");
delay(5000);
}
void timer_handle_interrupts(int timer) {
if(timer == _TIMER2_32BIT) {
static unsigned long prev_time = 0;
unsigned long _time = micros();
unsigned long _period = _time - prev_time;
prev_time = _time;
Serial.print("goodbye from timer1: ");
Serial.println(_period, DEC);
} else if(timer == _TIMER4_32BIT) {
static unsigned long prev_time = 0;
unsigned long _time = micros();
unsigned long _period = _time - prev_time;
prev_time = _time;
Serial.print("goodbye from timer2: ");
Serial.println(_period, DEC);
}
}
Результат:
Hello from loop
goodbye from timer2: 551004
goodbye from timer1: 1051004
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
Hello from loop
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
Hello from loop
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
goodbye from timer2: 474098
goodbye from timer1: 1000000
goodbye from timer2: 525902
Hello from loop
Только нужно учитывать, что на разных чипах разное количество таймеров и могут быть нюансы с именами и разрядностью. Например, на моей Arduino Leonardo всего один таймер (или по крайней мере, в Stepper.h, из которой я брал базовый код, выносился только один) и на ней этот пример не запустится. На PIC32 (пример выше запущен на ней) — 5 16-битных таймеров, 2 пары из которых можно объединить в 32-битные. На SAM/ARM Arduino Due вообще все 9 32-битных таймеров.
Какие есть таймеры на каких платформах написано в разделе «Выбор таймера» github.com/sadr0b0t/arduino-timer-api и можно посмотреть в исходниках в файле timer_setup.c для каждой платформы.
>— Что произойдёт, если код не успеет выполниться до следующего прерывания? Насколько катастрофическими будут последствия?
В комментариях уже рассказали. Нужно читать документацию на контроллер. Библиотека — не более, чем обертка вокруг кода, запускающего таймеры на разных контроллерах.
>— Планируется ли поддержка других платформ, например, ESP8266?
Я портировал библиотеку на платы, которые
1) были у меня под рукой для тестирования
2) поддерживались инфраструктурой Ардуино (там внутри ардуиноспецифического кода, если не считать, примеры, практически нет, но структура проекта сделана под структуру библиотеки Ардуино и вообще я пока сижу на ней)
Если эту плату можно шить из среды Ардуино и она когда-нибудь у меня появится, то, возможно, портирую и на нее.
Вообще, сделать порт для новой платформы не очень сложно
В дереве проекта для каждой архитектуры выделен отдельный каталожек, в каждом из них всего два файла: timer_setup.c и timer_freq.c
В timer_setup.c основной платфомо-зависимый код — в него нужно разместить реализации методов timer_init_ISR(int timer, int prescaler, unsigned int adjustment) и timer_stop_ISR(int timer), а также назначить значения констант для таймеров _TIMERXX и делителей частоты TIMER_PRESCALER_1_Y — на разных платформах они будут разные.
Файл timer_freq.c на вид большой, но код туда добавляется механистически — нужен только калькулятор и немного терпения. В нем подбираются значения prescaler+adjustment, чтобы получить нужную частоту таймера на чипе с известной базовой тактовой частотой. Это реализация всех вызовов timer_init_ISR_XYHz
Для timer_setup.c я во всех случаях брал за основу код Servo.cpp из стандартной библиотеки Ардуино (он тоже сделан на таймерах и портируется отдельно на каждую платформу из семейства Ардуино) и доводил его, поглядывая в даташит на контроллер в раздел про таймеры.
Вот, например, для архитектуры SAM (ARM)
github.com/sadr0b0t/arduino-timer-api/tree/master/src/sam
здесь примерно видно, как шел процесс портирования:
github.com/sadr0b0t/arduino-timer-api/issues/5
здесь процесс портирования на AVR/Ардуино:
github.com/sadr0b0t/stepper_h/issues/4
#include"timer-api.h"
void setup() {
Serial.begin(9600);
timer_init_ISR_1Hz(_TIMER2_32BIT);
delay(250);
timer_init_ISR_2Hz(_TIMER4_32BIT);
}
void loop() {
Serial.println("Hello from loop");
delay(5000);
}
void timer_handle_interrupts(int timer) {
if(timer == _TIMER2_32BIT) {
static unsigned long prev_time = 0;
unsigned long _time = micros();
unsigned long _period = _time - prev_time;
prev_time = _time;
Serial.print("goodbye from timer1: ");
Serial.println(_period, DEC);
} else if(timer == _TIMER4_32BIT) {
static unsigned long prev_time = 0;
unsigned long _time = micros();
unsigned long _period = _time - prev_time;
prev_time = _time;
Serial.print("goodbye from timer2: ");
Serial.println(_period, DEC);
}
}
и получил ровненькие периоды для обоих таймеров в отладке:
Hello from loop
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
Hello from loop
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
Hello from loop
goodbye from timer2: 500000
goodbye from timer1: 1000000
goodbye from timer2: 500000
goodbye from timer2: 500000
https://hi-lab.ru/arduino-mega-server/ams-pro/projects/garden
одного-двух таймеров, даже реального времени, будет недостаточно. Но ваша библиотека пригодится в некоторых специфических случаях. Кстати, в этом проекте (кроме всего прочего) 7 сервоприводов управляются в реальном времени без помощи прерываний.
На Arduino Due с чипом SAM/ARM, полагаю, можно запустить все 9 таймеров одновременно, но, пожалуй, будет сложно избежать чехарды с наложениями прерываний (видно, как на нее легко нарваться уже с 2мя таймерами).
Прикольный проектик. Реально где-то внедрено или в процессе разработки?
С помощью одного таймера можно сделать сетку импульсов с достаточной частотой и вести по ней несколько независимых «потоков»
Это совсем другое дело — в таком виде этому можно найти много полезных применений. Главное не превышать интервал до следующего прерывания.
Прикольный проектик. Реально где-то внедрено или в процессе разработки?
Там все проекты реальные. Конкретно этот уже несколько месяцев работает в режиме 24/7 (время от времени модернизируется в рабочем порядке перезаливкой прошивки).
Я бы не рекомендовал помещать в него вызовы блокирующих задержек delay, циклы с неизвестным заранее количеством итераций, любые другие вызовы с плохо предсказуемым временем выполнения (в том числе Serial.print).
s/не рекомендовал/категорически запрещал/
Generally, an ISR should be as short and fast as possible. If your sketch uses multiple ISRs, only one can run at a time, other interrupts will be executed after the current one finishes in an order that depends on the priority they have. millis() relies on interrupts to count, so it will never increment inside an ISR. Since delay() requires interrupts to work, it will not work if called inside an ISR. micros() works initially, but will start behaving erratically after 1-2 ms. delayMicroseconds() does not use any counter, so it will work as normal.
Внутри ISR прерывания запрещены, и некоторые функции будут работать неправильно.
А в заголовке же «таймеры без боли»…
Я вот взял SoftTimer: для задач, где нет необходимости точно соблюдать тайминги — как раз без боли и страданий
затем аккуратно передавать управление в основную программу так, что она вообще ничего не заметит
Это только если все операции, что вы выполняете — атомарные. К примеру работа с GPIO: на некоторых платформах есть специальные регистры установки и сброса значений, что позволяет выполнить операцию за один такт, а на других есть просто регистр состояния выводов — и там нужно
- Вычитать значение регистра
- Применить битовую операцию по маске (1-2 такта)
- Записать значение регистра.
А теперь вопрос: что будет, если один и тот же банк GPIO изменяется из этого красивого "таймера" во время модификации оного из основной программы? Напомню обработчик прерывания автоматически сохраняет контекст операции, но не отменяет ее или не отбрасывает ее к началу. Одним из вариантов является отключение прерываний во время таких операций.
А вообще если нужен гиперкроссплатформенная "многозадачность" — то есть Protothreads, которые, при должном понимании принципов работы, запустятся на любом Си-компиляторе и не требуют "нувообщеничего" дополнительного для работы.
есть Protothreads, которые, при должном понимании принципов работы, запустятся на любом Си-компиляторе и не требуют «нувообщеничего» дополнительного для работы.
О да! Ключевое здесь «при должном понимании принципов работы», а вывих мозга они дают быстро и надёжно :) Важно понимать, что это не полноценный планировщик, и способов выстрелить себе в ногу просто куча.
Нужно самому следить за передачей управления: если тред застрянет в бесконечном цикле, никто его прервать не сможет. Нет полноценного сохранения контекста: забыл объявить переменную в треде как static — она сбросится. Очень сложно отлаживать: всё на макросах, и порядок выполнения кода совсем не очевиден.
А в остальном — всё просто отлично: кроссплатформенно, очень быстро, почти без накладных расходов. Инструмент для настоящих ниндзя.
способов выстрелить себе в ногу просто куча.
Обычно они происходят при попытке скрестить ужа с ежом, т.е. обычный "синхронный код" и эти машины состояний. Рекурсивый вызов прототредов тоже тот еще гемор.
если тред застрянет в бесконечном цикле
На такой случай есть PT_YIELD(&self->pt); который будет стоять в начале или конце цикла. Но в безконечном цикле вообще любая программа может застрять, тут вопрос дизайна.
Очень сложно отлаживать
Тут согласен, особенно на микроконтроллерах, где количество точек останова ограниченно аппаратно.
забыл объявить переменную в треде как static
Я вообще ярый противник любых static внутри функций. Все, что нужно конкретному "потоку" выносится в отдельную структуру, в которой также и лежит struct pt, с таким подходом можно даже сделать несколько однотипных потоков.
всё на макросах
Отдать должное создателю библиотеки, они все легкочитаемы и понятны, чего не скажешь, к примеру, про тучу макросов в STM32 HAL.
Вообще Protothreads — это обычный switch-case. Меня больше напрягает, когда люди осознанно его велосипедят вместо того, чтобы взять готовую либу. Единственное исключение — я не знаю, разрешает ли MISRA C провал одной case-метки в другую.
Вероятно, это сделано для того, чтобы можно было подключать этот пин к концевым кнопкам на станке, которые при нажатии замыкают цепь (хотя, никто не мешает поставить кнопку, работающую на разрыв, не знаю).
Кнопку экстренного останова целесообразнее использовать на размыкание. Если она сломается, то просто встанет станок и кнопку
З.Ы. Станки на дуринках… о-хо-хонюшки…
Внутри ISR прерывания запрещены, и некоторые функции будут работать неправильно.
Говоря строго и применительно к AVR, разрешить вложенные прерывания можно.
Многозадачная Ардуина: таймеры без боли