To move beyond the "black box" of the Arduino IDE and manually deploy a binary onto a professional 32-bit ARM Cortex-M4 (STM32F407VET6).
I wanted to "get my hands dirty" and actually get a clue of what is needed to make it work, using an open source low level library (libopencm3).
PreparationsBefore anything, we need to install the software. I'm running a Debian machine, so, everything will be installed via apt.OpenOCD
arm-none-eabi-gcc
arm-none-eabi-objcopy
gdb-multiarch
libopencm3
Also, hardware wise, I'm using a Segger J-link (clone, of course), and a DIY More STM32F4 Discovery board.
I used SWD to "talk" with the chip, which uses only DIO and CLK pins, and the "juice" Vcc and GND.I also powered the board from a different source (USB cable) other than feeding it from the J-link.
J-link Vtref ---- STM32F407VET6 3v3
J-link Gnd ---- STM32F407VET6 Gnd
J-link Tck ---- STM32F407VET6 PA14 (CLK)
J-link Tms ---- STM32F407VET6 PA13 (DIO)
STM32F407VET6 was powered using a USB cable to a laptop USB port.
There is one more connection that I will tell about it later in the post!
To move beyond the Arduino "black box, " I implemented a professional-grade development pipeline. This chain of responsibility ensures that every line of C code is precisely mapped to the STM32's hardware registers.
- The Blueprint (Source & Linker): C code is written using the libopencm3 library. A custom Linker Script (
.ld) defines the memory map, telling the tools exactly where the 512KB of Flash and 128KB of RAM live on the STM32F407VET6. - The Construction (Toolchain): The
arm-none-eabi-gcccross-compiler translates high-level logic into ARM machine code. The Makefile automates this process, ensuring that the final ELF file contains both the executable binary and the debug symbols needed for GDB. - The Bridge (OpenOCD & J-Link):OpenOCD acts as the translator between the software world and the physical world. It communicates via USB with the Segger J-Link, which uses the SWD (Serial Wire Debug) protocol to manipulate the chip's internal state.
- The Control (GDB): Using
gdb-multiarch, I gained "God-mode" access to the CPU. This allowed me to halt execution, step through thedelay()loop, and manually inspect the GPIO registers to verify the hardware was responding to my commands.
Now, the more practical work.
After writing the source code, I started an OpenOCD server with the following command:
$ openocd -f tcl/interface/jlink.cfg -c "transport select swd" -f tcl/target/stm32f4x.cfg
Open On-Chip Debugger 0.12.0+dev-01390-gb4518ab78 (2026-02-09-18:02)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : J-Link V9 compiled Sep 1 2016 18:29:50
Info : Hardware version: 9.60
Info : VTarget = 3.338 V
Info : clock speed 2000 kHz
Info : SWD DPIDR 0x2ba01477
Info : [stm32f4x.cpu] Cortex-M4 r0p1 processor detected
Info : [stm32f4x.cpu] target has 6 breakpoints, 4 watchpoints
Info : [stm32f4x.cpu] Examination succeed
Info : [stm32f4x.cpu] starting gdb server on 3333
Info : Listening on port 3333 for gdb connectionsThis means the communication is fine, wiring is fine. However, when I tried to run, in another window, the Make file:I was getting this and other stuff like this in gdb window:
$ make debug
gdb-multiarch -ex "target extended-remote :3333" \
-ex "monitor reset halt" \
-ex "load" \
-ex "monitor reset halt" \
-ex "tbreak main" \
-ex "continue" \
bin/test.elf
GNU gdb (Debian 13.1-3) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bin/test.elf...
Remote debugging using :3333
0xd0f74546 in ?? ()
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 00000000 pc: 0x0f00f1b8 msp: 0x8000f8d0
Loading section .text, size 0xe70 lma 0x8000000
Loading section .data, size 0xc lma 0x8000e70
Start address 0x08000000, load size 3708
Transfer rate: 6 KB/sec, 1854 bytes/write.
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 00000000 pc: 0x0f00f1b8 msp: 0x8000f8d0
Temporary breakpoint 1 at 0x80001bc: file src/test.c, line 10.
Note: automatically using hardware breakpoints for read-only addresses.
Continuing.
[stm32f4x.cpu] clearing lockup after double fault
Program received signal SIGINT, Interrupt.
0xd0f74546 in ?? ()
(gdb)I struggled a bit to understand what was going on. Why a simple code was generating SIGINT, Interrupt.Looking at the registers, it tells us that the internal pointers pc and msp are pointing to the wrong place, where the factory-installed ST bootloader lives.Weird. The board has 2 push buttons. I tried to power-cycle the board, while pushing one button at a time, trying the upload, and even pushing both buttons at the same time and trying the upload multiple times but the internal pointers were always pointing to the wrong memory locations.Then, I tried the hard way. Manually short the BOOT0 pin of the chip by forcing it to GND and voilá...
Now, OpenOCD shows this:
[stm32f4x.cpu] halted due to debug-request, current mode: ThreadxPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000Correct memory addresses for pc and msp.And in gdb side I was seeing what I was expecting to see. The main function and the first lines of code being executed. I already have added a temporary breakpoint in the Makefile, that's why the code stops in line 10 of the source code file, as we see in the picture below
$ make debug
gdb-multiarch -ex "target extended-remote :3333" \
-ex "monitor reset halt" \
-ex "load" \
-ex "monitor reset halt" \
-ex "tbreak main" \
-ex "continue" \
bin/test.elf
GNU gdb (Debian 13.1-3) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bin/test.elf...
Remote debugging using :3333
0xd0f74546 in ?? ()
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000
Loading section .text, size 0xe70 lma 0x8000000
Loading section .data, size 0xc lma 0x8000e70
Start address 0x08000000, load size 3708
Transfer rate: 6 KB/sec, 1854 bytes/write.
[stm32f4x.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000ab4 msp: 0x20020000
Temporary breakpoint 1 at 0x80001bc: file src/test.c, line 10.
Note: automatically using hardware breakpoints for read-only addresses.
Continuing.
Temporary breakpoint 1, main () at src/test.c:10
10 rcc_clock_setup_pll(&rcc_hse_8mhz_3v3[RCC_CLOCK_3V3_168MHZ]);
(gdb)But to get here, I had to edit the linker script and I didn't know how to do it, so, for this, I asked for AI help and I managed to get away with it. I add to add a few lines with things I didn't have a clue in a way I didn't have a clue either. I'm talking about the format of a linker script, the nuances and all the details. Anyway, somehow I got to this point.
Now, another cool thing with debugging with gdb, is the fine tune we can do with it, from the command line! I like this... :)
On the STM32F4, the GPIOA base address is 0x40020000. The ODR is located at an offset of 0x14, making the full address 0x40020014.
To see the current state of Port A, use this command in gdb:
(gdb) p/t *(unsigned int *)0x40020014
$1 = 10101000000000000000000000000000
(gdb)The /t flag displays the output in binary.Bit 1 (the second digit from the right) corresponds to GPIO1.
...00000010 means the LED is ON
...00000000 means the LED is OFF
Then, I set a breakpoint at the line that performed the toggle, line 16, in my case:
(gdb) b test.c:16
Breakpoint 2 at 0x80001da: file src/test.c, line 16.
(gdb)Then I let the code continue
(gdb) c
Continuing.
Breakpoint 2, main () at src/test.c:16
16 gpio_toggle(GPIOA, GPIO1);
(gdb)and we hit the line that performs the toggle. So we check the register before we toggle to verify the state of the LED/pin.
(gdb) x/1xw 0x40020014
0x40020014: 0x00000000
(gdband then we let the code continue with command `next` to toggle the pin
(gdb) n
5 for(int i = 0; i < 1000000; i++)
(gdb)and we check the pin state again, after the toggle
(gdb) x/1xw 0x40020014
0x40020014: 0x00000002
(gdb)This proves the pin changed state. And the work is done.But some questions remain without an answer, at least for me!
The board has these 2 push buttons. One seems to serve the reset to the chip, via NRST pin. I kind of reverse engineered the connections and they are in the file attached.
The other one, I don't know what can it be used for. AI says it may be used as a user button, as a programmable input!The fact that BOOT0 and BOOT1 are not tied to any of these push buttons, is the most weird thing to me. Usually I find push buttons to be able to select between boot and run modes.Here is the circuitry I found for these 4 parts of the board:
Note: I didn't care about capacitor values. They are random values, probably the ones set by falstad simulator!I also want to save here a list of gbd command used and their purpose. I think it's a nice cheat sheet for later reference!p/t *(uint32_t *)0x40020014 print in binary. Shows the bits for GPIOA_ODRp/x *(uint32_t *)0x40020000 print in hex. Shows GPIOA_MODER (config)watch *(uint32_t *)0x40020014 Data watchpoint. Stops the CPU automatically when this memory address changesx/xw 0x08000000 Examine Hex word. x (examine) /xw (hex word). Looks at raw flash memoryx/2xw 0x08000000 Examine 2 Hex words. x (examine) /xw (hex word). Shows the Initial Stack Pointer and Reset Vectorinfo line *0x08000000 Map Addess to Line. Tells you which line of code lives at that addressp/x $pc Program counter. Shows the address of the current instruction being executed.p/x $msp Mais Stack Pointer. Shows the current top of the stack in RAM.
As I want to learn a bit more about libopenCM3 library and get used to it, I'll be doing a few more things like this one in the future!Thanks to anyone that enjoys the read!


Comments