Embedded development project structure with Git X-Modules

Embedded development project structure with Git X-Modules

Embedded development, just like any other, often depends on shared code components such as libraries for specific hardware. However, there's no de-facto industry standard for managing modular projects in this area. We are going to describe one of the ways to do it on a Git repository level. We will mostly concentrate on project organization topics rather than on particular questions of embedded development.


As an example, we will create a simple program that blinks the LEDs of STM32F4DISCOVERY board by STMicroelectronics. I assume we have a STM32F4DISCOVERY board with ST32F407VGT6 MCU. I'm using Debian 10.6, but for any other OS, the steps are similar. I have 'stlink-tools' and 'gcc-arm-none-eabi' packages installed. The former is a collection of tools (and 'st-flash' in particular) to work with the Discovery board (e.g. 'st-flash' writes the compiled binary file to the MCU's flash using STLink protocol). The latter is a generic ARM cross-compiler as ST32F407VGT6 is ARM Cortex M4 based MCU.

If you don't have these packages installed do that now:

 sudo apt install stlink-tools gcc-arm-none-eabi

STMicroelectronics provides a standard library for ST32F407VGT6 (STSW-STM32065). But instead of using it (not everybody is happy with its APIs), we will develop our own library by using MCU datasheets directly. Another reason to develop our own library is to demonstrate a typical scenario when libraries are developed together with the main project itself and can be reused in other projects.

Although we will have only one project in this post, we will assume there're other projects reusing the same library. In particular, fixes and updates to the library should get into these projects as well.

We will assume the code to be stored in Git and Atlassian Bitbucket Server/Data Center is used on the server's side. This is not necessary but would give us a nice UI.

Project Structure

Our project will consist of the following major components:

  • main project source file;
  • generic library supporting ST32F407VGT6 MCU;
  • files and compiler options needed to cross-compile the C code to the ARM platform.

To cross-compile a project, one has to use a number of specific compiler options and also a startup and a linker script file. When having several projects for the same MCU, it makes sense to share these files and options across projects. Thus we will have a common component dedicated to these compilation-related files. We will name this component 'common'.

We will name the library 'stm32' component. The main project will be named 'embedded-example'. 01-structure|690x269

Step 1. Create the 'stm32' project

Create 'stm32' Git repositrory with Atlassian Bitbucket Server/Data Center UI 02-create-stm32-repository|653x500 and clone it.

git clone http://example.org/scm/ee/stm32.git stm32/
cd stm32/

Create CMakeLists.txt file there:

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)

add_library(stm32 pin.c gpio.c util.c)
target_include_directories(stm32 PUBLIC .)

This CMakeLists.txt file describes a project with three C files and tells all the projects that would include it that the include directory is at the project root. So the files structure will be:

├── CMakeLists.txt
├── gpio.c
├── gpio.h
├── pin.c
├── pin.h
├── util.c
└── util.h

Now create these files and their headers:

  • util.h & util.c - they contain convenience functions to set and get special bits;
  • gpio.h & gpio.c - they contain a dull and straightforward implementation of ST32F407VGT6 datasheet GPIO specifications;
  • pin.h & pin.c - they define pin names like hw_pin_PD12 and their convenience equivalents like hw_pin_led_green.

For instance, on STM32F4DISCOVERY PD12 pin of the MCU is connected to the green LED, that's why it's convenient to define:

#define hw_pin_led_green hw_pin_PD12

Now add, commit, and push the changes to 'stm32' library:

git add *.c
git add *.h
git add CMakeLists.txt
git commit -m "Initial."
git push origin master

03-stm32-repository-toc|436x499 I would note that as this library is self-contained, one can even compile (but not cross-compile, so far) it:

mkdir build
cd build
cmake ..

If everything compiles, it's a good indicator, though a pretty useless property as our target platform is ARM.

Step 2. Create the 'common' project

