Crosscompilar un ARM Cortex-M0 con CMake

Publicado por Oravatec en

CMake

En artículos anteriores introdujimos los sistemas de compilación de los microprocesadores y vimos un ejemplo práctico de cómo realizar integración continua compilando con CMake. En el ejemplo utilizamos módulos existentes de CMake. Ahora vamos a aprender a utilizar compilación cruzada con CMake un Cortex-M0 desde 0 utilizando la HAL de ST.

Describiremos los siguientes pasos:

  1. Configurar el entorno.
  2. Configurar el compilador en CMake.
  3. Establecer el target.
  4. Listar los include paths y fuentes.
  5. Establecer las opciones de compilación y de link para un Cortex-M0.
  6. Establecer los defines necesarios.
  7. Generar opciones de compilación en base al modo de compilación (debug y release)
  8. Lanzar la compilación.

Configurar el entorno

Usaremos el mismo microcontrolador, el STM32F072RB [1]. Lo primero que necesitas es instalar la toolchain para poder compilar. En este caso la arm-none-eabi. Recomiendo utilizar Docker, en la imagen stm32-cmake está la toolchain y CMake instalados. Pero puedes instalar CMake (3.15 o superior) y la toolchain en tu máquina.

Crea un proyecto básico con STM32CubeMX para el STM32F072RB y genera el código.

Configurar el compilador en CMake

A continuación crea un módulo en CMake/toolchain.cmake para configurar la toolchain.

Establece algunas variables relacionadas con la crosscompilación: set(CMAKE_SYSTEM_NAME Generic) y set(CMAKE_SYSTEM_PROCESSOR arm).

A continuación indica donde encontrar los compiladores. Puedes utilizar la variable de entorno ARM_TOOLCHAIN porque está definida en la imagen de Docker, o bien cualquier directorio acorde a tu instalación en tu máquina.

find_program(CMAKE_C_COMPILER NAMES arm-none-eabi-gcc PATHS $ENV{ARM_TOOLCHAIN}/bin)
find_program(CMAKE_CXX_COMPILER NAMES arm-none-eabi-g++ PATHS $ENV{ARM_TOOLCHAIN}/bin)
find_program(CMAKE_ASM_COMPILER NAMES arm-none-eabi-gcc PATHS $ENV{ARM_TOOLCHAIN}/bin)

Establece algunos linker flags que serán comunes set(CMAKE_EXE_LINKER_FLAGS " --specs=nosys.specs --specs=nano.specs " CACHE INTERNAL "")

Y con esto ya tienes el módulo que carga el compilador.

# CMake/toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

find_program(CMAKE_C_COMPILER NAMES arm-none-eabi-gcc PATHS $ENV{ARM_TOOLCHAIN}/bin)
find_program(CMAKE_CXX_COMPILER NAMES arm-none-eabi-g++ PATHS $ENV{ARM_TOOLCHAIN}/bin)
find_program(CMAKE_ASM_COMPILER NAMES arm-none-eabi-gcc PATHS $ENV{ARM_TOOLCHAIN}/bin)

set(CMAKE_EXE_LINKER_FLAGS " --specs=nosys.specs --specs=nano.specs " CACHE INTERNAL "")

Una vez tienes la configuración del compilador, crea un CMakeLists.txt en el que haremos lo siguiente:

  1. Establecer target.
  2. Establecer los include paths y sources.
  3. Crear el binario.
  4. Configurar las opciones de compilación y link.
  5. Declarar los defines necesarios.

Establecer el target

Define la versión mínima requerida de CMake, establece el proyecto, los lenguajes de programación y establece el target de la siguiente manera:

cmake_minimum_required(VERSION 3.15)

project(stm32-cmake-basic C ASM)
set(TARGET ${PROJECT_NAME}.elf )

Establecer los include path y fuentes

Por claridad separa la parte que es propia de los drivers de lo que es propio de la aplicación. Hay que conocer cuales son los directorios y los ficheros fuente a incluir. Cuando no lo tienes claro lo mejor es revisar los makefiles de algún proyecto de ejemplo.

# STM32/CMSIS Drivers and HAL
include_directories(
    Drivers/STM32F0xx_HAL_Driver/Inc
    Drivers/STM32F0xx_HAL_Driver/Inc/Legacy
    Drivers/CMSIS/Device/ST/STM32F0xx/Include
    Drivers/CMSIS/Include
)
set(HAL_SOURCES
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_tim.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_tim_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_uart.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_uart_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_rcc.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_rcc_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_i2c.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_i2c_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_gpio.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_dma.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_cortex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_pwr.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_pwr_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_flash.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_flash_ex.c
    Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_exti.c
)

# STM32 System sources
set(SYSTEM_SOURCES
    Core/Startup/startup_stm32f072rbtx.s
    Core/Src/syscalls.c
    Core/Src/sysmem.c
    Core/Src/system_stm32f0xx.c
    Core/Src/stm32f0xx_it.c
    Core/Src/stm32f0xx_hal_msp.c
)

