Generating Dependencies Inside a Program

There are situations when new people need to be involved in firmware development. How can I explain to them the architecture of current software? In microcontroller programming, programs are often structured hierarchically. That is, one software component calls functions from another software component. For example, the SD card reader driver needs functions from the SPI driver, GPIO driver, CRC7, CRC16 component. How would one represent this relationship for each specific firmware build? Obviously, we need to draw a graph. That is, a picture where arrows and rectangles show how everything is connected. And here the graph markup language Graphviz comes to the rescue.

For code in the Graphviz language, you can make a semi-automatic code generator. The programmer must be required to include a *.gvi file in the package of each software component to indicate high-level dependencies. You look at the *.c code, you roughly see that #include “*.h“ is included there, which is called from the functions themselves, and you reflect this in the *.gvi file next to it. Here is some sample content for pdm.gv.

GPIO->PDM
NVIC->PDM
REG->PDM
DFT->PDM
RAM->PDM
AUDIO->PDM
DMA->PDM

In this *.gvi file, you must manually specify which dependencies this software component has on other software components. This can be done in the simple text language Graphviz. In fact, all you need from the Graphviz language syntax is the arrow operator “->”

You also need a root file main.gvi into which everything will be inserted by the preprocessor utility (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
}

You also need a makefile script that will collect all the individual files with dependencies into one single file in the Graphviz language. This is the plan. We need to organize a software pipeline like this.

Note that the most common preprocessor of C programs (the cpp utility) works here. The preprocessor doesn’t care what code it works with. The preprocessor simply inserts and replaces chunks of text.

Here is the generate_dependencies.mk script itself, which defines the ToolChain for building an image in a familiar *.pdf file.

$(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


The generate_dependencies.mk script should be conditionally connected to the main project build script rules.mk

ifeq ($(DEPENDENCIES_GRAPHVIZ), Y)
    include $(WORKSPACE_LOC)/generate_dependencies.mk
endif

then in the main make file define the environment variable 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
 

Now you just need to open the console and type make all, and along with the artifacts with the firmware, documentation files with dependency images will appear next to you. The build script will generate this final Graphviz code

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
}

As an example, you will get something like this dependency graph.

To expand the detail of the dependency tree, you just need to add new *.gvi files. There will be a lot of them (dozens), but they are simple, usually 3-6 lines each. Each folder with the code should contain one *.gvi file.

Advantages of a dependency graph

1–Good documentation will help quickly bring new people up to speed.

2–The dependency graph will allow you to identify parasitic dependencies and optimize the program architecture.

3–The dependency autogenerator is easily integrated into the assembly if the assembly system is pre-written in make scripts, since the make utility is, in essence, omnivorous. The make utility, like cpp, doesn’t care what programming language you called it for. Make is simply the conductor of the software pipeline.

Disadvantages of a Dependency Graph

1–You need to write a makefile, you need to master the GNU make specification (that is, look through 200 pages diagonally). If you are still collecting firmware from GUI-IDE in 2023, then I can only advise you to call the technical support of your IDE.

2–You must manually register a *.gvi file for each software component.

Conclusion

As you can see, assembly from scripts allows you, in addition to receiving binary artifacts (*.bin, *.hex, *.map, *.elf), to also automatically generate all kinds of documentation. For example, such a useful diagram as a tree of dependencies between software components. This is a good reason to build firmware not from GUI-IDE, but from self-written scripts.

Dictionary

Acronym

Decoding

GVI

Graphviz Include

Links

https://dreampuf.github.io/GraphvizOnline/

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

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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *