“软件是灵魂,硬件是躯体。要理解一个嵌入式系统,先要认识它的身体。”
上一章我们认识了 ESP32-C3 芯片和 CrossPoint Reader 项目。这一章,让我们打开阅读器的”外壳”,看看里面都有些什么。

2.1 阅读器硬件组成全景图

先用一张图建立整体印象。下面是 Xteink X4 阅读器内部各组件的连接关系:
                        ┌──────────────┐
                        │   锂电池      │
                        │   3.7V       │
                        └──────┬───────┘

                          ┌────┴────┐
                          │ MOSFET  │ ← GPIO13 控制
                          │ 电源锁存 │    (硬件断电开关)
                          └────┬────┘

                        ┌──────┴───────┐
                        │              │
                        │  ESP32-C3    │
                        │  (主控芯片)   │
                        │              │
                        │  RISC-V      │
                        │  160MHz      │
                        │  380KB RAM   │
                        │  16MB Flash  │
                        │  WiFi + BLE  │
                        │              │
                        └─┬──┬──┬──┬───┘
                          │  │  │  │
              ┌───────────┘  │  │  └────────────┐
              │              │  │                │
         SPI 总线        SPI 总线  GPIO×7      ADC/I2C
              │              │  │                │
        ┌─────┴─────┐  ┌────┴────┐  ┌───┴───┐  │
        │  4.7" 墨水屏│  │  SD 卡  │  │ 7个按键│  │
        │  800×480   │  │  模块   │  │       │  │
        │  E-Ink     │  │        │  │BACK   │  │
        └───────────┘  └────────┘  │CONFIRM│  │
                                    │LEFT   │  │
                                    │RIGHT  │  │
                                    │UP     │  │
                                    │DOWN   │  │
                                    │POWER  │  │
                                    └───────┘  │

                               ┌───────────────┘
                               │ (仅 X3 版本,通过 I2C)

                    ┌──────────┼──────────┐
                    │          │          │
              ┌─────┴────┐┌───┴────┐┌────┴────┐
              │ BQ27220  ││DS3231  ││QMI8658  │
              │ 电量计   ││RTC时钟 ││IMU传感器│
              └──────────┘└────────┘└─────────┘
看起来东西不少,但核心就是一颗 ESP32-C3 芯片通过不同的”通信方式”(SPI、GPIO、I2C、ADC)与各个外围设备对话。接下来我们逐个认识。

2.2 墨水屏(E-Ink):不用电也能显示的神奇屏幕

物理原理

墨水屏的正式名称是电泳显示屏(Electrophoretic Display)。它的原理出奇地好理解: 想象一个装满透明液体的微型胶囊(直径约 40 微米,比头发丝还细一半)。胶囊里悬浮着两种粒子:白色粒子带正电黑色粒子带负电
未施加电场时:          施加正电场:            施加负电场:
┌──────────────┐      ┌──────────────┐       ┌──────────────┐
│ ○●○●●○●○●○  │      │ ○○○○○○○○○○  │  观    │ ●●●●●●●●●●  │  观
│ ●○●○○●○●○●  │  →   │              │  看    │              │  看
│ ○●○●●○●○●○  │      │ ●●●●●●●●●●  │  方    │ ○○○○○○○○○○  │  方
└──────────────┘      └──────────────┘  向    └──────────────┘  向
   (混合=灰)             (白色朝上)       ↑       (黑色朝上)       ↑
                          显示白色                  显示黑色
当你给胶囊上方施加正电场时,带正电的白色粒子被排斥到顶部——你看到的就是白色。反之则是黑色。屏幕上有几十万个这样的胶囊,每个都可以独立控制,就组成了文字和图片。 CrossPoint Reader 使用的是一块 800x480 分辨率的 4.7 英寸墨水屏。

断电保持:Bistable 特性

墨水屏最神奇的特性是双稳态(Bistable)——断电后画面不会消失。 这和你手机的 LCD/OLED 屏幕完全不同。手机屏幕需要持续通电才能显示图像,断电就黑屏。而墨水屏上的粒子一旦被电场移动到位,即使切断电源,它们也会老老实实地待在原地。就像你用磁铁把铁粉排列成图案后,拿走磁铁,铁粉还是保持那个形状。 这意味着:如果屏幕内容没有变化,屏幕根本不耗电。 对于电子书阅读器来说,这简直完美——你一页书可能看好几分钟,这段时间屏幕是零功耗的。

