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).