Skip to content

Instantly share code, notes, and snippets.

@CrazyCoder
Last active February 12, 2026 19:11
Show Gist options
  • Select an option

  • Save CrazyCoder/1c5f846adee18e21f91e264601a6ddce to your computer and use it in GitHub Desktop.

Select an option

Save CrazyCoder/1c5f846adee18e21f91e264601a6ddce to your computer and use it in GitHub Desktop.
Xteink X3 GPIO (based on 5.0.3 firmware analysis)

Xteink X3 GPIO Pin Mapping

Complete GPIO Map

GPIO Function Direction Evidence
0 I2C SCL Bidir (OD) Wire.begin(sda=20, scl=0, freq=400000) in sub_42004146
1 Button ADC (primary) Analog In analogRead(1) in sub_42006294 — 4-button resistor ladder
2 Button ADC (secondary) Analog In analogRead(2) fallback in sub_42006294 — 2 buttons
3 Power/Wake input Digital In digitalRead(3) in sub_4200D0B6, triggers power-on sequence
4 EPD DC Output Hardcoded digitalWrite(4, 0/1) in sub_420050A8
5 EPD RST Output sub_42066090(&v7, 21, 4, 5, 6) in BLE config handler sub_42055840
6 EPD BUSY Input Hardcoded digitalRead(6) poll in sub_42004E04
7 SPI MISO Input SPI.begin(8, 7, 10, 12) in sub_4203D9D4 (shared: SD card)
8 SPI SCLK Output Same SPI.begin call
9 Boot strapping Not found in application code; ESP32-C3 BOOT pin
10 SPI MOSI Output Same SPI.begin call
11 VDD_SPI Flash voltage select; not available as GPIO
12 SD Card CS Output SPI.begin(8, 7, 10, 12) — SS parameter
13 Power/SD control Output Output with pullup, toggled in setup and sleep
14-17 SPI Flash Internal flash; not available as GPIO
18 USB D- Bidir Set open-drain LOW in shutdown (sub_42093DC2); USB_SERIAL_JTAG regs at 0x60043010
19 USB D+ Bidir Set open-drain LOW in shutdown (sub_42093DC2); USB_SERIAL_JTAG regs at 0x60043014
20 I2C SDA Bidir (OD) Wire.begin(sda=20, scl=0, freq=400000) in sub_42004146
21 EPD CS Output Hardcoded digitalWrite(21, 0/1) in sub_420050A8

I2C Bus (GPIO 0 SCL, GPIO 20 SDA, 400 kHz)

Three devices share the I2C bus:

QMI8658 — 6-Axis IMU (Gyroscope + Accelerometer)

Property Value Evidence
I2C Address 0x6B (107), fallback 0x6A sub_420913CE: tries both addresses
WHO_AM_I 0x05 (register 0x00) sub_420912D4 reads reg 0, expects 5
Init function sub_4200CA0E Probes I2C, creates driver object at 0x3FCAB8C8
Driver init sub_420913CE WHO_AM_I check, writes CTRL2=0x60, configures ODR/range
Interrupt GPIO 3 (likely INT1) digitalRead(3) triggers gyro/wake processing

Used for "Shake to turn page" feature. Configuration: accelerometer FS=±8g (CTRL2=0x60). If probe fails, bit 5 (0x20) is set in status register 0x3FCAB9B8 → "Gyroscope startup failed".

DS3231 — Real-Time Clock

Property Value Evidence
I2C Address 0x68 (104) sub_4200C662 probes address 104
Time read sub_420908FA Reads 7 BCD registers starting at 0x00
RTC object 0x3FCABA30 8-byte object, allocated on first probe

Register map (confirmed from BCD decoding in sub_420908FA):

  • Reg 0: Seconds (bits 6:4 tens, 3:0 units, bit 7 masked)
  • Reg 1: Minutes
  • Reg 2: Hours (24h mode, bits 5:4 tens)
  • Reg 3: Day of week
  • Reg 4: Date
  • Reg 5: Month
  • Reg 6: Year (+ 2000)

If probe fails, bit 3 (0x08) set in status → "Clock Chip Error" display.

BQ27220 — Battery Fuel Gauge

