Статья описывает этапы адаптации операционной системы Azure RTOS к микроконтроллеру платы управления резервным питанием BACKPMAN v1.0.

Все статьи по проекту

Cсылка на открытый проект: https://github.com/Indemsys/Backup-controller_BACKPMAN-v1.0

Поскольку опыт применения Azure RTOS в прошлом проекте был более чем успешным было решено применять ее везде.

Нельзя сказать что ядро этой RTOS чем-то уникально. Тут имеют значение другие факторы:

  • подтверждённая временем и сертификатами надёжность.

  • богатое программное обеспечение промежуточного уровня включающее: файловую систему, TCP стек, GUI, IoT протоколы, USB классы и т.д.

  • аскетичность и сбалансированность API ядра. Отсюда вытекает простота портирования и освоения.

  • наличие плагинов для отладки сервисов ядра RTOS в среде разработки IAR Embedded Workbench.

  • доступная online документация и книги об этой RTOS.

  • активное развитие. Обновления и исправления идут каждую неделю.

  • поддержка крупными производителей микроконтроллеров: ST, NXP, Cypress, Infineon, Renesas... Они включают эту RTOS в свои SDK.

  • наличие эмуляции API FreeRTOS, OSEK, POSIX.

  • поддержка технологии Symmetric Multi-Processing (SMP)  и динамической загрузки и исполнения программных модулей.

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

В данном проекте из всего репозитория будет использовано только ядро RTOS. Ядро называется Azure RTOS ThreadX. Остальные части RTOS в данном случае либо пока не нужны, либо для них не хватает ресурсов (но они пригодятся в следующей ревизии платы).

Старт с hello word без RTOS

Напомню что портировать будем на плату с микроконтроллером MKE18F512VLL16 ( 32-Bit 168MHz ARM​ Cortex​-M4F core, 512KB (512K x 8) FLASH, 64 KB SRAM).

Готовых примеров портирования ThreadX на эти чипы в репозиторий и на сайте NXP нет, но есть директория с портом для архитектуры Cortex-M4F под IAR.

В этом случае все что надо сделать - это сделать для чипов MKE18F простенький проект с пустой функцией main и правильной инициализацией всех тактирующих узлов, подключить к проекту директорию исходников ThreadX, немного откорректировать несколько файлов ThreadX, настроить приоритеты прерываний ядра и все!

Гораздо сложнее, как всегда, будет адаптировать слой абстракции периферии к RTOS. Но эта работа может быть разделена на этапы и растянута вплоть до завершения разработки окончательной функциональности.

Правильную инициализацию тактирования легко сделать с помощью утилиты MCUXpresso Config Tools и SDK с сайта NXP.

Несмотря на то что чип может работать на частоте 168 МГц, в конфигураторе выбираем частоту 120 МГц для ядра, поскольку на более высокой частоте будет затруднительно работать с встроенной EEPROM чипа. Встроенная EEPROM может записываться только на системной частоте ниже или равной 120 МГц.

Пины не конфигурируем, только может быть настроим 4-е пина UART0 чтобы передать информацию по UART. UART по умолчанию будет задействован при генерации приложения конфигуратором. Но использовать UART не обязательно. На начальном этапе все потребности в отладке покрывает SWD/JTAG адаптер J-Link.

Выбираем генерацию приложения hello_word и получаем проект с такой структурой директорий:

И среди прочего в директории source будет файл hello_world.c с такой функцией main:

int main(void)
{
    char ch;

    /* Init board hardware. */
    BOARD_InitPins();
    BOARD_InitBootClocks();
    BOARD_InitDebugConsole();

    PRINTF("hello world.\r\n");

    while (1)
    {
        ch = GETCHAR();
        PUTCHAR(ch);
    }
}

Перенос исходников ThreadX в свой проект

Теперь из репозитария threadx переносим в наш проект содержимое директорий common, threadx/ports/cortex_m4/iar/inc, threadx/ports/cortex_m4/iar/src/ и файл tx_initialize_low_level.s. Исключаем из проекта файл tx_misra.s поскольку он вызовет ошибку повторного объявления. Дописываем во вкладке препроцессора компилятора С в IDE IAR пути к добавленным директориям. Перекомпилируем проект. Все должно пойти без ошибок.

