Learning to program microcontroller units (MCUs) using makefiles and an ARM toolchain provides a powerful and flexible workflow for embedded development. Makefiles allow you to automate the build process while the ARM toolchain gives you access to the robust ARM Cortex architecture. With some setup and practice, you’ll be able to compile, flash, and debug your programs quickly and efficiently.
Understanding the Components
Before diving into the details, let’s review the key components that make up this embedded development environment:
- MCU – The microcontroller unit itself. This is the chip with the ARM processor that runs your code. Examples include STM32, NXP Kinetis, Atmel SAM, etc.
- Toolchain – The compiler, assembler, linker and other tools that convert your C/C++ code into an executable that can run on the target MCU.
- Makefile – A file that defines rules for building your executable from source code using the toolchain.
- Flashing – The process of getting your executable program onto the MCU’s flash memory.
- Debugging – Tools and techniques for running your program in a debug environment and finding bugs.
Choosing an MCU
The first step is choosing an MCU to work with. Some popular options include:
- STM32 – Cortex-M3/M4 chips made by STMicroelectronics. Very wide selection of affordable development boards.
- NXP Kinetis – Cortex-M4F chips made by NXP. Also with lots of dev board options.
- Atmel SAM – Cortex-M0+ chips made by Atmel/Microchip. Used in Arduino Zero/MKR boards.
Look at development boards from Adafruit, SparkFun, STM, and other vendors to find one that fits your needs and budget. The STM32 Discovery and Nucleo boards are a good starting point. Pick an MCU that supports ARM’s CMSIS libraries for simpler software development.
Installing the ARM Toolchain
You’ll need a toolchain compatible with your chosen MCU architecture. For Cortex-M0/M3/M4 parts, the GNU Arm Embedded Toolchain from ARM is a good option. You can download pre-built binaries for Windows, Mac and Linux. Make sure to get the version that matches the instruction set for your MCU (ARMv6-M, ARMv7-M, etc).
Add the bin directory for the toolchain to your system PATH so you can easily invoke the compiler, assembler and other tools from the command line. On Linux/Mac: export PATH=$PATH:/opt/gnu-arm-embedded/bin
The key tools you’ll use are:
- arm-none-eabi-gcc – The compiler that converts C/C++ code to assembly
- arm-none-eabi-as – The assembler that converts assembly to machine code
- arm-none-eabi-ld – The linker that combines object files into an executable
- arm-none-eabi-gdb – The GNU debugger for testing on the device or simulator
Configuring a Makefile
A makefile contains rules for building your executable from source files. This is more convenient than invoking the tools manually each time. Makefiles support variables, targets, automatic dependency checking, and handy features for embedded development like flashing and debugging targets.
Start with a simple makefile like this: # Toolchain path TOOLCHAIN=/opt/gnu-arm-embedded # Build target TARGET=program # Source files SRC=main.c utils.c # Compiler flags CFLAGS=-mcpu=cortex-m3 -mthumb -O1 -g3 -Wall -std=gnu99 # Linker flags LDFLAGS=-T../ld/stm32f1.ld -Wl,-Map=program.map # Rule to build executable $(TARGET).elf: $(SRC) arm-none-eabi-gcc $(CFLAGS) $(LDFLAGS) -o $@ $(SRC) # Rules for converting to hex/bin $(TARGET).hex: $(TARGET).elf arm-none-eabi-objcopy -O ihex $(TARGET).elf $(TARGET).hex $(TARGET).bin: $(TARGET).elf arm-none-eabi-objcopy -O binary $(TARGET).elf $(TARGET).bin
This makefile builds an executable called program.elf from main.c and utils.c, converting it to Intel hex and binary formats for flashing. Customize the flags for your MCU architecture, source files, and desired output filenames.
Flashing the Executable
To get your program running on the MCU, you’ll need to flash the executable. This writes the binary file to the chip’s memory. Flashing requires an external debugger/programmer hardware interface. Some options are:
- On-board debug circuitry like ST-LINK found on Discovery/Nucleo boards
- Dedicated JTAG/SWD adapters that connect to debug ports
- USB-to-serial adapters paired with the UART bootloader on the chip
For example, to flash using OpenOCD and an ST-LINK debugger: openocd -f board/st_nucleo_f3.cfg telnet localhost 4444 program program.elf verify reset
This connects OpenOCD to the debugger, uploads the program, resets the board, and starts execution. The makefile can automate this process so flashing is a single command.
Debugging with GDB
To test and debug code on the device, the GNU debugger (GDB) is quite useful. It allows stepping through code, setting breakpoints, watching variables, etc. To use the Arm GDB that comes with the toolchain: arm-none-eabi-gdb program.elf target remote localhost:3333 load …use step, next, break, watch, etc
This connects to a GDB server running on port 3333 (started by OpenOCD) and loads the program. Add commands like break main and run to control execution. Making effective use of GDB takes some practice but unlocks powerful debugging abilities.
Integrating Library Code
Most projects will utilize some external library code. This may include the MCU vendor’s HAL drivers, CMSIS libraries for ARM Cortex, middleware stacks like FreeRTOS, or other open source libraries. To use these in your project:
- Add include paths to the libraries in CFLAGS
- Link any necessary pre-built library files into the executable using LDFLAGS
- Call the appropriate initialization code early on
- Use clean and consistent style when invoking library APIs
Most libraries come with examples and documentation to help get started. Take time to understand the architecture and concepts used by the library code to make the most effective use of it.
Tips for Effective Embedded Programming
Here are some key tips to keep in mind when writing code for MCUs:
- Use a modular, layered architecture for better code organization
- Minimize dynamic memory allocation due to limited heap space
- Implement power management to reduce energy consumption
- Handle peripherals at a low level for best performance
- Write reentrant and thread-safe code when using an RTOS
- Enable optimizations in the compiler and verify they work correctly
- Handle errors and timeouts robustly to avoid crashing
Following best practices from the outset will prevent many issues and lead to more stable and reusable firmware code.
Expanding Your Skills
To become an expert embedded programmer using ARM MCUs, keep learning and expanding your skills in areas like:
- Peripheral interfacing – I2C, SPI, USB, CAN, etc.
- Real-time operating systems and multithreading
- Low power techniques and wireless communication
- Toolchain and compiler internals and usage
- Bootloaders, firmware upgrades, and robust field updates
- Testing and validation methodologies
There are great resources available through ARM, open source projects, MCU vendor communities, and books/training material. Immerse yourself and keep practicing – experience is the best teacher!
With the core skills of makefiles, the ARM toolchain, and solid embedded programming principles, you’ll be ready to take on any Cortex-M based project.