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

Если вы выбрали неверный порядок инициализации программы, то прошивка заклинит в run-time.

В переводе на кухонный язык. Нет смысла инициализировать UART, если не проинициализирован GPIO. Это очевидно. Однако так бывает не всегда. Когда программа построена из огромного количества программных компонентов (SWC) (десятки и сотни) и каждый программный компонент имеет зависимости от ещё нескольких программных компонентов, то тут в уме выявить корректную последовательность инициализации всей прошивки, простите, просто нереально...

Программа состоит из программных компонентов. Программные компоненты связаны друг с другом отношением зависимости.

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

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

При этом у нас система сборки и так автоматически компонует граф зависимостей между программными компонентами. Вот текст про это.

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

Хорошая новость в том что оказывается для топологической сортировки существует отдельная бесплатная утилита tsort.exe. Она входит в состав пакета CygWin и лежит в файловой системе по адресу

C:\cygwin64\bin\tsort.exe

Надо только как-то состыковать Graphviz файл с утилитой tsort и проблема решится сама собой.

Каков план?

1--Написать дерево зависимостей программных компонентов на языке Graphviz (*.gv файл). Не важно как: вручную или автоматически .

2--Произвести автоматический синтаксический разбор Graphviz *.gv файла и составить файл-конфиг (tsort.txt) со списком смежности специально для утилиты tsort.exe

3--Удалить повторения в tsort.txt утилитой uniq.exe или sort.exe c опцией -u

4--Произвести топологическую сортировку графа зависимостей программных компонентов утилитой tsort.exe. Получить файл *.init

5--Составить массив инициализации для прошивки в микроконтроллер

Схематично этот план можно показать вот так.

Сложность тут лишь в том, что надо из GraphViz кода сформировать список смежности для утилиты tsort.

По сути, надо сделать некоторый компилятор языка GraphViz.

