Hardware register access

Copyright © 1993–2025 J. M. Spivey
Jump to navigation Jump to search

The current state of this file represents a complete change of heart from the proposal made earlier.

The idea is to refer to hardware registers in the style TIMER0.MODE, making the timer look like a C structure with the registers as members. Code that manages multiple timers can use the notation TIMER[i]->MODE. This is achieved in the following way:

  • hardware.h declares a structure with tag _timer that has members named MODE, etc., at the appropriate offsets.
    • Structure members at a fixed offset can be described either with calculated padding between each member and the next (with the calculation done by a preprocessing step), or by using the trick of anonymous structures nested inside anonymous unions.
  • hardware.h also declares instances TIMER0, TIMER1, etc., with external linkage, and an array of pointers TIMER.
  • In some convenient place, say startup.c, there's a definition of the TIMER array as a constant table containing &TIMER0, &TIMER1, etc.
  • The linker script provides symbols TIMER0, TIMER1, etc., defining each with the appropriate absolute address.

This scheme avoids the use of macros and also avoids the trick of casting a hex constant to a pointer type. Because the linker script contains one address per device, the list of commands in the script stays reasonable in length.

The provision of pointer arrays should be done selectively, including all devices that may have more than one instance on some devices. I2C seems like a case in point, with one bus on V1 and two on V2 – though actually even the V1 chip has two combined I2C/SPI devices. In any case, it's wise to have an array of device addresses on both chips, so the same driver software can be compiled for either chip. The UART is a special case, with a single legacy UART (which we call UART) on both chips, and two extended UARTS, called UARTE0 and UARTE1, on V2 – with UARTE0 sharing hardware with the legacy UART. An array UARTE[] is provided on V2 but not on V1.

A minor disadvantage of this scheme is that the compiler, not knowing the absolute address of the timer, cannot reduce the reference TIMER0.MODE to a load from a single, fixed address, and instead needs a sequence such as

ldr r0, =TIMER0
ldr r1, =0x504
ldr r2, [r0, r1]

That costs at most one extra instruction per reference to a hardware register, and may be significant in timing loops, but perhaps nowhere else. Try writing

unsigned * const GPIO_OUT = &GPIO.OUT;

and then referring to *GPIO_OUT in the body of the loop: the address ought to be kept in a register, giving the bext possible code. It would be good to verify this with a loop that produces the fastest possible square wave by bit-banging.