Integración continua para STM32
En el post anterior revisamos la utilidad de controlar el sistema de compilación y de generar un entorno de compilación replicable.
Ahora realizaremos un ejemplo para los microcontroladores STM32 de ST usando CMake como sistema de compilación y pondremos en marcha un job de compilación utilizando Github Workflows y Travis CI.
Compilando STM32 con CMake
Utilizaremos módulos configurables de CMake para las familias de STM32. Permiten compilar de manera modular cualquier familia de STM32.
Con un CMakeLists.txt
tan simple como este podemos compilar un blinking led.
# CMakeLists.txt
PROJECT(stm32-blinky)
CMAKE_MINIMUM_REQUIRED(VERSION 2.8)
ENABLE_LANGUAGE(ASM)
FIND_PACKAGE(CMSIS REQUIRED)
FIND_PACKAGE(STM32HAL COMPONENTS gpio tim REQUIRED)
INCLUDE_DIRECTORIES(
${CMAKE_CURRENT_SOURCE_DIR}
${CMSIS_INCLUDE_DIRS}
${STM32HAL_INCLUDE_DIR}
)
SET(PROJECT_SOURCES
main.c
)
ADD_EXECUTABLE(${CMAKE_PROJECT_NAME} ${PROJECT_SOURCES} ${CMSIS_SOURCES} ${STM32HAL_SOURCES})
STM32_SET_TARGET_PROPERTIES(${CMAKE_PROJECT_NAME})
STM32_ADD_HEX_BIN_TARGETS(${CMAKE_PROJECT_NAME})
Para compilar con estos módulos necesitas cmake y la toolchain de ARM. Utilizaremos una imagen de Docker que contenga todas las dependencias.
Creando una imagen de Docker con el entorno de compilación
El objetivo es crear una imagen que permita compilar el proyecto sin ninguna dependencia más allá del código del proyecto. Aquí no hablaremos de cómo usar Docker, si no tienes experiencia con Docker te recomiendo buscar tutoriales en internet, hay muchos.
Para compilar necesitamos:
- gcc, g++ y cmake
- gcc-arm-none-eabi toolchain de ARM
- stm32-cmake modules
Para incluir las dependencias del punto 1, utilizaremos una imagen de Ubuntu e instalremos los paquetes con apt.
La toolchain de ARM la obtendremos del sitio oficial y la instalaremos en un directorio cualquiera para el que crearemos una variable de entorno definida como ARM_TOOLCHAIN
. Con los módulos de CMake haremos lo mismo en una variable llamada STM32_CMAKE_MODULES
.
Además, incluímos los drivers para todas las familias de STM32 en la imagen. Esto nos permitirá no tener que incluir los drivers en nuestro proyecto si no queremos. La desventaja de tomar esta elección es que no podrás depurar los drivers ya que el código no estará accesible. Sin embargo, es algo opcional pues como veremos más adelante podremos indicar el directorio del que queremos que se compilen los drivers.
El resultado es este Dockerfile. Puedes hacer pull de la imagen directamente de Dockerhub docker pull cortesja/stm32-cmake:latest
.
Compilando desde la imagen de Docker
Para lanzar la compilación es necesario utilizar los argumentos que requieren los los módulos de CMake para STM32 que estamos usando.
Utiliza las variables de entorno de nuestra imagen como argumentos. En este ejemplo usamos el micro STM32F072RB
, utilizando los drivers de la imagen y modo debug.
$ cd build
$ cmake -DSTM32_CHIP=STM32F072RB -DCMAKE_TOOLCHAIN_FILE=$STM32_CMAKE_MODULES/gcc_stm32.cmake -DTOOLCHAIN_PREFIX=$ARM_TOOLCHAIN -DCMAKE_BUILD_TYPE=Debug -DSTM32Cube_DIR=$STM32_FW_F0 ..
$ make
Para lanzar la compilación desde la imagen lanza un script build.sh
que contengan los comandos anteriores desde la carpeta raíz de tu proyecto:
$ docker run --rm -v $(pwd):/home/stm32/ws cortesja/stm32-cmake:latest bash -c "sh build.sh"
Con este comando lo que estás haciendo es lanzar un contenedor temporal (se borrará al finalizar la tarea) compartiendo la carpeta de tu proyecto con el contenedor de Docker dentro de la carpeta de trabajo del contenedor /home/stm32/ws
y lanzar el comando build.sh
que has creado previamente.
Si prefieres utilizar los drivers de tu proyecto sería tan sencillo como modificar el script build.sh
cambiando el directorio de STM32Cube tal que así: -DSTM32Cube_DIR=/home/stm32/ws
, si tienes los drivers dentro de la carpeta del proyecto tal y como los genera STM32CubeMX.
Integración en un sistema CI/CD
Una vez puedes compilar con la imagen de Docker, integrar el proyecto en un sistema CI/CD es muy sencillo. Sólo necesitas descargar el proyecto y lanzar una instancia de la imagen que lo compile.
Como ejemplo usaremos Github Actions. Usaremos la action checkout@v2
y la de lanzar un contenedor de Docker que ejecute nuestro script de compilación.
# build.yml
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build project
uses: docker://cortesja/stm32-cmake:latest
with:
args: bash -c "sh build.sh"
Accede al código fuente. De manera similar puedes utilizar este ejemplo para Travis CI.
Si tienes tests puedes incluirlos como otro job tras la compilación.
Depuración con STM32CubeIDE
Para depurar simplemente hay que configurar una debug configuration en STM32CubeIDE que tome el elf que hemos generado y deshabilitar la compilación automática al depurar.
Si usas el flag de compilación -fdebug-prefix-map=/home/stm32/ws/=
detectará automáticamente los directorios del código fuente. Si no utilizas este flag puedes proporcionarlo a mano en la configuración de debug utilizando la opción locate source files. En el proyecto de ejemplo está todo configurado.
Conclusiones
Para montar un sistema de integración contínua se necesita poder lanzar la compilación de manera controlada por línea de comandos. Es conveniente utilizar un sistema de compilación con el que te sientas cómodo que te permita extender el proyecto fácilmente, en este caso hemos usado CMake pero cualquier opción es válida.
Teniendo claras todas las dependencias del entorno de compilación se puede crear una imagen de Docker que las abstraiga y poder compilar en cualquier máquina.
Con esta imagen se puede crear un job rápidamente en cualquier sistema de CI/CD y compartir el entorno con cualquier usuario del proyecto.
- Código fuente: stm32-cmake-docker.
- Imagen de Docker: stm32-cmake.