Потоки в AVR

Тема в разделе "Микроконтроллеры AVR", создана пользователем AlexU, 2 ноя 2016.

Метки:
  1. AlexU

    AlexU Гуру

    Доброго всем времени суток!
    Хочу поделиться опытом знакомства с библиотекой 'avr-threads' (https://bitbucket.org/dferreyra/avr-threads/wiki/Home), которая позволяет организовать прерываемые (вытесняемые) потоки на микроконтроллерах семейства AVR.
    Почему озаботился этим вопросом, потому что неоднократно приходилось слышать вопросы типа "А есть ли в Arduino потоки?" и самому стало интересно "А есть ли? И если есть, то насколько функциональны?". И вот на просторах интернета наткнулся на библиотеку 'avr-threads'. Честно говоря попадались разные предложения и реализации потоков для AVR, но все они имели один недостаток -- они реализовывали "нечестные" потоки. Что в моём понимании означают "нечестные" потоки, то что в этих реализациях один поток может без особого труда заблокировать работу других потоков, например, вызвав функцию 'delay()'. С использованием "нечестных" потоков нужно очень аккуратно составлять код, постоянно думая о том, что бы он (этот код) не мешал работе других потоков. Конечно аккуратность нужна и с использованием библиотеки 'avr-threads', но всё же при проектировании потоков на основе этой библиотеки основные мысли концентрируются именно на функционале отдельного потока, не заморачивась о том, что тот или иной поток окажет влияние на работу независимых от него потоков. Но при этом нужно помнить, что вычислительные ресурсы микроконтроллеров ограничены, и потоки будут бороться за эти ограниченные ресурсы, что может привести к некоторым задержкам в работе того или иного потока или к другим неожиданным последствиям.
    С вводной частью закончим, перейдём к описанию того что получилось в итоге. Сразу оговорюсь, знакомство не долгое -- в течении одного вечера опробовал базовый функционал: инициализация и создание потоков. За рамками пока остались вопросы синхронизации, управление планированием (если таковое имеется) и т.п. В качестве подопытной будет выступать Arduino UNO rev.3 (ATmega328P), но думаю, что все опыты можно будет провести на любой Arduino на базе ATmega328 без каких либо доработок исходного кода. И второе -- не смотря на то, что пользуюсь библиотеками Arduino, я не использую Arduino IDE, поэтому как встраивать библиотеку avr-threads в эту среду описывать не буду, надеюсь нужную информацию сами найдёте в интернете.
    И так что нужно сделать:
    Во-первых подключение заголовочного файла в случае с исходниками C++ нужно делать следующим образом (для исходников на C просто #include "avr-thread.h"):
    Код (C++):

    #ifdef __cplusplus
    extern "C" {
    #endif

    #include "avr-thread.h"

    #ifdef __cplusplus
    } // extern "C"
    #endif
     
    Во-вторых нужно проинициализаровать библиотеку, пример кода будет приведён в третьем пункте.
    В-третьих для того что бы потоки по очереди выполняли свою работу нужен планировщик потоков, который будет распределять процессорное время между потоками. Планировщик должен запускаться с определённой периодичностью и от этой периодичности будет зависеть с какой частотой потоки будут менять друг друга. Самый простой способ это воспользоваться таймером и его прерыванием, например, по переполнению. В Arduino уже есть настроенный таймер, который отвечает за часы реального времени в Arduino, -- это нулевой таймер -- Timer0. Этот таймер настроен на частоту ~1 кГц. В принципе этой частоты хватит для обеспечения работы нескольких потоков. Но что бы этот таймер использовать нужно внести изменения в исходники библиотеки из комплекта Arduino IDE. Прикрепил файл hardware/arduino/avr/cores/arduino/wiring.c с необходимыми изменениями (изменения обёрнуты в #ifdef AVR_THREADS). Если же потрошить Arduino IDE желания нет, то можно воспользоваться, например, таймером Timer2 (аналог Timer0). Но этот подход ограничит возможности по настройке ШИМ на 3 и 11 выходах или будет конфликтовать с другими библиотеками, которые могут использовать этот таймер. Так же есть Timer1 (ШИМ ноги 9 и 10), проблемы те же, что и с Timer2. Какой таймер использовать решать вам -- можно вобще использовать внешний генератор, который будет "дёргать" ногу с обработчиком внешнего прерывания. Ниже приведён код инициализации Timer2 -- частота ~1 кГц, прерывание по переполнению -- а также код обработчика прерывания, который выполняет роль планировщика потоков:
    Код (C++):

    // обработчик прерывания по переполнению
    // декларировать нужно именно так
    extern "C" void TIMER2_OVF_vect (void) __attribute__((signal, naked, __INTR_ATTRS));
    void TIMER2_OVF_vect (void)
    {
    //   sei();   // можно сразу разрешить обработку прерываний -- это не нарушит работу планировщика потоков

       avr_thread_isr_start();   // старт планировщика потоков
       avr_thread_isr_end();   // остановка планировщика потоков
       // после вызова этих функций на выполнение будет запущен следующий поток

    }

    // функция инициализации библиотеки и планировщика
    // необходимо вызвать из setup()
    void initThreads()
    {
       cli();     // запрещаем прерывания

       avr_thread_init();   // инициализируем библиотеку avr-threads

       sbi(TCCR2A, WGM21);   // натраиваем Timer2 на 1 кГц
       sbi(TCCR2A, WGM20);
       sbi(TCCR2B, CS22);
       cbi(TCCR2B, CS21);
       cbi(TCCR2B, CS20);

       sbi(TIMSK2, TOIE2);   // разрешаем прерывание по переполнению

       sei();     // разрешаем прерывания
    }

     
    Если решились заменить "поторохи" библиотеки Arduino, то код приведённый выше использовать не надо, достаточно при компиляции задать параметр -DAVR_THREADS (аналог #define AVR_THREADS в исходном коде).

    Продолжение следует...

    Прекрепил Sandbox.cpp и Sanbox.h, в которых приведён код теста.
     

    Вложения:

    • wiring.c
      Размер файла:
      12,1 КБ
      Просмотров:
      461
    • Sandbox.cpp
      Размер файла:
      2,3 КБ
      Просмотров:
      456
    • Sandbox.h
      Размер файла:
      283 байт
      Просмотров:
      688
    Alex19, Igor68, Unixon и 2 другим нравится это.
  2. AlexU

    AlexU Гуру

    Продолжаем...

    И напоследок декларируем функции потоков и стартуем потоки. В качестве примера создадим два потока: первый мигает зелёным светодиодом с определенными паузами, второй шлёт данные в Serial (скорость выбрана самая низкая -- 2400 кБит/сек, а данных много, это гарантированно приведёт к переполнению передающего программного буфера, который по умолчанию 64 байт, и заблокирует выполнение потока до того момента, пока в передающий буфер не уйдут последние данные. О блокировке потока будет сообщать красный светодиод). А так же будет работать основной поток -- функция loop() -- так же как и первый поток будет мигать светодиодом, только жёлтым:
    Код (C++):

    // код первого потока
    // для обеспечения задержек используются привычные delay()
    // которые в простых скетчах тормозят работу всего кода
    // вот только в данном случае остановиться выполнение только этого потока
    // остальные будут продолжать выполнять свою работу
    void thr1()
    {
      // обязательно? нужен цикл (на счёт "обязательно" пока не уверен)
      while(true) {
      digitalWrite(4, HIGH);  // зажигаем зелёный светодиод
      delay(1000);
      digitalWrite(4, LOW);  // гасим зелёный светодиод
      delay(1000);
      }
    }

    // код второго потока
    // зажигаем светодиод и начинаем посылку большого пакета на малой скорости
    // функция Serial.println() заблокирует выполнения потока пока не передадутся данные
    // после отправки последнего байта данных в передающий буфер гасим красный светодиод
    void thr2()
    {
      // обязательно? нужен цикл (на счёт "обязательно" пока не уверен)
      while (true) {
      delay(2000);
      digitalWrite(6, HIGH);  // зажигаем красный светодиод
      Serial.println("Thread 2 sends data, big data, big big big data, big big big data, big big big data, big big big data, big big big data \
    , big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data \
    , big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data \
    , big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data, big big big data"
    );
      digitalWrite(6, LOW);  // гасим красный светодиод
      }
    }

    // Для каждого потока необходимо создать контекст и определить размер стека
    // размер стека должен быть достаточным для хранения всех переменных потока.
    // Здесь есть сложность, т.к. точный размер стека определить почти не возможно.
    // Всё это съедает "дорогую" оперативную память.

    // для первого потока
    avr_thread_context thr1_context;
    uint8_t thr1_stack[128];

    // для второго потока
    avr_thread_context thr2_context;
    uint8_t thr2_stack[512];

    // Привычные всем функции setup() и loop()
    void setup()
    {
      Serial.begin(2400);
      Serial.println("setup");

      // пины для мигания светодиодами
      // 4 - зелёный
      // 6 - красный
      // 4 - жёлтый
      pinMode(4, OUTPUT);
      digitalWrite(4, LOW);
      pinMode(6, OUTPUT);
      digitalWrite(6, LOW);
      pinMode(8, OUTPUT);
      digitalWrite(8, LOW);

      // если подменяли файл wiring.c, то эту функцию вызывать не нужно
      initThreads();  // вызов функции инициализации библиотеки и Timer2

      // перед стартом потоков несколько раз мигнём жёлтым светодиодом (просто для отладки)
      digitalWrite(8, HIGH);
      delay(100);
      digitalWrite(8, LOW);
      delay(100);
      digitalWrite(8, HIGH);
      delay(100);
      digitalWrite(8, LOW);
      delay(100);
      digitalWrite(8, HIGH);
      delay(100);
      digitalWrite(8, LOW);
      delay(500);


      // стартуем первый поток
      avr_thread_start(&thr1_context, thr1, thr1_stack, sizeof(thr1_stack));
     
      // стартуем второй поток
      avr_thread_start(&thr2_context, thr2, thr2_stack, sizeof(thr2_stack));
    }

    void loop()
    {
    // мигаем жёлтым светодиодом
      digitalWrite(8, HIGH);
      delay(100);
      digitalWrite(8, LOW);
      delay(100);
      digitalWrite(8, HIGH);
      delay(100);
      digitalWrite(8, LOW);
      delay(200);
    }
     
    Потоки работают параллельно, не мешая друг другу, не смотря на активное использование delay().
    Код совсем не оптимальный и бессмысленный -- основная задача продемонстрировать, что в потоках может выполняться любой код и он не приведёт к "заморозке" всего микроконтроллера.

    Ещё есть небольшой хук, в функции delay() предусмотрена возможность "быстрой" передачи управления, если время ожидания ещё не вышло. Что бы воспользоваться этим хуком нужно декларировать функцию:
    Код (C++):

    void yield(void)
    {
      // говорим библиотеке, что нужно отдать процессорное время другому потоку
      // так как текущему потоку пока делать нечего -- он ждёт пока пройдёт
      // время заданное при вызове функции delay()
      avr_thread_yield();
    }
     
    Хук этот не обязателен, но позволит более эффективно использовать процессорное время, т.к. "ждущие" потоки не будут его понапрасну использовать.

    На этом всё.

    Демонстрация работы потоков. На видео видно, что все три потока работают одновременно:

     
    Последнее редактирование: 2 ноя 2016
    Alex19, Unixon, koteika и 3 другим нравится это.
  3. Tomasina

    Tomasina Сушитель лампочек Модератор

  4. Igor68

    Igor68 Гуру

    ранее в Keil использовал его ARTX(RTX). Не думал что это будет применено и здесь!:)
    Это многое изменит!
     
  5. AlexU

    AlexU Гуру

    Именно этот yield и используется. Только в "обычном" режиме им пользоваться не очень удобно, поэтому ни кто и не пользуется. А в потоках позволяет сэкономить ресурс микроконтроллера.
     
    Igor68 нравится это.
  6. AlexVS

    AlexVS Гик

    Интересно будет попробовать вынести работу веб-сервера (это конечно громко сказано, но тем не менее) и прослушку радиоканала (nRF24) в отдельные потоки.
     
  7. Unixon

    Unixon Оракул Модератор

    Так это и не для AVR написано. Да и все равно многозадачность кооператвная, а не вытесняющая.
     
  8. AlexU

    AlexU Гуру

    В этой теме речь идёт о вытесняемых потоках для AVR. Приведён пример кода, в котором потоки переключаются с частотой 1 кГц. А что касается функции 'yield', то ей можно не пользоваться -- потоки всё равно будут переключаться с заданной частотой. Вот только при использовании хука с 'yield' вычислительный ресурс микроконтроллера будет использоваться более эффективно -- время, предназначенное "ожидающему" потоку, будет отдано другому потоку, т.к. "ожидающий" поток всё равно никакой полезной работы делать не будет -- он просто ждёт определённый интервал времени.
     
    Tomasina и Igor68 нравится это.
  9. Unixon

    Unixon Оракул Модератор

    Это понятно, я не о том.

    Мой комментарий относился к тому, что библиотека Scheduler, если верить справочнику arduino.cc, совершенно не реализована для AVR и ее функция yield() не имеет отношение к организации многозадачности на AVR, реализуемой посредством AVR-Threads.
     
  10. AlexU

    AlexU Гуру

    Здесь речь идёт про функцию 'yield()', объявленную в файле 'Arduino.h' и используемую в функции 'delay()'. Библиотека Scheduler здесь не при чём.
     
  11. Unixon

    Unixon Оракул Модератор

    Да, но @Tomasina не на нее ссылался, а я на его комментарий ответил... :) просто не туда написал.
     
    Последнее редактирование: 9 ноя 2016
  12. AlexU

    AlexU Гуру

    Теперь понятно. Сам не внимательно посмотрел -- увидел знакомые буквы подумал, что речь о том же, а по ссылкам ходить не стал.

    Что-то у меня сегодня "день невнимательности" -- уже не первый раз:)
     
  13. ostrov

    ostrov Гуру

    Народ, ближе к делу, кто нибудь уже выходил за рамки теории? Запускали что то полезное в многозадачном режиме? Стоит ли игра свеч или это просто упражнение для ума?
     
  14. Unixon

    Unixon Оракул Модератор

    Пока нет. Думаю это имеет смысл только если кооперативная многозадачность посредством самодельного автомата не справляется с работой и обязательно нужна вытесняющая на потоках, либо код достаточно сложный, чтобы его в рукопашную в таком виде писать и проще сделать delay+yield и т.п.. Пока таких задач не возникало, хватает рукопшно-автоматного подхода, но с ростом сложности задач, возможно, проще будет перейти к потокам. Ну и учитывая скромные ресурсы AVR-ок этот подход видится применимым на гораздо более толстых контроллерах.
     
  15. AlexU

    AlexU Гуру

    Планирую использовать для организации вывода информации на дисплей (16х2, 20х4 и т.п.). С потоками будет проще реализовать анимацию -- бегущий или мигающий участок текста и т.п.
     
  16. Onkel

    Onkel Гуру

    у меня на всех железках, что я делаю, всегда все в многозадачном режиме, см. например у DiHalt "операционная система для микроконтроллеров".
    300 байт на тиньке
     
  17. ostrov

    ostrov Гуру

    Интересно. А для чего на всех? Я для одной вот не могу придумать оправдание для применения. Может зря?
     
  18. Onkel

    Onkel Гуру

    у меня сделан шаблон (не только для atmega8, 328, но и для sam3 и stm32), и по конкретной задаче я лишь пишу специфические функции, а "общие" задачи вроде общения по usart, i2c и пр. уже реализованы, остается лишь расставить "приоритеты". Например, чтобы превратить 8 канальный релейный модуль в 8 канальный диммер, нужно переписать (а реально копи / пасте) лишь одну функцию, все остальное остается.
     
    Alex19 и Igor68 нравится это.
  19. ostrov

    ostrov Гуру

    Не ошибусь сказав, что многие собирают программы из готовых наработанных блоков копипастой. Не совсем понятно в чем здесь роль многопоточности.
     
  20. Onkel

    Onkel Гуру

    вычисления бьются по 8 (32 для stm32 и sam3) задачам, которые решаются по приоритетности. DiHalt читайте, у него хорошо написано.