Как стать автором
Обновить

Комментарии 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
В главный «поток» можно помещать не требующую точного тайминга логику взаимодействия с пользователем (прием команд через последовательный порт или по сети), отправку какой-нибудь информации по сети на сервер и т.п. В обработчик прерывания помещать коротки процедуры взаимодействия с железом — строго периодические опросы датчиков (при этом не опасаться, что значение будет пропущено из-за того, что какая-нибудь пользовательская процедура в главном цикле зависнет), программная генерация прямоугольного сигнала для управления мотором (как в примере с шаговиком).
Тут много гадать не надо — на отладочную строку уходит столько времени сколько его нужно для передачи на выбранной скорости — если 9600 и передать большую строку(960 символов) то и в секунду вложиться не сможет…
Как правило все разрешающие сигналы — низкого уровня, это пошло ещё с тех времён когда логика была TTL и неподключенный вход имел положительный потенциал по конструктивным причинам.
Тем, для кого этот материал оказался полезным, предлагаю не останавливаться на достигнутом и открыть для себя полноценные RTOS. Там есть:
— независимые процессы с возможностью задать приоритет для каждого
— таймеры (произвольное количество)
— средства межпроцессного взаимодействия: очереди, семафоры и т.д.

Например, FreeRTOS есть как под Arduino (https://github.com/greiman/FreeRTOS-Arduino), так и под голый AVR (http://www.freertos.org/AVR_IAR.html).
Т.е. FreeRTOS прошивается например в atmega328p и затем уже можно под нее писать программы которые прошивать в чип (прошитый FreeRTOS)?
«Программы» под FreeRTOS — это обычные сишные функции, они компилируются вместе с ОС в монолитную прошивку.
Тем, для кого этот материал оказался полезным, можно сначала попробовать какой-нибудь шедулер.
Ищется в менеджере библиотек по словам task, scheduler, thread. Например, Tasker, он наверняка менее гибок, зато более, хм, Arduino-style и user-frendly.
Спасибо, интересная информация. Вопросы:
— В статье и примерах речь идёт об одном таймере. Что насчёт работы одновременно нескольких таймеров? Можно добавить пример кода в библиотеку?
— Что произойдёт, если код не успеет выполниться до следующего прерывания? Насколько катастрофическими будут последствия?
— Планируется ли поддержка других платформ, например, ESP8266?
Если не успеет выполнится, то контроллер уйдёт в бесконечный цикл и будет постоянно отрабатывать только прерывание, правда, оставив основному циклу всего 1 такт процессора между прерываниями, а это считай капля в море и какой-нибудь простейший жесткий delay на 1мс в основном цикле растянется на минуты или часы, но в конце концов выполнится.
> — В статье и примерах речь идёт об одном таймере. Что насчёт работы одновременно нескольких таймеров? Можно добавить пример кода в библиотеку?

Можно. Написал на скорую руку скетчик, пожалуй, добавлю его в библиотеку
#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
Кстати, к вопросу о наложении таймеров. Немного модифицировал скетчик — добавил delay на 250 миллисекунд между запусками таймеров, чтобы не стартовали одновременно.

#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 сервоприводов управляются в реальном времени без помощи прерываний.
С помощью одного таймера можно сделать сетку импульсов с достаточной частотой и вести по ней несколько независимых «потоков». Например, 10 вызовов в секунду: на каждый 2й вызов мы шлем импульсы шагать мотору1, на каждый 3й вызов — импульсы шагать мотору2, на каждый 5й вызов — смотрим значение какого-нибудь датчика. Главное, чтобы каждый вызов умещался в сетку.

На Arduino Due с чипом SAM/ARM, полагаю, можно запустить все 9 таймеров одновременно, но, пожалуй, будет сложно избежать чехарды с наложениями прерываний (видно, как на нее легко нарваться уже с 2мя таймерами).

Прикольный проектик. Реально где-то внедрено или в процессе разработки?
С помощью одного таймера можно сделать сетку импульсов с достаточной частотой и вести по ней несколько независимых «потоков»

Это совсем другое дело — в таком виде этому можно найти много полезных применений. Главное не превышать интервал до следующего прерывания.

Прикольный проектик. Реально где-то внедрено или в процессе разработки?

Там все проекты реальные. Конкретно этот уже несколько месяцев работает в режиме 24/7 (время от времени модернизируется в рабочем порядке перезаливкой прошивки).
Эх молодёжь. Я сам писал на асме и на С код для моторчиков когда ардуино ещё не было, а была самосборная плата из рассыпухи и микроконтроллеров pic и avr. И использование прерываний вообще не вызывало проблем и вопросов.
Я бы не рекомендовал помещать в него вызовы блокирующих задержек 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. Применить битовую операцию по маске (1-2 такта)
  3. Записать значение регистра.

А теперь вопрос: что будет, если один и тот же банк GPIO изменяется из этого красивого "таймера" во время модификации оного из основной программы? Напомню обработчик прерывания автоматически сохраняет контекст операции, но не отменяет ее или не отбрасывает ее к началу. Одним из вариантов является отключение прерываний во время таких операций.


А вообще если нужен гиперкроссплатформенная "многозадачность" — то есть Protothreads, которые, при должном понимании принципов работы, запустятся на любом Си-компиляторе и не требуют "нувообщеничего" дополнительного для работы.

есть Protothreads, которые, при должном понимании принципов работы, запустятся на любом Си-компиляторе и не требуют «нувообщеничего» дополнительного для работы.

О да! Ключевое здесь «при должном понимании принципов работы», а вывих мозга они дают быстро и надёжно :) Важно понимать, что это не полноценный планировщик, и способов выстрелить себе в ногу просто куча.

