![](https://habrastorage.org/getpro/habr/upload_files/37d/768/1fe/37d7681fe44db1f125d3639fb444147f.png)
Бывают ситуации, когда к разработке firmware надо подключить новых людей. Как объяснить им архитектуру нынешнего ПО?
Глобально в каждом Firmware репозитории весь код можно отсортировать на такие мега-части как sensitivity, connectivity, control, computing , security и storage.
![](https://habrastorage.org/getpro/habr/upload_files/eca/1ba/3f5/eca1ba3f525715dbc82a1e9da4a19292.png)
Код из каждой части может быть как-то связан с частями из других программных компонентов. В программировании микроконтроллеров программы по-хорошему должны строится иерархично. То есть один программный компонент вызывает функции из другого программного компонента.
Например драйверу чтения 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. План такой. Надо организовать вот такой программный конвейер.
![](https://habrastorage.org/getpro/habr/upload_files/378/ec5/d9f/378ec5d9fa09c1a1717b4a152a4ca605.png)
Заметьте, тут работает самый обыкновенный препроцессор из программ на Си (утилита 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 и вместе с артефактами с прошивкой у Вас рядом появится и файлы документации с изображением зависимостей.
![](https://habrastorage.org/getpro/habr/upload_files/3c7/0a9/005/3c70a90058cd9d4c29a226682472b855.png)
Скрипт сборки сгенерирует вот такой финальный 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
}
В качеств примера у Вас получится примерно вот такой граф зависимостей.
![](https://habrastorage.org/getpro/habr/upload_files/4a7/d84/f94/4a7d84f942fc2baf7ac87b063c72dbf0.png)
Для расширения детализации дерева зависимостей надо просто добавлять новые *.gvi файлы. Их будет много (десятки) но они простые как правило по 3-6 строчек в каждом. В каждой папке с кодом должен лежать один *.gvi файл.
Вот, например, граф зависимости программных компонентов для прошивки хранителя паролей Pas~ r1.1
![](https://habrastorage.org/getpro/habr/upload_files/fae/341/b1a/fae341b1ace20930c930bc0d730502f7.png)
Достоинства графа зависимостей
Хорошая схема зависимостей программных компонентов поможет быстро ввести в курс дела новых людей. Прежде всего программистов.
Граф зависимостей позволит выявить паразитные зависимости и оптимизировать архитектуру всей программы.
Автогенератор зависимостей легко встраивается в сборку, если система сборки предварительно написана на make скриптах, так как утилита make она, в сущности, всеядная. Утилите make.exe как и cpp.exe всё равно для какого языка программирования Вы её вызвали. Make.exe — это просто дирижёр программного конвейера.
Для каждой отдельной сборки строится её собственный граф зависимостей. Лишний код из соседней папки не участвует.
Граф строится автоматически по коду из *.gv файла. Мышка тут абсолютно не нужна.
Недостатки графа зависимостей
Надо писать makefile надо освоить спецификацию GNU make (т. е. просмотреть по диагонали 200 страниц). Если Вы всё еще в 2023м собираете прошивки из GUI‑IDE, то могу Вам только посоветовать позвонить в техподдержку вашей IDE.
Надо вручную прописать *.gvi файл для каждого программного компонента.
Автоматический трассировщик графа от Graphviz не всегда удачно разводит граф
![пример не самой удачной разводки графа движком osage пример не самой удачной разводки графа движком osage](https://habrastorage.org/getpro/habr/upload_files/768/eb1/1c4/768eb11c421232a3c5f80608b4247fb1.png)
Вывод
Как видите, сборка из скриптов позволяет Вам помимо получения бинарных артефактов (*.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 для Автогенерации Блок-Схем Сложных Электронных Цепей
Задача про две ёмкости для жидкости