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

Еще меньше используют такую возможность на практике, т.к. в стандартном не слишком богатом API Arduino она не предусмотрена. И, хотя, доступ ко всем богатствам внутренних возможностей микроконтроллера лежит на расстоянии вытянутой руки через подключение одного-двух системных заголовочных файлов, не каждый пожелает добавить в свой аккуратный маленький скетч пару-тройку экранов довольно специфического настроечного кода (попутно потеряв с ним остатки переносимости между разными платами). Совсем единицы (тем более, среди аудитории Ардуино) решатся и смогут в нем разобраться.

Сегодня я избавлю вас от страданий.

и расскажу, как получить настоящие многозадачность и реальное время в прошивке вашего ардуино-робота, добавив в неё ровно 3 строчки кода (включая #include в шапке). Обещаю, что у вас всё получится, даже если вы только что в первый раз запустили Blink.

Начнем сразу с кода


arduino-timer-api/examples/timer-api/timer-api.ino

Подключаем библиотеку timer-api.h (раз)

#include "timer-api.h"

Запускаем таймер с нужной частотой с timer_init_ISR_XYHz: здесь XYHz=1Hz — 1 Герц — один вызов прерывания в секунду (два)

void setup() {
    Serial.begin(9600);

    // частота=1Гц, период=1с
    timer_init_ISR_1Hz(TIMER_DEFAULT);

    pinMode(13, OUTPUT);
}

(ISR — interrupt service routine, процедура-обработчик прерывания)

Добавляем в главный цикл loop любую блокирующую или неблокирующую ерунду: печатаем сообщение, ждём 5 секунд (здесь всё, как обычно, поэтому не считаем)

void loop() {
    Serial.println("Hello from loop!");
    delay(5000);

    // здесь любой код: блокирующий или неблокирующий
}

Процедура, вызываемая прерыванием по событию таймера с заданным периодом, — реализация для функции с именем timer_handle_interrupts: печатаем сообщение, мигаем лампочкой (три)

void timer_handle_interrupts(int timer) {
    Serial.println("goodbye from timer");

    // мигаем лампочкой
    digitalWrite(13, !digitalRead(13));
}

То же самое, только добавим замер времени между двумя вызовами для наглядности и отладки:

void timer_handle_interrupts(int timer) {
    static unsigned long prev_time = 0;
    
    unsigned long _time = micros();
    unsigned long _period = _time - prev_time;
    prev_time = _time;
    
    Serial.print("goodbye from timer: ");
    Serial.println(_period, DEC);

    // мигаем лампочкой
    digitalWrite(13, !digitalRead(13));
}

Шьем плату, открываем Инструменты > Монитор порта, наблюдаем результат:

image

Как видим, обработчик timer_handle_interrupts печатает сообщение каждые 1000000 (1 миллион) микросекунд, т.е. ровно раз в секунду. И (о чудо!) постоянная блокирующая задержка на 5 секунд delay(5000) в главном цикле никаким образом ему в этом действии не мешает.

Вот вам реальное время и многозадачность в одном скетче в 3 строчки, я обещал.

Варианты частот для timer_init_ISR_XYHz


    //timer_init_ISR_500KHz(TIMER_DEFAULT);
    //timer_init_ISR_200KHz(TIMER_DEFAULT);
    //timer_init_ISR_100KHz(TIMER_DEFAULT);
    //timer_init_ISR_50KHz(TIMER_DEFAULT);
    //timer_init_ISR_20KHz(TIMER_DEFAULT);
    //timer_init_ISR_10KHz(TIMER_DEFAULT);
    //timer_init_ISR_5KHz(TIMER_DEFAULT);
    //timer_init_ISR_2KHz(TIMER_DEFAULT);
    //timer_init_ISR_1KHz(TIMER_DEFAULT);
    //timer_init_ISR_500Hz(TIMER_DEFAULT);
    //timer_init_ISR_200Hz(TIMER_DEFAULT);
    //timer_init_ISR_100Hz(TIMER_DEFAULT);
    //timer_init_ISR_50Hz(TIMER_DEFAULT);
    //timer_init_ISR_20Hz(TIMER_DEFAULT);
    //timer_init_ISR_10Hz(TIMER_DEFAULT);
    //timer_init_ISR_5Hz(TIMER_DEFAULT);
    //timer_init_ISR_2Hz(TIMER_DEFAULT);
    //timer_init_ISR_1Hz(TIMER_DEFAULT);

(вызов timer_init_ISR_1MHz тоже есть, но он не даёт рабочий результат ни на одном из тестовых контроллеров)

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

Полагаю, излишне пояснять, что чем выше частота таймера, тем меньше период вызова прерываний, тем быстрее должен выполняться код обработчика. Я бы не рекомендовал помещать в него вызовы блокирующих задержек delay, циклы с неизвестным заранее количеством итераций, любые другие вызовы с плохо предсказуемым временем выполнения (в том числе Serial.print).

Суммирование периодов (деление частоты)


В том случае, если стандартные частоты из предложенных на выбор вас не устраивают, можно ввести в код прерывания дополнительный счетчик, который будет выполнять полезный код только после определенного количества пропущенных вызовов. Целевой период будет равен сумме пропускаемых базовых периодов. Или можно сделать его вообще переменным.

arduino-timer-api/examples/timer-api-counter/timer-api-counter.ino

#include"timer-api.h"

void setup() {
    Serial.begin(9600);
    while(!Serial);

    // частота=10Гц, период=100мс
    timer_init_ISR_10Hz(TIMER_DEFAULT);
    
    pinMode(13, OUTPUT);
}

void loop() {
    Serial.println("Hello from loop!");
    delay(6000);

    // здесь любой код: блокирующий или неблокирующий
}

void timer_handle_interrupts(int timer) {
    static unsigned long prev_time = 0;

    // дополнильный множитель периода
    static int count = 11;

    // Печатаем сообщение на каждый 12й вызов прерывания:
    // если базовая частота 10Гц и базовый период 100мс,
    // то сообщение будет печататься каждые 100мс*12=1200мс
    // (5 раз за 6 секунд)
    if(count == 0) {
        unsigned long _time = micros();
        unsigned long _period = _time - prev_time;
        prev_time = _time;
    
        Serial.print("goodbye from timer: ");
        Serial.println(_period, DEC);

        // мигаем лампочкой
        digitalWrite(13, !digitalRead(13));

        // взводим счетчик
        count = 11;
    } else {
        count--;
    }
}

image

Произвольная частота


Есть еще вариант установить практически произвольное (в определенных границах) значение частоты таймера при помощи вызова timer_init_ISR(timer, prescaler, adjustment) с параметрами — системным делителем тактовой частоты процессора prescaler и произвольным значением adjustment для размещения в регистре счетчика таймера.

Не вдаваясь в подробности, чтобы не перегружать пост, приведу ссылку на пример с подробными комментариями:
arduino-timer-api/examples/timer-api-custom-clock/timer-api-custom-clock.ino

И только отмечу, что использование такого подхода может привести к потере переносимости кода между контроллерами с разной тактовой частотой, т.к. параметры для получения целевой частоты таймера подбираются в прямой зависимости от частоты системного генератора сигнала на чипе, разрядности таймера, доступных вариантов системных делителей prescaler.

Запуск и остановка таймера в динамике


Для остановки таймера следует использовать вызов timer_stop_ISR, для повторного запуска — любой вариант timer_init_ISR_XYHz, как и раньше.

arduino-timer-api/examples/timer-api-start-stop/timer-api-start-stop.ino

#include"timer-api.h"

int _timer = TIMER_DEFAULT;

void setup() {
    Serial.begin(9600);
    while(!Serial);

    pinMode(13, OUTPUT);
}

void loop() {
    Serial.println("Start timer");
    timer_init_ISR_1Hz(_timer);
    delay(5000);
    
    Serial.println("Stop timer");
    timer_stop_ISR(_timer);
    delay(5000);
}

void timer_handle_interrupts(int timer) {
    static unsigned long prev_time = 0;
    
    unsigned long _time = micros();
    unsigned long _period = _time - prev_time;
    prev_time = _time;
    
    Serial.print("goodbye from timer: ");
    Serial.println(_period, DEC);

    // мигаем лампочкой
    digitalWrite(13, !digitalRead(13));
}

image

Установка библиотеки


Клонировать репозиторий прямо в каталог с библиотеками

cd ~/Arduino/libraries/
git clone https://github.com/sadr0b0t/arduino-timer-api.git

и перезапустить среду Arduino.

Или на странице проекта arduino-timer-api скачать снапшот репозитория Clone or download > Download ZIP или один из релизов в виде архива, затем установить архив arduino-timer-api-master.zip через меню установки библиотек в среде Arduino (Скетч > Подключить библиотеку > Добавить .ZIP библиотеку...).

Примеры должны появиться в меню File > Examples > arduino-timer-api

Поддерживаемые чипы и платформы


— Atmega/AVR 16 бит 16МГц на Arduino
— SAM/ARM 32 бит 84МГц на Arduino Due
— PIC32MX/MIPS 32 бит 80МГц на семействе ChipKIT (PIC32MZ/MIPS 200МГц — частично, в работе)

Ну и, напоследок,


Вращение шаговым мотором через интерфейс step-dir:
— в фоне по таймеру генерируем постоянный прямоугольный сигнал для шага по фронту HIGH->LOW на ножке STEP
— в главном цикле принимаем от пользователя команды для выбора направления вращения (ножка DIR) или остановки мотора (ножка EN) через последовательный порт

arduino-timer-api/examples/timer-api-stepper/timer-api-stepper.ino

#include"timer-api.h"

// Вращение шаговым моторов в фоновом режиме

// Pinout for CNC-shield
// http://blog.protoneer.co.nz/arduino-cnc-shield/

// X
#define STEP_PIN 2
#define DIR_PIN 5
#define EN_PIN 8

// Y
//#define STEP_PIN 3
//#define DIR_PIN 6
//#define EN_PIN 8

// Z
//#define STEP_PIN 4
//#define DIR_PIN 7
//#define EN_PIN 8

void setup() {
    Serial.begin(9600);

    // step-dir motor driver pins
    // пины драйвера мотора step-dir
    pinMode(STEP_PIN, OUTPUT);
    pinMode(DIR_PIN, OUTPUT);
    pinMode(EN_PIN, OUTPUT);

    // Будем вращать мотор с максимальной скоростью,
    // для разных настроек делителя шага оптимальная
    // частота таймера будет разная.
    // Оптимальные варианты задержки между шагами
    // для разных делителей:
    // https://github.com/sadr0b0t/stepper_h
    // 1/1: 1500 мкс
    // 1/2: 650 мкс
    // 1/4: 330 мкс
    // 1/8: 180 мкс
    // 1/16: 80 мкс
    // 1/32: 40 мкс


    // Делилель шага 1/1
    // частота=500Гц, период=2мс
    //timer_init_ISR_500Hz(TIMER_DEFAULT);
    // помедленнее
    timer_init_ISR_200Hz(TIMER_DEFAULT);


    // Делилель шага 1/2
    // частота=1КГц, период=1мс
    //timer_init_ISR_1KHz(TIMER_DEFAULT);
    // помедленнее
    //timer_init_ISR_500Hz(TIMER_DEFAULT);

    // Делилель шага 1/4
    // частота=2КГц, период=500мкс
    //timer_init_ISR_2KHz(TIMER_DEFAULT);
    // помедленнее
    //timer_init_ISR_1KHz(TIMER_DEFAULT);

    // Делилель шага 1/8
    // частота=5КГц, период=200мкс
    //timer_init_ISR_5KHz(TIMER_DEFAULT);
    // помедленнее
    //timer_init_ISR_2KHz(TIMER_DEFAULT);
    
    // Делилель шага 1/16
    // частота=10КГц, период=100мкс
    //timer_init_ISR_10KHz(TIMER_DEFAULT);
    // помедленнее
    //timer_init_ISR_5KHz(TIMER_DEFAULT);
    
    // Делилель шага 1/32
    // частота=20КГц, период=50мкс
    //timer_init_ISR_20KHz(TIMER_DEFAULT);
    // помедленнее
    //timer_init_ISR_10KHz(TIMER_DEFAULT);

    /////////
    // выключим мотор на старте
    // EN=HIGH to disable
    digitalWrite(EN_PIN, HIGH);

    // просим ввести направление с клавиатуры
    Serial.println("Choose direction: '<' '>', space or 's' to stop");
}

void loop() {
    if(Serial.available() > 0) {
        // читаем команду из последовательного порта:
        int inByte = Serial.read();
        if(inByte == '<' || inByte == ',') {
            Serial.println("go back");
            
            // назад
            digitalWrite(DIR_PIN, HIGH);
            
            // EN=LOW to enable
            digitalWrite(EN_PIN, LOW);
        } else if(inByte == '>' || inByte == '.') {
            Serial.println("go forth");

            // вперед
            digitalWrite(DIR_PIN, LOW);
            
            // EN=LOW to enable
            digitalWrite(EN_PIN, LOW);
        } else if(inByte == ' ' || inByte == 's') {
            Serial.println("stop");
            
            // стоп
            // EN=HIGH to disable
            digitalWrite(EN_PIN, HIGH);
        } else {
            Serial.println("press '<' or '>' to choose direction, space or 's' to stop,");
        }
    }
    delay(100);
}

void timer_handle_interrupts(int timer) {
    // шаг на фронте HIGH->LOW
    digitalWrite(STEP_PIN, HIGH);
    delayMicroseconds(1);
    digitalWrite(STEP_PIN, LOW);
}

Комментарии (24)


  1. MegaVaD
    08.09.2017 08:57

    Для наглядности не мешало бы замерять, сколько уходит на (отладочную) «печать» в последовательный порт (и из него), а то не каждый ардуинщик знает.
    Стоит, возможно, так же отметить, что
    >выполняться в строго определенное заранее запланированное время
    и выполняться _ строго определенное заранее запланированное время — две большие разницы.
    К тому же — о смене приоритетов выполнения процедур, вызванных прерываниями, насколько я понимаю, не может идти и речи. Поправьте меня, я просто начинающий ардуинщик, как это связано с настоящим реальным временем?
    И ещё вопрос — мы замыкаем цепь enable с микроконтроллера. В то же время сигнал enable обязан приходить на драйвера в случае безопасной эксплуатации устройства, в котором вращается двигатель. Не ошибка ли это? И почему En=low to enable?


    1. sadr0b0t Автор
      08.09.2017 09:32

      >выполняться в строго определенное заранее запланированное время
      и выполняться _ строго определенное заранее запланированное время — две большие разницы.

      Здесь именно _в_ строго определенное заранее запланированное время. Т.е. процедура прерывания будет вызываться точно каждые 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


    1. sadr0b0t Автор
      08.09.2017 09:38

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


    1. Alexeyslav
      08.09.2017 15:39

      Тут много гадать не надо — на отладочную строку уходит столько времени сколько его нужно для передачи на выбранной скорости — если 9600 и передать большую строку(960 символов) то и в секунду вложиться не сможет…
      Как правило все разрешающие сигналы — низкого уровня, это пошло ещё с тех времён когда логика была TTL и неподключенный вход имел положительный потенциал по конструктивным причинам.


  1. Ocelot
    08.09.2017 09:30
    +2

    Тем, для кого этот материал оказался полезным, предлагаю не останавливаться на достигнутом и открыть для себя полноценные RTOS. Там есть:
    — независимые процессы с возможностью задать приоритет для каждого
    — таймеры (произвольное количество)
    — средства межпроцессного взаимодействия: очереди, семафоры и т.д.

    Например, FreeRTOS есть как под Arduino (https://github.com/greiman/FreeRTOS-Arduino), так и под голый AVR (http://www.freertos.org/AVR_IAR.html).


    1. Andronas
      08.09.2017 12:23

      Т.е. FreeRTOS прошивается например в atmega328p и затем уже можно под нее писать программы которые прошивать в чип (прошитый FreeRTOS)?


      1. Ocelot
        08.09.2017 12:32
        +2

        «Программы» под FreeRTOS — это обычные сишные функции, они компилируются вместе с ОС в монолитную прошивку.


    1. kail
      08.09.2017 15:47

      Тем, для кого этот материал оказался полезным, можно сначала попробовать какой-нибудь шедулер.
      Ищется в менеджере библиотек по словам task, scheduler, thread. Например, Tasker, он наверняка менее гибок, зато более, хм, Arduino-style и user-frendly.


  1. smart_alex
    08.09.2017 10:40

    Спасибо, интересная информация. Вопросы:
    — В статье и примерах речь идёт об одном таймере. Что насчёт работы одновременно нескольких таймеров? Можно добавить пример кода в библиотеку?
    — Что произойдёт, если код не успеет выполниться до следующего прерывания? Насколько катастрофическими будут последствия?
    — Планируется ли поддержка других платформ, например, ESP8266?


    1. Alexeyslav
      08.09.2017 15:45

      Если не успеет выполнится, то контроллер уйдёт в бесконечный цикл и будет постоянно отрабатывать только прерывание, правда, оставив основному циклу всего 1 такт процессора между прерываниями, а это считай капля в море и какой-нибудь простейший жесткий delay на 1мс в основном цикле растянется на минуты или часы, но в конце концов выполнится.


    1. sadr0b0t Автор
      09.09.2017 19:18

      > — В статье и примерах речь идёт об одном таймере. Что насчёт работы одновременно нескольких таймеров? Можно добавить пример кода в библиотеку?

      Можно. Написал на скорую руку скетчик, пожалуй, добавлю его в библиотеку

      #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


    1. sadr0b0t Автор
      09.09.2017 19:23

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


      1. smart_alex
        09.09.2017 19:43
        +1

        Спасибо за развёрнутый ответ. Пара мыслей по поводу. Я в своих проектах привык оперировать десятками таймеров, правда они не реального времени, а скорее псевдо-реального — задержки не нормированы, но на практике хватает (с некоторыми трюками). Например, чтобы сделать такую штуку
        https://hi-lab.ru/arduino-mega-server/ams-pro/projects/garden
        одного-двух таймеров, даже реального времени, будет недостаточно. Но ваша библиотека пригодится в некоторых специфических случаях. Кстати, в этом проекте (кроме всего прочего) 7 сервоприводов управляются в реальном времени без помощи прерываний.


        1. sadr0b0t Автор
          09.09.2017 20:04

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

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

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


          1. smart_alex
            09.09.2017 20:23

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

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

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

            Там все проекты реальные. Конкретно этот уже несколько месяцев работает в режиме 24/7 (время от времени модернизируется в рабочем порядке перезаливкой прошивки).


  1. Aytuar
    08.09.2017 10:40
    +1

    Эх молодёжь. Я сам писал на асме и на С код для моторчиков когда ардуино ещё не было, а была самосборная плата из рассыпухи и микроконтроллеров pic и avr. И использование прерываний вообще не вызывало проблем и вопросов.


  1. iig
    08.09.2017 11:53
    +2

    Я бы не рекомендовал помещать в него вызовы блокирующих задержек 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.


  1. areht
    08.09.2017 12:30

    > Код прерывания, очевидно, должен выполняться достаточно быстро для того, чтобы успеть завершиться до следующего вызова прерывания и, желательно, еще оставить немного процессорного времени для выполнения главного цикла.

    А в заголовке же «таймеры без боли»…

    Я вот взял SoftTimer: для задач, где нет необходимости точно соблюдать тайминги — как раз без боли и страданий


  1. Dima_Sharihin
    08.09.2017 13:06
    +2

    затем аккуратно передавать управление в основную программу так, что она вообще ничего не заметит

    Это только если все операции, что вы выполняете — атомарные. К примеру работа с GPIO: на некоторых платформах есть специальные регистры установки и сброса значений, что позволяет выполнить операцию за один такт, а на других есть просто регистр состояния выводов — и там нужно


    1. Вычитать значение регистра
    2. Применить битовую операцию по маске (1-2 такта)
    3. Записать значение регистра.

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


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


    1. Ocelot
      08.09.2017 13:26

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

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

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

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


      1. Dima_Sharihin
        08.09.2017 15:34
        +1

        способов выстрелить себе в ногу просто куча.

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


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

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


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

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


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

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


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

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


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


      1. Amomum
        08.09.2017 21:44

        Если от прототредов начинается вывих мозга, то можно использовать обычные конечные автоматы на свитчах. Все честно, понятно, никаких макросов и самообмана.


  1. VT100
    08.09.2017 15:40
    +1

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

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

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

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


    1. Alexeyslav
      08.09.2017 15:55

      разрешить вложенные прерывания можно.

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