Kart Medulla (ESP32-S3)
The Kart Medulla is the MCU-based control hub between the Orin computer and the kart's sensors and actuators. The next revision is an interface PCB built around the ESP32-S3, with external level shifting, analog conditioning, an SPI DAC, an on-PCB manual/autonomous signal mux, and Wago-style push-in connectors replacing the hand-wired Dupont setup.
Currently hand-wired in the kart: classic ESP32
The kart is currently running a hand-wired classic-ESP32 (no PCB). That setup is documented on the Legacy wiring page and will be removed once the ESP32-S3 board is deployed.
Firmware repository: UM-Driverless/kart-medulla
Why ESP32-S3
The classic ESP32 ran out of usable GPIOs once CAN, SPI, status RGB, buzzer, and the Orin link were added on top of the existing I/O (3× halls, 3× pressure, accelerator, brake, SDC, steering, relay). The S3 solves this and adds several quality-of-life wins:
- ~45 GPIOs (vs ~34 on the classic), with fewer of them reserved or strap-pin traps.
- Native USB-OTG — the Orin link becomes a direct USB cable (CDC-ACM), dropping the USB-UART bridge IC and moving from ~1 Mbit/s UART to ~12 Mbit/s full-speed USB.
- Built-in USB-Serial-JTAG — flashing, serial monitor, and step-debugging all over the same USB cable. No external ESP-Prog / FT2232H needed.
- External DAC on the PCB (MCP4922, dual 12-bit, SPI) — replaces the classic ESP32's built-in 8-bit DAC. 12-bit resolution × 2 channels covers
CMD_ACC(accelerator, 0–5 V direct) andCMD_BRAKE(brake, 0–5 V → ×2 op-amp → 0–10 V for the Festo proportional valve) with no extra pin cost beyond the existing SPI bus.
Variants considered and rejected: S2 (has DAC but single-core, no BT), C3 (too few GPIOs), C6 (no DAC, Wi-Fi 6 overkill for a kart), H2 (no Wi-Fi).
ESP32-S3 Overview
Click the image for the full high-resolution pinout and specs page at mischianti.org.
- CPU: Xtensa dual-core 32-bit LX7, up to 240 MHz
- GPIOs: ~45 usable
- ADCs: 2× 12-bit, multi-channel
- DACs: none (external MCP4922 dual 12-bit SPI DAC on the interface PCB)
- USB: native USB-OTG + USB-Serial-JTAG
- Wireless: Wi-Fi 4 + BLE 5
- Communication Interfaces: SPI, I²C, UART, CAN (TWAI), I²S
Dev-board mechanical reference (ESP32-S3-DevKitC-1)
The medulla PCB hosts the ESP32-S3 module via a stock ESP32-S3-DevKitC-1 dev board (or a pin-compatible clone such as the YD-ESP32-S3 / "44 pines tipo C"). The medulla footprint must match this:
| Quantity | Value |
|---|---|
| Pin pitch (within a row) | 2.54 mm (0.1 ″) |
| Pins per row | 22 (44 total — 2 rows of female sockets) |
| Row centerline ↔ row centerline | 22.86 mm (0.9 ″) |
| PCB outer width | 25.40 mm (= 22.86 + 2 × 1.27 mm edge offset) |
| USB-C protrusion past board edge | ~8.00 mm |
The row spacing is 22.86 mm (0.9 ″), NOT 25.40 mm. Pin centerlines are inset 1.27 mm from each PCB edge. This was confirmed by physical caliper measurement on an official Espressif board on 2026-05-02 after two earlier wrong PDF-derived answers — the Espressif DXF_…_V1.1_20220429.pdf mechanical drawing has ambiguous 1.27 mm callouts that can be read as either antenna keepout or pin-row offset. Trust the physical measurement, not any single drawing. Local mirrors of the Espressif drawing, schematic, and the ground-truth measurement photo are kept in the dv vault at dv/kart/kart-medulla/resources/esp32-s3-devkitc-1/.
ESP32-S3 Pin Assignment
Pin map for the ESP32-S3 module on the interface PCB. Source of truth: projects/kart-medulla/docs/pinout-esp32-s3.md in the dv-hardware repo (which itself defers to the schematic). This table mirrors that file as of 2026-05-08; if the two disagree, dv-hardware wins. For the per-pin capability reference (which GPIOs can do ADC, which are strap pins, etc.) see lib/esp32-s3-pin-capabilities.md in the same repo.
Pin numbers 1–44 follow a chip-style counter-clockwise convention: pin 1 is the bottom-right contact (USB-C at the top, component side facing you), pins 1–22 climb the right edge, pins 23–44 descend the left edge. The medulla's left header is dual-row (44 pads, 22 unique nets — each row is shorted between its two pads for daughterboard pass-through).
Status legend: HOLD = unassigned, kept free for future use or module-variant compatibility. BLOCKED = physically off-limits (DevKit-side hardware constraint). NC = not wired on this PCB rev (no-connect symbol in schematic).
| Pin | Silkscreen | GPIO | Signal | Type | Notes |
|---|---|---|---|---|---|
| 1 | GND | – | GND | Power | Ground (bottom of right edge) |
| 2 | GND | – | GND | Power | Ground |
| 3 | 19 | 19 | NC | – | No USB-C connector on the medulla PCB; GPIO 19 unwired this rev |
| 4 | 20 | 20 | NC | – | Same as Pin 3 |
| 5 | 21 | 21 | MOTOR_HALL_3 | Digital In | Motor hall sensor 3 |
| 6 | 47 | 47 | MOTOR_HALL_2 | Digital In | Motor hall sensor 2 |
| 7 | 48 | 48 | BLOCKED | – | DevKit on-board RGB LED; no external LED on the medulla |
| 8 | 45 | 45 | HOLD | – | Strap pin (VDD_SPI voltage select). Idle-LOW at boot required. |
| 9 | 0 | 0 | HOLD | – | Strap pin (BOOT mode, must be HIGH at boot). Was previously CMD_STEER_DIR (moved to GPIO 17 on 2026-05-08 to remove strap risk). |
| 10 | 35 | 35 | HOLD | – | Octal-PSRAM pin on R8 modules; kept free for N8R2/N8R8 compatibility |
| 11 | 36 | 36 | HOLD | – | Octal-PSRAM pin on R8 modules. Was briefly CMD_REVERSE; moved to PCF8574 P0 on 2026-05-03. |
| 12 | 37 | 37 | HOLD | – | Octal-PSRAM pin on R8 modules |
| 13 | 38 | 38 | HOLD | – | Was SDC_NOT_EMERGENCY until 2026-05-08; signal moved to GPIO 18 (Pin 33) so the gate driver sits next to Q3 on the PCB. |
| 14 | 39 | 39 | HOLD | – | Free GPIO |
| 15 | 40 | 40 | CMD_STEER_PWM | LEDC PWM | Steering motor PWM (Cytron H-bridge) |
| 16 | 41 | 41 | HOLD | – | Held for future CAN_RX (no CAN transceiver this rev — CAN moved to Orin carrier) |
| 17 | 42 | 42 | HOLD | – | Held for future CAN_TX (same as Pin 16) |
| 18 | 2 | 2 | HYDRAULIC_2 | ADC1_CH1 | Hydraulic pressure sensor 2 |
| 19 | 1 | 1 | PRESSURE_3 | ADC1_CH0 | Pressure sensor 3 |
| 20 | RX | 44 | BLOCKED | – | DevKit USB-UART bridge owns U0RXD; not reclaimable on DevKitC-1 |
| 21 | TX | 43 | BLOCKED | – | DevKit USB-UART bridge owns U0TXD |
| 22 | GND | – | GND | Power | Ground (top of right edge) |
| 23 | 3V3 | – | 3V3 | Power | 3.3 V supply (S3 module LDO from 5 V) |
| 24 | 3V3 | – | 3V3 | Power | 3.3 V supply |
| 25 | EN | – | RST | Reset | Reset (silkscreened EN) |
| 26 | 4 | 4 | PEDAL_ACC | ADC1_CH3 | Accelerator pedal |
| 27 | 5 | 5 | PEDAL_BRAKE | ADC1_CH4 | Brake pedal |
| 28 | 6 | 6 | PRESSURE_1 | ADC1_CH5 | Pressure sensor 1 |
| 29 | 7 | 7 | PRESSURE_2 | ADC1_CH6 | Pressure sensor 2 |
| 30 | 15 | 15 | SELECT_THROTTLE | Digital Out | Drives the U14 MAX4660 SELECT pin (manual/autonomous throttle mux). 10 kΩ pulldown to GND on this net → hardware default = manual passthrough. |
| 31 | 16 | 16 | MOTOR_HALL_1 | Digital In | Motor hall sensor 1 (moved from GPIO 37 for N8R8 compatibility) |
| 32 | 17 | 17 | CMD_STEER_DIR__3V3 | Digital Out | Steering motor direction (Cytron H-bridge). Moved from GPIO 0 on 2026-05-08 to drop the BOOT-strap risk. |
| 33 | 18 | 18 | SDC_NOT_EMERGENCY__3V3 | Digital Out | Drives Q3 (IRLZ44N) gate via R22 (100 Ω). HIGH = Q3 ON = SDC chain return path closed = no emergency. R23 (100 kΩ) gate-pulldown ensures emergency-state at boot until firmware drives HIGH. Moved from GPIO 38 on 2026-05-08. |
| 34 | 8 | 8 | SDA | I²C | I²C data — AS5600 steering angle sensor + PCF8574 GPIO expander share the bus |
| 35 | 3 | 3 | BUZZER | Digital Out | Debug/status buzzer. Strap pin (JTAG src select), default-high; idle-high at boot is acceptable. Moved from GPIO 36 for N8R8 compatibility. |
| 36 | 46 | 46 | HOLD | – | Strap pin (ROM-print enable). Default LOW = silent boot — required idle state. |
| 37 | 9 | 9 | SCL | I²C | I²C clock (same bus as SDA) |
| 38 | 10 | 10 | HYDRAULIC_1 | ADC1_CH9 | Hydraulic pressure sensor 1 |
| 39 | 11 | 11 | MOSI | SPI | SPI data out → MCP4922 SDI |
| 40 | 12 | 12 | CLK | SPI | SPI clock → MCP4922 SCK |
| 41 | 13 | 13 | MISO | SPI | SPI data in (unused by MCP4922; available for future SPI peripheral) |
| 42 | 14 | 14 | CMD_DAC_CS | SPI | MCP4922 chip select (active low) |
| 43 | 5V | – | +5V_USB | Power | 5 V from medulla USB-C VBUS — powers the ESP32 dev board only (split-rail design) |
| 44 | GND | – | GND | Power | Ground (bottom of left edge) |
CMD_ACC and CMD_BRAKE go through the external SPI DAC
The classic ESP32 exposed CMD_ACC on a dedicated DAC pin. On the S3 there is no native DAC — both CMD_ACC and CMD_BRAKE are generated by the MCP4922 (dual 12-bit SPI DAC, see hardware decisions below) and ride the existing SPI bus (CS = GPIO 14). Channel A → CMD_ACC (0–5 V direct), Channel B → CMD_BRAKE → ×2 op-amp → 0–10 V for the Festo proportional brake valve.
GPIO restrictions (ESP32-S3)
Strap/boot pins on the S3 — notably GPIO 0, 3, 45, 46 — must be left at safe levels at reset; the table notes mark which assignments are constrained by this. On WROOM-1 modules some of GPIO 26–32 are tied to the SPI flash internally (always reserved). GPIO 33–37 are reserved by octal PSRAM on R8 variants (see the danger block below).
Module suffix: N8R2 preferred; N8R8 still works
The current pinout is N8R8-compatible: GPIO 33–37 are all HOLD (no signal routed), and CMD_REVERSE lives on the PCF8574 instead of GPIO 36. Either module variant fits, with the caveat below kept for context.
Module suffix: N8R2 is the ordered part — octal-PSRAM variants (R8) are tolerated, never preferred
The ordered part is ESP32-S3-WROOM-1-N8R2 (8 MB flash, 2 MB quad PSRAM). Do not substitute an R8 variant (e.g. N16R8), even if the plan is to "ignore" the extra PSRAM in firmware.
On R8 modules, the 8 MB octal PSRAM is hard-wired inside the module package to GPIO 33–37 (SPI0/1 extension pins). Espressif's ESP32-S3-WROOM-1 datasheet marks those pins as not available on R8 variants. This is a physical constraint: disabling PSRAM in sdkconfig does NOT reclaim the pins — the PSRAM die is still electrically attached to those traces, and driving them externally risks bus contention during boot.
Our pinout treats GPIO 33, 35, 37 as SPARE so that an R8 module would physically fit (courtesy compatibility), and keeps the assigned-but-droppable signal (CMD_REVERSE on GPIO 36) flagged as SPARE/CMD_REVERSE to make the loss obvious. That is not an endorsement of R8 — the module standard remains N8R2, and any move to R8 requires re-auditing the pinout.
Valid upgrade path if 8 MB flash isn't enough: N16R2 (16 MB flash, 2 MB quad PSRAM) — zero pinout change, zero GPIO cost. Not N16R8.
See the dv vault kart/kart-medulla/history.md (2026-04-23, 2026-04-29) for full reasoning.
Kart Medulla Interface PCB
Interface PCB hosting the ESP32-S3 module, signal conditioning, the SPI DAC, the manual/autonomous analog mux, and outside-world connectors. Design lineage (EasyEDA .epro project files) lives in the Drive folder formula_24-25-26/dv/kart/kart-medulla/project-backups/.
Hardware Decisions
- Shutdown circuit (SDC): a single ESP32 GPIO —
SDC_NOT_EMERGENCYon GPIO 18 (Pin 33) — drives the gate of Q3 (IRLZ44N) through R22 (100 Ω). When HIGH, Q3 conducts and pulls the kart'sSDC_IN_LOW_SIDEto GND, completing the SDC return path → no emergency. When LOW, Q3 is off and the chain breaks → emergency. R23 (100 kΩ) gate-pulldown forces Q3 OFF (= emergency) at boot until firmware actively drives HIGH. The signal name reflects the intent the ESP32 asserts, not the chain's electrical state. There is no separateSDC_STATreadback in this rev — the ESP32 trusts its own command. (The previous design used a relay + a status pin on GPIO 38/39; replaced 2026-05-08 with the MOSFET-only scheme so the gate driver sits next to Q3 on the PCB layout.) - Analog command outputs (
CMD_ACC,CMD_BRAKE): external MCP4922-E/SL — dual 12-bit SPI DAC. On the existing SPI bus (CS = GPIO 14). VREF tied to the 5 V rail through a 100 Ω + 10 µF RC filter to attenuate ~150 kHz switching ripple from the upstream XW-1224 buck. Channel A →CMD_ACC0–5 V direct; Channel B →CMD_BRAKE→ ×2 op-amp → 0–10 V for the Festo proportional brake valve. Decision history: 2026-04-13 (initial choice was MCP4728 I²C) → 2026-04-17 (switched to MCP4922 SPI because we already had MCP4922 chips on hand and SPI is cleaner for an analog-command bus shared with no other slow devices). - Manual/autonomous signal mux (decision 2026-05-01, refined 2026-05-02 / 03 / 08): one MAX4660EUA+T SPDT analog switch (U14) on the PCB muxes the throttle signal between the manual source and the ESP32 DAC output. Brake is NOT muxed — manual mode does not need brake control routed through the ESP32, so the brake DAC output goes directly to the brake valve driver with no switch (decision 2026-05-08). Reverse is NOT muxed via MAX4660 nor via a direct ESP32 GPIO — it is driven by U25 PCF8574T port P0 (I²C GPIO expander) in parallel with the manual reverse button (wired-OR via the motor controller's existing pull-up; the PCF8574's quasi-bidirectional outputs are natively open-drain). This frees GPIO 36 and gains N8R8 compatibility (decision 2026-05-03). The MAX4660's SELECT pin is driven by
SELECT_THROTTLEon GPIO 15 with a 10 kΩ pulldown to GND, so the hardware default is manual passthrough whenever the ESP32 is crashed, hung, resetting, or unbooted. Steering is NOT muxed — the ESP32 always drives the Cytron H-bridge directly; in manual mode firmware sets PWM = 0. - Cytron H-bridge (steering driver) power (decision 2026-05-01): powered permanently from kart 12 V — NOT switched through the manual/autonomous mode switch. The Cytron's inrush capacitors were browning out the Orin every time the kart was switched into autonomous. The PCB only routes signals (
CMD_STEER_PWM,CMD_STEER_DIR) to the Cytron, not power. - REVERSE-signal driver to the kart electronics box (U12, decision 2026-04-26): swapping PC357N1J000F optocoupler → BSS123 N-channel logic-level MOSFET (SOT-23) in the next schematic edit. Reason: medulla GND and box GND are bonded through several paths (USB ↔ Orin, signals ↔ Cytron, motor return ↔ battery), so the opto's isolation is moot — MOSFET is cheaper, smaller, faster, and doesn't age. Board not yet manufactured, swap is free. Drives the box's REVERSE wire (5 V via ~60 kΩ internal pull-up; pull to 0 V to engage reverse).
- Pressure sensor inputs (3× Festo, 24 V): voltage divider + input clamp / TVS protection on each channel to bring the signal into the S3's ADC range (≤ 3.3 V).
- Hydraulic pressure sensor inputs (2×): routed to ADC1_CH9 (GPIO 10) and ADC1_CH1 (GPIO 2).
- Hall sensor inputs (3× 5 V): dedicated level translator (NOT the optocoupler) to 3.3 V before the GPIO pins.
- Orin link: native USB-OTG on GPIO 19/20 (D∓). No USB-UART bridge chip.
- Power architecture: kart 12 V → external XW-1224 buck → 5 V kart-wide rail → medulla 5 V (H1.21) → ESP32-S3 module LDO → 3.3 V. The medulla can alternatively be powered from an on-board LM2596SX-ADJ buck (qty 8 in stock) if the kart-wide 5 V rail is unavailable. MCP4922 VDD and MAX4660 Vcc both run from the same 5 V rail.
Connector Pinout (Outside World)
The main connector is a set of green push-in headers labeled CN1..CN4 in the schematic. Signal names follow the Net Name Nomenclature convention.

