I'm adding a feature to minisvd2nim to support "dimensioned registers." That's SVD terminology for an array of identical registers differing only by offset from the base. My goal is to provide the programmer the ability to access the dim register like so:
let v0 = PERIPH.REG[0] # read static
let vn = PERIPH.REG[n] # read
PERIPH.REG[1] = v0 + vn # write
My first attempt is in the comment below. TIA,
!!Dean
#### metagenerator.nim
import std/[volatile]
type RegisterVal = uint32
template declarePeripheral*(
peripheralName: untyped, baseAddress: static uint32, peripheralDesc: static string
): untyped =
type `peripheralName Base` {.inject.} = distinct RegisterVal
const peripheralName* {.inject.} = `peripheralName Base`(baseAddress)
template declareDimRegister*(
peripheralName: untyped,
registerName: untyped,
addressOffset: static uint32,
dim: static uint32,
dimIncrement: static uint32,
readAccess: static bool,
writeAccess: static bool,
registerDesc: static string
): untyped =
type `peripheralName _ registerName Val`* {.inject.} = distinct RegisterVal
type `peripheralName _ registerName DimPtr` {.inject.} =
ptr `peripheralName _ registerName Val`
type `peripheralName _ registerName Ptr` {.inject.} =
ptr `peripheralName _ registerName Val`
const `peripheralName _ registerName` {.inject.} =
cast[`peripheralName _ registerName DimPtr`](`peripheralName`.uint32 + addressOffset)
when readAccess:
proc `registerName`*(base: static `peripheralName Base`): `peripheralName _ registerName DimPtr` {.inline.} =
`peripheralName _ registerName`
proc `[]`*(dimRegPtr: static `peripheralName _ registerName DimPtr`, index: static int): `peripheralName _ registerName Val` {.inline.} =
const regPtr = cast[`peripheralName _ registerName Ptr`](dimRegPtr.uint32 + index * dimIncrement)
volatileLoad(regPtr)Here is the same code in the playground: https://play.nim-lang.org/#ix=2lK1
The error is
/usercode/in.nim(47, 18) Error: type mismatch
Expression: ICER(3758153984'u)[0]
[1] ICER(3758153984'u): NVIC_ICERDimPtr
[2] 0: int literal(0)
Expected one of (first mismatch at [position]):
[0] proc `[]`[I: Ordinal; T](a: T; i: I): T
[1] proc `[]`(dimRegPtr`gensym1: static `NVIC _ ICER DimPtr`;
index`gensym1: static int): NVIC_ICERVal
... My experience has been hit and miss with LLM coding assistance. Mostly miss when dealing with my name-mangled Nim metaprogramming. But today was my lucky day:
- proc `[]`*(_: static `peripheralName _ registerName DimPtr`, index: static int): `peripheralName _ registerName Val` {.inline.} =
+ proc `[]`*(_: `peripheralName _ registerName DimPtr`, index: static int): `peripheralName _ registerName Val` {.inline.} =
The static keyword on the first argument was preventing the procedure resolution.
P.S. If you are here to learn Nim's templates/metaprogramming, please do not take this as a good example for typical templates.
What I am doing is putting necessary complexity into these templates (and hiding the templates in a file most of the people who use my package won't need to look into) so that the programmer can have a nice, clean and memory efficient interface to the ARM peripherals/registers/fields.
Looks terrible to me, what about:
# untyped_basics.nim
type
BitAddress = object
byteAdress: uint
bit: uint
ByteAddress = distinct uint
WordAddress = distinct uint
proc peek(a: BitAddress): byte
proc poke(a: BitAddress; b: byte)
proc flip(a: BitAddress) # maybe
proc peek(a: ByteAddress): byte
proc poke(a: ByteAddress; b: byte)
proc peek(a: WordAddress): uint32
proc poke(a: WordAddress; b: uint32)
And then for each Peripheral you have a type:
type
InterruptController* = distinct uint
proc createInterruptController*(): InterruptController =
# always at this fixed address for this SOC, but can also be dynamically determined
InterruptController(0xCAFEBABE'u32)
const
InterruptOffset = 12
proc interruptsOff*(c: InterruptController) =
poke(BitAddress(byteAddress: c + InterruptOffset, bit: 5), 0u8)
This way you don't invent your own language for types and operations for a language that already has types and operations...
Looks terrible to me, what about:
I agree this part looks terrible. What isn't visible in this post is the end result. With this tool, the author can write elegant-reading Nim that is efficient in memory and execution instruction. Here is a subset of how the output of the tool is used. Now, I know you don't like ALL_CAPS, but that is the syntax of the hardware access domain.
# read the register (v is a distinct type)
var v = PERIPH.REG
# overwrite the register (accepts the register's distinct type)
PERIPH.REG = v
# overwrite the register (also accepts a uint32)
PERIPH.REG = 0xC0'u32
# read a field from the register (f is a distinct type)
# the field value is right-shifted if necessary to occupy bit 0, up to the field's width
var f = PERIPH.REG.FIELD1
# read-modify-write one or more fields in the register.
# you may chain any number of fields.
# the values in parentheses will be shifted automatically
PERIPH.REG
.FIELD1(1'u32)
.FIELD2(42'u32)
.write()
For a real-world example, here is a Nordic nRF52 having a timer and an interrupt configured for periodic callback: https://github.com/dwhall/nim_nrf52/blob/main/src/timer.nim
I feel this end result, using mostly Nim's assignment operator for read/write is even better than peek/poke
I don't control the names of PERIPH, REG or FIELD. Those names come from the silicon manufacturers via the SVD file. Those names usually agree with their documentation, so it is best for this tool to leave them as-is.
Above the hardware interface, yes, it is our role as programmers to build up abstractions and to use explanatory names. Such an effort is bespoke for the meaning of the bits, fields and registers. It is not something that can be generalized across all microcontrollers. It is not yet something that can be easily automated.
Those names usually agree with the manufacturer's documentation, so it is best for this tool to leave them as-is.
No, it's best to have Nim code that looks like Nim code.
It is not yet something that can be easily automated.
Well but template enable(x) = x = 1 is.