Property Value Evidence
I2C Address 0x55 (85) battery_bq27220_init probes address 85
Device ID 0x0220 (544) battery_bq27220_configure sends Control(0x0001), reads MACData(0x40), expects 544
Init function battery_bq27220_init (0x4200D52E) Probes I2C, creates 44-byte object at 0x3FCAB944
Config function battery_bq27220_configure (0x42000802) Full fuel gauge initialization with data flash profile
Object init battery_bq27220_obj_init (0x42000020) Stores addr=0x55, Wire pointer, clears 44-byte object

If probe fails, bit 4 (0x10) set in status → "Electricity meter error" display.

BQ27220 Object Layout (44 bytes at 0x3FCAB944)

Offset Type Field Value
0 byte I2C address 0x55
1 byte Init param (hardcoded 6) 0x06
2 byte Init param (hardcoded 7) 0x07
4 dword Wire object pointer 0x3FCA3EEC
8–43 Data buffers (zeroed on init)

Register Read Map

battery_bq27220_read_u16(obj, reg) reads a 16-bit little-endian value from the BQ27220.

Register addresses confirmed against Flipper Zero BQ27220 driver (same chip, same unseal keys).

Register Address Unit Read by Stored at
Temperature 0x06 0.1 K sub_42000428 0x3FC965C8 (÷100.0 → float ≈ °C)
Voltage 0x08 mV sub_4200018A 0x3FC965C0 (÷1000.0 → float V)
Current 0x0C mA (signed) sub_4200018E 0x3FC965C4 (int16)
RemainingCapacity 0x10 mAh sub_42000430 0x3FC965CC
FullChargeCapacity 0x12 mAh sub_4200042C 0x3FC965CE
StateOfCharge 0x2C % sub_42000434 0x3FC965C6
StateOfHealth 0x2E % sub_4200043A 0x3FC965D0
OperationStatus 0x3A flags battery_bq27220_read_opstat local

