Skip to content

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

  1. The CPU writes a source address, an XOR mask, and a destination address into MMIO registers.
  2. The CPU writes START (bit 0) to the command register.
  3. The device DMA-reads 4 bytes from the source via ctx_.bus->dma_read.
  4. It XORs each byte against the corresponding byte of the mask in big-endian wire order.
  5. It DMA-writes the mutated bytes to the destination.
  6. It sets DONE (bit 1) and raises an IRQ.
  7. 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: DONE set, ERROR clear → raise().
  • Failure: ERROR set, DONE clear → raise().
  • Clear: writing 1 to DONE clears it; if both DONE and ERROR are then clear, lower() is called (demo_dma_device.cpp:55). Writing 1 to ERROR clears that bit.
  • Reset: reset() zeroes the registers and calls lower().

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

cmake -S . -B build -G Ninja -DTERO_BUILD_EXAMPLES=ON
cmake --build build --target demo_dma_device

The integration test is built whenever tests are enabled:

./build/tests/tero_tests "[integration]" "DemoDmaDevice"

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