В этом тексте я написал о некоторых трюках в организации кода для микроконтроллеров. Может при прочтении покажется, что это всё очевидно, однако за 12 лет я видел что-то похожее только в одном проекте, и то лишь от части. Итак, поехали...

Массивы конфигурационных структур

Дело в том, что для большинства прошивок конфиги на 100% статические. То есть конфиги для прошивки, как ни крути, известны до этапа компиляции программы. Да, это так...

Вот и получается что один из самых нормальных способов передавать конфиги в прошивки - это через переменные окружения. Про это у меня есть отдельный текст. https://habr.com/ru/articles/798213/. Это удобно с точки зрения масштабирования кодовой базы. Плюс в том, что переменные окружения можно определять прописывая прямо в скриптах (Make, CMake и т.п.).

Однако не всё удобно передавать через переменные окружения. Это происходит из-за того, что переменные конфигов имеют разные типы данных: целые числа, вещественные числа, комплексные числа, строки. К этому приходится приспосабливаться. Большинство просто добавляет тонны макроопределений и превращает свой код в свалку. Это не наш путь. Я решил пойти другим путём и остальные более детальные конфиги стал передавать через массивы структур. Вот как тут конфиг монохроматических светодиодов:

#include "led_mono_config.h"

#ifndef HAS_LED
#error "Add HAS_LED"
#endif /*HAS_LED*/

#include "data_utils.h"

const LedMonoConfig_t LedMonoConfig[] = {
    {
        .num = 1, .period_ms = 500,
        .phase_ms = 500,        .duty = 10,
        .pad = {.port = PORT_D, .pin = 15},
        .name = "Green",        .mode = LED_MODE_BAM,
        .active = GPIO_LVL_LOW,        .valid = true,
    },
    {
        .num = 2,        .period_ms = 1000,        .phase_ms = 0,
        .duty = 10,        .pad = {.port = PORT_D, .pin = 13},
        .name = "Red",        .mode = LED_MODE_OFF,
        .active = GPIO_LVL_LOW,        .valid = true,
    },
    {
        .num = 3,        .period_ms = 1000,
        .phase_ms = 0,        .duty = 10,
        .pad = {.port = PORT_D, .pin = 14},
        .name = "Yellow",        .mode = LED_MODE_PWM,
        .active = GPIO_LVL_LOW,        .valid = true,
    },
};

LedMonoHandle_t LedMonoInstance[] = {
    { .num = 1, .valid = true,   },
    { .num = 2, .valid = true,   },
    { .num = 3, .valid = true,   },
};

uint32_t led_mono_get_cnt(void) {
    uint32_t cnt = 0;
    uint32_t cnt1 = 0;
    uint32_t cnt2 = 0;
    cnt1 = ARRAY_SIZE(LedMonoInstance);
    cnt2 = ARRAY_SIZE(LedMonoConfig);
    if(cnt1 == cnt2) {
        cnt = cnt1;
    }
    return cnt;
}


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

Это ещё и потому удобно, что *.map файл покажет вам смещение в *.bin файле, где лежит эта структура. И вы сможете вручную аккуратно изменить константу перед прошивкой.

Массив функций инициализаций прошивки

Что такое инициализация? Это ведь упорядоченное множество Си-функций. То есть инициализация - это последовательность запуска Си-функций в правильном порядке. Только и всего... А почему бы тогда не организовать эту последовательность в массив функций? Да запросто... Вот.

#ifdef HAS_MICROCONTROLLER
#include "board_config.h"
#define BOARD_INIT                                                                                                     \
    { .init_function = board_init, .name = "board", },
#else /*HAS_MICROCONTROLLER*/
#define BOARD_INIT
#endif /*HAS_MICROCONTROLLER*/