Вот пример типичного дерева зависимостей программных компонентов прошивки на языке GraphViz
strict digraph graphname {
    rankdir=LR;
    splines=ortho
    node [shape="box"];
CORTEX_M4->NVIC
FPU->CORTEX_M4
THUMB->CORTEX_M4
NVIC->CORTEX_M4
SYSTICK->CORTEX_M4
subgraph cluster_mcal{
    label = "MCAL";
    style=filled;
    color=oldlace;
    ADC [shape = note][fillcolor = aquamarine][style="filled"]
    DMA [shape = note][fillcolor = aquamarine][style="filled"]
    FLASH [shape = note][fillcolor = aquamarine][style="filled"]
    GPIO [shape = note][fillcolor = aquamarine][style="filled"]
    I2C [shape = note][fillcolor = aquamarine][style="filled"]
    I2S [shape = note][fillcolor = aquamarine][style="filled"]
    PWM [shape = note][fillcolor = aquamarine][style="filled"]
    SPI [shape = note][fillcolor = aquamarine][style="filled"]
    TIMER [shape = note][fillcolor = aquamarine][style="filled"]
    UART [shape = note][fillcolor = aquamarine][style="filled"]
}
REG->ADC
GPIO->ADC
NVIC->ADC
REG->DMA
TIME->DMA
ARRAY->FLASH
REG->FLASH
TIME->GPIO
REG->GPIO
REG->I2C
GPIO->I2C
NVIC->I2C
DFT->I2S
SW_DAC->I2S
DMA->I2S
CRC->NVS
FLASH->NVS
INTERVALS->NVS
TIMER->PWM
TIME->SPI
GPIO->SPI
NVIC->TIMER
REG->TIMER
MATH->TIMER
TIME->UART
FIFO->UART
GPIO->ADC
TIME->ADC
PLL->CLOCK
NVIC->TIMER
CLOCK->TIMER
DMA->UART [color = red]
FIFO->UART [color = blue]
UART->LOG
subgraph cluster_adc{
    label = "ADT";
    style=filled;
    color=oldlace;
        ARRAY [shape = note][fillcolor = aqua][style="filled"]
        FIFO [shape = note][fillcolor = aqua][style="filled"]
}
RAM->ARRAY [color=blue]
RAM->FIFO
subgraph cluster_Connectivity{
    label = "Connectivity";
    style=filled;
    color=oldlace;
    STREAM
    LOG
subgraph cluster_Interfaces{
    label = "Interfaces";
    color=red
GPIO->CAN
REG->CAN
NVIC->CAN
ARRAY->RS485
FIFO->RS485
GPIO->RS485
UART->RS485
UART->RS232
FIFO->RS232
ARRAY->RS232
}
subgraph cluster_protocols{
    label = "Protocols";
    style=filled;
    color=oldlace;
        CLI;
        ISO_TP;
        UDS;
}
    UART->STREAM [color=chartreuse]
    RS232->STREAM [color=chartreuse]
    FIFO->STRING_READER [color=aqua]
    STRING->CLI [color=aqua]
    STRING_READER->CLI [color=aqua]
    STREAM->CLI [color=aqua]
    LOG->CLI [color=aqua]
    CSV->CLI [color=aqua]
CAN->ISO_TP [color="green"]
ISO_TP->UDS [color="blue"]
NVRAM->UDS [color="red"]
ARRAY->UDS [color="yellow"]
}
TIME->LOG
STREAM->LOG
FIFO->LOG
UART->LOG
subgraph cluster_control{
    label = "Control";
    style=filled;
    color=oldlace;
BOOT
DEBUGGER
LED
RELAY
SUPER_CYCLE
SYSTEM
TASK
}
PARAM->BOOT
FLASH->BOOT
FLASH->DEBUGGER
GPIO->LED
GPIO->LED_MONO
TIME->LED_MONO
MATH->LED_MONO
TIME->LOG
STREAM->LOG
FIFO->LOG
UART->LOG
GPIO->RELAY
TIME->RELAY
TASK->SUPER_CYCLE
TASK [label="Scheduler"]
LIMITER->TASK[color="green"]
TIME->TASK[color="red"]
FLASH->TASK[color="blue"]
subgraph cluster_Computing{
    label = "Computing";
    style=filled;
    color=oldlace;
 COMPLEX
    CRC32
    CRC8
    CRC16
    DFT
    LIMITER
    INTERVAL
    MATH
    SOLVER
    SW_DAC
}
MATH->COMPLEX
VECTOR->COMPLEX
RAM->CRC8
RAM->CRC16
RAM->CRC32
TIME->LIMITER
FLASH->LIMITER
RAM->INTERVAL
ASM->MATH
FLOAT->MATH
    MATH->VECTOR
LIFO->SOLVER
STRING->SOLVER
ARRAY->SW_DAC
MATH->SW_DAC
TIME->SW_DAC
HEAP->SW_DAC
subgraph cluster_storage{
    label = "Storage";
    style=filled;
    color=oldlace;
    RAM
    REG
    ALLOCATOR
    NVS
    FLASH
FLASH_FS
    PARAM
}
RAM->ALLOCATOR
FLASH_FS->PARAM
PARAM->NVRAM
NVS->FLASH_FS
CRC8->FLASH_FS
FLASH->NVS

I2C->NAU8814
I2S->NAU8814
SW_DAC->I2S
LOG->NAU8814
PWM->NAU8814
}

Почему для описания графа зависимостей выбран именно язык Graphviz ?

Ответ прост. Для языка Graphviz есть утилита render dot.exe, которая сама компонует и трассирует граф в различной топологии. Это очень удобно для визуализации данных и экономит тонну времени и сил.

Компилятор Graphviz

Задача компилятора Graphviz - построить список смежности. При этом предполагается, что в исходном *.gv файле все зависимости имеют простейший вид.

SOURCE->DESTINATION\n
SOURCE -> DESTINATION \n
SOURCE->DESTINATION   some text \n

