embsvd — a device's registers from its SVD file#
embsvd reads a microcontroller's CMSIS-SVD file, the XML description
every Cortex-M vendor publishes of a device's peripherals, registers, bit
fields and interrupts. From it, it writes the files a bare-metal project
starts with: the device header, a startup file and a linker script. It
also answers questions about the registers, and describes the whole
device as JSON for other tools. This page is the command reference.
embsim reads the same files with the same reader
(--svd), so a firmware built from embsvd's header runs over registers
placed exactly where the header put them.
Synopsis#
embsvd DEVICE.svd [--header FILE] [--no-cmsis]
[--nvic-prio-bits N] [--fpu-present 0|1]
[--startup FILE]
[--ld FILE --flash ORIGIN:LENGTH --ram ORIGIN:LENGTH]
[--json FILE [--flash ORIGIN:LENGTH] [--ram ORIGIN:LENGTH]]
embsvd DEVICE.svd --list
embsvd DEVICE.svd --show PERIPHERAL
A project from an SVD#
embsvd STM32F405.svd --nvic-prio-bits 4 --fpu-present 1 \
--header STM32F405.h --startup startup.c \
--ld STM32F405.ld --flash 0x08000000:1M --ram 0x20000000:128K
CF="--target=thumbv7em-none-eabi -O2 -ICMSIS/Core/Include -fgnuc-version=4.2.1"
embcc $CF -c startup.c && embcc $CF -c main.c
embcc --target=thumbv7em-none-eabi --link-script=STM32F405.ld startup.o main.o -o fw.elf
main.c includes STM32F405.h and drives the device through it:
#include "STM32F405.h"
void USART1_IRQHandler(void) { /* ... */ } /* takes the startup's weak one */
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_USART1EN_Msk;
USART1->CR1 = USART6_CR1_UE_Msk | USART6_CR1_TE_Msk;
NVIC_EnableIRQ(USART1_IRQn);
SysTick_Config(16000);
/* ... */
}
The header (--header FILE)#
The header has the shape of the headers CMSIS's svdconv writes, and of
the vendors' own:
IRQn_Type. The core's exceptions (SysTick_IRQn = -1, ...; a RISC-V part has none), then each interrupt of the SVD asNAME_IRQn, by number. ARMv6-M and ARMv8-M Baseline cores (CM0,CM0PLUS,CM1,CM23) get only the exceptions they have.- The core's configuration.
__CM4_REV(from the SVD'sr1p0-style revision),__MPU_PRESENT,__FPU_PRESENT,__NVIC_PRIO_BITSand__Vendor_SysTickConfig. Then the header includes CMSIS's core header for the SVD's cpu (core_cm4.hforCM4), which providesNVIC,SCB,SysTick, and the intrinsics. With the core header included, the SVD's own copies of the core's peripherals (everything from 0xE0000000 up to the vendor area at 0xE0042000) are left out, assvdconvleaves them out, and a comment lists them. - One struct per register layout. A peripheral's registers are laid
out by their offsets, with
uint8_t RESERVEDn[...]filling the gaps. Registers declared as each other'salternateRegistershare an anonymous union, as do a read-only and a write-only register at one offset (svdconv makes the same union). A register is__IO,__I(read-only) or__O(write-only), and isuint8_ttouint64_tby its size. The struct is named after the peripheral that declares the registers (USART6_Type), or itsheaderStructName. A peripheralderivedFromanother uses its type, soUSART1,USART2andUSART3are allUSART6_Typein ST's file. - One struct per cluster, before the struct it is in, innermost first (see Clusters and arrays).
- Where each one is.
NAME_BASEand#define NAME ((TYPE *)NAME_BASE). The SVD'sheaderDefinitionsPrefixgoes in front of both, and of a peripheral's struct, but not of a cluster's: Nordic'sNRF_givesNRF_UARTE00_S,NRF_UARTE_TypeandUARTE_PSEL_Type. - Every field.
STRUCT_REGISTER_FIELD_Posand..._Msk, whereSTRUCTis the struct that holds the register, less_Type(USART6_CR1_UE_Msk,UARTE_PSEL_TXD_CONNECT_Msk).
--no-cmsis writes the header without CMSIS. The core's peripherals then
stay, and __IO, __I and __O are defined in the header.
An SVD can be wrong about the core. ST's STM32F405.svd 1.2 says 3
priority bits and no FPU, but the part has 4 and an FPU, as ST's own
stm32f405xx.h says. --nvic-prio-bits N and --fpu-present 0|1
override what the file says.
Clusters and arrays#
A <cluster> is a group of registers at an addressOffset from the
peripheral or cluster it is in, and clusters nest to any depth. Each
becomes a struct, named, as svdconv names it, by its headerStructName,
else its dimName, else the enclosing struct's name and its own
(NETIF_PORT_Type for cluster PORT[%s] in peripheral NET whose
headerStructName is NETIF). The enclosing struct holds it as a
member at its offset.
An array -- a register, cluster, field or peripheral with dim and
dimIncrement -- is laid out by its name:
| SVD name | Elements | In the header |
|---|---|---|
DATA[%s], dimIncrement the register's size |
packed | __IO uint32_t DATA[4]; |
CH[%s], a cluster smaller than dimIncrement |
spaced | the cluster's struct is padded to dimIncrement: __IO DMA_CH_Type CH[4]; |
PRIO[%s], a register smaller than dimIncrement |
spaced | PRIO0, PRIO1, ... at their offsets, RESERVED between |
GPIO%s_CTRL |
any | GPIOA_CTRL, GPIOB_CTRL, ... at their offsets |
%s takes its names from dimIndex: a list (A,B,C), a range of
numbers (8-15) or of letters (A-D), or 0 to dim - 1 without one.
A field array (CH%s_IE) is one field per element, dimIncrement bits
apart. A peripheral array (TIMER%s) is one peripheral per element,
dimIncrement bytes apart, all with the first one's struct.
A register, cluster, field or enumeratedValues can be derivedFrom
another: in the same scope by its name (CTRL), or anywhere by its full
path from the peripheral (NET.PORT.CFG, DMA.CTRL.MODE). It is the
other one with what the derived element says written over it. An array's
dim comes along only when the new name has a %s for it, so
<register derivedFrom="DATA"> named DATAX is one register. A field
that gives only a new bitOffset keeps the other's width. A cluster
derived from another, with no registers of its own, uses the other's
struct.
Every struct is checked against C as it is laid out: a member must be at a multiple of its own alignment, as C would place it, and the elements of an array must not overlap.
The startup (--startup FILE)#
The startup is a C file:
- The vector table. It is placed in
.isr_vector: the initial stack pointer (_estack),Reset_Handler, the core's exceptions, and each interrupt in its slot (16 + NAME_IRQn). Unused slots are 0. - Handlers. Every handler is a weak alias of
Default_Handler, which spins. A function of the same name in the program, such asUSART1_IRQHandlerorSysTick_Handler, replaces it. Reset_Handler. It copies.datafrom_sidatato_sdata.._edata, zeroes_sbss.._ebss, and callsSystemInitif the program defines one (a weak empty one is there otherwise). It then runs the constructors from__init_array_startto__init_array_end, and callsmain.
For the RP2040 (the SVD's device RP2040, the Raspberry Pi Pico's
part), the startup holds a boot2 as well: the flash's first 256 bytes,
in .boot2, which the part's bootrom copies to SRAM and runs if the
CRC-32 of the first 252 is the last word. embsvd's sets the XIP up for
03h reads, which any SPI flash answers -- the settings of the Pico
SDK's boot2_generic_03h; its source is in tools/embsvd/embsvd.c --
and goes to the vector table after it, at 0x10000100; embsvd adds the
CRC.
For the ESP32-C3 (the SVD's device ESP32-C3, a RISC-V part), the
startup is the core's instead: its ROM's loader has put the image in
SRAM and jumps to the entry, _start, which sets the stack pointer to
_estack and calls Reset_Handler. That points mtvec at the vector
table -- 256-aligned, in the core's vectored mode, entry 0 a jump to
Exception_Handler and entry N to CPU_INTN_Handler (N 1-31), each
weak -- zeroes .bss (.data is already where it is linked), and goes
on as above. Which peripheral raises which CPU interrupt is the
interrupt matrix's to say (INTERRUPT_CORE0's map registers), not the
table's; a handler takes __attribute__((interrupt)). Another RISC-V
part's startup is refused: its interrupt controller is its vendor's, and
its SVD does not describe it.
The linker script (--ld FILE)#
An SVD describes peripherals, not memories, so the flash and RAM regions
are given on the command line: --flash ORIGIN:LENGTH --ram ORIGIN:LENGTH, in C notation with an optional K, M or G
(0x08000000:1M). The script has STM32CubeMX's shape:
.isr_vectorfirst in flash, kept;.text,.rodata,.ARM.exidxand the constructor arrays in flash;.datain RAM, stored in flash (_sidata);.bssin RAM;_estackat the top of RAM.
It defines every symbol the generated startup reads. For the RP2040,
.boot2 goes first and .isr_vector after it, at 0x10000100: the
flash must be given at 0x10000000, or embsvd refuses.
For the ESP32-C3 the script takes no --flash or --ram: the image runs
from SRAM1, which the core fetches from as IRAM at 0x40380000 and loads
from as DRAM at 0x3FC80000, the same bytes. The code goes in IRAM's
first 128 KiB and the rest in DRAM after them, up to 0x3FCDC000, below
the ROM loader's own RAM; _estack is the top of that. .data is not
stored twice: the loader puts it in place. Edit the script's MEMORY to
split the SRAM otherwise.
The hardware as JSON (--json FILE)#
--json writes what the SVD says about the device for tools rather than
for a compiler -- a debugger's register view, an RTOS's hardware
description -- with every array and cluster expanded and every register at
its absolute address. --flash and --ram add the memories. The
document is one object; its "schema" is 1, and stays 1 as long as
no key below changes its meaning or goes away (keys may be added).
{
"schema": 1,
"generator": "embsvd",
"source": "device.svd",
"device": {"name": "...", "vendor": "...", "version": "...", "description": "...",
"addressUnitBits": 8, "width": 32, "headerDefinitionsPrefix": "NRF_"},
"cpu": {"name": "CM33", "revision": "r0p4", "endian": "little",
"mpuPresent": true, "fpuPresent": true, "fpuDP": false,
"nvicPrioBits": 3, "vendorSystickConfig": false,
"deviceNumInterrupts": 270},
"memories": [{"name": "FLASH", "origin": 0, "length": 1572864, "access": "rx"},
{"name": "RAM", "origin": 536870912, "length": 262144, "access": "rwx"}],
"interrupts": [{"name": "SERIAL00", "value": 74, "description": null}],
"peripherals": [
{"name": "UARTE00_S", "description": "...", "groupName": null,
"baseAddress": 1342480384, "derivedFrom": "UARTE00_NS",
"typeName": "NRF_UARTE_Type",
"addressBlocks": [{"offset": 0, "address": 1342480384, "size": 4096,
"usage": "registers"}],
"interrupts": [{"name": "SERIAL00", "value": 74, "description": null}],
"registers": [
{"name": "TXD", "path": "PSEL.TXD", "index": [],
"address": 1342481924, "offset": 1540, "size": 32,
"access": "read-write", "resetValue": 4294967295,
"resetMask": 4294967295, "alternate": null, "description": null,
"fields": [
{"name": "CONNECT", "bitOffset": 31, "bitWidth": 1,
"access": "read-write", "description": null,
"enumeratedValues": [
{"name": "Connected", "value": 0, "isDefault": false,
"usage": "read-write", "description": null}]}]}]}]
}
device: the SVD'sname,vendor,versionanddescription(nullwhen the file has none), itsaddressUnitBitsandwidth, and itsheaderDefinitionsPrefixornull.cpu:nullwhen the SVD has no<cpu>. Otherwise itsname,revisionandendian(strings ornull),mpuPresent,fpuPresent,fpuDPandvendorSystickConfig(booleans,falsewhen not said),nvicPrioBits, anddeviceNumInterrupts(ornull).--nvic-prio-bitsand--fpu-presentcorrect them here too.memories: from--flash(FLASH,rx) and--ram(RAM,rwx), asoriginandlength; empty without them, because an SVD does not describe memories.interrupts: every interrupt of the device once, byvalue.peripherals: in the SVD's order, each array element its own.derivedFromis the peripheral whose registers it has, ornull;typeNamethe header's struct for them.addressBlocksgives each block'soffset, absoluteaddress,sizeandusage(a derived peripheral without its own has the other's).interruptsare the peripheral's own.registers: every register element, by address.nameis the SVD's with its%sreplaced (DATA[2],GPIOB_CTRL);pathis how C reaches it from the peripheral's struct in the generated header (PORT[1].QUEUE[2].DESC[1].FLAGS,PRIO1);indexis the element's position in each array on the way, outermost first ([1, 2, 1],[]when there is none).addressis absolute andoffsetfrom the peripheral's base.sizeis in bits;accessis one ofread-only,write-only,read-write,writeOnceandread-writeOnce, inherited as the SVD inherits it;resetValueandresetMaskare cut to the register's size.alternateis thealternateRegisteroralternateGroup, ornull.fields:bitOffsetandbitWidthhowever the SVD wrote them,access(the register's when the field says none), andenumeratedValues: each value'sname,value(nullfor anisDefaultone with no value),isDefault, theusageof the group it is in, anddescription. A value written with don't-care bits (#1x) hascare, the bits of the field it fixes.
Numbers are JSON integers; a 64-bit reset value may be larger than a JavaScript number holds exactly.
Asking (--list, --show)#
--list prints the peripherals: the base address, and either the number
of registers or which peripheral it is like (derived from).
STM32F405: CM4, 91 peripherals, 83 interrupts
USART1 0x40011000 like USART6 Universal synchronous asynchronous receiver transmitter
--show NAME prints one peripheral's registers, each array element and
cluster expanded and named by its path, with their addresses, sizes,
access, reset values and fields:
USART1 at 0x40011000: Universal synchronous asynchronous receiver transmitter
0x40011000 +0x000 SR 32 bits rw reset 0xc00000 Status register
[9] CTS
[7] TXE
0x40011008 +0x008 BRR 32 bits rw reset 0x0 Baud rate register
[15:4] DIV_Mantissa
[3:0] DIV_Fraction
What it reads#
- Numbers. Decimal,
0xhex, and#binary, withk/M/G. - Fields. Given by
bitOffset/bitWidth,lsb/msborbitRange. - Inherited values.
size,accessandresetValuecome from the device or the peripheral when a register does not give them. - Access. The five SVD access types, in any case (Nordic writes
read-writeonce). - Booleans.
true,false,1and0. derivedFromon peripherals, through any chain of them, and on registers, clusters, fields andenumeratedValues.- Clusters and arrays, as above.
These are refused by name, with the file's line, by every output, because a header whose offsets are approximately right compiles and then drives the wrong register:
- a register or cluster not at a multiple of its own alignment;
- two registers at one offset when neither is the other's
alternateRegister(and they are not a read-only and a write-only one); - a register that overlaps the one before it, and an array whose elements overlap;
[%s]anywhere but at the end of a name, two%sin one name,%swithoutdimanddimwithout%s, and adimIndexthat names a different number of elements;- an array of peripherals written
NAME[%s]; - a
derivedFromthat names nothing, or goes round in a circle; - a peripheral or cluster that is
derivedFromanother and also has registers of its own; - two different structs that would have one name (two clusters with
one
headerStructName, say); - a field outside its register;
- an
addressUnitBitsother than 8 -- but 32 in an Espressif SVD (vendorIDESPRESSIF), whose addresses count bytes all the same.
Exit status#
embsvd exits with 0 on success, 1 when the file cannot be read or is
refused, and 2 for a usage error.
Tests#
tests/golden/svd-stm32f405.sh checks embsvd against ST's own files:
- From ST's
STM32F405.svd, a table of 149 register addresses in 31 peripherals is compiled against the generated header and against ST's hand-writtenstm32f4xx.h, and the two must be byte-identical. - A program built only from the generated header, startup and linker
script, plus CMSIS-Core, runs on QEMU's netduinoplus2 (an
STM32F405). It checks
.data,.bss, a constructor, SysTick, and a pended USART1 interrupt reaching its handler through the generated vector table.
The SVD, CMSIS_5 and cmsis-device-f4 are looked for under ~/EmbRef.
The test skips when they are missing.
tests/golden/svd-clusters.sh checks clusters, arrays, derivedFrom
and --json with three witnesses that share no code: _Static_asserts
of offsetof and sizeof, worked out by hand from the SVDs, compiled
against the generated headers by EmbCC and by clang for a Cortex-M; a
Python reading of the SVD by the specification (svdref.py), whose
register addresses the JSON's must equal; and an assert per JSON
register that the header's BASE + offsetof(TYPE, path) is its address.
It runs them over features.svd, which holds every feature,
nrfshape.svd, a device in the shape of Nordic's nRF54L files, and
Nordic's own SVD files when ~/EmbRef/svd/nrf*.svd has them. ARM's
ARM_Example.svd is checked against the TIMER0_Type that CMSIS's own
svdconv wrote for it, from CMSIS_5's documentation.
See also#
Bare-metal programming,
embld (linker scripts).