As of v4.21.0, STM32CubeMX is now capable of generating Makefiles that can be used to build projects using the GNU ARM Embedded Toolchain. Makefiles allow you to be IDE independent and use you favorite text editor. For some people, IDEs are slow and take up a lot of resources. With a Makefile, building your project is as simple as typing make
in your Terminal be you in Linux, Mac, or Windows. No more restrictions.
Although this tutorial has been written with macOS in mind, similar steps can be applied to Linux or Windows machines.
- 0 - Installing the toolchain
- 1 - Create a Project using CubeMX
- 2 - Configure your Makefile
- 3 - Building your project
- 4 - Programming the board
- 5 - Debugging
- Step 1 - Start a GDB server
- Step 2 - Launch GDB:
- Step 3 - Using GDB
- Program stepping/execution:
- Setting a breakpoint:
- Inspecting and setting variables and memory:
- Manipulating registers:
- Viewing the call stack:
- Restarting/reset:
- Looking at the code (useful when TUI mode is disabled):
- Show next assembly instructions:
- Getting help:
- Other/Advanced GDB:
- Editing your Makefile
- Serial console:
- Other Resources:
- Issues:
- Debugging tips (Part 2?)
- STM32CubeMX to generate project templates
- STM32CubeProgrammer to easily program STM32 products using a GUI
- A hardware development board (e.g. a NUCLEO-L476RG board)
- macOS Command Line Tools (CLT)
- Homebrew package manager (recommended to install gcc-arm-embedded, openOCD and stlink)
- GNU ARM Embedded Toolchain (arm-none-eabi) for compiler and other tools
- OpenOCD (>= 0.10.0) or texane/stlink for programming and running a GDB server
- Install Xcode Command Line Tools (CLT). This will install Make and other UNIX goodies:
$ xcode-select --install
After the Command Line Tools were successfully installed, the remaining toolchain requirements can be installed using Homebrew.
- Install Homebrew. Follow instructions available on brew.sh
- Install GCC ARM Embedded Toolchain:
$ brew install homebrew/cask/gcc-arm-embedded
$ arm-none-eabi-gcc --version
arm-none-eabi-gcc (GNU Tools for Arm Embedded Processors 7-2017-q4-major) 7.2.1 20170904 (release) [ARM/embedded-7-branch revision 255204]
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- Install OpenOCD:
$ brew install openocd
$ openOCD --version
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
- Install open source texane/stlink:
$ brew install stlink
$ st-info --version
v1.4.0
- Install STM32CubeMX. After Downloading the installer, extract the archieve and try to run the macOS installer. If the macOS installer doesn't work, use the following command to manually launch the install. The procedure is described in the STM32CubeMX User Manual UM1718.
$ cd ~/Downloads/
$ unzip en.stm32cubemx.zip -d en.stm32cubemx
$ cd en.stm32cubemx
$ java -jar SetupSTM32CubeMX-4.26.1.exe
- Install STM32CubeProgrammer. Similarly to CubeMX, if the installer doesn't work, use:
$ unzip en.stm32cubeprog.zip -d en.stm32cubeprog
$ cd en.stm32cubeprog
$ java -jar SetupSTM32CubeProgrammer-1.1.0.exe
If the above command does not work, you could try installing java8
:
$ brew tap caskroom/versions
$ brew cask install java8
And Java 8 will be installed at /Library/Java/JavaVirtualMachines/jdk1.8.xxx.jdk/
. You can then use the full java path to use version 1.8 to launch the STM32CubeProgrammer setup.
$ /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java -jar SetupSTM32CubeProgrammer-1.0.0.exe
Pro Tip: Create a symbolic link to one of the binary directory searched by your $PATH
variable:
$ ln -sv /Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/STM32_Programmer_CLI /usr/local/bin/
Then, STM32_Programmer_CLI
can be invoked diectly without having to specify the full path:
$ STM32_Programmer_CLI
-------------------------------------------------------------------
STM32CubeProgrammer v1.1.0
-------------------------------------------------------------------
Usage :
STM32_Programmer_CLI.exe [command_1] [Arguments_1][[command_2] [Arguments_2]...]
If you are also using an NUCLEO-L476RG, you can use the example "blinky" project by cloning the following repo:
$ git clone https://github.com/glegrain/STM32-with-macOS.git
$ cd STM32-with-macOS/Example_Project
Alternatively, you can generate you own project:
- Create a New Project, and select your part number or development board
- Configure your Pins, Clock Settings and Peripherals
- When you click Project->Generate Code, the Project Settings window will show up. Under Toolchain / IDE, select Makefile.
For more information, refer to the STM32CubeMX User Manual available on st.com. Usefull sections include:
- Tutorial 1: From pinout to project C code generation using an STM32F4 MCU
- Tutorial 4: Example of UART communications with a STM32L053xx Nucleo board
Unfortunately, Makefiles generated by CubeMX do not work out-of-the-box. You need to edit the file and set your compiler path. Luckily, this step only has to be done once. Later on, if you want to add source, header files or simply change your compiler options, refer to the [Editing your Makefile] section for more details.
- Locate the ARM Embedded GCC compiler binary location:
$ which arm-none-eabi-gcc
/usr/local/bin/arm-none-eabi-gcc
- Open up the Makefile with your favorite text editor to set the
BINPATH
variable to the location of your compiler returned above:
#######################################
# binaries
#######################################
BINPATH = /usr/local/bin
PREFIX = arm-none-eabi-
CC = $(BINPATH)/$(PREFIX)gcc
AS = $(BINPATH)/$(PREFIX)gcc -x assembler-with-cpp
CP = $(BINPATH)/$(PREFIX)objcopy
AR = $(BINPATH)/$(PREFIX)ar
SZ = $(BINPATH)/$(PREFIX)size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
Pro Tip: To make the Makefile more portable between different users and environment, you can remove the BINPATH
variable and edit the CC
, AS
, CP
, AR
, SZ
as shown bellow. This way, make will look for binaries in your environment (i.e. executables located in your $PATH
setting):
#######################################
# binaries
#######################################
PREFIX = arm-none-eabi-
CC = $(PREFIX)gcc
AS = $(PREFIX)gcc -x assembler-with-cpp
CP = $(PREFIX)objcopy
AR = $(PREFIX)ar
SZ = $(PREFIX)size
HEX = $(CP) -O ihex
BIN = $(CP) -O binary -S
In a Terminal, navigate to your project's root directory (or Makefile location). Then use the make
command to invoke the Makefile to compile your project:
$ cd ~/path/to/Example_Project
$ make
Pro Tip: for faster build time, make
can be invoked using parallel build with the -j
option:
$ make -j 4
If all goes well, you should see a result without any errors or warning:
$ make
...
arm-none-eabi-size build/Example_Project.elf
text data bss dec hex filename
8880 24 1688 10592 2960 build/Example_Project.elf
arm-none-eabi-objcopy -O ihex build/Example_Project.elf build/Example_Project.hex
arm-none-eabi-objcopy -O binary -S build/Example_Project.elf build/Example_Project.bin
Pro Tip: The Makefile generated by CubeMX comes with a predefined rule called clean
to delete all generated files during the build process (object files, binaries, ... in the build/
directory).
This rule is very useful to force rebuild all or to cleanup the project directory before packaging your project for archiving.
$ make clean
rm -fR .dep build
- Open STM32CubeProgrammer
- Connect a USB cable from the board to your computer
- Click "Connect"
- Go to the "Erasing & Programming" window
- Browse to load the binary file (
*.hex
,*.bin
or*.elf
located in thebuild/
directory)- In case of a
*.bin
binary, the Start Address needs to be specified (typically0x08000000
for STM32)
- In case of a
- Click Start Programming
- By default, CubeProgrammer does not run the application after programming. Press the black reset button to run the firmware. You should see LD2 blinking.
Note: Because STM32CubeProgrammer is still relatively new, chances are you will have to upgrade your ST-Link firmware.
For more information, you can refer to the STM32CubeProgrammer User Manual
Below are some example commands to erase and program the target using STM32CubeProgrammer CLI:
/Applications/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/STM32_Programmer_CLI -c port=SWD -e all
/Applications/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/STM32_Programmer_CLI -c port=SWD mode=UR reset=HWrst -e all # hold reset button then release when connecting
/Applications/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/STM32_Programmer_CLI -c port=SWD -w build/Example_Project.elf
If all you want to do is program the board, then run any of the following commands:
$ st-flash write ./build/*.bin 0x08000000
$ st-flash --format ihex write ./build/*.hex
Otherwise, to program and debug run the gdb server with:
$ st-util
OpenOCD requires a a configuration file. If you installed openOCD using Homebrew, list of provided configuration (*.cfg
) files can be found using the following command:
$ ls /usr/local/Cellar/open-ocd/0.10.0/share/openocd/scripts/board/
$ ls /usr/local/Cellar/open-ocd/0.10.0/share/openocd/scripts/interface/
$ ls /usr/local/Cellar/open-ocd/0.10.0/share/openocd/scripts/target/
For example, you could the following command to program and verify using elf/hex/s19 files. Verify, reset and exit are optional parameters. Binary files need the flash address passing.
$ openocd -f board/st_nucleo_l476rg.cfg -c "program build/Example_Project.hex verify reset exit"
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32l4x.cfg -c "program build/Example_Project.elf verify reset exit"
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32l4x.cfg -c "program build/Example_Project.bin 0x08000000 verify exit"
More examples and documentation available here
Because you will be debugging a remote target device, GDB needs to connect to a gdbserver compliant debugger. Before launching GDB, you need to start a GDB server using your debugger to act as an interface between GDB and your device.
$ st-util
$ st-util --no-reset # to attach while running
$ st-util -p 3333 # for OpenOCD version of .gdbinit compatibility
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32l4x.cfg
$ openocd -f interface/stlink-v2-1.cfg -f target/stm32l4x.cfg -c "gdb_port 4242" # You can also specify to use the same port as st-util
- To program and debug in a single command, I recommend to creating a
.gdbinit
script
Here is my .gdbinit
. Place this file in your project's root directory, next to your Makefile:
file "./build/Example_Project.elf"
# Connect to texane stlink gdb server
target extended-remote :4242
# Or, connect to openOCD instead
# target remote localhost:3333
# monitor reset init
# monitor halt
# Uncomment to enable semihosting
# monitor arm semihosting enable
# Flash program and load symbols
load
break main
# Run to main (first breakpoint)
continue
- Launch GDB. GDB will execute commands from the
.gdbinit
script when launched
$ arm-none-eabi-gdb
...
Loading section .isr_vector, size 0x188 lma 0x8000000
Loading section .text, size 0x1708 lma 0x8000188
Loading section .rodata, size 0x50 lma 0x8001890
Loading section .init_array, size 0x8 lma 0x80018e0
Loading section .fini_array, size 0x8 lma 0x80018e8
Loading section .data, size 0x8 lma 0x80018f0
Start address 0x80017d8, load size 6392
Transfer rate: 9 KB/sec, 1065 bytes/write.
Breakpoint 1 at 0x80016d6: file ./Src/main.c, line 83.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, main () at ./Src/main.c:83
83 HAL_Init();
(gdb)
Pro Tip: Launch GDB in GDB in Text User Interface (TUI) mode to show the source file and GDB commands in separate windows:
$ arm-none-eabi-gdb -tui
https://sourceware.org/gdb/onlinedocs/gdb/Continuing-and-Stepping.html
Step over (step to next line of C code without going into functions):
(gdb) next
90 SystemClock_Config();
(gdb) n
97 MX_GPIO_Init();
(gdb)
Step into (step to next line of C, goes into functions):
(gdb) step
SystemClock_Config () at ./Src/main.c:124
124 {
Note: Use stepi
for assembly instruction stepping.
Return from a function:
(gdb) finish
Run till exit from #0 SystemClock_Config () at ./Src/main.c:124
main () at ./Src/main.c:97
97 MX_GPIO_Init();
Run until next breakpoint:
(gdb) continue
Continuing.
Pro Tip: for most command, you can simply type in the first letter. e.g.n
for next
.
Pro Tip: Press enter to repeat the previous command. Very usefull to quickly step trough a program.
Pro Tip: gdb also supports TAB completion. e.g cont
+ TAB will result in continue
.
Pro Tip: Use control
+ c
to stop execution
https://sourceware.org/gdb/onlinedocs/gdb/Breakpoints.html#Breakpoints http://www.unknownroad.com/rtfm/gdbtut/gdbbreak.html
From there, you can add breakpoints using any of the following methods in the GDB command:
Break on line number and run until breakpoint:
(gdb) break main.c:107
Breakpoint 2 at 0x800208e: file ./Src/main.c, line 107.
(gdb) continue
Continuing.
Breakpoint 2, main () at ./Src/main.c:107
107 HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
Break on function:
(gdb) break SystemClock_Config
Breakpoint 2 at 0x8001fd0: file ./Src/main.c, line 132.
(gdb) continue
Continuing.
Breakpoint 2, SystemClock_Config () at ./Src/main.c:132
132 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
(gdb)
List breakpoints:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0800207e in main at ./Src/main.c:83
breakpoint already hit 1 time
2 breakpoint keep y 0x0800208e in main at ./Src/main.c:107
3 breakpoint keep y 0x08001fd0 in SystemClock_Config at ./Src/main.c:132
Remove a breakpoint:
(gdb) delete 2
(gdb) disable 3
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0800207e in main at ./Src/main.c:83
breakpoint already hit 1 time
3 breakpoint keep n 0x08001fd0 in SystemClock_Config at ./Src/main.c:132
(gdb) print uwTick
$1 = 1206
(gdb) set uwTick=0
(gdb) p uwTick
$2 = 0
(gdb) x /32x 0x08000000
0x8000000: 0x20018000 0x080017d9 0x08001829 0x08001829
0x8000010: 0x08001829 0x08001829 0x08001829 0x00000000
0x8000020: 0x00000000 0x00000000 0x00000000 0x08001829
0x8000030: 0x08001829 0x00000000 0x08001829 0x08001785
0x8000040: 0x08001829 0x08001829 0x08001829 0x08001829
0x8000050: 0x08001829 0x08001829 0x08001829 0x08001829
0x8000060: 0x08001829 0x08001829 0x08001829 0x08001829
0x8000070: 0x08001829 0x08001829 0x08001829 0x08001829
https://community.st.com/s/question/0D50X00009XkeAmSAJ/reading-io-register-values-with-command-line-gdb https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gcc/Debugging-Options.html
Include additional debug information, such as all the macro definitions that can be used to inspect I/O registers:
- Edit you Makefile to add the
-g3
option to the compiler flags
CFLAGS += -g3
Level 3 includes extra information, such as all the macro definitions present in the program. Some debuggers support macro expansion when you use -g3.
- In GDB, type:
(gdb) p /x *GPIOA
$4 = {MODER = 0xabfff7ff, OTYPER = 0x0, OSPEEDR = 0xc000000, PUPDR = 0x64000000,
IDR = 0xc020, ODR = 0x20, BSRR = 0x0, LCKR = 0x0, AFR = {0x0, 0x0}, BRR = 0x0,
ASCR = 0x0}
(gdb) set GPIOA->ODR ^= 0x20
(gdb) p /x TIM3->CCMR1
https://sourceware.org/gdb/onlinedocs/gdb/Backtrace.html
(gdb) backtrace
#0 HAL_RCC_OscConfig (RCC_OscInitStruct=RCC_OscInitStruct@entry=0x20017fac)
at ./Drivers/STM32L4xx_HAL_Driver/Src/stm32l4xx_hal_rcc.c:409
#1 0x08001ff4 in SystemClock_Config () at ./Src/main.c:141
#2 0x08002086 in main () at ./Src/main.c:90
(gdb) monitor reset init
Unable to match requested speed 500 kHz, using 480 kHz
Unable to match requested speed 500 kHz, using 480 kHz
adapter speed: 480 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x080021d0 msp: 0x20018000
adapter speed: 4000 kHz
(gdb) c
Continuing.
Breakpoint 1, main () at ./Src/main.c:83
83 HAL_Init();
(gdb)
(gdb) n
107 HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
(gdb) list
102
103 /* Infinite loop */
104 /* USER CODE BEGIN WHILE */
105 while (1)
106 {
107 HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
108 HAL_Delay(200);
109
110 /* USER CODE END WHILE */
111
(gdb) n
108 HAL_Delay(200);
(gdb) tui enable
https://sourceware.org/gdb/onlinedocs/gdb/Machine-Code.html#Machine-Code
(gdb) set disassemble-next-line on
(gdb) show disassemble-next-line
Debugger's willingness to use disassemble-next-line is o
(gdb) next
107 HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
=> 0x08000626 <main+186>: 20 21 movs r1, #32
0x08000628 <main+188>: 4f f0 90 40 mov.w r0, #1207959552 ; 0x48000000
0x0800062c <main+192>: 01 f0 92 fb bl 0x8001d54 <HAL_GPIO_TogglePin>
(gdb) help step
Step program until it reaches a different source line.
Usage: step [N]
Argument N means step N times (or till program stops for another reason).
Using gdb-dashboard (do not use TUI mode) to get more complex window configuration (similarly to an IDE):
$ arm-none-eabi-gdb-py
Any additional source file (*.c
or *.s
) that needs to be compiled should be added to the C_SOURCES
or ASM_SOURCES
variables inside the Makefile.
######################################
# source
######################################
# C sources
C_SOURCES = \
./Src/main.c \
./Src/new_file.c \
./Src/stm32l4xx_it.c \
./Src/syscalls.c \
./Src/system_stm32l4xx.c \
# ASM sources
ASM_SOURCES = \
./startup_stm32l476xx.s
Pro Tip: I prefixed my paths with ./
to help vim trigger file path completion.
# C includes
C_INCLUDES = \
-IInc \
-IDrivers/STM32L4xx_HAL_Driver/Inc \
-IDrivers/STM32L4xx_HAL_Driver/Inc/Legacy \
-IDrivers/CMSIS/Device/ST/STM32L4xx/Include \
-IDrivers/CMSIS/Include
Pro Tip: if the compiler complains about a missing header file for example:
./Src/usbd_conf.c:53:10: fatal error: usbd_msc.h: No such file or directory
#include "usbd_msc.h"
^~~~~~~~~~~~
use find . -name "usbd_msc.h"
to get the path and add it to Makefile:
$ find . -name "usbd_msc.h"
./Middlewares/ST/STM32_USB_Device_Library/Class/MSC/Inc/usbd_msc.h
https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gcc/Preprocessor-Options.html
# C defines
C_DEFS = \
-DUSE_HAL_DRIVER \
-DSTM32L476xx \
-D__DEBUG__
https://gcc.gnu.org/onlinedocs/gcc-7.3.0/gcc/Optimize-Options.html
# optimization
OPT = -Os
- https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
- https://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html#Option-Summary
On embedded devices, printf
statements can become very useful for debugging. Retargeting the C printf
function is generally done is one of three ways:
- through semihosting (no extra hardware required but slow)
- through UART interface (most support, speed depends on baud rate)
- through ITM messages over SWO (fastest but not always supported)
Semihosting is relatively easy to setup but it is one of the slowest methods for printing debug messages.
Enable semihosting (see example .gdbinit
)
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0471m/pge1358787046598.html
Note: With GCC, add syscalls.c
See my GitHub gist to retarget printf to UART
Check which USB devices are connected to the serial port. Also make sure all users have read / write access to the serial device:
$ ls -l /dev/tty.usbmodem*
crw-rw-rw- 1 root wheel 9, 26 Mar 27 13:20 /dev/tty.usbmodem413
8 bit, no parity, one stop bit, translate input new line carriage return, newline performs a carriage return, local echo
Optional -
before SETTING indicates negation.
http://man7.org/linux/man-pages/man1/stty.1.html
$ screen /dev/tty.usbmodem413 115200,cs8,-parenb,-cstop,inlcr,onlret,echo
Note: To exit screen
, press control-A
then control-k
and y
.
$ brew install minicom
$ minicom -D /dev/tty.usbmodem413 -b 115200
With OpenOCD, ITM can be configured using the tpiu
command.
- https://mcuoneclipse.com/2016/10/17/tutorial-using-single-wire-output-swo-with-arm-cortex-m-and-eclipse/
- http://blog.japaric.io/itm/
https://developer.arm.com/open-source/gnu-toolchain/gnu-rm http://www.bravegnu.org/gnu-eprog/ http://blog.japaric.io/quickstart/#hello-world https://www.gnu.org/software/make/manual/make.html
When creating a brand new project (example NUCLEO-L476RG), C source files under Src/
are defined multiple times.
Please comment to request topics
- mdw
- flash program
- flash mass_erase
- viewing registers and SFRs
- reg
- mdw
- semihosting
- Linker file