Les Groupes ARM : STM32/toolchain sous Linux

De Wiki_du_Réseau_des_Electroniciens_du_CNRS
Aller à la navigationAller à la recherche



Objectif

Il existe de nombreuses combinaisons d'outils pour développer sur des STM32. La méthode présentée ici n'est qu'une solution parmi d'autres. Les outils logiciels sont tous gratuits et la plupart est libre. L'outil matériel pour programmer (in-circuit debugger) coûte environ 12€.

Cette page explique comment programmer et débugger les STM32 sous Linux avec les outils suivants :

  • Configurateur graphique STM32CubeMX
  • Compilateur GNU GCC + débugger GNU GDB
  • IDE Qt Creator
  • OpenOCD, the Open On-Chip Debugger
  • In-circuit debugger ST-Link V2 (par exemple celui d'une carte de démo Nucleo-64 à 10€ env.)

Cette méthode permet de compiler, programmer et débugger le code (points d'arrêt, valeur des variables, etc.). Une fois configurée, le confort d'utilisation est excellent, a fortiori si on est familier de Qt Creator.

Installer la toolchain

STM32CubeMX

STM32CubeMX est un logiciel pour générer le code d'initialisation : horloges, timers, interruptions, périphériques (I2C, SPI, etc.)...

Pour configurer un proxy, menu Help > Updater settings > Connection parameters.

Télécharger sur https://my.st.com (créer un compte). Extraire l'archive et lancer le programme d'

   $ sudo ./SetupSTM32CubeMX-<version>.linux

Le répertoire d'installation par défaut est /usr/local/. Créer un alias (modifier PATH ne marche pas), par exemple dans ~/.bashrc ou dans ~/.bash_aliases :

   alias stm32cubemx='/usr/local/STMicroelectronics/STM32Cube/STM32CubeMX/STM32CubeMX'

Menu Help > Install new libraries et choisir celles qui correspondent au composant sur lequel on développe.

OpenOCD

Installation depuis le code source

C'est l'Open On-Chip Debugger. Il permet de programmer le MCU et de débugger (points d'arrêt, valeur des variables), en lien avec gdb.

La version de la distribution Linux est peut-être trop ancienne pour supporter les STM32. Dans ce cas, il faut télécharger le code source et le compiler.

Installer le paquet pkg-config :

$ apt-get install pkg-config

Télécharger et sélectionner la dernière version stable :

$ cd ~/opt
$ git clone git://git.code.sf.net/p/openocd/code openocd
$ cd openocd
$ git tag                         // lister les tags de versions
$ git checkout v0.11.0            // sélectionner la dernière version stable

Compiler :

$ ./bootstrap
$ ./configure --enable-stlink --disable-werror
$ make -j4
$ sudo make install
$ which openocd
/usr/local/bin/openocd

L'option --disable-werror est nécessaire en cas d'erreur de compilation avec gcc v7+.

Mettre à jour :

$ cd ~/opt/openocd
$ make clean                      // nettoyer une éventuelle version précédente
$ git pull                        // intégrer les derniers changements
$ git tag                         // lister les tags de versions
$ git checkout v0.11.0            // sélectionner la dernière version stable
$ git checkout <tag_de_la_nouvelle_version_stable>
$ make clean


Puis refaire l'étape "compiler".

Pour tester, connecter soit une carte Nucleo avec les jumpers ST-Link installés, soit une carte Nucleo avec les jumpers ST-Link enlevés et connectée à une carte maison via le connecteur CN4 (SWD). Ctrl+C pour quitter).

Utilisation

Important : avant de lancer OpenOCD, il faut connecter une carte sur un port USB du PC hôte, sinon il quitte immédiatement avec le message Error: open failed.

Lancer OpenOCD dans un terminal, et le laisser tourner pendant qu'on utilise Qt Creator. On peut se connecter au serveur OpenOCD via telnet dans un autre terminal et lui envoyer des commandes.

$ sudo openocd -s /usr/local/share/openocd/scripts/target/ -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg
Open On-Chip Debugger 0.11.0-dirty (2021-07-06-11:05)
Licensed under GNU GPL v2
For bug reports, read
	http://openocd.org/doc/doxygen/bugs.html
hla_swd
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 500 kHz
Info : STLINK V2J28M17 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.229746
Info : stm32l4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32l4x.cpu on 3333
Info : Listening on port 3333 for gdb connections


Créer /usr/local/bin/oocd.sh pour lancer cette ligne de commande :

#!/bin/bash

OOCD_DIR=/usr/local/share/openocd/scripts/target/

# Avec ST-Link V2 :
#openocd -f interface/stlink-v2.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg
# Avec ST-Link V2-1 (Nucleo) :
openocd -s ${OOCD_DIR} -f interface/stlink.cfg -c "transport select hla_swd" -f target/stm32l4x.cfg


Donner les permissions d'exécution au script :

$ sudo chmod a+x /usr/local/bin/openocd-stm32l4x

Références :

gcc

Visiter https://launchpad.net/gcc-arm-embedded et sous "Series and milestones", choisir la version sur le graphe des branches de développement. Si les fichiers ne sont pas disponibles, un lien indique où les trouver (https://developer.arm.com/open-source/gnu-toolchain/gnu-rm). gdb fourni avec cette distribution a le support Python, requis par Qt Creator. Extraire l'archive :

$ sudo tar -xf gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2 -C /usr/local/

Qt Creator

Plugins

Le menu Help > About plugins affiche une boîte de dialogue. Vérifier que les plugins suivants sont installés :

  • QbsProjectManager (inutile si vous utilisez CMake)
  • BareMetal

OpenOCD

Dans le menu Tools > Options > Devices > Bare metal, cliquer Add puis OpenOCD.

Paramètre Valeur
Name OpenOCD
Startup mode No startup
Host localhost:3333
Init commands set remote hardware-breakpoint-limit 6

set remote hardware-watchpoint-limit 4
monitor reset halt
load
monitor reset halt

Reset commands monitor reset halt

Dans le menu Tools > Options > Devices > Devices, créer un device avec le GDB server provider défini plus haut.

Créer un kit avec le compilateur, le debugger, le device bare metal.

Kit

  • Compilateur C : arm-none-eabi-gcc
  • Compilateur C++ : arm-none-eabi-g++
  • Debugger : arm-none-eabi-gdb-py
  • System CMake at /usr/bin/cmake ou celui fourni avec Qt, qui est souvent plus récent.

CMake configuration :

CMAKE_CXX_COMPILER:STRING=%{Compiler:Executable:Cxx}
CMAKE_C_COMPILER:STRING=%{Compiler:Executable:C}

Si au moment d'ajouter GDB, Qt Creator indique "not recognized", essayer de lancer GDB dans un terminal pour voir quelle est l'erreur. S'il lui manque libncurses (error while loading shared libraries: libncurses.so.5: cannot open shared object file: No such file or directory) :

$ sudo apt install libncurses5

Puis fermer/relancer Qt Creator.

Matériel

ST-Link

La procédure expliquée sur cette page peut être appliquée de 2 manières :

  • avec une carte Nucleo64 : on programme le STM32 qui est dessus via le ST-Link de la carte connectée au PC en USB
  • avec une carte Nucleo64 et une carte maison : on programme le STM32 qui est sur la carte maison via le ST-Link de la carte Nucleo64, connectée au PC en USB

Pour programmer une carte maison avec le ST-Link d'une carte Nucleo64, souder R9 (boîtier 0603, de 1K à 47K) et dessouder SB12 (jumper NRST, face de dessous). Enlever les 2 jumpers sur CN2, et connecter la carte maison à CN4 (SWD).

Carte maison

Voici un schéma exemple pour dessiner une carte maison, programmable avec le module ST-Link présent sur une carte Nucleo64.

Utilisation

Support du MCU

Pour choisir les options du compilateur/linker, voir dans le répertoire d'installation de GCC, le tableau dans /share/doc/gcc-arm-none-eabi/readme.txt, sous "Architecture options usage".

Par exemple, pour un Cortex-M4 avec une FPU (floating point processing unit), les options de GCC sont :

  • soft FP : -mthumb -mcpu=cortex-m4 -mfloat-abi=softfp -mfpu=fpv4-sp-d16
  • hard FP : -mthumb -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16

Les STM32L4 ont une FPU matérielle. La deuxième ligne est donc à privilégier.

Important : ces options doivent être passées à la compilation ET à l'édition de liens. Pour cela, LD ne doit pas être appelé directement, mais via le driver de GCC.

Générer le code d'initialisation

STM32CubeMX permet de générer le code d'initialisation du MCU :

  • fonction attribuée à chaque broche
  • configuration des horloges
  • configuration des interruptions
  • configuration des middlewares (USB, CAN, etc.)

STM32CubeMX a un gestionnaire pour télécharger les librairies ST. Il existe 2 types de librairies : HAL, de haut niveau, et LL (low-level) de plus bas niveau. La première est plus simple d'utilisation, mais est moins efficace que la seconde.

STM32CubeMX propose de faire un lien vers les libraries, ou de les copier dans le projet. La 2ème solution permet une maintenance plus facile du projet sur le long terme.

Une fois le projet configuré, on peut générer le code.

  • Pour Qbs, choisir SW4STM32 pour Toolchain/IDE et cocher la case Generate under root.
  • Pour CMake, choisir Makefile.

Le code généré contient des balises BEGIN/END en commentaire (à ne surtout pas toucher !) pour insérer le code utilisateur. Le code entre ces balises est préservé si le code est généré de nouveau avec STM32CubeMX, pour par exemple, changer une configuration. Par exemple, pour faire clignoter la LED LD2 de la Nucleo-64 avec une période de 1 s, modifier main.c :

int main(void)
{
  /* some code here */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
    HAL_Delay(500);
  }
  /* USER CODE END 3 */

  /* some code here */
}

Projet Qbs

Dans Qt Creator, créer un nouveau projet (non-Qt application, C application). Sélectionner le kit défini précédemment, et cliquer sur Configure project. Ajouter le fichier Qbs ci-dessous au projet.

Qbs (prononcer "cubes") est un outil fourni avec Qt Creator pour simplifier la compilation et épargner l'écriture de makefiles. Il est appelé à disparaître car il sera remplacé par CMake, qui est un standard libre. En attendant, voici un exemple de fichier Qbs à adapter.

import qbs

CppApplication {
    consoleApplication: true
    property string family: "STM32L4xx"
    property string linkerScript: "STM32L476RGTx_FLASH.ld"
    cpp.positionIndependentCode: false              // make sure option -fPIC is not passed to GCC

    // Make sure to call the linker through GCC driver :
    cpp.linkerMode: "manual"        // default : "automatic"
    cpp.linkerName: "gcc"           // this string is appended to "<full_path_to_toolchain>/arm-none-eabi-"

    // Define some symbols (GCC -D flag)
    cpp.defines: [
        "USE_HAL_DRIVER",
        "STM32L476xx",
        "__weak=__attribute__((weak))",
        "__packed=__attribute__((__packed__))"
    ]

    // Options for compilation AND linking.
    cpp.driverFlags: [
        // CPU
        "-mcpu=cortex-m4",
        "-mthumb",
        "-mfpu=fpv4-sp-d16",
        "-mfloat-abi=hard",
        "-specs=nano.specs",                  // use smaller libc
    ]

    // Compiler flags for all langages (C, C++).
    cpp.commonCompilerFlags: [
        // Optimizations
//        "-Os",
//        "-O0",
        "-Og",

        "-Wall",
        "-fdata-sections",
        "-ffunction-sections",

        // For debug
        "-g",
        "-gdwarf-2",

        "-c",                                       // don't run the linker
//        "-v"                                      // print a whole lot of details
    ]

    // Linker flag only i.e. understood by LD !!!
    // Qbs will prepend all these flags with "-Wl," so that GCC transfers them to LD.
    // Other options required for linking should be set in the driverFlags section.
    cpp.linkerFlags: [
        "-T"+path+"/"+linkerScript,
        "-static",
        "--verbose",                                // displays library search
        "-lc",
        "-lm",
        "-lnosys",
        "-Map="+buildDirectory+"/memory.map",       // file created at the end of the link step
        "--cref",                                   // map file formatting
        "--gc-sections",                            // fixes "undefined reference to _exit" error
    ]

    // Include directories.
    cpp.includePaths: [
        "Inc",
        "Drivers/CMSIS/Include",
        "Drivers/CMSIS/Device/ST/"+family+"/Include",
        "Drivers/STM32L4xx_HAL_Driver/Inc",
        "Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Inc",
        "Middlewares/ST/STM32_USB_Device_Library/Core/Inc/"
    ]

    // Source files.
    files: [
        "Inc/*.h",
        linkerScript,
        "Src/*.c",
        "Drivers/CMSIS/Device/ST/"+family+"/Include/*.h",
        "startup/*.s",
        "Drivers/CMSIS/Device/ST/"+family+"/Source/Templates/*.c",
        "Drivers/CMSIS/Include/*.h",
        "Drivers/"+family+"_HAL_Driver/Inc/*.h",
        "Drivers/"+family+"_HAL_Driver/Src/*.c",
        "Middlewares/ST/STM32_USB_Device_Library/Class/CDC/Src/*.c",
        "Middlewares/ST/STM32_USB_Device_Library/Core/Src/*.c"
    ]

    Properties {
        condition: qbs.buildVariant === "debug"
        cpp.defines: outer.concat(["DEBUG=1"])
    }

    Group {     // Properties for the produced executable
        fileTagsFilter: product.type
        qbs.install: true
    }
}

Projet CMake

Si on a généré le code avec STM32CubeMX en choisissant Makefile, un fichier Makefile est créé. Ce fichier contient tout ce qu'il faut pour écrire le fichier de config de CMake, CMakeLists.txt :

  • liste des .c
  • liste des répertoires à inclure
  • les defines pour target_compile_definitions()
  • les options de compilation pour target_compile_options()

Avant de commencer, renommer Makefile en Makefile.STM32CubeMX pour éviter que la compilation du fichier CMake ne l'écrase.

Voici un CMakeLists.txt, à adapter selon le MCU, en s'aidant du Makefile généré par STM32CubeMX.

# Board : nucleo64-stm32l476rg

cmake_minimum_required(VERSION 3.15.3)

# Skip toolchain check
# Useful if CMake fails with error "The C compiler is not able to compile a simple test program".
set(CMAKE_C_COMPILER_WORKS 1)
set(CMAKE_CXX_COMPILER_WORKS 1)

project(L4_blinky_cmake)

enable_language(C ASM)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)

