Skip to content

hardware

Wouter Van de Wiele edited this page Aug 6, 2026 · 2 revisions

Hardware Reference

All pin assignments and I2C addresses live in a single file: lib/CONSTANTS/rocat_constants.h. Always check that file first when touching any peripheral.

Block diagram

flowchart TB
    ESP[ESP32]

    subgraph I2C["I2C bus (SDA=GPIO4, SCL=GPIO27, 100 kHz)"]
        SX[SX1509 IO expander 0x3E]
        LIS[LIS3DH accelerometer 0x18]
        PCF[PCF8523 RTC]
        TMP[TMP102 temp sensor 0x48]
    end

    ESP --- I2C
    ESP -->|8-bit parallel + A0/EN| LCD[SED1530 LCD 100x48]
    ESP -->|GPIO15| WS[6x WS2812 LEDs]
    ESP -->|GPIO25 DAC1 / I2S0| AMP[Audio amp + speaker]
    SX -->|dir/spd/stby/en| DRV[STSPIN240 motor driver]
    DRV --> M1[Motor 1]
    DRV --> M2[Motor 2]
    SX --> LCDPWR[LCD power + backlight + RW]
    SX --> RGBPWR[WS2812 power rail]
    SX --> AUDEN[Audio amp enable]
    SX --> KEEP[Power keep-awake latch]
    CHG[Charger IC] -->|CHG / STBY indicators| SX
    LIS -->|ADC1 via divider| BATT[Battery voltage]
    LIS -->|INT1 → GPIO2| ESP
    PCF -->|INT1 → GPIO12| ESP
Loading

ESP32 GPIO map

GPIO Constant Function
2 pin_acc_int1 LIS3DH INT1 (motion/click wake; RTC-capable)
4 pin_gio_sda I2C SDA
5, 9, 10, 18, 19, 21, 22, 23 pin_display[] LCD 8-bit data bus (order: 9, 10, 5, 18, 23, 19, 22, 21)
12 pin_rtc_int1 PCF8523 INT1 (alarm wake). Polled, not interrupt-driven — GPIO interrupts false-triggered from I2C coupling on the same wire
13 pin_dis_en LCD EN strobe
14 pin_dis_ao LCD A0 (command/data)
15 pin_gio_ws2812 WS2812 data
25 pin_aud_sig Audio DAC1 output
27 pin_gio_scl I2C SCL
32, 33 pin_touch_pet_sens_1/2 Capacitive touch pads (interleaved strip A,B,A,B)
34 pin_gpio_js_a_c1 ADC keys ch. 0 → DOWN / UP
35 pin_gpio_js_b_d1 ADC keys ch. 1 → LEFT / RIGHT
36 pin_gpio_js_c1_2 ADC keys ch. 2 → OK / CANCEL (also RTC ch. 0, wake button)
37 pin_gpio_js_a_c2 ADC keys ch. 3 → FUNC_1_DOWN / FUNC_2_UP
38 pin_gpio_js_b_d2 ADC keys ch. 4 → FUNC_3_LEFT / FUNC_4_RIGHT
39 pin_gpio_ldr LDR (light sensor) analog input

SX1509 IO-expander pins (I2C 0x3E)

Pin Constant Function
1 pin_m_fault Motor driver nFAULT (active-low input)
2 pin_m_stby Motor driver STBY/RESET
3 pin_m_drv_en Motor driver enable
4 pin_m1_dir Motor 1 direction
5 pin_m2_dir Motor 2 direction
6 pin_m1_spd Motor 1 speed (PWM)
7 pin_m2_spd Motor 2 speed (PWM)
8 pin_chg_ind Charger "charging" indicator (open-drain, active-low)
9 pin_stby_ind Charger "standby / charge complete" indicator (open-drain, active-low)
10 pin_dis_pwr LCD power
11 pin_dis_bl LCD backlight
12 pin_dis_rw LCD R/W
13 pin_aud_en Audio amplifier enable
14 pin_mcu_keep_awake Power latch — HIGH keeps the whole device powered; driving LOW powers off
15 pin_rgb_pwr WS2812 power rail switch

Init failure policy: if io.begin() fails at boot, setup() loops forever on purpose. The SX1509's reset is tied to the ESP32's reset, so only the physical reset button recovers both chips into a consistent state.

