Здесь разберем следующие темы:

  • Специфика конфигурации Azure RTOS на платформе BACKPMAN v2.0 с микроконтроллером STM32H753.

  • Подключение на один порт USB одновременно трех разных интерфейсов: Mass Storage, Virtual COM, RNDIS 

  • Универсальный драйвер последовательного ввода-вывода способный работать через UART, USB,  Telnet, FreeMaster и прочие каналы связи. 

Репозиторий проекта со схемами, исходными кодами проектов и статьями: https://github.com/Indemsys/Backup-controller_BACKPMAN-v2.0

Краткое содержание предыдущих статей:

Была разработана схема универсального контроллера резервного питания. Потом разработана плата. Затем начата разработка софта и после изготовления платы отладка. Затем кризис поставок микросхем нарушил планы и плата была переделана под STM32H753 и получила номер версии 2.0. 

И вот снова перед нами не отлаженная плата и снова ставим на нее Azure RTOS. Для демонстрации этапа развертывания Azure RTOS был создан демо-проект Example1.

По началу  нам помог могучий инструмент  STM32CubeMX. Но по мере разработки программного проекта контроллера приходится все дальше отдаляться от архитектуры задаваемой при генерации исходников этим инструментом. Поэтому несмотря на то что файл Example1.ioc проекта STM32CubeMX  все еще находится в рабочей директории запускать генерацию из него нельзя. Он нужен только для того чтобы посмотреть в него и вспомнить настройки портов и тактирования, поскольку из содержимого исходников выяснить эти настройки довольно трудно.  

Специфика конфигурации Azure RTOS на платформе BACKPMAN v2.0

Сам базис приложения с Azure RTOS изначально был сгенерирован с помощью STM32CubeMX. Но затем была мной сильно изменена структура директорий поскольку совсем не понравилось как выполнена структура и именование директорий после STM32CubeMX. Далее было обнаружено что даже самый последний апгрейд STM32CubeMX не содержит самую последнюю версию Azure RTOS и дальше уже апгрейды Azure RTOS приходится делать вручную. 

STM32CubeMX освободил от многих нюансов портирования RTOS, но некоторые моменты в файле  tx_initialize_low_level.s все же пришлось изменить - это объявление системного тика , объявление начала свободной памяти и установка приоритетов системных прерываний.  

Выделение динамической памяти через файл линкера и стартовую процедуру RTOS

Память в микроконтроллере сильно фрагментирована. Поэтому для динамической памяти выбираем наиболее универсальную и наибольшую область памяти - AXI SRAM. 

В файле stm32h753xx_example1.icf объявляем в этой области секцию FREE_MEM

Далее в исходниках RTOS в файле tx_initialize_low_level.s присутствует размещение переменной _tx_free_memory_start  в секции FREE_MEM. Адрес этой переменной передается в виде аргумента в функцию tx_application_define.  И уже в реализации этой функции наше демо-приложение получает адрес для организации байтового пула динамической памяти.   Размер пула определяется границей области AXI SRAM -    AXI_SRAM_END

Управление размерами стеков и приоритетами задач

Часто встречается когда размеры стеков и приоритеты задач устанавливаются непосредственно в программных модулях организующих эти задачи. Это усложняет поиск и планирование всей архитектуры проекта и может приводить к ошибкам. Поэтому в данном проекте все стеки и приоритеты объявлены в одном файле - App.h

Дополнение управляющей структуры  RTOS полями с вспомогательными объектами.

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

Кастомные поля объявляются в файле tx_user.h в макросе TX_THREAD_USER_EXTENSION .  

#define TX_THREAD_USER_EXTENSION                ULONG environment; ULONG driver;

Переменная environment используется для передаче в управляющую структуру задачи указателя на другие управляющие структуры связанные с функционированием приложения, например управляющую структуру сервера VT100, который может присутствовать в виде отдельного экземпляра в нескольких задачах.  Переменная driver  используется для передачи указателя на API универсального драйвера последовательного ввода-вывода.  

Важно!

Для обеспечения работы с контроллером SD карты через его встроенный IDMA было отключено кэширование RAM.

Подключение на один USB порт интерфейсов Mass Storage, Virtual COM и RNDIS.

