How can I make programming an ARM chip as hard as possible?

This article is part of a series.

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.

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)

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:

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:

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.

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.

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…)

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 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.

JLink SWD mode pinout

Trinket Pinout Diagram

JLink connector

Closeup of my terrible soldering job

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.

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).

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;'

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.

In the mean time these should:

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:

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:

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/.

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.

# 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!

###############################################################################
# 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.