Device Block Operations
Device provides a family of methods that traverse a tree and initiate or
check Block transactions:
These methods sit between the tree model and the lower-level Block
transaction layer. They are used in three main ways:
As the default path for bulk reads and writes across a subtree.
As the per-Variable path used by many hardware-backed reads and writes.
As the main override points when hardware needs custom sequencing.
Why These Methods Exist
PyRogue separates transaction initiation from transaction completion. That is why these methods come in pairs and helper sequences rather than as one large “do everything” call.
At a high level:
writeBlocksinitiates write transactions.verifyBlocksinitiates verify transactions.readBlocksinitiates read transactions.checkBlockswaits for initiated transactions to complete.
That split lets a Device issue many transactions first and then wait for
completion afterward. The composed helpers:
writeAndVerifyBlocksreadAndCheckBlocks
simply bundle the most common full flows on top of those same methods. In practice, most readers only need two ideas first:
The default bulk path is “issue transactions across this
Deviceor subtree, then check them.”These same methods are also the normal place to override sequencing when the hardware needs something more specific.
Default Full-Device Behavior
When called without variable=..., these methods operate on the current
Device and, optionally, its child Devices.
The default traversal is simple:
Process the
Blockobjects attached directly to the currentDevice.Recurse into child Devices if
recurse=True.
So the current Device goes first, and child Devices follow after that.
That is the baseline behavior that custom overrides either preserve or replace.
Ordering Rules
Within that default traversal, the ordering is also defined.
Inside one Device:
Automatically built hardware-backed Blocks are created from Variables sorted by
(offset, varBytes), so those Blocks are normally issued in address-oriented order.LocalVariableBlocks are appended as the Device walks its child Nodes, so they follow Node insertion order rather than address sorting.
For the Block-building process that creates that ordering, see Blocks.
For child Devices, the traversal follows add order. Node stores children
in an OrderedDict, and Device recurses through
self.devices.values().
So the default recursive flow is:
This Device’s Blocks first.
Then child Devices in add order.
Single-Variable Behavior
The same methods can also target one specific Variable by passing
variable=....
That path does not perform full subtree traversal. Instead, it operates only on
the Block associated with that Variable. This is how many hardware-backed
per-Variable get and set paths reuse the same Device block methods
without turning into full-tree operations.
Common Usage Patterns
Most manual use of these APIs falls into one of three patterns: operate on a
subtree, operate on one Variable’s backing Block, or call a composed
helper when the full flow is what you actually want.
Read One Subtree
# Initiate reads across this Device subtree, then wait for completion.
my_dev.readAndCheckBlocks(recurse=True)
This is the normal manual pattern when you want a current hardware snapshot of one part of the tree.
Write And Verify One Subtree
# Force a full write/verify/check pass across this Device subtree.
my_dev.writeAndVerifyBlocks(force=True, recurse=True)
This pattern is often useful for ADC and mixed-signal configuration, where a command needs to push a complete known-good register set into hardware in one step.
Target One Variable’s Block
# Only issue a write for the Block backing one Variable.
my_dev.writeBlocks(variable=my_dev.MyRegister, checkEach=True)
my_dev.checkBlocks(variable=my_dev.MyRegister)
This is the manual form of the narrower path that hardware-backed per-Variable operations use internally.
Composed Helpers
Two helpers cover the most common complete flows:
writeAndVerifyBlocks(...)runswriteBlocks->verifyBlocks->checkBlocks.readAndCheckBlocks(...)runsreadBlocks->checkBlocks.
These helpers are often the clearest way to trigger a full operation from a script, a command callback, or a one-shot configuration step. They are also a good way to avoid open-coding the same multi-step sequence in many places.
How YAML Configuration Uses These Methods
YAML configuration loading is one of the main places where users encounter bulk block operations indirectly.
For Root.loadYaml(..., writeEach=False) and Root.setYaml(..., writeEach=False),
PyRogue first stages values into Variables with setDisp(..., write=False).
Only after that full YAML payload has been applied to the tree does Root
commit the configuration through its normal bulk write path.
In practice, that means YAML configuration uses the same transaction model described on this page:
Values are staged first.
The resulting Block writes are issued across the tree.
Verification and completion checks run through the normal bulk helpers.
So the YAML page should be read as “how the tree is described and matched,” while this page remains the right place for “how the resulting hardware transactions are issued and checked.”
Two Root settings are especially relevant in that YAML-driven path:
ForceWritecan force writes of non-stale Blocks during config apply.InitAfterConfigcan triggerinitialize()after the configuration has been committed.
Method Parameters
The methods share a common shape, but the meaning of the parameters is easiest
to understand after the default traversal and common usage patterns are clear.
The most important controls are recurse, variable, checkEach, and
for writes, force.
writeBlocks
Signature:
writeBlocks(*, force=False, recurse=True, variable=None, checkEach=False, index=-1, **kwargs)
Important parameters:
force: Write even when the value is not marked stale.recurse: Include child Devices whenTrue.variable: Operate on one Variable’s Block instead of the full Device subtree.checkEach: Request per-transaction checking behavior instead of deferring checks.index: Array index used for targeted Variable operations.**kwargs: Passed through to the underlying transaction helpers.
verifyBlocks
Signature:
verifyBlocks(*, recurse=True, variable=None, checkEach=False, **kwargs)
Important parameters:
recurse: Include child Devices whenTrue.variable: Verify only one Variable’s Block.checkEach: Request per-transaction checking behavior.**kwargs: Passed through to the transaction helper.
readBlocks
Signature:
readBlocks(*, recurse=True, variable=None, checkEach=False, index=-1, **kwargs)
Important parameters:
recurse: Include child Devices whenTrue.variable: Read only one Variable’s Block.checkEach: Request per-transaction checking behavior.index: Array index used for targeted Variable reads.**kwargs: Passed through to the transaction helper.
checkBlocks
Signature:
checkBlocks(*, recurse=True, variable=None, **kwargs)
Important parameters:
recurse: Include child Devices whenTrue.variable: Check only one Variable’s Block.**kwargs: Passed through to the transaction helper.
Device-Wide checkEach Behavior
The checkEach control exists at two levels.
At call time, writeBlocks, verifyBlocks, readBlocks,
writeAndVerifyBlocks, and readAndCheckBlocks all accept a
checkEach=... argument. That requests transaction-by-transaction checking
for that one operation.
At Device scope, the persistent control is forceCheckEach. Each block
method combines the caller’s argument with the Device setting:
checkEach = checkEach or self.forceCheckEach
So a Device can force per-transaction checking for all reads, writes, and verifies by setting:
self.forceCheckEach = True
There is not a separate Device(..., checkEach=...) constructor argument in
this implementation. If a Device should always use this stricter behavior, set
forceCheckEach in the subclass:
class MyDevice(pyrogue.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.forceCheckEach = True
This is useful when the hardware benefits from immediate acknowledgement of each transaction instead of the default “issue many operations, then check them” flow. One example is a board or front-end path where tight sequencing or hardware-side backpressure makes deferred checking less desirable.
How To Override Properly
The normal reason to override these methods is that the hardware needs extra ordering around the default traversal. Before overriding, it helps to separate two questions:
Are you keeping the inherited traversal and just adding steps around it?
Or are you intentionally replacing part of the inherited traversal order?
Common cases:
A commit or apply strobe must be written after configuration registers.
A page or bank select register must be set before reads or writes.
A debug-freeze or capture gate must wrap a read sequence.
Child Devices must be traversed in a hardware-specific order rather than the default add order.
A subsystem needs to change the order in which some operations happen.
The general rule is:
Keep the default traversal unless you intentionally need to replace it.
Preserve the relevant keyword parameters.
Add only the extra pre- or post-sequencing behavior you need.
The following examples move from the smallest override to the most structural one.
Example: Post-Write Update Strobe
This pattern is often needed for ADC and similar devices whose configuration registers are shadowed internally and do not take effect until a separate update strobe is written:
class MyDevice(pyrogue.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pyrogue.RemoteCommand(
name='DeviceUpdate',
offset=0x3FC,
function=pyrogue.BaseCommand.touchZero,
))
def writeBlocks(self, force=False, recurse=True, variable=None, checkEach=False, index=-1, **kwargs):
super().writeBlocks(
force=force,
recurse=recurse,
variable=variable,
checkEach=checkEach,
index=index,
**kwargs,
)
self.DeviceUpdate()
Here, DeviceUpdate is a real hardware command register. The write to
0x3FC tells the ADC to latch the configuration values that were just
written through the normal block path. A closely related pattern is an
ApplyConfig bit or pulse in clocking and timing devices.
Example: Wrap Reads With A Control Register
Another common pattern is to bracket reads with a hardware control action when the live status registers would otherwise change while the read sequence is in progress:
class MyReader(pyrogue.Device):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add(pyrogue.RemoteCommand(
name='FreezeDebug',
description='Freeze debug snapshot registers during readout',
offset=0xA0,
bitSize=1,
bitOffset=0,
base=pyrogue.UInt,
function=pyrogue.RemoteCommand.touch,
))
def readBlocks(self, *, recurse=True, variable=None, checkEach=False, index=-1, **kwargs):
self.FreezeDebug(1)
try:
super().readBlocks(
recurse=recurse,
variable=variable,
checkEach=checkEach,
index=index,
**kwargs,
)
finally:
self.FreezeDebug(0)
In this pattern, FreezeDebug controls a hardware snapshot or hold function.
The assertion at the start of readBlocks freezes a bank of debug registers
so the read transactions see one consistent sample of the hardware state. The
final write releases the freeze after the read requests have been issued.
Example: Custom Child-Device Ordering
Sometimes the reason to override a block method is not a local register strobe, but the ordering of child-Device configuration. The default recursive behavior visits child Devices in add order. That is often correct, but some hardware trees need a stricter sequence.
One real pattern is a mixed-clocking and data-converter system where the clock generator must be configured first, some DAC setup must complete before a JESD initialization step runs, and the ADC devices should only be configured after that intermediate hardware procedure:
class MyCarrier(pyrogue.Device):
def writeBlocks(self, force=False, recurse=True, variable=None, checkEach=False, index=-1, **kwargs):
super().writeBlocks(
force=force,
recurse=False,
variable=variable,
checkEach=checkEach,
index=index,
**kwargs,
)
self.Clock.writeBlocks(force=force, recurse=True, variable=variable, checkEach=checkEach, index=index, **kwargs)
self.DacA.writeBlocks(force=force, recurse=True, variable=variable, checkEach=checkEach, index=index, **kwargs)
self.DacB.writeBlocks(force=force, recurse=True, variable=variable, checkEach=checkEach, index=index, **kwargs)
self.InitJesdLinks()
self.AdcA.writeBlocks(force=force, recurse=True, variable=variable, checkEach=checkEach, index=index, **kwargs)
self.AdcB.writeBlocks(force=force, recurse=True, variable=variable, checkEach=checkEach, index=index, **kwargs)
self.checkBlocks(recurse=True)
In that pattern, the override is intentionally replacing the default child recursion order. The important part is to make that decision explicit: preserve the normal parameter behavior, disable the inherited recursion where needed, and then issue child-Device operations in the hardware order that the system requires.
Example: One-Shot Configure Command
Sometimes the cleanest solution is not to override the block methods at all,
but instead to call a composed helper from a Command:
@self.command()
def Configure():
self.writeAndVerifyBlocks(force=True, recurse=True, checkEach=True)
This is a good fit when the special behavior is a named procedure rather than a global change to all future block writes.
Relationship To Blocks
This page is about the Device-level traversal methods. The companion
Blocks page is about what a Block is, how Variables map into Blocks,
and how conversion and transaction layers are separated.
Use the two pages together:
Blocks for what Blocks are.
This page for how Devices traverse and sequence Block operations.