问题背景
省级工业园区要落实重点用能单位能耗在线监测要求,园区企业的配电房里电表、水表、气表品牌杂乱、协议不一,过去靠人工抄表、月报表格,数据滞后无法监管。EM-200 的任务很明确:本地定时抄读三表,每日 0 点冻结,经 4G 上报园区能耗监测平台,数据直通市级平台。抄表数据关联能耗考核,一条都不能丢。
硬件是 ESP32-S3-WROOM-1(16MB Flash + 8MB PSRAM,双核 Xtensa LX7 @240MHz,512KB 内部 SRAM),3 路 RS485 各配一片 SP3485,4G 走 USB-OTG 做主上行、W5500 以太网做备份,TF 卡挂 SPI3 存冻结与断网缓存,另有外置 RTC 和掉电检测电路。
选型阶段算过三笔账:64 块表每 5 分钟一轮,每表合并后 1–2 帧,单帧总线时间约 60ms,总线占用约 2.6%;每帧 CRC 加解析只有几十微秒,双核占用低于 1%;表档案 64×300B 约 19KB、值库约 12KB、冻结数据约 12KB/天,总量不到 100KB,远小于 512KB。唯一瓶颈是 UART 只有 3 个,正好被 3 路 RS485 占满,所以 4G 走 USB-OTG 而不是 UART。
设计思路
软件分四层:服务层(抄表调度、MQTT 上报、存储续传、配置系统服务)、数据模型(表档案库 + 实时值库 + 冻结/事件记录)、协议适配(Modbus RTU 主站,协议层留抽象)、总线(FreeRTOS 队列 + 事件组)。
三个决策定了整个系统的形状:
| 决策 | 选择 | 理由 |
|---|---|---|
| 表计适配 | 表档案配置化,地址/波特率/点表/周期全配置 | 现场表计品牌杂,换表不改代码 |
| 模块间通信 | 服务化 + FreeRTOS 队列,不直接调用 | 解耦,单服务崩溃不影响其他 |
| 可靠性优先级 | 完整性优先于实时性 | 冻结必达、失联必报 |
关键实现
表档案配置化
每块表一份档案,定长数组存 64 条上限,NVS 持久化:
typedef struct {
uint16_t meter_id;
uint8_t channel; /* 0/1/2 对应三路 RS485 */
uint8_t slave_addr;
uint32_t baudrate;
uint16_t period_s;
uint8_t priority;
uint16_t fail_cnt;
int64_t last_ok_ms;
point_t points[]; /* 寄存器地址+数量+点ID+缩放系数 */
} meter_archive_t;换表只改档案不改代码,这是现场实施效率的关键。
三通道错峰轮询
3 个通道各一个调度器,3 个 UART 物理并行;通道内按"下次执行时刻"排序(时间轮或小堆),到期才发。错峰的目的是避免 64 块表同一时刻齐抄把总线堵死。失败的表下次间隔 ×2 并封顶,失联表重试不挤占正常表的带宽。
寄存器区间合并
每块表有多个测点(电压/电流/功率/电量),零散读会产生大量小帧。同表连续寄存器一次事务读完:
点表按寄存器地址升序:0x0000, 0x0001, 0x0002, 0x0006
合并结果:读 0x0000 起 3 个 + 读 0x0006 起 1 个中间夹着不读的寄存器是主要边界情况:间隔小于阈值就一起吃进同一个读事务,大于阈值就拆开。大点表超过帧长上限(如 60 字节)时自动分批。
协议层抽象与 DL/T 645 预留
协议层用函数指针抽象,当前只实现 Modbus RTU:
typedef struct {
int (*encode)(const rd_task_t *t, uint8_t *buf, size_t *len);
int (*decode)(const uint8_t *rx, size_t *len, rd_result_t *out);
} proto_ops_t;园区表统一是支持 Modbus 的导轨表,国网抄表体系才用 DL/T 645。留一个注册位,将来接 645 只加一个实现,调度与上报都不用动。这是"预留但不实现"的克制,避免过度设计。
实时值库与日冻结
值库条目 = 点ID + 值 + 质量戳(新鲜/超时/失联)+ 时间戳,采用双缓冲:抄读任务写后备缓冲,周期性翻转,读侧无锁。上报服务从这里取数发实时遥测(QoS0);每日 0 点冻结当日水电气数据,形成考核用正式记录(QoS1)。
断网缓存与按水位补传
上报失败或断网时,冻结与告警数据落 TF 卡。存储不用文件系统数据库,按条追加:记录 = 记录头(序号 + 时间戳 + 类型)+ 数据 + CRC,环形滚动。链路恢复后严格按水位顺序补传,上报服务确认一条推进一档,平台侧按序号去重防重复。启动时扫描环形区,用 CRC 丢掉半条记录并修复水位。容量超限丢最旧并上报告警。
失联判定与直报
连续 N 次失败或 M 秒无成功即标失联,抛告警事件走 QoS1 且插队优先——链路再堵,失联告警也必须先出去。
踩坑与经验
- 波特率按通道固定。换表时动态切波特率极易出错,实施阶段统一约定更稳。
- 应答超时固定 100ms 起步,慢表在档案里单独放宽。
- 帧间静默遵守 3.5 字符间隔,配合 DE/RE 收发切换,否则会踩到上一条帧的尾巴。
- 广播校时无应答,成功与否只能靠回读抽查代表表时钟来验证。
- 现场联调遇到的真实问题集中在表地址冲突、波特率配错、总线没接终端电阻——硬件侧由实施同事处理,协议层要能留下排查记录自证无责。
- 掉电检测触发中断,在彻底断电前的几十 ms 窗口里落最后一批数据。
小结
EM-200 的核心不是"把数据抄上来",而是在表计品牌杂、链路不稳定、现场无人值守的约束下,让数据完整可达。表档案配置化解决适配,错峰轮询与区间合并解决总线带宽,环形存储与水位补传解决断网,分级 QoS 与失联直报解决时效。完整性优先于实时性,是这个项目所有取舍的出发点。