Многозадачная Ардуина: таймеры без боли

в 23:05, , рубрики: arduino, stepper_h, программирование микроконтроллеров, Разработка робототехники, роботы

Не каждый ардуинщик знает о том, что помимо стартового кода в 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

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

— AVR/Atmega 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);
}

Автор: sadr0b0t

Источник

Поделиться

* - обязательные к заполнению поля