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_until exists 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/vmac is built on it and it has its own codegen lowering. If you are reading vmac.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() — zero head and tail, record capacity in word 2. Called once by whichever side owns setup.
  • write(data, element_type=None) (producer / host) — enqueue, blocking until the whole batch is in. With element_type given, data is serialized through the schema; with element_type=None, data is a raw word array. It advances tail by 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 advances head. 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 advance tail. Rather than step every cycle, it uses the poll_until loosely-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 write blocks (host backpressure) until the consumer frees a slot by advancing head.

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


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.

  • HLSEndpoint interfaces for the port; the ring itself is not generated.
  • Writing the bodyMemory command queue, the queue_get dequeue. 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_write port gets; the queue has no dual of its own because it is not a port.

See also