Редактирование исходников ThreadX и конфигурирование

Во-первых, надо сделать исправления в файле tx_initialize_low_level.s. Изменим объявление константы:

SYSTICK_CYCLES    EQU   ((SYSTEM_CLOCK / 100) -1)

Эта константа инициализирует генератор системных тиков RTOS.
Мы записываем:

SYSTICK_CYCLES    EQU   ((SYSTEM_CLOCK / TX_TIMER_TICKS_PER_SECOND) -1)

В этом же файле добавляем в начале строку

#include "tx_user.h"

Затем файл tx_user_sample.h переименовываем в tx_user.h

В IDE IAR в закладке препроцессора компилятора добавляем запись: TX_INCLUDE_USER_DEFINE_FILE

Теперь при компиляции будет учитываться файл tx_user.h
В этом файле находятся все важнейшие опции RTOS.

Их надо соответствующим образом выбрать.

В начале файла делаем объявления использованных выше констант:

#define SYSTEM_CLOCK              120000000
#define TX_TIMER_TICKS_PER_SECOND 1000

Системный тик RTOS выбираем равным 1 мс. Это будет удобно при при манипуляциях со временем в автоматах состояний.

Список макросов активизированных в файле конфигурации:

#define TX_MAX_PRIORITIES                       32
#define TX_DISABLE_PREEMPTION_THRESHOLD
#define TX_DISABLE_REDUNDANT_CLEARING
#define TX_DISABLE_NOTIFY_CALLBACKS
#define TX_NO_FILEX_POINTER
#define TX_TIMER_PROCESS_IN_ISR

Такой список обеспечивает минимальные размеры RTOS.

Теперь остается только модифицировать наш файл hello_world.c. Полностью он будет выглядеть так:

#include "fsl_device_registers.h"
#include "fsl_debug_console.h"
#include "board.h"

#include "pin_mux.h"
#include "clock_config.h"

#include "tx_API.h"

#define THREAD_MAIN_STACK_SIZE             1024  // Размер стека выделяемый задаче
#define THREAD_MAIN_PRIORITY               1     // Приоритет первой задачи

TX_THREAD               main_thread;
#pragma data_alignment=8
uint8_t                 thread_main_stack[THREAD_MAIN_STACK_SIZE];
static void             Thread_main(ULONG initial_input);

/*-----------------------------------------------------------------------------------------------------
  Модифицированная функция main из SDK
  
  \param void  
  
  \return int 
-----------------------------------------------------------------------------------------------------*/
int main(void)
{
  /* Init board hardware. */
  BOARD_InitPins();
  BOARD_InitBootClocks();
  BOARD_InitDebugConsole();
  tx_kernel_enter();
}

/*-----------------------------------------------------------------------------------------------------
  Вспомогательная функция инициализирующая и запускающая первую задачу
  
  \param first_unused_memory  
-----------------------------------------------------------------------------------------------------*/
void    tx_application_define(void *first_unused_memory)
{
  tx_thread_create(&main_thread, "Main", Thread_main,
    0,
    (void *)thread_main_stack, // stack_start
    THREAD_MAIN_STACK_SIZE,    // stack_size

    THREAD_MAIN_PRIORITY,     // priority. 
                              // Numerical priority of thread. 
                              // Legal values range from 0 through (TX_MAX_PRIORITES-1), where a value of 0 represents the highest priority.

    THREAD_MAIN_PRIORITY,     // preempt_threshold. 
                              // Highest priority level (0 through (TX_MAX_PRIORITIES-1)) of disabled preemption. 
                              // Only priorities higher than this level are allowed to preempt this thread. 
                              // This value must be less than or equal to the specified priority. 
                              // A value equal to the thread priority disables preemption-threshold.
    TX_NO_TIME_SLICE,
    TX_AUTO_START);
}