Нужно самому следить за передачей управления: если тред застрянет в бесконечном цикле, никто его прервать не сможет. Нет полноценного сохранения контекста: забыл объявить переменную в треде как static — она сбросится. Очень сложно отлаживать: всё на макросах, и порядок выполнения кода совсем не очевиден.

А в остальном — всё просто отлично: кроссплатформенно, очень быстро, почти без накладных расходов. Инструмент для настоящих ниндзя.
способов выстрелить себе в ногу просто куча.

Обычно они происходят при попытке скрестить ужа с ежом, т.е. обычный "синхронный код" и эти машины состояний. Рекурсивый вызов прототредов тоже тот еще гемор.


если тред застрянет в бесконечном цикле

На такой случай есть PT_YIELD(&self->pt); который будет стоять в начале или конце цикла. Но в безконечном цикле вообще любая программа может застрять, тут вопрос дизайна.


Очень сложно отлаживать

Тут согласен, особенно на микроконтроллерах, где количество точек останова ограниченно аппаратно.


забыл объявить переменную в треде как static

Я вообще ярый противник любых static внутри функций. Все, что нужно конкретному "потоку" выносится в отдельную структуру, в которой также и лежит struct pt, с таким подходом можно даже сделать несколько однотипных потоков.


всё на макросах

Отдать должное создателю библиотеки, они все легкочитаемы и понятны, чего не скажешь, к примеру, про тучу макросов в STM32 HAL.


Вообще Protothreads — это обычный switch-case. Меня больше напрягает, когда люди осознанно его велосипедят вместо того, чтобы взять готовую либу. Единственное исключение — я не знаю, разрешает ли MISRA C провал одной case-метки в другую.

Если от прототредов начинается вывих мозга, то можно использовать обычные конечные автоматы на свитчах. Все честно, понятно, никаких макросов и самообмана.
Вероятно, это сделано для того, чтобы можно было подключать этот пин к концевым кнопкам на станке, которые при нажатии замыкают цепь (хотя, никто не мешает поставить кнопку, работающую на разрыв, не знаю).

Кнопку экстренного останова целесообразнее использовать на размыкание. Если она сломается, то просто встанет станок и кнопку замкнут жучком поменяют. А если сломается кнопка на замыкание — это может остаться тайной до момента несчастного случая.
З.Ы. Станки на дуринках… о-хо-хонюшки…

Внутри ISR прерывания запрещены, и некоторые функции будут работать неправильно.

Говоря строго и применительно к AVR, разрешить вложенные прерывания можно.
разрешить вложенные прерывания можно.

При этом надо делать это аккуратно, иначе можно нечаянно вызвать рекурсию.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.