# Application includes and sources
include_directories(
    Core/Inc
)
set(APP_SOURCES
    Core/Src/main.c
)

set(PROJECT_SOURCES
    ${HAL_SOURCES}
    ${SYSTEM_SOURCES}
    ${APP_SOURCES}
)

Crear el binario

Usa la función add_executable:

add_executable(${TARGET} ${PROJECT_SOURCES})

Establecer las opciones de compilación y de link para un Cortex-M0

Las opciones de compilación necesarias son las siguientes:

  • -mcpu=cortex-m0 – Establecer el core.
  • --specs=nano.specs – Usar newlib-nano, la versión optimizada para sistemas embebidos de la libc.
  • -mthumb – Usar el juego de instrucciones T32.
  • -mfloat-abi=soft – Utilizar las librerías software para las operaciones de punto flotante ya que no tenemos hardware dedicado.

Para los ficheros en ensamblador (generalmente el código de startup) usa una generator expression tal que así: $<$<COMPILE_LANGUAGE:ASM>: -x assembler-with-cpp >. De este modo añadirá la opción de compilación sólo para los fuentes en ensamblador. Otra opción con la que puedes ahorrarte este paso es definir el compilador de assembler directamente en el módulo toolchain tal que así:

find_program(CMAKE_ASM_COMPILER NAMES arm-none-eabi-as PATHS $ENV{ARM_TOOLCHAIN}/bin)

También podemos establecer otras opciones estándar como:

  • -std=gnu11 – Establece C 2011 como estándar con extensiones de GNU, dependiendo del proyecto puede que tengas que modificarlo.
  • -ffunction-sections y -fdata-sections para eliminar el código muerto, opcional pero muy recomendable.
  • -Wall – Para activar las warnings, muy recomendable.

Hay que establecer el linker script, utiliza el que genera STM32CubeMX o uno personalizado.

target_link_options(
  ${TARGET} PRIVATE -T ${CMAKE_SOURCE_DIR}/STM32F072RBTX_FLASH.ld
)

A continuación añade las librerías libc y libm:

target_link_libraries(${TARGET} -lc -lm)

Establecer los defines necesarios

Necesitas los siguientes defines:

  • USE_HAL_DRIVER – Necesario para utilizar los HAL drivers.
  • STM32F072xB – Necesario para que el código de ST reconozca el microcontrolador.
  • DEBUG – Opcional, sólo si quieres depurar.

Generar opciones de compilación en base al modo de compilación (debug y release)

Paso opcional pero muy conveniente. Compilar en modo debug es útil para desarrollar, pero cuando el firmware está listo es recomendable optimizar el código, lo que nos impedirá depurar. Con CMake puedes elegir el modo de compilación y utilizar este flag para configurar parámetros de compilación.

Un ejemplo:

set( DEBUG_COMPILE_OPTIONS
	-O0
	-g3
	# Use relative build path for debugging when building with Docker image
	-fdebug-prefix-map=/home/stm32/ws/=
)
set( DEBUG_COMPILE_FLAGS
	DEBUG
)
set( RELEASE_COMPILE_OPTIONS
	-O2
)

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    message("-- Compiling in debug mode")
    target_compile_options( ${TARGET} PRIVATE ${DEBUG_COMPILE_OPTIONS})
    target_compile_definitions(${TARGET} PRIVATE ${DEBUG_COMPILE_FLAGS})
else()
    message("-- Compiling in release mode")
    target_compile_options( ${TARGET} PRIVATE ${RELEASE_COMPILE_OPTIONS})
endif(CMAKE_BUILD_TYPE STREQUAL "Debug")

Lanzar la compilación

Para compilar hay que lanzar CMake, indicándole si quieres compilar en modo debug o release e indicarle el módulo para configurar la toolchain. Lo ejecutaremos dentro de una nueva carpeta llamada build. Tras generar el código lanza make para compilar.

$ cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE:PATH="CMake/toolchain.cmake" ..
$ make

Y como resultado obtendremos el binario stm32-cmake-basic.elf.

Resumen

En esta guía has visto como crosscompilar paso a paso un ARM Cortex-M0 de ST usando CMake. Has configurado el compilador, las opciones de compilación, de link y defines necesarios. Has enumerado todos los fuentes necesarios para usar la HAL con un STM32F0. Y has visto un ejemplo de cómo establecer modos de compilación con CMake y cómo compilar.

Tienes disponible todos los fuentes en stm32f0-cmake-example.


[1] Nota: el proceso debería ser muy similar para cualquier micro de la familia STM32F0.


¡No te pierdas nada!


Desarrollo de productos electrónicos | Diseño electrónico | Software embebido y firmware | Fabricación electrónica e industrialización de productos