Exocortex Labs

Camera board (EXO-450)

Five identical boards sit in the beanie's folded cuff: three across the front and two on the slanted back rim (beanie). A camera has no radio. Its RP2350A reads the OV2640's JPEG frames and a PDM microphone, and sends both to the hub over the 12-pin FPC part that the rest of the helmet uses. The hub powers each camera, switches it on and off, sends it a sync pulse and a slow control UART, and passes its video and audio to the host: all of it over USB, a chosen profile over the hub's Wi-Fi (hub). The hub's ESP32-S3 is the helmet's only transmitter (regulatory § 2). The decision and its numbers are in cameras-wired.

Source: exo/pcb/camera.py (circuit as code → out/pcb/camera/camera_circuit.json), exo/pcb/camera_fp.py (land patterns, merged into exo/pcb/footprints.json), exo/pcb/camera_place.py (placement), pcb/camera.mjs (tscircuit build and gerber export), exo/pcb/camera_route.py (Freerouting + DRC) and exo/pcb/camera_fab.py (BOM, CPL, gerber zip, render). KiCad project: pcb/kicad/camera/ (exo/pcb/kicad_export.py, boards § KiCad files). Firmware: firmware/camera/ (pico-sdk).

uv run python -m exo.pcb.footprints            # land patterns (adds the camera's, leaves the others unchanged)
uv run python -m exo.pcb.camera                # circuit -> out/pcb/camera/camera_circuit.json
uv run python -m exo.pcb.camera_place          # placement -> out/pcb/camera/placement.json
node pcb/camera.mjs                            # tscircuit board (pads, nets, outline) -> out/pcb/camera/circuit.json
REMOTE=gb10-direct uv run python -m exo.pcb.camera_route route   # Freerouting on the peer (~/exocortex-cam), DRC here
uv run python -m exo.pcb.camera_route repair   # A* repair of what is open or too close, pours, DRC
uv run python -m exo.pcb.camera_fab            # gerbers + drill zip, JLC BOM + CPL, routed render
KICAD_CLI=kicad-cli uv run python -m exo.pcb.kicad_export camera --check   # KiCad's DRC and ERC -> pcb/kicad/export.json

routed copper

The board

