diff --git a/README.md b/README.md index 5a0fe1a..7e09655 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,11 @@ Before flashing custom firmware, back up the factory firmware: # Read entire 16MB flash esptool.py --chip esp32c3 --port COM5 read_flash 0x0 0x1000000 firmware_backup.bin ``` +```powershell +# Read only app0 (faster) +esptool.py --chip esp32c3 --port COM5 read_flash 0x10000 0x640000 app0_backup.bin +``` + ### Restore Original Firmware @@ -56,6 +61,11 @@ To restore the backed-up firmware: esptool.py --chip esp32c3 --port COM5 write_flash 0x0 firmware_backup.bin ``` +```powershell +# Write back only app0 (faster) +esptool.py --chip esp32c3 --port COM5 write_flash 0x10000 app0_backup.bin +``` + **Important**: Make sure to use the correct COM port for your device. ## Notes @@ -67,9 +77,9 @@ esptool.py --chip esp32c3 --port COM5 write_flash 0x0 firmware_backup.bin ## Open Tasks - Better font rendering with grayscale support -- Power on/off +- ~~Power on/off~~ - SD card reader -- Read battery percentage +- ~~Read battery percentage~~ - WiFi - Bluetooth @@ -89,6 +99,16 @@ The XteinkX4 uses **resistor ladder networks** connected to two ADC pins for but - Volume Up: ~2205 - Volume Down: ~3 +**GPIO3 (Power button)**: +- Pressed: ~3 +- This example uses a 2-second-long press for sleep and a 1.5-second-long press to wake from sleep + +**Battery ADC**: +- GPIO0, raw value ranges up to ~2800 when charging. ~2760 when not charging and full. +- Voltage divider is ~2 (2 x 10K resistors), `CONV_FACTOR=1.6113` for [this library](https://github.com/pangodream/18650CL) +- [See here](https://www.pangodream.es/esp32-getting-battery-charging-level) for details how voltage and charge level are calculated +- `CONV_FACTOR` may need calibration depending on the device, `1.5176` working well on mine + ### Implementation Notes - Use threshold ranges (e.g., `value > 3200 && value < 3700`) to detect button presses diff --git a/default_16MB.csv b/default_16MB.csv new file mode 100644 index 0000000..67d7737 --- /dev/null +++ b/default_16MB.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x640000, +app1, app, ota_1, 0x650000,0x640000, +spiffs, data, spiffs, 0xc90000,0x360000, +coredump, data, coredump,0xFF0000,0x10000, diff --git a/platformio.ini b/platformio.ini index f2f7ca2..75a0c80 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,17 +4,23 @@ board = esp32-c3-devkitm-1 framework = arduino monitor_speed = 115200 upload_speed = 921600 - +; upload to app0 +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_upload.offset_address = 0x10000 ; Board configuration board_build.flash_mode = dio -board_build.flash_size = 4MB +board_build.flash_size = 16MB +board_build.partitions = default_16MB.csv ; Libraries lib_deps = zinggjm/GxEPD2@^1.5.9 + https://github.com/pangodream/18650CL ; Build flags build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 -DCONFIG_ESP_TASK_WDT_INIT=0 + -DDEBUG_IO=1 diff --git a/src/main.cpp b/src/main.cpp index 2ef97d4..d99c55c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include #include #include "image.h" +#include "Pangodream_18650_CL.h" // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) #define EPD_SCLK 8 // SPI Clock @@ -16,6 +17,17 @@ // Button pins (ADC resistor ladders) #define BTN_GPIO1 1 // 4 buttons: Back, Confirm, Left, Right #define BTN_GPIO2 2 // 2 buttons: Volume Up, Volume Down +#define BTN_GPIO3 3 // Power button +#define BAT_GPIO 0 // Battery voltage + +#define ADC_PIN 0 +#define CONV_FACTOR 1.5176 +#define READS 10 +#define CHARGER_THRESHOLD 2770 + +Pangodream_18650_CL BL(ADC_PIN, CONV_FACTOR, READS); + +static int rawBat = 0; // Button enum enum Button @@ -26,7 +38,8 @@ enum Button CONFIRM, BACK, VOLUME_UP, - VOLUME_DOWN + VOLUME_DOWN, + POWER }; // Display command enum @@ -35,11 +48,13 @@ enum DisplayCommand DISPLAY_NONE = 0, DISPLAY_HEADER, DISPLAY_TEXT, + DISPLAY_SLEEP, TOGGLE_IMAGE }; // Button ADC thresholds const int BTN_THRESHOLD = 100; // Threshold tolerance +const int BTN_POWER_VAL = 3; const int BTN_RIGHT_VAL = 3; const int BTN_LEFT_VAL = 1470; const int BTN_CONFIRM_VAL = 2655; @@ -66,6 +81,8 @@ const char *getButtonName(Button btn) return "VOLUME UP pressed!"; case VOLUME_DOWN: return "VOLUME DOWN pressed!"; + case POWER: + return "POWER pressed!"; default: return ""; } @@ -76,7 +93,13 @@ Button GetPressedButton() { int btn1 = analogRead(BTN_GPIO1); int btn2 = analogRead(BTN_GPIO2); + int btn3 = analogRead(BTN_GPIO3); + // Check BTN_GPIO3 (Power button) + if (btn3 < BTN_POWER_VAL + BTN_THRESHOLD) + { + return POWER; + } // Check BTN_GPIO1 (4 buttons on resistor ladder) if (btn1 < BTN_RIGHT_VAL + BTN_THRESHOLD) { @@ -110,7 +133,8 @@ Button GetPressedButton() // GxEPD2 display - Using GxEPD2_426_GDEQ0426T82 // Note: XteinkX4 has 4.26" 800x480 display -GxEPD2_BW display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY)); +GxEPD2_BW display( + GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY)); // FreeRTOS task for non-blocking display updates TaskHandle_t displayTaskHandle = NULL; @@ -146,12 +170,14 @@ void displayUpdateTask(void *parameter) display.setFont(&FreeMonoBold12pt7b); display.setCursor(20, 100); display.print(getButtonName(currentPressedButton)); + display.setCursor(20, 200); + display.print(esp_reset_reason()); } while (display.nextPage()); } else if (cmd == DISPLAY_TEXT) { // Use partial refresh for text updates - display.setPartialWindow(0, 75, display.width(), 150); + display.setPartialWindow(0, 75, display.width(), 300); display.firstPage(); do { @@ -159,6 +185,14 @@ void displayUpdateTask(void *parameter) display.setFont(&FreeMonoBold12pt7b); display.setCursor(20, 100); display.print(getButtonName(currentPressedButton)); + display.setCursor(20, 160); + display.printf("Battery: %s", rawBat > CHARGER_THRESHOLD ? "Charging" : "Discharging"); + display.setCursor(40, 200); + display.printf("Raw: %i", rawBat); + display.setCursor(40, 240); + display.printf("Volts: %.2f", BL.getBatteryVolts()); + display.setCursor(40, 280); + display.printf("Charge level: %i%%", BL.getBatteryChargeLevel()); } while (display.nextPage()); } else if (cmd == TOGGLE_IMAGE) @@ -179,6 +213,20 @@ void displayUpdateTask(void *parameter) } } while (display.nextPage()); } + else if (cmd == DISPLAY_SLEEP) + { + // Use full window for initial welcome screen with header + display.setFullWindow(); + display.firstPage(); + do + { + display.fillScreen(GxEPD_WHITE); + // Header font + display.setFont(&FreeMonoBold18pt7b); + display.setCursor(120, 380); + display.print("Sleeping..."); + } while (display.nextPage()); + } } vTaskDelay(10 / portTICK_PERIOD_MS); } @@ -186,6 +234,40 @@ void displayUpdateTask(void *parameter) void setup() { + // Check if boot was triggered by the Power Button (Deep Sleep Wakeup) + // If triggered by RST pin or Battery insertion, this will be false, allowing normal boot. + if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) + { + // Woke up from sleep via Power Button. Verifying long press... + // Temporarily configure pin as digital input to check state + pinMode(BTN_GPIO3, INPUT); + + const unsigned long LONG_PRESS_MS = 1500; // 1.5 seconds required + unsigned long pressStart = millis(); + bool bootConfirmed = true; + + // Monitor button state for the duration + while (millis() - pressStart < LONG_PRESS_MS) + { + // If button reads HIGH (released) before time is up + if (digitalRead(BTN_GPIO3) == HIGH) + { + bootConfirmed = false; + break; + } + delay(10); + } + + if (!bootConfirmed) + { + // Button released too early. Returning to sleep. + pinMode(BTN_GPIO3, INPUT_PULLUP); + // IMPORTANT: Re-arm the wakeup trigger before sleeping again + esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); + esp_deep_sleep_start(); + } + } + Serial.begin(115200); // Wait for serial monitor @@ -195,14 +277,17 @@ void setup() delay(10); } + Serial.println("\n================================="); Serial.println(" xteink x4 sample"); Serial.println("================================="); Serial.println(); // Initialize button pins + pinMode(BAT_GPIO, INPUT); pinMode(BTN_GPIO1, INPUT); pinMode(BTN_GPIO2, INPUT); + pinMode(BTN_GPIO3, INPUT); // Initialize SPI with custom pins SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS); @@ -246,6 +331,46 @@ void loop() Serial.println(getButtonName(currentButton)); currentPressedButton = currentButton; + + if (currentButton == POWER) + { + Serial.println("Power button pressed"); + displayCommand = DISPLAY_TEXT; + // 1. Switch pin to Digital Input with Pull-up + // This is crucial! 'analogRead' configures the pin as ADC, often disabling the digital input buffer. + // If the digital buffer is disabled, the sleep controller reads '0' (LOW) and wakes immediately. + // The Pull-up also prevents the pin from floating if the external circuit is open. + pinMode(BTN_GPIO3, INPUT_PULLUP); + + // 2. Wait for button release + // We wait until the digital state is HIGH (released) + unsigned long pressedTime = millis(); + bool longPress = false; + while (digitalRead(BTN_GPIO3) == LOW) + { + delay(50); + if (millis() - pressedTime > 2000) + { + longPress = true; + break; + } + } + + // Power button long pressed, go to sleep + if (longPress) + { + displayCommand = DISPLAY_SLEEP; + Serial.println("Power button released after a long press. Entering deep sleep."); + delay(2000); // Allow Serial buffer to empty and display to update + + // 3. Enable Wakeup on LOW + esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW); + + // 4. Enter Deep Sleep + esp_deep_sleep_start(); + } + } + if (currentButton == CONFIRM) { displayCommand = TOGGLE_IMAGE; @@ -257,5 +382,37 @@ void loop() } lastButton = currentButton; + +#ifdef DEBUG_IO + // Log raw analog levels of BTN1 and BTN2 not more often than once per second + static unsigned long lastLogMs = 0; + unsigned long now = millis(); + if (now - lastLogMs >= 1000) + { + rawBat = analogRead(BAT_GPIO); + int rawBtn1 = analogRead(BTN_GPIO1); + int rawBtn2 = analogRead(BTN_GPIO2); + int rawBtn3 = analogRead(BTN_GPIO3); + Serial.print("ADC BTN1="); + Serial.print(rawBtn1); + Serial.print(" BTN2="); + Serial.print(rawBtn2); + Serial.print(" BTN3="); + Serial.print(rawBtn3); + + Serial.println(""); + + Serial.print("Value from pin: "); + Serial.println(rawBat); + Serial.print("Average value from pin: "); + Serial.println(BL.pinRead()); + Serial.print("Volts: "); + Serial.println(BL.getBatteryVolts()); + Serial.print("Charge level: "); + Serial.println(BL.getBatteryChargeLevel()); + Serial.println(""); + lastLogMs = now; + } delay(50); +#endif }