/*-----------------------------------------------------------------------------------------------------
  Первая задача
  
  \param initial_input  
-----------------------------------------------------------------------------------------------------*/
static void Thread_main(ULONG initial_input)
{
  char ch;
  PRINTF("hello world.\r\n");
  while (1)
  {
    ch = GETCHAR();
    PUTCHAR(ch);
  }
}

И все! Процесс установки RTOS закончен. Осталось проверить работу. Компилируем, загружаем в контроллер и проверяем вывод в терминал.

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

Из map файла сгенерированного после компиляции видим что вся RTOS заняла 2506 байт памяти программ. Весь размер программы 10198 байт.

Особенности порта Azure RTOS

Надо сказать что эффективность RTOS сильно зависит от того как реализован ее порт (т.е. адаптация к конкретной архитектуре микроконтроллера и компилятора).

Порт может быть перегружен хуками и отладочными вставками, усложнен фичами планировщика, неэффективно использовать контроллер прерываний и не учитывать особенности компилятора. Т.е. у в принципе хорошей RTOS плохой порт может все испортить. В ThreadX с этим все в порядке, но все же вот несколько особенностей которые надо знать:

1. По дефолту не все функции стандартной библиотеки С могут быть безопасно использованы в вытесняющей RTOS . Объявление макроса TX_ENABLE_IAR_LIBRARY_SUPPORT в файле tx_user.h позволяет сделать стандартные библиотеки С в среде IAR мультипоточными. Для этого в ThreadX есть файл tx_iar.c

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

3. В ThreadX используется так называемая "ленивое" сохранение стека . Это когда регистры сопроцессора операций с плавающей точкой не сохраняются если такие операции не выполнялись в задаче перед переключением контекста. Отсюда следует что переключение контекста может замедлиться на время сохранения 33-х регистров FPU если задача использует операции с плавающей точкой. Но операции с сопроцессором FPU могут использоваться даже когда нет явного использования типов float или double. Это свойство оптимизирующего компилятора IAR. Поэтому для полной уверенности в быстроте ISR всегда следует просматривать ассемблерный код.