Как известно, текстовые паттерны отлично распознаются конечными автоматами. Вот такой конечный автомат может выхватывать указанный синтаксис.

Я написал на Си программную смесь, которая обрабатывает graphviz файл и, таким образом, на выходе получается файл вида.

ADC GPIO
ADC NVIC
ADC REG
...
...
UART GPIO
UART TIME
UDS ARRAY
UDS ISO_TP
UDS NVRAM
VECTOR MATH

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

На самом деле вовсе не обязательно писать и собирать программную смесь для анализа *.gv файла. Можно воспользоваться тандемом утилит dot.exe и awk. Вот так.

dot -Tplain at_start_f437_wm8731_m.gv | gawk '/^edge / { print $3 " " $2 }' >  adjacency_list.txt

Тут опция -Tplain означает Simple, line-based language. Своего рода, огромный комментарий к коду на dot.

Запуск программы tsort.exe

Важно чтобы в том файле, что поступает на вход утилиты tsort не было символа \r (возврат каретки). Иначе произойдет аварийный запуск и утилита выдаст неправильный результат. В качестве разделителя строк надо использовать символ \n (перенос строки). Вот так.

Отладка

Вот я запустил на исполнение свою утилиту graphviz_to_tsort.exe

Утилита легко встраивается в общий скрипт сборки вот таким make скриптом

$(info auto_init_script)

$(info WORKSPACE_LOC=$(WORKSPACE_LOC))
WORKSPACE_LOC := $(subst /cygdrive/c/,C:/, $(WORKSPACE_LOC))
$(info WORKSPACE_LOC=$(WORKSPACE_LOC))
#$(error WORKSPACE_LOC=$(WORKSPACE_LOC))

GRAPHVIZ_TO_TSORT_TOOL=$(WORKSPACE_LOC)../tool/graphviz_to_tsort.exe
$(info GRAPHVIZ_TO_TSORT_TOOL=$(GRAPHVIZ_TO_TSORT_TOOL))

.PHONY: auto_init
auto_init: $(SOURCES_DOT_RES)
	$(info RunAutoInit...)
	cd $(MK_PATH_WIN) && $(GRAPHVIZ_TO_TSORT_TOOL) gts $(SOURCES_DOT_RES)

После исполнения make all, ко всему прочему, я также автоматически получил вот такую рекомендованную последовательность инициализации для сборки at_start_f437_generic_monolithic_m

REG FPU, NVIC, SYSTICK, THUMB, CORTEX_M4, PLL, ASM, FLOAT, 
CLOCK, MATH TIMER, TIME RAM DMA, ARRAY MCU, CRC, FLASH, 
INTERVALS, FIFO, GPIO, CRC8, NVS, UART, FLASH_FS, ALLOCATOR, HEAP, 
RS232, CAN, PARAM, LIMITER, DFT, SW_DAC, STREAM, ISO_TP, NVRAM, TASK, 
LIFO, STRING, I2C, I2S, LOG, PWM, VECTOR, CSV, STRING_READER, UDS, 
SUPER_CYCLE, SPI, SOLVER, RS485, RELAY, NAU8814, LED_MONO, LED, INTERVAL,
DEBUGGER, CRC32, CRC16, CLI, BOOT, ADC

Выглядит вполне разумно.

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

вот так

dot -Tplain at_start_f437_m_dep.gv | gawk '/^edge / { print $3 " " $2 }' | sort -u | tsort | tac > init_order.txt

Easy!

Итоги

Вопрос проектирования функции инициализации можно считать закрытым. Это оказывается чистая механика и тут абсолютно нет места импровизации. Утилиты CygWin, как лакмусовая бумажка покажут подлинную последовательность запуска всей системы.

Используя GraphViz для графа зависимостей мы убиваем сразу трех зайцев:

1--Получаем возможность синтезировать документацию автоматически утилитой препроцессора cpp.exe

2--Получаем возможность визуализировать граф утилитой dot.exe

