Skip to main content

Arduino 菜谱:板载外设(reTerminal E 系列)

在找显示相关内容?

本页重点介绍如何使用 Arduino 驱动 reTerminal E 系列的板载硬件外设。如果你想在电子纸屏幕上渲染文本、图形或图像,请前往 Arduino 菜谱:电子纸显示

介绍

reTerminal E 系列不仅仅是一块电子纸屏幕——每一款型号还提供了板载 LED、蜂鸣器、三个用户按键、SHT4x 温湿度传感器、电池电压监测以及 microSD 卡槽。本菜谱收集了针对这些外设的可直接烧录的 Arduino 示例,外加一个端到端图像处理流水线:从 SD 卡加载 JPEG / BMP / PNG 文件,为面板的调色板进行抖动处理,并将其渲染到电子纸屏幕上——每种面板版本(E1001 BW、E1001 Gray4、E1002、E1003、E1004)都提供一份现成的示例草图。

本菜谱涵盖内容:

  • LED 控制,使用 GPIO6(反向逻辑)。
  • 蜂鸣器 警报和音乐音调,使用 GPIO45。
  • 三个用户按键(KEY0 / KEY1 / KEY2),带消抖状态检测。
  • 通过 I²C(GPIO19 SDA / GPIO20 SCL)使用 Sensirion 库的 SHT4x 传感器
  • 通过 ADC + 使能引脚电路进行 电池电压监测
  • 在共享 SPI 总线上进行 microSD 卡 挂载 / 检测 / 文件列出。
  • 高级示例——SD 卡图像流水线:从 SD 卡中选择任意 JPEG / BMP / PNG 文件,使用五种内置抖动算法之一进行处理,并以可配置的锚点、适配模式和缩放比例将其渲染到面板上。

所需材料

本菜谱适用于全部四款 reTerminal E 系列型号。请选择你手头拥有的设备:

reTerminal E1001reTerminal E1002reTerminal E1003reTerminal E1004

前置准备

在运行下面的任何示例之前,你应该已经完成:

  • 已安装 Arduino IDE,并安装好 ESP32 开发板包,选择了 XIAO_ESP32S3 开发板。
  • 准备好一根可用的 USB-C 数据线,并选择了正确的串口。
  • 已确认可以向设备烧录一个基础草图——如果尚未完成,请参考 Arduino 菜谱:电子纸显示 中的环境搭建部分。

本菜谱中的所有草图都会通过 Serial1GPIO44(RX)/ GPIO43(TX) 引脚上以 115200 波特率 输出调试信息。请打开 Arduino 串口监视器,并选择匹配的端口和波特率以便查看输出。

LED 控制

reTerminal E 系列带有一个可通过 GPIO 控制的板载 LED。请注意,该 LED 逻辑是反向的(LOW = 亮,HIGH = 灭)。不同型号的 LED 引脚如下:

型号LED GPIO
E1001 / E1002GPIO6
E1003GPIO16
E1004GPIO48
// reTerminal E1001/E1002 - LED Control Example

#define SERIAL_RX 44
#define SERIAL_TX 43
#define LED_PIN 6 // GPIO6 - Onboard LED (inverted logic)

void setup() {
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10);
}

Serial1.println("LED Control Example");

// Configure LED pin
pinMode(LED_PIN, OUTPUT);
}

void loop() {
// Turn LED ON (LOW because it's inverted)
digitalWrite(LED_PIN, LOW);
Serial1.println("LED ON");
delay(1000);

// Turn LED OFF (HIGH because it's inverted)
digitalWrite(LED_PIN, HIGH);
Serial1.println("LED OFF");
delay(1000);
}

蜂鸣器控制

reTerminal E 系列在 GPIO45 上集成了一个蜂鸣器,可以发出各种音调和提示音。

// reTerminal E Series - Buzzer Control Example

#define SERIAL_RX 44
#define SERIAL_TX 43
#define BUZZER_PIN 45 // GPIO45 - Buzzer

void setup() {
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10);
}

Serial1.println("Buzzer Control Example");
}

void loop() {
Serial1.println("Simple beep");
tone(BUZZER_PIN, 1000, 100); // 1kHz for 100ms
delay(1000);

Serial1.println("Double beep");
for (int i = 0; i < 2; i++) {
tone(BUZZER_PIN, 2000, 50); // 2kHz for 50ms
delay(100);
}
delay(900);

Serial1.println("Long beep");
tone(BUZZER_PIN, 800, 500); // 800Hz for 500ms
delay(1500);

Serial1.println("Alarm sound");
for (int i = 0; i < 5; i++) {
tone(BUZZER_PIN, 1500, 100);
delay(100);
tone(BUZZER_PIN, 1000, 100);
delay(100);
}
delay(2000);
}

带音调的蜂鸣器

点击展开完整蜂鸣器示例代码
#define SERIAL_RX 44
#define SERIAL_TX 43
#define BUZZER_PIN 45 // GPIO7 - Buzzer

