Skip to content

Math co-processor (MECH)

The Mathematical Expression Co-processor Helper (MECH) gives the FPU-less CPU realms a hardware maths engine: IEEE-754 single and double precision, the full libm (transcendentals), integer multiply/divide/bitwise, and vector (SIMD) ops. It is not fabric floating-point IP — it is a thin mailbox in the PL that hands work to the Cortex-A9 PS, whose real hardware FPU already has all of the above essentially for free. Building float IP in the FPGA would reinvent that at fabric cost; MECH borrows the silicon that already exists.

The defining trick: the cost is one round-trip per expression, not per operation. A whole batched expression — or a 32-element vector multiply, or a transcendental — costs about the same as a single add.

  • 6502 (X realm) runs in fabric and reaches the A9 through the mailbox page described below.
  • m68k (T realm) runs on the A9 as a JIT, so 68881/68882 instructions map 1:1 onto the A9’s VFP with no mailbox at all — a shorter path to the same silicon.

The 6502 fills the page and rings a doorbell; work crosses into the fabric, over to the A9, and the results come back into the same page — one round-trip:

6502 · X realm PL fabric · mailbox Cortex-A9 · FreeRTOS IRQ 62 MATH_DONE done 1 Map math page $D5C6.0 = 1 2 Write operands + program slots S0…S255 + op words 3 Strobe EXEC doorbell $D5C7 4 Flush dirty lines → chunk the DDR mailbox, then IRQ 5 Worker runs program on the VFP + libm 6 Write results + STATUS poke MATH_DONE 7 Reload result span raise done 8 Poll done, read results $D5C7.0

An 8 KB BRAM math page is overlaid on the CPU’s view of the $4000-$5FFF aperture by a single register flip ($D5C6.0) — no copy, so entering and leaving the page costs nothing in a hot loop. The screen bank underneath is untouched and an in-flight video page-flip carries on concurrently. On EXEC, the PL flushes only the page’s dirty 64-byte lines to a per-task DDR chunk (the mailbox), raises an interrupt, and a top-priority A9 worker task interprets the program on the VFP and writes the results back into the chunk; the PL reloads the result span into the page and raises done.

The full register list ($D5C6$D5CC), the 8 KB page ABI, the op encoding, and the A9-side GP0 block live in the register map and memory map.

The protocol is deliberately tiny — five steps, no driver:

  1. Map the page: set $D5C6.0 = 1.
  2. Store operands into the slot file (S0..S255, 8 bytes each, LSB-first — the layout matches both the 6502 and the A9, so a POKE of the raw bytes is the operand).
  3. Store the program — a list of 4-byte op words.
  4. Strobe the doorbell: write the op count, then write $D5C7 (EXEC).
  5. Poll $D5C7.0 (done), then read the result slots.

Each op word is 3-address form — dst = op(src1, src2) — and a result left in a slot feeds a later op without leaving the A9. So a compound expression is a single program and a single doorbell:

; y = a·x² + b·x + c (S0=a S1=b S2=c S3=x, Horner form)
MUL S0,S3 -> S4
ADD S4,S1 -> S4
MUL S4,S3 -> S4
ADD S4,S2 -> S4 ; y in S4 — four ops, ONE doorbell, one result read

The op set covers + − × ÷, neg/abs/sqrt/min/max/cmp/rem, the libm transcendentals (sin cos tan asin acos atan atan2 exp log log10 pow floor ceil round trunc), int↔float↔double conversions, and integer and/or/xor/not/shl/shr/sar.

A vector op is one op-word pair: the scalar op plus a lane-geometry word (lane count and signed per-operand strides in elements). That makes MECH shine on array work:

  • A 32-element multiply is one 8-byte op pair instead of 32 scalar ops.
  • A 4×4 matrix multiply is 16 VDOTs (row stride 1, column stride 4) instead of 112 scalar ops.
  • Stride-0 on a source broadcasts it — free scale / axpy forms via vmla.

Strides make matrix columns, interleaved buffers and reversals addressable with no CPU-side reshuffling. Reductions (vdot, vsum) collapse a whole vector into a single slot in one op.

The op stream is exactly what a compiler back-end emits, so this is mostly invisible in xtc: a float or integer expression tree lowers to 3-address ops with the register allocator targeting the slot file, array expressions lower to vector ops, and all that is left in the generated code is “read the result slot.” The Math standard library routes through MECH.

Reusable programs (define once, call by id)

Section titled “Reusable programs (define once, call by id)”

An op-word program can be uploaded inline on every doorbell (the default, id 0), or registered once under an id and invoked by reference. The define / call / free commands ride the op stream itself as control ops — there are no extra registers or header fields:

Control opEffect
DEF <id>ENDcapture the enclosed op words and store them under id
CALL <id>run stored program id against the current slots
UNDEF <id>free a stored program

The id space is signed:

  • 0 — inline: the op words sitting in the page run as-is (what the examples above do).
  • > 0 — a user program: an interpreted op-word stream held in a per-task table.
  • < 0 — a native-C builtin (below).

Because CALL is itself just an op word, a stored program can call another, so programs compose (nestable, depth ≤ 8).

Why it matters. Inline upload re-POKEs the whole (up to 4 KB) program on every call — thousands of 6502 cycles for a kernel you run in a loop. Register it once and each later call is a single CALL op word plus fresh operand slots; EXEC then flushes only the operand lines, not the program. That is the decisive win for tight loops of one kernel — matmul, FIR / VMLA, Horner:

first use: DEF 5 …kernel ops… END CALL 5 ; define + run, one doorbell
thereafter: (set input slots) CALL 5 ; one op word, nothing restaged

Negative ids are reserved for native-C kernels — the heavy forms it is not worth expressing as op words. Each reads a small i32 parameter header plus its data from a base slot passed in the CALL. The user-program path above is the general mechanism; these are the fast native shortcuts reachable through the same CALL:

idKernelShape
MATMULC = A·BM×K · K×N
FFTcomplex FFT, in placeN points, N ≤ 128
CONV1-D convolutionsignal L, kernel K
CROSS3-vector cross producta × b
QROOTSquadratic rootsa·x² + b·x + c, f64

The ~23 µs is a fixed per-doorbell floor, dominated by the FreeRTOS round-trip (interrupt latency + notify + context-switch into the FPU worker and back) — not the flush or the maths, which are sub-µs and nanoseconds respectively. A transcendental therefore costs the same wall-clock as an add, and the floor is flat across the whole batch.

Two consequences drive every “should I offload this?” decision:

Batch aggressively. Because the floor is per-doorbell, packing many op words into one EXEC amortises the ~23 µs toward zero-per-op. One big program always beats many small ones.

The break-even is speed-dependent. The 23 µs is real wall-clock (the latency counter at $D5C9 runs on the raw 100 MHz fabric clock, so it is turbo-independent). Against the CPU it is therefore worth different amounts depending on how fast the 6502 is clocked:

CPU speedMECH’s 23 µs floor ≈ this much 6502 workInline beats MECH only for…
1× (1.79 MHz)~42 cyclesa bare byte / 16-bit add. Any multiply, divide or float is hundreds-to-thousands of cycles → offload wins.
56× turbo (top tier)~2300 cyclescheap integer work — adds, a single narrow multiply. A 32-bit divide (~2–3k cyc), a short expression like y = x / b * c (~4k cyc), or any float op still favour MECH.

Note that the turbo case does not demand “heavy batches”: 6502 software multiply and especially divide run into thousands of cycles, so even a two-op integer y = x / b * c (~4k cycles of software work) stays ahead of the ~2.3k-cycle floor even at the 56× top turbo tier — and well past it. Vectors, matrices and transcendentals are simply where MECH wins by the widest margin, not the threshold for winning at all.

Because the break-even moves with the turbo multiplier, the xtc cost model takes the target CLOCK_MULT as an input when deciding inline-vs-offload.

  • Any float op, at any CPU speed — software float is hundreds to tens-of-thousands of cycles per operation; the flat ~23 µs beats it outright, even a single op at 1×. Transcendentals (sin/cos/exp/pow) win hardest — tens of thousands of cycles in software, the same flat floor here.
  • Integer divide, and short integer expressions — a 6502 32-bit divide is ~2–3k cycles and a multiply ~1–2k, so even a two-op y = x / b * c (~4k cycles) beats inline right through the top turbo tier. No batch required.
  • Compound scalar expressions — polynomials, Horner evaluation: a whole chain for one round-trip.
  • Double precision — free on the A9’s FPU, brutal in 6502 software.
  • Vectors and matrices — one op pair per vector; where MECH wins by the widest margin.
  • Cheap scalar integer work under turbo — a single add, or a single narrow multiply, is under the ~2.3k-cycle floor at 56×, so the fast CPU does it inline quicker. At 1× even those win; the penalty is a turbo effect, and it does not extend to divides, multi-op expressions, or floats.
  • A trivial op at any speed — a single byte / 16-bit integer add is below even the 1× floor; never worth a round-trip.
  • Bulk DSP over very large arrays — the page is an 8 KB window, not a streaming interface. For big arrays, hand the A9 the whole array (it is all DDR) rather than paging it through MECH.
  • Uploading a kernel inline every call — re-POKEing a multi-KB program each doorbell burns thousands of 6502 cycles. Not a real limit, just don’t do it: register the kernel once as a stored program and each call becomes a single CALL.