mirror of
https://github.com/open-x4-epaper/sample-firmware.git
synced 2026-04-29 10:24:26 -07:00
Merge pull request #4 from CrazyCoder/main
This commit is contained in:
@@ -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)
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user