// Reference: This list was adapted from the table located here:
// http://www.phy.mtu.edu/~suits/notefreqs.html
#define NOTE_C0 16.35 //C0
#define NOTE_Db0 17.32 //C#0/Db0
#define NOTE_D0 18.35 //D0
#define NOTE_Eb0 19.45 //D#0/Eb0
#define NOTE_E0 20.6 //E0
#define NOTE_F0 21.83 //F0
#define NOTE_Gb0 23.12 //F#0/Gb0
#define NOTE_G0 24.5 //G0
#define NOTE_Ab0 25.96 //G#0/Ab0
#define NOTE_A0 27.5 //A0
#define NOTE_Bb0 29.14 //A#0/Bb0
#define NOTE_B0 30.87 //B0
#define NOTE_C1 32.7 //C1
#define NOTE_Db1 34.65 //C#1/Db1
#define NOTE_D1 36.71 //D1
#define NOTE_Eb1 38.89 //D#1/Eb1
#define NOTE_E1 41.2 //E1
#define NOTE_F1 43.65 //F1
#define NOTE_Gb1 46.25 //F#1/Gb1
#define NOTE_G1 49 //G1
#define NOTE_Ab1 51.91 //G#1/Ab1
#define NOTE_A1 55 //A1
#define NOTE_Bb1 58.27 //A#1/Bb1
#define NOTE_B1 61.74 //B1
#define NOTE_C2 65.41 //C2 (Middle C)
#define NOTE_Db2 69.3 //C#2/Db2
#define NOTE_D2 73.42 //D2
#define NOTE_Eb2 77.78 //D#2/Eb2
#define NOTE_E2 82.41 //E2
#define NOTE_F2 87.31 //F2
#define NOTE_Gb2 92.5 //F#2/Gb2
#define NOTE_G2 98 //G2
#define NOTE_Ab2 103.83 //G#2/Ab2
#define NOTE_A2 110 //A2
#define NOTE_Bb2 116.54 //A#2/Bb2
#define NOTE_B2 123.47 //B2
#define NOTE_C3 130.81 //C3
#define NOTE_Db3 138.59 //C#3/Db3
#define NOTE_D3 146.83 //D3
#define NOTE_Eb3 155.56 //D#3/Eb3
#define NOTE_E3 164.81 //E3
#define NOTE_F3 174.61 //F3
#define NOTE_Gb3 185 //F#3/Gb3
#define NOTE_G3 196 //G3
#define NOTE_Ab3 207.65 //G#3/Ab3
#define NOTE_A3 220 //A3
#define NOTE_Bb3 233.08 //A#3/Bb3
#define NOTE_B3 246.94 //B3
#define NOTE_C4 261.63 //C4
#define NOTE_Db4 277.18 //C#4/Db4
#define NOTE_D4 293.66 //D4
#define NOTE_Eb4 311.13 //D#4/Eb4
#define NOTE_E4 329.63 //E4
#define NOTE_F4 349.23 //F4
#define NOTE_Gb4 369.99 //F#4/Gb4
#define NOTE_G4 392 //G4
#define NOTE_Ab4 415.3 //G#4/Ab4
#define NOTE_A4 440 //A4
#define NOTE_Bb4 466.16 //A#4/Bb4
#define NOTE_B4 493.88 //B4
#define NOTE_C5 523.25 //C5
#define NOTE_Db5 554.37 //C#5/Db5
#define NOTE_D5 587.33 //D5
#define NOTE_Eb5 622.25 //D#5/Eb5
#define NOTE_E5 659.26 //E5
#define NOTE_F5 698.46 //F5
#define NOTE_Gb5 739.99 //F#5/Gb5
#define NOTE_G5 783.99 //G5
#define NOTE_Ab5 830.61 //G#5/Ab5
#define NOTE_A5 880 //A5
#define NOTE_Bb5 932.33 //A#5/Bb5
#define NOTE_B5 987.77 //B5
#define NOTE_C6 1046.5 //C6
#define NOTE_Db6 1108.73 //C#6/Db6
#define NOTE_D6 1174.66 //D6
#define NOTE_Eb6 1244.51 //D#6/Eb6
#define NOTE_E6 1318.51 //E6
#define NOTE_F6 1396.91 //F6
#define NOTE_Gb6 1479.98 //F#6/Gb6
#define NOTE_G6 1567.98 //G6
#define NOTE_Ab6 1661.22 //G#6/Ab6
#define NOTE_A6 1760 //A6
#define NOTE_Bb6 1864.66 //A#6/Bb6
#define NOTE_B6 1975.53 //B6
#define NOTE_C7 2093 //C7
#define NOTE_Db7 2217.46 //C#7/Db7
#define NOTE_D7 2349.32 //D7
#define NOTE_Eb7 2489.02 //D#7/Eb7
#define NOTE_E7 2637.02 //E7
#define NOTE_F7 2793.83 //F7
#define NOTE_Gb7 2959.96 //F#7/Gb7
#define NOTE_G7 3135.96 //G7
#define NOTE_Ab7 3322.44 //G#7/Ab7
#define NOTE_A7 3520 //A7
#define NOTE_Bb7 3729.31 //A#7/Bb7
#define NOTE_B7 3951.07 //B7
#define NOTE_C8 4186.01 //C8
#define NOTE_Db8 4434.92 //C#8/Db8
#define NOTE_D8 4698.64 //D8
#define NOTE_Eb8 4978.03 //D#8/Eb8

void buzzer_tone (float noteFrequency, long noteDuration, int silentDuration){
if(silentDuration==0) {silentDuration=1;}

tone(BUZZER_PIN, noteFrequency, noteDuration);
delay(noteDuration); // milliseconds
noTone(BUZZER_PIN); // stop the tone

delay(silentDuration);
}

void setup() {
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10);
}

Serial1.println("Buzzer Control Example");

// Configure buzzer pin
pinMode(BUZZER_PIN, OUTPUT);
}

void loop() {
buzzer_tone(NOTE_C5, 80, 20);
buzzer_tone(NOTE_E5, 80, 20);
buzzer_tone(NOTE_G5, 80, 20);
buzzer_tone(NOTE_C6, 150, 0);
delay(30000);
}

蜂鸣器功能:

  • digitalWrite(): 用于基本蜂鸣的简单开/关控制
  • tone(pin, frequency, duration): 生成特定频率,用于旋律或警报
  • noTone(pin): 停止音调生成

常见警报模式:

  • 单声蜂鸣:确认
  • 双声蜂鸣:警告
  • 三声蜂鸣:错误
  • 持续蜂鸣:严重警报

用户按键

reTerminal E 系列配备了三个用户可编程按键,可用于各种控制用途。本节演示如何读取按键状态,并使用 Arduino 响应按键按下。

reTerminal E 系列有三个按键,通过 KEY0(GPIO3)、KEY1(GPIO4)和 KEY2(GPIO5)连接到 ESP32-S3。所有按键为低电平有效,这意味着按下时读取为 LOW,松开时读取为 HIGH。

这些按键在不同型号上的物理布局和功能有所不同:

KeyE1001 / E1002 / E1003E1004
KEY0 (GPIO3)右侧按键(绿色按键)右方向按键(正面)
KEY1 (GPIO4)中间按键左方向按键(正面)
KEY2 (GPIO5)左侧按键刷新按键(左前方)
note

E1004 在设备的正面和背面都有按键。上述 KEY0–KEY2 的连接对应的是前面板上的按键。

基本按键读取示例

此示例演示如何检测按键按下,并向串口监视器打印消息。

// reTerminal E Series - Button Test
// Based on hardware schematic

// Define button pins according to schematic
const int BUTTON_KEY0 = 3; // KEY0 - GPIO3
const int BUTTON_KEY1 = 4; // KEY1 - GPIO4
const int BUTTON_KEY2 = 5; // KEY2 - GPIO5

// Button state variables
bool lastKey0State = HIGH;
bool lastKey1State = HIGH;
bool lastKey2State = HIGH;

