A Stackmat tournament-style 6-digit LED timer display for speedcubing. It listens to a Gen4/Gen5 Stackmat timer over a serial line and shows the solve time in the same style as the official SpeedStacks tournament display: dim idle 0.000, full brightness when you are on the pads, a blinking colon while the timer runs, and three blinks when a solve ends.
Hardware is built around an STM32F030F4P6 driving six large common-anode 7-segment digits (74HC595 + ULN2803 per digit, daisy-chained). A CH340 programming header lets you flash firmware without disconnecting the Stackmat cable.
- 6 static digits — no multiplexing; one shift register + Darlington driver per digit
- Stackmat Gen4/Gen5 — 1200 baud, 10-byte packets, 6 time digits
- Tournament-style UI — dim idle, bright active, colon blink while running, 3× blink at stop
- No inspection mode — matches typical tournament display use
- Auto-off — blanks after ~5 minutes without Stackmat packets
- Exposed Programming Pins — J1 header wired to the STM32 ROM UART bootloader pins
Cube-Timer-Display/
├── Schematics/ KiCad schematics
│ ├── Cube-Timer-Display.kicad_sch
│ ├── digit.kicad_sch
│ └── digit_colon.kicad_sch digit 1 (minutes + colon)
├── firmware/ STM32 firmware (PlatformIO)
│ ├── src/ Application source
│ ├── build/ Pre-built binaries
│ │ └── cube_timer_display.bin
│ ├── build.sh One-command rebuild
│ └── platformio.ini
└── tools/
└── extract_segment_map.py Helper for 595 segment wiring
| Block | Parts | Role |
|---|---|---|
| MCU | STM32F030F4P6 (TSSOP-20) | Display + Stackmat decode |
| Display | 6× 57 mm CA 7-segment + 74HC595 + ULN2803 | Daisy-chained shift registers |
| Input | LM393 comparator | Stackmat serial into STACKMAT_RX |
| Power | MT3608 boost (~10.5 V +VLED) |
Digit anodes |
| Program | CH340 (external pogo programmer) | UART flash via J1 Programming Pins |
Digit chain (shift order): data enters digit 1 (minutes), exits digit 6 (milliseconds). The first byte you shift out lands in digit 6.
Digit 1 uses digit_colon.kicad_sch: the QH bit drives colon LEDs instead of the DP-Decimal Point. Digit 2-6 use QH for the decimal point after the seconds digit.
| Signal | Pin | Function |
|---|---|---|
SER_D1 |
PA5 | Shift data into digit 1 |
SRCLK |
PA7 | Shift clock |
RCLK |
PA4 | Register latch |
nOE |
PA6 | Display enable / PWM dimming (active LOW) |
STACKMAT_RX |
PA3 | Stackmat serial RX (USART1, 1200 baud) |
TX_PROGRAM |
PA9 | J1 → CH340 RX (USART1 TX, bootloader) |
RX_PROGRAM |
PA10 | J1 → CH340 TX (USART1 RX, bootloader) |
BOOT_PROGRAM |
BOOT0 | J1 boot mode |
RST_PROGRAM |
NRST | J1 reset |
PA3 and PA4 are not on the programming header. J1 uses PA9/PA10, which is exactly what the STM32F030 factory ROM bootloader expects.
| State | Display |
|---|---|
| No Stackmat / timeout | Blank (auto-off after ~5 min) |
| Idle on pads | 0.000 dimmed |
| Hands on / ready / stopped time | Full brightness |
| Running | Time updates + colon blinks (~500 ms) |
| Just stopped | 3 blinks at medium speed, then hold time |
Configuration lives in firmware/src/config.h (brightness, timing, segment map, Stackmat invert, segment test on boot).
- PlatformIO (CLI or IDE extension)
Cursor / VS Code: install the PlatformIO IDE extension when prompted (see .vscode/extensions.json).
Flash-ready file:
firmware/build/cube_timer_display.bin
cd firmware
./build.shOr:
cd firmware
pio run -e cube_timer_displayOutput is copied to firmware/build/ and also under firmware/.pio/build/cube_timer_display/.
Typical size: ~16 KB flash (97% of 16 KB part — very little room left for big new features).
You do not need an ST-Link for normal updates if you use the J1 PROGRAM_PINS header with a CH340 programmer (pogo pins + BOOT0/RST circuit).
| J1 pin | Signal | Connect to |
|---|---|---|
| 1 | +3V3 | 3.3 V (optional if the board is already powered) |
| 2 | GND | GND |
| 3 | RX_PROGRAM (PA10) |
CH340 TX |
| 4 | TX_PROGRAM (PA9) |
CH340 RX |
| 5 | BOOT_PROGRAM |
BOOT0 — high to enter bootloader |
| 6 | RST_PROGRAM |
NRST — reset pulse |
UART is crossed: programmer TX goes to MCU RX (PA10), programmer RX goes to MCU TX (PA9).
The MCU nets have fixed pull resistors — idle levels matter when the programmer is connected:
| Part | Net | Resistor | Idle when J1 pin is not driven |
|---|---|---|---|
| R1 | NRST |
10 kΩ to +3V3 | NRST high (normal run) |
| R2 | BOOT0 |
10 kΩ to GND | BOOT0 low (boot from flash) |
- J1 pin 6 open → R1 holds NRST high.
- J1 pin 5 open → R2 holds BOOT0 low.
- To enter the ROM bootloader you must actively drive BOOT0 high (strong enough to overcome R2), then pulse NRST low and release.
If CH340 RTS/DTR are wired to your pogo BOOT_PROGRAM / RST_PROGRAM lines, set their idle levels so they do not fight these resistors when you are not flashing.
Use STM32CubeProgrammer → UART mode.
- Set UART options in CubeProgrammer before Connect (see table below). Use RTS = Low and DTR = High so idle serial lines match “run mode” and do not pull BOOT0 high or NRST low through your programmer.
- On the pogo: drive BOOT0 high (J1 pin 5), pulse reset (J1 pin 6), release reset while BOOT0 stays high.
- Click Connect in CubeProgrammer.
- Download
firmware/build/cube_timer_display.binat0x08000000. - Release BOOT0 (or let R2 pull it low), reset once — firmware runs.
Typical wiring: RTS → BOOT_PROGRAM, DTR → reset transistor → RST_PROGRAM.
| Phase | RTS (BOOT0) | DTR (reset circuit) | Result on MCU |
|---|---|---|---|
| Enter bootloader | High | Low (reset) | BOOT0 high, NRST low |
| Release reset | High | High | BOOT0 high, NRST high → ROM UART bootloader |
| After flash / normal run | Low | High | BOOT0 low (R2), NRST high (R1) → user flash |
Sequence: RTS high, DTR low briefly, then DTR high while RTS stays high before Connect. After programming, set RTS low and DTR high before disconnect.
If your reset transistor is inverted (DTR high = reset active), swap the DTR column.
| Setting | Value |
|---|---|
| Mode | UART |
| Port | Your CH340 COM port |
| USART | USART1 |
| Baudrate | 115200 |
| Parity | Even |
| Data bits | 8 |
| Stop bits | 1 |
| Flow control | Off |
| RTS | Low (idle — lets R2 hold BOOT0 low) |
| DTR | High (idle — lets R1 hold NRST high) |
Use the RTS/DTR row as the safe idle for manual pogo flashing. Only drive RTS high / DTR low when you intentionally enter the bootloader (manually or via your programmer sequence).
CLI equivalent for idle lines before a manual boot entry:
STM32_Programmer_CLI.exe -c port=COM3 uart=115200 parity=even rts=low dtr=high
- Run after download
- RTS Low, DTR High (or BOOT0 released / J1 pin 5 open so R2 pulls low)
- Reset the MCU
- Display firmware should start
With BOOT0 high and a reset into bootloader mode:
stm32flash -b 115200 -m serial \
-w firmware/build/cube_timer_display.bin \
-v -g 0 /dev/ttyUSB0Replace /dev/ttyUSB0 with your CH340 device (COM3 on Windows, etc.).
- BOOT0 must be high during reset to enter the ROM bootloader, not only after the chip is running.
- R2 (10 kΩ) pulls BOOT0 low when J1 pin 5 is open — you need a strong high on
BOOT_PROGRAMto enter the bootloader, not a weak float. - R1 (10 kΩ) pulls NRST high when J1 pin 6 is open — reset is active low (pull NRST low, then release).
- Set CubeProgrammer RTS Low / DTR High at idle so CH340 does not accidentally boot or reset the chip when connecting.
- If connect fails, swap-check TX/RX against the J1 table above.
- After flashing, BOOT0 must be low (RTS low or pin 5 open) otherwise the chip stays in bootloader mode.
- If UART is flaky, tie 3.3 V on J1 pin 1 in addition to board power.
- You can flash with Stackmat and display wiring connected — programming uses PA9/PA10 only.
SWD (PA13/PA14) is not on J1. ST-Link only helps if you add test pads or a debug header:
cd firmware
pio run -e cube_timer_display -t upload- Flash firmware (above).
- Connect Stackmat to the comparator input; power the timer.
- If time/state decode looks wrong, set
STACKMAT_RX_INVERT_DEFAULT = trueinfirmware/src/config.hand rebuild. - If digits show wrong glyphs, set
SEGMENT_TEST_ON_BOOT = trueto cycle segments on boot, then adjustSEG_*inconfig.hif needed (see routed map in that file).
Bits are routed for PCB layout, not A→B→C order:
| 595 output | Segment |
|---|---|
| QA | B |
| QB | A |
| QC | C |
| QD | E |
| QE | D |
| QF | F |
| QG | G |
| QH | DP / colon |
KiCad 8+ sheets in Schematics/:
Cube-Timer-Display.kicad_sch— main sheet (MCU, power, J1, digit instances)digit.kicad_sch— one digit driver (595 → ULN2803 → 7-segment)digit_colon.kicad_sch— digit 1 variant with colon LEDs
To inspect 595→segment labels from the sheets:
python3 tools/extract_segment_map.pySee LICENSE (GPL-3.0).
| Task | Command / path |
|---|---|
| Flash binary | firmware/build/cube_timer_display.bin @ 0x08000000 |
| Rebuild | cd firmware && ./build.sh |
| Flash docs | BOOT0 high + reset → UART 115200 8E1; idle RTS Low, DTR High |
| Config | firmware/src/config.h |
| Pins | firmware/src/board_pins.h |