3--Получаем возможность производить топологическую сортировку для выявления подлинной процедуры инициализации всей системы утилитами awk.exe + tsort.exe

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

Ссылки

Название текста

URL

1

Как составить функцию инициализации микроконтроллера (Топологическая сортировка графов утилитой Make)

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

2

Генерация зависимостей внутри программы

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

3

Topological Sort

https://www.manniwood.com/2019_09_08/tsort.html

4

Graphviz Visual Editor

5

Remove a carriage return with sed

https://unix.stackexchange.com/questions/170665/remove-a-carriage-return-with-sed

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


  1. Flammmable
    27.07.2024 11:26

    Нет смысла инициализировать UART, если не проинициализирован GPIO.

    Возьмём для примера ATmega328P, один из самых распространённых микроконтроллеров в DIY (ну просто ввиду распространённости, хотим мы того или нет, Arduino).

    Какая разница, сначала мы выставим делитель UART, а потом направление GPIO или наоборот?


    1. aabzel Автор
      27.07.2024 11:26

      UART при инициализации должен выдать Hello сигнал. Если инициализировать UART до GPIO, то вы не увидите в терминале или на осциллографе Hello текста.

      А техники сочтут, что ваша прошивка вообще зависла, и нажалуются на вас вашему начальнику.


      1. andreili
        27.07.2024 11:26
        +2

        Инит - это только инит и ничего более. А игите ничего не должно генерироваться и так далее.

        Прошли все-все иниты - и только потом приртуем и запускаем железки.


        1. aabzel Автор
          27.07.2024 11:26

          и только потом приртуем и запускаем железки.

          Что это за термин такой "приртовать"?


          1. andreili
            27.07.2024 11:26
            +2

            Блин, с мобилки в толпе печатал.

            "Принтовать"


        1. aabzel Автор
          27.07.2024 11:26

          А если в середине system init прошивка зависла?

          Как вы поймёте, где без отладчика?

          Надо обязательно непрерывно логировать в uart лог загрузки микроконтроллера.


          1. vadjuse
            27.07.2024 11:26

            вообще годная статья, думал о таком, но не знал, что есть инструменты.

            Сложно, но надо прогнать на своём проекте


            1. aabzel Автор
              27.07.2024 11:26

              вообще годная статья, думал о таком,

              Благодарю Вас, Вадим!


      1. Flammmable
        27.07.2024 11:26
        +1

        UART при инициализации должен выдать Hello сигнал.

        Кому должен? Это какой-то стандарт разработки типа MISRA C? Или это ваш авторский подход?

        Если второе, то создаётся ощущение, что у вас синдром неприятия чужой разработки по отношению к JTAG )))

        То вы тестирование непропаев/замыканий через UART пытаетесь делать. То отладочную информацию пытаетесь через UART же выдать. За что вы ненавидите JTAG? ))))


        1. aabzel Автор
          27.07.2024 11:26

          К jtag я нормально отношусь. Когда прошивка наглухо зависает, то включаю пошаговую отладку jtag. Если он есть. В 99% случаях на плате только интерфейс SWD.

          А все остальное отлично отлаживается через uart cli

          А когда я на работе попросил схемотехников протестировать плату на протай с помощью JTAG по Net List(у) и них глаза на лоб полезли.

          Вот и пришлось делать программный Cross-Detect прямо на чипе.


        1. aabzel Автор
          27.07.2024 11:26

          Кому должен? Это какой-то стандарт разработки типа MISRA C?

          Это требование стандарта ISO 26262


          1. Flammmable
            27.07.2024 11:26

            -UART при инициализации должен выдать Hello сигнал.

            -Кому должен? Это какой-то стандарт разработки типа MISRA C?

            -Это требование стандарта ISO26262

            Можете ли вы указать конкретный пункт в данном стандарте, интерпретируя который вы предполагаете обязательным выдачу сигнала Hello в конце процедуры инициализации UART?

            Или речь идёт скорее об интерпретации общего духа стандарта?


            1. aabzel Автор
              27.07.2024 11:26

              Можете ли вы указать конкретный пункт в данном стандарте, интерпретируя который вы предполагаете обязательным выдачу сигнала Hello в конце процедуры инициализации UART?

              Конечно. Это относится к требованию Walk-through of the design.


        1. aabzel Автор
          27.07.2024 11:26

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

          UART это только более простой вариант.

          Никто не запрещает вам крутить автомат поиска непропая на PC и управлять GPIO по JTAG.

          Вот только отдельный MCAL для PC для каждого MCU вам придется писать самим.
          Как и убеждать начальство, что вам на это надо подарить 80-100 часов.


  1. belav
    27.07.2024 11:26
    +2

    А почему не написать инициализатор UART, который вызывает инициализацию тактирования UART и соответствующих GPIO?


    1. aabzel Автор
      27.07.2024 11:26

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


      1. belav
        27.07.2024 11:26
        +2

        Так, может, уделить время написанию драйвера, а не костылей?


        1. aabzel Автор
          27.07.2024 11:26

          Это не костыль, а математический подход к программной инженерии.


    1. aabzel Автор
      27.07.2024 11:26

      SPI тоже нуждается в GPIO и I2C и I2S, PDM, PWM.
      И что нам все 144 GPIO 12 раз инициализировать при старте?


  1. Feeel
    27.07.2024 11:26
    +2

    Сама идея такого решения мне нравится, но проблема мне кажется слегка надуманной. Хотя я не работал с реально БОЛЬШИМИ сборками для МК...

    Не понимаю, что именно мешает сначала инициализировать всё, что нужно, а потом уже слать всякое в UART и т.д.? Процесс инициализации можно сделать меганаглядным (нужна одна свободная нога GPIO):

    1) самым первым инициализируем GPIO

    2) подаём импульс на некоторую ногу

    3) инициализируем что там ещё нам надо

    4) подаём импульс на эту же ногу

    И так на каждом шаге.

    Если что-то пошло не так - вешаем осциллограф и смотрим сколько импульсов прошло: 6 - значит повисло на 7 шаге, 4 - значит на 5 и т.д.

    На запуск главного цикла импульс тройной ширины, на исследкемые функции ещё какие-нибудь хитрые импульсы или их наборы. Сиди и смотри, что называется...


    1. aabzel Автор
      27.07.2024 11:26

      Процесс инициализации можно сделать меганаглядным (нужна одна свободная нога GPIO):

      Наглядность повысится еще лучше, если просто запустить UART-CLI. Так делают во всем мире.
      И дорогой осциллограф не нужен.


    1. aabzel Автор
      27.07.2024 11:26

      Если что-то пошло не так - вешаем осциллограф и смотрим сколько импульсов прошло: 6 - значит повисло на 7 шаге, 4 - значит на 5 и т.д.

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


      1. x11term
        27.07.2024 11:26
        +2

        и что в этом плохого? особенно учитывая, что компилятор С++ это делает сам :)


        1. aabzel Автор
          27.07.2024 11:26

          Вот именно что ничего плохого в этом нет. Вот только за 12 лет работы я такого ни у кого из коллег в коде не видел.


  1. kozlyuk
    27.07.2024 11:26
    +1

    Вместо "программной смеси":

    dot -Tplain mcu.dot | awk '/^edge / { print $2 " " $3 }' | tsort >order.txt
    

    Из недостатков — не показывает, для каких компонент порядок не важен.
    Кстати, если подать на вход приведенный пример, tsort находит в нем цикл:

    tsort: -: input contains a loop:
    tsort: CORTEX_M4
    tsort: NVIC
    


    1. aabzel Автор
      27.07.2024 11:26

      dot -Tplain mcu.dot | awk '/^edge / { print $2 " " $3 }' | tsort >order.txt

      Гениально!
      Благодаря Вас за подсказку, Дмитрий.
      Только в CygWin awk почему-то называется gawk.

      awk - отличная утилита.