/*Order matters!*/
define INIT_FUNCTIONS \
    MCAL_INIT         \
    HW_INIT           \
    INTERFACES_INIT   \
    PROTOCOLS_INIT    \
    CONTROL_INIT      \
    STORAGE_SW_INIT   \
    SW_INIT           \
    UNIT_TEST_INIT    \
    ASICS_INIT        \
    BOARD_INIT

/*Order matter!*/
const SystemInitInstance_t SystemInitInstance[] = {INIT_FUNCTIONS};

Выигрыш тут тройной:

1--Вся инициализация в одном месте.

2--Прядок инициализации определён индексом в массиве.

3--Для каждой функции инициализации легко выполнить один какой-то общий пролог и эпилог-код. Например печать порядкового номера или сброс сторожевого таймера.

Потом, прошивка должна печатать отчет о своей загрузке в UART или в SD карту.

Лог позволит анализировать ошибки в конфигурациях или брак в аппаратуре PCB (железе) еще до запуска суперцикла.

Массив функций для суперцикла

У каждой прошивки так или иначе есть суперцикл. Либо он прописан явно внутри main(), либо в составе bare-bone потока на какой-нибудь RTOS. Тут тоже, функции суперцикла можно объединить в массив структур. При этом каждую функцию можно пропускать через компонент limiter и, тем самым, вызывать её с определённым в конфиге периодом. Не чаще чем, скажем, 500ms.

поле структуры диспетчера

тип данных

1

указатель на Си-функцию

адрес в Flash

2

имя процедуры

текст

3

период с которым следует вызывать Си-функцию

натуральное число

Про это у меня есть отдельный текст. Называется Диспетчер Задач для Микроконтроллера https://habr.com/ru/articles/757000/


#ifdef HAS_BOARD_PROC
#include "board_at_start_f437.h"
#define BOARD_TASK {.name="board",   \
     .period_us=BOARD_POLL_PERIOD_US,      \
    .limiter.function=board_proc,},
#else
#define BOARD_TASK
#endif /**/

#define TASK_LIST_ALL     \
    ASICS_TASK            \
    APPLICATIONS_TASKS    \
    BOARD_TASK            \
    MCAL_TASKS            \
    TASK_CORE             \
    COMPUTING_TASKS       \
    CONNECTIVITY_TASKS    \
    CONTROL_TASKS         \
    SENSITIVITY_TASKS     \
    STORAGE_TASKS

TaskConfig_t TaskInstance[] = {
    TASK_LIST_ALL
};

Массив параметров в NVRAM

Любой прошивке надо запоминать какие-то параметры в энергонезависимой памяти. Это происходит по разным причинам. Про это есть отдельный текст: NVRAM для микроконтроллеров https://habr.com/ru/articles/706972/ У каждого NVRAM параметра есть минимум такие свойства как

свойство NVRAM записи

тип данных

1

размер

натуральное число байт

2

имя

текстовая строка

3

тип данных

перечисление (целое число)

4

адрес в NVRAM

целое число (0<=)

5

принадлежность к SW компоненту

перечисление (целое число)

Поэтому в прошивке определяем массив структур, который явно покажет с какими параметрами прошивка будем работать после запуска.


const ParamItem_t ParamArray[] = {
    FLASH_FS_PARAMS
    PARAMS_GNSS
    IWDG_PARAMS
    PARAMS_PASTILDA
    PARAMS_SDIO
    PARAMS_TIME
    PARAMS_BOOTLOADER
    {.facility=BOOT, .id=PAR_ID_BOOT_CMD, .len=1, .type=TYPE_UINT8, .name="BootCmd"}, /*num*/
    {.facility=BOOT, .id=PAR_ID_REBOOT_CNT, .len=2, .type=TYPE_UINT16, .name="ReBootCnt"}, /*num*/
    {.facility=SYS, .id=PAR_ID_SERIAL_NUM, .len=4, .type=TYPE_UINT32, .name="SerialNum"},  /**/
};

Массив отладочных токенов для диагностики

