Repository navigation
Hardware
What the device is made of, how it is wired, and which parts of that are verified rather than assumed. The authoritative detail lives in docs/HARDWARE_BOM.md and docs/HARDWARE_INTEGRATION.md; this page is the orientation and the warnings.
As of 13 October 2026 the Qwiic cable has arrived, the receiver is on the I2C socket, and the device configures it at boot and reads accepted fixes from it: 20 Hz as configured, 49 frames, 49 accepted, 0 rejected, on 15 satellites. Nothing was damaged connecting it.
The firmware configures the receiver itself, because a NEO-M9N in its factory default emits
NMEA and this firmware parses UBX. UBX-CFG-VALSET enables NAV-PVT on I2C, silences NMEA
and sets the rate — written to RAM, battery-backed RAM and flash, so it survives a power
cycle rather than working only until the device is unplugged.
Three things worth noting from bring-up:
- the antenna is doing its job. A valid fix indoors on 15-16 satellites across multiple constellations, within a second of boot
- 25 Hz on four constellations is real. Measured at 25.3 frames/s when configured for a 40 ms period, which confirms the part choice below rather than taking it on trust. 20 Hz is what runs, per #16
-
whether a configuration persists cannot be tested by power-cycling the board. The
firmware configures on every boot, so a receiver that kept its settings and one that
reverted behave identically from the first fix onwards, and the power-on boot overwrites
the evidence before anything looks. Looking before writing does not fix it either: that
is only evidence if the receiver actually lost power, and a power-on boot's log cannot be
captured on this hardware at all. The answer comes from asking the receiver what is in its
flash layer with
UBX-CFG-VALGET, which needs no power cycle and no listener
The Waveshare has two physically identical SH1.0 4-pin sockets, one UART and one I2C. The concern on #9 was that the I2C block's pin order had never been reported, so if its power ordering differed from the UART block's, a board-to-board cable could put 3.3 V onto a data line.
A standard Qwiic cable has now been used on that socket without incident, so this is no longer a hazard. It is still not an answer: the pin order was established by a connection that happened to work rather than read off the board, which covers the one cable that was used and nothing else. Check the label anyway before using a cable you have not used before, particularly a home-made one.
| Ref | Part | Why this one |
|---|---|---|
| H1 | Waveshare ESP32-S3-Touch-AMOLED-2.41-B | 600×450 AMOLED, touch, ESP32-S3R8 with 8 MB PSRAM and 16 MB flash, QMI8658 IMU, RTC, microSD, supplied case |
| H2 | SparkFun GPS-17285 — u-blox NEO-M9N, SMA, Qwiic | 25 Hz navigation rate on four constellations, 3.3 V logic, UART and I2C, UBX |
| H3 | Taoglas Magma X AA.170.301111 |
Covers all four constellations the M9N tracks, including BeiDou B1 at 1561 MHz, which common 1575–1610 MHz antennas miss |
| H4 | High-endurance microSD, 8–32 GB | Session traces and event logs, FAT32 |
| H6 | Fused 5 V vehicle USB supply | Never connect raw vehicle 12 V to the Waveshare board |
Confirmed from the board itself: chip revision v0.2, 16 MB flash, and the fitted card reports as SN32G, 30 GB, FAT.
The antenna band, not its gain, is what decides it. BeiDou B1 sits at 1561.098 MHz, below the 1575–1610 MHz window that most "GPS/GLONASS/Galileo" antennas cover. Fit one of those and BeiDou is quietly absent — the receiver still works, so nothing looks broken, while a quarter of the constellations silently stop contributing.
A newer u-blox part would be a downgrade. The M9N does 25 Hz on four constellations. The M10 manages 10 Hz on four, and 20 Hz only with three and a raised CPU clock. For lap timing, fix rate is the whole point, so the older generation is the right one.
| On GPIO47 (SDA) / GPIO48 (SCL) |
|---|
| Touch controller |
| QMI8658 IMU |
| RTC |
| IO expander (AMOLED enable, tearing effect, touch interrupt) |
GNSS receiver at 0x42, joining via Qwiic |
The receiver is the one participant on that bus with a deadline. At 25 Hz a fix is 40 ms and the receiver's buffer holds roughly 410 ms, so the margin is about ten missed polls — which is less comfortable than it sounds on a device whose UI task has starved before. Bus contention is a real design consideration, not a theoretical one.
Both of the breakout's connectors are Qwiic on the same bus, and its I2C jumper carries
the pull-ups.
| Function | Pins | Bus |
|---|---|---|
| AMOLED QSPI CS / CLK / D0–D3 | GPIO9 / 10 / 11–14 | SPI2_HOST |
| AMOLED reset | GPIO21 | |
| Touch reset | GPIO3 | |
| microSD CS / SCLK / MOSI / MISO | GPIO2 / 4 / 5 / 6 |
SPI3_HOST, SPI mode |
| UART socket | GPIO43 / 44 |
UART0 — also the default console
|
This is the constraint most likely to catch someone out.
GPIO43/44 is UART0, which is the firmware's primary console. The staged transport
plan in
ADR-005
moves GNSS to UART for vehicle use, because UART is pushed by DMA while I2C must be polled.
That puts the receiver on the same two pins the console speaks on.
If both are used at once, log output is transmitted into the receiver and the console driver owns the pin the receiver answers on. So the console has to move to USB Serial/JTAG before GNSS takes the UART socket.
Two practical consequences:
- Do not plan to use UART0 as a permanent diagnostic tap. It is wanted for the receiver. Boot logs are obtainable over USB instead — see below.
- The breakout's UART is plated through-holes only, so the UART stage needs soldering.
I2C is solderless, which is why bring-up uses it. Leave the breakout's
SPIsolder jumper open: closing it disables the UART on those lines.
The module's UART default is 38400 baud, 8N1, per the data sheet. The 115200 figure in SparkFun's hookup guide is their serial monitor, not the module.
make firmware-capture-boot captures a cold boot over the board's own USB-Serial-JTAG port
— the full log from the ROM banner to the application, with no extra hardware.
Two traps, both of which produce silence rather than an error:
- Opening the port and waiting captures nothing. Boot output is written and discarded before a host attaches, so the listener has to hold the port before the reset.
- Pulsing RTS alone boots the ROM download stub, which prints nothing. RTS is wired to EN (reset) and DTR to GPIO0 (boot select), and pyserial asserts both on open. Releasing RTS with GPIO0 still low starts the download stub. An empty capture then reads as "the output was discarded" when it means "the application never ran".
A genuine power cycle cannot be captured this way at all: the USB device is inside the
chip, so cutting power destroys the port the listener is holding. An RTS reset is a chip
reset (rst:0x15 USB_UART_CHIP_RESET) and the handle survives it. Only a separate USB-UART
adapter on the UART socket could see a post-power-cycle boot, since its own connection to
the host is independent of the target's power — and that socket is wanted for the receiver.
Background in #181.
TrackSessionTimer GPS bootstrap
imu: ready
sd-card: mounted
sd-card name=SN32G capacity=30424 MB fs-total=30409 MB free=30408 MB
settings: source=0 current-format=1, mode track-day
track catalog: built (26 discovered, 26 loaded, 0 rejected, cap 32)
gnss: transport attached over i2c
ready screen presented on the 600x450 panel
gnss: searching (sats 7, polls 412, bytes 38104, frames 331, ..., 24.9 Hz)
gnss: fix accepted (sats 11, ..., accepted 1, rejected 331, 25.0 Hz)
gnss: transport attached over i2c is weaker than it reads: it means only that the I2C
driver accepted an address, since adding a device to the bus never talks to the chip. The
stage lines after it are what report a receiver. They are logged on every change, with
six named stages ordered the way the link actually fails — bus unreadable, bus ok, receiver silent, bytes arriving, no valid frame, ubx arriving, no nav-pvt, searching,
fix accepted — so the cause is named rather than left as "no fix". The third of those also
logs the receiver's first bytes as text, because neither NMEA nor a corrupted bus produces a
checksum error and the bytes are the only thing that tells them apart.
Note that an NMEA sentence contains a position, so a captured boot log is as sensitive as the place it was captured.
Full detail in DEVELOPMENT_SETUP.md.
settings: source=0 means the stored configuration was read in its current format; a
non-zero value means it was migrated from an older one on that boot. The track counts depend
on what is on the card, and rejected or a truncation warning means definitions did not
make it into the catalog.
| State | |
|---|---|
| Display, touch, gesture UI | working on hardware |
| microSD, settings persistence, session lifecycle | working, survives power cycle |
| QMI8658 IMU, gyro-tracked attitude, G recording | working |
| Boot log capture over USB | working |
| I2C transport and DDC protocol |
working on hardware. The receiver answers at 0x42 and its bytes reach the pipeline |
| GNSS fixes | working. 20 Hz, configured at boot and acknowledged, every frame accepted |
| Receiver configuration surviving a power cycle | confirmed. Read back from the receiver's flash layer: nav-pvt on, nmea off, 50 ms |
| RTC | present, not driven |
| Vehicle power path, enclosure, antenna mounting | not built |
- Wire the timing engine to the fix queue. Nothing consumes it, so no lap can be timed however good the fixes are. This is the critical path, and it is software (#18).
- Check GPS Only on the device and confirm it reads out a live receiver (#178).
- Starve the poll deliberately and watch the gap counter trip on the device. The host tests prove the logic; they do not prove the wiring (#18).
- Select Donington Park and confirm the timing engine arms (#139).
- Drive the Test Circuit and capture its gates on the device (#184, #37).
- Only then the UART stage, which needs soldering and the console moved first (ADR-005).