# blackbox-rs
Board support crate for the **Blackbox** board — STM32H743XI (Cortex-M7 @ 399.36 MHz, rev.Y), built on [Embassy](https://embassy.dev).
One call brings the whole board up at its design clocks and hands you ready-to-use drivers for every on-board peripheral:
- **11 LEDs** — active-high indicators
- **13 buttons** — named, active-low (`Button::Play`, `Button::Rec`, …)
- **4 endless encoders** — dual-wiper, absolute angle via ADC1 (`Knob::Tl`, …)
- **GT9147 touchscreen** — I2C, panel-aligned coordinates
- **320×240 RGB565 display** — LTDC, vblank-synced double buffer, [`embedded-graphics`](https://crates.io/crates/embedded-graphics) draw target, backlight
- **CS42528 audio** — SAI + DMA, 8 DAC outputs / 2 ADC inputs, 48 kHz
The tricky bits (rev.Y cache erratum, the exact SDRAM timings, the SAI OLM topology the HAL can't express, the LTDC pin map with the unrouted R2 line) are all handled inside the crate — see [Hardware](#hardware) below.
The bundled example renders a live debug screen — this is its framebuffer, captured straight off the panel's SDRAM over SWD:

## Quick start
```toml
# Cargo.toml
[dependencies]
blackbox-rs = "0.1"
embassy-executor = { version = "0.7", features = ["arch-cortex-m", "executor-thread"] }
embassy-time = "0.4"
```
```rust
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use blackbox_rs::buttons::Button;
use {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// Clocks, CPU/MPU, SDRAM, every peripheral — plus a 440 Hz tone on the phones.
let mut board = blackbox_rs::init().await;
loop {
if board.buttons.is_pressed(Button::Play) {
defmt::info!("play!");
}
// draw with embedded-graphics on board.display.target(), then present at vblank:
board.display.swap().await;
}
}
```
`blackbox_rs::init()` returns a [`Board`]:
```rust
pub struct Board {
pub display: display::Display,
pub leds: leds::Leds,
pub buttons: buttons::Buttons,
pub knobs: knobs::Knobs,
pub touch: touch::Touch,
pub i2c: I2c<'static, Blocking>, // shared by codec + touch
pub codec_ok: bool,
}
```
## Using the peripherals
```rust
// LEDs — set individually, or light exactly one
board.leds.set(0, true);
board.leds.only(3);
// Buttons — typed, no magic indices
for b in Button::ALL {
if board.buttons.is_pressed(b) {
defmt::info!("{}", b.label());
}
}
// Knobs — absolute angle from the two wipers
let r = board.knobs.read(Knob::Tl);
defmt::info!("TL = {} deg (a={}, b={})", r.angle_deg(), r.a, r.b);
// Touch — panel coordinates, None when idle
if let Some(tp) = board.touch.poll(&mut board.i2c) {
defmt::info!("touch {},{}", tp.x, tp.y);
}
// Display — embedded-graphics on the back buffer, then swap
use embedded_graphics::{prelude::*, pixelcolor::Rgb565, primitives::*};
let mut fb = board.display.target();
Circle::new(Point::new(160, 120), 40)
.into_styled(PrimitiveStyle::with_fill(Rgb565::CYAN))
.draw(&mut fb)
.ok();
board.display.swap().await; // tear-free, paces to ~59 Hz
board.display.set_backlight(20); // percent, clamped to the 35% ceiling
// Audio — route a sine to a stereo output port, or to raw DAC channels
use blackbox_rs::audio::{self, Dac, Output};
audio::play_sine_out(1000, Output::Jack1); // → DAC3/4
audio::play_sine_on(1000, &[Dac::Dac1, Dac::Dac6]); // arbitrary channel set
```
## Controls reference
| `buttons::Button` | enum | `Pads Keys Seqs Song Fx Mix Pset Tools Rec Stop Play Back Info` |
| `knobs::Knob` | enum | `Tl Tr Bl Br` |
| `audio::Dac` | enum | `Dac1`…`Dac8` |
| `audio::Output` | enum | `Phones Jack1 Jack2 Jack3` (stereo pairs → `.left()`/`.right()`/`.channels()`) |
| `audio::AudioIn` | enum | `Adc1 Adc2` |
Output port → DAC mapping: `Phones`=DAC1/2, `Jack1`=DAC3/4, `Jack2`=DAC5/6, `Jack3`=DAC7/8
(assumed — adjust in `audio::Output` if the hardware differs).
Each enum has `::ALL`, `.index()` and `.label()`.
## Prerequisites
```sh
rustup target add thumbv7em-none-eabihf
cargo install probe-rs-tools # provides `probe-rs`
```
A debug probe wired to the board's SWD. The repo's `.cargo/config.toml` sets the target and a
`probe-rs run --chip STM32H743XI` runner, so flashing the example is just:
```sh
cargo run --release # builds, flashes, streams defmt/RTT logs
DEFMT_LOG=debug cargo run --release # more verbose
```
The bundled `src/main.rs` is a live **debug screen** example: every control and its state on
the panel, each LED lit while its button is held, and a 440 Hz tone — a quick bring-up smoke test.
## Notes & safety
- **Backlight is hard-capped at 35%** (`display::MAX_BACKLIGHT_PCT`) — the boost regulator's
thermal limit. `set_backlight` clamps; do not raise without a thermal basis.
- **D-cache is off** by design. SDRAM and the audio buffers are coherent with the LTDC/SAI DMA
with no maintenance; the MPU still marks those regions non-cacheable so enabling D-cache later
is correct (rev.Y erratum ES0392).
- Audio currently renders a sine. The DMA-ISR double-buffer is in place; swap the sine source
for a real render to stream arbitrary audio.
- **Touch** latches its I2C address from the INT (PG12) level at the chip's own power-on:
low → `0x5D` (operational, config loaded), high → `0x14` (degraded, no scanning). `init` drives
PG12 low across bring-up to bias `0x5D`, but the chip only re-latches on a real **power cycle**
(not an SWD reset) — if touch is dead and the log shows `@ 0x14`, power-cycle the board.
## Hardware
STM32H743XI (TFBGA240, **rev.Y**), Cortex-M7 @ 399.36 MHz. Cargo feature `stm32h743`
(not `stm32h743v` — the `v` variant hangs ADC power-up). Facts recovered from working silicon.
**Clocks** — HSE is a 6.144 MHz crystal (clean audio divisors):
| PLL1 P | 399.36 MHz | sysclk (AHB ÷2 → 199.68 MHz, SDCLK 100 MHz) |
| PLL2 P | 12.288 MHz | SAI kernel (256 × 48 kHz) |
| PLL3 R | 6.4 MHz | LTDC pixel clock (~59.4 Hz) |
**Memory**
| FLASH | 0x0800_0000 | 2 MB | |
| AXI SRAM | 0x2400_0000 | 512 KB | main RAM (.data/.bss/stack) |
| D2 SRAM | 0x3000_0000 | 256 KB | audio DMA buffers; **clock-gated off at reset** |
| SDRAM | 0xC000_0000 | 64 MB | external (FMC), framebuffers |
rev.Y erratum ES0392: D-cache stays off; MPU marks SDRAM + D2 SRAM non-cacheable so the LTDC/SAI
DMA stay coherent without maintenance.
**Peripherals** (pin → function)
| Display LTDC | 25 pins, AF14 (R7=PJ0/G6=PI11 AF9) | 320×240 RGB565; panel power **PK7**; R2 not routed |
| Backlight | PJ6 (TIM8_CH2, AF3) | 50 kHz, **35% hard cap** (regulator thermal limit) |
| Audio SAI1/2 | PE2/4/5 PB2 PE3 (AF6), PD11/12/13 (AF10) | CS42528 codec, OLM 20-bit, 48 kHz |
| SDRAM FMC | A/BA/D/NBL/ctrl, all AF12 | 2× IS42S16160J, CL2 |
| I2C1 | PB6 SCL / PB7 SDA (AF4) | 400 kHz blocking; codec 0x4C + touch share it |
| Touch GT9147 | INT **PG12** | addr 0x5D (low at POR) / 0x14 (degraded) |
| Buttons ×13 | active-low, board pull-ups | PA0/PA1/PC2/PC3 need the SYSCFG dual-pad fix |
| LEDs ×11 | active-high | |
| Knobs ×4 | 8 wipers on ADC1 | endless encoders, `atan2` angle |
## License
MIT OR Apache-2.0