Как правило в прошивках есть UART для printf() отладки. Одновременно с этим, каждая взрослая прошивка состоит из десятков программных компонентов. Для того, чтобы отличать какому именно программному компоненты принадлежат те или иные отладочные сообщения в коде прошивки должно быть определено перечисление, где каждому программному компоненту будет присвоено натуральное число. Таким образом, при печати лога (в UART или SD карту) каждому такому числу ставится в соответствие текстовый токен. Поэтому в прошивке должен быть определен массив структур, где каждая структура ставит в соответствие числу его текстовый токен.

const static FacilityInfo_t FacilityInfo[] = {

#ifdef HAS_AES
    { .facility = AES, .name = "AES", },
#endif /*HAS_AES*/

#ifdef HAS_AD9833
    { .facility = AD9833, .name = "AD9833", },
#endif /*HAS_AD9833*/

#ifdef HAS_NOR_FLASH
    { .facility = NOR_FLASH, .name = "NorFlash", },
#endif /*HAS_NOR_FLASH*/
 
#ifdef HAS_NVRAM
    { .facility = NVRAM, .name = "NvRam", },
#endif /*HAS_NVRAM*/ 
  ....
};

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

Удобно? Очень!

Массив команд для CLI

В каждой нормальный взрослой прошивке есть UART-CLI. Для отладки очень полезна UART-CLI. Функции CLI тоже складируем штабелями в массив структур. Подробнее про это можно почитать в тексте: Почему Нам Нужен UART-Shell? https://habr.com/ru/articles/694408/

#define CLI_CMD(LONG_CMD, SHORT_CMD, FUNC)                                 \
    { .short_name = SHORT_CMD, .long_name = LONG_CMD, .handler = FUNC }    

bool nau8814_i2c_ping_command(int32_t argc, char* argv[]);
bool nau8814_reg_map_command(int32_t argc, char* argv[]);

#define NAU8814_COMMANDS          \                        \             
    NAU8814_DAC_COMMANDS                 \                           
    NAU8814_ADC_COMMANDS                    \                        
    CLI_CMD("nau8814_ping", "nap", nau8814_i2c_ping_command ),  \      
    CLI_CMD("nau8814_map", "nrm", nau8814_reg_map_command ),        

#define CLI_COMMANDS      \                                                
  ASICS_COMMANDS          \                                         
  APPLICATIONS_COMMANDS   \                                      
  CONTROL_COMMANDS        \                                   
  CONNECTIVITY_COMMANDS   \                                
  COMPUTING_COMMANDS      \                             
  MCAL_COMMANDS           \                          
  MULTIMEDIA_COMMANDS     \                       
  PROTOTYPE_COMMANDS      \                    
  STORAGE_COMMANDS        \                                                
  SENSITIVITY_COMMANDS  

const CliCmdInfo_t CliCommands[] = {CLI_COMMANDS};

Массив функций-модульных тестов

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


bool test_c_types(void) {
    LOG_INFO(TEST, "%s()..", __FUNCTION__);
    bool res = true;
 
    EXPECT_EQ(4, sizeof(long));
    EXPECT_EQ(4, sizeof(1UL));
    EXPECT_EQ(4, sizeof(1L));
    EXPECT_EQ(4, sizeof(size_t));
    EXPECT_EQ(4, sizeof(1l));
 
    EXPECT_EQ(4, sizeof(1));
    EXPECT_EQ(4, sizeof(-1));
    EXPECT_EQ(4, sizeof(0b1));
    EXPECT_EQ(4, sizeof(1U));
    EXPECT_EQ(4, sizeof(1u));
    EXPECT_EQ(4, sizeof(1.f));
 
    LOG_INFO(TEST, "%s() Ok", __FUNCTION__);
    return res;
}

