Demo DMA device¶
The reference custom peripheral lives under examples/demo-dma/.
It performs a DMA read, XORs the payload against a configurable mask, writes
the result back, and raises an IRQ. It is fully wired for MMIO, DMA, and
interrupts — about 200 lines of C++.
What it does¶
- The CPU writes a source address, an XOR mask, and a destination address into MMIO registers.
- The CPU writes
STARTto the command register. - The device DMA-reads 4 bytes from the source address via
IBusMaster. - 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 address.
- It sets the
DONEflag in the status register and raises an IRQ. - If any bus access fails, it sets
ERRORand raises an IRQ.
Register map¶
The device occupies 16 bytes at its base address (default 0x80000800). All
registers are 32-bit; non-word accesses return AlignmentError.
| Offset | Name | Access | Description |
|---|---|---|---|
0x00 |
SOURCE | R/W | Physical address to read from |
0x04 |
XORVAL | R/W | 32-bit XOR mask |
0x08 |
DEST | R/W | Physical address to write to |
0x0C |
CMD | R/W | Command / status register |
Command / status register bits¶
| Bit | Name | Access | Description |
|---|---|---|---|
| 0 | START | W1S | Write 1 to trigger the DMA transaction |
| 1 | DONE | R/W1C | Set by hardware on completion; write 1 to clear |
| 2 | ERROR | R/W1C | Set by hardware on bus error; write 1 to clear |
Writing 1 to DONE or ERROR clears that bit. When both are clear, the
IRQ line is lowered.
DMA flow in detail¶
The transaction is driven by trigger_dma(), called when the CPU writes
kCmdStart:
std::array<std::byte, sizeof(std::uint32_t)> buf{};
// 1. Read from guest memory
auto read_res = ctx_.bus->dma_read(PhysAddr{source_addr_}, buf);
if (!read_res) { set_error(); return; }
// 2. Endianness-safe byte-wise XOR
for (std::size_t i = 0; i < buf.size(); ++i) {
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};
}
// 3. Write back to guest memory
auto write_res = ctx_.bus->dma_write(PhysAddr{dest_addr_}, buf);
if (!write_res) { set_error(); return; }
// 4. Signal completion
status_ |= kCmdDone;
if (ctx_.irq) ctx_.irq->raise();
Why the byte-wise XOR matters¶
The original implementation memcpy'd the 4-byte buffer into a host
uint32_t and XORed the whole word. That produced different results on
little-endian vs big-endian hosts because the in-memory byte order of the
uint32_t depends on the host. The corrected version operates on each byte
individually, aligning the most-significant byte of xor_val_ with the byte
at addr+0 (the big-endian wire order). This makes the result identical to
what a SPARC ld / xor / st sequence would produce
(Decision 35).
IRQ semantics¶
The device uses a level-sensitive interrupt line:
- Success:
DONEset,ERRORclear →raise(). - Failure:
ERRORset,DONEclear →raise(). - Reset:
lower(). - Clear: Writing
1toDONEclears it. If bothDONEandERRORare now clear,lower()is called.
The runtime's IrqBridge translates raise() / lower() into the correct
IrqMP::external_assert / external_clear bit.
Building and running the example¶
The demo device is not built by default. Enable the example target:
The integration test that exercises it is always built when tests are enabled:
There are two test cases:
- Happy path: A payload
0xCAFEBABEis placed in RAM at0x40001000, the device is programmed with XOR mask0x12345678, and the result read back from0x40001100is verified as0xCAFEBABE ^ 0x12345678. - Error path: The source address is set to an unmapped location
(
0xDEAD0000). The test asserts thatERRORis set andDONEis not.
Source files¶
| File | Purpose |
|---|---|
examples/demo-dma/demo_dma_device.hpp |
Class declaration and register constants |
examples/demo-dma/demo_dma_device.cpp |
Implementation |
examples/demo-dma/README.md |
One-page summary |
tests/integration/test_demo_dma_device.cpp |
End-to-end integration tests |