On this page
The previous article added startup code, connected it to the linker script, and moved the project from compile-only object files to a linked firmware image.
This article takes the next step: build the firmware outputs and flash the STM32 Nucleo L433RC-P without opening STM32CubeIDE.
The workflow is still deliberately simple. VSCode debugging comes later. For now, the goal is to prove that the command-line project can produce firmware artifacts and load them onto the board through ST-LINK.
What We Have So Far
At this point, the project has these important pieces:
CMakeLists.txtwith an executable firmware target.cmake/arm-none-eabi-gcc.cmakeselecting the Arm GNU toolchain.linker/STM32L433RCTx_FLASH.lddescribing flash and RAM.src/startup_stm32l433xx.cdefining the vector table andReset_Handler.src/main.cproviding a simple placeholder application loop.
That is enough to link an ELF file. It is not enough to prove the firmware runs correctly, but it is enough to test the build and flashing path.
Name the Firmware Output
CMake target names and output filenames are related, but they are not the same thing.
The target can still be named firmware, while the output file can be named firmware.elf:
set_target_properties(firmware PROPERTIES
OUTPUT_NAME "firmware.elf"
)
Add this after add_executable(firmware ...) in CMakeLists.txt.
The .elf extension is not required for the file to be an ELF file, but it makes the artifact easier to recognize. It also makes later commands clearer because the filename describes the format.
Generate BIN and HEX Files
The ELF file is the main build artifact. It contains code, data, symbols, and debug information. Debuggers usually prefer ELF files because they include more information than a raw binary.
For flashing and distribution, it is also useful to generate .bin and .hex files.
Update the post-build command:
add_custom_command(TARGET firmware POST_BUILD
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:firmware>
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:firmware> ${CMAKE_BINARY_DIR}/firmware.bin
COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:firmware> ${CMAKE_BINARY_DIR}/firmware.hex
)
This does three things after a successful link:
- Prints a size summary with
arm-none-eabi-size. - Converts the ELF file to a raw binary file.
- Converts the ELF file to an Intel HEX file.
The raw binary contains the firmware bytes without symbol or address metadata. The HEX file includes address records and is often useful with flashing tools.
Build From a Clean Directory
Configure from a clean build directory:
rm -rf build
cmake -S . -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake
Then build:
cmake --build build
The build should compile the startup file and main.c, link the firmware image, print size information, and generate the extra output formats.
What Files the Build Produces
After a successful build, expect files similar to these in the build directory:
build/
├── firmware.elf
├── firmware.bin
├── firmware.hex
└── firmware.map
The exact build directory will contain more CMake and Ninja files. These four firmware-related files are the ones to care about for now.
firmware.elf is the linked firmware image with symbols and debug information.
firmware.bin is a raw binary image.
firmware.hex is an Intel HEX image.
firmware.map is a linker map showing how sections and symbols were placed in memory.
Flash with OpenOCD
Connect the STM32 Nucleo L433RC-P over USB.
Then flash the ELF file with OpenOCD:
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg -c "program build/firmware.elf verify reset exit"
This command uses the onboard ST-LINK interface and the STM32L4 target configuration.
The command sequence means:
program build/firmware.elfwrites the firmware image.verifyreads back memory and checks the programmed contents.resetresets the target after flashing.exitcloses OpenOCD after the operation.
OpenOCD configuration names can vary by installation and version, but interface/stlink.cfg and target/stm32l4x.cfg are the expected starting point for this board family.
A successful flash usually ends with output like this:
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
shutdown command invoked
The important lines are Programming Finished and Verified OK. They mean OpenOCD connected to the target, wrote the firmware image, read it back, and confirmed that the programmed flash matches the file.
shutdown command invoked is normal when the command includes exit. It means OpenOCD is closing after the flash operation.
You may also see a warning like this:
Warn : Adding extra erase range, 0x080000a0 .. 0x080007ff
That warning is usually not a failure. Flash memory is erased in aligned pages or ranges, so OpenOCD may erase a larger range than the exact firmware byte count.
Reset and Run
The flash command above includes reset, so the target should reset after programming.
The current application only increments a volatile counter forever. There is no LED toggle, serial output, or visible peripheral behavior yet. That is intentional. This article is validating the build and flash path, not demonstrating hardware behavior.
If the flash command succeeds, you have proven that:
- CMake can build the firmware.
- The linker can produce an STM32 image.
- OpenOCD can connect through ST-LINK.
- The image can be written and verified on the target.
That is the foundation needed before adding debugger integration or visible peripheral code.
Verify the Board Connection
If you want to test only the OpenOCD connection before programming, run:
openocd -f interface/stlink.cfg -f target/stm32l4x.cfg
If OpenOCD connects, it will keep running and wait for debugger connections. Stop it with Ctrl+C.
If this connection-only command fails, flashing will fail too. Fix the USB, ST-LINK, or OpenOCD configuration before investigating the firmware image.
Common Mistakes
If OpenOCD cannot find interface/stlink.cfg, check that OpenOCD was installed correctly and that its script directory is available. Homebrew's OpenOCD package normally handles this automatically.
If OpenOCD cannot connect to ST-LINK, check the USB cable. Many USB cables are power-only and do not carry data.
If another program is already connected to the board, OpenOCD may fail to claim ST-LINK. Close other debug or flashing tools and try again.
If flashing fails with a target or device error, confirm the board is powered and that the selected target configuration matches the STM32L4 family.
If OpenOCD prints Verified OK and then shutdown command invoked, the flash operation succeeded. Look for runtime visibility with a debugger or peripheral output instead of treating that as a flashing error.
If firmware.elf does not exist, the issue is in the build, not flashing. Run cmake --build build again and fix compile or link errors first.
If the firmware flashes successfully but there is no visible behavior, that is expected for the placeholder loop. Visibility comes later when the project toggles GPIO, logs over UART, or runs under a debugger.
What This Still Does Not Prove
A successful flash does not prove the application logic is correct.
It proves that the firmware image can be built, loaded, and verified. The current program has no observable output. It may be running correctly, but without a debugger or peripheral signal, you cannot see much from the outside.
That is why the next article adds VSCode debugger support. With a debugger attached, you can halt the core, inspect the program counter, view memory, check the vector table path, and watch the placeholder counter change.
Next Steps
The next article runs STM32 firmware under a VSCode debugger.
It will use the same CMake build output and ST-LINK connection, but instead of just flashing and exiting, it will start a debug session that can stop at Reset_Handler, step into main(), and inspect target state.