set(STM32CUBEMX_GENERATED_FILES

    "Core/Src/main.c"
    "Core/Src/stm32l4xx_it.c"
    "Core/Src/stm32l4xx_hal_msp.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_tim.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_tim_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_uart.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_uart_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_i2c.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_i2c_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_rcc.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_rcc_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_flash.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_flash_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_flash_ramfunc.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_gpio.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_dma.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_dma_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_pwr.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_pwr_ex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_cortex.c"
    "Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_exti.c"
    "Core/Src/system_stm32l4xx.c"

    "startup_stm32l476xx.s"
    )

set(EXECUTABLE ${PROJECT_NAME}.out)

add_executable(${EXECUTABLE} ${STM32CUBEMX_GENERATED_FILES})

target_include_directories(${EXECUTABLE} PRIVATE
    Core/Inc
    Drivers/STM32L4xx_HAL_Driver/Inc
    Drivers/STM32L4xx_HAL_Driver/Inc/Legacy
    Drivers/CMSIS/Device/ST/STM32L4xx/Include
    Drivers/CMSIS/Include
    )

target_compile_definitions(${EXECUTABLE} PRIVATE
    -DUSE_HAL_DRIVER
    -DSTM32L476xx
    )

set(MCU
    -mcpu=cortex-m4
    -mthumb
    -mfpu=fpv4-sp-d16
    -mfloat-abi=hard
    )