为什么刷新慢?为什么会闪烁?

你可能注意到,墨水屏翻页时有明显的延迟(几百毫秒),有时还会出现黑白闪烁。这背后有物理原因:
  1. 刷新慢:移动胶囊里的微粒需要时间。不像 LCD 中液晶分子的快速旋转,电泳粒子要在黏性液体中”游泳”到另一端,这需要几十到几百毫秒。
  2. 需要闪烁(全刷):如果每次都只把需要变化的粒子移动一下(称为”局部刷新”),时间久了,一些粒子会”偷懒”——没有完全移到位,造成残影(Ghosting)。所以偶尔需要做一次”全刷”:先把所有粒子移到一端(全黑),再移到另一端(全白),最后才显示新内容。这就是你看到的”闪烁”。

屏幕与 ESP32-C3 的连接

墨水屏通过 SPI(Serial Peripheral Interface,串行外设接口)协议与芯片通信。你可以把 SPI 理解为一种”对话方式”——芯片通过几根线给屏幕发送指令和数据。 以下是项目中实际的引脚定义:
// lib/hal/HalGPIO.h —— 墨水屏 SPI 引脚定义
#define EPD_SCLK 8    // SPI 时钟线:节拍器,同步数据传输
#define EPD_MOSI 10   // SPI 数据线:芯片→屏幕的数据通道(Master Out Slave In)
#define EPD_CS   21   // 片选:拉低电平 = "我在跟你说话"
#define EPD_DC   4    // 数据/命令切换:高 = 发数据,低 = 发命令
#define EPD_RST  5    // 复位:拉低后再拉高 = 重启屏幕控制器
#define EPD_BUSY 6    // 屏幕忙碌信号:屏幕告诉芯片 "我还在刷新,等等"
#define SPI_MISO 7    // SPI 读取线:屏幕→芯片的数据通道(Master In Slave Out)
用生活场景来理解这些引脚的作用:
  • SCLK(时钟线):就像两个人对话时的节拍,确保双方同步。“我说一个字,你听一个字。”
  • MOSI(数据输出线):芯片往屏幕传数据的”嘴巴”。
  • CS(片选):如果 SPI 总线上接了多个设备(比如屏幕和 SD 卡),拉低某个设备的 CS 线就是在说”接下来我要跟你说话”。
  • BUSY(忙碌信号):屏幕正在刷新时会把这根线拉高,告诉芯片”别催,我还没画完”。

2.3 SD 卡模块:书架的扩展空间

为什么用 SD 卡?

ESP32-C3 自带 16MB Flash 存储,听起来还行?但是:
  • 一本纯文字的小说(TXT),约 0.5-2 MB
  • 一本带排版的电子书(EPUB),约 1-50 MB(图片多的可能更大)
  • 16MB Flash 中大部分已经被固件程序本身占用(后文详述),实际可用空间极小
所以我们需要 SD 卡作为外部存储——一张 32GB 的 SD 卡就能装几百上千本书。

SPI 模式连接

SD 卡同样通过 SPI 总线与 ESP32-C3 通信。这意味着它和墨水屏共用一部分线路(SCLK、MOSI、MISO),但各自有独立的 CS(片选)线。
                    ESP32-C3

                ┌──────┼──────┐
                │   SPI 总线   │
                │              │
            CS=21          CS=??(SD)
                │              │
           ┌────┴────┐   ┌────┴────┐
           │  墨水屏  │   │  SD 卡  │
           └─────────┘   └─────────┘

共用线路:SCLK(GPIO8)、MOSI(GPIO10)、MISO(GPIO7)
各自独立:CS(片选线)
SPI 总线的妙处在于:一套线路可以接多个设备,通过不同的 CS 线来选择当前跟谁通信。 这在引脚数量有限的嵌入式系统中非常重要——每多用一个引脚,PCB 设计和成本都会增加。

2.4 七个物理按键:简单而可靠的交互

