Programming ARM registers

Like the AVR Version 4.1 of uLisp released earlier this month, the ARM version now adds a register function to allow you to read the values of 32-bit peripheral registers, or write values to the registers. It allows you to control the peripherals in the processor from a Lisp program, or interactively experiment with the peripherals by giving commands at the uLisp prompt.

Note that because peripherals are mapped to memory addresses on ARM processors you can also use the register command to read and write memory, although this is not recommended as you may interfere with running programs.

This article first appeared on the uLisp Forum.

Example

To read the register at address #x42004808 on the ATSAMD21 give the command:

> (register #x42004808)
0

To write a value to the same register give the value as the third parameter:

> (register #x42004808 65432)
65432

You can now confirm the contents of the register with:

> (register #x42004808)
65432

Note that for this example I've chosen the DAC DATA register which is a 16-bit register.

Examples

Examples for specific boards are given in the following sections:

ATSAMD21 and ATSAMD51 boards

uLisp provides predefined keywords for the most useful ATSAMD21 and ATSAMD51 port registers:

Register Keywords Description
DIR :pa-dir, :pb-dir Define port data direction bits
DIRCLR :pa-dirclr, :pb-dirclr Clear bits in data direction register
DIRSET :pa-dirset, :pb-dirset Set bits in data direction register
DIRTGL :pa-dirtgl, :pb-dirtgl Toggle bits in data direction register
OUT :pa-out, :pb-out Write to port outputs
OUTCLR :pa-outclr, :pb-outclr Clear bits in output port
OUTSET :pa-outset, :pb-outset Set bits in output port
OUTTGL :pa-outtgl, :pb-outtgl Toggle bits in output port
IN :pa-in, :pb-in Read port inputs

For any other registers you want to access you will need to specify their address, or define a variable to refer to them. To find the addresses for each register refer to the processor's datasheet.

For example, for the ATSAMD21 the register base addresses are defined in the Peripherals Configuration Summary table, on page 66 of the SAMD21 Family Data Sheet, and the specific registers provided for the PORT peripheral are defined in the PORT Register Summary on page 371 of the SAMD21 Family Data Sheet. For example, from this table we can see that the PORT base address for PA is #x41004400. One other thing we need to know is that the base address for successive ports PB, etc is got by adding #x80 to the PORT base address.

Blinking an LED

For the first example we will blink the built-in LED on the Arduino Zero or MKRZERO board by directly accessing the port it's connected to, without using the usual pinmode and digitalwrite functions.

On the MKRZERO the LED is connected to the pin PB08, which corresponds to bit 8 in port B. We can define a variable for a bit pattern with bit 8 set:

(defvar led (ash 1 8))

First we need to define the I/O pin as an output by writing this bit pattern to the data-direction register DIRSET:

(register :pb-dirset led)

To light the LED we write the bit pattern to the register OUTSET:

(register :pb-outset led)

Blink for Arduino Zero and MKRZERO

Finally here's the whole blink routine to flash the built-in LED, using the register OUTTGL as a shortcut to toggling the appropriate bit in the port:

(defun blink ()
  (let ((led (ash 1 8)))
    (register :pb-dirset led)
    (loop
     (register :pb-outtgl led)
     (delay 1000))))

Blink for other ATSAMD21 and ATSAMD51 boards

On other M0 and M4 boards the built-in LED is on different pins on PORT A or PORT B, as shown in the following table:

Board Pin
Adafruit Gemma M0 PA23
Adafruit Feather M0 PA17
Adafruit ItsyBitsy M0 PA17
Adafruit Metro M0 PA17
Adafruit Feather M4 PA23
Adafruit ItsyBitsy M4 PA22
Adafruit Metro M4 PA16
Adafruit Grand Central M4 PB01
Adafruit QT-Py (M0) n/a
Seeduino XIAO (M0) PA17
Adafruit PyBadge (M4) PA23
Adafruit PyGamer (M4) PA23

The boards shown n/a don't have a built-in LED. Some of them have a Neopixel or DotStar serial RGB LED instead which won't work with this program.

To find the correct pin for boards not listed above, search for the board's schematic in Google, and then identify the pin connected to the built-in LED. Alternatively use the port scanner below.

Run the routine blinka or blinkb as appropriate, giving the pin number in the port as a parameter:

(defun blinka (p)
  (let ((led (ash 1 p)))
    (register :pa-dirset led)
    (loop
     (register :pa-outtgl led)
     (delay 1000))))
(defun blinkb (p)
  (let ((led (ash 1 p)))
    (register :pb-dirset led)
    (loop
     (register :pb-outtgl led)
     (delay 1000))))

For example, on the ItsyBitsy M4:

(blinka 22)

To exit from the program enter a ~.

Port scanner for ATSAMD21 boards

The following program scans the digital input pins on port A of the ATSAMD21 every second, and displays their state as 0 or 1:

(defun scan ()
  (let ((wrconfig #x41004428))
    (register :pa-dir #x00000000) ; 0 = input
    (register :pa-out #xFFFFFFFF) ; 1 = pullup
    (register wrconfig #x4006FFFC) ; Set pullups and 
    (register wrconfig #xC00618FF) ; enable input buffers
    ; Scan ports
    (format t "...22...22.2111111111100000000..~%")
    (format t "...87...32.0987654321098765432..~%")
    (loop
     (format t "~32,'0b~%" (register :pa-in))
     (delay 1000))))

To configure the pullups and enable the input buffers it uses the PORT WRCONFIG register at #x41004428. This allows you to configure the port 16 bits at a time.

The scan program prints two heading lines, showing the first and second digit of the pin number corresponding to each column. The pins marked '.' are not available as I/O pins, because they are assigned to other functions such as the clock crystal, and they may have indeterminate values. All the other pins are defined as inputs with a pullup, and so will initially read as a '1'. The subsequent lines show the states of the pins every second.

By connecting a pin to GND you can identify which PA pin it corresponds to. For example, connecting the pin labelled D12 to GND on the Adafruit Feather M0 board after two seconds gives the following display:

> (scan)
...22...22.2111111111100000000..
...87...32.0987654321098765432..
01011010110111111111111111111100
01011010110111111111111111111100
01011010110101111111111111111100
01011010110101111111111111111100
01011010110101111111111111111100

The printout shows that the pin in the column 19 has changed from 1 to 0, showing that the D12 pin on the board is connected to the I/O line PA19.

The program could be extended to test the pins on port PB too, or to work with other processors. 

nRF51822 boards

The nRF51822 used in the BBC Micro:bit v1 has one port of 32 I/O lines. The port is referred to as P0, rather than PA as on the ATSAMD processors.

uLisp provides predefined keywords for the most useful registers on this port:

Register Keywords Description
OUT :p0-out Write to port outputs
OUTSET :p0-outset Set bits in output port
OUTCLR :p0-outclr Clear bits in output port
IN :p0-in Read port inputs
DIR :p0-dir Define port data direction bits
DIRSET :p0-dirset Set bits in data direction register
DIRCLR :p0-dirclr Clear bits in data direction register

There are no toggle options, but you can achieve the same effect by a read-modify-write as in the blink example below.

For any other registers you want to access you will need to specify their address, or define a variable to refer to them. To find the addresses for each register refer to the processor's datasheet.

Blink for the BBC Micro:bit v1

The BBC Micro:bit doesn't have an LED on a single I/O line, but a matrix of 25 LEDs are connected with their cathodes to nine columns and their anodes to three rows, so to light one LED it's necessary to take its column line low and its row line high. Here's a routine to flash the top left LED, which is on row 1, P0.13, and column 1, P0.04:

(defun blink ()
  (let ((row (ash 1 13))
        (col (ash 1 4)))
    (register :p0-dirset row)
    (register :p0-dirset col)
    (register :p0-outclr col)
    (loop
     (register :p0-out (logxor (register :p0-out) row))
     (delay 1000))))

nRF52833 and nRF52840 boards

The nRF52833 used in the BBC Micro:bit v2, and the nRF52840 used in the Adafruit ItsyBitsy nRF52840, each provide two I/O ports, P0 and P1. uLisp provides predefined keywords for the most useful port registers:

Register Keywords Description
OUT :p0-out:p1-out Write to port outputs
OUTSET :p0-outset:p1-outset Set bits in output port
OUTCLR :p0-outclr:p1-outclr Clear bits in output port
DIRTGL :p0-in:p1-in Read port inputs
OUT :p0-dir:p1-dir Define port data direction bits
OUTCLR :p0-dirset:p1-dirset Set bits in data direction register
OUTSET :p0-dirclr:p1-dirclr Clear bits in data direction register

There are no toggle options, but you can achieve the same effect by a read-modify-write as in the blink examples below.

For any other registers you want to access you will need to specify their address, or define a variable to refer to them. To find the addresses for each register refer to the processor's datasheet.

Blink for Adafruit ItsyBitsy nRF52840

On the Adafruit ItsyBitsy nRF52840 there's a built-in LED on P0.06. Here's a routine to flash this LED:

(defun blink ()
  (let ((led (ash 1 6)))
    (register :p0-dirset led)
    (loop
     (register :p0-out (logxor (register :p0-out) led))
     (delay 1000))))

Blink for the BBC Micro:bit v2

The BBC Micro:bit v2 doesn't have an LED on a single I/O line, but a matrix of 25 LEDs are connected with their cathodes to five columns and their anodes to five rows, so to light one LED it's necessary to take its column line low and its row line high. Here's a routine to flash the centre LED, which is on row 3, P0.15, and column 3, P0.31:

(defun blink ()
  (let ((row (ash 1 15))
        (col (ash 1 31)))
    (register :p0-dirset row)
    (register :p0-dirset col)
    (register :p0-outclr col)
    (loop
     (register :p0-out (logxor (register :p0-out) row))
     (delay 1000))))

RP2040 boards

The RP2040 used in the Raspberry Pi Pico and other RP2040 boards provides 29 general purpose digital I/O pins. uLisp provides predefined keywords for the most useful port registers:

Register Keywords Description
GPIO_IN :gpio-in Read port inputs
GPIO_OUT :gpio-out Write to port outputs
GPIO_OUT_SET :gpio-out-set Set bits in output port
GPIO_OUT_CLR :gpio-out-clr Clear bits in output port
GPIO_OUT_XOR :gpio-out-xor Toggle bits in output port
GPIO_OE :gpio-oe Define port data direction bits
GPIO_OE_SET :gpio-oe-set Set bits in data direction register
GPIO_OE_CLR :gpio-oe-clr Clear bits in data direction register
GPIO_OE_XOR :gpio-oe-xor Toggle bits in data direction register

You can read the input from any pin using :gpio-in. However, before using any pin for output you first need to define its function as SIO (software I/O) by setting the appropriate GPIOx_CTRL register to 5.

The address of each GPIOx_CTRL register is:

IO_BANK_BASE + pin * 8 + 4 where IO_BANK_BASE is #x40014000.

Here's the blink function:

(defun blink (pin)
  (let ((io-bank0-base #x40014000)
        (led (ash 1 pin))
        (sio 5))
    (register (+ io-bank0-base (* pin 8) 4) sio)
    (register :gpio-oe-set led)
    (loop
     (register :gpio-out-xor led)
     (delay 1000))))

For example, to blink the green LED on pin 25 of the Raspberry Pi Pico:

(blink 25)

and on the Adafruit Feather RP2040 do:

(blink 13)