target_compile_options(${EXECUTABLE} PRIVATE
    ${MCU}

    -fdata-sections
    -ffunction-sections

    -Wall

    $<$<CONFIG:Debug>:-Og>		# add -g if debug is selected
    )

target_link_options(${EXECUTABLE} PRIVATE
    -T${CMAKE_SOURCE_DIR}/STM32L476RGTx_FLASH.ld
    ${MCU}
    -specs=nano.specs
    -lc
    -lm
    -lnosys
    -Wl,-Map=${PROJECT_NAME}.map,--cref
    -Wl,--gc-sections                   # fixes "undefined reference to _exit" error
    )

# Get the path to the toolchain
if(CMAKE_VERSION VERSION_LESS "3.20")
    get_filename_component(TOOLCHAIN_PATH ${CMAKE_C_COMPILER} DIRECTORY)
else()
    cmake_path(GET CMAKE_C_COMPILER ROOT_PATH TOOLCHAIN_PATH)
endif()
message(STATUS "Toolchain path: ${TOOLCHAIN_PATH}")

# Print executable size
add_custom_command(TARGET ${EXECUTABLE}
    POST_BUILD
    COMMAND echo "Target size:"
    COMMAND ${TOOLCHAIN_PATH}/arm-none-eabi-size ${EXECUTABLE}
    )