#define TEST_SUIT_SW                                                  \
    {"array_init", test_array_init},                                  \
    {"bit_fields", test_bit_fields},                                  \
    {"c_types", test_c_types},                                        \
    {"memset", test_memset},                                          \
    {"memcpy", test_memcpy},                                          \
    {"bit_shift", test_bit_shift},                                    \
    {"int_overflow", test_int_overflow},                              \
    {"sprintf_minus", test_sprintf_minus},                            \
    {"endian", test_endian},                                          

/*Compile time assemble array */
const UnitTestHandle_t TestArray[] = {
#ifdef HAS_SW_TESTS
    TEST_SUIT_SW
#endif /*HAS_SW_TESTS*/

#ifdef HAS_HW_TESTS
    TEST_SUIT_HW
#endif /*HAS_HW_TESTS*/

};

Массив функций потоков для RTOS

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

Вот как тут.

#include "FreeRTOSConfig.h"

#include "free_rtos_drv.h"
#include "data_utils.h"
#ifdef HAS_KEEPASS
#include "keepass.h"
#endif /*HAS_KEEPASS*/

const RtosTaskConfig_t RtosTaskConfig[] = {
    { .num=1, .TaskCode=bare_bone, .name="BareBone", 
      .stack_depth_byte=2048, .priority=PRIORITY_LOW, .valid=true,},
  
    { .num=2, .TaskCode=default_task, .name="DefTask", 
      .stack_depth_byte=256, .priority=PRIORITY_LOW, .valid=true,},
  
#ifdef HAS_KEEPASS
    { .num=3, .TaskCode=keepass_proc_task, .name="KeePass", 
      .stack_depth_byte=1024, .priority=PRIORITY_LOW, .valid=true,},
#endif /*HAS_KEEPASS*/
};

RtosTaskHandle_t RtosTaskInstance[] = {
    { .num=1, .valid=true,  },
    { .num=2, .valid=true,  },
#ifdef HAS_KEEPASS
    { .num=3, .valid=true,  },
#endif
};

uint32_t rtos_task_get_cnt(void){
    uint32_t  cnt  = 0 ;
    uint32_t  cnt1  = 0 ;
    uint32_t  cnt2  = 0 ;
    cnt1 = ARRAY_SIZE(RtosTaskConfig);
    cnt2 = ARRAY_SIZE(RtosTaskInstance);
    if(cnt1==cnt2) {
        cnt = cnt1;
    }
    return cnt;
}

Таким образом вы будете помнить про все потоки в данной сборке.

Итоги

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

массив

Количество элементов на SW компонент

1

функций инициализации

1

2

функций суперцикла\планировщика

несколько

3

параметры NVRAM

несколько

4

номеров и их токенов для логирования

1

5

команды CLI

несколько

6

потоков RTOS

1 или несколько

7

функций модульных тестов

несколько

А теперь внимание.. Формирование этих всех массивов можно организовать на этапе отработки препроцессора! Да... Переменные окружения вызывают нужные скрипты сборки. Скрипты сборки передают макросы препроцессора. Препроцессор выбирает нужный код. Компилятор собирает только нужный код. Easy!

Благодаря тому что у вас каждая сущность хранится в массиве Вы можете также в RunTime проверять конфиги на наличие дубликатов, найти конфликты и прочее.

Можно и вовсе справедливо заметить, что любая программа как машинный код - это не что иное как массив assembler инструкций в ROM памяти. А микропроцессор - это эдакая электрическая цепочка, которая просто исполняет одну инструкцию за другой из ROM, пока не достигнет конца массива инструкций. Вот так, господа...

В сухом остатке, благодаря организации всех программных сущностей в массивы у Вас прошивка, как кристалл растёт из одной исходной точки. У вас одна точка отсчета для техподдержки и масштабирования проекта.

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

Словарь

Акроним

Расшифровка

NVRAM

Non-Volatile Random-Access Memory

CLI

Command line interface

UART

Universal Asynchronous Receiver-Transmitter

RTOS

real-time operating system

Ссылки

#

название

URL

1