I2C devices

Address Device Used by
0x3E SX1509 IO expander motors, LCD power/backlight/RW, RGB power, audio enable, charger indicators, power latch
0x18 LIS3DH accelerometer motion/orientation, temperature, battery voltage via its ADC1 input, motion/click wake interrupts
PCF8523 (0x68 fixed) External RTC timekeeping, daily alarm, alarm wake
0x48 TMP102 temperature sensor (driver present, not wired into the main loop)

Display

lib/LCD/SED1530_LCD.h — SED1530/SED1531 controller, 100×48 px monochrome, 8-bit parallel bus. The class extends GFXcanvas1 (Adafruit_GFX), so all standard GFX primitives draw into a RAM framebuffer; updateWholeScreen() / updatePages() push it to the panel.

  • Icon markers: the panel has a separate icon RAM row addressed via setMarker(id, on) — independent of the framebuffer, used for the status bar (battery / star / arrows-cross). See GUI & Carousel.
  • Backlight via setBacklight(bool) (SX1509 pin 11).
  • Font restriction: only setTextSize(1) renders correctly on this panel; size 2+ is broken. Default 6×8 px font → 16×6 characters.

LEDs

Six WS2812 pixels (count_ws2812 = 6), driven by Adafruit_NeoPixel on GPIO 15, powered through SX1509 pin 15:

Index Position
0 Left eye
1 Right eye
2 Left ear, right half
3 Left ear, left half
4 Right ear, left half
5 Right ear, right half

The eyes (0/1) face the user; LedDriver::show() forces them dark whenever set_eyes_suppressed(true) (active during games and the menu).

Motors

Two DC motors through an STSPIN240 dual H-bridge, all control lines on the SX1509. MotorDriver::set_motorN(speed) takes −100…100 (%); the sign is direction and the magnitude maps onto a raw PWM duty window of 66…255. The mapping is inverted: a low raw duty value is max power and a high raw duty value is the weakest setting (confirmed by testing, twice — see the comment in motor_driver.cpp, don't "fix" this on sight). set_enabled() sequences STBY→EN in the order the chip requires; fault() reads latched overcurrent/thermal/undervoltage faults from nFAULT.

Battery sensing

There is no fuel gauge. The battery voltage reaches the LIS3DH's auxiliary ADC1 through a voltage divider (ratio ≈ 0.3226). BatteryDriver converts the 10-bit reading to millivolts at the battery (raw*0.78125 + 800 mV at the ADC pin, then /0.3226 + 316 mV offset calibration to get the battery-side value) and maintains min/max/avg windows: a 2 s base window of 100 samples at 50 Hz, cascaded into a 1 min tier (30×2s) and a 5 min tier (5×1min) — that's the full cascade, there's no 15 min/1 h/2 h tier. Charger status comes from the two SX1509 indicator pins (active-low: chg = charging, stby = charge complete).

The battery-low unit mismatch that used to affect CarouselManager's LCD marker and MoodManager's threshold (comparing a millivolt value against a bare 3.4f) has been fixed — both now use a millivolt constant (BATTERY_LOW_MV = 3400.0f) compared directly against the millivolt reading.

Audio output

Single-channel analog audio on DAC1 (GPIO 25) via the arduino-audio-tools AnalogAudioStream (I2S0 in internal-DAC mode), through an amplifier gated by SX1509 pin 13.

Input hardware

  • Two 4-way joysticks + center buttons are read as five analog channels (resistor-ladder encoding, two buttons per ADC pin — thresholds 403 mV / 1040 mV distinguish none/low/high). Debounced by a 3-sample majority filter in AdcKeys, with hold-repeat. See GUI & Carousel for the logical key map.
  • Touch strip: two ESP32 touch channels wired to interleaved pads (A,B,A,B). At least 3 A/B transitions (MIN_TRANSITIONS), each within 800 ms of the last (STROKE_GAP_MS), and spanning ≥3 s total (GESTURE_MIN_MS) yields PetGesture::UP or DOWN (used as "petting" by the mood engine).
  • LDR on GPIO 39, converted to ohms, EMA-smoothed in loop().

Clone this wiki locally