# Create all necessary files to flash the MCU from the command line.
# Only relevant in Release mode.
if(CMAKE_BUILD_TYPE STREQUAL "Release")
    message(STATUS "Build type: Release")

    # Create bin and hex file
    add_custom_command(TARGET ${EXECUTABLE}
        POST_BUILD
        COMMAND ${TOOLCHAIN_PATH}/arm-none-eabi-objcopy -O binary ${EXECUTABLE} ${PROJECT_NAME}.bin
        COMMAND ${TOOLCHAIN_PATH}/arm-none-eabi-objcopy -O ihex ${EXECUTABLE} ${PROJECT_NAME}.hex
        )

    # Concatenate 2 OpenOCD config files into one, into the build directory.
    add_custom_command(TARGET ${EXECUTABLE}
        POST_BUILD
        COMMAND cat /usr/local/share/openocd/scripts/interface/stlink.cfg /usr/local/share/openocd/scripts/target/stm32l4x.cfg > ${PROJECT_NAME}.cfg
        )

    # Create a Bash script in the build directory to flash the MCU from the command line.
    # The script must be run as ROOT !
    file(WRITE "${CMAKE_BINARY_DIR}/flash.sh"
        "#!/bin/bash\n\nopenocd -f ${PROJECT_NAME}.cfg -c \"init; reset halt; stm32l4x mass_erase 0; program ${PROJECT_NAME}.bin reset exit 0x08000000\""
        )

    # Set script permissions.
    file(CHMOD "${CMAKE_BINARY_DIR}/flash.sh"
        FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_WRITE GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
        )

