
Бывают ситуации, когда к разработке firmware надо подключить новых людей. Как объяснить им архитектуру нынешнего ПО?
Глобально в каждом Firmware репозитории весь код можно отсортировать на такие мега-части как sensitivity, connectivity, control, computing , security и storage.

Код из каждой части может быть как-то связан с частями из других программных компонентов. В программировании микроконтроллеров программы по-хорошему должны строится иерархично. То есть один программный компонент вызывает функции из другого программного компонента.
Например драйверу чтения SD‑карты нужны функции от драйвера SPI, драйвера GPIO, компонента CRC7, CRC16 и т. п.
Как бы представить эту взаимосвязь для каждой конкретной сборки прошивки? Очевидно, что надо нарисовать граф. То есть, картинку, где стрелочки и прямоугольники покажут как всё взаимно связано. И тут-то на помощь нам как раз и приходит язык разметки графов Graphviz.
Для кода на языке Graphviz можно сделать полуавтоматический кодо генератор. Надо обязать программиста, чтобы в папке каждого программного компонента был крохотный файлик *.gvi для явного указания высокоуровневых зависимостей. Для простоты поддержки этот *.gvi файл надо называть так же как имя папки в которой лежит *.gvi.
Смотришь в *.c код, примерно видишь, что там подключается #include «*.h», что вызывается в самих функциях и отражаешь это в *.gvi файле рядом. Вот примерное содержимое для pdm.gvi. PDM — это программный компонент для записи звука из MEMS микрофонов.
GPIO->PDM NVIC->PDM REG->PDM DFT->PDM RAM->PDM AUDIO->PDM DMA->PDM
В этом *.gvi файле надо вручную прописать какие у данного программного компонента есть зависимости от других программных компонентов. Сделать это можно на простеньком текстовом языке Graphviz. По факту, всё что понадобится из синтаксиса языка Graphviz — это оператор стрелка «->»
Также нужен корневой файл main.gvi в который будет всё вставляться утилитой препроцессором (cpp.exe).
strict digraph graphname { rankdir=LR; splines=ortho node [shape="box"]; #ifdef HAS_BSP #include "bsp.gvi" #endif #ifdef HAS_THIRD_PARTY #include "third_party.gvi" #endif #ifdef HAS_PROTOCOLS #include "protocols.gvi" #endif #ifdef HAS_ADT #include "adt.gvi" #endif #ifdef HAS_ASICS #include "asics.gvi" #endif #ifdef HAS_MCU #include "mcu.gvi" #endif #ifdef HAS_COMMON #include "common.gvi" #endif #include "components.gvi" #ifdef HAS_UTILS #include "utils.gvi" #endif #ifdef HAS_CORE #include "core.gvi" #endif #ifdef HAS_DRIVERS #include "drivers.gvi" #endif #ifdef HAS_INTERFACES #include "interfaces.gvi" #endif }
Также нужен makefile скрипт, который будет собирать все отдельные файлы с зависимостями в один единый файл на языке Graphviz. План такой. Надо организовать вот такой программный конвейер.

Заметьте, тут работает самый обыкновенный препроцессор из программ на Си (утилита cpp). Препроцессору всё равно с каким кодом работать. Препроцессор просто вставляет и заменяет куски текста. Утилиту cpp.exe можно вообще порекомендовать писателям учебников или чертёжникам.
Далее сам скрипт generate_dependencies.mk, который определяет ToolChain для построения изображения в привычном *.pdf/*.svg файле.
#CC=C:/cygwin64/bin/dot.exe $(info Generate Dependencies) CC_DOT="C:/Program Files/Graphviz/bin/dot.exe" RENDER="C:/Program Files/Google/Chrome/Application/chrome.exe" $(info MK_PATH=$(MK_PATH)) MK_PATH_WIN := $(subst /cygdrive/c/,C:/, $(MK_PATH)) $(info MK_PATH_WIN=$(MK_PATH_WIN)) $(info WORKSPACE_LOC=$(WORKSPACE_LOC)) ARTEFACTS_DIR=$(MK_PATH_WIN)$(BUILD_DIR) $(info ARTEFACTS_DIR=$(ARTEFACTS_DIR)) SOURCES_DOT=$(WORKSPACE_LOC)main.gvi $(info SOURCES_DOT=$(SOURCES_DOT)) SOURCES_DOT:=$(subst /cygdrive/c/,C:/, $(SOURCES_DOT)) $(info SOURCES_DOT=$(SOURCES_DOT)) SOURCES_DOT_RES += $(ARTEFACTS_DIR)/$(TARGET)_dep.gv $(info SOURCES_DOT_RES=$(SOURCES_DOT_RES)) ART_SVG = $(ARTEFACTS_DIR)/$(TARGET)_res.svg ART_PDF = $(ARTEFACTS_DIR)/$(TARGET)_res.pdf $(info ART_SVG=$(ART_SVG) ) $(info ART_PDF=$(ART_PDF) ) CPP_GV_OPT += -undef CPP_GV_OPT += -P CPP_GV_OPT += -E CPP_GV_OPT += -nostdinc CPP_GV_OPT += $(OPT) DOT_OPT +=-Tsvg #DOT_OPT +=-L10 #DOT_OPT +=-v #LAYOUT_ENGINE = -Kneato #LAYOUT_ENGINE = -Kfdp #LAYOUT_ENGINE = -Ksfdp #LAYOUT_ENGINE = -Ktwopi #LAYOUT_ENGINE = -Kosage #LAYOUT_ENGINE = -Kpatchwork LAYOUT_ENGINE = -Kdot DEPENDENCY_GRAPH += generate_dep preproc_graphviz:$(SOURCES_DOT) $(info Preproc...) #mkdir $(ARTEFACTS_DIR) cpp $(SOURCES_DOT) $(CPP_GV_OPT) $(INCDIR) -E -o $(SOURCES_DOT_RES) generate_dep_pdf: preproc_graphviz $(info route graph...) $(CC_DOT) -V $(CC_DOT) -Tpdf $(LAYOUT_ENGINE) $(SOURCES_DOT_RES) -o $(ARTEFACTS_DIR)/$(TARGET).pdf generate_dep_svg: preproc_graphviz $(info route graph...) $(CC_DOT) -V $(CC_DOT) $(DOT_OPT) $(SOURCES_DOT_RES) -o $(ARTEFACTS_DIR)/$(TARGET).svg generate_dep: generate_dep_svg generate_dep_pdf $(info All) print_dep: generate_dep $(info print_svg) $(RENDER) -open $(ARTEFACTS_DIR)/$(TARGET).svg $(RENDER) -open $(ARTEFACTS_DIR)/$(TARGET).pdf #clean: # $(info clean) # rm $(MK_PATH)/artefacts/*.* #$(ART_SVG):$(ART_SVG)
Скрипт generate_dependencies.mk следует условно подключить к основному скрипту сборки проекта rules.mk
DEPENDENCY_GRAPH= ifeq ($(DEPENDENCIES_GRAPHVIZ), Y) include $(WORKSPACE_LOC)/generate_dependencies.mk endif ....... # default action: build all all: generate_definitions $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin $(DEPENDENCY_GRAPH) generate_definitions: cpp $(CPP_FLAGS) $(WORKSPACE_LOC)empty_sourse.c -dM -E> c_defines_generated.h
далее в основном make файле определить переменную окружения DEPENDENCIES_GRAPHVIZ=Y
MK_PATH:=$(dir $(realpath $(lastword $(MAKEFILE_LIST)))) #@echo $(error MK_PATH=$(MK_PATH)) WORKSPACE_LOC:=$(MK_PATH)../../ INCDIR += -I$(MK_PATH) INCDIR += -I$(WORKSPACE_LOC) DEBUG=Y TARGET=board_name_build_name DEPENDENCIES_GRAPHVIZ=Y include $(MK_PATH)config.mk ifeq ($(CLI),Y) include $(MK_PATH)cli_config.mk endif ifeq ($(DIAG),Y) include $(MK_PATH)diag_config.mk endif ifeq ($(UNIT_TEST),Y) include $(MK_PATH)test_config.mk endif include $(WORKSPACE_LOC)code_base.mk include $(WORKSPACE_LOC)rules.mk
Теперь достаточно просто открыть консоль и набрать make all и вместе с артефактами с прошивкой у Вас рядом появится и файлы документации с изображением зависимостей.

Скрипт сборки сгенерирует вот такой финальный Graphviz код
strict digraph graphname { rankdir=LR; splines=ortho node [shape="box"]; REG->ADC NVIC->ADC REG->FLASH REG->GPIO REG->TIMER TIMER->TIME REG->I2S NVIC->I2S SW_DAC->I2S NVIC->I2C GPIO->I2C REG->I2C FLASH->NVS GPIO->PDM NVIC->PDM REG->PDM DFT->PDM RAM->PDM AUDIO->PDM DMA->PDM GPIO->SPI NVIC->SPI SYSTICK->TIME GPIO->UART UART->LOG LOG->CLI CLI->PROTOCOL CRC8->TBFP RAM->ARRAY SPI->DW1000 GPIO->DW1000 TIME->DW1000 DW1000->DWM1000 CRC7->SD_CARD CRC16->SD_CARD SPI->SD_CARD GPIO->SD_CARD TIME->SD_CARD DW1000->DECADRIVER TIME->DECADRIVER GPIP->DECADRIVER SPI->DECADRIVER I2S->MAX98357 GPIO->MAX98357 SD_DAC->MAX98357 NVIC->CORTEX_M33 SYSTICK->CORTEX_M33 }
В качеств примера у Вас получится примерно вот такой граф зависимостей.

Для расширения детализации дерева зависимостей надо просто добавлять новые *.gvi файлы. Их будет много (десятки) но они простые как правило по 3-6 строчек в каждом. В каждой папке с кодом должен лежать один *.gvi файл.
Вот, например, граф зависимости программных компонентов для прошивки хранителя паролей Pas~ r1.1

Достоинства графа зависимостей
Хорошая схема зависимостей программных компонентов поможет быстро ввести в курс дела новых людей. Прежде всего программистов.
Граф зависимостей позволит выявить паразитные зависимости и оптимизировать архитектуру всей программы.
Автогенератор зависимостей легко встраивается в сборку, если система сборки предварительно написана на make скриптах, так как утилита make она, в сущности, всеядная. Утилите make.exe как и cpp.exe всё равно для какого языка программирования Вы её вызвали. Make.exe — это просто дирижёр программного конвейера.
Для каждой отдельной сборки строится её собственный граф зависимостей. Лишний код из соседней папки не участвует.
Граф строится автоматически по коду из *.gv файла. Мышка тут абсолютно не нужна.
Недостатки графа зависимостей
Надо писать makefile надо освоить спецификацию GNU make (т. е. просмотреть по диагонали 200 страниц). Если Вы всё еще в 2023м собираете прошивки из GUI‑IDE, то могу Вам только посоветовать позвонить в техподдержку вашей IDE.
Надо вручную прописать *.gvi файл для каждого программного компонента.
Автоматический трассировщик графа от Graphviz не всегда удачно разводит граф

Вывод
Как видите, сборка из скриптов позволяет Вам помимо получения бинарных артефактов (*.bin, *.hex, *.map, *.elf) также авто генерировать всяческую документацию (*.pdf, *.svg, *.gv). Например, такую полезную схему как дерево зависимостей между программными компонентами. Это является хорошей причиной, чтобы собирать прошивки не из GUI-IDE, а из самописных скриптов.
Стоит отметить, что получившееся дерево зависимостей программных компонентов поймут скорее только программисты-микроконтроллеров нежели схемотехники. Чтобы читать дерево зависимостей надо примерно знать основы Computer Science. Надо понимать, что такое AES256, BASE64, CRC, CLI, CSV, FIFO, LIFO, DFT, FFT, XLM и прочие акронимы.
А вообще как по мне, дак самая лучшая документация к коду - это модульные тесты. https://habr.com/ru/articles/698092/
В модульных тестах сразу видно как заполнять прототипы функций и что делать с результатом работы функции.
Еще голый код и утилита grep при определенной сноровке могут позволить извлечь полезные сведения о коде.
Вариантов много.
У меня есть еще один текст про документацию. Это схема ToolChain(а). Про это можно почитать тут https://habr.com/ru/articles/688542/ . Тоже весьма полезная вещь для понимания происходящего под капотом.
Словарь
Акроним | Расшифровка |
GVI | Graphviz Include |
Links
https://dreampuf.github.io/GraphvizOnline/
Что Должно Быть в Каждом FirmWare Pепозитории https://habr.com/ru/articles/689542/
Синергия Graphviz и препроцессора C/C++
Почему Важно Собирать Код из Скриптов
Тандем Cpp/Dot для Описания Сложных ToolСhain(ов)
Язык Graphviz для Автогенерации Блок-Схем Сложных Электронных Цепей
Задача про две ёмкости для жидкости
