Demo DMA device¶
The reference custom peripheral lives under examples/demo-dma/
(demo_dma_device.hpp / demo_dma_device.cpp, namespace tero::demo). It is
a DMA-capable, IRQ-generating peripheral in ~110 lines: it DMA-reads 4 bytes,
XORs them against a configurable mask, DMA-writes the result back, and raises an
interrupt on completion or bus error. It exercises every external seam a custom
peripheral touches: MMIO registers, IBusMaster DMA, and IInterruptSource.
Using it¶
What it does¶
- The CPU writes a source address, an XOR mask, and a destination address into MMIO registers.
- The CPU writes
START(bit 0) to the command register. - The device DMA-reads 4 bytes from the source via
ctx_.bus->dma_read. - It XORs each byte against the corresponding byte of the mask in big-endian wire order.
- It DMA-writes the mutated bytes to the destination.
- It sets
DONE(bit 1) and raises an IRQ. - If any bus access fails it sets
ERROR(bit 2) and raises an IRQ instead.
Register map¶
16 bytes at the base (default 0x80000800, an unused APB slot). All registers
are 32-bit.
| Offset | Const | Name | Access | Description |
|---|---|---|---|---|
0x00 |
RegSource |
SOURCE | R/W | Physical address to read from. |
0x04 |
RegXorVal |
XORVAL | R/W | 32-bit XOR mask. |
0x08 |
RegDest |
DEST | R/W | Physical address to write to. |
0x0C |
RegCmd |
CMD | R/W | Command / status register. |
Non-word access policy differs from the GRLIB devices
Unlike the built-in GRLIB peripherals (which return AlignmentError for
sub-word access), this demo returns ErrorCode::BusError for any non-word
access (demo_dma_device.cpp:23). Both are valid choices for a custom
peripheral — the demo just illustrates that the policy is yours to pick.
Command / status register bits¶
| Bit | Const | Name | Access | Behaviour |
|---|---|---|---|---|
| 0 | CmdStart |
START | W | Write 1 → run the DMA transaction (trigger_dma). |
| 1 | CmdDone |
DONE | R / write-1-clear | Set by the device on success. Write 1 to clear; if DONE and ERROR are then both clear, the IRQ is lowered. |
| 2 | CmdError |
ERROR | R / write-1-clear | Set by the device on bus error. Write 1 to clear. |
Reading CMD returns the live status_ word. The START bit is write-only — it
triggers the transfer and is not stored.
Driving it (host or guest)¶
emu.write_physical_u32(PhysAddr{base + 0x00}, 0x40001000); // SOURCE
emu.write_physical_u32(PhysAddr{base + 0x04}, 0x12345678); // XORVAL
emu.write_physical_u32(PhysAddr{base + 0x08}, 0x40001100); // DEST
emu.write_physical_u32(PhysAddr{base + 0x0C}, 1); // START
// ... DONE bit set, IRQ raised; result at DEST = *SOURCE ^ XORVAL ...
emu.write_physical_u32(PhysAddr{base + 0x0C}, 2); // clear DONE → lowers IRQ
Registering it — both forms¶
Form A — declarative PeripheralSpec (preferred)¶
#include "demo_dma_device.hpp"
auto cfg = tero::compose::gr712rc_config();
cfg.peripherals.push_back({
.instance_name = "demo_dma",
.factory = [](const tero::PeripheralContext&)
-> std::unique_ptr<tero::IPeripheral> {
return std::make_unique<tero::demo::DemoDmaDevice>();
},
.irqs = {tero::IrqLine{5}},
});
auto emu = tero::runtime::Emulator::create(std::move(cfg));
(*emu)->initialize();
The Emulator invokes the factory during initialize(), allocates one
IrqBridge for IRQ 5, calls attach() with a fully-wired PeripheralContext,
and maps the MMIO range on the bus.
Form B — add_peripheral sugar (post-initialize)¶
auto emu = tero::runtime::Emulator::create(tero::compose::gr712rc_config());
(*emu)->initialize();
auto dev = std::make_unique<tero::demo::DemoDmaDevice>();
(*emu)->add_peripheral(std::move(dev), tero::IrqLine{5});
add_peripheral builds a single-IRQ, no-chardev, no-connections spec
internally. Prefer Form A for statically-assembled configs; Form B is for tests
and REPL exploration.
Internals / how it's modelled¶
DMA flow¶
trigger_dma() (demo_dma_device.cpp:70) is called when the CPU writes
CmdStart:
std::array<std::byte, sizeof(std::uint32_t)> buf{};
auto read_res = ctx_.bus->dma_read(PhysAddr{source_addr_}, buf); // 1. read
if (!read_res) { status_ |= CmdError; if (ctx_.irq) ctx_.irq->raise(); return; }
for (std::size_t i = 0; i < buf.size(); ++i) { // 2. byte-wise XOR
const auto shift = static_cast<unsigned>((buf.size() - 1U - i) * 8U);
const auto mask_byte = static_cast<std::uint8_t>((xor_val_ >> shift) & 0xFFU);
buf[i] ^= std::byte{mask_byte};
}
auto write_res = ctx_.bus->dma_write(PhysAddr{dest_addr_}, buf); // 3. write back
if (!write_res) { status_ |= CmdError; if (ctx_.irq) ctx_.irq->raise(); return; }
status_ |= CmdDone; // 4. signal done
if (ctx_.irq) ctx_.irq->raise();
A null ctx_.bus is also treated as an error (sets CmdError, raises the IRQ).
Why the byte-wise XOR matters¶
XORing byte-by-byte, with the most-significant byte of xor_val_ aligned to the
byte at addr+0, makes the result identical to what a SPARC ld / xor / st
sequence produces — and identical on little-endian and big-endian hosts. The
original implementation memcpy'd the buffer into a host uint32_t and XORed
the whole word, which gave host-dependent results
(Decision 35).
IRQ semantics¶
The device uses a level-sensitive line via the injected IInterruptSource:
- Success:
DONEset,ERRORclear →raise(). - Failure:
ERRORset,DONEclear →raise(). - Clear: writing 1 to
DONEclears it; if bothDONEandERRORare then clear,lower()is called (demo_dma_device.cpp:55). Writing 1 toERRORclears that bit. - Reset:
reset()zeroes the registers and callslower().
The runtime's IrqBridge translates raise()/lower() into the correct
IrqMP::external_assert/external_clear bit — the device never knows it is
line 5.
Building and running¶
The integration test is built whenever tests are enabled:
Two cases: a happy path (0xCAFEBABE at 0x40001000, mask 0x12345678,
result at 0x40001100 verified as 0xCAFEBABE ^ 0x12345678 with DONE set), and
an error path (source at an unmapped 0xDEAD0000, asserting ERROR set and
DONE clear).
Source files¶
| File | Purpose |
|---|---|
examples/demo-dma/demo_dma_device.hpp |
Class declaration + register constants. |
examples/demo-dma/demo_dma_device.cpp |
Implementation. |
examples/demo-dma/README.md |
One-page summary (spec + add_peripheral snippets). |
tests/integration/test_demo_dma_device.cpp |
End-to-end integration tests. |
See also¶
- Custom-peripheral walkthrough — the step-by-step guide this example illustrates.
- Peripheral system § DMA bus-master wiring.
examples/custom-board/main.cpp— multi-IRQ +ISignalPortpeer wiring.