Стандартные драйвера сопровождающие примеры из STM32CubeMX были значительно отрефакторены и очищены от некоторых зависимостей и макросов.  

Важные изменения в исходном коде для правильной организации интерфейсов.

  1.  Чтобы стек USB мог организовать 3-и независимых устройства на одном USB порте  нужно переопределить макрос в файле ux_port.h:
    #define UX_MAX_SLAVE_CLASS_DRIVER                           3

  2. В файле USB_descriptors_builder.h назначаем желаемые VID и PID устройства и строковые дескрипторы.

  3. Назначаем адреса конечных точек всех интерфейсов. Адреса у всех точек должны быть уникальными и быть в диапазоне от 1 до 8 (без учета старшего бита направления) . Не имеет значение как будут распределены адреса: последовательно или хаотично.

  4. Объявляем массив интерфейсов  UserClassInstance в файле USB_descriptors_builder.c. Здесь следует оставить порядок такой какой выполнен в файле. Перенос интерфейса CLASS_TYPE_MSC в начало массива может вызвать неработоспособность композитного устройства. 

  5. Основная работа по конструированию дескрипторов находится в функции USBD_FrameWork_AddToConfDesc. Она рефакторится по необходимости для включения тех или иных интерфейсов.

  6. Непосредственно инициализация и запуск стека и интерфейса USB находится в функции  App_USBX_Device_Init.    

  7. В файле  usb_descriptor_examples.txt находятся примеры дескрипторов композитных устройств (Samsung S8 RNDIS descriptor in USB tethering mode и Beaglebone Board) на случай появления проблем дескрипторов и совместимости с PC.   

Чем RNDIS лучше виртуального COM порта (VCOM). 

При подключении через VCOM порт пользователю приходится самому искать и настраивать номер порта в клиентских программах на PC. Это несколько утомительно учитывая эффект периодического изменения номеров VCOM портов устройств при переподключении USB в разные разъемы. 

В случае  RNDIS сетевое соединение на PC устанавливается автоматически.  Клиентским программам  достаточно только слушать заданный TCP порт.   Вторым преимуществом является то, что устройство способно установить соединение с любым удаленным компьютером в сети, а не только с локальным к которому оно физически подключено.  

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

В дальнейшем проект будет дополняться WEB и FTP серверами через RNDIS. FTP сервер может как дублировать функции Mass Storage так и предоставлять доступ к нестандартной файловой системе реализованной на внутренней Flash микроконтроллера, чего не может сделать Mass Storage.

Настройки RNDIS в контроллере.

MAC адреса интерфейса контроллера и клиента  задаются в функции Register_rndis_class

Контроллер может работать в двух режимах назначения IP адресов. Режим устанавливается в параметрах переменной wvar.rndis_config.

Если переменной назначено значение RNDIS_CONFIG_PRECONFIGURED_DHCP_SERVER, то контроллер на старте включает DHCP сервер и назначает себе IP адрес 192.168.3.1, а клиенту (компьютеру, планшету, смартфону...) адрес 192.168.3.2.

Если переменной назначено значение RNDIS_CONFIG_WINDOWS_HOME_NETWORK, то контроллер назначает себе IP адрес 192.168.137.2, а клиенту адрес 192.168.137.1. DHCP сервер в этом случае не включается. Этот режим позволяет контроллеру через технологию Windows  Internet Connection Sharing выходить в интернет посредством подключенного компьютера. 

Назначение IP адресов происходит в функции _RNDIS_get_IP_proprties

Почему все же реализован один VCOM?

Потому что он нужен для работы протокола FreeMaster. О том что такое и чем удобен FreeMaster рассказано здесь.

Универсальный драйвер последовательного ввода-вывода

В малых RTOS типа Azure RTOS ThreadX нет концепции драйверов принятой в OS общего назначения типа Windows. Понятие драйвер здесь применено в том смысле что это некое программное обеспечение для доступа операционной системы к периферии. Скажем есть в операционной системе необходимость доступа к абстрактному носителю и  есть конкретный носитель. Но чтобы носитель выполнял команды и указания из абстрактного интерфейса системы нужен драйвер  переводящий эти указания в правильные числовые коды и протоколы взаимодействия. Сменой драйвера можно легко вместо одного носителя представить системе другой на других физических принципах и система этого вполне может не заметить если не интересуется этим специально. 