Note: Previous analysis had registers 0x06/0x08 swapped and register 0x2C misidentified as "StateOfHealth". The BQ27220 has Temperature at 0x06 and Voltage at 0x08 (unlike most other BQ27xxx variants). Register 0x2C is StateOfCharge (RSOC, 0–100%), not StateOfHealth. DesignCapacity is at register 0x3C (not read by the firmware's regular status polling).

All battery status is read in battery_bq27220_read_all_status (0x4200D482) and cached in the global battery struct at 0x3FC965C0.

Charging Detection

battery_bq27220_poll_charging (0x4200D61C): reads StateOfCharge and Current registers. Returns Current > 0 (positive current = charging). Called at end of setup() to initialize charging indicator flag at 0x3FCAB97A.

Battery Percentage (State of Charge) — How It Works

The firmware does not compute battery percentage from voltage. Instead, the BQ27220 fuel gauge computes SOC internally using TI's CEDV (Compensated End of Discharge Voltage) algorithm, and the firmware reads the result directly from register 0x2C (StateOfCharge).

Read path:

  1. battery_bq27220_read_all_status → calls sub_42000434battery_bq27220_read_u16(obj, 0x2C)
  2. Raw u16 value (0–100) stored at 0x3FC965C6
  3. Displayed directly as battery percentage text (e.g., "85%") in status bar functions (sub_4200DE14, sub_42013F56, sub_420140BC)
  4. Low battery warning triggers when MEMORY[0x3FC965C6] <= 9 (in sub_42020658)

Sample code (Arduino/ESP32, matching the X3 hardware):

#include <Wire.h>

#define BQ27220_ADDR        0x55
#define REG_TEMPERATURE     0x06  // 0.1 K
#define REG_VOLTAGE         0x08  // mV
#define REG_CURRENT         0x0C  // mA (signed)
#define REG_REMAINING_CAP   0x10  // mAh
#define REG_FULL_CHARGE_CAP 0x12  // mAh
#define REG_STATE_OF_CHARGE 0x2C  // 0–100 %
#define REG_STATE_OF_HEALTH 0x2E  // %
#define REG_OPERATION_STATUS 0x3A

// Read a 16-bit little-endian register from the BQ27220
uint16_t bq27220_read_u16(uint8_t reg) {
    Wire.beginTransmission(BQ27220_ADDR);
    Wire.write(reg);
    Wire.endTransmission(false);
    Wire.requestFrom(BQ27220_ADDR, (uint8_t)2);
    uint8_t lo = Wire.read();
    uint8_t hi = Wire.read();
    return (hi << 8) | lo;
}

void setup() {
    Wire.begin(/*sda=*/20, /*scl=*/0, /*freq=*/400000);
    Serial.begin(115200);
}

void loop() {
    uint16_t soc     = bq27220_read_u16(REG_STATE_OF_CHARGE);
    uint16_t voltage = bq27220_read_u16(REG_VOLTAGE);
    int16_t  current = (int16_t)bq27220_read_u16(REG_CURRENT);
    uint16_t temp    = bq27220_read_u16(REG_TEMPERATURE);

    Serial.printf("SOC: %u%%  Voltage: %u mV (%.3f V)  Current: %d mA  Temp: %.1f °C\n",
                  soc, voltage, voltage / 1000.0f,
                  current, temp / 10.0f - 273.15f);

    delay(2000);
}

CEDV Battery Profile (Data Flash Configuration)

During initialization, battery_bq27220_configure (0x42000802) programs the BQ27220's data flash with a battery-specific profile via the MACDataSum register (0x3E). The profile is a command table at DROM address 0x3C1614F0.

Battery Cell Parameters:

Parameter DF Address Value Unit
GaugingConfig 0x929B 0x0D31 flags (see below)
FullChargeCapacity 0x929D 642 mAh
DesignCapacity 0x929F 642 mAh
EMF (nominal cell voltage) 0x92A3 3750 mV
C0 (capacity coefficient) 0x92A9 200
R0 (initial resistance) 0x92AB 400
T0 (temperature factor) 0x92AD 4200
R1 (resistance coefficient) 0x92AF 408
TC (temperature coefficient) 0x92B1 11
C1 (capacity coefficient) 0x92B2 0

GaugingConfig (0x0D31) decoded:

  • CCT=1 (Continuous Coulomb Tracking)
  • SC=1 (Smoothing for capacity reporting)
  • FIXED_EDV0=1 (Fixed end-of-discharge voltage)
  • FCC_LIM=1 (Full Charge Capacity limiting)
  • FC_FOR_VDQ=1 (Full Charge for Valid Discharge Qualification)
  • IGNORE_SD=1 (Ignore self-discharge)

OCV–SOC Lookup Table (DOD Profile)

The CEDV algorithm uses an Open Circuit Voltage (OCV) vs Depth of Discharge (DOD) table programmed into data flash. DOD is the inverse of SOC: DOD 0% = fully charged (100% SOC), DOD 100% = fully discharged (0% SOC).

DF Address Parameter OCV (mV) SOC (%)
0x92BD StartDOD0 4250 100%
0x92BF StartDOD10 4100 90%
0x92C1 StartDOD20 3950 80%
0x92C3 StartDOD30 3850 70%
0x92C5 StartDOD40 3780 60%
0x92C7 StartDOD50 3750 50%
0x92C9 StartDOD60 3720 40%
0x92CB StartDOD70 3700 30%
0x92CD StartDOD80 3680 20%
0x92CF StartDOD90 3650 10%
0x92D1 StartDOD100 3200 0%

This is the characteristic discharge curve of the X3's single-cell Li-ion battery (642 mAh). The voltage drops steeply below 3650 mV (10% SOC) and above 4100 mV (90% SOC), with a relatively flat region between 3700–3850 mV (30–70% SOC).

End-of-Discharge Voltage Thresholds (EDV)

DF Address Parameter Voltage (mV) Purpose
0x92B4 EDV0 3210 First discharge termination threshold
0x92B7 EDV1 3150 Second threshold (deeper discharge)
0x92BA EDV2 3100 Final cutoff voltage

When cell voltage drops below EDV thresholds, the BQ27220 sets status flags indicating the battery should be recharged. EDV2 (3100 mV) is the hard cutoff — operating below this risks cell damage.

Other Configuration Parameters

DF Address Parameter Value
0x91DE CurrentDeadband 1 mA
0x9217 SleepCurrent 1 mA

How the BQ27220 CEDV Algorithm Computes SOC

The CEDV algorithm combines multiple inputs to produce an accurate SOC estimate:

  1. OCV-based estimation: When the battery is at rest (no load current for a period), the gauge measures Open Circuit Voltage and looks it up in the DOD profile table above to get an initial SOC estimate.

  2. Coulomb counting: During active use, the gauge integrates current flow over time (using its built-in current sense resistor) to track charge entering/leaving the battery. This is the primary SOC tracking method during operation.

  3. Voltage compensation: The CEDV parameters (C0, R0, T0, R1, C1, TC) model the battery's internal impedance. The gauge compensates for voltage drops under load (IR drop) so that voltage-based SOC corrections are accurate even while the battery is being discharged.

  4. Temperature compensation: The TC coefficient adjusts for the fact that battery capacity and impedance vary with temperature.

  5. Learning: Over charge/discharge cycles, the gauge updates its FullChargeCapacity estimate based on actual measured charge delivered, improving accuracy over time.

The result is a single 0–100% value at register 0x2C that the firmware reads and displays directly — no additional computation needed on the ESP32 side.

Configuration / Security Sequence

The BQ27220 configuration (battery_bq27220_configure, 0x42000802) performs:

  1. Read Device ID: Control(0x0001) → MACData(0x40), expects 0x0220
  2. Unseal (battery_bq27220_unseal, 0x42000248): reads OperationStatus, if SEC bits == 3 (sealed):
    • Control(0x0414) — Unseal key 1
    • Wait 5s
    • Control(0x3672) — Unseal key 2
    • Wait 5s
    • Verify SEC bits == 2 (unsealed)
  3. Enter Config Update (battery_bq27220_enter_cfgupdate, 0x420003B0): Control(0x0041), poll OperationStatus bit 5 (CFGUPMODE), timeout 4000s
  4. Write Data Flash (battery_bq27220_write_dataflash, 0x420005F8): writes configuration profile via MACDataSum(0x3E) register, with command table (delays, byte/word/dword writes)
  5. Exit Config Update: Control(0x0091), wait for CFGUPMODE to clear
  6. Seal (battery_bq27220_seal, 0x420001D6): Control(0x0030), verify SEC bits == 3

Unseal keys: 0x0414, 0x3672 (found in battery_bq27220_unseal)

OperationStatus Register (0x3A) Bits

Bits Field Values
1:0 SEC 0=Full Access, 1=Unsealed, 2=Sealed, 3=Reserved
5 CFGUPMODE 1 = Config update mode active
10 INITCOMP 1 = Initialization complete

Display (SSD1677 E-Paper)

Signal GPIO Notes
CS 21 Directly toggled in SPI send functions
DC 4 LOW = command, HIGH = data
RST 5 Active LOW, 15ms reset delay
BUSY 6 Active LOW (loop until HIGH = idle), 5s timeout
SCLK 8 Shared SPI bus
MOSI 10 Shared SPI bus

Resolution: 792 x 528 (confirmed in sub_42066090sub_42065972 params) SPI frequency: 10 MHz (set in sub_42065972)

Button System (Resistor Ladder ADC)

GPIO 1 — Primary (4 buttons)

ADC configured with 11dB attenuation. Fixed thresholds from sub_42006B8C:

ADC Range Button (orientation-dependent)
0 – 1194 Button A (OK/Power or UP)
1194 – 2408 Button B (BACK or DOWN)
2408 – 3260 Button C (DOWN or BACK)
3260 – 3999 Button D (UP or Power)
> 3999 No press (falls through to GPIO 2)

GPIO 2 — Secondary (2 buttons)

Used when GPIO 1 reads > 3999 (no button pressed on primary). Fixed thresholds from button_read_fixed_gpio2 (0x42006C42):

ADC Range Button (orientation 3) Button (default)
0 – 1944 UP (code 1) OK (code 0)
1945 – 2583 OK (code 0) UP (code 1)
> 3999 No press No press

Center values (from X4 hardware measurements): ~3 (low button), ~2205 (high button).

Calibration

NVS namespace "eepuser", keys: adcUP, adcDOWN, adcBACK, adcOK, adcUP2, adcDOWN2. Tolerance: ±319 ADC counts around calibrated center values. Calibration function: sub_420126B0

Button Codes (from sub_42006294 / sub_42006910)

Code Short Press Long Press Code
0 OK/Power 6
1 UP 7
2 DOWN 8
3 BACK 9
4 Button 5 10
5 Button 6 11

microSD Card (Shared SPI Bus)

Signal GPIO
CS (SS) 12
MISO (DO) 7
MOSI (DI) 10
SCLK 8

GPIO 13 controls power to SD card (OUTPUT with pullup, toggled in setup/sleep).

USB (Serial/JTAG)

GPIO 18 (D-) and GPIO 19 (D+) are the ESP32-C3 built-in USB Serial/JTAG interface. Hardware registers at 0x60043000 (USB_SERIAL_JTAG peripheral).

During shutdown (sub_42093E74):

  1. Clears USB_SERIAL_JTAG registers (0x60043010, 0x60043014)
  2. Sets GPIO 18/19 as open-drain output LOW (disconnect USB)
  3. Removes console putchar handler (ets_install_putc2(0))

Previous analysis incorrectly identified GPIO 18/19 as I2C SDA/SCL.

Corrections from Previous Version

Item Previous (Wrong) Corrected
GPIO 18 I2C SDA (speculative) USB D-
GPIO 19 I2C SCL (speculative) USB D+
GPIO 0 Battery ADC (speculative) I2C SCL
GPIO 20 USB detect (speculative) I2C SDA
Display RST Unknown (struct offset 0x14) GPIO 5 (confirmed)
Battery GPIO ADC (speculative) BQ27220 fuel gauge via I2C at 0x55
Gyroscope Unknown chip QMI8658 at I2C 0x6B
RTC Not identified DS3231 at I2C 0x68
BQ27220 reg 0x06 Voltage (wrong) Temperature (0.1K units)
BQ27220 reg 0x08 Temperature (wrong) Voltage (mV units)
BQ27220 reg 0x2C StateOfHealth (wrong) StateOfCharge / RSOC (0–100%)
BQ27220 reg 0x2E DesignCapacity (wrong) StateOfHealth (%)
BQ27220 DesignCapacity reg 0x2E (wrong) reg 0x3C (not read by firmware)

Key Functions Map

Function Address Purpose
Main setup sub_4203D9D4 Arduino setup() — GPIO init, SPI, display, filesystem
BLE config / global init sub_42055840 Initializes display struct, SPI, BLE, all peripherals
SPI.begin sub_4205A67C SPIClass::begin(sclk, miso, mosi, ss)
Wire.begin sub_42004146 I2C init: Wire.begin(sda=20, scl=0, freq=400000)
TwoWire::begin sub_4205A2E4 Inner Wire.begin (setPins + i2c_driver_install)
i2c_param_config sub_4209A15A ESP-IDF I2C bus configuration
i2c_set_pin sub_42099EA8 ESP-IDF I2C pin assignment
I2C probe sub_4200C63A Ping I2C address (start+addr+stop)
EPD constructor sub_42066090 GxEPD2(cs=21, dc=4, rst=5, busy=6, 792x528)
EPD SPI device init sub_42065AC0 Configure CS/DC/RST/BUSY pins, HW reset
EPD display init sub_4200DAB2 Full display initialization sequence
EPD send command sub_420050A8 DC=LOW, CS=LOW, SPI.transfer(cmd), CS=HIGH
EPD send data sub_42005104 CS=LOW, SPI.transfer(data, len), CS=HIGH
EPD waitWhileBusy sub_42004E04 Poll GPIO 6 until HIGH, timeout 5s
EPD reset sub_42065A14 Toggle RST pin from struct
EPD write LUT sub_4200531A Send LUT commands 0x20-0x24
EPD write image sub_42007AB4 Write LUT + RAM data (cmds 0x13, 0x10)
Gyro init sub_4200CA0E QMI8658 probe + configuration
Gyro WHO_AM_I sub_420912D4 Read register 0, return chip ID
Gyro driver init sub_420913CE WHO_AM_I check + sensor config
RTC init sub_4200C662 DS3231 probe + time read
RTC read time sub_420908FA Read 7 BCD time registers from 0x68
RTC time sync sub_4200C810 Read RTC + apply timezone offset
Battery init battery_bq27220_init (0x4200D52E) BQ27220 probe + driver setup
Battery config battery_bq27220_configure (0x42000802) Full initialization (device ID check, data flash)
Battery control cmd battery_bq27220_control_cmd (0x420000B0) Send Control() subcommand to BQ27220
Battery read reg battery_bq27220_read_reg (0x420000C6) Read N bytes from BQ27220 register
Battery read u16 battery_bq27220_read_u16 (0x42000168) Read 16-bit LE value from register
Battery read all battery_bq27220_read_all_status (0x4200D482) Read voltage, temp, current, capacity, SOH
Battery poll charging battery_bq27220_poll_charging (0x4200D61C) Read SOH + current, return charging state
Battery unseal battery_bq27220_unseal (0x42000248) Unseal with keys 0x0414/0x3672
Battery seal battery_bq27220_seal (0x420001D6) Seal with Control(0x0030)
Battery write flash battery_bq27220_write_dataflash (0x420005F8) Write config profile via MACDataSum
Button read (calibrated) button_read_calibrated (0x42006294) ADC read GPIO 1/2, map via NVS thresholds
Button read (fixed) button_read_fixed_gpio1 (0x42006B8C) ADC read GPIO 1, fixed threshold mapping
Button read (GPIO 2) button_read_fixed_gpio2 (0x42006C42) ADC read GPIO 2 only
analogRead sub_42096500 Arduino ADC read wrapper
adc1_config_channel_atten sub_4209B94A ESP-IDF ADC1 channel config
digitalWrite sub_420965FE gpio_set_level wrapper
pinMode sub_42096574 gpio_config wrapper
digitalRead sub_42096602 gpio_get_level wrapper
delay sub_42096BA2 vTaskDelay wrapper
millis sub_42096B82 xTaskGetTickCount wrapper
USB/UART deinit sub_42093E74 Shutdown USB D-/D+, clear JTAG regs
Sleep/power power_enter_deep_sleep (0x4200CB6A) Deep sleep entry, toggles GPIO 13
Boot wake handler power_boot_wake_handler (0x4200CF1E) Handle wake-from-sleep on boot
Power wake handler power_wake_handler (0x4200D0B6) GPIO 3 polling task during operation
Button event handler power_handle_button_event (0x4200CCA2) Process long-press power/sleep events
Save bookmark + sleep power_save_bookmark_and_sleep (0x4200CD4C) Save reading state, enter sleep
Free sensors power_free_sensors (0x4200CB24) Free gyro/sensor objects before sleep
Wake cause esp_sleep_get_wakeup_cause (0x420C91B2) Read RTC_CNTL wake cause register
GPIO wakeup config esp_sleep_enable_gpio_wakeup (0x420C909C) Configure GPIO deep-sleep wakeup
Timer wakeup config esp_sleep_enable_timer_wakeup (0x420C9054) Configure timer deep-sleep wakeup
Deep sleep esp_deep_sleep (0x420C9068) Enter deep sleep with timer
Settings menu settings_menu_handler (0x42039138) Handle settings button presses
Button calibration sub_420126B0 Read ADC, compare thresholds, save to NVS

Display Object Layout

Outer struct at 0x3FC966E0 (BSS)

Offset Type Field
0x08 int16 Width (792)
0x0A int16 Height (528)
0x0C int16 Computed width (rotation-dependent)
0x0E int16 Computed height (rotation-dependent)
0x1A byte Rotation mode (0-3)

Inner struct at 0x3FC96704 (BSS, offset 0x24 from outer)

Offset Type Field Value
0x00 dword VTable pointer 0x3C5AD4EC
0x10 int16 CS pin 21
0x12 int16 DC pin 4
0x14 int16 RST pin 5
0x16 int16 BUSY pin 6
0x20 byte Reset active level 0 (active LOW)
0x21 byte Unknown flag 1
0x24 dword SPI class pointer 0x3FCA3F04
0x36 word Reset delay (ms) 15

Pin values are set at runtime by sub_42055840 via sub_42066090(obj, cs=21, dc=4, rst=5, busy=6).

Status Register (0x3FCAB9B8)

Bit flags for peripheral error states:

Bit Flag Meaning Set by
3 (0x08) Clock error DS3231 RTC not found at 0x68 sub_4200C662
4 (0x10) Battery error BQ27220 not found at 0x55 sub_4200D52E
5 (0x20) Gyro error QMI8658 not found at 0x6B sub_4200CA0E

Error display function: sub_4200E57C — shows multi-language error messages based on status bits.

Power Button & Sleep System (GPIO 3)

GPIO 3 is the power button input (active LOW = button pressed). It serves as both the sleep trigger during normal operation and the deep-sleep wakeup source via esp_sleep_enable_gpio_wakeup(bitmask=0x08, level=LOW).

RTC Fast Memory State Variables

These 32-bit values in RTC IRAM survive deep sleep:

Address Name Values
0x50000020 Wake state 0=normal, 1=soft-sleep, 2=timed-sleep, 3=power-off
0x5000002C Power-off flag 0=running/wake-from-button, 1=entering power-off
0x50000028 Saved page/bookmark Preserved across sleep for resume
0x50000034 Saved state 2 Cleared on clean shutdown
0x50000038 Saved state 3 Written before bookmark-sleep

Boot Flow (sub_4203D9D4 = Arduino setup())

  1. pinMode(13, OUTPUT); digitalWrite(13, HIGH) — Power on SD card
  2. pinMode(3, INPUT) — Power button
  3. pinMode(1, INPUT); pinMode(2, INPUT) — Button ADCs
  4. Call power_boot_wake_handler (0x4200CF1E) — handles wake-from-sleep logic
  5. Initialize SPI, display, filesystem, I2C, peripherals
  6. dword_5000002C = 0 — Mark as running

Wake Handler (power_boot_wake_handler, 0x4200CF1E)

Called during boot to determine if this is a cold start or a wake from sleep.

  1. Read NVS settings: eepInit (first-boot flag), onTime (auto-on timer), systemErr
  2. If eepInit is set (not first boot):
    • Get esp_sleep_get_wakeup_cause() → if cause == 7 (timer): clear wake state, clear onTime
    • Read button state (check if any button held during boot)
    • If onTime > 0: poll GPIO 3 in a loop for onTime × 100ms:
      • If GPIO 3 == HIGH (button released): call power_enter_deep_sleep(0, 0, 1, 0) → immediate shutdown, no display update
      • If loop completes without release: device stays on (button was held long enough)
    • This implements: hold power button for onTime × 100ms to turn on

Power Button During Operation (power_wake_handler, 0x4200D0B6)

Called from setup(), runs as a task. Polls GPIO 3:

  1. Record start time via millis()
  2. Loop: digitalRead(3) every ~60ms
  3. When GPIO 3 == HIGH (button released after being pressed):
    • Initialize I2C bus and RTC
    • Measure charge state for 200ms
    • Enter timed deep sleep: power_enter_deep_sleep(60000 - 1000*charge_state, 0, 0, 0)
    • Sleep duration ≈ 60 seconds (adjusted down if USB charging detected)
  4. Timeout (onTime × 100ms): function returns, device continues running

Sleep Entry (power_enter_deep_sleep, 0x4200CB6A)

Parameters: (timer_ms, timer_hi, immediate_off, update_display)

power_enter_deep_sleep(timer_ms, 0, immediate_off, update_display)
  │
  ├─ if update_display: write sleep image to EPD, send display hibernate command
  ├─ End SPI bus
  ├─ if BLE active: disable BLE, save state to NVS
  ├─ delay(10)
  ├─ pinMode(3, INPUT)
  ├─ esp_sleep_enable_gpio_wakeup(0x08, LOW)  ← GPIO 3 as wake source
  │
  ├─ if immediate_off:
  │    ├─ dword_5000002C = 0
  │    ├─ pinMode(13, OUTPUT); digitalWrite(13, LOW)  ← Power off SD
  │    └─ esp_deep_sleep_start()  ← Indefinite sleep (GPIO wake only)
  │
  └─ else (timed sleep):
       ├─ dword_5000002C = 1
       ├─ Free gyro/sensor objects
       ├─ if timer_ms > 0:
       │    ├─ dword_50000020 = 2  (timed-sleep state)
       │    ├─ pinMode(2, INPUT)
       │    ├─ esp_sleep_enable_gpio_wakeup(0x04, LOW)  ← GPIO 2 also wakes
       │    ├─ Deinit USB, power off GPIO 13
       │    └─ esp_deep_sleep(timer_ms * 1000)  ← Sleep with timer
       │
       └─ dword_50000020 = 3  (power-off state)
          ├─ Deinit USB, power off GPIO 13
          └─ esp_deep_sleep_start()  ← Indefinite sleep

Wake Sources by Sleep Mode

Sleep Mode GPIO 3 (Power) GPIO 2 (Buttons) Timer
Power off (immediate_off=1) Wake on LOW No No
Timed sleep (60s cycle) Wake on LOW Wake on LOW Yes
Indefinite sleep (no timer) Wake on LOW No No

Sleep/Power-off Trigger Sources

Trigger Function Parameters Display Update
Power button released power_wake_handler timer=60s, timed No
OK long press (code 6) power_handle_button_event timer=1s, timed No
SPIFFS mount fail sub_4203D9D4 immediate_off, display Yes
BLE restore after sleep sub_4203D9D4 timer=1s, display Yes
Bookmark save + sleep power_save_bookmark_and_sleep timer=1s, display Yes
Clean shutdown power_clear_state_and_sleep variable timer No
Settings menu timeout sub_420126B0 (calibration) timer=1s, display Yes

NVS Sleep Settings (namespace "eepuser")

Key Type Values Set by
sleepTime u16 5, 15, 6000 (minutes; 6000 = never) Settings menu cycles: 5→15→6000
shutdownTime u16 20, 60, 6000 (minutes; 6000 = never) Settings menu cycles: 20→60→6000
onTime u8 0–5 (×100ms hold-to-wake time; 0=instant) Settings menu cycles in steps of 5

ESP32-C3 Wake Cause Register (0x600080F8)

esp_sleep_get_wakeup_cause (0x420C91B2) returns:

Value Cause Register Bit
0 Power-on reset / no wake
4 GPIO wake (power button pressed) bit 3 (0x08)
7 Timer wake (timed sleep expired) bit 2 (0x04)
8 UART wake bits 6-7 (0xC0)
9 Unknown bit 5 (0x20)
12 Unknown bit 10 (0x400)

X3 vs X4 GPIO Comparison

GPIO X3 Function X4 Function Notes
0 I2C SCL Battery ADC (voltage divider) X4 reads battery via analog; X3 uses BQ27220 fuel gauge
1 Button ADC (4 buttons) Button ADC (4 buttons) Same resistor ladder approach
2 Button ADC (2 buttons) Button ADC (2 buttons) Same
3 Power button (digital) Power button (digital) Same — active LOW, 1s hold for sleep/wake
4 EPD DC EPD DC Same
5 EPD RST EPD RST Same
6 EPD BUSY EPD BUSY Same
7 SPI MISO SPI MISO Same
8 SPI SCLK SPI SCLK Same
9 Boot pin (unused) Boot pin (unused) Same
10 SPI MOSI SPI MOSI Same
12 SD Card CS SD Card CS Same
13 Power/SD control (not documented) X3 toggles for SD power
18 USB D- USB D- Same
19 USB D+ USB D+ Same
20 I2C SDA USB detect (charging) X4 uses GPIO20 to detect USB connection
21 EPD CS EPD CS Same

Key Hardware Differences

Feature X3 X4
Display 792×528 (SSD1677) 800×480 (SSD1677, GDEQ0426T82)
Battery monitoring BQ27220 fuel gauge via I2C (0x55) Analog voltage divider on GPIO0
I2C bus GPIO0 (SCL) + GPIO20 (SDA), 400kHz Not confirmed (no I2C devices documented)
I2C devices QMI8658 gyro, DS3231 RTC, BQ27220 None documented
USB detect Not via GPIO (BQ27220 Current > 0) GPIO20 level check
Flash 16MB 16MB
RAM 400KB (no PSRAM) 400KB (no PSRAM)
Firmware version V1.0.7 V3.0.1
OTA server 8.216.34.42:5001 (raw IP) gotaserver.xteink.com
Device type ESP32C3_X3 ESP32C3

Unresolved

  • GPIO 9: ESP32-C3 BOOT strapping pin. Not found in application code. May be unused or used for boot mode selection only.
  • GPIO 3 dual role: Primarily a power button. The QMI8658 gyro interrupt line (INT1) may also be connected to GPIO 3, sharing the pin for "shake to wake" during deep sleep. Hardware verification needed to confirm whether GPIO 3 carries both the power button signal and gyro INT1.
@CrazyCoder
Copy link
Author

CrazyCoder commented Feb 12, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment