Автоматическая Генерация Конфигураций для Make Сборок

c67022239e6d74de726994bc1c05656c.png

Пролог

При разработке программного обеспечения (особенно для микроконтроллеров) рано или поздно придется столкнуться с тем, что надо как-то передавать конфигурации для данного программного проекта.

В своем опыте я пришел к выводу, что с точки зрения масштабирования кодовой базы, конфиги проще всего передавать через переменные окружения. Да. Плюс в том, что переменные окружения можно определять прописывая прямо в скриптах (Make, CMake и т.п.).

Выглядит это так. У каждой сборки есть файл config.mk в котором перечислены программные компоненты из которых должна собираться эта конкретная сборка. Содержимое этого файла обычно выглядит так.


ADC=Y
ADC_ISR=Y
ADT=Y
ALLOCATOR=Y
ARRAY=Y
ASICS=Y

....

TASK=Y
TBFP=Y
TERMINAL=Y
TIME=Y
UART0=Y
UART2=Y
UNIT_TEST=Y
UTILS=Y
UWB=Y

Это просто атомарные строчки. Тут происходит определение переменных окружения. Декларативно перечисляется из чего собирать прошивку. У другой сборки есть свой такой же декларативный config.mk файл и свой собственный набор переменных окружения.

Именно эти самые переменны окружения решают какие файлы добавить в компиляцию, а какие исключить. Вот например переменная окружения SD_CARD=Y добавит в сборку вот этот make скрипт


ifneq ($(SD_CARD_MK_INC),Y)
    SD_CARD_MK_INC=Y

    $(info + SD card SPI driver)

    SD_CARD_DIR = $(ASICS_DIR)/sd_card
    #@echo $(error SD_CARD_DIR= $(SD_CARD_DIR))

    INCDIR += -I$(SD_CARD_DIR)

    OPT += -DHAS_CRC7
    OPT += -DHAS_CRC16
    OPT += -DHAS_SD_CARD
    OPT += -DHAS_SD_CARD_CRC7
    OPT += -DHAS_SD_CARD_CRC16

    SOURCES_C += $(SD_CARD_DIR)/sd_card_drv.c
    SOURCES_C += $(SD_CARD_DIR)/sd_card_crc.c
    SOURCES_C += $(SD_CARD_DIR)/sd_card_crc16.c

    ifeq ($(DIAG),Y)
        ifeq ($(SD_CARD_DIAG),Y)
            $(info + SD card diag)
            OPT += -DHAS_SD_CARD_DIAG
            SOURCES_C += $(SD_CARD_DIR)/sd_card_diag.c
        endif
    endif

    ifeq ($(CLI),Y)
        ifeq ($(SD_CARD_COMMANDS),Y)
            $(info + SD card commands)
            OPT += -DHAS_SD_CARD_COMMANDS
            SOURCES_C += $(SD_CARD_DIR)/sd_card_commands.c
        endif
    endif
endif

Достаточно внутри config.mk прописать SD_CARD=Y и скрипты сами добавят всё что надо для сборки Си кода: исходный код, пути к исходному коду и директивы препроцессора. Вы заменили в скриптах один символ c «Y» на «N» и скрипты make автоматически исключили из сборки исходный код, пути к исходному коду и директивы препроцессора для этого конкретного программного компонента. Easy!

Переменные окружения внутри config.mk, внимание, отсортированы! И конфиги двух разных сборок очень удобно сравнивать в утилите WinMerge.

4300fdf443115f305d88ed05eaf6de99.png

Как известно, в компьютерные программы по-хорошему строятся иерархично. Вот, например, программный компонент интерфейса командной строки CLI нуждается в том чтобы в программе уже были реализованы такие программные компоненты как UART, TIMER, распознаватель чисел из строки и прочее по мелочи MISC.

В чем проблема?
Проблема в том, что переменных окружения (конфигов) становится много. Если Вы собираете через make скрипты, то у Вас будет много переменных окружения для каждой конкретной сборки. В каждом config.mk будет по 100 …200 строчек и можно нечаянно забыть прописать какую-то важную переменную окружения, которая активирует какой-то конфиг. И без которой прошивка будет неправильно работать в run-time. Вот пример типичного config.mk