CrossPoint Reader 有 7 个物理按键:
┌─────────────────────────────────────┐
│                                     │
│          4.7" E-Ink Display         │
│            800 × 480                │
│                                     │
│                                     │
├─────────────────────────────────────┤
│                                     │
│   [BACK]  [UP]  [CONFIRM]          │
│                                     │
│   [LEFT] [DOWN] [RIGHT]   [POWER]  │
│                                     │
└─────────────────────────────────────┘

GPIO 连接方式

每个按键都通过一个 GPIO(General Purpose Input/Output,通用输入输出)引脚连接到芯片。GPIO 是嵌入式开发中最基础的概念之一——它就是芯片上可以读取或输出高/低电平的引脚。 按键的工作原理非常简单:
        3.3V (高电平)

         ├── 上拉电阻(通常内置于芯片中)

  GPIO ──┤

        按键

        GND (地,低电平)
  • 按键没按下时:GPIO 引脚通过上拉电阻连接到 3.3V,芯片读到”高电平”(1)
  • 按键按下时:GPIO 引脚直接连接到 GND,芯片读到”低电平”(0)
就这么简单。芯片不断读取这 7 个引脚的电平状态,就能知道用户按了哪个键。 项目中的按键索引定义:
// lib/hal/HalGPIO.h —— 按键索引定义
static constexpr uint8_t BTN_BACK    = 0;
static constexpr uint8_t BTN_CONFIRM = 1;
static constexpr uint8_t BTN_LEFT    = 2;
static constexpr uint8_t BTN_RIGHT   = 3;
static constexpr uint8_t BTN_UP      = 4;
static constexpr uint8_t BTN_DOWN    = 5;
static constexpr uint8_t BTN_POWER   = 6;

为什么用按键而不是触摸屏?

你可能会问:现在智能手机都用触摸屏了,为什么这个阅读器还在用”古老”的按键?原因有三:
  1. 成本:触摸屏(尤其是墨水屏配套的触摸层)价格远高于 7 个按键开关
  2. 功耗:触摸屏控制器需要持续工作来检测手指,按键只在按下时才产生信号
  3. 交互匹配:墨水屏刷新速度慢(几百毫秒),无法支撑触摸屏需要的流畅拖拽、滑动操作。按键的”按一下翻一页”反而是最自然的交互方式
在嵌入式开发中,选择技术方案不是看”什么最先进”,而是看什么最适合

2.5 X3 额外硬件:I2C 总线上的三个”助手”

Xteink X3 是 X4 的升级版,额外配备了三颗通过 I2C 总线连接的芯片。

先说说 I2C 是什么

I2C(读作 “I-squared-C” 或 “I-two-C”)是另一种常见的通信协议,只需要两根线:
  • SDA(Serial Data):数据线
  • SCL(Serial Clock):时钟线
// lib/hal/HalGPIO.h —— X3 的 I2C 引脚定义
#define X3_I2C_SDA  20        // I2C 数据线
#define X3_I2C_SCL  0         // I2C 时钟线
#define X3_I2C_FREQ 400000    // 400KHz 通信速率
I2C 和 SPI 相比,优点是线更少(只需 2 根 vs SPI 的 4+ 根),缺点是速度更慢。但对于读取电量、时间这类数据量小的任务,I2C 完全够用。 同一条 I2C 总线上可以挂多个设备,每个设备有自己的”地址”(类似网络中的 IP 地址),芯片通过地址来区分跟谁通信。

BQ27220 电量计:电池还剩多少?

这是一颗由 Texas Instruments(德州仪器)生产的专用电量计芯片。它不是简单地测量电池电压来估算电量(电压法误差很大),而是通过库仑计数法——持续跟踪充入和放出的电流——来精确计算剩余电量百分比。 就像水表:你不需要每次都量水箱里还有多少水,只要记录流入和流出了多少水,就能算出剩余量。 X4 版本没有这颗芯片,只能通过 ADC(模数转换)读取电池电压来粗略估算电量:
// lib/hal/HalGPIO.h —— X4 电池电压 ADC 引脚
#define BAT_GPIO0 0   // X4 通过 GPIO0 的 ADC 读取电池电压

