Series / STM32 Development with CMake and VSCode / Building and Flashing STM32 Firmware Without CubeIDE

Building and Flashing STM32 Firmware Without CubeIDE

Build a linked STM32 firmware image, generate binary and hex outputs, and flash the Nucleo board from command-line tools instead of STM32CubeIDE.

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.txt with an executable firmware target.
  • cmake/arm-none-eabi-gcc.cmake selecting the Arm GNU toolchain.
  • linker/STM32L433RCTx_FLASH.ld describing flash and RAM.
  • src/startup_stm32l433xx.c defining the vector table and Reset_Handler.
  • src/main.c providing 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.elf writes the firmware image.
  • verify reads back memory and checks the programmed contents.
  • reset resets the target after flashing.
  • exit closes 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.