Merge pull request #4 from CrazyCoder/main

This commit is contained in:
CidVonHighwind
2025-11-22 16:09:59 +01:00
committed by GitHub
5 changed files with 282 additions and 57 deletions
+35 -11
View File
@@ -2,14 +2,22 @@
A simple sample project for the Xteink X4 e-ink device using the GxEPD2 library.
Join our Discord server for support and discussion:
- [Xteink eReader Community](https://discord.gg/2cdKUbWRE8)
![Sample](images/sample.png)
## Hardware
- **Device**: Xteink X4
- **Device**: [Xteink X4](https://www.xteink.com/products/xteink-x4)
- **Board**: ESP32-C3 (QFN32)
- **Display**: [4.26" E-Ink (800×480px, GDEQ0426T82, SSD1677 controller)](https://www.good-display.com/product/457.html)
- **Flash**: 16MB (SPI), 6.5MB app0 / app1 partitions + spiffs
- **RAM**: 400KB (327680 bytes usable with PlatformIO, no PSRAM)
- **Display**: [4.26" E-Ink (800×480px, GDEQ0426T82, SSD1677 controller)](https://www.good-display.com/product/457.html) (220PPI)
- **Custom SPI pins**: SCLK=8, MOSI=10, CS=21, DC=4, RST=5, BUSY=6
- **Battery**: 650mAh
- **Storage**: microSD card slot
### Resources
@@ -46,6 +54,7 @@ Before flashing custom firmware, back up the factory firmware:
# Read entire 16MB flash
python -m esptool --chip esp32c3 --port COM4 read_flash 0x0 0x1000000 firmware_backup.bin
```
```powershell
# Read only app0 (faster)
python -m esptool --chip esp32c3 --port COM4 read_flash 0x10000 0x640000 app0_backup.bin
@@ -94,7 +103,7 @@ python -m esptool --port COM4 write_flash 0xE000 otadata_boot_app1.bin
- [x] Wakeup and deep sleep
- [x] Read battery percentage
- [ ] Better rendering with grayscale support
- [ ] SD card reader
- [x] SD card reader
- [ ] WiFi
- [ ] Bluetooth
@@ -105,24 +114,39 @@ The XteinkX4 uses **resistor ladder networks** connected to two ADC pins for but
### Button ADC Values
**GPIO1 (4 buttons)**:
- Back: ~3470
- Confirm: ~2655
- Left: ~1470
- Right: ~3
**GPIO2 (2 buttons)**:
- 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
### Power Button
**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
**GPIO3**:
- Pressed: LOW
- This example uses a 1-second-long press for sleep and a 1-second-long press to wake from sleep
### Battery Voltage
- GPIO0 is connected to the battery via a voltage divider (2x10K resistors), reading 1/2 of the actual voltage
- UART0_RXD/GPIO20 can be used to detect USB connection (charging or not)
### microSD (TF) Card
SD SPI bus is shared with EPD, GPIO12 is used for CS (SS).
```
CS (SS) -> IO12
DO (MISO) -> IO7
DI (MOSI) -> IO10
SCK (SCLK) -> IO8
```
### Implementation Notes
-1
View File
@@ -16,7 +16,6 @@ board_build.partitions = default_16MB.csv
; Libraries
lib_deps =
zinggjm/GxEPD2@^1.5.9
https://github.com/pangodream/18650CL
; Build flags
build_flags =
+54
View File
@@ -0,0 +1,54 @@
// BatteryMonitor.cpp
#include "BatteryMonitor.h"
#include "esp_adc_cal.h"
BatteryMonitor::BatteryMonitor(uint8_t adcPin, float dividerMultiplier)
: _adcPin(adcPin), _dividerMultiplier(dividerMultiplier)
{
}
uint16_t BatteryMonitor::readPercentage() const
{
return percentageFromMillivolts(readMillivolts());
}
uint16_t BatteryMonitor::readMillivolts() const
{
const uint16_t raw = readRawMillivolts();
const uint32_t mv = millivoltsFromRawAdc(raw);
return static_cast<uint32_t>(mv * _dividerMultiplier);
}
uint16_t BatteryMonitor::readRawMillivolts() const
{
const uint16_t raw = analogRead(_adcPin);
return raw;
}
double BatteryMonitor::readVolts() const
{
return static_cast<double>(readMillivolts()) / 1000.0;
}
uint16_t BatteryMonitor::percentageFromMillivolts(uint16_t millivolts)
{
double volts = millivolts / 1000.0;
// Polynomial derived from LiPo samples
double y = -144.9390 * volts * volts * volts +
1655.8629 * volts * volts -
6158.8520 * volts +
7501.3202;
// Clamp to [0,100] and round
y = max(y, 0.0);
y = min(y, 100.0);
y = round(y);
return static_cast<int>(y);
}
uint16_t BatteryMonitor::millivoltsFromRawAdc(uint16_t adc_raw)
{
esp_adc_cal_characteristics_t adc_chars;
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_12, ADC_WIDTH_BIT_12, 1100, &adc_chars);
return esp_adc_cal_raw_to_voltage(adc_raw, &adc_chars);
}
+33
View File
@@ -0,0 +1,33 @@
// BatteryMonitor.h
#pragma once
#include <Arduino.h>
class BatteryMonitor
{
public:
// Optional divider multiplier parameter defaults to 2.0
explicit BatteryMonitor(uint8_t adcPin, float dividerMultiplier = 2.0f);
// Read voltage and return percentage (0-100)
uint16_t readPercentage() const;
// Read the battery voltage in millivolts (accounts for divider)
uint16_t readMillivolts() const;
// Read raw millivolts from ADC (doesn't account for divider)
uint16_t readRawMillivolts() const;
// Read the battery voltage in volts (accounts for divider)
double readVolts() const;
// Percentage (0-100) from a millivolt value
static uint16_t percentageFromMillivolts(uint16_t millivolts);
// Calibrate a raw ADC reading and return millivolts
static uint16_t millivoltsFromRawAdc(uint16_t adc_raw);
private:
uint8_t _adcPin;
float _dividerMultiplier;
};
+160 -45
View File
@@ -3,9 +3,15 @@
#include <Fonts/FreeMonoBold12pt7b.h>
#include <GxEPD2_BW.h>
#include <SPI.h>
#include "image.h"
#include "Pangodream_18650_CL.h"
#include <FS.h>
#include <SD.h>
#include <string.h>
#include "image.h"
#include "BatteryMonitor.h"
#define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
#define EPD_SCLK 8 // SPI Clock
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
@@ -20,12 +26,14 @@
#define BTN_GPIO3 3 // Power button (digital)
#define UART0_RXD 20 // Used for USB connection detection
#define BAT_GPIO0 0 // Battery voltage
#define READS 10
#define CONV_FACTOR 1.5176
Pangodream_18650_CL BL(BAT_GPIO0, CONV_FACTOR, READS);
#define SD_SPI_CS 12
#define SD_SPI_MISO 7
static bool g_sdReady = false;
static BatteryMonitor g_battery(BAT_GPIO0);
static int rawBat = 0;
@@ -44,7 +52,7 @@ volatile DisplayCommand displayCommand = DISPLAY_NONE;
// GxEPD2 display - Using GxEPD2_426_GDEQ0426T82
// Note: XteinkX4 has 4.26" 800x480 display
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(
GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
// FreeRTOS task for non-blocking display updates
TaskHandle_t displayTaskHandle = NULL;
@@ -78,6 +86,7 @@ volatile Button currentPressedButton = NONE;
const unsigned long POWER_BUTTON_WAKEUP_MS = 1000; // Time required to confirm boot from sleep
const unsigned long POWER_BUTTON_SLEEP_MS = 1000; // Time required to enter sleep mode
// Get button name as string
const char *getButtonName(Button btn)
{
@@ -163,11 +172,90 @@ void drawBatteryInfo()
display.printf("Power: %s", charging ? "Charging" : "Battery");
display.setCursor(40, 200);
display.printf("Raw: %i", rawBat);
display.printf("Raw: %i", g_battery.readRawMillivolts());
display.setCursor(40, 240);
display.printf("Volts: %.2f V", BL.getBatteryVolts());
display.printf("Volts: %.2f V", g_battery.readVolts());
display.setCursor(40, 280);
display.printf("Charge: %i%%", BL.getBatteryChargeLevel());
display.printf("Charge: %i%%", g_battery.readPercentage());
}
// Draw up to top file names from SD on the display, below battery info
static void drawSdTopFiles()
{
// Layout constants aligned with drawBatteryInfo() block
const int startX = 40;
const int startY = 350;
const int lineHeight = 26;
const int maxLines = 5;
const int maxChars = 30;
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(20, 320);
display.print("Top 5 files on SD:");
auto drawTruncated = [&](int lineIdx, const char *text)
{
// Render a single line, truncating with ellipsis if needed
String s(text ? text : "");
if ((int) s.length() > maxChars)
{
s.remove(maxChars - 1);
s += "";
}
display.setCursor(startX, startY + lineIdx * lineHeight);
display.print(s);
};
// Ensure SD is initialized using global flag; try to init if needed
if (!g_sdReady)
{
if (SD.begin(SD_SPI_CS, SPI, SPI_FQ))
{
g_sdReady = true;
}
}
if (!g_sdReady)
{
drawTruncated(0, "No card");
return;
}
File root = SD.open("/");
if (!root || !root.isDirectory())
{
drawTruncated(0, "No card");
if (root) root.close();
return;
}
int count = 0;
for (File f = root.openNextFile(); f && count < maxLines; f = root.openNextFile())
{
if (!f.isDirectory())
{
const char *name = f.name();
// Ensure only name + extension, no leading path
const char *basename = name;
if (basename)
{
const char *slash = strrchr(basename, '/');
if (slash && *(slash + 1))
basename = slash + 1;
}
drawTruncated(count, basename ? basename : "");
count++;
}
f.close();
}
if (count == 0)
{
drawTruncated(0, "Empty");
}
root.close();
}
// Display update task running on separate core
@@ -201,6 +289,8 @@ void displayUpdateTask(void *parameter)
// Draw battery information
drawBatteryInfo();
// Draw top 3 SD files below the battery block
drawSdTopFiles();
// Draw image at bottom right
int16_t imgWidth = 263;
@@ -214,7 +304,7 @@ void displayUpdateTask(void *parameter)
else if (cmd == DISPLAY_TEXT)
{
// Use partial refresh for text updates
display.setPartialWindow(0, 75, display.width(), 300);
display.setPartialWindow(0, 75, display.width(), 225);
display.firstPage();
do
{
@@ -321,6 +411,13 @@ void setup()
delay(10);
}
if (Serial)
{
// delay for monitor to start reading
delay(1000);
}
Serial.println("\n=================================");
Serial.println(" xteink x4 sample");
Serial.println("=================================");
@@ -333,10 +430,21 @@ void setup()
pinMode(BTN_GPIO3, INPUT_PULLUP); // Power button
// Initialize SPI with custom pins
SPI.begin(EPD_SCLK, -1, EPD_MOSI, EPD_CS);
SPI.begin(EPD_SCLK,SD_SPI_MISO, EPD_MOSI, EPD_CS);
// Initialize display
display.init(115200);
SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
display.init(115200, true, 2, false, SPI, spi_settings);
// SD Card Initialization
if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ))
{
Serial.print("\n SD card not detected\n");
}
else
{
Serial.print("\n SD card detected\n");
g_sdReady = true;
}
// Setup display properties
display.setRotation(3); // 270 degrees
@@ -344,6 +452,7 @@ void setup()
Serial.println("Display initialized");
// Draw initial welcome screen
currentPressedButton = NONE;
displayCommand = DISPLAY_INITIAL;
@@ -362,6 +471,39 @@ void setup()
Serial.println("Setup complete!\n");
}
#ifdef DEBUG_IO
void debugIO()
{
// Log raw analog levels of BTN1 and BTN2 not more often than once per second
rawBat = analogRead(BAT_GPIO0);
int rawBtn1 = analogRead(BTN_GPIO1);
int rawBtn2 = analogRead(BTN_GPIO2);
int rawBtn3 = digitalRead(BTN_GPIO3);
Serial.print("ADC BTN1=");
Serial.print(rawBtn1);
Serial.print(" BTN2=");
Serial.print(rawBtn2);
Serial.print(" BTN3=");
Serial.print(rawBtn3);
Serial.println("");
// log battery info
Serial.printf("== Battery (charging: %s) ==\n", isCharging() ? "yes" : "no");
Serial.print("Value from pin (raw/calibrated): ");
Serial.print(rawBat);
Serial.print(" / ");
Serial.println(BatteryMonitor::millivoltsFromRawAdc(rawBat));
Serial.print("Volts: ");
Serial.println(g_battery.readVolts());
Serial.print("Charge level: ");
Serial.println(g_battery.readPercentage());
Serial.println("");
// SD card
}
#endif
void loop()
{
Button currentButton = GetPressedButton();
@@ -375,6 +517,10 @@ void loop()
currentPressedButton = currentButton;
displayCommand = DISPLAY_TEXT;
#ifdef DEBUG_IO
debugIO();
#endif
if (currentButton == POWER)
{
unsigned long startTime = millis();
@@ -391,36 +537,5 @@ 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_GPIO0);
int rawBtn1 = analogRead(BTN_GPIO1);
int rawBtn2 = analogRead(BTN_GPIO2);
int rawBtn3 = digitalRead(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
}