Devices¶
Status: preview. This works and is used daily, but it is still being developed and sits outside termapy's compatibility promise: its commands, files and behavior may change in any release.
A linker map has every symbol the firmware defines and none of the
registers the silicon fixes -- no linker ever sees PORTA_OUT. A
device file supplies that half: one flat JSON list of a part's
registers, with names, addresses, bit fields and the safety flags that
/mem.* acts on. "Device", not "CPU": an MCU's peripherals, an ADC on
the external bus and an FPGA register block are the same kind of fact, and
a board usually has several.
Get a part¶
Any part with a CMSIS-SVD file -- every ARM Cortex-M vendor, most RISC-V -- is one command:
/dev.import C:\...\PIC32CM-LE_DFP\1.3.280\svd\PIC32CM5164LE00100.svd category=mcu/pic32cm
Imported 2517 registers for pic32cm5164le00100 from PIC32CM5164LE00100.svd (svd)
-> m3_bin/lib/microchip-technology/mcu/pic32cm/pic32cm5164le00100.device.json
50 peripherals; referenced by dev/pic32cm5164le00100.device.json
That writes two files: the part (the registers, ~2 MB, stored once in
the config's library) and a reference in dev/ (three lines saying
this board uses it). From then on the part loads every time the config
loads -- TUI, CLI and MCP server alike -- and /mem.read PORT_GROUP0_DIR
works. Nothing goes in the .cfg file: a file being in dev/ is the
declaration.
category= nests the part under its vendor, which the file supplies.
to=global stores it in the shared termapy_cfg/lib/ for every config
instead. use=off imports without touching this config. The SVD is read
once, at import; to re-convert it every session, put the command in
on_connect_cmd -- a missing SVD then reports an error and the existing
part stays in force.
SVDs come from the vendor's CMSIS pack (MPLAB X's pack manager keeps
Microchip's under .mchp_packs) or the
cmsis-svd-data repository.
A format termapy doesn't know is a plugin converter with KIND = "device"
-- the same four names as a symbol converter.
Installed vs available¶
Two questions, two commands:
/dev.list # what THIS config loaded: layer and placement
/dev.lib {pattern} # what the libraries hold; LOADED marks what this config has
The library is a tree of parts, nested however suits you
(microchip/mcu/pic32cm/, lattice/fpga/). There are two, layered the way
plugin/ and dev/ are: the config's own <cfg>/lib/ over the shared
termapy_cfg/lib/, the per-config one winning a name clash. Keep a
board's parts in its own lib/ and the config folder is self-contained:
check it in and its references resolve on any machine. Nothing in a
library loads by itself; it is a pool to pick from with /dev.use.
See a part's registers¶
/dev.regs pic32cm5164le00100 # every register: address, size, access, flags
/dev.regs pic32cm5164le00100 SERCOM0 # one peripheral (contains, or a glob)
/dev.regs reads the device file, never the chip, so it is safe on a live
board. FLAGS marks the registers to handle with care: read_effect
(reading it changes state -- a FIFO pops, a flag clears) and rmw: false
(a masked write would write back, and so clear, flags you did not name).
An SVD import sets them from readAction and modifiedWriteValues, on
the register or on any of its fields. The PIC32CM SVDs carry neither tag,
so on those parts FLAGS is empty and the datasheet is the only source;
add the flags to the library part by hand where it matters.
Placing a part: two ADCs of the same type¶
A bus ADC has no SVD, so its part is written once by hand, with offsets
and relocatable: true -- no base address anywhere in it:
m3_bin/lib/acme/adc/acme-adc16.device.json
{
"device_version": 1,
"device": "acme-adc16",
"relocatable": true,
"registers": [
{"name": "STATUS", "addr": "0x00", "size": 4, "access": "ro",
"fields": [{"name": "BUSY", "bit": 0}, {"name": "READY", "bit": 1}]},
{"name": "CTRL", "addr": "0x04", "size": 4,
"fields": [{"name": "START", "bit": 0}, {"name": "CHAN", "bit": 4, "width": 3}]},
{"name": "DATA", "addr": "0x08", "size": 4, "access": "ro", "read_effect": true}
]
}
Then the board says where its two chips sit:
/dev.use acme-adc16 at=ADC1@0x60000000 ADC2@0x60001000
Using acme-adc16: 6 registers at ADC1@0x60000000 ADC2@0x60001000 -> dev/acme-adc16.device.json
which writes the reference
{"device_version": 1, "ref": "acme-adc16",
"instances": [{"name": "ADC1", "base": "0x60000000"},
{"name": "ADC2", "base": "0x60001000"}]}
and yields ADC1_STATUS at 0x60000000, ADC2_STATUS at 0x60001000,
and so on: /mem.read ADC1_STATUS.READY, /mem.write ADC2_CTRL.START 1.
Two files, however many chips. Move one to a new chip select by editing
its base or re-running /dev.use; add a third with ADC3@....
A relocatable part must be placed: /dev.use refuses rather than
guessing, because any addresses in the library part are an example, not
this board's. A fixed-address part (an MCU) needs no at=; its reference
is one line.
A reference may override instances and nothing else -- to change a
part's registers, edit the library part. A whole .device.json copied
into dev/ still loads, and is right for a one-off.
What a device file may say¶
| Key | Meaning |
|---|---|
device |
Identity; must match the filename (vendor-partnumber keeps it unique) |
name, addr, size |
Required per register. addr is absolute, or an offset when relocatable |
fields |
Bit fields (bit, width, optional values labels); type is derived from them, so never give both |
access, read_effect, rmw |
rw/ro/wo; reading changes state (a FIFO pops, a flag clears); mask-writes forbidden |
peripheral, group, description, reset |
Context shown by /dev.regs |
relocatable + instances |
Offsets from a base; each placed copy prefixes its registers with its name |
Register names must be identifiers (no dots -- . is field access).
Registers merge over the build's symbols at load and survive
/sym.import; a build symbol with the same name wins, with a warning.
termapy_cfg/dev/ loads into every config; a per-config file replaces a
global one with the same device.
Registers are not variables¶
Loading a device file changes what /mem.* allows at those addresses. A
register is live silicon read through a driver that assumes it is the only
reader, so a bulk read over registers is refused, a write-only register
refuses every read (a whole-word write skips its audit pre-read instead),
a masked write refuses a target whose read half would pop or garble it,
and single reads are logged. See
two bargains.
See also: Symbols for the build's own symbols and the address
grammar, Memory for /mem.* and the wire spec a device
implements.