This project will contain the common configuration that is shared between projects. It's also convenient to put it into a separate Git repository and insert it into each project repository to be included by the project CMakeLists.txt.

Create the 'common' repository with Atlassian Bitbucket Server/Data Center

04-create-common-repository|659x500 and clone the newly created repository:

git clone http://example.org/scm/ee/common.git common/
cd common/

Now create vars.cmake:

set(CMAKE_SYSTEM_NAME      Generic)

set(CMAKE_C_COMPILER       arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER     arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER     arm-none-eabi-as)
set(CMAKE_OBJCOPY          arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP          arm-none-eabi-objdump)

set(CMAKE_C_FLAGS "-mthumb -mcpu=cortex-m4 -fno-builtin -Wall -Wno-pointer-to-int-cast -std=gnu99 -fdata-sections -ffunction-sections" CACHE INTERNAL "c compiler flags")
set(CMAKE_CXX_FLAGS "-mthumb -mcpu=cortex-m4 -fno-builtin -Wall -Wno-pointer-to-int-cast -fdata-sections -ffunction-sections" CACHE INTERNAL "cxx compiler flags")
set(CMAKE_ASM_FLAGS "-mthumb -mcpu=cortex-m4" CACHE INTERNAL "asm compiler flags")

set(CMAKE_EXE_LINKER_FLAGS "-nostartfiles -Wl,--gc-sections -mthumb -mcpu=cortex-m4" CACHE INTERNAL "exe link flags")


This file defines various variables needed for the cross-compilation of sources with ARM as the target platform. It sets compiler flags and a path to the 'st-flash' utility that writes binary files to MCU.

Also add stm32f407vg_flash.ld, startup_stm32f40xx.s, and stm32f4xx.h files. I will not cite them here, but they are quite standard and distributed by STMicroelectronics.

So the repository content becomes:

├── startup_stm32f40xx.s
├── stm32f407vg_flash.ld
├── stm32f4xx.h
└── vars.cmake

Commit and push the changes:

git add vars.cmake stm32f4xx.h startup_stm32f40xx.s stm32f407vg_flash.ld
git commit -m "Initial."
git push origin master


Step 3. The project repository

Now let's create the main project repository. The repository will have the following structure:

├── CMakeLists.txt
├── common   <--- here common.git will be inserted
├── libs
│   └── stm32  <--- here stm32.git will be inserted
└── src
    └── main.c

So create an empty Git repository using Atlassian Bitbucket Server/Data Center UI

06-create-embedded-example-repository|660x500 and clone it:

git clone http://example.org/scm/ee/embedded-example.git embedded-example/
cd embedded-example/

Create CMakeLists.txt there:

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
enable_language(C ASM)



target_include_directories(${PROJECT_NAME}.elf PRIVATE stm32)
target_link_libraries(${PROJECT_NAME}.elf PRIVATE stm32)

add_custom_target(write-flash DEPENDS ${PROJECT_NAME}.bin COMMAND ${STLINK_CMD})

It's important to call include() on the file with common variables before project() call otherwise CMake falls into an infinite loop (I would consider this strange behavior as a CMake bug). It's also important to specify the linker script that we've put into the 'common.git' project.

add_subdirectory() call includes the library. We can include several libraries there. The libraries will be inserted in the libs/ directory.

The last line creates a make write-flash target that will not only create a binary file for the MCU but also will write it into its flash using STLink protocol. ${STM32_STLINK_CLI_EXECUTABLE} is defined in the 'common' project.

Now create src/main.c file:

#include <gpio.h>