4. Общепринято защищать критические секции мьютексами или другими стандартными сервисами RTOS. Но выполнение таких сервисов может занимать сотни тактов. На некоторых этапах выполнения у них запрещаются любые прерывания. Это не хорошо если в системе присутствуют критические к времени отклика прерывания. Например прерывания по сигналам перенапряжения или перегрузки по току, которые должны что-то сделать в течении долей микросекунд от момента появления сигнала. Проблему можно решить используя вместо мьютексов простое поднятие приоритета выше приоритета прерывания системного тика и прерывания по вектору PendSV. Но для этого надо в файле tx_initialize_low_level.s поменять приоритеты этих прерываний как показано ниже:

    LDR     r1, =0xF0000000                         ; SVCl, Rsrv, Rsrv, Rsrv
    STR     r1, [r0, #0xD1C]                        ; Setup System Handlers 8-11 Priority Registers
                                                    ; Note: SVC must be lowest priority, which is 0xFF

    LDR     r1, =0xE0F00000                         ; SysT, PnSV, Rsrv, DbgM
    STR     r1, [r0, #0xD20]                        ; Setup System Handlers 12-15 Priority Registers
                                                    ; Note: PnSV must be lowest priority, which is 0xFF

Здесь системному тику присвоен приоритет 14, а PendSV приоритет 15. Если поднять приоритет выше 14 при входе в критическую секцию, то никакие задачи и сервисы RTOS не смогут прервать ее выполнение.

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

#define DISABLE_OS_PRI_LEV    (14 << (8 - __NVIC_PRIO_BITS))  // Маска приоритета в регистре BASEPRI запрещающая прерывания PendSV и  SysTick
#define ENABLE_OS_PRI_LEV     0   // Маска приоритета в регистре BASEPRI разрешающая прерывания с любыми приоритетами

#define  DISABLE_OS_INTERRUPTS __set_BASEPRI(DISABLE_OS_PRI_LEV)
#define  ENABLE_OS_INTERRUPTS  __set_BASEPRI(ENABLE_OS_PRI_LEV)

К слову сказать пока писал эту статью решил посмотреть что произошло за это время в репозитарии Azure RTOS. Оказалось что совсем недавно в версии 6.1.7 ребята пришли к аналогичной мысли и ввели макрос TX_PORT_USE_BASEPRI, а с ним и константу TX_PORT_BASEPRI. Если их объявить, то можно пользоваться стандартными макросами ThreadX для управления прерываниями TX_DISABLE и TX_RESTORE и они будут не запрещать прерывания, а только повышать приоритет до уровня TX_PORT_USE_BASEPRI . При этом в процедурах использующих эти макросы нужно в начале вставлять запись TX_INTERRUPT_SAVE_AREA.

Константа TX_PORT_BASEPRI имеет тот же смысл что и константа DISABLE_OS_PRI_LEV в примере выше.

Однако надо помнить что нельзя делать вызовы сервисов RTOS из прерываний с приоритетом выше TX_PORT_BASEPRI если мы объявили TX_PORT_USE_BASEPRI. Иначе это разрушит работу ядра из-за нарушения защиты критических секций в функциях сервисов.

Проект демонстрационной программы hellow_word находится здесь.

Включение профилирования RTOS

Профилирование дает возможность в среде отладчика IAR видеть статистику о ресурсах процессорного времени занимаемых задачами.

Для этого надо подключить к проекту два файла из репозитария: tx_execution_profile.c и tx_execution_profile.h. После этого в файл tx_user.h добавить объявление:

#define TX_EXECUTION_PROFILE_ENABLE

Целиком файл tx_user.h будет выглядеть так:

#define TX_MAX_PRIORITIES                       32
#define TX_DISABLE_PREEMPTION_THRESHOLD
#define TX_DISABLE_REDUNDANT_CLEARING
#define TX_DISABLE_NOTIFY_CALLBACKS
#define TX_NO_FILEX_POINTER
#define TX_TIMER_PROCESS_IN_ISR
#define TX_ENABLE_IAR_LIBRARY_SUPPORT
#define TX_ENABLE_STACK_CHECKING

#define TX_EXECUTION_PROFILE_ENABLE
#define TX_BLOCK_POOL_ENABLE_PERFORMANCE_INFO
#define TX_BYTE_POOL_ENABLE_PERFORMANCE_INFO
#define TX_EVENT_FLAGS_ENABLE_PERFORMANCE_INFO
#define TX_MUTEX_ENABLE_PERFORMANCE_INFO
#define TX_QUEUE_ENABLE_PERFORMANCE_INFO
#define TX_SEMAPHORE_ENABLE_PERFORMANCE_INFO
#define TX_THREAD_ENABLE_PERFORMANCE_INFO
#define TX_TIMER_ENABLE_PERFORMANCE_INFO

Сюда еще добавлены макросы для активизации инструментов измерения производительности сервисов RTOS.

Также надо добавить подключение #include "tx_user.h" в файл tx_thread_schedule.s

Убираем файл hello_world.c и вставляем в проект файл demo_threadx.c из репозитория ThreadX. Немного корректируем функцию main, вставив туда вызовы инициализации чипа. Этот демонстрационный файл создает 8 задач и по одной очереди, семафору, событию, пулу байтов, пулу блоков. Это дает возможность наблюдать за работой всех основных сервисов.

После запуска на плате под отладчиком IAR получаем очень подробную статистику по функционированию задач и сервисов RTOS:

Здесь видим сколько задач запущено, сколько каждая задача потребляет процессорного времени, сколько времени занимают прерывания, сколько стека использовано задачами и много другое. Видно что прерывание переключателя контекста PendSV длиться не дольше 1.7 мкс, а прерывание тика системы не дольше 8 мкс. Если период тиков 1 мс то эти все прерывания для 8-и задач занимают всего около 2% процессорного времени. Это и будут накладные на использование RTOS.

Проект демонстрационной программы RTOS_profiling находится здесь.

Итак, первичное исследование на плате контроллера резервного питания подтвердило возможность использования ThreadX из репозитария Azure RTOS в качестве основы для реализации управления с жестким реальным временем.

Ссылка на открытый проект: https://github.com/Indemsys/Backup-controller_BACKPMAN-v1.0