Pull to refresh

Диспетчер Задач для Микроконтроллера

Level of difficultyEasy
Reading time7 min
Views11K

Постановка задачи

В программировании микроконтроллеров часто нужно написать простые тестировочные прошивки. При этом надо некоторые функции вызывать чаще, а некоторые реже. Вот например как тут

Для этого конечно можно запустить FreeRTOS, однако тогда код не будет переносим на другие RTOS, например Zephyr RTOS/TI-RTOS/RTEMS/Keil RTX/Azure RTOS или SafeRTOS. Потом прошивку как код часто приходится частично отлаживать на PC а там никакой RTOS в помине нет.

Поэтому надо держать наготове какой-нибудь простенький универсальный переносимый кооперативный NoRTOS планировщик с минимальной диагностикой и возможностью в run-time отключать какие-то отдельные задачи для отладки оставшихся.

Проще говоря нужен диспетчер задач для микроконтроллера.

Определимся с терминологией

Кооперативный планировщик - это такой способ управления задачами, при котором задачи сами вручную передают управления другим задачам.

Супер-цикл - тело оператора бесконечного цикла в NoRTOS прошивках. Обычно бесконечный цикл в прошивках можно найти по таким операторам как for(;;){} или while(1){}

Bare-Bone сборка - это сборка прошивки на основе API какой-нибудь RTOS, где только один поток и этот поток прокручивает супер-цикл с кооперативным планировщиком. Эта сборка нужна главным образом только для отладки RTOS: настройки стека, очередей и прочего.

Ядром любого планировщика является генератор или источник стабильного тактирования. Желательно с высокой разрешающей способностью. Микросекундный таймер. Это может быть SysTick таймер с пересчетом в микросекунды или отдельный аппаратный таймер общего назначения. Обычно аппаратных таймеров от 3х до 14ти в зависимости от модели конкретного микроконтроллера. Также важно, чтобы таймер возрастал. Так проще и интуитивно понятнее писать код нам человекам, так как мы привыкли к тому что время оно всегда непрерывно идет вперед, а не назад.

Для определенности будем считать, что все задачи имеют одинаковых прототип

 bool task_proc(void);

Сначала надо определить типы данных.

#ifndef TASK_GENERAL_TYPES_H
#define TASK_GENERAL_TYPES_H

/*Mainly for NoRtos builds but also for so-called RTOS Bare-Bone build*/
#include <stdbool.h>

#include "task_const.h"
#include "limiter.h"

typedef struct  {
    uint64_t period_us;
    bool init;
#ifdef HAS_LIMITER
    Limiter_t limiter;
#endif
    const char* const name;
} TaskConfig_t;

#endif /* TASK_GENERAL_TYPES_H */

Ключевым компонентом планировщика, его ядром является программный компонент называемый Limiter. Это такой программный компонент, который не позволит вызывать функцию function чаще чем установлено в конфиге. Например вызывать функцию не чаще чем раз в секунду или не чаще чем раз в 10 ms.

#ifndef LIMITER_TYPES_H
#define LIMITER_TYPES_H

#include <stdbool.h>
#include <stdint.h>

#include "data_types.h"

typedef bool (*TaskFunc_t)(void);

typedef struct {
    bool init;
    bool on_off;
    uint32_t call_cnt;
    uint64_t start_time_next_us;
    U64Value_t duration_us;
    U64Value_t start_period_us;
    uint64_t run_time_total_us;
    uint64_t start_time_prev_us;
    TaskFunc_t function;
} Limiter_t;

#endif /* LIMITER_TYPES_H */

Вот API планировщика. Механизм очень прост. Limiter измеряет время с момента подачи питания up_time_us, смотрит на расписание следующего запуска start_time_next_us и, если текущее время (up_time_us) больше времени запуска, назначает следующее время запуска и запускает задачу (limiter_task_frame).

bool inline limiter(Limiter_t* const Node, uint32_t period_us, uint64_t up_time_us) {
    bool res = false;
    if(Node->on_off) {
        if(Node->start_time_next_us < up_time_us) {
            Node->start_time_next_us = up_time_us + period_us;
            res = limiter_task_frame(Node);
        }

        if(up_time_us < Node->start_time_prev_us) {
            LOG_DEBUG(LIMITER, "UpTimeOverflow %llu", up_time_us);
            Node->start_time_next_us = up_time_us + period_us;
        }
        Node->start_time_prev_us = up_time_us;
    }
    return res;
}

Limiter также ведёт аналитику. Измеряет время старта и окончания задачи, вычисляет продолжительность исполнения задачи (duration), вычисляет минимум (run_time.min) и максимум (duration.max), суммирует общее время, которое данная задача исполнялась на процессоре (run_time_total).

