AXI-MM Command Queue
The command queue moves the control plane into memory: instead of the host pushing each command
down a dedicated stream, it appends to a ring buffer in shared DRAM and the accelerator dequeues over
its ordinary m_axi master. Control and data share one memory bundle.
AXIMMQueue is not a new endpoint — it is a thin proxy over an existing
MMIFMaster. Producer and consumer each bind one to the same region and
talk through write / get.
This is not the pattern to reach for first. A queue in memory gives the consumer no way to be told that the head moved, so it must poll — and that polling costs real bandwidth, which is why
poll_untilexists to charge for it. Prefer a stream, or the reverse channels when you need a producer to know about room or outcome without polling for it.It is documented because it is used:
examples/vmacis built on it and it has its own codegen lowering. If you are readingvmac.py, this is the page. If you are choosing an interface for new work, start elsewhere.
What the queue is
A single-producer / single-consumer ring buffer laid out in memory by
AXIMMQueueLayout. The first four words are control; the slots follow:
word 0 head consumer-owned read index
word 1 tail producer-owned write index
word 2 capacity number of slots (informational)
word 3 reserved (room for status/flags)
─────────────────────────────────────────────
data capacity × elem_words the command slots
AXIMMQueueLayout(base_addr, capacity, elem_words, mem_bw) computes the byte
addresses — head_addr, tail_addr, capacity_addr, data_base, and
slot_addr(idx) — from the base address and the memory word width. The ring is
empty when head == tail and full when (tail + 1) % capacity == head, so
the usable depth is capacity - 1 (one slot distinguishes full from empty).
Contrast this with the stream command path of the shared-memory histogram example, where the command rides a dedicated AXI4-Stream: there the control channel is a separate port; here it is just another region of the same memory, and ordering between producer and consumer is mediated entirely by the two pointers.
Operations
A queue is created from a bound master and a layout:
queue = AXIMMQueue(master=mm_master, layout=AXIMMQueueLayout(base, cap, elem_words, mem_bw))
reset()— zeroheadandtail, recordcapacityin word 2. Called once by whichever side owns setup.write(data, element_type=None)(producer / host) — enqueue, blocking until the whole batch is in. Withelement_typegiven,datais serialized through the schema; withelement_type=None,datais a raw word array. It advancestailby the number of slots written.get(schema_type=None, count=None)(consumer) — dequeue. The typed single-element form,get(schema_type), reads one slot, deserializes it into one schema instance, and advanceshead. This is the form the accelerator uses in its run loop, and the one that lowers to hardware.
Pointer advance wraps modulo capacity — head = (head + 1) % capacity. When
capacity is a power of two (the synthesizable requirement), that modulo is a
bit-mask & (capacity - 1), which is why the layout enforces a power-of-two
capacity on the hardware path.
A free-running consumer is just a loop over get:
while True:
cmd = yield from queue.get(Cmd) # one command, deserialized
if cmd.op == OpCode.end: # sentinel ends the stream
return
... # act on cmd
Timing model
The queue inherits the loosely-timed model of its underlying m_axi master
(see MM Interfaces and Polling Overhead). A get
reads (head, tail) and, when non-empty, one data slot — each a timed transaction
on the shared bus.
Two cases need the polling model:
- Empty ring (
head == tail). The consumer must wait for the producer to advancetail. Rather than step every cycle, it uses thepoll_untilloosely-timed model: a bandwidth-steal (ov) derating on the shared bus plus a deterministic discovery-latency delay. The default poll interval is configurable per queue (poll_interval, in cycles). - Full ring. The producer’s
writeblocks (host backpressure) until the consumer frees a slot by advancinghead.
Because the pointers and the data live in the same memory, occupancy and bus
contention both fall out of the standard interconnect model — the queue adds no new
timing primitive, only a new use of m_axi plus poll_until.
Vehicles
examples/interface/aximm_queue_demo.py— the minimal producer/consumer demo over oneAXIMMCrossBarIF.- The VMAC example — the worked use: a host enqueues
VmacCmds and a free-running accelerator dequeues and executes them.
How it lowers
An AXIMMQueue is a protocol over an MMIFMaster, not a channel of its own: what lowers is the
m_axi port underneath it, plus a hand-written body that implements the ring discipline.
- HLS — Endpoint interfaces for the port; the ring itself is not generated.
- Writing the body — Memory command queue, the
queue_getdequeue. This is the advanced hook case: a hand-written body that is the synthesizable half of a transport interface. - BFM / XSI — whatever the underlying
maxi_read/maxi_writeport gets; the queue has no dual of its own because it is not a port.
See also
- MM Interfaces — the
MMIFMasterthe queue is built on. - Polling Overhead — the
poll_untilmodel the empty-ring wait uses. - Custom Hooks → Memory command queue — the
synthesizable side:
AXIMMQueue.getlowered to the ring-dequeue kernel hook.