В Azure RTOS ThreadX и других малых RTOS такого нет, поскольку сама система настолько мала что ей не требуется самой по себе ни носителей, ни сетевых стеков ни других сложных стеков сопряженных с периферией. Системный таймер да несколько специфических процедур переключения контекста, учитывающих организацию регистров и контроллера прерываний, - вот все что RTOS нужно от периферии.    Но к Azure RTOS прилагается так называемое промежуточное программное обеспечение (middleware), которое включает файловую систему FileX, сетевой стек NetX Duo, графическую библиотеку GuiX, USB стек USBX и проч. Они являются частью пакета, но не являются неотъемлемой частью RTOS. Они имеют своё изолированное API и только пользуются некоторыми сервисами RTOS. У промежуточного программного обеспечения (ППО) Azure RTOS уже есть интерфейсы подключения драйверов, но у каждого компонента он специфичный и довольно плохо документированный. С  STM32CubeMX поставляются уже готовые драйверы для работы с SD/MMC картами памяти, с Ethernet и USB периферией, графическим контроллером. К остальной периферии предоставляются наборы  функций именуемые  HAL (hardware abstraction layer) and low-layer drivers, но не имеющие к RTOS никакого отношения. 

Но вот среди всего этого нет драйвера абстрагирующего сервер VT100 или например движок  FreeMaster от среды передачи.  Если работаем с UART, то нужно делать вызовы HAL, если через USB, то нужно вызывать функции стека USBX, если через канал Telnet, то нужно привлекать функции NetX Duo. Таким образом появляется зависимость исходников прикладного уровня VT100 от  низкоуровневой периферии, которая на каждой аппаратной платформе разная. Для абстрагирования от периферии и конкретных коммуникационных стеков  и была создана мной спецификация универсального последовательного драйвера для Azure RTOS. Особенностью моей спецификации является то что она примитивна. Я просто решил что универсальному драйверу нужно только пять функций: инициализаци, деинициализация, отсылка буфера, отсылка форматированной строки, прием символа. Никаких специфических IOControl и проч. 

Все какие нужны драйверу объявления сделаны в файле Serial_driver.h

Управляющая структура драйвера выглядит вот так:

typedef struct
{
    const uint32_t   mark;                                          // Magic word. Идентифицирует блок как управляющую структуру драйвера  
    const int        driver_type;                                   // Идентификатор типа драйвера
    int              (_init)(void **pcbl, void pdrv);               // Инициализация
    int              (_send_buf)(const void buf, unsigned int len); // Отсылка буфера с данными
    int              (_wait_char)(unsigned char b,  int ticks);     // Ожидание символа. ticks - время ожидания выражается в тиках (если 0 то без ожидания)
    int              (_printf)(const char , ...);                   // Вывод форматированной строки
    int              (_deinit)(void **pcbl);                        // Деинициализация
    void              pdrvcbl;                                      // Указатель на управляющую структуру необходимую для работы драйвера
} T_serial_io_driver;

А дальше программируется реализация драйверов для нужных интерфейсов.  Для Telnet драйвер реализован в файле Net_Telnet.c, для USB VCOM драйвер реализован в файле USB_CDC_driver.c. Для UART-а драйвера нет, поскольку на плате BACKPMAN v2.0 просто нет UART-а.

В задачу просто передаем указатель на драйвер, например в таком виде:

Task_VT100_create(Mnsdrv_get_usbfs_vcom0_driver(),0);

B дальше задача располагая только знанием об API этого абстрактного драйвера вызывает его инициализацию и ведет обмен. Драйвера реализуются используя сервисы RTOS и потому однотипных задач может работать несколько одновременно. Например контроллер может одновременно поддерживать независимые сессии VT100 с локальным компьютером через USB и с удаленным компьютером через  Telnet. 

На базе такого же драйвера организовано взаимодействие с движком FreeMaster.

