DEV Community

Cover image for Embedded development project structure with Git X-Modules

Embedded development project structure with Git X-Modules

gitmodules profile image Dmitry Pavlenko Originally published at ・8 min read

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
Enter fullscreen mode Exit fullscreen mode

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'.

Step 1. Create the 'stm32' project

Create 'stm32' Git repositrory with Atlassian Bitbucket Server/Data Center UI
and clone it.

git clone stm32/
cd stm32/
Enter fullscreen mode Exit fullscreen mode

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 .)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ..
Enter fullscreen mode Exit fullscreen mode

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

and clone the newly created repository:

git clone common/
cd common/
Enter fullscreen mode Exit fullscreen mode

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")

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode

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

and clone it:

git clone embedded-example/
cd embedded-example/
Enter fullscreen mode Exit fullscreen mode

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})
Enter fullscreen mode Exit fullscreen mode

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--) {
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode


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]( and type "X-Modules" from Bitbucket Server/Data Center UI.
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.
Then click 'Add Module' to add the first module (let it be 'common.git').
Choose 'common' repository and 'master' branch. Make sure "This Repository Path:" is 'common'. It's the path where the repository will be inserted:
Click 'Add Module'. Without applying the changes click 'Add Module' again to add 'stm32' repository as module.
Choose 'stm32' repository and 'master' branch.
Make sure "This Repository Path:" is "libs/stm32", this is the insertion path for 'stm32.git' repository.
Click 'Add Module' and apply the changes.
Now the repositories are inserted as X-Modules.
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Check that the project compiles:

mkdir build
cd build
cmake ..
make write-flash
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Editor guide