Пролог
Как известно, программа - это реализация алгоритма. А алгоритм - это упорядоченная последовательность действий. Поэтому очень большое значение имеет правильный порядок исполнения программы.
Если вы выбрали неверный порядок инициализации программы, то прошивка заклинит в run-time.
В переводе на кухонный язык. Нет смысла инициализировать UART, если не проинициализирован GPIO. Это очевидно. Однако так бывает не всегда. Когда программа построена из огромного количества программных компонентов (SWC) (десятки и сотни) и каждый программный компонент имеет зависимости от ещё нескольких программных компонентов, то тут в уме выявить корректную последовательность инициализации всей прошивки, простите, просто нереально...
Программа состоит из программных компонентов. Программные компоненты связаны друг с другом отношением зависимости.
В прошлом тексте я показал, как утилитой make можно формально произвести топологическую сортировку графа. Однако это не удобно, так как требует вручную писать отдельный make файл только лишь для того, чтобы отсортировать граф. Формально да, всё работает, но много лишней суеты и телодвижений.
Что надо из софтвера?
Для решения этой задачи нам понадобится вот такой джентельменский набор Unix утилит. Все они бесплатно прилетают вместе с CygWin или Git Bash.
№ | Название утилиты | назначение утилиты |
1 | dot | Трассировщик текстового языка GraphViz |
2 | gawk | построчный синтаксический анализатор текста |
3 | sort (cygwin) | утилита построчной сортировки текстового файла |
4 | tsort | утилита топологической сортировки |
5 | tac | построчный реверс файла |
6 | cpp | препроцессор из языка Си |
Причем нужен именно cygwin sort.exe (C:\cygwin64\bin\sort.exe), а не C:\Windows\System32\sort.exe. Стандартный windows System32\sort.exe не умеет обрабатывать файл in-place, а cygwin sort.exe умеет. Вот такие пирожки с капустой...
Реализация
Дело в том, что для разметки графов уже давным-давно существует культовый текстовый язык разметки 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 есть консольная утилита-трассировщик 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 и gawk. Вот так.
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)
или вот такой, более переносимый скрипт
$(info auto_init_script) GRAPHVIZ_PLAIN = $(BUILD_DIR)/$(TARGET).plain $(info GRAPHVIZ_PLAIN=$(GRAPHVIZ_PLAIN)) ADJACENCY_LIST = $(BUILD_DIR)/$(TARGET).adjlist $(info ADJACENCY_LIST=$(ADJACENCY_LIST)) INIT_ORDER_FILE = $(BUILD_DIR)/$(TARGET).init $(info INIT_ORDER_FILE=$(INIT_ORDER_FILE)) $(GRAPHVIZ_PLAIN): preproc_graphviz dot -Tplain $(SOURCES_DOT_RES) > $@ $(ADJACENCY_LIST):$(GRAPHVIZ_PLAIN) $(info AdjacencyList...) gawk '/^edge / { print $$3 " " $$2 }' $< > $@ $(info SortAdjacencyList...) sort -u $(ADJACENCY_LIST) -o $(ADJACENCY_LIST) $(INIT_ORDER_FILE):$(ADJACENCY_LIST) $(info TopologicalSorting...) tsort $(ADJACENCY_LIST) | tac > $@ .PHONY: auto_init auto_init: preproc_graphviz $(ARTIFACTS) $(INIT_ORDER_FILE) $(info RunAutoInit...)
После исполнения 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 | https://habr.com/ru/articles/818917/ | |
2 | https://habr.com/ru/articles/765424/ | |
3 | https://www.manniwood.com/2019_09_08/tsort.html | |
4 | ||
5 | https://unix.stackexchange.com/questions/170665/remove-a-carriage-return-with-sed |
