基于ESP32与电子墨水屏的智能公交信息显示系统设计与实现

发布时间:2026/8/19 4:04:52
基于ESP32与电子墨水屏的智能公交信息显示系统设计与实现 1. 项目缘起为什么是电子墨水屏公交时刻表几年前我在家里玄关处挂了一个小相框用来放一些便签和提醒事项。后来我突发奇想能不能把它变成一个能自动显示公交到站信息的“智能挂件”这样出门前看一眼就知道下一班车还有多久到不用再掏出手机、解锁、打开APP省去不少麻烦。市面上当然有现成的智能家居显示屏但要么太贵要么功耗高需要频繁充电要么屏幕在室内强光下反光严重。直到我接触到了电子墨水屏e-Paper它的特性完美契合了这个场景超低功耗只在刷新画面时耗电、类纸质感、强光下清晰可视而且一旦显示内容即使断电也能持续显示。这个“e-Paper Bus Timetable”项目就是基于这个想法诞生的。它的核心目标是打造一个低成本、低功耗、免维护理想情况下的离线公交信息显示器。你可以把它挂在门后、贴在冰箱上或者放在办公桌旁让它静静地、清晰地为你提供最关心的那几路公交车的实时到站信息。这不仅仅是一个极客玩具更是对“无感交互”和“环境化信息显示”的一次实践——让信息自然地融入环境在你需要的时候它就在那里。整个项目的技术栈并不复杂但涉及硬件选型、软件架构、数据获取和电源管理等多个环节的巧妙结合。下面我就把自己从构思、选型到实现、优化的全过程以及踩过的坑和收获的经验毫无保留地分享出来。2. 核心设计思路与方案选型做一个电子墨水屏公交牌听起来简单但拆解开来需要解决几个关键问题显示设备用什么数据从哪里来主控芯片选哪个如何供电每个选择背后都有一连串的权衡。2.1 显示核心电子墨水屏的选型考量电子墨水屏是项目的灵魂。我的选型主要基于以下几点尺寸与分辨率玄关或桌面使用尺寸不宜过大。我最终选择了7.5英寸、800x480分辨率的黑白三色屏黑、白、红。这个尺寸足够显示4-5条公交线路的详细信息800x480的分辨率保证了汉字显示的清晰度。三色屏通常是黑白红的“红”色可以用来高亮显示“即将到站”、“延误”等关键信息视觉提示效果更好。驱动接口常见的有SPI和并行接口。SPI接口简单引脚占用少但刷新速度慢并行接口速度快但需要更多IO。对于公交信息这种几分钟才刷新一次的场景SPI接口完全足够还能简化硬件连接。我选择的屏幕配套的驱动板已经将复杂的波形控制集成好了我们只需要通过SPI发送图像数据即可。刷新模式电子墨水屏有局部刷新和全局刷新。全局刷新会闪屏全屏先变黑再变白但显示效果最干净局部刷新几乎无闪烁但长期使用可能留下残影。我的策略是定时如每10次进行一次全局刷新以清残影平时使用局部刷新来更新部分数据如倒计时时间这样兼顾了体验和显示质量。注意购买时一定要确认屏幕配套的控制器是否支持你计划使用的微控制器如ESP32以及是否有完善的驱动库如GxEPD2。自己从零写驱动非常复杂涉及电压波形控制不建议尝试。2.2 大脑与联网主控芯片的抉择主控需要负责获取网络数据、处理信息、控制屏幕刷新。可选方案有树莓派Zero W、ESP8266、ESP32等。树莓派Zero W功能强大能跑完整Linux编程方便。但功耗相对较高上百mA需要复杂的电源管理才能实现电池续航系统也有一定复杂性。ESP8266性价比之王Wi-Fi功能完善深度睡眠下功耗极低~20μA。但内存和计算资源有限处理复杂JSON或同时驱动较大屏幕时可能吃力。ESP32在ESP8266的基础上增加了蓝牙、更多内存、更快的CPU和更丰富的外设。功耗控制同样优秀。我选择了ESP32。原因如下首先公交API返回的JSON数据可能不小ESP32的更大内存通常4MB处理起来更从容其次未来如果想增加语音提示、按钮交互等功能ESP32的扩展能力更强最后其双核处理器允许我将网络请求和屏幕刷新放在不同任务中提高响应性。虽然成本比ESP8266稍高但带来的灵活性是值得的。2.3 数据来源公交API的获取与处理这是项目的“食材”来源。国内许多城市的公交数据都有开放接口通常由本地公交集团或数据服务商提供。寻找API可以搜索“XX市公交实时数据API”、“交通数据开放平台”。一些常见的模式是请求“线路站点列表”和“车辆到站信息”。你需要找到两个关键接口一是通过线路名称获取线路ID和所有站点ID二是通过站点ID查询经过该站点的所有车辆的实时位置和预计到站时间。API调用与分析使用工具如Postman或写一段简单的Python脚本测试API。重点关注返回的数据格式通常是JSON、数据更新频率、是否需要API Key或Token。例如一个典型的响应可能包含busNumber车号、distance距离本站米数、arriveIn预计到达秒数。数据处理逻辑微控制器拿到JSON后需要解析并计算出最直观的信息。我的逻辑是过滤出我关心的线路如“123路”、“456路”。对每条线路找出距离目标站点最近的一辆车。将arriveIn秒转换为“X分钟”或“即将进站”等更友好的格式。如果arriveIn小于90秒则用红色高亮显示“即将到站”。实操心得很多API有调用频率限制。不要过于频繁地请求如每10秒一次这不仅可能被禁也毫无必要。公交车的移动速度没那么快。将数据获取间隔设置为30-60秒是合理的。此外一定要在代码中加入健壮的错误处理当网络异常或API返回错误时设备应能优雅降级如显示上次成功获取的数据和时间戳而不是死机或白屏。2.4 供电方案持久续航的秘诀我们希望设备能无线、安静地工作数周甚至数月。ESP32在深度睡眠Deep Sleep模式下的电流可以低至10μA左右但电子墨水屏本身不通电时几乎不耗电关键耗电在于Wi-Fi连接和数据传输的瞬间。我的供电方案是一块6000mAh的锂聚合物电池Li-Po配合一个高效的5V升压模块。因为ESP32和屏幕驱动板通常需要3.3V而电池电压在3.7V-4.2V之间波动直接连接可能不稳定。升压模块能提供稳定的5V输出再通过板载LDO降到3.3V。工作流程与功耗控制ESP32从深度睡眠中被定时器唤醒。连接Wi-Fi约2-3秒电流~100mA。请求并获取公交数据约1-2秒电流~80mA。处理数据生成新图像通过SPI发送给屏幕并触发刷新屏幕刷新时电流峰值可达100mA以上持续约2-4秒。所有任务完成后ESP32再次进入深度睡眠等待下一个周期如60秒后。续航估算 假设一个周期总活跃时间为10秒平均电流150mA睡眠50秒电流10μA。 每小时耗电量 ≈ (10/3600)h * 0.15A (50/3600)h * 0.00001A ≈ 0.000417 Ah 6000mAh电池理论续航 ≈ 6Ah / 0.000417 A ≈14388小时约600天。这仅是理论值实际会因Wi-Fi信号强度、屏幕刷新类型全局刷新更耗电、电池自放电等因素打折扣但实现数月续航是完全可行的。3. 硬件组装与电路连接详解有了设计方案接下来就是动手把零件组装起来。这个过程需要细心避免接错线烧毁元件。3.1 物料清单BOM组件型号/规格数量备注主控制器ESP32开发板如ESP32-DevKitC1带Wi-Fi引脚引出方便电子墨水屏7.5英寸800x480黑白红三色1需配套SPI接口驱动板电池锂聚合物电池6000mAh3.7V1带JST-PH连接线升压模块3.7V转5V效率90%1如MT3608模块电源开关小型拨动开关1用于彻底断电杜邦线公对公、母对母若干用于连接外壳3D打印或现成相框改造1用于保护和美观3.2 电路连接步骤核心连接原则先电源后信号断电操作仔细核对。电源通路搭建将电池的正极红线和负极黑线分别连接到升压模块的IN和IN-。将升压模块的OUT5V连接到电源开关的一端开关的另一端引出作为系统的VCC5V。将系统的VCC5V连接到ESP32开发板的VIN引脚注意不是3.3V引脚。同时也将VCC连接到屏幕驱动板的VCC引脚通常是5V输入。将电池的负极地连接到升压模块的IN-并从此处引出一根公共地线GND分别连接到ESP32的GND引脚和屏幕驱动板的GND引脚。SPI信号连接ESP32 - 屏幕驱动板SCLK时钟连接ESP32的GPIO 18到屏幕的SCK。MOSI主机输出连接ESP32的GPIO 23到屏幕的SDI或MOSI。CS片选连接ESP32的GPIO 5到屏幕的CS。DC数据/命令连接ESP32的GPIO 17到屏幕的DC。RST复位连接ESP32的GPIO 16到屏幕的RST。BUSY忙信号连接ESP32的GPIO 4到屏幕的BUSY。这个引脚很重要用于检测屏幕是否正在刷新避免主机在屏幕忙时发送数据。重要提示引脚编号并非绝对你可以根据自己ESP32板子的可用引脚在代码中重新定义。但一旦定义好物理连接必须与代码一致。建议先用面包板测试所有功能确认无误后再焊接或使用排针固定。最终检查确保所有电源极性正确正对正负对负。确保信号线连接牢固没有虚接。将电池放入电池仓或固定好准备进行软件烧录。4. 软件实现从数据到显示的完整流程硬件是躯体软件是灵魂。我们使用Arduino IDE来开发ESP32的程序因为它对硬件操作和网络通信的支持非常友好。4.1 开发环境与库准备安装Arduino IDE并添加ESP32开发板支持在“首选项”的附加开发板管理器网址中添加。安装必要的库GxEPD2这是一个功能强大、支持多种墨水屏的驱动库。在库管理中搜索并安装。ArduinoJson用于解析从API返回的JSON数据。WiFi和HTTPClientESP32内置无需额外安装。4.2 代码结构解析整个程序将围绕一个主循环和深度睡眠展开。以下是核心代码逻辑的分解// 1. 引脚定义与库引入 #include GxEPD2_BW.h // 黑白驱动我们用的三色屏有特定驱动 #include GxEPD2_3C.h // 黑白红三色驱动 #include WiFi.h #include HTTPClient.h #include ArduinoJson.h // 定义你的屏幕型号根据购买的具体型号修改 GxEPD2_3CGxEPD2_750c, GxEPD2_750c::HEIGHT display(GxEPD2_750c(/*CS*/5, /*DC*/17, /*RST*/16, /*BUSY*/4)); // WiFi凭证 const char* ssid 你的WiFi名称; const char* password 你的WiFi密码; // 公交API信息 const String apiUrl https://api.example.com/bus?stopIdYOUR_STOP_ID; // 2. 初始化设置 void setup() { Serial.begin(115200); display.init(115200); // 初始化显示屏 connectToWiFi(); // 获取并显示数据 if (fetchAndDisplayBusInfo()) { Serial.println(信息更新成功); } else { displayError(更新失败); } // 所有任务完成进入深度睡眠 Serial.println(进入深度睡眠...); esp_sleep_enable_timer_wakeup(60 * 1000000); // 睡眠60秒微秒单位 esp_deep_sleep_start(); // setup()函数不会再次运行直到下一次被唤醒 } // 3. 主循环本例中为空因为所有工作都在setup里完成 void loop() { // 深度睡眠模式下不会执行到这里 } // 4. 连接WiFi函数 void connectToWiFi() { WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(WiFi连接成功); } // 5. 核心函数获取并显示公交信息 bool fetchAndDisplayBusInfo() { HTTPClient http; http.begin(apiUrl); int httpCode http.GET(); if (httpCode HTTP_CODE_OK) { String payload http.getString(); http.end(); // 解析JSON DynamicJsonDocument doc(2048); // 根据API返回大小调整 DeserializationError error deserializeJson(doc, payload); if (error) { Serial.print(JSON解析失败: ); Serial.println(error.c_str()); return false; } // 假设API返回一个包含buses数组的JSON JsonArray buses doc[buses]; // 创建显示内容 display.setRotation(1); // 根据你的安装方向调整旋转 display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); display.setTextColor(GxEPD_BLACK); display.setFont(FreeMonoBold12pt7b); // 选择一种字体 int yPos 30; // 起始Y坐标 display.setCursor(10, yPos); display.println(公交站XX站); yPos 30; for (JsonObject bus : buses) { const char* line bus[line]; // 123路 int arriveIn bus[arriveIn]; // 秒 String displayText String(line) : ; if (arriveIn 90) { display.setTextColor(GxEPD_RED); // 使用红色高亮 displayText 即将到站; } else { display.setTextColor(GxEPD_BLACK); displayText String(arriveIn / 60) 分钟; } display.setCursor(10, yPos); display.println(displayText); yPos 25; display.setTextColor(GxEPD_BLACK); // 重置为黑色 } // 显示更新时间 display.setCursor(10, 280); display.setTextColor(GxEPD_BLACK); display.println(更新于: getTimeString()); } while (display.nextPage()); return true; } else { http.end(); Serial.printf(HTTP请求失败错误码: %d\n, httpCode); return false; } } // 6. 辅助函数获取时间字符串 String getTimeString() { // 这里需要从网络获取时间或使用RTC模块 // 简化版仅返回编译时间或粗略时间 return __TIME__; } // 7. 错误显示函数 void displayError(const char* msg) { display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); display.setTextColor(GxEPD_RED); display.setFont(FreeMonoBold12pt7b); display.setCursor(50, 150); display.println(错误); display.setCursor(50, 200); display.println(msg); } while (display.nextPage()); }代码关键点解释深度睡眠模式整个程序逻辑都在setup()中完成。工作结束后通过esp_deep_sleep_start()进入深度睡眠。被定时器唤醒后ESP32会重启重新执行setup()而不是从loop()继续。这是实现超低功耗的关键。显示流程GxEPD2库使用一种“分页”渲染模式。在do...while(display.nextPage())循环中构造显示内容库会自动处理数据发送和屏幕刷新。错误处理网络请求可能失败JSON可能解析错误。必须有相应的if判断和降级显示如displayError函数避免设备“变砖”。字体处理墨水屏显示中文需要额外的字体库。你可以使用U8g2_for_Adafruit_GFX库配合中文字体文件.ttf转换后的.c文件但这会占用大量程序空间。对于固定线路信息也可以考虑用英文或拼音或者将常用汉字做成位图。4.3 数据解析与显示优化上面的示例代码假设了一个理想的API结构。现实中API可能更复杂。你需要仔细研究API文档使用Serial.print()将原始响应打印出来然后逐步解析。显示优化技巧局部刷新如果只是更新倒计时数字可以使用display.setPartialWindow()指定刷新区域而不是刷新整个屏幕速度更快、无闪烁。避免频繁全局刷新设置一个计数器每更新N次数据后才执行一次display.clearScreen()或全屏刷新以消除局部刷新可能积累的残影。缓存与对比可以在内存中缓存上一次显示的内容只有当新数据确实发生变化时才触发屏幕刷新进一步节省电力。5. 外壳设计与组装成品一个美观耐用的外壳能让项目从“原型”升级为“产品”。设计思路外壳需要留出屏幕开口、为ESP32和电池预留空间、考虑散热虽然功耗很低、以及安装方式挂墙、立式。实现方式3D打印这是最灵活的方式。使用Fusion 360或Tinkercad等软件建模。注意为屏幕和驱动板之间的排线预留走线空间。外壳可以设计成前后盖扣合的形式。现成相框改造找一个内尺寸略大于屏幕的相框。将屏幕、驱动板和ESP32层叠固定在一块亚克力板或PVC板上然后整体放入相框。电池可以贴在背面。这种方式成本低外观也不错。组装将所有部件用螺丝或双面胶固定在外壳内。确保排线不被过度弯折。开关按钮要露出外壳方便操作。可以在屏幕表面加一层亚克力板作为保护但注意不要压得太紧影响屏幕刷新。6. 常见问题与故障排查实录在开发过程中我遇到了不少问题这里把典型的列出来供大家参考。问题现象可能原因排查步骤与解决方案屏幕全白或全黑无内容1. 电源未接通或电压不足。2. SPI引脚连接错误。3. 屏幕初始化失败。1. 用万用表测量屏幕VCC和GND之间电压确保在3.3V/5V根据屏幕要求。2. 逐一检查SCLK, MOSI, CS, DC, RST, BUSY引脚连接是否与代码定义一致。3. 检查display.init()的波特率参数是否正确尝试降低波特率如115200改为9600。4. 在setup()中Serial.print调试信息确认程序执行到了初始化之后。屏幕刷新异常有残影或鬼影1. 刷新模式使用不当。2. 刷新后未等待足够时间就断电。1. 确保在需要完全清屏时使用全局刷新display.setFullWindow()display.display(true)。2. 屏幕刷新尤其是全局刷新后必须等待display.hibernate()被调用或等待足够时间查看屏幕规格书通常几百毫秒再切断电源。ESP32无法连接Wi-Fi1. SSID/密码错误。2. Wi-Fi信号太弱。3. 路由器设置了MAC过滤等。1. 检查代码中的SSID和密码注意特殊字符。2. 在代码中加入WiFi.setTxPower(WIFI_POWER_19_5dBm)提高发射功率试试。3. 尝试用手机热点测试排除路由器问题。HTTP请求失败1. 网络未连接。2. API URL错误或失效。3. 服务器证书问题HTTPS。1. 先确保Wi-Fi连接成功。2. 用电脑浏览器或Postman测试API URL是否有效。3. 对于HTTPSESP32可能需要更新根证书。在Arduino IDE中工具-SSL 根证书-更新。或者尝试使用http.begin(apiUrl, root_ca)指定证书较复杂。初期可先使用HTTP API测试如果可用。JSON解析失败1. 响应数据不是合法JSON。2.DynamicJsonDocument分配的内存太小。1. 将payload打印到串口复制到在线JSON验证器检查格式。2. 增大DynamicJsonDocument doc(2048)中的缓冲区大小。可以通过serializeJsonPretty(doc, Serial)打印解析后的结构确认键名是否正确。设备续航远低于预期1. 深度睡眠未成功进入。2. 有外围电路在睡眠时仍在耗电。3. 电池老化或质量差。1. 测量深度睡眠时ESP32的电流。将万用表串联在电池回路中应看到电流降至几十微安级别。如果仍在毫安级检查代码是否调用了esp_deep_sleep_start()且之前没有delay()或循环阻塞。2. 检查屏幕的BUSY引脚是否被上拉/下拉在睡眠时可能形成漏电回路。可以尝试在代码中将连接屏幕的GPIO在睡眠前设置为输入模式。3. 测试电池的实际容量。一个我踩过的大坑早期版本中我为了省事将屏幕的RST引脚直接接到了ESP32的EN使能引脚。结果每次ESP32深度睡眠后重启都会给屏幕一个复位信号导致屏幕异常。后来将RST接到一个普通的GPIO上由软件控制问题才解决。教训是硬件连接不能想当然要仔细看数据手册和原理图。7. 进阶优化与扩展思路基础功能实现后还可以让它变得更智能、更好用。增加物理按钮添加1-2个按钮用于手动触发刷新、切换显示的公交站点、进入配置模式等。配合ESP32的触摸引脚甚至可以实现触摸切换。离线时钟RTC模块加入DS3231等RTC模块即使在深度睡眠中也能保持精确时间。这样显示“更新于 14:30”就更有意义了也便于实现定时开关屏如夜间不刷新。Web配置界面让设备启动时作为一个Wi-Fi热点AP模式手机连接后通过网页输入家庭Wi-Fi密码、要查询的公交站点ID等配置信息无需再修改代码烧录。这需要用到ESP32的AsyncWebServer等库。多站点/多线路轮播如果你关心多个方向的公交车可以让设备每隔几次刷新就轮换显示不同站点的信息。天气信息集成在屏幕的角落显示当前天气和温度出门前一眼掌握更多信息。这需要调用另一个天气API。更优雅的电源管理使用太阳能板配合充电管理模块在光照好的环境下可以实现真正的“永久”续航。这个项目最吸引我的地方在于它完美地结合了硬件和软件将一个实用的想法变成了一个安静融入日常生活的物件。每次出门前目光自然地落在它上面获取信息的过程毫无负担这种“科技服务于人”的体验正是创造的乐趣所在。从最初的闪烁不定到现在的稳定运行期间每一次调试和优化都让这个小小的屏幕变得更加可靠。如果你也厌倦了频繁查看手机不妨动手试试打造一个属于你自己的信息窗口。