void setup() {
// Initialize serial communication
Serial1.begin(115200, SERIAL_8N1, 44, 43);
while (!Serial1) {
delay(10); // Wait for serial port to connect
}

Serial1.println("=================================");
Serial1.println("reTerminal E Series - Button Test");
Serial1.println("=================================");
Serial1.println("Press any button to see output");
Serial1.println();

// Configure button pins as inputs
// Hardware already has pull-up resistors, so use INPUT mode
pinMode(BUTTON_KEY0, INPUT);
pinMode(BUTTON_KEY1, INPUT);
pinMode(BUTTON_KEY2, INPUT);

// Read initial states
lastKey0State = digitalRead(BUTTON_KEY0);
lastKey1State = digitalRead(BUTTON_KEY1);
lastKey2State = digitalRead(BUTTON_KEY2);

Serial1.println("Setup complete. Ready to detect button presses...");
}

void loop() {
// Read current button states
bool key0State = digitalRead(BUTTON_KEY0);
bool key1State = digitalRead(BUTTON_KEY1);
bool key2State = digitalRead(BUTTON_KEY2);

// Check KEY0
if (key0State != lastKey0State) {
if (key0State == LOW) {
Serial1.println("KEY0 (GPIO3) pressed!");
} else {
Serial1.println("KEY0 (GPIO3) released!");
}
lastKey0State = key0State;
delay(50); // Debounce delay
}

// Check KEY1
if (key1State != lastKey1State) {
if (key1State == LOW) {
Serial1.println("KEY1 (GPIO4) pressed!");
} else {
Serial1.println("KEY1 (GPIO4) released!");
}
lastKey1State = key1State;
delay(50); // Debounce delay
}

// Check KEY2
if (key2State != lastKey2State) {
if (key2State == LOW) {
Serial1.println("KEY2 (GPIO5) pressed!");
} else {
Serial1.println("KEY2 (GPIO5) released!");
}
lastKey2State = key2State;
delay(50); // Debounce delay
}

delay(10); // Small delay to prevent excessive CPU usage
}

代码工作原理:

  1. 引脚定义:我们为每个按键的 GPIO 引脚号定义常量。

  2. 引脚配置:在 setup() 中,将每个按键引脚配置为 INPUT

  3. 按键检测:在 loop() 中,使用 digitalRead() 持续检查每个按键的状态。当按键被按下时,引脚读取为 LOW。

  4. 消抖:在每次按键按下后加入简单的 200ms 延时,以防止由于机械抖动导致一次按下被多次检测。

  5. 串口输出:每次按键按下都会向串口监视器发送一条消息,用于调试和验证。


步骤 1. 将代码上传到你的 reTerminal E 系列设备。

步骤 2. 在 Arduino IDE 中打开串口监视器(Tools > Serial Monitor)。

步骤 3. 将波特率设置为 115200。

步骤 4. 依次按下每个按键,并观察串口监视器中的输出。

按下按键时的预期输出:

=================================
reTerminal E Series - Button Test
=================================
Press any button to see output

KEY0 (GPIO3) pressed!
KEY0 (GPIO3) released!
KEY1 (GPIO4) pressed!
KEY1 (GPIO4) released!
KEY2 (GPIO5) pressed!
KEY2 (GPIO5) released!

环境传感器(SHT4x)

reTerminal E 系列集成了一个通过 I2C 连接的 SHT4x 温湿度传感器。

安装所需库

通过 Arduino 库管理器(Tools > Manage Libraries...)安装两个库:

  1. 搜索并安装 "Sensirion I2C SHT4x"
  2. 搜索并安装 "Sensirion Core"(依赖库)

基本温湿度示例

// reTerminal E Series - SHT40 Temperature & Humidity Sensor Example

#include <Wire.h>
#include <SensirionI2cSht4x.h>

// Serial configuration for reTerminal E Series
#define SERIAL_RX 44
#define SERIAL_TX 43

// I2C pins for reTerminal E Series
#define I2C_SDA 19
#define I2C_SCL 20

// Create sensor object
SensirionI2cSht4x sht4x;

void setup() {
// Initialize Serial1 for reTerminal E Series
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10);
}

Serial1.println("SHT4x Basic Example");

// Initialize I2C with custom pins
Wire.begin(I2C_SDA, I2C_SCL);

uint16_t error;
char errorMessage[256];

// Initialize the sensor
sht4x.begin(Wire, 0x44);

// Read and print serial number
uint32_t serialNumber;
error = sht4x.serialNumber(serialNumber);

if (error) {
Serial1.print("Error trying to execute serialNumber(): ");
errorToString(error, errorMessage, 256);
Serial1.println(errorMessage);
} else {
Serial1.print("Serial Number: ");
Serial1.println(serialNumber);
Serial1.println();
}
}

void loop() {
uint16_t error;
char errorMessage[256];

delay(5000); // Wait 5 seconds between measurements

float temperature;
float humidity;

// Measure temperature and humidity with high precision
error = sht4x.measureHighPrecision(temperature, humidity);

if (error) {
Serial1.print("Error trying to execute measureHighPrecision(): ");
errorToString(error, errorMessage, 256);
Serial1.println(errorMessage);
} else {
Serial1.print("Temperature: ");
Serial1.print(temperature);
Serial1.print("°C\t");
Serial1.print("Humidity: ");
Serial1.print(humidity);
Serial1.println("%");
}
}

Setup 函数:

  1. 串口初始化:使用 Serial1,引脚 44(RX)和 43(TX)为 reTerminal E 系列特定引脚
  2. I2C 初始化:使用引脚 19(SDA)和 20(SCL)配置 I2C
  3. 传感器初始化:调用 sht4x.begin(Wire, 0x44) 在地址 0x44 初始化 SHT4x 传感器
  4. 序列号读取:读取并显示传感器的唯一序列号以进行验证

Loop 函数:

  1. 延时:在两次测量之间等待 5 秒,以避免过度采样
  2. 测量:使用 measureHighPrecision() 进行高精度读数(耗时约 8.3ms)
  3. 错误处理:检查错误并使用 errorToString() 将其转换为可读消息
  4. 显示结果:以摄氏度打印温度,并打印相对湿度百分比

预期输出

SHT4x Basic Example
Serial Number: 331937553

Temperature: 27.39°C Humidity: 53.68%
Temperature: 27.40°C Humidity: 53.51%
Temperature: 27.38°C Humidity: 53.37%

电池管理系统

reTerminal E 系列通过带分压电路的 ADC 引脚实现电池电压监测功能。

简单电池电压监测

// reTerminal E Series - Simple Battery Voltage Reading

// Serial configuration
#define SERIAL_RX 44
#define SERIAL_TX 43

// Battery monitoring pins
#define BATTERY_ADC_PIN 1 // GPIO1 - Battery voltage ADC
#define BATTERY_ENABLE_PIN 21 // GPIO21 - Battery monitoring enable

void setup() {
// Initialize serial
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10);
}

Serial1.println("Battery Voltage Monitor");

// Configure battery monitoring enable pin
pinMode(BATTERY_ENABLE_PIN, OUTPUT);
digitalWrite(BATTERY_ENABLE_PIN, HIGH); // Enable battery monitoring

// Configure ADC
analogReadResolution(12); // 12-bit resolution
analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);

delay(100); // Allow circuit to stabilize
}

void loop() {
// Enable battery monitoring
digitalWrite(BATTERY_ENABLE_PIN, HIGH);
delay(5);

// Read voltage in millivolts
int mv = analogReadMilliVolts(BATTERY_ADC_PIN);

// Disable battery monitoring
digitalWrite(BATTERY_ENABLE_PIN, LOW);

// Calculate actual battery voltage (2x due to voltage divider)
float batteryVoltage = (mv / 1000.0) * 2;

// Print voltage
Serial1.print("Battery: ");
Serial1.print(batteryVoltage, 2);
Serial1.println(" V");

delay(2000);
}

代码说明:

  • GPIO1 通过 ADC 读取分压后的电池电压
  • GPIO21 启用电池监测电路
  • 由于分压器的存在,实际电池电压是测量电压的两倍
  • 对于完全充电的锂聚合物电池,电压大约为 4.2V
  • 当电池电量低时,电压会下降到大约 3.3V

预期输出

Battery Voltage Monitor

Battery: 4.18 V
Battery: 4.19 V
Battery: 4.18 V

使用 MicroSD 卡

对于需要额外存储空间的应用,例如数码相框或数据记录,reTerminal E 系列配备了一个 MicroSD 卡槽。

如果计划将设备用作数码相框或需要额外存储空间,请插入一张 microSD 卡。

note

reTerminal E 系列仅支持容量不超过 64GB 且使用 Fat32 文件系统格式化的 MicroSD 卡。

基本 SD 卡操作:列出文件

本示例演示如何初始化 SD 卡、检测其插入或移除状态,并列出其根目录中的所有文件和文件夹。SD 卡电源使能引脚(SD_EN_PIN)在不同型号之间有所不同:

型号SD_EN_PINGPIO
E1001 / E1002 / E100416GPIO16
E100339GPIO39

所有其他 SD 卡引脚(DET、CS、MOSI、MISO、SCK)在各型号之间都是相同的。请选择与你的设备对应的选项卡,并将代码复制到 Arduino IDE 草稿中。

点击展开完整的 SD 卡示例代码
#include <SD.h>
#include <SPI.h>

// SD Card Pin Definitions
#define SD_EN_PIN 16 // Power enable for the SD card slot
#define SD_DET_PIN 15 // Card detection pin
#define SD_CS_PIN 14 // Chip Select for the SD card
#define SD_MOSI_PIN 9 // Shared with ePaper Display
#define SD_MISO_PIN 8
#define SD_SCK_PIN 7 // Shared with ePaper Display

// Serial configuration for reTerminal E Series
#define SERIAL_RX 44
#define SERIAL_TX 43

// Use the HSPI bus for the SD card to avoid conflict with other peripherals
SPIClass spiSD(HSPI);

// Global variables to track SD card state
bool sdMounted = false;
bool lastCardPresent = false;
unsigned long lastCheckMs = 0;
const unsigned long checkIntervalMs = 1000; // Check for card changes every second

// Checks if a card is physically inserted.
// The detection pin is LOW when a card is present.
bool isCardInserted() {
return digitalRead(SD_DET_PIN) == LOW;
}

// Helper function to print indentation for directory listing
void printIndent(uint8_t level) {
for (uint8_t i = 0; i < level; ++i) {
Serial1.print(" ");
}
}

// Recursively lists files and directories
void listDir(File dir, uint8_t level) {
while (true) {
File entry = dir.openNextFile();
if (!entry) {
// No more entries in this directory
break;
}

printIndent(level);
if (entry.isDirectory()) {
Serial1.print("[DIR] ");
Serial1.println(entry.name());
// Recurse into the subdirectory
listDir(entry, level + 1);
} else {
// It's a file, print its name and size
Serial1.print("[FILE] ");
Serial1.print(entry.name());
Serial1.print(" ");
Serial1.print(entry.size());
Serial1.println(" bytes");
}
entry.close();
}
}

// Opens the root directory and starts the listing process
void listRoot() {
File root = SD.open("/");
if (!root) {
Serial1.println("[SD] Failed to open root directory.");
return;
}
if (!root.isDirectory()) {
Serial1.println("[SD] Root is not a directory.");
root.close();
return;
}
Serial1.println("[SD] Listing files in /");
listDir(root, 0);
root.close();
}

// Initializes the SPI bus and mounts the SD card
bool mountSD() {
// Enable power to the SD card slot
pinMode(SD_EN_PIN, OUTPUT);
digitalWrite(SD_EN_PIN, HIGH);
delay(5);

// Initialize the HSPI bus with the correct pins for the SD card
spiSD.end(); // Guard against repeated begin calls
spiSD.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);

// Attempt to mount the SD card file system
if (!SD.begin(SD_CS_PIN, spiSD)) {
Serial1.println("[SD] MicroSD initialization failed. Check card formatting.");
return false;
}
Serial1.println("[SD] MicroSD mounted successfully.");
return true;
}

// Unmounts the SD card by releasing the SPI bus
void unmountSD() {
SD.end();
spiSD.end();
Serial1.println("[SD] MicroSD unmounted.");
}

void setup() {
// Start the secondary serial port for output
Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
while (!Serial1) {
delay(10); // Wait for Serial1 to be ready
}

// Set up the card detection pin with an internal pull-up resistor
pinMode(SD_DET_PIN, INPUT_PULLUP);
// Set up the power enable pin
pinMode(SD_EN_PIN, OUTPUT);
digitalWrite(SD_EN_PIN, HIGH);

// Check for a card at startup
lastCardPresent = isCardInserted();
if (lastCardPresent) {
sdMounted = mountSD();
if (sdMounted) {
listRoot(); // If mounted, list files
}
} else {
Serial1.println("[SD] No card detected at startup. Please insert a card.");
}
}