static inline bool limiter_task_frame(Limiter_t* const Node) {
    bool res = false;
    if(Node) {
        uint64_t start_us = 0;
        uint64_t stop_us = 0;
        uint64_t duration_us = 0;
        uint64_t period_us = 0;

        start_us = limiter_get_time_us();

        if(Node->start_time_prev_us < start_us) {
            period_us = start_us - Node->start_time_prev_us;
            res = true;
        } else {
            period_us = 0; /*(0x1000000U + start) - TASK_ITEM.start_time_prev; */
            res = false;
        }

        Node->start_time_prev_us = start_us;
        if(res) {
           	data_u64_update(&Node->start_period_us, period_us);
        }
        

        res = true;
#ifdef HAS_FLASH
        res = is_flash_addr((uint32_t)Node->function);
#endif /*HAS_FLASH*/
        if(res) {
            Node->call_cnt++;
            res = Node->function();
        } else {
            res = false;
        }

        stop_us = limiter_get_time_us();

        if(start_us < stop_us) {
            duration_us = stop_us - start_us;
            res = true;
            data_u64_update(&Node->duration_us, duration_us);
            Node->run_time_total_us += duration_us;
        } else {
            duration_us = 0;
            res = false;
        }
    }
    return res;
}

Стоит заметить, что перед непосредственным запуском конкретной задачи Limiter может проверить, что указатель на функцию в самом деле принадлежит Nor-Flash памяти микроконтроллера.

В основном супер цикле достаточно только перечислить те задачи, которые будут исполняться. Вот функция одной итерации супер-цикла.


bool inline tasks_proc(uint64_t loop_start_time_us){
    bool res = false;
    uint32_t cnt = task_get_cnt();
    uint32_t t = 0;
    for (t=0; t<cnt; t++) {
        if(TaskInstance[t].limiter.on_off) {
            res = limiter(&TaskInstance[t].limiter, TaskInstance[t].period_us, loop_start_time_us);
        }
    }
    return res;
}

bool super_cycle_iteration(void) {
    bool res = false;
    if(SuperCycle.init) {
        SuperCycle.spin_cnt++;
        res = true;
        SuperCycle.run = true;
        SuperCycle.start_time_us = time_get_us();
        LOG_DEBUG(SUPER_CYCLE, "Proc %f Spin:%u", USEC_2_SEC(SuperCycle.start_time_us),SuperCycle.spin_cnt);
        if(SuperCycle.prev_start_time_us < SuperCycle.start_time_us) {
        	SuperCycle.error++;
        }
      
        SuperCycle.duration_us.cur = (uint32_t)(SuperCycle.start_time_us - SuperCycle.prev_start_time_us);
        SuperCycle.duration_us.min = (uint32_t)MIN(SuperCycle.duration_us.min, SuperCycle.duration_us.cur);
        SuperCycle.duration_us.max = (uint32_t)MAX(SuperCycle.duration_us.max, SuperCycle.duration_us.cur);
       
        super_cycle_check_continuity(&SuperCycle, loop_start_time_us);

        tasks_proc(SuperCycle.start_time_us);
      
        SuperCycle.prev_start_time_us = SuperCycle.start_time_us;
    }

    return res;
}


Вот код запуска супер цикла. Из функции super_cycle_start и будет исполняться вся прошивка, за исключением вызова обработчиков прерываний ISR.

_Noreturn void super_cycle_start(void) {
    LOG_INFO(SUPER_CYCLE, "Start");
    super_cycle_init();

    SuperCycle.start_time_ms = time_get_ms();
    LOG_INFO(SUPER_CYCLE, "Started, UpTime: %u ms", SuperCycle.start_time_ms);
  
    for(;;) {
    	super_cycle_iteration();
    }
}

Такая сформировалась зависимость между программными компонентами данного планировщика.

Отладка планировщика

Очевидно, что надо как-то наблюдать за работой планировщика. Для этого планировщик и были разработан, чтобы снимать метрики. Для этого можно воспользоваться интерфейсом командной строки CLI поверх UART.

В данном скриншоте можно замерить, что больше всего процессорного времени потребляет задача DASHBOARD (приборная панель). Тут же видно, что были такие итерации супер цикла, что задача DASHBOARD непрерывно исполнялась аж 0.33 сек!

Можно измерить период с которым вызывалась каждая из задач и сопоставить с конфигом для каждой задачи. Тут видно, что в среднем реже всего вызывается задача FLASH_FS (менеджер файловой системы). Одновременно драйвер светодиода LED_MONO отрабатывает c частотой (44 Hz). А чаще всего происходит опрос DecaDriver(а).