| Connector | Pin | Signal | Notes |
|---|---|---|---|
| CN1 | 1 | MOTOR_HALL_3__5V | Motor hall sensor 3 (5 V, level-shifted on PCB) |
| CN1 | 2 | MOTOR_HALL_2__5V | Motor hall sensor 2 (5 V, level-shifted on PCB) |
| CN1 | 3 | MOTOR_HALL_1__5V | Motor hall sensor 1 (5 V, level-shifted on PCB) |
| CN2 | 1 | PRESSURE_1__24V | Festo pressure sensor 1 (24 V, divided + clamped on PCB) |
| CN2 | 2 | PRESSURE_2__24V | Festo pressure sensor 2 (24 V, divided + clamped on PCB) |
| CN2 | 3 | PRESSURE_3__24V | Festo pressure sensor 3 (24 V, divided + clamped on PCB) |
| CN3 | 1 | GND | |
| CN3 | 2 | CMD_STEER_DIR__3V3 | Steering direction command |
| CN3 | 3 | CMD_STEER__PWM_3V3 | Steering PWM command (to Cytron H-bridge) |
| CN4 | 1 | 3V3 | |
| CN4 | 2 | I2C_STEER_SDA__I2C | AS5600 steering angle sensor data |
| CN4 | 3 | I2C_STEER_SCL__I2C | AS5600 steering angle sensor clock |