NVRAM для микроконтроллеров

https://habr.com/ru/articles/706972/

2

NVRAM Поверх off-chip SPI-NOR Flash

https://habr.com/ru/articles/732442/

3

Почему Нам Нужен UART-Shell?

https://habr.com/ru/articles/694408/

4

Автоматическая Генерация Конфигураций для Make Сборок

https://habr.com/ru/articles/798213/

5

11 Aтрибутов Хорошего Firmware

https://habr.com/ru/articles/655641/

6

51 Атрибут Хорошего С-кода

https://habr.com/ru/articles/679256/

7

16 Способов Отладки и Диагностики FirmWare

https://habr.com/ru/articles/681280/

8

Архитектура Хорошо Поддерживаемого драйвера

https://habr.com/ru/articles/683762/

9

Что Должно Быть в Каждом FirmWare Pепозитории

https://habr.com/ru/articles/689542/

10

Почему важно собирать код из скриптов

https://habr.com/ru/articles/723054/

11

Модульное Тестирование в Embedded

https://habr.com/ru/articles/698092/

12

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

https://habr.com/ru/articles/757000/

13

23 Атрибута Хорошего Загрузчика

https://habr.com/ru/articles/754216/

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


  1. lolikandr
    29.05.2024 18:59
    +2

    Вместо

        MCAL_INIT         \
        HW_INIT           \
        INTERFACES_INIT   \
    

    я предпочту

    mcal_init();
    hw_init();
    interfaces_init();
    


    1. aabzel Автор
      29.05.2024 18:59

      Ваш подход исключает возможность кодо генерации препроцессором срр.exe из конфигов.


  1. RTFM13
    29.05.2024 18:59

    Что касается конфигов - понравилось как в esp-idf прикрутили kconfig.

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


    1. aabzel Автор
      29.05.2024 18:59
      +1

      Существует ли stand alone kconfig.exe для любых проектов без esp32?


      1. RTFM13
        29.05.2024 18:59
        +2

        На счет exe не уверен, но можно посмотреть раз, два

        Kconfig считается устаревшей вещью.

        не удивительно.

        Ей на замену приходит DeviceTree

        Посмотрю, спасибо, пока не сталкивался.


    1. aabzel Автор
      29.05.2024 18:59

      Вообще Kconfig считается устаревшей вещью. Ей на замену приходит DeviceTree.
      Zephyr Project c Nrf5340 уже так и делают.


      1. rukhi7
        29.05.2024 18:59
        +1

        это говорит о том что массивы маленько тоже устарели. Им на смену уже вроде как пришли деревья (Tree) во многих применениях.


        1. aabzel Автор
          29.05.2024 18:59

          Вы видели stand alone DevTree компилятор для произвольных Win проектов?

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

          А язык Си простой, притом на нем еще и сам код пишут. Почему бы тогда на Си и сами конфиги не писать?


          1. rukhi7
            29.05.2024 18:59
            +2

            А язык Си простой, притом на нем еще и сам код пишут. Почему бы тогда на Си и сами конфиги не писать?

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


      1. aectaan
        29.05.2024 18:59

        Внезапно Kconfig из зефира никуда не делся. Используется и то и то, только для разных целей


        1. aabzel Автор
          29.05.2024 18:59

          Существует ли stand alone kconfig.exe для любых Win проектов без Zephyr RTOS?

          В доках Zephyr упоминается, что Kconfig это пережиток прошлого, который сообщество собирается со временем полностью заменить на DevTree 


  1. latonita
    29.05.2024 18:59
    +1

    В попытках прочитать примеры сильно пугает микс из snake_case и CamelCase


    1. aabzel Автор
      29.05.2024 18:59

      Про оформление кода у меня есть отдельный текст. Называется
      51 Атрибут Хорошего С-кода

      https://habr.com/ru/articles/679256/


  1. Yuri0128
    29.05.2024 18:59
    +1

    Тута у вас то ли ошибка, то ли неточность:

    4

    адрес в NVRAM

    натуральное число

    адрес может быть равным 0, который не входит во множество натуральных чисел.

    А по факту - интересно, но довольно громоздко. Ну и не всегда конфиг известен при прошивке, ой не всегда. Иногда прошивка на одном посту а запись конфига вообще за 3 ккм где-то....


    1. Vladislav_Dudnikov
      29.05.2024 18:59
      +1

      Во французской школе (см. Бурбаки) 0 включают во множество натуральных чисел.


      1. Yuri0128
        29.05.2024 18:59

        Я чего-то думаю, что большинство читателей ХАБРа учились не во французской школе....

        Да по памяти - все-же немецкая школа дала основные положения по натуральным числам.


  1. maxwolf
    29.05.2024 18:59
    +1

    1. aabzel Автор
      29.05.2024 18:59

      Ccылка не открыватеся


      1. maxwolf
        29.05.2024 18:59
        +1

        Прошу прощения. Что-то где-то взглючило, и последняя буква в ссылке заменилась кракозябрами. А я не проверил :(
        Правильная ссылка http://lib.ru/ANEKDOTY/non_pas.txt


  1. Indemsys
    29.05.2024 18:59

    Что это !?

    /*Order matters!*/
    define INIT_FUNCTIONS \
        MCAL_INIT         \
        HW_INIT           \
        INTERFACES_INIT   \
        PROTOCOLS_INIT    \
        CONTROL_INIT      \
        STORAGE_SW_INIT   \
        SW_INIT           \
        UNIT_TEST_INIT    \
        ASICS_INIT        \
        BOARD_INIT

    Спрашивается, как узнать из этого кода какой порядок вызовов правильный?

    И где тут те макросы которые были тут

    #ifdef HAS_AES
        { .facility = AES, .name = "AES", },
    #endif /*HAS_AES*/
    
    #ifdef HAS_AD9833
        { .facility = AD9833, .name = "AD9833", },
    #endif /*HAS_AD9833*/
    
    #ifdef HAS_NOR_FLASH
        { .facility = NOR_FLASH, .name = "NorFlash", },
    #endif /*HAS_NOR_FLASH*/
    

    Чтобы такое неприличное шаманство не делать, первым вызовом идет запуск RTOS.

    А последним на старте идет ожидание флагов инициализации всех задач периферии.

    Wait_app_event(EVENT_GUI_TASK_READY
                     | EVENT_LEDS_TASK_READY
                     | EVENT_CAN_TASK_READY
                     | EVENT_INPUTS_TASK_READY
                     | EVENT_OUTPUTS_TASK_READY
                     | EVENT_NET_TASK_READY,
                     TX_AND, TX_WAIT_FOREVER);

    Вся инициализация идёт максимально параллельно в асинхронных функциях. И тогда мистический порядок вызовов не нужен и массив не нужен.
    А иначе будет не embedded дивайс, а черепаха с временем загрузки сравнимым с Windows.

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


    1. aabzel Автор
      29.05.2024 18:59

      Что это !?

      Это перечень элементов массива структур: указатель на функцию инициализации + имя функции.

      Спрашивается, как узнать из этого кода какой порядок вызовов правильный?

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


      1. Indemsys
        29.05.2024 18:59
        +2

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

        Факт в том, что массивы тоже иногда неуместны.


        1. aabzel Автор
          29.05.2024 18:59

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

          Да есть такое.
          Не всегда очевидна оптимальная последовательность инициализации системы.

          Но к этой проблеме можно подойти с точки зрения дискретной математики.

          Постановка задачи
          Есть неупорядоченное множество компонентов. Просто набор строк (токенов).

          start_pause,Writer,Log,Int,Clk,SysTick,Flash,Timer,GPIO,UART,Swd,ADC,AdcChannels,I2C,SPI,CAN,I2S,WatchDog,Pwm,DMA,DmaChannels,Mcu,DACsw,RS232,RS485,StringReader,CLI,iso_tp,UDS,DID,Relay,LedMonoInit,NVS,Flash_FS,Param,ADT,Heap,TIME,SuperCycle,task,Button,HwInit,UnitTest,Nau8814,board




          У каждого компонента есть зависимости. Как правило несколько. У каких-то тривиальных компонентов нет зависимостей.

          start_pause,Clk
          Writer,UART
          Log UART
          Clk,
          Flash
          SysTick,Clk
          Timer, Clk
          GPIO,
          UART, GPIO
          Swd,GPIO...
          ADC,AdcChannels,..
          I2C,GPIO UART
          SPI,GPIO UART
          CAN,GPIO
          I2S,GPIO DMA
          WatchDog,
          RS232,GPIO UART
          RS485,GPIO
          StringReader,
          CLI,RS232
          Relay,GPIO Time
          LedMonoInit,
          NVS,Flash
          Flash_FS, NVS Flash
          Param, Flash_FS
          SuperCycle,
          task, SuperCycle
           
           



          Получается граф зависимостей программных компонентов.



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

          То есть обойти граф в порядке увеличения количества зависимостей.

          --------------------
          Вот только типичный российский программист микроконтроллеров решить такую сложную задачу дискретной оптимизации не сможет.


          1. Indemsys
            29.05.2024 18:59

            Вы изобразили скопление простейшей периферии. Задачу очередности включения периферии сейчас легко решают генераторы кода типа ModusToolbox, STM32CubeMX, e2studio и т.п. Тут как раз проблем нет.

            Я говорил о задачах уровня middleware, которые надо поднимать после периферии. Такие как: стеки полевых шин, TCP стек и все прикладные протоколы поверх него, логеры, файловые системы и USB классы, BLE профили, GUI и проч.
            Там не построить каких-то простых однозначных графов. Поскольку эти задачи могут давать отказы на старте и их приходится деинициализировать и снова инициализировать. Какие-то, как файловые системы, могут уходить в долгое восстановление. Тут графы не работают, а массивы вредны. Только дебагинг и постоянный рефакторинг. Да еще не забыть, что задачи могут отключать, реинициализировать и включать периферию. Такой вообще не место в массивах.


            1. aabzel Автор
              29.05.2024 18:59

              ModusToolbox, STM32CubeMX, e2studio - это всё платформа зависимые утилиты. Они полезны чтобы научится пользоваться вендоровским HALом. Только и всего.

              А репозиторий с кодом надо организовать так, чтобы можно было работать с микроконтроллерами разных производителей и быстро мигрировать в случае надобности на микроконтроллеры других вендоров.

              Сегодня вы работаете с STM32, следующий MDR32, параллельно на вас висит сборка под ESP32, далее вы работаете с SPC58, после NRF53, потом с CC2642, А теперь Artery. На очереди микроконтроллеры от Nuvoton Technology, GigaDev, Holtec и прочие.

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

              В таких условиях нет смысла привязываться к какому-то там STM32CubeMX. Понимаете?

              Надо просто cделать, к примеру, одну

              bool uart_mcal_send(uint8_t num, const uint8_t* const data, size_t len);
              которая будет везде делать одно и тоже.

              И так же с инициализацией.

              Надо строить своё приложение на каком-то более высокоуровневом API и не привязываться к STшному SPL или HAL.

              Посмотрите как сделан Zephyr Project, в качестве иллюстрации.


          1. lolikandr
            29.05.2024 18:59
            +1

            А вот кодогенерация для корректного обхода графа - достойна отдельной статьи. И, видимо, уже из неё (кодогенерации) как следствие будут нужны массивы функций, описанные тут.


    1. aabzel Автор
      29.05.2024 18:59

      первым вызовом идет запуск RTOS.

      Не всегда в прошивках разрешена RTOS