Тут заметно даже, что накладные расходы на данный планировщик составляют 47.9% по времени. Это лишь потому, что в обработчиках самих задач не происходит пока никакой обработки. Прошивка работает вхолостую. Не было прерываний и флаги не устанавливаются. Не приходят пакетов в интерфейсы. Ничего не происходит.

Также высокое процентное значение SchedulerOverhead это признак того, что периоды у всех задач большие и процессору нечего делать. А значит можно смело добавлять в супер цикл больше кооперативных задач или уменьшать периоды у нынешних задач.

Анализируя эти ценнейшие метрики данного импровизированного планировщика можно принимать решения по оптимизации кода всего проекта. Получился своеобразный Code Coverage.

Достоинства данного планировщика

1--Простота, очевидность, прозрачность, мало кода.

2--Можно вычистить процент загрузки процессора по каждой задаче.

3--Переносимость. Можно его прокручивать хоть на микроконтроллере, хоть на BareBone потоке в RTOS, хоть в консольном приложении на LapTop PC.

4--Приоритет задачи задается периодом её запуска. Чем ниже период, тем выше приоритет.

5--Легко масштабировать прошивку. Просто добавляем новые строчки в super цикл.

6--Можно весь этот планировщик вообще реализовать на функциях препроцессора. И тогда не будет накладных расходов на запуск функций. Однако так будет невозможно осуществлять пошаговую отладку программы.

7--Можно переназначать функции для узлов планировщика и таким образом перепрограммировать устройство далеко в Run-Time.

Недостатки данного планировщика

1--Если одна задача зависла, то считай что зависли все остальные задачи.

2--Надо проектировать задачи так, чтобы они что-то делали за один прогон и не тратили много времени внутри себя. Например переключили состояние конечного автомата и вышли. Совсем не здорово, если какая-то задача начнет расшифровывать 150kByte KeePass файл внутри общего супер цикла или вычислять обратную матрицу 100x100. У Вас перестанет мигать Heart Beat LED, перестанет отвечать CLI и пользователь будет с полной уверенностью считать, что прошивка просто взяла и зависла! А на самом деле программа через 57 секунд снова воспрянет.

3--Требуются накладные расходы (в виде процессорного времени) для вычисления метрик за которыми следит Limiter. Но это не такая и большая проблема, так как отладочные метрики можно включать или исключать на стадии препроцессора #ifdef(ами).

Вывод

Вот и Вы умеете делать кооперативный планировщик. Супер цикл это не такая уж и плохая вещь. Его можно отлично использовать и в RTOS прошивках. Есть код которому точно нужен RTOS. Это BLE/LwIP стек, однако всё остальное: LED, Button может отлично работать в пределах супер цикла в отдельном BareBone потоке. Благодаря супер циклу вы сэкономите на переключении контекста. Надеюсь, что этот текст поможет кому-нибудь писать прошивки и оценивать нагрузку на процессор.

Словарь

Акроним

Расшифровка

1

ISR

Interrupt Service Routine

2

RTOS

real-time operating system

3

UART

Universal asynchronous receiver/transmitter

4

CLI

command-line interface

5

API

Application Programming Interface

Links

Контрольные вопросы:

1--В какую сторону в ARM Cortex-M4 считает Sys Tick таймер?

2--Как измерить загруженность процессора в NoRTOS прошивке?

3--Сколько тактов процессора нужно для вызова Си-функции на микропроцессоре ARM Cortex-M4?

4--Сколько тактов процессора нужно для вызова обработчика прерываний на микропроцессоре ARM Cortex-M4?

Only registered users can participate in poll. Log in, please.
Как вы организовываете прошивки?
3.19% супер-цикл без прерываний3
29.79% RTOS28
26.6% супер-цикл + прерывания25
40.43% супер-цикл, который прокручивает конечные автоматы + прерывания38
94 users voted. 12 users abstained.
Only registered users can participate in poll. Log in, please.
Какую RTOS вы использовали при программировании микроконтроллеров?
38.79% никакую45
2.59% Azure RTOS3
0.86% CHibi1
0% Contiki0
1.72% Embox2
55.17% FreeRTOS64
2.59% Keil RTX3
0.86% Nucleus RTOS1
0% OpenRTOS0
1.72% RTEMS2
0% SafeRTOS0
3.45% TI-RTOS4
0.86% TNKernel1
0.86% TinyOS1
1.72% vxWorks2
6.9% Zephyr8
0.86% NuttX1
3.45% QNX4
0.86% scmRTOS1
6.9% Я сам написал себе RTOS, вытесняющий планировщик, синхронизацию, ADT и модульные тесты8
116 users voted. 17 users abstained.
Tags:
Hubs:
Total votes 21: ↑19 and ↓2+22
Comments26

Articles