In embedded world, using IDE (like Keil and STM32Cube IDE) is heavily the preferred choice because of simplifying the all processes. But I think that we, embedded software developers, must learn and know what is going on under the hood!
In this articles, I'll show the all the steps of flashing and debugging a firmware written for stm32f446x series microcontroller. Firstly, I'll introduce the flashing a firmware into microcontroller and then give you how to debug it.
Before giving details, let's talk about what we need:
- A Linux terminal (Ubuntu terminal)
- A firmware (ended with .elf or .bin)
- ARM GCC toolchain (arm-none-eabi- commands)
- Open On-Chip Debugger (openocd command)
- ST-Link hardware
Please get these before going further according to your system. I'm current on Ubuntu 22.04.
When talking about a embedded firmware, we have two program formats that we use in general. These are .bin and .elf formats. Ended with .bin firmware just contains the pure machine code and it has not debug information, program sections or other stuffs. It is used just for flashing, not for debugging. Beside that .elf firmware contains both pure machine code and other stuffs so that we can flash and debug it.
Please use the firmware ended with .elf
Another important point is the connection type between your host and microcontroller. For ARM Cortex, we have a few options:
- openocd
- dfu-util
- st-flash
I have ST-Link connection so that openocd is preferred way in our case. If you have USB connection, you can use dfu-util. Most of the STM32 microcontrollers support DFU (Device Firmware Update) mode. Don't forget that you can just flash the firmware over USB, not for debugging.
All right, let's dive into flashing.
First of all, open a terminal and type this command:
$ openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
In here, we specified two files. These are interface (which connection type that I use?) and target (what type of microcontroller that I wanna flash and debug). You can see all interface and target files from openocd folder that you've installed.
After typed this command, you get:
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select <transport>'.
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 2000 kHz
Info : STLINK V2J33M25 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.228402
Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected
Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f4x.cpu on 3333
Info : Listening on port 3333 for gdb connections
[stm32f4x.cpu] halted due to breakpoint, current mode: Thread
xPSR: 0x61000000 pc: 0x08000530 msp: 0x2001ffd8
You can think that openocd is a bridge between your host and microcontroller. To flash and debug the firmware, we'll use arm-none-eabi-gdb command. It is actually a debugger for ARM Cortex. But we can flash too.
Right now, open a new terminal and type:
$ arm-none-eabi-gdb firmware.elf
After that you are in gdb session and we will make all operations from here. The first thing that we wanna is to connect to somewhere.
From openocd output, we see the "Info : Listening on port 3333 for gdb connections" line. As I said before, openocd is a bridge and created a port 3333 for gdb that we will connect there from gdb session. Let's do it:
(gdb) target remote localhost:3333
After that, you see the first line of main() from the firmware. It's time to flash the firmware and then inspect it:
(gdb) monitor reset halt
(gdb) load
Important note is that before flashing the firmware, resetting the microcontroller and halting the current execution flow is good practice. After these commands, you should see:
Loading section .isr_vector, size 0x1c4 lma 0x8000000
Loading section .text, size 0x1080 lma 0x80001d0
Loading section .rodata, size 0x10 lma 0x8001250
Loading section .init_array, size 0x4 lma 0x8001260
Loading section .fini_array, size 0x4 lma 0x8001264
Loading section .data, size 0x10 lma 0x8001268
Start address 0x80011d0, load size 4716
Transfer rate: 9 KB/sec, 786 bytes/write.
Congrats! You finally flashed the firmware.
In here, you see that many sections written to base address of flash memory (0x08000000). I will not make end-to-end binary analysis but let's explain basically what these sections are:
- .isr_vector --> IRS (Interrupt Service Routine) handlers
- .text --> machine code that I've written
- .rodata --> read-only variable declarations (with const in C)
- .init_array, .fini_array --> hardware-level machine code that I've not written
- .data --> static or global initialized variables (if exists)
- .bss --> static or global uninitialized variables (if exists)
These are the main blocks/sections in any sort of program. You can inspect these in deeply with arm-none-eabi-objdump and arm-none-eabi-readelf commands. But for now, let's go with debugging.
The main purpose of debugging is to track the execution of firmware and, sometimes, extract the any possible information about firmware.
You can look at these information over info commands. Some examples are below:
(gdb) info files
(gdb) info registers
(gdb) info functions
(gdb) info variables
(gdb) info locals
(gdb) info frame
(gdb) info breakpoints
(gdb) info line *addr
With list command, you can see the part of source file from gdb session:
(gdb) list 1
1/**
2 * FreeRTOS 101 - Complete Introductory Program
3 */
4
5#include "main.h"
6
7void TaskLED(void *params)
8{
9while (1) {
10HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);
After gaining a overview about the firmware, next step is to track the execution flow. To control the execution flow, we have break, continue, step, next, finish commands:
(gdb) break main # put the breakpoint at begin of main() function
(gdb) break *0x80001d0 # put the breakpoint at 0x080001d0 address
(gdb) delete # delete all breakpoints
(gdb) continue # continue the execution flow until any breakpoint
(gdb) next # go to next instruction (over functions)
(gdb) step # go to next instruction (into functions)
(gdb) finish # finish the execution flow
When dealing with embedded systems, we probably want to look at (and set) core and peripheral registers or variables so that we gain the low-level insights about it. For this we have print, x/, set commands. We use print for displaying any variable defined in source code, x/ for displaying registers, set for setting register bits:
(gdb) print initLED # display the GPIO initialization structure
(gdb) x/x 0x40023830 # look at RCC AHB1 peripheral clock enable register in hex
(gdb) x/d 0x40023830 # look at RCC AHB1 peripheral clock enable register in decimal
(gdb) x/s 0x40023830 # look at RCC AHB1 peripheral clock enable register in string
(gdb) x/10wx 0x40023830 # look at RCC AHB1 peripheral clock enable with next 9 word (32-bit) registers
(gdb) set {int32_t}0x40023830 |= (1 << 2) # set the third bit of RCC AHB1 peripheral clock enable register
Well done! This is 80% of debugging.
Until now, I've tried to give you general workflow and commands about flashing and debugging the firmware. But as you guest, this is deep and complex area so that it deserves many articles. For now, I think that it is enough!