ADC=N
ADC_ISR=Y
ALLOCATOR=Y
ARRAY=Y
ASICS=Y
AUDIO=Y
BIN_2_STR=Y
BOARD=Y
BOARD_INFO=Y
BOARD_UTILS=Y
BUTTON=Y
CLI=Y
CLOCK=Y
CMSIS=Y
COMMON=Y
COMPLEX=Y
COMPONENTS=Y
CONNECTIVITY=Y
CORE=Y
CORE_APP=Y
CORE_EXT=Y
CORTEX_M33=Y
CRC16=Y
CRC8=Y
CRC=Y
CSV=Y
CUSTOM_PRINTF=Y
DATA_POC=Y
DEBUG=Y
DEBUGGER=Y
DFT=Y
DIAG=Y
DRIVERS=Y
DSP=Y
DYNAMIC_SAMPLES=Y
FIFO=Y
FIFO_CHAR=Y
FIFO_INDEX=Y
FLASH=Y
FLASH_EX=Y
FLASH_FS=Y
FLASH_FS_WRITE=Y
FLASH_WRITE=Y
GENERIC=Y
GPIO=Y
HEALTH_MONITOR=Y
I2C1=Y
I2C=Y
I2S0=Y
I2S0_MASTER=Y
I2S=Y
I2S_ISR=Y
INDICATION=Y
INTERFACES=Y
LED=Y
LED_MONO=Y
LED_VERIFY=Y
LIMITER=Y
LOG=Y
LOG_COLOR=Y
LOG_DIAG=Y
LOG_TIME_STAMP=Y
LOG_UTILS=Y
MATH=Y
MATH_VECTOR=Y
MCAL=Y
MCAL_NRF5340=Y
MICROCONTROLLER=Y
MISCELLANEOUS=Y
MULTIMEDIA=Y
NORTOS=Y
NRF5340=Y
NRF5340_APP=Y
NRF5340_DK=Y
NRFX=Y
NVIC_COMMANDS=Y
NVS=Y
NVS_WRITE=Y
PARAM=Y
PARAM_SET=Y
PCM_16_BIT=Y
PINS=Y
PROTOCOLS=Y
REAL_SAMPLE_ARRAY=N
SENSITIVITY=Y
SOFTWARE_TIMER=Y
STORAGE=Y
STR2_DOUBLE=Y
STREAM=Y
STRING=Y
STRING_PARSER=Y
SUPER_CYCLE=Y
SW_DAC=Y
SW_DAC_STATIC_SAMPLES=Y
SYSTEM=Y
SYSTICK=Y
SYS_INIT=Y
TABLE_UTILS=Y
TASK=Y
TERMINAL=Y
TEST_HW=Y
TEST_SW=Y
THIRD_PARTY=Y
TIME=Y
TIMER0=Y
TIMER1=Y
TIMER2=Y
TIMER=Y
UART0=Y
UART2=Y
UART=Y
UART_INTERRUPT=Y
UART_ISR=Y
UNIT_TEST=Y
WM8731=Y
WM8731_I2S_SLAVE=Y
WM8731_USB_MODE=Y
WM8731_VERIFY=Y
WRITE_ADDR=Y

В Zephyr Project проблему забытых конфигов частично решает такой механиpм как KConfig. Если Вы не прописали конфиг, то KConfig выдаст ошибку сборки или сам автоматически молча добавит нужный конфиг и продолжит сборку. Однако, к сожалению, не существует stand alone утилиты KConfig.exe которую можно было бы использовать в любой сборке на Windows без отношения к Zephyr Project. Вот так…

Решение

Очевидно, что надо сделать так, чтобы на стадии отработки make скриптов как-то автоматический волшебным образом прописывались забытые конфиги для зависимостей тех программных компонентов, которые мы первоначально выбрали в файле config.mk.

Надо сделать так, чтобы конфиги прописывались автоматически.

