Генерация Зависимостей Внутри Программы
Бывают ситуации, когда к разработке firmware надо подключить новых людей. Как объяснить им архитектуру нынешнего ПО? В программировании микроконтроллеров программы часто строятся иерархично. То есть один программный компонент вызывает функции из другого программного компонента. Например драйверу чтения SD-карты нужны функции от драйвера SPI, драйвера GPIO, компонента CRC7, CRC16. Как бы представить эту взаимосвязь в для каждой конкретной сборке прошивки? Очевидно, что надо нарисовать граф. То есть картинку, где стрелочки и прямоугольники покажут как всё связано. И тут на помощь приходит язык разметки графов Graphviz.
Для кода на языке Graphviz можно сделать полуавтоматический кодо генератор. Надо обязать программиста, чтобы в паке каждого программного компонента был файлик *.gvi для указания высокоуровневых зависимостей. Смотришь в *.c код, примерно видишь, что там подключается #include »*.h» что вызывается с самих функциях и отражаешь это в *.gvi файле рядом. Вот примерное содержимое для pdm.gv.
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). Препроцессору всё равно с каким кодом работать. Препроцессор просто вставляет и заменяет куски текста.
Вот сам generate_dependencies.mk скрипт, который определяет ToolChain для построения изображения в привычном *.pdf файле.
$(info Generate Dependencies)
CC_DOT="C:/Program Files/Graphviz/bin/dot.exe"
RENDER="C:/Program Files/Google/Chrome/Application/chrome.exe"
MK_PATH_WIN := $(subst /cygdrive/c/,C:/, $(MK_PATH))
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
LAYOUT_ENGINE = -Kdot
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
Скрипт generate_dependencies.mk следует условно подключить к основному скрипту сборки проекта rules.mk
ifeq ($(DEPENDENCIES_GRAPHVIZ), Y)
include $(WORKSPACE_LOC)/generate_dependencies.mk
endif
далее в основном 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 файл.
Достоинства графа зависимостей
1--Хорошая документация поможет быстро ввести в курс дела новых людей.
2--Граф зависимостей позволит выявить паразитные зависимости и оптимизировать архитектуру программы.
3--Автогенератор зависимостей легко встраивается в сборку, если система сборки предварительно написана на make скриптах, так как утилита make она, в сущности, всеядная. Утилите make как и cpp всё равно для какого языка программирования Вы её вызвали. Make — это просто дирижёр программного конвейера.
Недостатки графа зависимостей
1--Надо писать makefile надо освоить спецификацию GNU make (т.е. просмотреть по диагонали 200 страниц). Если Вы всё еще в 2023 м собираете прошивки из GUI-IDE, то могу вам только посоветовать позвонить в техподдержку вашей IDE.
2--Надо вручную прописать *.gvi файл для каждого программного компонента.
Вывод
Как видите, сборка из скриптов позволяет Вам помимо получения бинарных артефактов (*.bin, *.hex, *.map, *.elf) также авто генерировать всяческую документацию. Например такую полезную схему как дерево зависимостей между программными компонентами. Это является хорошей причиной, чтобы собирать прошивки не из GUI-IDE, а из самописных скриптов.
Словарь
Акроним | Расшифровка |
GVI | Graphviz Include |
Links
https://dreampuf.github.io/GraphvizOnline/
https://habr.com/ru/articles/688542/
https://habr.com/ru/articles/499170/