Specialized Register Kinds#
Most registers in a SystemRDL design are plain RW words. A handful are not:
they tick on their own (counters), self-clear after a single cycle
(singlepulse), guard a reset sequence, or are locked by a key. Each kind
gets a small, opinionated wrapper on top of the four primitives in
Values and I/O so the typical operation is one obvious method
call, the bus cost is honest, and the side-effecting cases are surfaced
loudly rather than hidden behind a generic write.
This page is the canonical reference for those wrappers. If a register has
none of these special properties, this page does not apply — use read(),
write(), modify() from the values-and-I/O page directly.
Counters#
When a field carries the RDL counter property, PeakRDL-pybind11 emits a
Counter wrapper rather than a bare field. A counter knows its own
saturation rules, its threshold (if any), and which of increment/decrement
the hardware actually supports.
c = soc.peripheral.event_counter
c.value() # current count (read)
c.reset() # clear (per RDL — could be wclr or hwreset)
c.threshold() # if `incrthreshold` set
c.is_saturated() # if `incrsaturate`
c.increment(by=1) # if sw-incr supported (writes the incrvalue field)
c.decrement(by=1) # if `decr` supported
c.value() is one bus read. c.reset() is one bus write whose exact
semantics are determined by the RDL — typically a wclr write or a
hardware-driven reset path. c.increment(by=N) and c.decrement(by=N)
exist only when the source RDL declares software-incrementable or
software-decrementable counters; c.threshold() and c.is_saturated()
exist only when incrthreshold / incrsaturate are set. When the
underlying property is absent, the wrapper does not silently no-op — the
method is not generated in the first place, so a typo in user code is an
AttributeError at the call site.
Singlepulse#
A singlepulse field self-clears one cycle after a write of 1. The
register’s write() would clobber other fields, so the wrapper exposes a
dedicated pulse() instead:
soc.dma.channel[0].control.start.pulse() # writes 1; hardware clears
pulse() is one bus write. The bit reads back as 0 immediately after
because the hardware has already cleared it; this is correct, not a bug, and
pulse() is named to make that visible at the call site. Use pulse()
for “kick this state machine” semantics rather than reaching for the
generic field.write(1) form — the dedicated name is the documented
surface for singlepulse fields.
Reset semantics#
Every register knows its declared reset value, can compare its current value against that reset value, and can write itself (or its parent subtree) back to those reset values explicitly. This is the right tool for “give me a clean baseline before this test” and for snapshot-driven golden-state checks.
soc.uart.control.reset_value # 0x0
soc.uart.control.is_at_reset() # read & compare
soc.uart.reset_all(rw_only=True) # write reset values to writable regs
soc.reset_all() # whole tree, with safety check on side-effecting fields
reg.reset_value is a static property derived from the RDL — no bus
traffic. reg.is_at_reset() is one bus read followed by an in-process
comparison.
The reset_all calls are deliberately conservative:
rw_only=Trueis the default. Read-only registers are skipped, since you cannot write back a hardware-driven status. Passrw_only=Falseonly if you really mean “panic-write the whole subtree”, and the RDL declares those registers writable in some access mode.The whole-tree
soc.reset_all()runs a side-effect safety check before issuing any writes. Each register slated for restore is inspected for fields that combine RW withrclrsemantics — those are flagged as ambiguous to “reset” because the next read will both compare against the reset value and clear the field as a read side effect.
Warning
soc.reset_all() warns if any RW register also has an rclr field.
Such fields make “reset” ambiguous: the act of verifying reset state
itself mutates the field. Resolve by passing an explicit list of paths
to skip, or by using the per-register reg.reset_value and
reg.write(...) calls in a controlled order.
For a single register’s reset, write the value directly:
soc.uart.control.write(soc.uart.control.reset_value)
This is one bus write and reads cleanly without dragging in the whole-subtree machinery.
Lock#
The RDL lock family covers fields whose writes are gated by a separate
key sequence — STM32-style LCKR registers are the canonical example. The
exporter emits lock / is_locked / unlock_sequence accessors on
the gating register so the user does not hand-roll the key dance:
soc.gpio_a.lckr.lock(["pin0", "pin5"]) # programs LCK + sets LCKK key per RDL `lock`
soc.gpio_a.lckr.is_locked("pin0") # True
soc.gpio_a.lckr.unlock_sequence() # explicit, vendor-specific UDP
lock(paths) programs the lock-bits register with the named field set,
then drives the key write sequence the RDL lock property declares.
is_locked(name) returns True/False for one named lock target.
unlock_sequence() is generated when (and only when) the RDL or a
vendor-specific UDP declares an explicit unlock path; on parts that do not
support unlock — many do not, by design — the method is simply absent.
The exact bus traffic of lock and unlock_sequence is part-specific
and visible through the trace surface; expect more than one transaction
because the key sequence itself is multiple writes by definition.
External regs#
The RDL external reg keyword declares a register whose backing storage
lives outside the generated regblock — typically a different master serves
its address. PeakRDL-pybind11 emits the same Reg node for an
external reg as for an ordinary one, with the same primitive ops,
the same field accessors, and the same value types.
soc.peripherals.foo.external_reg.read() # same as any other Reg
soc.peripherals.foo.external_reg.modify(enable=1)
The only difference is bus dispatch: when masters are routed by region (see
The Bus Layer), an external reg may end up on a different
master than its siblings. From the Python user’s perspective, that
distinction is invisible by design — there is one API regardless of where
the bytes ultimately land.
If a routing decision is unexpectedly wrong, reg.info.address and the
master-routing tools described in The Bus Layer are the
discoverability surface, not a separate API for external registers.
See also#
Read/Write Side Effects — singlepulse and counter saturation intersect with the broader rules around read-side and write-side effects; read that page for
rclr,woclr, and theno_side_effects()context.The Bus Layer — external registers and per-region master routing; the place to look when an
external reglands on a different bus master than its siblings.