Прежде всего нужно для каждого программного компонента создать отдельный make файл, который будет содержать информацию про его зависимости. Иначе откуда система сборки догадается, что надо ещё подключить? Назвать этот файл можно xxx_preconfig.mk. Вот, например, файл nvram_preconfig.mk. Очевидно, что для работы кода on-chip NVRAM необходимы такие программные компоненты как CRC8 и MCAL для Flash периферии. В связи с этим и определяются нужные переменные окружения.

ifneq ($(NVRAM_PRECONFIG_INC),Y)
    NVRAM_PRECONFIG_INC=Y

    NVRAM=Y
    FLASH=Y
    NVS=Y
    NVRAM_PROC=Y
    CRC=Y
    CRC8=Y
endif

Если в скриптах сборки отработает скрипт nvram_preconfig.mk, то вам не придется в файле config.mk прописывать CRC8=Y, FLASH=Y, NVS=Y и прочее. Вам будет достаточно лишь только прописать NVRAM=Y. Далее всё остальное выставится само собой.

А вот это preconfig для SD-карты в режиме SPI.


ifneq ($(SD_CARD_PRECONFIG_MK_INC),Y)
    SD_CARD_PRECONFIG_MK_INC=Y

    CRC7=Y
    SPI=Y
    GPIO=Y
    CRC16=Y
    SD_CARD=Y
    SD_CARD_CRC7=Y
    SD_CARD_CRC16=Y
endif

На самом деле основная идея этого трюка взята из идеологии CMake. CMake собирает конфиг, далее система сборки (make, ninja или IDE) собирает сам проект. Только в этом случае всё проще. И конфиг, и проект собирает сама система сборки! Тут это делает всеядная утилита make.

Вот упрощенный корневой файл code_base.mk сборки пере используемого репозитория

ifneq ($(CODE_BASE_MK),Y)
    CODE_BASE_MK=Y

    include $(WORKSPACE_LOC)/code_base_preconfig.mk

    #preconfig/presets done!

    ifeq ($(CORE),Y)
        include $(WORKSPACE_LOC)/core/core.mk
    endif

    ifeq ($(MICROCONTROLLER),Y)
        include $(WORKSPACE_LOC)/microcontroller/microcontroller.mk
    endif

    ifeq ($(BOARD),Y)
        include $(WORKSPACE_LOC)/boards/boards.mk
    endif

    ifeq ($(THIRD_PARTY),Y)
        include $(WORKSPACE_LOC)/third_party/third_party.mk
    endif

    ifeq ($(APPLICATIONS),Y)
        include $(WORKSPACE_LOC)/applications/applications.mk
    endif

    ifeq ($(MCAL),Y)
        include $(WORKSPACE_LOC)/mcal/mcal.mk
    endif

    ifeq ($(ADT),Y)
        include $(WORKSPACE_LOC)/adt/adt.mk
    endif

    ifeq ($(CONNECTIVITY),Y)
        include $(WORKSPACE_LOC)/connectivity/connectivity.mk
    endif

    ifeq ($(CONTROL),Y)
        include $(WORKSPACE_LOC)/control/control.mk
    endif
    
    ifeq ($(COMPONENTS),Y)
        include $(WORKSPACE_LOC)/components/components.mk
    endif

    ifeq ($(COMPUTING),Y)
        include $(WORKSPACE_LOC)/computing/computing.mk
    endif

    ifeq ($(SENSITIVITY),Y)
        include $(WORKSPACE_LOC)/sensitivity/sensitivity.mk
    endif

    ifeq ($(STORAGE),Y)
        include $(WORKSPACE_LOC)/storage/storage.mk
    endif

    ifeq ($(SECURITY),Y)
        include $(WORKSPACE_LOC)/security/security.mk
    endif

    ifeq ($(ASICS),Y)
        include $(WORKSPACE_LOC)/asics/asics.mk
    endif

    ifeq ($(UNIT_TEST),Y)  
        include $(WORKSPACE_LOC)/unit_tests/unit_test.mk
    endif

    ifeq ($(MISCELLANEOUS),Y)
        include $(WORKSPACE_LOC)/miscellaneous/miscellaneous.mk
    endif
endif

Обратите внимание, что code_base_preconfig.mk

include $(WORKSPACE_LOC)/code_base_preconfig.mk

