
简介面向嵌入式与物联网开发者的电子纸/NB-IoT/GPRS HAT 扩展板示例代码包聚焦电子纸显示、NB-IoT/GPRS 通信与树莓派 HAT 标准集成适合需要快速上手低功耗远程可视化终端的初学者和做原型验证的工程师。压缩包内共 151 个文件以 40 个 C 源码和 40 个目标文件为主体配以 34 个 H 头文件和 32 张 BMP 图片素材可用于图像取模与界面设计包内另有 makefile 工程脚本与配置文件整体仅 815KB结构精简。代码中既包含电子纸驱动、屏幕刷新和图像缓冲管理也涵盖 NB-IoT 模块 AT 指令交互、GPRS 网络注册与数据通道建立便于对照学习模块初始化、协议栈调用和低功耗处理逻辑。已有 192 人学习/下载适合在树莓派或同类微控制器上验证 HAT 外设控制流程并作为自己项目改造与协议二次开发的参考起点。整体可作为从零配置到联调上手的完整参考。1. 把一块墨水屏 HAT 和 NB-IoT/GPRS 模组合并到一块板子上Demo Code 写的究竟是什么你手头这块 HAT 看起来像是一块带 SPI 接口的墨水屏扩展板但它真正特殊的部分在于背面还焊着一个 NB-IoT/GPRS 透传模组。这类板子面向的场景很直接在没有 Wi-Fi、也不方便布线的露天环境里用一个低功耗终端显示数据然后通过蜂窝网络把设备状态或传感器采集值推上云端。墨水屏负责显示,NB-IoT/GPRS 负责联网。HAT 的意义在于它把两件事拼到了树莓派上,过去要单独接屏幕、单独接串口模组、再自己做电平转换和电源管理的活现在全部集成在一块板子上。Demo Code 的价值在于给了你一条从点亮屏幕到打通网络的最短路径。我在拿到类似的板子时第一件事不是跑完整的业务逻辑而是按 点亮墨水屏→注册网络→发送一条 AT 指令→显示一个远程下发的数字 这个顺序把每段代码独立跑通。这篇文章就按这个顺序来讲硬件上哪些引脚需要注意初始化时三条主线如何分开写以及真正上生产时哪些参数要调哪些坑能提前避开。适合正在做环境监测、资产标签、物流信息屏这类低功耗应用的工程师也适合第一次接触 e-paper 和蜂窝模组合体的嵌入式开发者。2. HAT 上的硬件结构e-paper 驱动引脚与 NB-IoT/GPRS 模组共存的关键设计拿到一块 Demo 板先不要急着编译代码。打开原理图找到两块芯片之间的连线很多跑不起来的故障都出在总线复用和供电上。2.1 SPI 总线怎么分配屏幕和模组各自占用了哪些引脚墨水屏控制器的标准接口是 SPISCLK、MOSI、MISO、CS、DC、RST、BUSY。通常在 HAT 上会占用树莓派的 SPI0 引脚CS 默认接到 CE0DC 用 GPIO24RST 用 GPIO25BUSY 用 GPIO17。而 NB-IoT/GPRS 模组走的是 UART 串口常见接法是 UART0ttyAMA0或迷你 UARTttyS0模组的 TX/RX 经过板上电平转换后接到树莓派的 RX/TX。这里最容易犯的错是把模组的 PWRKEY 当成普通 GPIO 拉高就行实际上需要一个大于 500ms 的低电平脉冲来触发开机。如果你的 Demo Code 初始化之后日志里一直看不到RDY先查 PWRKEY 的延时代码而不是查网络。另外一个高频坑是引脚复用。树莓派默认把 GPIO14/15 分配给了 UART但如果你同时启用了蓝牙主 UART 会被改到 ttyAMA0miniuart 则是 ttyS0。不同板卡默认串口名不一样。在跑 Demo Code 之前先确认你的系统里/dev/ttyAMA0是否存在以及你的模组是否要求用硬件流控RTS/CTS。有一部分 NB-IoT 模组支持高波特率下的流控但 Demo Code 通常默认关闭。2.2 NB-IoT 和 GPRS 不是同一个网络连接策略要分开写同一个模组支持 NB-IoT 和 GPRS 双模不意味着你可以用一套 AT 命令走到底。两者的核心差异是频点和数据能力。GPRS 使用现有 2G 网络覆盖广但延迟高、速率低优势是运营商退网之前任何一个地方都有信号。NB-IoT 是窄带物联网带宽只有 200kHz却能深入地下室、水表井支持的连接数远大于 GPRS但它的 PS 业务不能实时收发要等网络侧寻呼窗口。所以 Demo Code 里常见的做法是把连接流程做成可配置的ATCMEE2开启详细错误码ATCFUN?查询射频状态然后根据当前模组返回的CPSI消息判断是落在 LTE 还是 GSM 上。对比项NB-IoTGPRS下行速率约 100kbps 上限理论 85kbps 左右连接时延高取决于 PSM 周期低随时可传覆盖能力好比 GPRS 强约 20dB一般受基站距离影响大流量资费低按消息计费按流量稍高模组休眠支持 PSM/eDRX不支持靠 DRX实话说如果你做的是每天上传一次并在本地显示一个结果的场景NB-IoT 的 PSM 模式就是为这个需求量身定做的。但如果你需要频繁下发命令到终端比如远程翻页或更新屏幕内容GPRS 在网络层面的即时性反而更好。我的建议是Demo Code 里预留一个网络制式选择宏不要写死在初始化流程里等现场实测信号之后再做决定。2.3 供电与电平为什么屏幕在刷新一瞬间画面会出现乱码墨水屏刷新的瞬间电流可以到 50mA 左右NB-IoT 发射时瞬间电流能到 2A 峰值。如果这两者共用同一个 LDO屏幕的驱动电压会被拉低表现为刷新后残留残影、局部不清洗或者干脆花屏。HAT 设计合理的话会给蜂窝模组走独立的 DC-DC并且电源输入一般要求 5V/3A。如果你外接的树莓派电源功率不足问题通常不会直接暴露在开机的 5 分钟里而是在 NB-IoT 第一次注册网络、模组发射功率拉满时出现整板重启。除了电源电平也需要确认。树莓派 GPIO 是 3.3V部分 GPRS 模组如果要直连必须看它的 UART 电平。HAT 板上有电平转换芯片就不用管如果是自己飞线的板子宁可加一个 3.3V 到 1.8V/2.8V 的转换也不要直接连。这个细节在跑 Demo Code 时完全看不出来要等接上真正的模组才会暴露。3. Demo Code 的文件构成与初始化流程先点亮墨水屏再唤醒模组无论是官方提供的示例还是你自己整理的工程一套可用的 Demo 通常会分成三个独立模块屏幕驱动、蜂窝模组 AT 指令封装、业务主循环。这样拆分的好处是调试时可以单独跑不用为了看屏幕就非得等网络注册完成。3.1 典型目录结构与主循环逻辑常见的目录会是这样lib/放 e-paper 的底层驱动gprs/放 AT 指令的收发函数main.py负责把两部分串起来。我在写这类 demo 时会额外加一个status.py用来保存屏幕内容和网络状态的枚举值避免后面读写屏幕和模组时 import 循环。主循环的运行顺序固定为初始化 SPI 和 GPIO → 模组做软复位 → 查询信号强度 → 执行一次业务上报 → 根据返回结果刷新屏幕 → 进入低功耗等待。注意这里“低功耗等待”并不是简单地sleep而应该是让模组进入 PSM 并在树莓派侧关掉不必要的外设时钟。如果你用的是树莓派而不是单片机树莓派本身待机功耗就有几百毫瓦这和 HAT 的低功耗设计其实是矛盾的。所以不少 Demo Code 实际上会跑一段时间后主动关机用 RTC 或模组的定时唤醒引脚来重启系统——这属于进阶玩法但至少你得在代码里留出这个分支。3.2 用 Python 初始化墨水屏的最小 Demo我现在一般用 Python 写初始化和显示逻辑原因很简单demo 不需要极致性能Python 的 GPIO 库和 SPI 库足够成熟而且可读性高。下面这段代码删掉了厂商库中不必要的封装保留最核心的时序import spidev import RPi.GPIO as GPIO import time # 引脚定义参考 HAT 原理图 PIN_DC 24 # 数据/命令选择 PIN_RST 25 # 复位 PIN_BUSY 17 # busy 检测低电平表示忙 GPIO.setmode(GPIO.BCM) GPIO.setup(PIN_DC, GPIO.OUT) GPIO.setup(PIN_RST, GPIO.OUT) GPIO.setup(PIN_BUSY, GPIO.IN) spi spidev.SpiDev() spi.open(0, 0) # CE0 spi.max_speed_hz 4000000 # 墨水屏通常不超过 4MHz def reset(): GPIO.output(PIN_RST, GPIO.LOW) time.sleep(0.1) GPIO.output(PIN_RST, GPIO.HIGH) time.sleep(0.1) def send_command(cmd, dataNone): GPIO.output(PIN_DC, GPIO.LOW) spi.xfer([cmd]) if data: GPIO.output(PIN_DC, GPIO.HIGH) spi.xfer(data) def wait_until_idle(): while GPIO.input(PIN_BUSY) GPIO.LOW: time.sleep(0.01) reset() wait_until_idle() # 初始化序列示例先关显示后开显示 send_command(0x00) # 设置命令表 time.sleep(0.05) send_command(0x04) # power on send_command(0x06) # booster soft start send_command(0x50) # VCOM and data interval send_command(0x12) # 这里演示的是驱动扫描周期设置代码最后的send_command(0x12)是一个占位不同的 e-paper 屏幕宽高不同驱动 IC 的寄存器地址也不同。你拿到 Demo Code 后需要对照屏幕型号的数据手册确认这个初始化序列是否完整。这个最小 demo 的重点是wait_until_idle函数——墨水屏的驱动 IC 在刷新时会拉低 BUSY 引脚如果跳过这一步紧接着发进来的扫描命令会被丢弃画面就会出问题。我见过很多开发者认为初始化没反应是硬件坏了其实只是缺少忙状态等待。3.3 通过 UART 初始化 NB-IoT/GPRS 模组的 AT 指令序列蜂窝模组的初始化不是一次AT就能完成的。正确的顺序是打开串口 → 发送AT同步波特率 → 设置模块全功能 → 查询 SIM 卡 → 注册网络。代码中需要处理两个细节命令后的\r\n结尾以及模组可能返回的 unsolicited result code如URC: ...。下面的代码演示了如何写一个带超时的发送函数import serial ser serial.Serial( port/dev/ttyAMA0, baudrate9600, timeout1.5 ) def send_at(cmd, wait_time1, patternOK): ser.reset_input_buffer() ser.write((cmd \r\n).encode()) time.sleep(wait_time) resp ser.read_all().decode(errorsignore) if pattern in resp: return resp else: raise RuntimeError(fAT 指令失败: {cmd} - {resp}) # 同步波特率并关闭回显 send_at(AT, patternOK) send_at(ATE0, patternOK) # 设置网络制式14 代表 LTE13 代表 GPRS 优先不同模组数值有差异 send_at(ATCNMP14, patternOK) # 设置 APN以阿里云物联网卡示例实际按运营商文档 send_at(ATCGDCONT1,IP,cmnbiot, patternOK) # 等到网络注册成功条件为 1 或 5 resp send_at(ATCEREG?, patternOK) if CEREG: 1,0 in resp or CEREG: 1,5 in resp: print(网络注册成功)注意ATCNMP这个值不是 apn 后面的参数它是网络制式选择每个模组厂家的定义都不一样。你必须在 Demo Code 里找到这行指令改成现场网络环境对应数值。ATCEREG?返回的第二位如果一直是3说明被网络拒绝大概率是 SIM 卡没插到位或者 APN 填错。如果一直是2说明搜索不到网络这时要检查天线是否接好。很多开发板没有焊接天线座直接用弹簧天线放在金属机箱里信号强度会掉得极快这是现场最常见的返修原因。4. 从 Demo 到可用的物联网节点数据上云与远程刷新屏幕屏幕点亮了模组也注册上网络了下一步就是让它做一件有意义的事。常见的 demo 会做成一个环境数据展示终端传感器读到温度湿度通过模组上报到云平台平台再下发一行文字显示在墨水屏上。这两条链路看似对称实现上的坑却完全不同。下面分别梳理。4.1 用 HTTP 或 MQTT 上行的最小实现方案蜂窝模组通常自带 TCP/IP 协议栈你只需要通过 AT 指令建立 socket 连接。用 HTTP 还是 MQTT取决于云平台。我比较推荐在 NB-IoT 场景里直接使用 MQTT因为它的报头开销小而且支持遗嘱消息和会话保持。但很多模组固件的 MQTT 指令并不支持 TLS 加密因此你必须把 MQTT 服务器端口从默认的 8883 改成 1883并在核心网侧用内网 VIP 做一层安全隔离。下面是一段在模组上通过 AT 指令连接 MQTT broker 的示例# 建立 TCP 连接到 broker假设 IP 为 120.55.178.20端口 1883 send_at(ATQIOPEN1,0,\TCP\,\120.55.178.20\,1883,0,1, wait_time5, patternOK) # 发送 MQTT CONNECT 报文这里只是为了演示指令格式 # 实际应用建议把 MQTT 协议栈直接放到树莓派侧 import paho.mqtt.client as mqtt def on_connect(client, userdata, flags, rc): print(Connected to broker, rc:, rc) client mqtt.Client() client.on_connect on_connect client.connect(120.55.178.20, 1883, keepalive60) client.loop_start() client.publish(/device/1985/sensor, {\temp\:25.6,\hum\:48.0})注意这里出现了两条路径一条是 AT 指令直接让模组发数据另一条是树莓派侧跑 MQTT 协议栈、模组只当一个透传网关。前者省电但代码不可读、调试痛苦后者功耗更高但迭代快。Demo Code 通常会把两条路径都写在注释里工程落地我一般选后者——树莓派的经济价值在于可以跑完整的应用逻辑没必要把自己变成一颗 AT 指令翻译机。4.2 远程下发一条刷新指令数据格式与脏标记墨水屏不同于 LCD它的内容更新需要切换全局刷新和局部刷新。如果你从云端下发一条 JSON 指令告诉它有新数据屏幕驱动要做的不是直接改写显存而是先把原有内容清成白色再画新内容最后刷新。这个流程里最怕的是在刷新过程中断电墨水屏会留下永久性的残留。所以我在处理远程刷新时会在数据包里额外携带两个字段refresh_type和display_mode。refresh_type用于告诉终端用全局刷新还是局部刷新display_mode用来说明新数据是覆盖整个屏幕还是仅更新某个区域的数字。终端收到消息后先写到一个本地文件再在树莓派空闲时间执行刷新。这样可以避免因为 NB-IoT 网络延迟导致的数据包乱序也方便在断网重连后重新拉取未确认的消息。import json import os # 模拟云端下发的消息结构 message { device_id: epaper_hat_01, data: {pressure: 1013, temperature: 24}, refresh_type: partial, # 局部刷新 display_mode: replace_area, # 仅更新内容区域 seq: 1004 } cache_path /tmp/display_pending.json with open(cache_path, w) as f: json.dump(message, f) # 显示前先检查是否有未处理的刷新请求 if os.path.exists(cache_path): with open(cache_path) as f: pending json.load(f) if pending[refresh_type] partial: # 调用墨水屏局部刷新函数只刷新数据区域 draw_partial_text(pending[data]) else: draw_full_screen(pending[data]) os.remove(cache_path)这里最容易被忽略的是seq字段。物联网设备如果掉线后由于网络重放可能收到两条一模一样的指令。没有序列号去重的话屏幕会在几分钟内被刷新两次不仅耗电而且伤屏。还有一个更隐蔽的问题NB-IoT 的下行数据可能是通过短消息服务SMS下发的也就是云端把 JSON 内容封装进一条短信里模组通过CMTI提示符收到。这种情况下消息顺序由网络侧决定你必须在应用层做去重和排序。4.3 必调的四个参数波特率、APN、超时时间与显示刷新间隔从 Demo 走向产品最先需要固化的四个参数可以列成表格。这些参数在 demo 代码里往往是以“默认值”出现的但恰恰就是它们决定设备在公网环境里的表现。参数推荐值为什么这么设UART 波特率9600 或 115200NB-IoT 模组对波特率没有严格要求但越低越不容易因干扰丢字节APN按运营商卡设置写错 APN 不会报错但网络注册永远是拒绝状态TCP 连接超时至少 15sNB-IoT 随机接入可能加长到 10s 以上超时设短会导致误判屏幕刷新间隔300s 以上墨水屏刷新本质是电泳过程频繁刷新会缩短寿命第四个参数容易有争议墨水屏显示静态信息很合适但如果你的应用要每秒刷新一次数据那就不适合这类屏幕。HAT Demo 板的正确用法是定时上传 按需刷新。我一般会把刷新间隔设计成与 NB-IoT 的 TAU 周期对齐比如 TAU 设为 2 小时则屏幕也最多两小时刷新一次。这样既能用 PSM 省电又能保证用户看到的不是太久以前的数据。5. 进阶调优从 Demo Code 到户外长期运行验证和功耗怎么平衡最后一节不铺开讲只讲两个我在调试这类板子时一定会做的动作以及一个快速定位问题的方法。5.1 墨水屏的 LUT 刷新波形与残影处理驱动墨水屏的关键不在画图函数而在控制刷新波形LUT。同一种屏幕在不同温度下需要的驱动电压和帧率不同。很多 Demo Code 把 LUT 烧死在寄存器里冬天户外出现重影时会花大量时间检查硬件。我的做法是改用一个可调整的公共 LUT 数组把温度区间分成 010℃、1030℃、30℃ 以上三档每档使用对应的对比度参数。改动幅度只有几十行代码显示效果能提升一个量级。在跑 Demo Code 时如果你发现刷新后图案边缘有细密的白点大概率是 LUT 低温参数不合适和屏幕本身无关。5.2 在 Red Hat 系 Linux 上运行 Demo Code 的设备权限问题树莓派默认系统是 Raspberry Pi OS但不少开发者会把这套 HAT 接到工业级的 ARM 板卡上跑 Fedora 或 Red Hat 兼容发行版。这时会遇到 GPIO 库不一致的问题RPi.GPIO只支持树莓派内核换到这类系统后 import 直接报错。解决方法是用libgpiod提供的gpioset和gpioget命令行接口或者直接操作/dev/gpiochip0。另外串口设备权限在 Red Hat 系默认属于dialout组记得把当前用户加入该组否则 python 的 serial 库会提示无法打开端口。# 用 gpioset 代替 RPi.GPIO控制 HAT 复位引脚 gpioset gpiochip0 250 sleep 0.1 gpioset gpiochip0 251 # 检查串口设备属于谁通常 ttyAMA0 属于 root:dialout ls -l /dev/ttyAMA0 sudo usermod -aG dialout $USER这算不上高深技术但能让你少浪费半天去编译一堆过时的内核模块。如果你打算长期维护这个 demo尽量在上层封装一个hardware.py把 GPIO 操作和串口打开方式与具体平台解耦后续迁移成本会低很多。5.3 快速验证 Demo 是否可用的三个检查点当设备从实验室移到户外后我会用以下三个检查点判断整体流程是否正常而不是直接去看屏幕有没有字。第一打开模组日志观察一条 AT 指令后是否有CEREG: 1,5或CGREG: 1没有这个结果就说明网络注册失败此时刷屏没有任何意义。第二在数据上报之后立即用另一台设备订阅同一个 MQTT 主题看从终端发出publish到云端收到数据的时间差。NB-IoT 场景里这个差值如果低于 5 秒说明设备正好在 RRC Connected 状态这是正常现象如果大于 30 秒说明网络侧进入了空闲状态你最好不要用心跳每 60 秒一次这种优化去做保活而是直接接受这个延时并调整上报周期。第三检查屏幕在断电重连后是否能恢复到最后一次显示的内容。如果每次重启屏幕都花屏大概率是初始化序列里缺少恢复环境温度的步骤把模组和屏幕放在同一个机壳里就能解决。完成这三个检查点这个合体的 HAT 基本就算过了现场验收线。接下来你需要做的只是根据具体业务去替换数据源和显示布局。本文还有配套的精品资源点击获取