void loop() {
// Periodically check for card insertion or removal without blocking the loop
unsigned long now = millis();
if (now - lastCheckMs >= checkIntervalMs) {
lastCheckMs = now;

bool present = isCardInserted();
if (present != lastCardPresent) {
lastCardPresent = present; // Update the state

if (present) {
Serial1.println("\n[SD] Card inserted.");
if (!sdMounted) {
sdMounted = mountSD();
}
if (sdMounted) {
listRoot(); // List files upon insertion
}
} else {
Serial1.println("\n[SD] Card removed.");
if (sdMounted) {
unmountSD();
sdMounted = false;
}
}
}
}
// You can place other non-blocking code here
}

代码说明

  • 引脚定义: 代码首先定义了用于 MicroSD 卡槽的 GPIO 引脚。请注意,SPI 引脚(MOSISCK)与电子纸显示屏共享,但通过单独的片选引脚(SD_CS_PIN)和独立的 SPI 实例(spiSD),可以确保它们被独立使用。
  • SPI 初始化: 我们实例化了一个新的 SPI 对象 spiSD(HSPI),以使用 ESP32 的第二个硬件 SPI 控制器(HSPI)。这是避免与其他 SPI 设备冲突的最佳实践。
  • 卡检测: isCardInserted() 函数读取 SD_DET_PIN。在 reTerminal 硬件上,当卡存在时,该引脚会被拉低。
  • 挂载/卸载: mountSD() 函数为卡供电,使用正确的引脚配置 HSPI 总线,并调用 SD.begin() 初始化文件系统。unmountSD() 用于释放资源。
  • 文件列出: listRoot() 打开根目录(/),而 listDir() 是一个递归函数,用于遍历文件系统并打印所有文件和目录的名称。
  • setup() 初始化用于输出的 Serial1,配置卡检测引脚,并在设备上电时执行一次初始检查,以查看是否已经插入了卡。
  • loop() 代码没有持续不断地检查卡,而是使用非阻塞定时器(millis())每秒检查一次卡状态是否发生变化。如果检测到变化(插入或移除卡),则挂载或卸载该卡,并将状态打印到串口监视器。

预期结果

  1. 将代码上传到你的 reTerminal。
  2. 打开 Arduino IDE 的串口监视器(Tools > Serial Monitor)。
  3. 确保波特率设置为 115200

你将看到与以下操作相对应的输出:

  • 启动时没有插卡: 监视器会打印 [SD] No card detected at startup...
  • 当你插入一张卡时: 监视器会打印 [SD] Card inserted.,随后会完整列出卡上所有文件和目录。
  • 当你移除该卡时: 监视器会打印 [SD] Card removed.
[FILE] live.0.shadowIndexGroups  6 bytes
[FILE] reverseStore.updates 1 bytes
[DIR] journals.repair
[FILE] Cab.modified 0 bytes
[FILE] live.1.indexPositionTable 8192 bytes
[FILE] live.1.indexTermIds 8192 bytes
[FILE] tmp.spotlight.loc 2143 bytes
[FILE] live.1.shadowIndexTermIds 624 bytes
[FILE] live.1.indexArrays 65536 bytes
[FILE] live.1.shadowIndexArrays 65536 bytes
[FILE] live.1.indexHead 4096 bytes
[FILE] live.1.indexPostings 4096 bytes

高级示例:SD 卡 → 电子纸图像流水线

这是 reTerminal E 系列的旗舰示例。它从 microSD 卡中加载一个 JPEG / BMP / PNG 文件,将其送入可配置的抖动处理流水线,并将结果渲染到电子纸面板上——你可以调节 抖动算法亮度锚点位置 以及 适配 / 缩放。同一套代码结构适用于全部四种面板版本;每个型号唯一变化的是输出色深(1 位黑白、2 位 4 级灰度、4 位 16 级灰度,或 6 色 E6)。

Seeed_GFX 库中提供了五个可直接烧录的示例草图——选择与你的硬件匹配的那个:

示例索引

设备示例草图面板分辨率输出调色板
reTerminal E1001(BW)reTerminal_E1001_SDcard_BW800 × 4801 位黑 / 白
reTerminal E1001(Gray4)reTerminal_E1001_SDcard_Gray4800 × 4802 位 4 级灰度
reTerminal E1002reTerminal_E1002_SDcard_Color6800 × 4806 色(B / W / R / Y / G / B)
reTerminal E1003reTerminal_E1003_SDcard_Gray161872 × 14044 位 16 级灰度
reTerminal E1004reTerminal_E1004_SDcard_Color61200 × 16006 色(B / W / R / Y / G / B)

这五个草图都位于 Seeed_GFX/examples/ePaper/reTerminal_SDcard_Bitmap/ 下。每个文件夹都是完全自包含的——无需安装额外库,只需打开并烧录即可。

流水线的工作内容

图像处理流水线:microSD 到电子纸

microSDJPG / BMP / PNGPSRAMRGB888缩放缓冲区scaled RGB888调色板缓冲区面板调色板索引电子纸面板刷新解码pngle / jpeg / bmpresizenearest neighborditherBayer / FS / ...pushSprite
  1. 解码(Decode) — 通过魔数(FF D8BM89 50 4E 47)检测文件格式。具有误导性的扩展名会被自动纠正,并记录一条警告日志。
  2. 缩放(Resize)(可选)— 基于 DISPLAY_FIT / DISPLAY_SCALE 的最近邻降采样。
  3. 抖动(Dither) — 五种算法之一将 24 位 RGB 量化为面板的微小调色板(2 / 4 / 6 / 16 级)。
  4. 推送(Push) — 量化后的缓冲区被写入 ePaper Sprite 的锚点位置,然后由 epaper.update() 将其时钟输出到面板。

步骤 1 — 打开适用于你型号的示例

在 Arduino IDE 中:File → Examples → Seeed_GFX → ePaper → reTerminal_SDcard_Bitmap → (选择你的型号)

reTerminal_SDcard_Bitmap/
└── reTerminal_E1001_SDcard_BW/
├── reTerminal_E1001_SDcard_BW.ino ← config + setup() + loop()
├── dither.{h,cpp} ← BW dither algorithms
├── image_loader.{h,cpp} ← JPEG / BMP / PNG decoder
├── pngle.{h,c} + miniz.{h,c} ← PNG backend (MIT, vendored)
└── driver.h ← selects Setup520 (UC8179, 800x480)
自包含示例草图

你不需要从 Arduino Library Manager 安装 pngle、miniz 或任何其他库。PNG 解码器源码就放在每个示例文件夹内部,因此 Arduino IDE 在编译草图时会自动将其包含进来。

完整草图代码

每个变体的完整 .ino 源码如下所示。所有用户可调节的设置(图像路径、抖动算法、锚点、适配/缩放)都位于靠前位置的 USER CONFIGURATION 配置块中——文件的其余部分是通常无需编辑的样板代码。

点击此处预览完整代码 — reTerminal_E1001_SDcard_BW.ino
#include <SPI.h>
#include <FS.h>
#include <SD.h>
#include "TFT_eSPI.h"
#include "dither.h"
#include "image_loader.h"

#ifndef EPAPER_ENABLE
#error "This example requires Setup520_Seeed_reTerminal_E1001 -- check driver.h selects BOARD_SCREEN_COMBO 520"
#endif

EPaper epaper;

static constexpr int PIN_SD_SCK = 7;
static constexpr int PIN_SD_MISO = 8;
static constexpr int PIN_SD_MOSI = 9;
static constexpr int PIN_SD_CS = 14;
static constexpr int PIN_SD_EN = 16;
static constexpr int PIN_SD_DET = 15;
static constexpr int PIN_DBG_RX = 44;
static constexpr int PIN_DBG_TX = 43;
#define LOG Serial1
#define TAG "[e1001-bw]"

// =============================================================================
// USER CONFIGURATION
// =============================================================================

static const char* IMAGE_PATH = "/img/demo.jpg";

// DITHER_NONE / DITHER_BAYER8 / DITHER_FS / DITHER_JARVIS / DITHER_ATKINSON
static const DitherMethod DITHER_METHOD = DITHER_FS;
// 1.0 = neutral, >1.0 darkens, <1.0 brightens
static const float DITHER_GAMMA = 1.0f;
// false = normal, true = invert black/white
static const bool DITHER_INVERT = false;

// ANCHOR_TOP_LEFT / ANCHOR_TOP_CENTER / ANCHOR_TOP_RIGHT /
// ANCHOR_MIDDLE_LEFT / ANCHOR_CENTER / ANCHOR_MIDDLE_RIGHT /
// ANCHOR_BOTTOM_LEFT / ANCHOR_BOTTOM_CENTER / ANCHOR_BOTTOM_RIGHT
enum DisplayAnchor {
ANCHOR_TOP_LEFT, ANCHOR_TOP_CENTER, ANCHOR_TOP_RIGHT,
ANCHOR_MIDDLE_LEFT, ANCHOR_CENTER, ANCHOR_MIDDLE_RIGHT,
ANCHOR_BOTTOM_LEFT, ANCHOR_BOTTOM_CENTER, ANCHOR_BOTTOM_RIGHT,
};
static const DisplayAnchor DISPLAY_ANCHOR = ANCHOR_CENTER;

// FIT_ORIGINAL / FIT_CONTAIN / FIT_SCALE
enum DisplayFit { FIT_ORIGINAL, FIT_CONTAIN, FIT_SCALE };
static const DisplayFit DISPLAY_FIT = FIT_SCALE;
static const float DISPLAY_SCALE = 0.7f;

// =============================================================================
// (implementation below -- no need to edit)
// =============================================================================

static void log_mem(const char* tag) {
LOG.printf("[mem] %-22s heap=%lu kB PSRAM free=%lu/%lu kB\n", tag,
(unsigned long)(ESP.getFreeHeap() / 1024),
(unsigned long)(ESP.getFreePsram() / 1024),
(unsigned long)(ESP.getPsramSize() / 1024));
LOG.flush();
}

static void list_sd_root(int max_entries = 32) {
File root = SD.open("/");
if (!root || !root.isDirectory()) { LOG.println("[sd] cannot open '/'"); return; }
LOG.println("[sd] contents of '/' :");
int n = 0;
File entry = root.openNextFile();
while (entry) {
if (entry.isDirectory()) LOG.printf(" <DIR> %s\n", entry.name());
else LOG.printf(" %7lu B %s\n", (unsigned long)entry.size(), entry.name());
entry.close();
if (++n >= max_entries) { LOG.printf(" ... (truncated at %d)\n", max_entries); break; }
entry = root.openNextFile();
}
if (n == 0) LOG.println(" (empty)");
root.close(); LOG.flush();
}

static void compute_target_size(int src_w, int src_h, DisplayFit fit, float scale,
int panel_w, int panel_h, int* out_w, int* out_h) {
switch (fit) {
case FIT_ORIGINAL: *out_w = src_w; *out_h = src_h; break;
case FIT_CONTAIN: {
double s = (double)panel_w/src_w; if ((double)panel_h/src_h < s) s = (double)panel_h/src_h;
if (s > 1.0) s = 1.0;
*out_w = (int)(src_w*s+0.5); *out_h = (int)(src_h*s+0.5); break;
}
case FIT_SCALE: *out_w = (int)(src_w*scale+0.5); *out_h = (int)(src_h*scale+0.5); break;
}
if (*out_w & 7) *out_w &= ~7;
if (*out_w < 8) *out_w = 8;
if (*out_h < 1) *out_h = 1;
}

static void compute_anchor_xy(int img_w, int img_h, DisplayAnchor a,
int panel_w, int panel_h, int* out_x, int* out_y) {
int x = 0, y = 0;
switch (a) {
case ANCHOR_TOP_LEFT: x=0; y=0; break;
case ANCHOR_TOP_CENTER: x=(panel_w-img_w)/2; y=0; break;
case ANCHOR_TOP_RIGHT: x=panel_w-img_w; y=0; break;
case ANCHOR_MIDDLE_LEFT: x=0; y=(panel_h-img_h)/2; break;
case ANCHOR_CENTER: x=(panel_w-img_w)/2; y=(panel_h-img_h)/2; break;
case ANCHOR_MIDDLE_RIGHT: x=panel_w-img_w; y=(panel_h-img_h)/2; break;
case ANCHOR_BOTTOM_LEFT: x=0; y=panel_h-img_h; break;
case ANCHOR_BOTTOM_CENTER: x=(panel_w-img_w)/2; y=panel_h-img_h; break;
case ANCHOR_BOTTOM_RIGHT: x=panel_w-img_w; y=panel_h-img_h; break;
}
if (x & 7) x &= ~7;
*out_x = x; *out_y = y;
}

