
前阵子收拾书架的时候看着那些积灰的纸质书突然想做一个真正属于自己的电子阅读器。要求不高屏幕要护眼、能装几本常看的小说、最重要的是从电脑或手机传书要方便。市面上主流电纸书当然能买但自己用便宜物料焊一台那种“什么都能控制”的感觉完全不一样。所以就有了这个项目用ESP32做主控配一块2.13寸墨水屏做一个能通过WiFi自动接收书籍文件的小型阅读器。整个过程不算复杂核心就三件事——驱动墨水屏显示文字、处理文本分页、让ESP32开一个网页上传服务。最终的效果是手机连上阅读器开的热点浏览器里选一个txt文件传上去屏幕上立刻出现书页还能按键翻页、记住阅读进度。这篇文章就把我整个踩坑和实现过程写出来从引脚接线到代码逻辑都给你捋清楚适合手里有ESP32开发板和墨水屏模块、想自己动手折腾的玩家。1. 项目整体设计与思路拆解1.1 核心需求一台能“传书”的阅读器动手之前我先反复确认了需求我并不是要做一个能媲美商用电纸书的设备而是做一个“电子书工具”每天导入几章公众号文章、小说TXT、甚至我整理的读书笔记在墨水屏上看不伤眼睛也不容易被手机消息干扰。所以项目的核心功能只有三点第一屏幕能稳定显示中文小说内容第二电脑或手机能通过WiFi把文件传进来第三翻页流畅、关机能保存进度打开还能继续看。这个定位决定了整个技术选型。商用设备动不动几千块是因为它要解决排版、格式、字体渲染、超大存储等一系列问题。而我的需求很克制用一个带WiFi的MCU加一块低功耗屏就能覆盖。我不需要背光不需要触控也不需要高性能处理器。把需求收敛得足够小实现难度就直线下降这才是DIY项目能做成的关键。1.2 为什么选ESP32和2.13寸墨水屏选ESP32几乎是必然的。这个芯片带双核240MHz处理器、520KB SRAM还内置WiFi和蓝牙双模价格却只要十几块钱开发板更是白菜价。它的Flash有4MB甚至16MB版本这对本地存储小体积TXT文本绰绰有余。市面上的Arduino、树莓派Pico等等级单片机也能做显示驱动但要做“WiFi传书”就麻烦得多要么需要外挂WiFi模块要么网络性能不稳定。ESP32天生就是干这个的一张开发板全搞定。墨水屏选2.13寸分辨率250x122驱动IC大多是SSD1680。我之所以选这个尺寸第一是它功耗极低只在刷新瞬间耗几毫安电流静态显示完全不耗电非常适合阅读这种长时间显示静态画面的场景第二是它们的模组很普及淘宝几十块就能买到带转接板的模块SPI接口也容易接线第三2.13寸刚好能显示20x10个12像素汉字一屏能装两百字左右读三五行翻一页节奏正好。1.3 方案选型与取舍对比先列一下我到最终版本之间做过的几个关键选型对比项选项A选项B我的选择理由墨水屏驱动库Adafruit_GFXGxEPD2GxEPD2对多款SSD1680模组支持完善自带全刷/局刷接口中文字体方案字模数组U8g2字库U8g2内置文泉驿12像素汉字字库不需要自己取模书籍存储SD卡模块LittleFSLittleFSESP32 Flash足够存几十本TXT省掉一张TF卡和接线Web服务库ESP8266WebServer内置WebServer内置WebServerArduino专用稳定不依赖第三方异步库翻页刷新局部刷新全屏刷新全屏刷新局刷会有残影阅读场景翻页频次低全刷更干净这些选择不是拍脑袋每一项都对应着特定的工程问题。比如存储SD卡方案虽然容量大但会让接线多一倍的引脚而且SD卡模块供电不稳会造成读卡失败排查起来非常麻烦。在只读TXT文本的情况下ESP32自带的LittleFS分区足够放几十万字的内容所以直接砍掉SD卡项目可靠性大幅提升。选择U8g2字库也是如果自己取模一个12x12的汉字库几千个汉字就要大几十KB空间还得写脚本转换而U8g2里已经集成了文泉驿点阵字库直接调用即可编译后Flash占用多一点但开发效率高得多。2. 硬件准备与接线详解2.1 元器件清单与购买建议这个项目的物料极其克制总共就这么几样ESP32开发板一块DevKit V1带38引脚排针ESP32-WROOM-32模组2.13寸墨水屏模块SSD1680驱动带FPC转接板引脚接口是SPI三个轻触按键用来做翻页、菜单、返回功能一个10uF和一个100uF的电解电容给3.3V供电做滤波若干杜邦线和面包板或者直接洞洞板焊接可选一个锂电池和3.3V升压板做成便携版买墨水屏的时候有个坑市面上2.13寸墨水屏有两代产品分辨率虽然一样但驱动IC不同老款是SSD1675新款是SSD1680两者的初始化时序不完全一样。购买时尽量选“SSD1680”驱动的因为GxEPD2库里对这个型号支持最成熟。如果买到的是SSD1675在写代码时把GxEPD2的类改成对应的型号也能驱动但别搞混否则屏幕刷新会异常。2.2 引脚定义与完整接线表ESP32 DevKit V1有38个引脚实际这次项目用到的不到10个。墨水屏走SPI我用的是VSPI接口MOSI、SCK、CS、DC、RST、BUSY这6根信号线。按键则各自接一个GPIO并开启内部上拉按下时读到低电平。功能墨水屏引脚ESP32 GPIO备注电源VCC3.3V注意不要接5V会烧屏幕地GNDGND保证共地数据DIN(MOSI)GPIO23串行数据输入时钟CLK(SCK)GPIO18SPI时钟片选CSGPIO5低电平有效数据/命令DCGPIO17高电平为数据低电平为命令复位RSTGPIO16低电平复位忙检测BUSYGPIO4高电平表示屏幕忙按键部分我用了GPIO12、GPIO14和GPIO27。GPIO12在ESP32上是特殊引脚不能接上拉电阻因为如果上拉到高电平会导致Flash电压异常使芯片无法启动。所以如果你也把按键接在GPIO12上要记得开启内部下拉电阻模式等待高电平触发而不是用常见的“按下接GND”方式。这个坑让我折腾了两个小时后面会在问题排查里详细说。2.3 供电设计要点避开复位电流的坑很多人做ESP32项目时忽略了供电结果屏幕刷新瞬间单片机就重启或者WiFi连上后运行一会儿就死循环。ESP32的WiFi工作瞬时电流能达到240mA甚至更高加上墨水屏驱动电路峰值电流可能超过300mA。如果用一个普通AMS1117-3.3V从5V供电虽然芯片标称最大输出1A但实际在小压差场景下性能一般很容易跌落到3.3V以下触发芯片欠压复位。我的做法是外接5V电源先经过一个二极管防反接再进入AMS1117-3.3V在3.3V输出端并联一个100uF钽电容和一个10uF瓷片电容。这个组合能有效吸收瞬时电流尖峰。实际测试时用USB供电后接入WiFi热点电压跌落控制在0.1V以内。另外千万不要尝试用3.3V直接驱动墨水屏背面的升压电路屏幕的VCC引脚应该接3.3V而不是接5V虽然大部分模块做了电平转换但万一买到简化版模块接错可能直接烧坏屏幕。3. 开发环境搭建与基础验证3.1 Arduino IDE安装ESP32核心代码开发我选择Arduino IDE原因特别简单GxEPD2和U8g2这两个关键库的示例和文档都以Arduino为主PlatformIO虽然工程管理更好但在这种小项目里没有明显优势反而会增加配置负担。我在Arduino IDE的“开发板管理器”地址中添加乐鑫官方JSON源然后打开开发板管理器搜索“esp32”并安装。如果你下拉列表里看不到esp32 by Espressif多半是网络连接问题可以检查“文件—首选项—附加开发板管理器网址”是否填对了。安装成功后在“工具—开发板”里选择“ESP32 Dev Module”在Flash Size里选4MB分区方案选择“Huge APP (3MB No OTA)”或者“Default 4MB”。对于我这个项目我建议选择“Huge APP (3MB No OTA)”因为U8g2字库编译后的固件体积会超过1MB默认分区里App区只有1.2MB编译可能直接报错提示固件太大。3.2 墨水屏驱动库安装与选型在Arduino IDE的库管理器里搜索并安装两个库GxEPD2和U8g2_for_Adafruit_GFX。前者负责墨水屏的底层驱动比如全刷、局刷、休眠后者负责在高分辨率屏上绘制中文文字它就是我在前面选型时提到的U8g2字库整合方案。注意U8g2_for_Adafruit_GFX是U8g2库的附加包装所以同时也要安装U8g2本体。选GxEPD2库时要看清它内部对不同型号屏幕的类名定义。我的屏幕是SSD1680驱动在GxEPD2里面对应的类是GxEPD2_213_B74构造时传入CS、DC、RST、BUSY四个引脚即可。如果你的屏幕是SSD1675驱动则需要换一个类。最稳妥的确认方式是看GxEPD2源码里的GxEPD2_display_selection.h文件里面把所有支持的屏幕排列了出来找到自己型号对应的那一行。3.3 烧录前必须检查的三个设置第一个是开发板型号选择错了会烧录失败或者串口时序错乱工具菜单里如果同时插入了USB转TTL等其他设备要确认串口选的是ESP32的COM号。第二个是Flash大小和分区我之前把Flash选成16MB但实际板子只有4MB烧录时写入成功但上电后疯狂崩溃排查了很久才发现是Flash size不匹配。第三个是波特率下载速度115200通常稳如果遇到“A fatal error occurred: Timeout waiting for packet header”的报错按住开发板上的BOOT键不放再点击下载看到“Connecting…”字样时松开BOOT键就能进入下载模式。这里额外说一句很多新手把USB线插上后什么都不设置就直接烧录大概率会失败。ESP32在烧录前需要进入下载模式不同开发板的操作略有差异但DevKit V1一般只需要按住BOOT再点烧录出现进度条后松开即可。如果你连“Connecting”都看不到检查驱动装没装Windows下需要CH340或CP2102驱动Mac和Linux一般免驱。4. 核心功能代码实现4.1 墨水屏驱动与文字显示我采用面向对象思路把显示相关操作封装成一个DisplayManager类。初始化时先调用display.init()然后设置屏幕方向最后调用U8g2字体库的begin(display)把两者绑定。#include GxEPD2_BW.h #include U8g2_for_Adafruit_GFX.h #define EPD_CS 5 #define EPD_DC 17 #define EPD_RST 16 #define EPD_BUSY 4 GxEPD2_BWGxEPD2_213_B74, GxEPD2_213_B74::HEIGHT display( GxEPD2_213_B74(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY)); U8G2_FOR_ADAFRUIT_GFX u8g2Fonts; void initDisplay() { display.init(115200, true, 2, false); display.setRotation(0); u8g2Fonts.begin(display); u8g2Fonts.setFont(u8g2_font_wqy12_t_gb2312); u8g2Fonts.setFontMode(1); // 透明背景 u8g2Fonts.setForegroundColor(GxEPD_BLACK); u8g2Fonts.setBackgroundColor(GxEPD_WHITE); }这里有两个容易踩的坑。第一个是display.init()的第三个参数reset_duration不同模组参数不一致如果发现初始化后屏幕一直处于忙状态把它从2调成3或4试试。第二个是setRotation(0)的方向如果显示文字是横躺的改成display.setRotation(1)就会旋转90度。我个人习惯横向显示因为2.13寸的宽度是250像素横放能在一行放20个汉字阅读宽度比较舒服。4.2 文本分页算法实现墨水屏不像手机可以上下滑动它更适合“翻页”一屏显示固定内容翻页后整屏替换。整个项目最核心的算法就是分页。我用一个全局的int textPos变量保存当前读到文本的字节偏移量然后每一帧从textPos开始按“像素宽度”一个一个读取UTF-8字符直到填满当前页。const uint16_t SCREEN_W 250; const uint16_t SCREEN_H 122; const uint16_t LINE_H 14; // 12像素字高加两像素行距 String bookText; // 整个书的文本内容 int textPos 0; void renderPage() { display.clearBuffer(); u8g2Fonts.setFont(u8g2_font_wqy12_t_gb2312); int pos textPos; int y 2; while (pos bookText.length() y LINE_H SCREEN_H) { uint16_t lineWidth 0; String line ; while (pos bookText.length()) { uint8_t c bookText[pos]; // 根据UTF-8首字节判断字符占用长度 int charLen 1; if (c 0xE0) charLen 3; else if (c 0xC0) charLen 2; if (charLen 1 c \n) { pos; break; } if (pos charLen bookText.length()) break; String ch bookText.substring(pos, pos charLen); uint16_t w u8g2Fonts.getUTF8Width(ch.c_str()); if (lineWidth w SCREEN_W) break; line ch; lineWidth w; pos charLen; } u8g2Fonts.setCursor(2, y); u8g2Fonts.print(line); y LINE_H; } display.display(); }这个算法看似简单却是最容易出bug的地方。第一它必须“按字符宽度”累加而不能简单截取固定字节数因为英文字母在u8g2内置字体中只有6像素宽中文是12像素宽如果按字节截会把一行塞爆。第二它要处理字符截半的问题比如刚好遇到一个汉字被切成两半那半个字节会变成乱码所以用charLen判断字节数后整体取出。第三遇到换行符时要把当前位置跳过换行符本身但英文换行和中文换行逻辑都要兼容。4.3 WiFi传书服务实现传书是本项目的灵魂功能。我的实现方式是ESP32启动后先开启一个SoftAP热点名字叫eReader密码为12345678然后在内网开启一个HTTP服务器监听80端口。手机上连接这个热点后浏览器访问192.168.4.1就能看到一个简单的上传页面。#include WiFi.h #include LittleFS.h #include WebServer.h WebServer server(80); File uploadFile; void handleRoot() { String html F(!DOCTYPE htmlhtmlheadmeta charsetutf-8 title传书/title/headbody h3上传TXT文件到阅读器/h3 form methodPOST action/upload enctypemultipart/form-data input typefile namefilebrbr input typesubmit value上传/form/body/html); server.send(200, text/html, html); } void handleUpload() { HTTPUpload upload server.upload(); if (upload.status UPLOAD_FILE_START) { uploadFile LittleFS.open(/book.txt, w); } else if (upload.status UPLOAD_FILE_WRITE) { if (uploadFile) uploadFile.write(upload.buf, upload.currentSize); } else if (upload.status UPLOAD_FILE_END) { if (uploadFile) { uploadFile.close(); } server.send(200, text/plain, Upload OK); } } void setup() { LittleFS.begin(); WiFi.softAP(eReader, 12345678); server.on(/, handleRoot); server.on(/upload, HTTP_POST, []() { // 上传完成后的回调 }, handleUpload); server.begin(); }实际测试时网页上传一个2MB的TXT文件到LittleFS大约需要10秒速度取决于热点信号和孩子列入网络的稳定程度。上传完成后代码会把文件内容整体读入RAM再调用renderPage()显示第一页。要注意的是这里为了简化示例直接用了固定的/book.txt作为存储文件名也就是说每次上传都会覆盖之前的书。如果需要管理多本书可以在文件名后面加时间戳或者序号构建一个简单的书库列表。4.4 翻页交互与阅读进度记忆翻页按键需要做消抖否则按一次可能跳两页。我采用状态变量加时间戳的方式在循环中轮询检测只有按键状态从高电平变为低电平并且距离上次触发超过80毫秒才处理一次翻页。#define BTN_NEXT 14 #define BTN_PREV 27 int lastButtonState HIGH; unsigned long lastDebounceTime 0; void loop() { int reading digitalRead(BTN_NEXT); if (reading ! lastButtonState) { lastDebounceTime millis(); } if ((millis() - lastDebounceTime) 80 reading LOW lastButtonState HIGH) { nextPage(); } lastButtonState reading; server.handleClient(); }阅读进度的记忆我用的是ESP32的Preferences库它内部基于NVS参数存储掉电不丢失。翻页时把textPos写入NVS开机时读取如果读取到有效值就直接跳到上次的位置。这里有一个细节如果每次都调用prefs.begin()和prefs.end()会频繁写入Flash减少Flash寿命。所以我只在切换页面或者关机时才写一次阅读过程中所有进度都保存在内存里。#include Preferences.h Preferences prefs; void saveProgress() { prefs.begin(reader, false); prefs.putInt(pos, textPos); prefs.end(); } void loadProgress() { prefs.begin(reader, false); textPos prefs.getInt(pos, 0); prefs.end(); }5. 常见问题与排查技巧实录5.1 花屏和残影问题排查墨水屏最常见的问题就是花屏。如果上电后屏幕出现随机噪点或者刷新一半停住首先检查SPI接线CS、DC、RESET、BUSY这几根信号线如果接触不良偶尔能显示偶尔花屏。我建议在接线后用万用表量一遍连通性不要只用肉眼确认杜邦线插紧了。CS和DC线如果接反了GxEPD2库会初始化失败屏幕直接白屏或黑屏。残影问题是墨水屏固有特性局部刷新时残影尤其明显。我的方案是每次翻页都做全屏刷新虽然刷新时间要1.5秒左右但阅读场景下这个延迟完全可以接受。如果你想要更流畅的体验也可以实现“局刷翻页每十页全刷一次”的策略但局刷一多残影会积累成阴影看着非常难受。5.2 WiFi连接不稳定与上传超时ESP32开热点后手机偶尔会提示“没有网络”这是正常现象因为热点本身不提供网络出口。你只需要在浏览器地址栏输入192.168.4.1不必依赖系统的“已连接WiFi”提示。上传大文件超时是另一个常见问题。WebServer库的客户端处理一次请求要占用一个连接如果上传过程中有数据包丢失重传可能导致内存溢出或超时。我的经验是把上传文件限制在10MB以内同时在上传前用LittleFS.remove(/book.txt)删掉旧文件确保有足够的空间。另外尽量避免在WiFi信号差的情况下传书ESP32的SoftAP本身信号强度一般距离超过三米就可能出现速度变慢。5.3 反复重启问题供电与复位电流如果发现在一刷新屏幕或者WiFi连上的瞬间ESP32重启十有八九是供电问题。这类问题的典型现象是串口监视器打印一行“Brownout detector was triggered”之后重启。这个英文提示的意思是芯片检测到供电电压跌落触发了欠压保护。解决办法就是把100uF电容尽可能靠近开发板的3.3V引脚铜线太长会产生寄生电感影响瞬时供流能力。还有一种情况就是ESP32开发板上的USB转串口芯片和主控共用5V电源当主控瞬时电流过大时USB口电压被拉低电脑端可能弹出“USB设备无法识别”。这时应改用独立5V电源给开发板供电不要依赖电脑USB口的电流稳定度。5.4 中文乱码与编码陷阱上传的TXT文件如果是从Windows记事本保存的大概率是GBK编码而ESP32侧使用UTF-8处理文本直接读出来就是乱码。这正是很多初学者问“为什么中文显示全是问号”的原因。最简单的解决办法是在传书之前把TXT文件另存为UTF-8编码记事本保存时在编码下拉框选择“UTF-8”。如果你想在代码里做一个自动转换也可以引入GBKtoUTF8这类转换库但会让代码量增加不少而且转换表会占用额外Flash空间。我的建议是与其在做转换上费功夫不如做一次“文件格式检查”——在上传完成后读取文件开头几个字节如果发现非UTF-8编码就提示用户改成UTF-8再传。还有一个小坑UTF-8 BOM文件开头三个字节EF BB BF会导致第一页显示一个隐藏字符读取文件时记得把BOM跳过去。6. 后续扩展与我的实操心得6.1 可以做哪些“接地气”的扩展这个项目的基础版已经能完整实现“传书—阅读—翻页—记忆进度”的闭环但我继续做了几个小扩展都效果不错。第一个是加蓝牙传书很多人说ESP32蓝牙和WiFi不能同时用其实它们可以共存只是在时间片切换上会有相互影响。我在项目里增加了BLE服务的支持手机用BLE调试助手发送TXT文件的数据块代码接收到后写入LittleFS这样就可以在没有电脑的情况下通过手机蓝牙传书。实际测试时BLE传输速度大约每秒1KB传一本几百KB的小说要几分钟不如WiFi快但胜在连接简单。第二个是加多本书管理在LittleFS里建立一个目录每次上传时用时间戳命名文件名然后在墨水屏上做一个极简的“书库列表”界面用按键上下选择按确认键打开。这也就是说阅读器要为每本书维护一个独立的进度记录我选用文本偏移量存在NVS里的一个JSON字符串中格式大概是{book1:123,book2:456}。第三个是加自动关机如果五分钟没有按键操作调用esp_deep_sleep()进入深度睡眠唤醒方式可以用一个GPIO连接到按键。深度睡眠时整机电流降到几十微安一个2000mAh的锂电池能撑几周。6.2 复盘时最想提醒新手的三件事第一件是“先驱动屏幕再写逻辑”不要一上来就把整个项目拆成一堆模块同时写。墨水屏的驱动库示例代码能正常显示“Hello World”之后再开始写分页和WiFi这样出问题时排查范围会小很多。我一开始想一步到位结果最后花了很长时间定位是屏幕驱动还是分页逻辑出了问题。第二件是“谨慎处理GPIO12”ESP32的GPIO12是MTDI引脚不能外部上拉涉及这一引脚的按键方案最好换成其他引脚或改用下拉触发。我在接线时没有注意这个问题导致程序反复重启后来查资料才知道是GPIO12的坑。第三件是“墨水屏不是LCD”它不能高频刷新也不应该作为实时显示设备。做这个项目我最大的感悟是先想清楚它能做什么再动手而不是为了炫技把所有传感器都堆上去。墨水屏和ESP32的组合天然适合做低功耗、静态阅读场景的设备如果想做动画或者看视频请换回TFT屏幕别又绕回去了。这套代码我从零写到最后跑通大概用了两个周末的时间。如果只是跟着抄代码应该两三个小时就能让书页显示出来。真正花时间的是在分页算法、中文编码、供电稳定性这些细节上做调整。如果你是第一次碰ESP32和墨水屏从拷贝代码开始再逐段理解逻辑是最快上手的方式。后续我还会在这台阅读器上继续做屏幕方向自动切换和目录跳转功能到时候再写一篇分享。