Releases: Keyvast-Technology/kdi
Release list
KDI contract v0.7.0
Contract 0.7, synced from the build repo at 1699470. descriptor.json is the machine-readable promise, KDI-0.7.md the implementer guide, and the vectors archive the cross-language gate.
KDI contract v0.6.0
Contract 0.6, synced from the build repo at 8be7c73. descriptor.json is the machine-readable promise, KDI-0.6.md the implementer guide, and the vectors archive the cross-language gate.
KDI contract v0.5.0
Contract 0.5, synced from the build repo at 015143f. descriptor.json is the machine-readable promise, KDI-0.5.md the implementer guide, and the vectors archive the cross-language gate.
Gateware v0.16.0 (KDI contract 0.5)
Gateware for the Keyvast acquisition instrument, implementing KDI contract 0.5.
| file | keyvast-v0.16.0-kdi0.5.bit |
| implements | KDI contract 0.5 -- see the spec-v0.5.0 release |
gateware_sha |
7467bbf3 |
| size | 2765617 bytes |
| sha256 | b90ddcb43d87cd7fe83ab1adc465f4b8835a070b9a29a238caabf7e75463d053 |
manifest.json beside this image carries the same values for a program to read.
Verify what you flashed
The firmware is already baked into the bitstream; load it over USB3. ReleaseManifest::judge
refuses an image whose bytes do not hash to the value above, and refuses a contract major this
host cannot bind to, before a byte reaches the board:
let m = kdi::ReleaseManifest::parse(&std::fs::read_to_string("manifest.json")?)?;
let dev = kdi::Device::open_usb3_from_manifest("", &std::fs::read("keyvast-v0.16.0-kdi0.5.bit")?, &m)?;
assert_eq!(format!("{:08x}", dev.gateware_sha()), "7467bbf3");A host built against contract 0.5 will bind to this gateware; the handshake checks the
contract major itself and refuses a mismatch rather than misreading frames.
KDI contract v0.4.0
Two independently addressed streams on one device-wide timebase, and KDI's registers moved off addresses the incumbent acquisition host writes for its own purposes.
Assets
| file | what it is |
|---|---|
descriptor.json |
the contract — read this at build time; everything else is support |
schema.json |
JSON Schema (draft 2020-12), to validate a descriptor without our code |
manifest.json |
sha256 of every artifact + contract identity + the conformance result |
kdi-vectors-0.4.0.tar.gz |
the golden vectors, as a set — only meaningful together |
Re-read the vectors
v0.3 published one 64-byte adio_dig vector with rows == 1, where a row-major and a lane-major decoder emit identical bytes — so a decoder transposing rhd_matrix's 35 rows against its lanes passed the whole oracle while emitting plausible data at the wrong channel. That is a failure this project shipped once.
The bundle is now six frames, a streaming case, and 20 negatives across all 11 reject tokens. golden_frame.json inside the archive is the manifest that describes them; the .bin files are not independently interpretable. golden_wide is the one to read hardest: it uses descriptor strides and element widths no current device emits, so a decoder that compiles in today's values is caught here rather than by a future device.
Verification status
The data plane is verified on silicon: ~6,600 probe-runs across sample rates and lane masks, bounded and free-running, zero failures — bounding the failure rate under 0.045% at 95% confidence.
Row order is NOT bench-verified. That check needs a driven input, and with floating inputs both the amplifier rows and the slow aux rows are noise, so the comparison decides nothing. The authority is a golden chip-model simulation that drives known values. See CHANGELOG.md.
KDI v0.3.1 — streams are not co-sampled (clarification, no wire change)
No wire change. The golden vector is byte-identical to v0.3.0 — a conforming host needs no changes. The clarification is the point.
Nothing said streams are independently sampled, so a host could reasonably assume they were and pair samples across streams by index. That failure is silent: the pairing is simply wrong by a per-run offset.
New normative invariant not_co_sampled: a host MUST locate a stream's samples by that stream's own timestamps and MUST NOT pair across streams by index or arrival order. adio_dig takes its first sample at the epoch origin; rhd_matrix takes its first at the engine's next timestep boundary — so the offset between two streams' first samples is uniform in [0, one frame period of the slower stream) and differs on every run. It does not drift.
That last claim is measured, not asserted: four independent 2-minute runs on hardware gave +0.31, −0.34, −0.61, +0.73 frame periods — mean +0.02, sign alternating, i.e. noise around zero. Two independent 100 MHz oscillators at even 10 ppm would show ~12 frame periods, consistently signed. Cross-stream alignment is exact integer subtraction of timestamps, which is what the shared timebase is for.
loss_oracle is now scoped within one stream. Applying it across streams was always wrong and the text did not say so.
KDI v0.3.0 — breaking: control registers moved off the addresses Rhythm writes
Breaking, and act on the register move first. KDI's control WireIns were sitting on registers the Rhythm host writes — 0x15 is WireInTtlOut, 0x16–0x1d are WireInDacSource1..8. Verified against the two hosts that actually speak this board's protocol: one writes arbitrary TTL state to 0x15, the other zeroes the whole word. A host doing nothing unusual could silently stop both KDI streams — and now that run_samples starts acquisition, the same write starts the engine.
| register | was | now |
|---|---|---|
run_digital |
0x15.0 |
0x11.0 |
run_samples |
0x15.1 |
0x11.1 |
lanes_samples |
0x16 |
0x12 |
burst_digital |
0x17 |
0x13[15:0] |
burst_samples |
0x18 |
0x13[31:16] |
0x0f–0x13 is the only gap in that map and 0x0f/0x10 are taken, so three words hold four registers and the burst bounds share 0x13.
Two things every host must change. Bindings can now name a field (wirein:0x13[31:16]) — a parser that only understands 0xNN and 0xNN.b will fail on this descriptor. And since two registers share each word, every write must be masked; an unmasked write to one field clears its neighbour, which was a real shipped bug against the old shared 0x15.
run_samples now acquires. It previously gated emission only — with the engine stopped the stream produced nothing. Measured on silicon: 0 words resident before, 1024 words and decodable frames after, legacy pipe staying empty. Still one shared engine and one row schedule, so two streams cannot run at different rates.
Wire format unchanged apart from contract_rev 1→3 at byte 0x1c and its CRC — exactly five bytes in the golden vector. A format-2 decoder needs no changes.
0.2 was staged and never released, because its register layout is the colliding one above. Everything it carried is in 0.3: the first_of_run semantics change (successive announcements must have strictly increasing timestamps, not timestamp 0), per-stream run/lane registers, bounded capture, per-stream status words, and the corrected rhd_matrix cadence.
conformance in the manifest is target: software-reference — the contract is self-consistent and the reference host obeys it, not a claim that any gateware conforms. The gateware verified on hardware for this revision is e8f7f60e: acceptance campaign 6/6, bounded capture exact on both streams, RHX unregressed.
KDI v0.1.1 — correction: rhd_matrix row order was off by one
If you built a decoder against v0.1.0, fix this first.
v0.1.0 published "rows 0..31 are amplifier channels in ascending order, rows 32..34 are the chip's three aux results". That is wrong in the most dangerous way available: a host implementing it reads channel n at row n and gets channel n−1, with row 0 pure garbage — plausible-looking neural data at the wrong index, with a valid CRC and correct lengths.
The RHD SPI returns a command's result during the next command, so row k carries the capture from command k−1:
| row | content |
|---|---|
| 0 | the previous timestep's aux2 (aux_adc) — it lags |
| 1..32 | amplifier channels 0..31, ascending |
| 33, 34 | this timestep's aux0 (temp) and aux1 (supply) |
This is the hardware, not a choice. An earlier revision shipped the un-rotated version once and a real RHD2132's ROM/ID answers missed their expected slots at every MISO delay; static-MISO simulations cannot see it.
No wire bytes change — only the published meaning of the rows. format stays 2, and the golden vector is unaffected (it carries a digital section, not an rhd_matrix one), so a decoder's framing, CRC and negative-case handling are all still correct as shipped.
Still not emitted by gateware: no bitstream produces format 2 and clean_frame stays clear.
Conformance 12/12. Source: keyvast-fpga@869bb93.
KDI v0.1.0 — frame format 2 (self-describing container)
Frame format 2. decode needs no descriptor: every length, lane identity, element width and cadence is on the wire.
⚠️ Not yet emitted by gateware. No shipping bitstream produces format 2, and the device leaves theclean_framecapability bit clear — so a host that requires the clean frame will correctly refuse to bind to today's hardware. The format is published ahead of the emitter, on purpose, so that it is fixed before anything binds to it. The control plane in this descriptor is hardware-verified.
The container
32 B header magic | format=2 | flags | timestamp u48 | frame_words | layout |
hdr_words | n_sections | run_id | contract_rev | desc_words
descriptors n × 16 B, stride taken FROM THE WIRE so it can grow again
lane ids Σ n_lanes × u16 · bodies element-major (1/16/32/64-bit) · CRC-32 trailer
magicis a resync anchor only, never a validity test. Validity is CRC + declared length + timestamp continuity. Host-side that is one call:zlib.crc32(frame) == 0x2144DF1C.- A new module type is a new
kind, skipped bysection_words— additive, no decoder change, no version bump. An unknown format is skippable too, byframe_words:magic,formatandframe_wordsare frozen at their offsets for every future format. - Cadence is an exact rational (
tick_num/tick_den; 30 kHz =10000/3). Rate is never normative in this contract — the timebase is, so a hardware rate change needs no contract change. - One shared 48-bit timebase at 10 ns, sampled per frame, epoch per run paired with
run_id. Aligning two streams is exact integer subtraction — no sync channel, no per-stream linear fit. The frame-to-frame delta jitters (3333/3334 at 30 kHz) and there is deliberately no constant-delta invariant, because at 30 kS/s one is not implementable. - Digital input is bit-packed: 16 lines cost one word, every bit named by its own lane id — no host-side slot→bit formula.
to_deviceis reserved (declared, unimplemented) — the container is direction-agnostic.
Testable in both directions
vectors/golden_frame.json now ships 7 negative frames alongside the good one, each with the normative reason token it must be rejected with (crc_err, reserved_bits, tick_sane, timestamp_top16, desc_words, section_words). A decoder can be proven to reject what it must, not merely accept what it should.
Known limitation, published deliberately
Digital-in levels are sampled at the end of the frame that carries them, so they trail their own timestamp by up to one frame period, and the offset varies with the enabled lane count. Derived from RTL, not bench-measured. A future emitter latches at frame start and this note tightens.
Breaking
Format 1 frames no longer decode. Safe now because nothing has bound. kdi: stays 0.1 — it is the command-set version, and no command changed.
Conformance: 12/12 pass. Source: keyvast-fpga@d567775.
KDI v0.0.2 — typed command protocol (hardware-verified)
The typed command protocol is live on hardware: the device answers framed request/response commands over the control channel, verified on a real XEM7310 (10/10 cases including every negative path — range-checked args, the presence gate, the service tier gate, unknown commands).
Published commands: sys.hello, power.status, power.up, adio.mode, adio.adc — all implemented in the shipping firmware. Unimplemented commands were removed rather than published.
kdi stays 0.1 (the register/frame surface is unchanged). See CHANGELOG.md for the full list and the pre-1.0 caveat. Verify downloads against the sha256 sums in manifest.json (conformance: pass 11/11).