| 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 |
Three devices share the I2C bus:
| 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".
| 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.
| 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.
| 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) | — |
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.
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.
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:
battery_bq27220_read_all_status→ callssub_42000434→battery_bq27220_read_u16(obj, 0x2C)- Raw u16 value (0–100) stored at
0x3FC965C6 - Displayed directly as battery percentage text (e.g., "85%") in status bar functions (
sub_4200DE14,sub_42013F56,sub_420140BC) - Low battery warning triggers when
MEMORY[0x3FC965C6] <= 9(insub_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);
}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 | mΩ |
| 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)
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).
| 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.
| DF Address | Parameter | Value |
|---|---|---|
| 0x91DE | CurrentDeadband | 1 mA |
| 0x9217 | SleepCurrent | 1 mA |
The CEDV algorithm combines multiple inputs to produce an accurate SOC estimate:
-
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.
-
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.
-
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.
-
Temperature compensation: The TC coefficient adjusts for the fact that battery capacity and impedance vary with temperature.
-
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.
The BQ27220 configuration (battery_bq27220_configure, 0x42000802) performs:
- Read Device ID: Control(0x0001) → MACData(0x40), expects 0x0220
- 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)
- Enter Config Update (
battery_bq27220_enter_cfgupdate, 0x420003B0): Control(0x0041), poll OperationStatus bit 5 (CFGUPMODE), timeout 4000s - Write Data Flash (
battery_bq27220_write_dataflash, 0x420005F8): writes configuration profile via MACDataSum(0x3E) register, with command table (delays, byte/word/dword writes) - Exit Config Update: Control(0x0091), wait for CFGUPMODE to clear
- Seal (
battery_bq27220_seal, 0x420001D6): Control(0x0030), verify SEC bits == 3
Unseal keys: 0x0414, 0x3672 (found in battery_bq27220_unseal)
| 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 |
| 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_42066090 → sub_42065972 params)
SPI frequency: 10 MHz (set in sub_42065972)
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) |
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).
NVS namespace "eepuser", keys: adcUP, adcDOWN, adcBACK, adcOK, adcUP2, adcDOWN2.
Tolerance: ±319 ADC counts around calibrated center values.
Calibration function: sub_420126B0
| 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 |
| 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).
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):
- Clears USB_SERIAL_JTAG registers (0x60043010, 0x60043014)
- Sets GPIO 18/19 as open-drain output LOW (disconnect USB)
- Removes console putchar handler (
ets_install_putc2(0))
Previous analysis incorrectly identified GPIO 18/19 as I2C SDA/SCL.
| 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) |
| 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 |
| 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) |
| 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).
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.
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).
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 |
pinMode(13, OUTPUT); digitalWrite(13, HIGH)— Power on SD cardpinMode(3, INPUT)— Power buttonpinMode(1, INPUT); pinMode(2, INPUT)— Button ADCs- Call
power_boot_wake_handler(0x4200CF1E) — handles wake-from-sleep logic - Initialize SPI, display, filesystem, I2C, peripherals
dword_5000002C = 0— Mark as running
Called during boot to determine if this is a cold start or a wake from sleep.
- Read NVS settings:
eepInit(first-boot flag),onTime(auto-on timer),systemErr - If
eepInitis set (not first boot):- Get
esp_sleep_get_wakeup_cause()→ if cause == 7 (timer): clear wake state, clearonTime - Read button state (check if any button held during boot)
- If
onTime> 0: poll GPIO 3 in a loop foronTime × 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)
- If GPIO 3 == HIGH (button released): call
- This implements: hold power button for
onTime × 100msto turn on
- Get
Called from setup(), runs as a task. Polls GPIO 3:
- Record start time via
millis() - Loop:
digitalRead(3)every ~60ms - 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)
- Timeout (
onTime × 100ms): function returns, device continues running
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
| 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 |
| 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 |
| 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 |
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) |
| 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 |
| 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 |
- 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.
See https://gist.github.com/CrazyCoder/82fec0bbd0e515dcc237d3db7451ec6f for EPD (epaper display) analysis.