static bool show_image_on_panel(RgbImage* img) {
int W, H;
compute_target_size(img->width, img->height, DISPLAY_FIT, DISPLAY_SCALE,
EPD_WIDTH, EPD_HEIGHT, &W, &H);
int x, y;
compute_anchor_xy(W, H, DISPLAY_ANCHOR, EPD_WIDTH, EPD_HEIGHT, &x, &y);

if (W != img->width || H != img->height) {
if (!resize_image(img, W, H)) { LOG.println("[layout] resize OOM"); return false; }
}
const size_t npx = (size_t)W * H;
uint8_t* idx = (uint8_t*)ps_malloc(npx);
if (!idx) idx = (uint8_t*)malloc(npx);
if (!idx) { LOG.println(TAG " OOM idx"); return false; }

static const char* kDN[] = {"NONE","BAYER8","FS","JARVIS","ATKINSON"};
LOG.printf(TAG " dithering BW with %s, gamma=%.2f\n", kDN[(int)DITHER_METHOD], DITHER_GAMMA);
if (!dither_image(img->pixels, W, H, PAL_BW, DITHER_METHOD, DITHER_GAMMA, DITHER_INVERT, idx)) {
free(idx); return false;
}
image_free(img);

const size_t bm_bytes = ((size_t)W+7)/8 * (size_t)H;
uint8_t* bm = (uint8_t*)ps_malloc(bm_bytes);
if (!bm) bm = (uint8_t*)malloc(bm_bytes);
if (!bm) { free(idx); return false; }
pack_1bpp_msb(idx, bm, W, H, true);
free(idx);

epaper.drawBitmap(x, y, bm, W, H, TFT_BLACK, TFT_WHITE);
epaper.update();
free(bm);
return true;
}

void setup() {
LOG.begin(115200, SERIAL_8N1, PIN_DBG_RX, PIN_DBG_TX);
delay(2500);
LOG.println("==============================================");
LOG.println(" reTerminal E1001 -- SD Bitmap (BW)");
LOG.println("==============================================");
log_mem("start");

pinMode(PIN_SD_EN, OUTPUT); digitalWrite(PIN_SD_EN, HIGH);
pinMode(PIN_SD_DET, INPUT_PULLUP); delay(50);

epaper.begin();
epaper.fillScreen(TFT_WHITE);
epaper.update();

// UC8179 is write-only (TFT_MISO=-1 in Setup520). Re-init SPI with MISO for SD.
SPIClass& spi = epaper.getSPIinstance();
spi.end();
spi.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, -1);

if (!SD.begin(PIN_SD_CS, spi)) {
LOG.println(TAG " SD.begin FAILED -- aborting"); return;
}
list_sd_root();

RgbImage img;
if (!load_image_from_sd(IMAGE_PATH, 0, 0, &img)) {
LOG.println(TAG " load failed -- aborting"); return;
}
log_mem("after decode");
show_image_on_panel(&img);
image_free(&img);

epaper.sleep();
LOG.println(TAG " done.");
}

void loop() { delay(1000); }
精简版与完整源码

上面的代码块经过了轻微精简(内联了一些辅助函数,移除了冗长的日志调用),以便在此处保持可读性。库中的示例草图——通过 File → Examples → Seeed_GFX → ePaper → reTerminal_SDcard_Bitmap 打开——包含完整的诊断日志、完整的适配/锚点逻辑以及所有注释。

步骤 2 — 准备 microSD 卡

  1. 将卡格式化为 FAT32

  2. 创建一个与您在草图中设置的路径相匹配的文件夹结构——默认是 /img/demo.jpg

    <SD root>/
    └── img/
    └── demo.jpg ← or demo.png / demo.bmp
  3. 在上电 之前 将卡插入 reTerminal(热插拔也能工作,但可靠性较差)。

步骤 3 — 准备你的图像

加载器开箱即用地支持三种格式:

格式适用情况需要避免
JPEG (.jpg / .jpeg)基线 8 位,YCbCr 或灰度,任意色度抽样(4:4:4 / 4:2:2 / 4:2:0)。渐进式 JPEG、CMYK、仅依赖 EXIF 旋转信息的源文件。
BMP (.bmp)24 位 BGR 无压缩,或 4 位索引色(调色板 + BI_RGB)。BI_BITFIELDS、RLE 压缩的 BMP。
PNG (.png)任意标准 PNG(8 位、16 位、调色板、隔行、RGBA)。RGBA 会在白色背景上进行合成,因为电子墨水屏是非透明的。无——pngle 能处理所有标准 PNG 变体。

文件的实际格式是通过 magic bytes 嗅探出来的,而不是通过扩展名。一个保存为 .bmp 的 JPEG 依然可以正常工作(你只会在串口日志中看到一条警告)。

针对不同面板的尺寸建议:

面板为 800 × 480。任意不超过约 1600 × 1200 的源图像在 8 MB PSRAM 上都能正常解码。更大的图像也可以接受,但你会希望将 DISPLAY_FIT = FIT_CONTAIN,这样加载器可以在量化之前先将其缩小。

步骤 4 — 配置草图

所有用户可调选项都位于每个 .ino 文件顶部的配置块中。下面将逐一介绍四个最重要的控制项。

IMAGE_PATH — 要显示的文件

static const char* IMAGE_PATH = "/img/demo.jpg";

请使用前导 /。加载器会从 magic bytes 嗅探格式,因此扩展名纯粹是装饰——包含真实 JPEG 数据的 /photo.bmp 依然可以正常解码。

DITHER_METHOD — 使用哪种抖动算法

电子墨水屏在物理上只能显示 2 / 4 / 6 / 16 种颜色。为了表示典型照片中数以百万计的颜色,加载器必须将每个像素量化到这少数几个调色板条目之一。抖动算法决定了这种量化误差是如何在相邻像素之间分布的。

static const DitherMethod DITHER_METHOD = DITHER_FS;
选项作用适用场景
DITHER_NONE最近颜色,无误差扩散。最快,但最块状。诊断用途,或当你想要海报化效果时。
DITHER_BAYER88×8 有序 Bayer 矩阵。确定性,无需误差缓冲区在 E1003 / E1004 上以面板分辨率运行时最安全的选择——绝不会耗尽内存。
DITHER_FSFloyd-Steinberg 误差扩散。在画质 / 速度之间取得最佳平衡。在 E1001 / E1002 上为默认值。非常适合具有平滑渐变的照片。
DITHER_JARVISJarvis-Judice-Ninke。更宽的 12 系数卷积核,输出更平滑。画质高于 FS,但速度约慢 3 倍,并占用更多 PSRAM。
DITHER_ATKINSONAtkinson(经典 Mac)。只扩散 6/8 的误差 → 对比度更高,更具“蚀刻”感。风格化黑白输出、漫画 / 线稿内容。
误差扩散的内存开销