size 19.0 × 17.0 mm, rectangular, 1.0 mm thick, centred on the lens
stack JLC 4-layer: top signal + GND fill, In1 GND plane, In2 signal + +3V3 fill, bottom signal + GND fill
rules 0.1 mm track and space (0.3 mm on VSYS and the buck's switch node), 0.3/0.2 mm vias, copper ≥ 0.3 mm from the edge, pours 0.2 mm from other nets
vias plain through vias only, none in a pad: no filling to order. Each GND pad has a dogbone (a short stub to a via just outside it); the QFN's exposed pad has plain thermal vias, kept clear of the parts on the back
routing Freerouting 2.4.1, then exo/pcb/repair.py (A* on a 0.05 mm grid) for the last connections: 50/50 nets, 100 %, 660 track segments, 139 vias (51 placed before routing: 41 dogbones and 10 thermal vias in the QFN's exposed pad)
DRC own check (route.json): no open nets, no shorts, no clearance or edge violations. All nets stay connected with every pour neck under 0.12 mm removed. Drill-to-drill gap ≥ 0.20 mm (two vias of one net that the repair put closer are merged). Tracks ≥ 0.09 mm (Freerouting's neck-downs at fine-pitch pads are widened to JLC's minimum and checked). KiCad 10.0.7's DRC with the schematic-parity check (2026-11-05, pcb/kicad/export.json): 0 errors, no unconnected item; warnings only (3 copper slivers, 1 track end KiCad reads as dangling). Its first export had 499 clearance errors (KiCad stops counting one kind at 499): every one a track or via 0.1875-0.2 mm from a GND or +3V3 pour, against the 0.2 mm pour rule. The top and inner-2 fills were cut at exactly 0.2 mm and then simplified by 0.01 mm; they are now cut POUR_MARGIN (0.02 mm) wider, as exo/pcb/route.py cuts its pours. The tightened own check (forgives 0.0001 mm, it forgave 0.005) then found 3 near misses of the 0.1 mm rule (0.0984 and 0.0989 mm: a +3V3 track at U1's CAM_Y2 pad, a QSS track at a CAM_RESET via); a repair round rerouted them (2026-11-05)
parts 49 placed parts (28 BOM lines) + 6 test pads. About $4.0 in parts per board at JLC prices, $1.30 of it the RP2350A and $1.52 the flash

Frame. The board is drawn as seen from the front (its outer face, towards the knit): x runs along the cuff band (the camera's e1), y runs up the head (e2), and the origin is on the lens axis. The board spans x −9.5 … +9.5 and y −8.5 … +8.5.

Front (outer face): - The RP2350A (QFN-60, 7 × 7 × 0.85 mm) at the centre, under the lens. The camera module rests on it with 0.2 mm PI tape. Under the camera's 8.5 mm body there are only the QFN, the USON-8 flash and 0402 parts (0.55 mm). - Round it: the QSPI flash top left, the core regulator's inductor and capacitors top right, the 12 MHz crystal below. - The top-port microphone in the bottom right corner (centred at (6.9, −6.5)) and the red status LED on the right edge. Both are clear of the camera body and of the camera's tail. - Six 1 mm test pads in a strip along the left edge. The camera's tail lies over them once it is fitted.

Back (shell side): - The 24-pin camera FPC J1 under the left half, pads at x = −5.5. The cable enters from the left edge. - The 12-pin hub FPC J2 near the bottom edge at x = +1.0. The cable leaves downwards (−y), towards the hub. - The two camera LDOs and their passives between J1 and J2, the buck converter on the right.

For the 3D (helmet.js / web_model.py, cad_doc.py). - board: 19.0 × 17.0 × 1.0, centre (0, 0) in the camera frame - camera module on the RP2350A: 8.5 × 8.5 body at (0, 0); the lens starts about 2.0 mm out from the board's back face (1.0 board + 0.85 QFN + tape) - microphone 3.5 × 2.65 at (6.9, −6.5); LED 1.6 × 0.8 on the right edge - hub flex leaving the bottom edge at (+1.0, −8.5), on the back face - camera tail wrapping round the left edge (x = −9.5)

The board is 17.0 mm tall in the ~25 mm cuff. With the lens at cuff mid-height it reaches 8.5 mm above and below the lens. Nothing hangs past its edges.

placement

Parts

All parts are JLC-assemblable and carry LCSC numbers. Prices and stock are from JLC's parts API on 2026-11-03.

ref part LCSC JLC why
U1 Raspberry Pi RP2350A, QFN-60 7 × 7 C42411118 ext., $1.30, 4,457 in stock see below
U5 GigaDevice GD25Q16EEIGR, 2 MB QSPI flash, USON-8 2 × 3 C2986331 ext., $1.52, 1,505 program and the A/B firmware slots
Y1 YXC X322512MSB4SI 12 MHz, 3225 (the hub's crystal) C9002 basic, $0.10 RP2350 clock; 15 pF load caps, 1k on XOUT
L2 Sunlord MPL2016S3R3MHT 3.3 µH, 2016 (the hub's part) C98336 ext., $0.08 RP2350 core regulator
J1 HCTL HC-FPC-05-10-24RLTAG, 24-pin 0.5 mm, front flip, bottom contact C5213739 ext., $0.19, 631 same family and drawing as the helmet's 12-pin
J2 HCTL HC-FPC-05-10-12RLTAG, 12-pin (circuit.py FPC12) C5213732 ext., $0.13, 13,057 the helmet's FPC part
U2 TI TLV62569DBVR 2 A buck, 100 % duty mode C141836 ext., $0.07, 274,098 VSYS → +3V3
L1 Sunlord WPN201610H2R2MT 2.2 µH, Isat 2.15 A, 2016 C97005 ext., $0.04
U3 Microne ME6211C28M5G-N, 2.8 V LDO C53099 ext., $0.07, 20,783 camera DOVDD/AVDD
U4 Natlinear LN1234B132MR-G, 1.3 V 300 mA LDO C3008051 ext., $0.05, 1,930 camera DVDD
MK1 LinkMems LMD3526T261-OA5, top-port PDM MEMS mic, 3.5 × 2.65 C49230436 ext., $0.31, 7,381
D1 KT-0603R red LED C2286 basic
FB1 Murata BLM15PD121SN1D ferrite, 0402 C76891 ext. AVDD filter
R8 UniOhm 0402WGF470JTCE, 47 Ω C25118 ext. CAM_TX source termination
C, R Samsung CL05/CL10/CL21 MLCCs, FH 0402CG150 15 pF, UniOhm 0402WGF resistors C1525, C52923, C23733, C1548, C19666, C19702, C45783, C25105, C25741, C25744, C25768, C25900, C11702, C25104 basic

Camera module (not assembled; plugged in by hand). An OV2640 2 MP, 160° wide-angle module with a 21 mm, 24-pin 0.5 mm FPC tail in the ESP32-CAM pinout. The exact listing is Probots PRO2229, "OV2640 160 Degree Wide Angle Lens 2MP Camera Module 24pin 21mm" (probots.co.in/ov2640-160-degree-wide-angle-lens-2mp-camera-module-24pin-21mm.html). It is the same commodity part sold everywhere as "OV2640 160° 21 mm for ESP32-CAM". The sensor's own JPEG engine compresses the frames: UXGA 1600×1200 at 15 fps, SVGA at 30 fps.

Why an RP2350A. The camera needs a parallel-pixel input, a PDM input and one fast serial output, and no radio. The RP2350's PIO does all three: one state machine reads the 8-bit DVP bus on PCLK (≤ 24 MHz), one clocks the PDM microphone, one sends a UART at 9 Mbaud (16 clocks per bit at 144 MHz). The hub already uses the RP2350 (the B package), so the crystal, the inductor and the firmware tools are shared. Its 520 KB SRAM holds two SVGA JPEG frames (about 30 KB each at q12). A radio module on the camera would put a transmitter that firmware can turn on next to the head (cameras-wired § 3.1).

Supply notes (supply). The flash is the GD25Q16EEIGR, not Winbond's W25Q16JVUXIQ: same package, land and pins, but the Winbond part is end of life and JLC holds 51. The 1.3 V LDO is the LN1234B132MR-G, not TI's TLV70313DBVR (same SOT-23-5 pins; TI has no stock until 2027-01). The LN1234B's CE has no internal pull-down; it is tied to +3V3.

Why a buck and not an LDO. VSYS runs 3.4–4.4 V. An LDO from VSYS would burn up to 0.1 W at 100 mA, inside a knit cuff, and at 3.4 V it would sit in dropout. The TLV62569 runs at about 85 % at this load. In 100 % mode it passes VSYS through at low battery. VOUT = 0.6 V × (1 + 100k/22k) = 3.33 V.

Camera rails. The rails follow the two reference boards that run these modules: - DOVDD 2.8 V and AVDD 2.8 V (through FB1), as on Espressif's ESP32-S3-EYE and Seeed's XIAO ESP32S3 Sense. - DVDD 1.3 V, as on the XIAO Sense. The module's approval sheet (Union Image ZY-XSF3M2640B) gives 1.3 V ± 5 %.

The RP2350 reads the camera's 2.8 V outputs as high (VIH 2.0 V at IOVDD 3.3 V). The SCCB lines have 4.7k pull-ups to 2.8 V. RESETB has a 10k pull-up (run). PWDN has a 10k pull-up to 2.8 V (R6): from reset until the firmware runs, the sensor is off. The RP2350's pads come out of reset with a pull-down (about 50k), and R6 still holds PWDN at about 2.3 V, above the sensor's input high.

RP2350 support. As on the hub: the core regulator (VREG_LX → 3.3 µH → DVDD, VREG_FB on DVDD, 4.7 µF on VREG_VIN and on DVDD, VREG_AVDD through 33 Ω with 4.7 µF), 100 nF on DVDD and on the IOVDD pins, the crystal with 15 pF and 1k, RUN with a 10k pull-up. USB_DP/DM and GPIO22–29 are not connected. The L2 dot (start of the winding) goes to VREG_LX, as the RP2350 hardware guide asks.

Pinouts

Hub FPC J2. The pinout is fixed and shared with the hub. The connector numbering is the helmet's 12-pin land pattern.

pin net camera side
1, 2, 3 VSYS buck input (2 × 10 µF). 3 contacts × 0.4 A rating = 1.2 A
4, 5, 6, 8, 12 GND GND (hold-down tabs too)
7 SYNC (hub → camera) 1k → GPIO19 (GPIO interrupt: frame/timestamp sync)
9 CAM_TX (camera → hub) GPIO20 (PIO UART TX, 9 Mbaud) → 47 Ω
10 CAM_RX (hub → camera) 1k → GPIO21 (UART1 RX, 921,600 baud)
11 CAM_EN (hub high = on) buck EN (100k pull-down: no hub or hub low = off)

The 1k series resistors on SYNC and CAM_RX keep a hub that drives those lines from powering an unpowered camera through the RP2350's ESD diodes. They limit that current to < 3 mA. CAM_TX is the other way: only the camera drives it, at 9 Mbaud, into up to 400 mm of FFC. There the 47 Ω resistor is a source termination (about the line impedance less the driver's). A 1k resistor with the cable's ≈ 25 pF would give a 64 ns edge in a 111 ns bit (cameras-wired § 3.2). The hub has 100 Ω in series at its end. CAM_EN needs ≥ 1.2 V (TLV62569 VIH). All logic is 3.3 V.

Camera FPC J1. The pinout is the ESP32-CAM's, the same as Espressif ESP32-S3-EYE J4 and Seeed XIAO Sense JA1:

pin signal net / GPIO pin signal net / GPIO
1 NC – 13 XCLK GPIO11 (PWM 5B, 24 MHz)
2 AGND GND 14 Y8 GPIO6
3 SIO_D GPIO12, I2C0 SDA (4.7k to 2V8) 15 DGND GND
4 AVDD 2.8 V via FB1 16 Y7 GPIO5
5 SIO_C GPIO13, I2C0 SCL (4.7k to 2V8) 17 PCLK GPIO8
6 RESETB GPIO15 (10k to 2V8) 18 Y6 GPIO4
7 VSYNC GPIO10 19 Y2 GPIO0
8 PWDN GPIO14 (10k to 2V8: off at reset) 20 Y5 GPIO3
9 HREF GPIO9 21 Y3 GPIO1
10 DVDD 1.3 V 22 Y4 GPIO2
11 DOVDD 2.8 V 23, 24 NC –
12 Y9 GPIO7

Y2…Y9 are on GPIO0–7 and PCLK, HREF, VSYNC on GPIO8–10, in that order: one PIO program reads 8 data pins from a base pin and waits on the next three.

Other GPIOs. GPIO16 MIC_CLK, GPIO17 MIC_DATA (mic L/R tied low: left slot, data valid on the rising clock edge), GPIO18 LED (high = on, 330 Ω, about 4 mA), GPIO19 SYNC, GPIO20 CAM_TX, GPIO21 CAM_RX. firmware/camera has the same table in its board.h.

Test pads (front, 1.0 mm, along the left edge; one pogo jig or fine wires, before the camera is fitted):

TP1 GND TP2 +3V3 TP3 SWCLK TP4 SWDIO TP5 RUN TP6 QSPI_SS

Power

These are the design figures at the 3.3 V rail (cameras-wired § 4.2; [bench] to confirm):

load mA at 3.3 V source
RP2350A at 144 MHz, three PIO state machines and DMA ~30 RP2350 datasheet § power consumption (estimate)
OV2640 at UXGA JPEG, 140 mW at 2.8/1.3 V, drawn through LDOs from 3.3 V ~50 module approval sheet
flash ~3
mic (0.75 mA), LED (4 mA), LDO quiescent ~6
streaming, average ~89

From VSYS through the buck (η ≈ 0.85): 0.29 W at the rail is 0.35 W from VSYS, about 93 mA at 3.7 V. Budget: 150 mA per camera from VSYS. The hub's port switch trips at ≥ 250 mA (hub). Five cameras take up to 0.75 A, about 2.8 W at the budget and 1.75 W typical. With CAM_EN low a board draws < 2 µA (buck shutdown 0.1 µA typ. plus the 100k pull-down).

Each streaming board dissipates about 0.35 W in the cuff next to the head. There is no RF amplifier on it.

How it streams

  1. The hub raises CAM_EN. The buck starts and the RP2350 boots from its flash.
  2. The firmware sets up the OV2640 over SCCB (I2C0) in JPEG mode, with XCLK at 24 MHz from PWM, and reads the DVP bus into SRAM with a PIO state machine and DMA: two frame buffers, the newest whole frame wins.
  3. Audio. A second state machine clocks the PDM microphone at 1.024 MHz. A CIC + FIR filter in software decimates it to 16 kHz, 16 bit.
  4. Uplink. Each JPEG frame goes up CAM_TX in VIDEO_FRAG messages and each audio block in an AUDIO message (protocol), COBS-framed, at 9 Mbaud: 7.2 Mb/s of payload. SVGA at 15 fps (≈ 30 KB per frame, 3.6 Mb/s) plus audio (0.26 Mb/s) uses 55 % of it. The link carries SVGA up to 25 fps.
  5. At the hub. The RP2350s take in the five uplinks (four hardware UARTs and one PIO state machine) and splice the camera messages into their USB FIFO stream (all video) and their SPI stream to the ESP32. The ESP32 sends a worn profile over Wi-Fi as RTP/UDP, about 6–11 Mb/s for all five cameras, chosen by the host with CAM_STREAM (protocol, hub). One Wi-Fi channel cannot carry five cameras at SVGA 15 fps and the EEG (cameras-wired § 2.2); USB can.
  6. Sync. The hub pulses SYNC on its shared frame clock. The RP2350 timestamps each falling edge (the reference edge, protocol) in a GPIO interrupt and stamps every frame and audio block with the latest SYNC count plus the µs offset from it. Video, audio and EEG then share one timeline on the host. The camera also reports each SYNC count it saw, so the hub can log any it missed.
  7. Control. CAM_RX carries the hub's commands at 921,600 baud: stream settings, status, firmware update. Replies go up CAM_TX with the video.
  8. Privacy LED. GPIO18 is on whenever the sensor is powered (PWDN low). The firmware lights it before it drives PWDN low and turns it off only after PWDN reads back high; R6 keeps the sensor off before the firmware runs. The firmware has no "off while recording" option. The microphone is covered too: audio runs only while the stream runs, and the stream runs only with the sensor on (camcore.c: audio_run follows running, stop_stream stops the audio before the sensor), so the light is on whenever the board can hear.

The light seen from outside. D1 sits on the outer face 8.3 mm from the lens axis, under the knit, so on its own nobody else sees it (regulatory § 5). The lens grommet (EXO-146, beanie) carries it out: a clear silicone ring that the lens passes through, with a tab under the knit that lies on D1. The tab ends over D1 in a 45° face that turns the LED's light along the tab; the ring's outer face is frosted, so the whole ring round the lens glows red while the camera can record. No board change: D1 stays where it is. What is not done: - the light is the camera firmware's (above); a light that firmware cannot turn off needs D1 on the camera's +3V3 through R2 instead of GPIO18 (on whenever the board has power), a one-net change for the next EXO-450 spin; - how bright the ring is, in daylight and at 2 m, needs a grommet and a board ([bench]).

Firmware notes

  • Build: pico-sdk, platform rp2350-arm-s, board header firmware/camera (12 MHz crystal, 2 MB GD25Q16E flash). Details and the commands are in firmware/camera/README.md.
  • Fisheye: the 160° lens's image is a fisheye. Undistort on the host, after calibrating each lens once with a checkerboard.
  • Flash layout: partition table at 0x000000 (8 KB), partition A at 0x002000 and B at 0x100000 (1,012 KB each); the last 12 KB stay unused (RP2350-E10).
  • Updates go over the control link (FW_BEGIN / FW_DATA / FW_COMMIT) into the partition the camera is not running from. FW_COMMIT reboots into it; if it never comes up, the image before it boots again.
  • First flash: 1. Before the camera module is fitted, put a pogo jig or wires on the test pads. 2. Power through TP2 (+3V3, 3.3 V) and TP1 (GND). 3. Flash over SWD on TP3 (SWCLK) and TP4 (SWDIO) with a Debug Probe and OpenOCD: the partition table, then the image (firmware/camera/README.md). TP5 (RUN) resets. TP6 (QSPI_SS) held to GND at reset stops a broken image from running while SWD rewrites the flash.

Assembly

  1. JLC assembles both sides. The front holds the RP2350A, the flash, the crystal, the core regulator, the mic, the LED and their passives; the back holds the rest.
  2. Order 4 layers, 1.0 mm. No via-in-pad, no via filling.
  3. Camera:
  4. Open J1's flip lock.
  5. With the board face down, slide the camera's tail into J1 from the left edge, gold fingers towards the board (J1 is bottom-contact).
  6. Close the lock.
  7. Fold the tail 180° round the left edge and stick the camera body on the RP2350A with 0.2 mm double-sided PI tape, lens out, centred on the lens mark at (0, 0).

The tail runs about 3.5 mm in the connector, 0.45 mm to the edge, a ~8 mm wrap and 5.25 mm on the front to the camera body: 17.2 of the 21 mm. The remaining 3.8 mm loosens the wrap. Seen through the wrap, the module's lens faces the board inside the connector. That is why J1 sits on the back and is bottom-contact: OV2640 modules carry the gold fingers on the side opposite the lens, the way the ESP32-CAM uses them. 4. Hub flex: from J2 on the back, downwards to the hub.

Land patterns

All added in exo/pcb/camera_fp.py, as names in footprints.json that only the camera uses:

  • RP2350A_QFN-60_7x7mm_P0.4mm_EP3.4mm: RP2350 datasheet QFN-60 pinout, KiCad's QFN-60-1EP_7x7mm_P0.4mm_EP3.4x3.4mm land (0.2 × 0.8 pads, centres 3.45 from the middle, 3.4 mm exposed pad).
  • USON-8_3x2mm_P0.5mm: the GD25Q16E's USON-8 2 × 3 (KiCad Winbond_USON-8-1EP_3x2mm_P0.5mm_EP0.2x1.6mm; the exposed pad is not connected).
  • HCTL_FPC_24P_P0.5mm: the HCTL drawing's recommended layout (0.30 × 0.85 pads, hold-downs D = 13.27 apart and 2.91 behind). Pin 1 follows LCSC/EasyEDA's C5213739 footprint, and the same orientation as JUSHUO AFC01-S24FCC-00 on the XIAO Sense.
  • LinkMems_LMD3526_LGA-6: datasheet sec. 8.1, top view.
  • Ind_0806_2016 (both inductors), Cap_0805_2012, LED_0603_1608: IPC nominal.

Bench checks before the first order

  • The 9 Mbaud link over 400 mm of FFC, an RP2350 at each end: error rate over hours, a scope at both ends (edge, ringing with the 47 Ω), crosstalk into CAM_RX (pin 10, next to CAM_TX on pin 9).
  • PIO capture of the OV2640's JPEG at SVGA 15–25 fps, PCLK up to 24 MHz, with the 2.8 V camera I/O.
  • Power and heat of a streaming board in the cuff (the 89 mA estimate; the firmware busy-polls one core at 144 MHz with no sleep).
  • Privacy from power-on: the time from reset to the LED on, with R6 holding the sensor off.
  • The ring seen from outside: the EXO-146 light-guide grommet on a lit board, under the beanie, in a lit room and in daylight at 2 m; the tab's 45° face over D1 and the ring's frosting.