How can I make programming an ARM chip as hard as possible?
This article is part of a series.
- Part 1: Hello LED on an AVR (ATtiny45) in C
- Part 2: This Article
- Part 3: How can I get this SAMD21E18 startup code a little sturdier?
- Part 4: It's ALIVE! (SAMD21E18A, Assembly, No SDK)
- Part 5: And now for 3 ways to set an internal pullup
- Part 6: It'd make sense to do some toolchain clean up
- Part 7: Neat, switching to the ItsyBitsy just... works
Related Repo: https://github.com/carlynorama/StrippedDownChipRosetta
Historically chip companies have designed and manufactured their own chips. Switching to a different manufacturer meant changing everything, software to board.
Arm changed that by licensing their designs and instruction sets to more than one manufacturer.
- 44 min video overview of the architecture and ecosystem put out by Arm
- 13 min Interview with Prof. Furber by Computerphile
With so many to choose from, I’m going to go with the Arm chip that reminds me the most of the ATtiny - Microchip’s (Atmel) SAMD21 family. (Also - We have A LOT of Trinket M0’s in the house)
- Product Reference Page (has application notes)
- Full Data Sheet
- Especially Section 10: Memories pg 41
- Summary Data Sheet
- Especially Figure 9-1. SAM D21 Product Mapping
- About Core M0 chips (ARMv6-M)
- Peripheral Comparison Chart for Microchip 32-Bit chips
What’s the First Step?
Embedded Swift isn’t an Arduino replacement. It’s gunning to be pretty low level. Documenting how to load an assembly program on to the chip with the GNU tool chain provides a solid reference base for then showing what it takes to switch over to the C from the assembly. Adding Embedded Swift to the mix will hopefully feel like just another think to add to a Makefile…errr… Cmake…file.
I don’t love it, but “bare metal” has become a trendy term in programming these days. Like calling a product “green”, there’s no precise definition as to what it means. While environments without an operating system may have more of claim than dockerless servers, even within that context people differ in how they use it. In general it means the author will be trading expedience for control, so it can be a useful search term. That said, when known, use:
- actual part numbers
- software package names
- chip family names
- language names
- code snippets
Using these I still got results with “bare metal” in the title! Ha! My top instructional resources that contributed to this page and will help it make more sense:
- This post essentially implements the following tutorial using a different chip (SAMD21E18 vs STM32-F031K6) and a different programming toolchain (OpenOCD vs STLink).
- STM32 Based Fastbit Embedded Brain Academy “Bare metal embedded” playlist. No-library C based. Incredibly thorough review of how the software interacts with the hardware. A++. (Windows, but since GNU command line tools doesn’t matter.)
- I stole his programmer, he told me about OpenOCD, recently updated: https://github.com/todbot/samd21-programming-notes
- The product I’m using, but the Metro M4 would be a better choice if shopping. Shows the CircuitPython path: https://learn.adafruit.com/adafruit-pyruler/downloads
- Has a a good chip overview and overall a much more reasonable Arduino based start up process with a better board than my example: https://learn.sparkfun.com/tutorials/samd21-minidev-breakout-hookup-guide/all#samd21-overview
- Epic linker script with a ton of further reading, other SAMD21 information on the blog
- Using OpenOCD: https://omzlo.com/articles/programming-the-samd21-using-atmel-ice-with-openocd-(updated)
- Introduction to Assembly Programming with Arm - Nice overview but NOT .thumb mode needed for Coretex M0+. Uses an emulator for the first part but switches to a VM for the last part. Handy.
TODO: I’ve got a giant notes page with even more links that needs to be organized.
Which Programmer?
I love the UF2 (USB Flashing Format) programming method which allows many boards to be programmed via USB, no programmer needed.
- 2017 makecode blog post
- USB Flashing Format (UF2) explanation from microsoft.github.io
This post will not use that feature.
I would like the first couple of examples to be as close as possible to the AVR process which means programming via a programmer, not USB.
The role of AVRDude will be played by OpenOCD software. OCD stands for “On Chip Debugger” so it’s not exactly a 1 to 1 swap. Also install the gnu gcc arm toolchain because it has the version of GDB that we need.
To install on MacOS
## this is the cross compiler version
## installs from https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain
## see https://formulae.brew.sh/cask/gcc-arm-embedded#default
## the below command worked for me in 4/2024
## if it doesn't work for you try "brew install --cask gcc-arm-embedded"
brew install gcc-arm-embedded
arm-none-eabi-gcc --help
brew install openocd
## confirm install see options
openocd --help
## Optional
## if want to use openocd in telnet mode
brew install telnet
There are MANY programmers that will work with OpenOCD. Anything implementing JTAG (Joint Test Action Group), really.
Since the SAMD21D has so few pins, the Trinket exposes the SWD (Serial Wire Debug) interface instead of JTAG. The following programmers can work with it (~INT = ball park price in US dollars). The first -three- four I have seen in action personally.
- Segger JLink EDU edition (this post, ~150, pro is ~1k comparison guide)
- Atmel ICE (~200, CMSIS-DAP)
- ST-Link (V2 around ~23, V2-ISOL ~90)
- New! todbot just got a DapLink compatible programmer from Amazon and confirmed it worked. (specifically this one, requires soldering ~8) (affiliate links heads up. I made him.) (PLANS)
- Segger JLink EDU mini (~60)
- Raspberry pi debug probe (~12)
- Black Magic Probe (~75)
- SAMD11C SWD Programmer Stick PLANS ONLY! (repo)
- ESP32 Wifi Debugger PLANS ONLY (repo)
- MIT Machines That Make open source design PLANS ONLY
Prep the Board & Programmer
I’ve appropriated a PyRuler I’ve had on the shelf for awhile. It has a lot going on around the Trinket. They are super fun. If I was going to actually buy something for this task, I would buy something with a SWD or JTAG connection that was ready to go. When shopping you might see boards with a place for the tag-connect, but you’ll need a special cable for your debugger.
Trinkets expose the pads, but you have to solder to them or design a jig. (Looking forward to putting the UF2 bootloader back on…)
- https://www.segger.com/products/debug-probes/j-link/technology/interface-description/#jtag-interface-connection-20-pin
- https://learn.adafruit.com/adafruit-trinket-m0-circuitpython-arduino/pinouts
- https://learn.adafruit.com/how-to-program-samd-bootloaders/trinket-m0-wiring
Reset pin notes: The PyRuler does not overload the SWD pads (PA31, PA31) so setting up the reset pin may not be needed or useful for your set up.
Programmer | Trinket | Wire Color |
---|---|---|
1 VTref | 3V | brown |
7 SWDIO | pad further from usb edge | blue |
9 SWCLCK | pad closer to usb edge | yellow |
15 RESET | RESET | green |
19 5V-Supply* | BAT | red |
18 GND | GND | black |
- The 5V-Supply/red wire in theory could be used to power the board. But I haven’t sorted the configuration yet, so I’m using USB for power. It’s attached in the pictures so it isn’t flapping in the breeze.
The official pin out is from the point of view of looking down at a board’s connector, not AT the programmer’s connector. I’ve flipped this to make it look more like the picture.
Talk to the Board
Config File and Start Up
OpenOCD requires .cfg files for programmers, boards and chips. Since we have a single chip board we only need two config files, but we write one config file that can find them both for us.
- https://github.com/openocd-org/openocd/tree/v0.6.1/tcl/interface
- https://github.com/openocd-org/openocd/tree/v0.6.1/tcl/board
- https://github.com/openocd-org/openocd/tree/v0.6.1/tcl/target
The below is the minium viable config file. No commands. Nothing but the programmer and board info. Call it openocd.cfg
# Segger J-Link EDU, SWD mode
## adapter line same as
# #source [find interface/jlink.cfg]
adapter driver jlink
transport select swd
# Chip info (pyruler trinket)
set CHIPNAME at91samd21e18
source [find target/at91samdXX.cfg]
From the directory with this file running the bare openocd
command will start up the various local servers that openocd will manage for you (gdbserver, telnet and tcl).
- https://openocd.org/doc-release/doxygen/index.html
- https://openocd.org/doc/html/OpenOCD-Project-Setup.html
If one has more than one config file, you might want to call a specific one directly. Then openocd will want some direction about what you want to do with it.:
## init - necessary start up
## targets - lists all attached chips
## reset halt - puts chip in reset mode (i.e. in order to program, etc.)
openocd -f ./YOUR_FILE_NAME.cfg -c 'init; targets; reset halt;'
- more commands: https://openocd.org/doc/html/General-Commands.html
Those commands could be in the .cfg file instead, and then you don’t need the -c part of the above command.
# Segger J-Link EDU, SWD modere
## adapter line same as
# #source [find interface/jlink.cfg]
adapter driver jlink
transport select swd
# Chip info (pyruler trinket)
set CHIPNAME at91samd21e18
source [find target/at91samdXX.cfg]
init
targets
reset halt
The output with the first set up should look something like the below, but keep in mind my chip already has code on it. If you have trouble getting here, try putting your Trinket in reset mode by hand to start (double click the reset button).
Open On-Chip Debugger 0.12.0
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 Dec 13 2019 11:14:50
Info : Hardware version: 9.20
Info : VTarget = 3.325 V
Info : clock speed 400 kHz
Info : SWD DPIDR 0x0bc11477 # <-- common place to fail
Info : [at91samd21e18.cpu] Cortex-M0+ r0p1 processor detected
Info : [at91samd21e18.cpu] target has 4 breakpoints, 2 watchpoints
Info : starting gdb server for at91samd21e18.cpu on 3333
Info : Listening on port 3333 for gdb connections
Telnet Mode
Notice that OpenOCD is Listening on port 4444 for telnet connections
that’s for people. If one wants to write automation that’s what the tcl option on port 6666 is for. Those port numbers can be changed via the config file.
In a separate shell try:
telnet localhost 4444
# ... once connected
> reset
> targets
> reg
reset
gets the board out of reset mode. targets
was covered above and the reg command will print out all the values of the registers.
> targets
TargetName Type Endian TapName State
-- ------------------ ---------- ------ ------------------ ------------
0* at91samd21e18.cpu cortex_m little at91samd21e18.cpu running
> reg
## If no values, try again when in debug mode
===== arm v7m registers
(0) r0 (/32): 0x00003200
(1) r1 (/32): 0x00000000
(2) r2 (/32): 0x00000000
(3) r3 (/32): 0x0000131a
(4) r4 (/32): 0x00400040
(5) r5 (/32): 0xe000ed00
(6) r6 (/32): 0x00000005
(7) r7 (/32): 0x20002db0
(8) r8 (/32): 0x7afff4ef
(9) r9 (/32): 0xdde5fa1f
(10) r10 (/32): 0x27f9bfba
(11) r11 (/32): 0xefedfdff
(12) r12 (/32): 0xb9e9f4ff
(13) sp (/32): 0x20002dd8
(14) lr (/32): 0x00000527
(15) pc (/32): 0x00000288
(16) xPSR (/32): 0x21000000
(17) msp (/32): 0x20002dd8
(18) psp (/32): 0xf63efff0
(20) primask (/1): 0x00
(21) basepri (/8): 0x00
(22) faultmask (/1): 0x00
(23) control (/3): 0x00
===== Cortex-M DWT registers
Debugger Mode
In yet another shell window, start up gdb, but tell it to run from the server that’s already going.
## iex - Execute GDB command before loading the inferior.
arm-none-eabi-gdb -iex "target extended-remote localhost:3333"
This command will open gdb but push it straight over to the openocd host. Since in my case there’s no compiled file on the local machine I can give it to cross check (yet) many of the usual commands won’t work.
- https://sourceware.org/gdb/current/onlinedocs/gdb.html/Inferiors-Connections-and-Programs.html
- https://sourceware.org/gdb/current/onlinedocs/gdb.html/Connecting.html
In the mean time these should:
layout asm
to see the assemblyshow architecture
current target architecture.help target
To be honest, I haven’t used gdb before this past week. It wasn’t a thing I thought I needed. So wrong. So. So. Wrong. I love it. (Even Arduino IDE 2 has it now! I did not know!)
I’ve got a lot to learn - reading list:
- YouTube: Last video in assembly playlist
- YouTube: Chris Bourke UNebraska 9.2 - Debugging - GDB Tutorial
- https://sourceware.org/gdb/documentation/
- https://www.cs.cmu.edu/~gilpin/tutorial/
- https://jacobmossberg.se/posts/2017/01/17/use-gdb-on-arm-assembly-program.html
- https://azeria-labs.com/debugging-with-gdb-introduction/
- https://www.usna.edu/Users/cs/lmcdowel/courses/ic220/S22/labs/armIntro/assembly.html
- http://cs107e.github.io/guides/gdb/
- https://beej.us/guide/bggdb/ from tutorial below
core_hello.S and linker_hello.ld
The below code is pretty much taken from the excellent Hello Arm example, but with the linker script edited and commented for the SAMD21E18.
I’m not going to comment on them until the next post with a more complete example. I don’t recommend editing them piecemeal. They work because there’s almost nothing in them. Adding more with out adding ALL the needed more could end up being more trouble than just doing the full linker and chip specific start script. Next post!
If you’ve never seen assembly or a linker script before I’d say do the following in this order:
- watch the assembly playlist (keep in mind, it’s not quite the same version, see below. )
- watch the C based playlist because he covers a lot of using the compiler to learn
- Read the Vivonomicon tutorial
- read stargirl’s commented linker script.
If coming from desktop Arm, the Coretex-M0+ class of chip uses the 16-bit Thumb instruction set (video 7min | wikipedia) It’s a little different.
// These instructions define attributes of our chip and
// the assembly language we'll use:
.syntax unified
.cpu cortex-m0
.fpu softvfp //The Cortex-M0 line has no floating-point hardware
.thumb //name for the ARM Cortex-M instruction set
// Global memory locations.
//don't put anything above the fake vector table.
.global vector_table
.global on_reset
/*
* interupt vector table
* Only the RAM boundry and reset handler are
* included, for simplicity.
*/
.type vector_table, %object
vector_table:
.word _estack
.word on_reset
.size vector_table, .-vector_table
/*
* The Reset Handler.
*/
.type on_reset, %function
on_reset:
// Set the stack pointer to the end of the stack.
// The '_estack' value is defined in the linker script.
LDR r0, =_estack
MOV sp, r0
// Set some dummy values. When we see these values
// in our debugger, we'll know that our program
// is loaded on the chip and working.
LDR r7, =0xDEADBEEF
MOVS r0, #0
main_loop:
// Add 1 to register 'r0'.
ADDS r0, r0, #1
// Loop back.
B main_loop
.size on_reset, .-on_reset
And the linker
MEMORY {
/* Syntax: name(attr): ORIGIN = int, LENGTH = int */
/* attr is kindof like chmod */
/* get values from chip data sheet: SAMD21 section 10.2 */
/* only need to mentions the one you'll use */
/* also called CODE_MEMORY or ROM */
/* NOT LEAVING ROOM FOR BOOTLOADER */
flash(rx): ORIGIN = 0x00000000, LENGTH = 0x00040000 /*256K*/
/* also called just RAM */
sram(rwx): ORIGIN = 0x20000000, LENGTH = 0x00008000 /*32K*/
}
/* end of the stack See 9.1 in Summary Data sheet */
_estack = ORIGIN(sram) + LENGTH(sram); /* stack points to end of SRAM */
Compile, Upload, Debug
First we use the tools to compile core_hello.S to core.o to main.elf in a way that should feel very familiar from the [AVR post]/excuses/hello-led-on-an-avr-attiny45-in-c/.
- WARNING: Don’t forget the -g flag! (need it for gdb later)
arm-none-eabi-gcc -x assembler-with-cpp -c -O0 -mcpu=cortex-m0 -mthumb -Wall -g core_hello.S -o core.o
arm-none-eabi-gcc core.o -mcpu=cortex-m0 -mthumb -Wall --specs=nosys.specs -nostdlib -lgcc -T./linker_hello.ld -o main.elf
arm-none-eabi-nm main.elf
Once we have the elf we can use openocd to load it on the the chip. I put the commands in another cfg file.
- https://openocd.org/doc/html/Flash-Programming.html
- https://openocd.org/doc/html/Flash-Commands.html#program
- https://openocd.org/doc/html/Flash-Commands.html#flashprogrammingcommands
- https://openocd.org/doc/html/GDB-and-OpenOCD.html#programmingusinggdb
# Segger J-Link EDU, SWD mode
adapter driver jlink
transport select swd
# Chip info (pyruler trinket)
set CHIPNAME at91samd21e18
source [find target/at91samdXX.cfg]
init
reset halt
at91samd bootloader 0
program main.elf verify
openocd -f program_and_serve.cfg
The resulting output:
Open On-Chip Debugger 0.12.0
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
Info : J-Link V9 compiled Dec 13 2019 11:14:50
Info : Hardware version: 9.20
Info : VTarget = 3.327 V
Info : clock speed 400 kHz
Info : SWD DPIDR 0x0bc11477
Info : [at91samd21e18.cpu] Cortex-M0+ r0p1 processor detected
Info : [at91samd21e18.cpu] target has 4 breakpoints, 2 watchpoints
Info : starting gdb server for at91samd21e18.cpu on 3333
Info : Listening on port 3333 for gdb connections
[at91samd21e18.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x61000000 pc: 0x00000008 msp: 0x20008000
[at91samd21e18.cpu] halted due to debug-request, current mode: Thread
xPSR: 0x61000000 pc: 0x00000008 msp: 0x20008000
** Programming Started **
Info : SAMD MCU: SAMD21E18A (256KB Flash, 32KB RAM)
** Programming Finished **
** Verify Started **
** Verified OK **
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
In a new shell window, launch gdb with elf file specified and pushed into openocd host. OpenOCD and gdb work together to match up what’s on the chip with the expected symbol table. The example commands below will show the constant and the loop.
cd $YOUR_PROJECT_FOLDER
arm-none-eabi-gdb main.elf -iex "target extended-remote localhost:3333"
## Optional Layout Changes
(gdb) layout asm
(gdb) layout regs
## ^x o to switch between
## ^x a to close
(gdb) info registers
(gdb) info register r0
(gdb) info register r7
(gdb) info break
(gdb) break main_loop
(gdb) info break
(gdb) continue # ^c to escape
(gdb) info register r0
(gdb) info register r7
(gdb) continue # ^c to escape
(gdb) info register r0
(gdb) stepi
(gdb) info register r0
# etc...
Makefile
Based on the AVR Makefile and the commands ran above. Fewer options for now. Critical for this gdb focused example because missing the -g flag makes it kind of useless.
An important difference
MCU = attiny
-mmcu=$(MCU)
MACH = cortex-m0
-mcpu=$(MACH)
Different hardware specific version of GCC have different flag sets. Always check!
- https://gcc.gnu.org/onlinedocs/gcc/Submodel-Options.html
- https://gcc.gnu.org/onlinedocs/gcc/AVR-Options.html
- https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html
- -march vs -mtune- vs -mcpu for arm
###############################################################################
# Makefile for a simple SAMD21 Assembly project
###############################################################################
## General Flags
TARGET ?= hello#set if not defined
LINKER_FILE ?= $(TARGET).ld
MACH = cortex-m0
TOOL_BASE = arm-none-eabi-
CC = $(TOOL_BASE)gcc
## Options common to compile, link and assembly rules
COMMON = -mcpu=$(MACH)
COMMON += -mthumb
## Compile options common for all C compilation units.
CFLAGS = $(COMMON)
CFLAGS += -Wall
CFLAGS += -g
CFLAGS += -Os
## Linker flags
LDFLAGS = $(COMMON)
LDFLAGS += --specs=nosys.specs -nostdlib -lgcc
## Objects that must be built in order to link
OBJECTS = $(TARGET).o
## Objects explicitly added by the user
LINKONLYOBJECTS =
# Programming support using avrdude. Settings and variables.
PROGRAMMER = openocd
PROGRAMMER_FLAGS = -f program_and_serve.cfg
## Build
all: $(TARGET).elf
## Compile
%.o : %.S
$(CC) -x assembler-with-cpp -c $(CFLAGS) $<
##Link
%.elf: $(OBJECTS)
$(CC) $(LDFLAGS) $(OBJECTS) $(LINKONLYOBJECTS) -T./$(LINKER_FILE) -o $(TARGET).elf
# Program the device.
program: $(TARGET).elf
$(PROGRAMMER) $(PROGRAMMER_FLAGS)
## Clean target
#.PHONY: clean
clean:
-rm -rf $(OBJECTS) $(TARGET).elf $(TARGET).hex $(TARGET).lss $(TARGET).map
Summary
This assembly example works because there’s nothing in it. To do anything else it would be better to have a real start up script for the chip… a Makefile… A repo so much to improve! And that’s the next post!
Errata
I made a mistake when I first posted this (since corrected). So that big table of peripherals is for all SADM21*, not just for the SAMD21E18A [TODO LINK]. I had properly left out the ones that didn’t belong in my original original vector table code, but in a fit of insecurity I put them all back in. I’ve now taken them out again. The source of my insecurity was all this code on the internet! When looking at code on github, make sure both projects face the same tradeoffs. Notice the difference between the two files below. One has ALL of peripherals, the other does not. One is called startup_SAMD21.s the other is called startup_samd21e18a.s The first file supports the whole family at once. Presumably their default handlers can handle it. The second is the one that matches JUST this chip. There is a lot of SADM21.s and SADM21XXX.h code in the world. I was worried I was missing something… turns out I was. I was missing that it wasn’t solving the same problem.
This article is part of a series.
- Part 1: Hello LED on an AVR (ATtiny45) in C
- Part 2: This Article
- Part 3: How can I get this SAMD21E18 startup code a little sturdier?
- Part 4: It's ALIVE! (SAMD21E18A, Assembly, No SDK)
- Part 5: And now for 3 ways to set an internal pullup
- Part 6: It'd make sense to do some toolchain clean up
- Part 7: Neat, switching to the ItsyBitsy just... works