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.
Both FPU-less realms benefit
Section titled “Both FPU-less realms benefit”- 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.
How it works
Section titled “How it works”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:
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.
Using it from the 6502
Section titled “Using it from the 6502”The protocol is deliberately tiny — five steps, no driver:
- Map the page: set
$D5C6.0 = 1. - Store operands into the slot file (
S0..S255, 8 bytes each, LSB-first — the layout matches both the 6502 and the A9, so aPOKEof the raw bytes is the operand). - Store the program — a list of 4-byte op words.
- Strobe the doorbell: write the op count, then write
$D5C7(EXEC). - Poll
$D5C7.0(done), then read the result slots.
One expression, one round-trip
Section titled “One expression, one round-trip”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 -> S4ADD S4,S1 -> S4MUL S4,S3 -> S4ADD S4,S2 -> S4 ; y in S4 — four ops, ONE doorbell, one result readThe 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.
Vectors are the bulk-maths win
Section titled “Vectors are the bulk-maths win”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 /
axpyforms viavmla.
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.
From xtc
Section titled “From xtc”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 op | Effect |
|---|---|
DEF <id> … END | capture 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 doorbellthereafter: (set input slots) CALL 5 ; one op word, nothing restagedNative builtins
Section titled “Native builtins”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:
| id | Kernel | Shape |
|---|---|---|
MATMUL | C = A·B | M×K · K×N |
FFT | complex FFT, in place | N points, N ≤ 128 |
CONV | 1-D convolution | signal L, kernel K |
CROSS | 3-vector cross product | a × b |
QROOTS | quadratic roots | a·x² + b·x + c, f64 |
Latency & when it pays off
Section titled “Latency & when it pays off”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 speed | MECH’s 23 µs floor ≈ this much 6502 work | Inline beats MECH only for… |
|---|---|---|
| 1× (1.79 MHz) | ~42 cycles | a bare byte / 16-bit add. Any multiply, divide or float is hundreds-to-thousands of cycles → offload wins. |
| 56× turbo (top tier) | ~2300 cycles | cheap 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.
Where MECH excels
Section titled “Where MECH excels”- 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.
Where it doesn’t
Section titled “Where it doesn’t”- 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 singleCALL.