hw_pin_t leds[] = {
//  hw_pin_led_orange

int leds_count = sizeof(leds) / sizeof(hw_pin_t);

void SystemInit() {

int main() {
  hw_gpio_configure(hw_pin_led_blue, hw_gpio_mode_output_pull_push, hw_gpio_speed_2mhz, hw_gpio_alternate_function_none);
  hw_gpio_configure(hw_pin_led_green, hw_gpio_mode_output_pull_push, hw_gpio_speed_2mhz, hw_gpio_alternate_function_none);
  hw_gpio_configure(hw_pin_led_orange, hw_gpio_mode_output_pull_push, hw_gpio_speed_2mhz, hw_gpio_alternate_function_none);
  hw_gpio_configure(hw_pin_led_red, hw_gpio_mode_output_pull_push, hw_gpio_speed_2mhz, hw_gpio_alternate_function_none);

  int current_led = 0;

  while (TRUE) {  
    hw_gpio_set(leds[current_led], FALSE);
    current_led = (current_led + 1) % leds_count;
    hw_gpio_set(leds[current_led], TRUE);

    int delay = 1000000;
    while (delay--) {

This file is simple: it initializes pins related to LEDs and blinks with red, green, and blue pins. We've commented out the orange pin as default STM32F4DISCOVERY firmware blinks with all 4 LEDs and we want to differ from that.

Commit and push the changes:

git add src/main.c
git add CMakeLists.txt
git commit -m "Initial."
git push origin master


Step 4. Insert 'common.git' and 'stm32.git' repositories into 'embedded-example.git'

To insert one repository to another we will use Git X-Modules. For Atlassian Bitbucket Server/Data Center there's a dedicated app with a nice UI. For other Git servers use the: Git X-Modules Command-Line Tool.

Make sure you have X-Modules app installed into Atlassian Bitbucket Server/Data Center. If not, visit Administration | Find new apps | Search the [Marketplace](https://marketplace.atlassian.com/apps/1223696/git-x-modules-for-bitbucket-server) and type "X-Modules" from Bitbucket Server/Data Center UI. 07-install-x-modules-app|690x203 Go to the 'embedded-example' Git repository page. When the Git X-Modules app is installed there's a Git X-Modules button on the left panel, click it. 09-x-modules-button|690x181 Then click 'Add Module' to add the first module (let it be 'common.git'). 11-x-modules-add-first-module|690x275 Choose 'common' repository and 'master' branch. Make sure "This Repository Path:" is 'common'. It's the path where the repository will be inserted: 11-x-modules-add-common-preview|690x443 Click 'Add Module'. Without applying the changes click 'Add Module' again to add 'stm32' repository as module. 12-x-modules-add-second-module|690x422 Choose 'stm32' repository and 'master' branch. 13-x-modules-choose-stm32-repository|690x464 Make sure "This Repository Path:" is "libs/stm32", this is the insertion path for 'stm32.git' repository. 14-x-modules-choose-stm32-module-path|690x453 Click 'Add Module' and apply the changes. 15-x-modules-apply-changes|664x500 Now the repositories are inserted as X-Modules. 16-embedded-example-repository-toc|560x500 This means that 'common.git' and 'stm32.git' are synchronized with corresponding directories ('common' and 'libs/stm32' respectively) of 'embedded-example.git'.

Fetch the changes from 'embedded-example':

cd embedded-example/
git pull --rebase

Now the project contains everything:

├── CMakeLists.txt
├── common
│   ├── startup_stm32f40xx.s
│   ├── stm32f407vg_flash.ld
│   ├── stm32f4xx.h
│   └── vars.cmake
├── libs
│   └── stm32
│       ├── CMakeLists.txt
│       ├── gpio.c
│       ├── gpio.h
│       ├── pin.c
│       ├── pin.h
│       ├── util.c
│       └── util.h
└── src
    └── main.c

Check that the project compiles:

mkdir build
cd build
cmake ..
make write-flash

At the moment of running make write-flash, the STM32F4DISCOVERY board should be connected. If you do everything correctly, you'll see 3 LEDs blinking.

This repository structure can be reused for any other ST32F407VGT6-based project, it's enough to insert 'common' and 'stm32' at the corresponding places. Moreover, the directories are bi-directionally synchronized with the inserted repositories, so one can change, see the results, and modify 'stm32' directly from the project repository.