STATUS: WORK IN PROGRESS. NOTHING TO SEE HERE.
A minimalist guide and skeleton for building, running, and debugging firmware for ARM-based microcontrollers.
You will use only the following software tools:
-
Make, for orchestrating build tasks
-
GNU GCC and LD, for compiling ARM binaries
-
OpenOCD, for flashing firmware and interactive debugging.
-
GDB, for interactive debugging via OpenOCD.
-
Your favorite text editor, for editing source files.
-
Your favorite terminal emulator, for doing CLI tasks.
After following this guide, you will be able to:
-
Demonstrate a minimal Makefile-based solution for a very simple LED-blinking firmware.
-
Understand the basic concepts of MCU development.
-
Perform build/test/debug cylces entirely from the CLI on Linux (and eventually Mac)
-
Adapt the Makefile to a continuous integration system such as Jenkins. In other words, it should be straightforward to automate building and publishing firmware binary release-candidates automatically following code review
-
Avoid copying vendor code with non-free licenses. Some vendors provide code examples or supporting source files with non-free licenses. Instead of copying example code from these vendors, we will reference datasheets and manuals.
-
The classic vendor-supported methods of developing firmware for MCUs is using a full-blown GUI-based IDE like Keil, IAR, or MCU Eclipse.
-
These tools are GUI-based and can be proprietary and/or expensive. They also have their own learning curves as they are advanced tools for "serious professionals".
-
GUI-based tools can obscure the underlying concepts which aren’t really that complicated!
-
Using vendor-provided SDKs and their accompanying non-free licenses could inadvertently lock you into a particular MCU vendor. There are many different ARM MCU vendors you can choose from, and you should be able to switch when you want.
This guide targets the following MCU vendor development kits:
-
ST
-
STM32F3DISCOVERY STM32F3 Discovery Board (STM32F303VCT6) (Cortex-M4)
-
STM32F407G-DISC1 STM32F4 Discovery Board (STM32F407VGT6) (Cortex-M4)
-
-
Nuvoton
-
NuTiny-NUC029SEE (NUC029SEE) (Cortex-M0)
-
-
NXP
-
OM13080UL: LPCXpresso1125 Board (LPC1125JBD48) (Cortex-M0)
-
OM13093UL: LPCXpresso board for LPC11C24 with CMSIS DAP probe (LPC11C24FBD48) (Cortex-M0)
-
The datasheet for your microcontroller is often the best source of information about how to program your MCU. Find your vendor’s datasheet on their website and put it someplace handy. You will be using the datasheet a lot!
Things we need from the Vendor Datasheets:
-
ROM (flash) size
-
ROM base address
-
RAM size
-
RAM base address
-
GPIO Bank and Pin for blinking LED
-
GPIO configuration and/or control registers
In order to enable the GPIOs as outputs and blink an LED, we have to find the addresses of the control registers from the vendor datasheets. We also need to know which GPIO bank (and/or pin) is mapped to an existing LED on the dev board (hopefully there is one).
-
Nuvoton
-
MCU: NUC029SEE
-
MCU Datasheet: Nuvoton NUC029xEE technical user manual (TRM_NUC029xEE_EN_Rev1.01.pdf)
-
Note: For this model, the 'S' in the model designates the physical package type. This datasheet is shared across a few different package types.
-
-
SDK Datasheet: NuMicro Family NuTiny-SDK-NUC029SEE User Manual (UM_NuTiny-SDK-NUC029SEE_EN_Rev1.00.pdf)
-
SVD File: NUC029EE_v1.svd (from Keil pack downloads, see link below)
-
ROM size: 128K
-
ROM base address: 0x0000_0000
-
RAM size: 16K
-
RAM base address: 0x2000_0000
-
LED GPIO: GPIO1 (from "SDK Circuit Schematic" in SDK User Manual). Acccording to "Target Chip Schematic", LED is hooked up to GPIO Bank B (PB.4), Pin 10
-
LED PIN Bit Offset: 4 (i.e. PB.4 is enabled using the fifth bit in the register, GPIOB_DOUT)
-
See "Section 6.2.4 System Memory Map", "6.2.7 Register Map", "6.6.5 Register Map", "SDK Circuit Schematic"
-
System Control Register: GPB_MFP, Offset: GCR_BA(0x5000_0000)+0x34 "GPIOA Multiple Function and Input Type Control Register"
-
0x5000_4000-0x5000_7FFF: "GPIO Control registers"
-
GPIO_BA (base address): 0x5000_4000
-
GPIOB_PMD (I/O mode control register): GPIO_BA+0x040
-
GPIOB_DOUT (output register): GPIO_BA+0x048
-
-
ST
-
ST STM32F407xx: look in "Memory mapping" (Section 4), "Table 10. register boundary addresses"
-
0x4002_0C00-0x4002_0FFF: GPIOD
-
TODO: exact register addresses of the GPIO bank we want to enable for use.
-
The following source files are used to build the binary you will load:
-
The device startup ARM assembly source (startup_ARMCMX.s)
-
The device memory parameters (heap and stack size) (mem_ARMCM0.h)
-
The device application C source (main.c)
We’ll start by copying the the ARM assembly startup file we need from the official ARM software github.
x@x1carbon:~/Code$ git clone git@github.com:ARM-software/CMSIS_5.git x@x1carbon:~/Code$ cd CMSIS_5/CMSIS x@x1carbon:~/Code/CMSIS_5/CMSIS$ find | grep "startup" | grep CM0 | grep GCC ./DSP/Platforms/FVP/ARMCM0/Startup/GCC/startup_ARMCM0.S ./DSP/Platforms/IPSS/ARMCM0/Startup/GCC/startup_ARMCM0.S x@x1carbon:~/Code/CMSIS_5/CMSIS$ cp ./DSP/Platforms/FVP/ARMCM0/Startup/GCC/startup_ARMCM0.S ~/Code/arm-blink/
What is going on in this file? First go read the first five paragraphs from the fine manual for binutils as (the GNU binutils assembler). The manual explains what a binary section is and will help you understand how the linker works, so don’t skip it!
The first two lines of startup_ARMCM0.S set the ASM syntax and architecture (the Cortex-M0 is an armv6-m).
-
The next lines of the setup assembly define a binary section called ".vectors" with a two byte alignment. This section is commonly called the interrupt vector table.
-
The interrupt vector table is essentially an array of function pointers.
-
In the assembly code, three global symbols are declared. These symbols help the linker find the vector table when it assembles the final binary that you will flash to your device.
-
Next, the vector table itself is defined. The ARM Cortex-M0 vector table layout is specified by ARM.
-
When an interrupt occurs the CPU jumps to the 32-bit address for the specific interrupt that occurred and continues executing code there.
-
The interrupt we care about right now is the Reset interrupt that occurs when the CPU powers on.
-
The first 32-bits in the vector table are special: they hold the initial stack pointer, the address of the top of the stack. (note: stacks grow down on ARM and nearly all other modern processors).
-
The next 32-bits point at the address of the reset handler code (which will be defined later in the assembly file).
-
A bunch more default handlers are created for different interrupts that we reload-firefox
-
Next, the "BL" instruction tells the CPU to branch to the SystemInit function, when the SystemInit function returns it will continue on the next instruction. This function will be written in C code later and linked using the link script.
-
After we return from SystemInit, we pick up by loading two addresses using two ARM-specific "synthetic" opcodes. These lines reference two symbols,
_copy_table_start_
and_copy_table_end_
. These symbols are used by ResetHandler to copy the data sections of the binary from ROM to RAM. We’ll see them being defined later in the linker script. -
A similar process happens with
_zero_table_start_
and_zero_table_end_
in order to zero out the BSS section.
The main purpose of the linker script is to describe how the sections in the input files should be mapped into the output file, and to control the memory layout of the output file.
The linker script instructs GNU ld to create a binary that includes the vector table section, compiled assembly instructions section, and compiled C program sections. All these sections need to go into the exact right locations in the binary. This way the CPU can find the vector table where it expects it to be, the vector table’s second entry points at the compiled ResetHandler code, and so on.
We will just the copy linker script from the ARM CMSIS v5 distribution since it has an open license:
x@x1carbon:~/Code/arm-blink$ cp /home/x/Code/CMSIS_5/CMSIS/DSP/Platforms/IPSS/ARMCM0/LinkScripts/GCC/lnk.ld .
The linker script references a file called mem_ARMCM0.h which defines the stack size as 12 KB and a heap size of 1024 KB (1 MB).
x@x1carbon:~/Code/arm-blink$ cp /home/x/Code/CMSIS_5/CMSIS/DSP/Platforms/IPSS/ARMCM0/LinkScripts/GCC/mem_ARMCM0.h .
Next, edit the linker script to make the size value parameters match your MCU from the information you collected above. Make sure to convert decimal sizes to hex!
For example, for the NUC029SEE has 128KB ROM and 16KB RAM, so you would set the values as follows.
-
Set __ROM__SIZE to "0x000020000" (128Kb*1024 is 131072 bytes in decimal, 0x20000 in hex)
-
Set __RAM__SIZE to "0x000004000" (16Kb*1024 is 16384 bytes in decimal, 0x4000 in hex)
-
The __ROM_BASE and __RAM_BASE do not need to be changed since they are standard values.
The linker script defines the memory layout using the MEMORY
command. This command
allows later parts of the linker script to reference specific regions of memory.
Our memory consists of two regions: FLASH
and RAM
, which are defined according to
the size/base parameters you setup.
Next, the linker script declares that that ResetHandler function (defined in the startup assembly file) should be the main entry point for our final binary.
The script then uses the SECTIONS
keyword to define the binary sections:
-
The main
.text
section is defined in theFLASH
memory region. It includes the following susubsections:-
The .vectors section, which references the vector table defined in the startup assembly file.
-
All input program binary .text sections follow next. In our case the C code will be compiled into a binary object with a single text section.
-
The code for any initializers, finalizers, constructors, and destructors (i.e. for C libraries or C++ applications)
-
Read-only data (rodata)
-
A special section called the eh-frame is used by GCC to handle C++ exceptions and unwind the stack when debugging.
-
-
The
.ARM.extab
and.ARM.exidx
sections are also used for exceptions and stack unwinding, but are specifically part of the ARM standard. -
The
.copy.table
section is a special section that is used by the ARM startup assembly code. This section will contain the memory addresses of the program data section (i.e. the part that would contain global variables or structures). -
The
.zero.table
section is similar to the.copy.table
sections. The memory regions referenced by the symbols in this section will be zeroed out in the ARM startup assembly. -
The
.data
section is the first binary section in theMEMORY
region. It includes the following subsections:-
the
data_start
symbol marks the start of this section; it is referenced earlier in the linker script and in startup assembly fiile. -
the vtable (a virtual method table) used by C++ programs. **
-
Next, the linker includes the main data section and all other data sections from any input binary files.
-
Data values for preinit, init, finit come next. These are also used for C++ programs.
-
-
TODO: Walkthrough install of ARM GCC toolchain
export PATH=$PATH:/home/x/Toolchains/gcc-arm-none-eabi-9-2019-q4-major/bin
-
TODO: Walkthrough build/install OpenOCD
-
Cortex-M0 Boot nice article describing how the Cortex-M0 boots
-
Bare Metal ARM Programming a great guide with all the basics you need to know.
-
Blink, the HelloWorld of Hardware another fantastic but more in-depth guide.
-
Cross compiling from Linux for the Cortex-M basics of using GCC (targeting the STM32F407)
-
Setting up STM32F4 Clocks detailed explanation of setting up accurate clocks (i.e. for USB)
-
Embed With Elliot: ARM Makefile Madness, targeting the STM32F407 MCU, specifically the STM32F407G-DISC1 development board, and using CMSIS standard ARM libraries.
-
GNU ARM Embedded Toolchain for Centos 7.3 Using the ARM GCC toolchain on CentOS 7.x
-
OpenOCD CLI tool used for firmware loading and to enable interactive debugging using GDB.
-
OpenOCD for Nuvoton MCUs Customized (forked) OpenOCD for Nuvoton devices
-
HIDAPI Library Cross-platform library for programming USB devices (used by OpenOCD)
-
Vendor MDK5 Software Packs Vendor software packs for Keil MDK. These .pack files are just zip files with interesting stuff inside, even if you aren’t using Keil MDK. In particular we are interested in the SVD XML files which describe the hardware in a standardized machine-readable format.
-
MCU Clocks and Introduction to Interrupts article about the basics of clocks on MCUs.
-
A Deep Dive into ARM Cortex-M Debug Interfaces in-depth guide to how Cortex-M debugging works
-
Nuvoton Board Support Packages Links to various downloads (including github links) for many different Nuvoton MCUs, including the NUC029.
-
GNU LD Command language Understanding GNU LD linker scripts.
-
From Zero to main(): Demystifying Firmware Linker Scripts How linker scripts work, illustrated by a simple blinky hello world.
The following series of videos can help you understand how the Cortex-M processors work.
-
Learn the Fundamentals of ARM® Cortex®-M0 Processor How a basic Cortex-M0 processor works
-
How to Choose your ARM Cortex-M Processor Learn the difference between various Cortex-M processors.
-
Efficient Software Development with ARM CMSIS v4 Overview of the Cortex-M Microcontroller Software Interface Standard (CMSIS), a set of vendor-agnostic and RTOS-agnostic APIs which are implemented by various MCU vendors.