
Esp32-wroom timer
AlessandroAU/Chorus32-ESP32LapTimer
This is an ESP32 port of the popular Chorus RF Laptimer

GPT on a $1 chip (MSPM0G3507) - A tiny character-level transformer running on-chip
HardGPT is an open-source printed circuit board by hemeshch, from the hemeshch/HardGPT repository on GitHub. It is a 2-layer board of 118.1 × 105.8 mm with 30 parts on 46 nets, routed with 206 tracks and 0 vias.
It is built around the AMS1117. Its parts include 6 resistors, 6 connectors, 4 diodes, 4 switches and 3 capacitors.
A character-level GPT running entirely on a TI MSPM0G3507 Cortex-M0+ — no host PC, no network, no accelerator. Press Generate and the chip composes fake-Shakespeare one character at a time on a 16x2 LCD, beeping at the start and end. Three buttons, three LEDs, an active buzzer, and a dollar's worth of flash.
The model is a 3-layer transformer (4 heads, 48 embedding dims, block size 48, 65-character vocab) trained on the tiny-shakespeare corpus. Weights are int8 symmetric per-output-channel quantized so the whole network fits in flash, and the KV cache is also int8 to keep SRAM use sane during generation.
HardGPT/
├── main.c firmware entry point — UI, transformer inference loop
├── lcd.h header-only HD44780-via-PCF8574 driver
├── weights.h int8 weights + vocab, generated by training/export
├── hardgpt.syscfg sysconfig project: clocks, GPIO, I2C
├── targetConfigs/ CCS debug configs for the launchpad
├── pcb/ kicad project for the board
├── img/ photos used in this readme
└── training/ offline training + weight export pipelineThe whole firmware lives in a single main.c because the model is small
enough that splitting it would just add ceremony. Walking through it top to
bottom:
A handful of static SRAM arrays hold the activations of one token at a time:
x, x_residual, x_norm — the running embedding vector (length N_EMBD)qkv — the combined query/key/value projection (length 3 * N_EMBD)ffn_hidden — the 4×-wide MLP hidden statelogits — the output distribution over the 65-character vocabularyx_q — int8 scratch buffer used whenever we need to quantize an activationThe KV cache is int8 kv_cache[N_LAYER][BLOCK_SIZE][2 * N_EMBD] with
float kv_scales[...] alongside, so each cached K and V vector keeps its own
quantization scale. Storing it as int8 cuts the cache footprint by ~4× versus
float.
Everything that touches the hot path uses fast, approximate floats:
quantize_vec — symmetric int8 quantization of a vector with one shared
scale, used to feed activations into the int8 matmul.matvec_i8i8_pc — the workhorse. Multiplies an int8 weight matrix (with
per-row scales) against an int8 activation vector (with one scale) and
accumulates into int32, then converts to float at the end. This single
routine handles QKV projection, attention output projection, and both MLP
matmuls.fast_invsqrt — the famous Quake 0x5f3759df trick + one Newton-Raphson
step. Used by both LayerNorm and the attention scaling factor.fast_expf — Schraudolph's bit-hack exp. Wildly inaccurate in absolute
terms but plenty good once softmax normalizes.fast_tanhf — a Padé approximation, ~1 % error in [-3, 3], used inside
GELU.layernorm, gelu, softmax — built on top of the above.Layer is just a struct of pointers into weights.h so the block code can
work with L->attn_qkv_w etc. instead of macro-mangled names.
init_layers() wires layer 0/1/2 once at boot.
block_forward(layer_i, pos) does one transformer block:
x_norm.x_norm, run the QKV matmul, split into q, k, v.k and v and store them at kv_cache[layer_i][pos].q against every cached k_t (dequantizing
on the fly), scale by 1/sqrt(head_dim), softmax, then accumulate the
weighted sum of cached v_t.forward(token_id, pos) adds the token + position embeddings, calls
block_forward for each layer, runs the final LayerNorm, and reuses the
embedding table as the LM head (weight tying) to produce logits.
slide_kv_cache() shifts the cache left by one when generation runs past
BLOCK_SIZE, so the model always sees the most recent BLOCK_SIZE - 1
tokens.
Two sampling modes:
sample_argmax — greedy.sample_temp(temperature) — divide logits by temperature, softmax, draw
from the distribution using a small linear-congruential RNG.The probability of the chosen token is stashed in last_token_prob so the
"confidence" LED can light up when the model is sure.
LcdView holds a 2-row shadow buffer plus the current (row, col). As
characters arrive, lcd_view_putchar writes them straight to the LCD and
mirrors them into the shadow buffer. When the cursor walks off row 1, we
copy row 1 up to row 0, blank row 1, and continue — the result is a smooth
typewriter that scrolls upward indefinitely.
Boot sequence:
SYSCFG_DL_init() brings up clocks/GPIO/I2C from sysconfig.init_layers() wires the layer pointer table.lcd_init() runs the HD44780 wake-up dance and switches it into 4-bit mode.Then the forever loop:
HardGPT v1.0 / Press Generate!. The buttons let you
cycle through seeds (Random, KING, ROMEO, LORD, HENRY) or peek at
the temperature.forward(token, pos)last_token_prob > 0.5pos; slide the KV cache if we hit BLOCK_SIZE| Peripheral | Pin | Purpose |
|---|---|---|
| 16x2 LCD | I2C0 — SDA on PA0, SCL on PA1 | shows generated text, scrolls upward (PCF8574 backpack) |
| Generate btn | PA5 (input, internal pull-up) | start / interrupt generation |
| Temp btn | PA7 (input, internal pull-up) | preview the sampling temperature |
| Seed btn | PA8 (input, internal pull-up) | cycle the seed character / name |
| LED 1 | PA12 (output) | "running" — lit during generation |
| LED 2 | PA17 (output) | toggles once per generated token |
| LED 3 | PA18 (output) | lit when sampled token's probability > 0.5 |
| Buzzer | PA22 (output, NPN switch) | boot beep + start/end-of-generation cue |
| SWD | SWCLK on PA20, SWDIO on PA19 | flashing / debug from CCS |
Pin assignments live in hardgpt.syscfg; the SDK generates ti_msp_dl_config.{c,h}
into Debug/ from it.
The model is trained offline using a trimmed-down fork of Andrej Karpathy's
nanoGPT. Pretty much all of the
training code (model.py, train.py, sample.py, configurator.py, the
data prep scripts, the overall project structure) is his — go give that repo
a star. The training pipeline lives in training/ and is a UV-managed Python
project.
training/
├── pyproject.toml
├── uv.lock
├── nanoGPT/ Karpathy's nanoGPT (trimmed to what we use)
│ ├── model.py GPT model definition
│ ├── train.py training loop
│ ├── sample.py inference / sampling reference
│ ├── configurator.py
│ ├── config/train_micro.py our tiny config (3 layers, 48 dims, ...)
│ ├── data/shakespeare_char/ tiny-shakespeare, char-level tokenizer
│ ├── out-micro/ckpt.pt trained checkpoint
│ ├── LICENSE nanoGPT MIT license
│ └── README.md upstream nanoGPT README
├── export/
│ ├── export_weights.py converts ckpt.pt → weights.h
│ └── weights.h generated header (also copied into the firmware)
└── inference_c/ desktop C reference impl, used to sanity-check
├── main.c the on-chip math before flashing
└── weights.hEnd-to-end flow:
training/nanoGPT/data/shakespeare_char/prepare.py
│
▼ (tokenize)
train.bin / val.bin
│
▼ (uv run python nanoGPT/train.py config/train_micro.py)
training/nanoGPT/out-micro/ckpt.pt
│
▼ (uv run python export/export_weights.py)
training/export/weights.h ── copied to ──▶ HardGPT/weights.h
│
▼ (CCS build + flash)
on-chip generationconfig/train_micro.py sets the architecture used by the firmware
(n_layer=3, n_head=4, n_embd=48, block_size=48, dropout 0.2) and
trains for 30k iterations on shakespeare_char with the AdamW recipe nanoGPT
ships with.
export_weights.py does symmetric int8 per-output-channel quantization of
every weight matrix, keeps LayerNorm gains as float (they're tiny), and emits
a single weights.h containing:
#defines (VOCAB_SIZE, N_LAYER, N_HEAD, N_EMBD,
HEAD_DIM, BLOCK_SIZE)vocab[] lookup tableThat header is exactly what main.c #includes. The on-chip int8 matmul in
matvec_i8i8_pc is the dequantization counterpart to what
export_weights.py does at quantization time.
cd training
uv sync # install torch, numpy, etc.
uv run python nanoGPT/data/shakespeare_char/prepare.py
uv run python nanoGPT/train.py config/train_micro.py
uv run python export/export_weights.py
cp export/weights.h ../weights.h # firmware picks it up on next buildThen build the firmware in CCS (Project → Clean → Build) and flash to the
LP-MSPM0G3507.
nanoGPT) is his work,
used here under its MIT license. See training/nanoGPT/LICENSE.main.c, lcd.h, weights.h packaging, sysconfig, board
bring-up) is original to this project.| Qty | Value | Footprint | References |
|---|---|---|---|
| 3 | 220 ohms | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R3, R4, R5 |
| 3 | LED | LED_THT:LED_D3.0mm | D1, D2, D3 |
| 1 | 0.1uF | Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P2.50mm | C2 |
| 1 | 0.47uF | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C1 |
| 1 | 100k | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R1 |
| 1 | 1N4148 | Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal | D4 |
| 1 | 1k | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R8 |
| 1 | 2N3904 | Package_TO_SOT_THT:TO-92_Inline_Wide | Q1 |
| 1 | 47k | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal | R2 |
| 1 | 47u | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C4 |
| 1 | AMS1117 | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | J5 |
| 1 | Buzzer | Buzzer_Beeper:Buzzer_TDK_PS1240P02BT_D12.2mm_H6.5mm | BZ1 |
| 1 | Conn_01x08 | Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical | J6 |
| 1 | Debug | Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical | J2 |
| 1 | Generate | Button_Switch_THT:SW_PUSH_6mm | SW1 |
| 1 | LCD | Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical | J3 |
| 1 | MSPM0 Breakout | Package_DIP:DIP-32_W15.24mm_Socket | J1 |
| 1 | Mode | Button_Switch_THT:SW_PUSH_6mm | SW3 |
| 1 | Potentiometer | Potentiometer_THT:Potentiometer_Alpha_RD901F-40-00D_Single_Vertical | RV1 |
| 1 | SW_SPDT | Button_Switch_THT:SW_Slide-03_Wuerth-WS-SLTV_10x2.5x6.4_P2.54mm | SW4 |
| 1 | Seed | Button_Switch_THT:SW_PUSH_6mm | SW2 |
| 1 | UART | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J4 |
The 26 most common of 30 parts.
It measures 118.1 × 105.8 mm and has 2 copper layers, 1.6 mm thick.
30 parts, built around the AMS1117. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the hemeshch/HardGPT repository on GitHub, at the commit this page was made from: the board is pcb/hardgpt.kicad_pcb and its schematic pcb/hardgpt.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository states no licence, so its author keeps all rights: ask hemeshch before reusing the design.

AlessandroAU/Chorus32-ESP32LapTimer
This is an ESP32 port of the popular Chorus RF Laptimer




uli/basicengine-pcb
Schematics and PCB layout for the BASIC Engine home computer



MR-DOS/TDR_diploma_thesis
Time domain reflectometer. Includes Hardware, firmware and GUI in Octave. Also a diploma thesis.