И, как всегда, первое что мы делаем - это проверяем загрузку процессора при работающей RTOS. За измерение нагрузки процессора отвечает фоновая задача в файле  IDLE_task.c

Как видно при частоте системного тика 1000 Гц загрузка процессора не превышает 2.4%. Всплески же активности до 5% можно объяснить работой самого движка FreeMaster.Итак RTOS и отладочные коммуникации запущены. Работа над основной программой продолжается. 

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


  1. predator86
    11.10.2021 16:01

    Спасибо очень интересная статья.
    Есть так же пара вопросов:
    1) Динамическая память нужна для работы самой ОС или вы собираетесь использовать её для пользовательского кода?
    2) Постоянно 2.4% процессорного времени затрачивается только на ОС, или там есть ещё какие то постоянно работающие пользовательские задачи?


    1. Indemsys Автор
      11.10.2021 16:28

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

      middleware тоже организует свои пулы и не нуждается в пулах памяти предоставляемых пользователем.

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

      По второму вопросу есть некая неясность что считать затратами на ось.
      Можно сказать что сама ось вообще ничего не делает, она в sleep когда нет задач.
      Но я считаю такой расчет не верным.
      Я считаю что если есть задачи и middleware которые не реализуют полезный прикладной алгоритм, а нужны только для поддержки коммуникаций, сервисов реального времени и диагностики, то это издержки операционной системы

      Задач в демо-приложении вот столько:

      И вот это всё потребляет 2.5%

      Тут видно что основное время уходит на задачу IDLE и расчет в ней загрузки процессора.
      Не будь этой задач, то процент уменьшился до 1% , а не будь FreeMaster с USB то и меньше 0.5..0.2 % Но это все нужно, поэтому я это отношу на счет RTOS как издержки.


  1. predator86
    11.10.2021 16:36

    1.5% на расчёт загрузки процессора? Как производится этот расчёт?


    1. Indemsys Автор
      11.10.2021 16:44

      Это очень приблизительная цифра. И само измерение приблизительное.
      Оно основано на референсном значении времени в переменной ref_time.
      Референсное значение берется на старте RTOS когда еще не запущены никакие задачи и прерывания.

      /*-----------------------------------------------------------------------------------------------------
        Измеряем длительность интервала времени заданного в милисекундах
      -----------------------------------------------------------------------------------------------------*/
      uint64_t Measure_reference_time_interval(uint32_t time_delay_ms)
      {
        ULONG   tickt1;
        ULONG   tickt2;
        uint64_t diff;
      
        tickt1 = tx_time_get();
        DELAY_ms(time_delay_ms);
        tickt2 = tx_time_get();
      
        diff = ((tickt2 - tickt1)*1000000ull)/(TX_TIMER_TICKS_PER_SECOND);
        return diff;
      }
      
      /*-----------------------------------------------------------------------------------------------------
        Тело задачи IDLE
      -----------------------------------------------------------------------------------------------------*/
      static void Thread_idle(ULONG initial_input)
      {
        uint64_t  t;
        uint64_t  dt;
      
        for (;;)
        {
          t = Measure_reference_time_interval(REF_TIME_INTERVAL);
          if (t < ref_time)
          {
            dt = 0;
          }
          else
          {
            dt = t - ref_time;
          }
          g_cpu_usage =(1000ull * dt) / ref_time;
          g_cpu_usage_fp = (float)dt*100.0f/(float)ref_time;
        }
      }
      


      1. predator86
        11.10.2021 16:47

        Возможно ли измерить время точнее, например в микросекундах или вообще в тактах?


        1. Indemsys Автор
          11.10.2021 16:58

          Конечно можно.

          Вот в этой функции время измеряется с точностью до такта - Get_hw_timestump


          1. predator86
            11.10.2021 16:59

            И какой получился результат?


            1. Indemsys Автор
              11.10.2021 17:34

              Тут как в принципе неопределённости Гейзенберга, если точно знаете процент, то не знаете когда он был таким. Т.е. процент плавает. Лучше знать его максимальное значение. А для этого надо иметь максимальное количество задач и объектов синхронизации.
              Такой проект я еще не создал.


              1. predator86
                11.10.2021 17:35

                Понятно, спасибо.