DS3231 RTC 实时时钟:关机也能走的时钟

RTC(Real-Time Clock)是一个独立的时钟芯片,自带纽扣电池或超级电容供电。即使主芯片完全断电,RTC 也能继续计时。 这就像你家墙上的挂钟——即使全家停电了,挂钟(如果是电池驱动的)照样走。而 ESP32-C3 自带的时钟更像是微波炉上的时钟——停电后就归零了。 DS3231 的精度很高,每月误差不超过几秒钟,用于在阅读器上显示当前时间。

QMI8658 IMU 陀螺仪/加速度计:感知设备姿态

IMU(Inertial Measurement Unit,惯性测量单元)能感知设备的运动和姿态——它知道你是横着还是竖着拿设备。 CrossPoint 利用这颗传感器实现自动旋转屏幕:当你把阅读器横过来时,屏幕内容会自动旋转 90 度。和你手机的自动旋转功能一样,只不过在墨水屏上实现这个功能需要额外考虑刷新速度的问题。

2.6 电池电路:不只是”关机”,而是”断电”

MOSFET 电池锁存开关

CrossPoint Reader 的电源管理中有一个很有趣的设计——GPIO13 控制的 MOSFET 电池锁存开关
          锂电池 (+)


        ┌───────────┐
        │  MOSFET   │ ← GPIO13 控制栅极
        │  开关     │    HIGH = 导通(供电)
        └─────┬─────┘    LOW  = 断开(断电)


         ESP32-C3 供电

为什么需要硬件断电?

你的手机”关机”后其实并没有完全断电——它还在维持 RTC 时钟、监听电源键等。这叫做”软关机”。 但对于一个电池容量有限的阅读器来说,即使是微安级别的待机电流,日积月累也会把电池耗光。CrossPoint 的做法更加彻底:
  1. 用户长按 POWER 键关机
  2. 软件把需要保存的数据写入 Flash
  3. 软件把 GPIO13 拉低
  4. MOSFET 断开,电池与主芯片之间的电路物理切断
  5. ESP32-C3 完全失去供电——不是 sleep,是真的”死”了
开机时,按下 POWER 键会通过硬件电路将 MOSFET 重新导通,ESP32-C3 上电重启。 这种设计让阅读器在关机状态下的功耗为(忽略电池自放电),可以放置数月而不掉电。这就是嵌入式开发中”硬件和软件协同设计”的典型案例。

2.7 Flash 分区布局:16MB 的精打细算

ESP32-C3 自带的 16MB Flash 需要存放固件程序、配置数据、崩溃日志等,每一字节都需要精心规划。

分区表总览

0x000000 ┌────────────────────────────┐
         │     Bootloader             │  引导加载程序
0x008000 ├────────────────────────────┤
         │     Partition Table        │  分区表(记录以下分区的位置和大小)
0x009000 ├────────────────────────────┤
         │     NVS                    │  Non-Volatile Storage
         │     (非易失性存储)           │  保存 WiFi 密码、阅读进度等配置
0x010000 ├────────────────────────────┤
         │                            │
         │     OTA 分区 0             │  固件程序 A(约 6.25 MB)
         │     (app0)                 │
         │                            │
0x650000 ├────────────────────────────┤
         │                            │
         │     OTA 分区 1             │  固件程序 B(约 6.25 MB)
         │     (app1)                 │
         │                            │
0xC90000 ├────────────────────────────┤
         │     Coredump               │  崩溃转储(64 KB)
         │                            │  程序崩溃时保存现场信息,用于调试
0xCA0000 ├────────────────────────────┤
         │     剩余空间                │
0xFFFFFF └────────────────────────────┘
         总计:16 MB

双 OTA 分区:安全的空中升级

OTA(Over-The-Air)是指通过 WiFi 无线更新固件。为什么需要两个固件分区? 想象你在给汽车换轮胎:如果你只有一个千斤顶的位置,卸了旧轮胎、装新轮胎的过程中,车是悬空不稳定的。但如果你有两个位置,可以先在另一个位置装好新轮胎,确认没问题后再切换过去。 双 OTA 分区的工作方式:
  1. 当前运行的固件在 OTA 分区 0
  2. 下载新固件,写入 OTA 分区 1
  3. 验证新固件完整性(校验哈希值)
  4. 重启,从 OTA 分区 1 启动
  5. 如果新固件有问题,可以回退到 OTA 分区 0
这样即使更新过程中断电,或者新固件有 bug,设备也不会变成”砖头”——旧固件还完好无损地躺在另一个分区里。 每个 OTA 分区约 6.25 MB,这就是 CrossPoint 固件的大小上限。在这 6.25 MB 里要塞下所有代码、字体资源、UI 图片……所以你理解为什么 16MB Flash 装不了几本书了吧?

Coredump 分区:程序崩溃时的”黑匣子”

就像飞机的黑匣子在坠机后帮助调查事故原因一样,Coredump 分区在程序崩溃时保存 CPU 寄存器状态、堆栈内容等关键信息。开发者可以事后分析这些数据来定位 bug。 64KB 不大,但足够保存最关键的崩溃现场信息。

NVS 分区:掉电也不丢的”记事本”

NVS(Non-Volatile Storage,非易失性存储)用于保存少量但重要的键值对数据,比如:
  • WiFi 名称和密码
  • 阅读进度(读到哪一页了)
  • 用户偏好设置(字体大小、亮度等)
  • OTA 更新状态
“非易失性”的意思是:断电后数据不会丢失。相对地,RAM 是”易失性”的——掉电就清零。

2.8 引脚定义汇总

最后,让我们把所有重要的引脚定义放在一起,作为本章的参考手册:
// lib/hal/HalGPIO.h —— 完整引脚定义

// ===== 墨水屏 SPI 接口 =====
#define EPD_SCLK 8    // SPI 时钟线
#define EPD_MOSI 10   // SPI 数据线(芯片→屏幕)
#define EPD_CS   21   // 片选(低电平有效)
#define EPD_DC   4    // 数据/命令切换
#define EPD_RST  5    // 复位
#define EPD_BUSY 6    // 屏幕忙碌信号
#define SPI_MISO 7    // SPI 读取线(屏幕→芯片)

// ===== 电池相关 =====
#define BAT_GPIO0 0   // X4 电池电压 ADC 引脚

// ===== X3 I2C 总线 =====
#define X3_I2C_SDA  20       // I2C 数据线
#define X3_I2C_SCL  0        // I2C 时钟线
#define X3_I2C_FREQ 400000   // 400KHz 通信速率

// ===== 按键索引 =====
static constexpr uint8_t BTN_BACK    = 0;
static constexpr uint8_t BTN_CONFIRM = 1;
static constexpr uint8_t BTN_LEFT    = 2;
static constexpr uint8_t BTN_RIGHT   = 3;
static constexpr uint8_t BTN_UP      = 4;
static constexpr uint8_t BTN_DOWN    = 5;
static constexpr uint8_t BTN_POWER   = 6;

本章要点

  1. 墨水屏(E-Ink) 利用电泳原理,通过电场驱动黑白粒子移动来显示图像。它的核心优势是断电保持(Bistable),代价是刷新速度慢。
  2. SPI 和 I2C 是两种常见的嵌入式通信协议。SPI 速度快但需要的线多,I2C 只要两根线但速度慢。墨水屏和 SD 卡用 SPI,传感器类芯片用 I2C。
  3. SD 卡通过 SPI 模式连接,弥补了 16MB Flash 无法存储大量书籍的不足。
  4. 7 个物理按键通过 GPIO 引脚读取高低电平来判断是否被按下。选用按键而非触摸屏是成本、功耗和交互匹配的综合考量。
  5. X3 版本额外配备了电量计(BQ27220)、实时时钟(DS3231)和姿态传感器(QMI8658),全部通过 I2C 总线连接。
  6. MOSFET 电源锁存实现了真正的硬件断电(而非 sleep),让关机功耗为零。
  7. 16MB Flash 被精心划分为多个分区:双 OTA 分区保障安全升级,NVS 分区保存用户数据,Coredump 分区记录崩溃信息。

下一章预告:第 3 章我们将进入软件世界,搭建开发环境,编译并刷入你的第一个固件。“Hello, E-Ink!”——让墨水屏显示你写的第一行字。