License note: this directory is licensed under CC BY-NC-ND 4.0 (noncommercial, no derivative redistribution) — see
LICENSE.mdin this directory. Most of the rest of this repository is MIT; see the rootREADME.md's "License" section for the split.
A 200-clause Tsetlin Machine MNIST digit classifier, running entirely on the hotstate engine (see the repo-level README for what that is), with a plain UART front-end — no extra hardware beyond a Tang Nano 9K/20K or Tang Primer 25K's existing USB-serial bridge.
The inference engine itself (tm_core_v2.v's clause evaluation and vote
accumulators, tm_seq_controller.c's hotstate clause sequencer, and the
trained clause-mask ROMs in gen/) is hand-designed/trained Verilog and
hotc C, not something this README will re-derive — what's documented here
is the UART front-end wired around it and how to build, flash, and talk to
the result.
The design auto-starts inference right after the 98th image byte arrives — no separate trigger, a natural fit for UART's byte-stream model:
- Host sends exactly 98 raw image bytes over UART (no framing needed).
- The design immediately starts inference (no separate trigger).
- Host waits for 1 response byte: the predicted digit (0-9) in the low nibble.
- Repeat — the design loops forever, one image in, one result byte out.
Host (send_image.py)
│ 98 raw bytes (MNIST image, 1 bit/pixel packed to bytes)
▼
uart_rx (hotstate)
│ rx_done/rx_byte, one pulse per byte
▼
tm_uart_loader (hotstate)
│ writes img_waddr/img_wdata/img_wen (98x), then go=1
▼
tm_top (tm_seq_controller + tm_core_v2)
│ 200-clause evaluation + sequential argmax -> done, winner[3:0]
▼
tm_uart_loader
│ tx_data = winner, tx_start (held until tx_busy)
▼
uart_tx (hotstate)
│ 1 byte out
▼
Host
IP_DIR=../../IP
verilator --timing --cc -Wno-fatal -Wno-WIDTHTRUNC --exe --build -j 4 \
-I. -I$IP_DIR -Igen \
sim_main.cpp tm_hw_top_uart_tb.v tm_hw_top_uart.v tm_top.v tm_core_v2.v \
tm_seq_controller_template.v tm_uart_loader_template.v uart_tx_template.v uart_rx_template.v \
$IP_DIR/hotstate.sv $IP_DIR/microcode.sv $IP_DIR/control.sv $IP_DIR/next_address.sv \
$IP_DIR/timer.sv $IP_DIR/variable.sv $IP_DIR/switch.sv $IP_DIR/stack.sv \
--top tm_hw_top_uart_tb
./obj_dir/Vtm_hw_top_uart_tbExpected: [TB] PASS: result=7 (expected 7, matches the reference prediction for this sample).
make -f Makefile.synth_tang9k prog # or Makefile.synth_tang20k / _primer25k
python3 send_image.py --port /dev/ttyUSB1 # all-zero test image
python3 send_image.py --port /dev/ttyUSB1 --image path/to/98-byte-image.bin
python3 send_batch.py --port /dev/ttyUSB1 # all 100 batch_ref.bin samples
python3 draw_digit_uart.py --port /dev/ttyUSB1 # draw a digit, classify interactivelyUse udevadm info -a -n /dev/ttyUSBn | grep bInterfaceNumber to find the
right port if /dev/ttyUSB1 isn't correct on your machine — interface 00
is the JTAG debug interface, interface 01 is the UART.
Verified on real Tang Nano 9K hardware against the full 100-sample
batch_ref.bin batch: 100/100 (100.0%) agreement with both the
reference predictions and the true labels (see verified_bitstreams/ for
that exact build). See "Known limitations" below for one open, rare,
unreproduced result and what's known about it.
BSRAM is dominated by the 200-clause mask ROMs; LUT/DFF usage is mostly the four hotstate machines' control logic (loader + two UART engines + the clause sequencer) plus the clause evaluation datapath itself.
This design builds for the Tang Nano 20K (Makefile.synth_tang20k). It
passes synth_gowin -noalu, and that flag is required for correctness
on this part — it is not a tuning knob. See KNOWN_ISSUES.md for the
full story (a yosys ALU-carry-cell miscompile on GW2A-18C, corroborated
upstream at YosysHQ/apicula#514, not specific to this design). Verify
after any change to the sources, to yosys, or to the constraints:
make -f Makefile.synth_tang20k verify # build, flash, run all 100 samplesand require 100/100.
- Batch-test, don't single-image-test, when judging correctness. An
all-zero image written to input_bram[0]on every iteration is indistinguishable from one correctly advancing through the image, so a single all-zero test can pass even with a stuck write address. Usesend_batch.pyagainst real labeled data to actually validate a build.
- See KNOWN_ISSUES.mdfor two hardware/toolchain findings worth knowing about before you build: the Tang Nano 20K-noalurequirement above, and one rare, unreproduced 93/100 batch result on the Tang Nano 9K (root cause not confirmed, most likely a UART link glitch — the protocol has no framing or checksum to guard against one).