вызывается до сборки самого проекта. Что такое code_base_preconfig.mk? Это как раз скрипт для автоматического расставления конфигов про которые мы забыли составляя config.mk.

ifneq ($(CODE_BASE_PRECONFIG_MK),Y)
    CODE_BASE_PRECONFIG_MK=Y

    ifeq ($(BOARD),Y)
        include $(WORKSPACE_LOC)/boards/boards_preconfig.mk
    endif

    ifeq ($(MICROCONTROLLER),Y)
        include $(WORKSPACE_LOC)/microcontroller/microcontroller_preconfig.mk
    endif

    ifeq ($(CORE),Y)
        include $(WORKSPACE_LOC)/core/core_preconfig.mk
    endif

    ifeq ($(MCAL),Y)
        include $(WORKSPACE_LOC)/mcal/mcal_preconfig.mk
    endif

    ifeq ($(ADT),Y)
        include $(WORKSPACE_LOC)/adt/adt_preconfig.mk
    endif

    ifeq ($(CONNECTIVITY),Y)
        include $(WORKSPACE_LOC)/connectivity/connectivity_preconfig.mk
    endif

    ifeq ($(CONTROL),Y)
        include $(WORKSPACE_LOC)/control/control_preconfig.mk
    endif
    
    ifeq ($(COMPONENTS),Y)
        include $(WORKSPACE_LOC)/components/components_preconfig.mk
    endif

    ifeq ($(COMPUTING),Y)
        include $(WORKSPACE_LOC)/computing/computing_preconfig.mk
    endif

    ifeq ($(SENSITIVITY),Y)
        include $(WORKSPACE_LOC)/sensitivity/sensitivity_preconfig.mk
    endif

    ifeq ($(STORAGE),Y)
        include $(WORKSPACE_LOC)/storage/storage_preconfig.mk
    endif

    ifeq ($(SECURITY),Y)
        include $(WORKSPACE_LOC)/security/security_preconfig.mk
    endif

    include $(WORKSPACE_LOC)/asics/asics_preconfig.mk
    
endif

Те же скрипты nvram_preconfig.mk и sd_card_preconfig.mk будут вызываться где-то внутри storage_preconfig.mk. И т. д.

Таким образом ваш изначальный config.mk можно упростить до вот такого вида

CLI=Y
COMPLEX=Y
CSV=Y
DEBUG=Y
NVRAM=Y
DEBUGGER=Y
DFT=Y
DIAG=Y
DSP=Y
DYNAMIC_SAMPLES=Y
GENERIC=Y
I2C1=Y
I2S0_MASTER=Y
NRF5340_DK=Y
NORTOS=Y
TASK=Y
TIMER0=Y
TIMER1=Y
TIMER2=Y
UART0=Y
UART2=Y
UNIT_TEST=Y
WM8731_I2S_SLAVE=Y
WM8731_USB_MODE=Y

Всё остальное расставить preconfig автоматически! Корневой конфиг config.mk стал проще в 10 раз! Успех!

Итог
Как видите сборка из скриптов дает такие бонусы, как возможность автоматически расставлять конфигурации!

Разработана технология простого автоматического переносимого прописывания конфигов зависимостей программных компонентов на основе зависимостей указанных в фалах xxx_preconfig.mk

Надеюсь этот текст поможет другим программистам в разработке их программ.

Links

Почему Важно Собирать Код из Скриптовhttps://habr.com/ru/articles/723054/

Сортировка Конфигов для Make Сборок https://habr.com/ru/articles/745244/

Настройка ToolChain (а) для Win10+GCC+С+Makefile+ARM Cortex-Mx+GDBhttps://habr.com/ru/articles/673522/

Сборка firmware для CC2652 из Makefilehttps://habr.com/ru/articles/726352/

Генерация зависимостей внутри программыhttps://habr.com/ru/articles/765424/

ToolChain: Настройка сборки прошивок для микроконтроллеров Artery из Makefile https://habr.com/ru/articles/792590/

Автоматическое Обновление Версии Прошивки https://habr.com/ru/articles/791768/

© Habrahabr.ru