DITHER_FSDITHER_JARVISDITHER_ATKINSON 需要一个大约为 W × H × N_channels × 4 字节的浮点误差缓冲区。在 1872 × 1404 分辨率下,彩色约为 31 MB,灰度约为 10 MB——远远超过可用的 PSRAM。

ps_malloc 失败时,加载器会打印

[dither] FS error buffer alloc FAILED (10358 kB) -- falling back to DITHER_NONE

并悄悄切换到 DITHER_NONE。如果你不希望出现这种回退,请改用 DITHER_BAYER8(有序、零分配)或者先缩小图像。

DITHER_GAMMA — 亮度补偿

static const float DITHER_GAMMA = 1.0f;

1.0 为中性。增大可压暗输出(适合在电子墨水屏上显得过亮的户外照片)。减小可提亮(适合夜景照片或截图)。典型的有效范围为 0.8 – 1.6

DISPLAY_ANCHOR — 图像在面板上的落点位置

一个 3×3 的锚点网格。图像会被放置为其角 / 边 / 中心与对应的面板位置对齐。

ANCHOR_TOP_LEFT       ANCHOR_TOP_CENTER       ANCHOR_TOP_RIGHT
ANCHOR_MIDDLE_LEFT ANCHOR_CENTER ANCHOR_MIDDLE_RIGHT
ANCHOR_BOTTOM_LEFT ANCHOR_BOTTOM_CENTER ANCHOR_BOTTOM_RIGHT
static const DisplayAnchor DISPLAY_ANCHOR = ANCHOR_CENTER;

任何比面板小的图像都会在未使用区域自动填充白色,无需预先调整尺寸以与面板完全匹配。比面板大的图像会围绕锚点对称裁剪

DISPLAY_FIT + DISPLAY_SCALE — 调整图像尺寸

static const DisplayFit  DISPLAY_FIT   = FIT_ORIGINAL;
static const float DISPLAY_SCALE = 1.0f;
模式行为
FIT_ORIGINAL保持解码后的尺寸不变。推荐默认值——可预测且始终安全。
FIT_CONTAIN按比例缩小图像,使其在保持纵横比的前提下完全适配到面板内。从不放大——小图像会保持小(如需放大请使用 FIT_SCALE)。
FIT_SCALE将源尺寸乘以 DISPLAY_SCALE。支持缩小(< 1.0)和放大(> 1.0)。

DISPLAY_SCALE 的典型取值:0.25 四分之一,0.5 一半,1.0 原始大小,2.0 2 倍。

在大面板上放大很容易导致内存不足(OOM)

在 E1003(1872 × 1404)和 E1004(1200 × 1600)上,DISPLAY_SCALE 大于 1.0 会很快耗尽 PSRAM。加载器会打印内存不足信息并中止。建议改用裁剪或在主机端预缩放。

灰度深度(仅限 E1001)

E1001 随机附带两个示例程序,因为同一块 UC8179 面板可以工作在 BW(快速,1 位) Gray4(较慢,2 位,4 级灰度)模式。请根据内容选择:

内容推荐示例程序
线稿、二维码、文本、手绘漫画。reTerminal_E1001_SDcard_BW
照片、具有平滑明暗过渡的插画。reTerminal_E1001_SDcard_Gray4

E1003 始终使用 16 级灰度(initGrayMode(16))——该模式是这块面板的标志性特性。E1002 和 E1004 为 6 色面板,不提供灰度深度选择。

步骤 5 —— 构建、烧录并查看日志

  1. Arduino IDE → Tools 中:选择开发板 XIAO_ESP32S3PSRAM = OPI PSRAMFlash = 8 MBPartition Scheme = Default 8 MB
  2. 插入准备好的 microSD 卡。
  3. 上传示例程序。
  4. 打开载板上的 USB-UART 转串口桥的串口监视器(GPIO43 TX / GPIO44 RX,115200 波特率,8N1)——注意这是 Serial1而不是 IDE 自动打开的 USB-CDC Serial

典型日志输出(E1004,使用 1080 × 1920 PNG):

[reTerm_E1004] dithering Color6 with BAYER8, gamma=1.00 ...
[mem] before image load heap=275 kB PSRAM free=8030/8192 kB
[png] IHDR 1080x1920, allocating RGB888 buffer: 6075 kB
[img] image decoded: 1080x1920 (6075 kB in PSRAM)
[mem] after image decoded heap=275 kB PSRAM free=1955/8192 kB
[mem] after dither heap=275 kB PSRAM free=1955/8192 kB
[reTerm_E1004] anchor=CENTER fit=ORIGINAL scale=1.00
[reTerm_E1004] frame pushed OK
[reTerm_E1004] done. Sleeping panel.

之后面板会刷新——一次完整刷新需要 15 – 45 秒,具体取决于型号以及所选灰度 / 彩色模式。请保持静止,不要在刷新过程中重置开发板。

内存预算速查表

面板RGB888 缓冲区文件系统错误缓冲区(峰值)适合与文件系统一起使用?
E1001 BW @ 800×4801.1 MB1.5 MB✅ 是
E1001 Gray4 @ 800×4801.1 MB1.5 MB✅ 是
E1002 E6 @ 800×4801.1 MB4.6 MB✅ 是
E1003 Gray16 @ 1872×14047.5 MB10.1 MB❌ 否——请使用 DITHER_BAYER8 或缩小源图像
E1004 E6 @ 1200×16005.5 MB22.0 MB❌ 否——请使用 DITHER_BAYER8 或缩小源图像

XIAO ESP32-S3 模块上的 8 MB OPI PSRAM 模块在扣除 Arduino 运行时开销后,大约有 7.9 MB 可用空间。如果加载器无法满足一次内存分配,它会记录所需的确切大小,并在 DISPLAY_FIT = FIT_CONTAIN 时尝试调整尺寸后重试,或回退到 DITHER_NONE

关于刷新速度

上传后,电子墨水屏在前几秒内可能保持空白,因为驱动正在运行初始波形。第一次完整刷新在冷屏状态下可能需要长达几分钟——这是面板的电化学特性,而不是 Bug。后续刷新会更快。

故障排查

关于 Arduino IDE 安装问题、USB 驱动问题、上传失败,或“电子墨水屏不刷新”等问题,请参阅 Arduino Cookbook: ePaper Display 中的 Troubleshooting 部分。

技术支持与产品讨论

感谢您选择我们的产品!我们将为您提供多种支持,确保您在使用我们产品的过程中尽可能顺畅。我们提供多种沟通渠道,以满足不同偏好和需求。

Loading Comments...