endif()

Référence :

Autres références :

Programmer le STM32

En mode debug

  1. Connecter la carte Nucleo-64 (avec le ST-Link V2) au PC (USB).
  2. Dans un terminal, lancer OpenOCD via le script écrit plus haut en tant que root.
  3. Dans Qt Creator
    1. configurer le projet en mode debug
    2. taper Ctrl+R (run) pour compiler, programmer, et exécuter le code sur la cible.

Et voilà !

En mode release

Bien qu'au moment de lancer le programme (Ctrl+R : flasher + exécuter) sur la cible en mode Release, Qt Creator affiche un warning "This does not seem to be a Debug build", le MCU est bel et bien programmé.

Mais on peut aussi le programmer en ligne de commande grâce à CMake et au script flash.sh généré par CMake. Voir la doc de CMake. Utile si on ne veut pas lancer Qt Creator.

  1. Stopper OpenOCD s'il est lancé dans un terminal (Ctrl+C).
  2. Dans Qt Creator, configurer le projet en mode release.
  3. Compiler (Ctrl+B).
  4. Ouvrir un terminal dans le répertoire de compilation (qui en général n'est pas celui du source).
  5. Exécuter $ sudo ./flash.sh, le script créé par CMake.

Ce script concatène 2 fichiers de config d'OpenOCD, puis efface/programme le STM32. Il est équivalent aux 2 commandes ci-dessous.

$ cat /usr/local/share/openocd/scripts/interface/stlink.cfg /usr/local/share/openocd/scripts/target/stm32l4x.cfg > myboard.cfg
$ sudo openocd -f myboard.cfg -c "init; reset halt; stm32l4x mass_erase 0; program L4_blinky_cmake.bin reset exit 0x08000000"

Erreurs connues

object_file uses VFP register arguments, target does not

Incompatibilité de FPU entre le code utilisateur et les libraries liées (-mfloat-abi=hard vs. -mfloat-abi=softfp).

Le programme compile mais bloque dans un error handler.

Vérifier que LD n'est pas appelé directement, mais via le driver de GCC. Vérifier que la ligne de commande de GCC au moment de la compilation ET au moment de l'édition de liens contient bien les options qui définissent le MCU et la libc : -mcpu=cortex-m4, -mthumb, -mfpu=fpv4-sp-d16, -mfloat-abi=hard, -specs=nano.specs.