Convert ARM CMSIS-SVD files to Nim register memory mappings.
svd2nim is a tool that generates Nim modules providing access to microcontroller peripheral registers. This is a low-level building block used for writing peripheral drivers in Nim. Therefore, svd2nim is similar to svd2rust (Rust), regz (Zig) and ARM Devtools svdconv (C).
-
Conform to the CMSIS-SVD spec in order to be compatible with all Cortex-M devices, given that the SVD file is conforming.
-
Provide a high-performance yet type-safe API for low level register access.
This project also aims to provide Nim bindings for CMSIS core_*.h
headers,
which provide access to peripherals that are common to a given Cortex-M core,
such as the NVIC (interrupt controller) and the SysTick timer.
Install Nim and Nimble: see https://nim-lang.org/install.html. Then,
git clone https://github.com/auxym/svd2nim
cd svd2nim
nimble install -d
nimble build
The svd2nim binary will be created in the ./build
subdirectory.
svd2nim - Generate Nim peripheral register APIs for ARM using CMSIS-SVD files.
Usage:
svd2nim [options] <SvdFile>
svd2nim (-h | --help)
svd2nim (-v | --version)
Options:
-h --help Show this screen.
-v --version Show version.
-o DIR Specify output directory for generated files. (default: ./)
--ignore-prepend Ignore peripheral <prependToName>
--ignore-append Ignore peripheral <appendToName>
Three files will be generated by svd2nim in the output directory:
-
The "device" module (eg.
atsamd21g18a.nim
), which is the main module containing the register access API. -
The "core" module (eg.
core_cm0plus.nim
), see Core header bindings. -
uncheckedenums.nim
, which is a dependency of the device module. See Unchecked Enums.
First, a short usage example, inspired by Thea Flowers's blog post on the SAMD21 clock system.
# Import device module generated by svd2nim
import atsamd21g18a
proc initDfll48m*() =
# Modify a single bitfield in a register (read-modify-write operation)
# Here we set the number of wait states for the flash (NVM) controller
NVMCTRL.CTRLB.modifyIt:
it.RWS = HALF
# Write a full register value, specifying all bitfield values
# Configure the external 32 kHz oscillator peripheral settings
# Note: Explicitly set ONDEMAND=false because the argument default is true
SYSCTRL.XOSC32K.write(
XTALEN=true, STARTUP=0x7, EN32K=true, ONDEMAND=false
)
# Enable external 32 kHz oscillator (read-modify-write operation)
SYSCTRL.XOSC32K.modifyIt:
it.ENABLE = true
# Read a boolean bitfield from a register (wait until the external 32 kHz
# oscillator is ready for operation).
while not SYSCTRL.PCLKSR.read().XOSC32KRDY: discard
# (...)
The Nim module generated by svd2nim tries to stay close to CMSIS conventions
when it makes sense to do so. However, Nim does not map 1:1 to C (in particular,
for marking struct members volatile
or const
, and for anonymous unions),
therefore the API is not identical to CMSIS C device.h
headers.
The Nim API has was designeed with two main goals:
-
As close as possible to zero performance cost for accessing registers, compared to using C headers directly.
-
Use Nim's type system to provide as much safety as possible while still respecting goal #1.
The examples below are taken from the Nim module generated from the
ATSAMD21G18A.svd
file found under the tests
folder of the repository.
For each peripheral, a const
object is defined, each member is either a
register or cluster (a container type for other registers).
const GCLK* = GCLK_Type(
CTRL: GCLK_CTRL_Type(loc: 0x40000c00),
STATUS: GCLK_STATUS_Type(loc: 0x40000c01),
CLKCTRL: GCLK_CLKCTRL_Type(loc: 0x40000c02),
GENCTRL: GCLK_GENCTRL_Type(loc: 0x40000c04),
GENDIV: GCLK_GENDIV_Type(loc: 0x40000c08),
)
Note that all fields are marked public.
Cluster objects are similar to Peripherals: they are "container" objects that contain either registers or other clusters. svd2nim supports clusters nested arbitrarily deep.
Registers are also object
types. However, registers contain a
single field, which is the address (loc
) to the memory-mapped register,
represented as uint
. Example:
type GCLK_GENDIV_Type = object
loc: uint
Note that the loc
field is private. Indeed, registers are only intended to
be accessed using accessor templates described in the following section. This
allows:
-
Using Nim's type system to enforce register access (read-only, write-only, read/write) permissions specified in the SVD file.
-
Automatically calling
volatileLoad
andvolatileStore
calls for reads and writes. -
Convenient access to bitfields, also described below.
As noted above, registers can only be read or written using generated accessor
procs. For each register object type, either a read
proc, a
write
proc, or both may be generated, depeding on the register access
permissions defined by the SVD file. Example (for a read/write register):
proc read*(reg: ADC_WINLT_Type): uint16 {.inline.} =
volatileLoad(cast[ptr uint16](reg.loc))
proc write*(reg: ADC_WINLT_Type, val: uint16) {.inline.} =
volatileStore(cast[ptr uint16](reg.loc), val)
SVD registers may define field
elements, which means that the register value
is split into bitfields. For these registers, svd2nim generates a distinct
integer type which is used as the register value type (for read
/write
)
instead of the base integer type. Example:
type
# (...)
GCLK_GENDIV_Fields* = distinct uint32
Each bitfield of the distinct type can be read or set using bitfield accessors.
Here, the GCLK_GENDIV
register defines 2 fields: ID
(bits 0-3) and DIV
(bits 8-23). Other bits are unused (reserved). The accessors generated are:
func ID*(r: GCLK_GENDIV_Fields): uint32 {.inline.} =
r.uint32.bitsliced(0 .. 3)
proc `ID=`*(r: var GCLK_GENDIV_Fields, val: uint32) {.inline.} =
var tmp = r.uint32
tmp.clearMask(0 .. 3)
tmp.setMask((val shl 0).masked(0 .. 3))
r = tmp.GCLK_GENDIV_Fields
# Note: `div` is a reserved keyword in Nim, so DIVx is used
# to avoid the conflict.
func DIVx*(r: GCLK_GENDIV_Fields): uint32 {.inline.} =
r.uint32.bitsliced(8 .. 23)
proc `DIVx=`*(r: var GCLK_GENDIV_Fields, val: uint32) {.inline.} =
var tmp = r.uint32
tmp.clearMask(8 .. 23)
tmp.setMask((val shl 8).masked(8 .. 23))
r = tmp.GCLK_GENDIV_Fields
Credit goes to the cdecl/bitfields library, which strongly inspired this approach to handling bitfields, due to issues with "native" C bitfields.
svd2nim generates two write
accessors for registers that are writable and
define fields. One takes a full value (the distinct integer type) and the second
takes a separate value for each field, with a default value equal to the
register's reset value for that field as defined by the SVD. Example:
proc write*(reg: GCLK_GENDIV_Type, val: GCLK_GENDIV_Fields) {.inline.} =
volatileStore(cast[ptr GCLK_GENDIV_Fields](reg.loc), val)
proc write*(reg: GCLK_GENDIV_Type, ID: uint32 = 0, DIVx: uint32 = 0) =
var x: uint32
x.setMask((ID shl 0).masked(0 .. 3))
x.setMask((DIVx shl 8).masked(8 .. 23))
reg.write x.GCLK_GENDIV_Fields
Finally, for convenience when doing a read-modify-write operation, a modifyIt
template is also generated for read-write registers with fields. Similarly to
the the *it
templates in Nim's std/sequtils
module, modifyIt
reads the
register and stores its value in the it
variable. The op
parameter passed to
the template can then modify it
. Finally, it
is written back to the
register. Example template code:
template modifyIt*(reg: GCLK_GENDIV_Type, op: untyped): untyped =
block:
var it {.inject.} = reg.read()
op
reg.write(it)
This allows modifying mutliple fields with a single read-modify-write operation.
IMPORTANT NOTE: Due to a currently open Nim
bug related to volatileStore
and volatileLoad
, calling modifyIt
from the top-level in a module results in
incorrect codegen by the Nim compiler and in C compiler errors. The workaround
is simple: ensure that all calls are made from inside a proc
.
Some bitfields have associated enum types defined by the SVD file. svd2nim generates these as Nim enum types, and uses the type for the bitfield accessors described above.
However, when reading a bitfield, we cannot guarantee that converting the
resulting numerical value to the enum type will succeed. The numerical value may
be invalid for the enum type: a 4-bit bitfield may read 11
when only values
0
through 10
are defined in the enum type.
For this reason, accessors generated by svd2nim for bitfields with enum value
types return UncheckedEnum[FieldEnumType]
instead of trying to convert directly
to FieldEnumType
(and possibly failing).
The uncheckedenums
module provides procs to convert unchecked enums to their
base enum type but also work with unchecked enum values directly, as conversion
is often uneeded (eg. for comparison):
import atsamd21g18a
# A holey enum
type ADC_COMPCTRL_MUXPOS* {.size: 4.} = enum
muxPIN0 = 0x0,
muxPIN1 = 0x1,
muxPIN2 = 0x2,
# (...)
muxDAC = 0x1c,
let muxpos: UncheckedEnum[ADC_COMPCTRL_MUXPOS] = AC.COMPCTRL0.read().MUXPOS
# Can be compared directly with an enum value, without converting
if muxpos == muxPIN2:
# do something
discard
# We can use `get` for converting to the underlying enum type,
# ADC_COMPCTRL_MUXPOS. `get` will raise a Defect if the value is invalid, so you
# likely want to check `isValid` first.
if muxPos.isValid:
case muxPos.get:
of muxPIN0:
discard
of muxPIN1:
discard
else:
discard
else:
# Handle invalid value
discard
For more information, see the uncheckedenums.nim
file.
The "core" C header file for a given ARM Cortex-M CPU (eg, core_cm0plus.h
for Cortex-M0+) contains functions related to peripherals that are common to
the CPU core, such as the NVIC (interrupt controller) and the SysTick Timer.
When svd2nim
is called, a second file (eg. core_cm0plus.nim
), containing
bindings for the core header, will be generated in the same output directory as
the main device module. Import the core Nim module requires that the
corresponding C headers can be found by the C compiler (eg. by passing
--passC:-I./lib/CMSIS/Core/Include
to the Nim compiler). The CMSIS headers can
be obtained from:
https://github.com/ARM-software/CMSIS_5/tree/develop/CMSIS/Core/Include
And are documented here: https://arm-software.github.io/CMSIS_5/Core/html/modules.html
See the nim-on-samd21 repository for an example on building the Nim core bindings against the CMSIS headers.
Currently only the core_cm0plus.nim
module, for Cortex-M0+ CPUs, is provided
by svd2nim , but PRs are welcome for others.
Unless specified otherwise in specific files, svd2nim is distributed under the
terms of the MIT license. See LICENSE
file for the full terms and copyright
notice.
The SVD files under the tests directory are copyright of their respective authors and used under license, as specified in each file.