Skip to content

Devices

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 or not (* = loaded here)

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.

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 for /sym.info and the register view
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.