
简介面向电力计量、智能电表与数据采集相关软硬件开发者这份资源整合了电表数据采集系统的原理图与可参考源码可用于理解硬件电路设计、采集流程以及数据上报机制。包体共8个文件包含Altium Designer原理图schdoc、PCB工程文件prjpcb、Visio设计图vsd以及C语言程序源文件c等压缩包约499KB结构紧凑便于按图对照学习。已有182人下载学习。通过原理图可查看电压、电流采样电路、信号调理、模数转换及微控制器接口等设计细节源码部分有助于理解电量数据处理与通信交互逻辑也能为二次开发提供思路。对于从事智能电表、物联网终端或嵌入式数据采集开发的工程技术人员这套资料可作为快速上手的参考设计模板也可作为排查相关问题的辅助材料帮助缩短项目前期的方案验证时间。1. 拿到 cj.zip先从电表原理图看懂这套采集链路在工控现场摸爬滚打的人大概率遇到过这种场景供应商或者甲方抛过来一个压缩包文件名就叫 cj.zip里面散着几页原理图、一份电表通讯协议说明运气好还有一段采集示例代码。整个包没有目录没有 CHANGELOG有的只是“电表数据采集”几个字。这个 cj.zip 指的就是电表采集工程包绝大多数情况下它对应的是基于 RS485 总线的多功能电表数据采集方案电表上传电压、电流、功率、电量等数据采集终端通过 Modbus RTU 协议轮询多块电表再上传到上位机。把这个链路的原理图识读和采集代码调通是物联网、能效管理、注塑机以及配电监控项目里最常见的第一步。这篇就按一线工程师的做法把 cj.zip 当黑盒来处理先拆包看原理图再从原理图里反推 MCU 选型与接线用 STM32F103C8T6 落一套最小采集硬件把 Modbus RTU 的请求帧、CRC 校验和轮询参数讲透最后用模拟从站验证整套流程。凡是水文介绍里说不细的选型理由、差分信号时序、超时窗口和终端电阻位置都会在这里直接给出可抄的结论和参数。2. 电表原理图识读从 cj.zip 的 PDF 里反推三条信号通路拿到工程包先别急着写代码第一步是把原理图读透。电表数据采集的原理图往往不大但抽掉任何一条信号通路现场问题都会被伪装成“偶尔读不到数据”或“数据跳变”排查成本远高于前期看图成本。2.1 先解包再看文件cj.zip 里的四类常见文件我一般会先把压缩包解压到一个固定目录命名避免中文和空格否则后面 Keil 和 CAD 工程都可能出编码问题。常见做法是mkdir -p cj_electric_meter unzip cj.zip -d cj_electric_meter ls -la cj_electric_meter解压后按文件类型粗分为四类原理图与 PCB 文件、Modbus 通讯协议文档、示例源码、以及 BOM 表。优先读协议文档它决定你用什么功能码读哪些寄存器其次读原理图它决定你的 MCU 引脚和收发器接法BOM 是排查 120Ω 电阻和光耦型号时用来对照的。很多工程师直接跳去看代码最后在 RS485 方向控制和 A/B 线上反复返工就是因为没把原理图里的收发器电路先确认清楚。2.2 在电表原理图上先找 RS485 收发器和 A/B 端子打开电表原理图第一件事不是看 MCU而是在通讯接口区域找 RS485 收发器。电表内部除了计量芯片外必然有一颗 485 收发器型号常见的有 MAX485、SP3485、ISO3082 等。找到它之后顺着收发器的 RO、DI、RE、DE 四个脚往回追网络标号RO 一定连 MCU 的 UART RXDI 连 TXRE 和 DE 通常并在一起由 MCU 一个 GPIO 控制用来切换收/发方向。CJ 系列电表的采集原理图还有一个关键点就是它上面是否带光耦隔离。如果 RO 和 DI 之间串了高速光耦如 6N137说明采集终端是隔离型设计MCU 地GND1与 485 地GND2是分开的。此时你在原理图上看到 A、B 两线之外还有一个独立管脚标着 GND_485这条信号地必须接到电表的通讯地上不能省。2.3 隔离与非隔离选型的差异和适用场景隔离与非隔离不是成本问题是可靠性问题。非隔离方案最典型的隐患是地电位差当两块电表距离超过 100 米或现场有大功率变频器启动时A、B 线上叠加强大共模干扰轻则烧 MAX485重则顺着串口把 MCU 一并带走。对比项非隔离MAX485 直连隔离光耦 DCDC 隔离电源成本1~2 元级别5~10 元级别耐共模电压低地电位差超过 ±7V 可能损坏可耐 1000V 以上通讯距离1200 米以内理论值实际 300~500 米稳妥同波特率下距离更稳典型场景实验室、短距离柜内走线跨车间、多电表分散布置原理图复杂度简单一个收发器加两个偏置电阻需要隔离 DCDC 和光耦如果 cj.zip 里原理图的 RS485 部分只有收发器、三个电阻、一个 TVS 管那属于非隔离如果还有 B0505S 这类隔离电源和光耦就是隔离型。后续程序上两者的 Modbus 帧内容完全一致差别只在硬件布局。我的建议是哪怕原理图上画的是非隔离只要项目允许第一版就打隔离现场问题会少一半。3. 用 STM32F103C8T6 最小系统板搭电表数据采集的硬件原理图确认完接下来就是把采集终端的最小链路搭出来。很多开发都习惯直接拿现成最小系统板飞线这没有错但前提是你得知道原理图里哪些引脚已经被占用。STM32F103C8T6 这颗芯片在 cj.zip 这类电表采集工程里出场频率极高72MHz 主频足够做 32 块表的轮询调度自带 3 个 USART内置 20KB RAM跑 Modbus RTU 从站响应和主站轮询都绰绰有余。它本身的 STM32F103C8T6 最小系统板原理图网上到处都有重点是这一次我们只需要引出 USART1 和一块 RS485 电平转换。3.1 最小接线表STM32 与 MAX485 的引脚映射手头没有定制板时用一张最小系统板加 MAX485 模块按下面的表飞线就能完成数据采集终端的硬件原型。STM32F103C8T6 引脚MAX485 模块引脚作用PA9USART1_TXDI发送数据PA10USART1_RXRO接收数据PA8普通 GPIO 推挽输出RE DE方向控制高电平发送低电平接收3.3V 或 5V按模块要求VCC供模块电源GNDGND共地隔离方案则接隔离侧参考地不接A、B接 RS485 总线差分对接线时注意一个细节很多 MAX485 模块把 RE 和 DE 两个引脚在 PCB 上已经用跳线连好飞线只需要接一个 GPIO 就能同时控制收发方向。如果模块上没有合并你要自己在模块外部把 RE 和 DE 短接后再接 PA8否则可能出现收发冲突。3.2 用 HAL 库驱动 USART1 发送一帧 Modbus RTU 请求接线完成后先不跑完整协议栈用最直接的方式验证硬件链路MCU 发送 8 个字节的请求帧用串口助手看 485 总线上是否有数据。发送逻辑基于 HAL 库的阻塞式发送函数方向切换放在发送前后#include stm32f1xx_hal.h UART_HandleTypeDef huart1; void RS485_SendBytes(uint8_t *data, uint16_t len) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); // 拉高 DE使能发送 HAL_UART_Transmit(huart1, data, len, 100); // 阻塞发送超时 100ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); // 拉低 RE切回接收 }参数说明PA8 在发送前置为高电平MAX485 才把差分驱动器打开HAL_UART_Transmit 的最后一个参数是超时时间单位毫秒电表采集场景下 100ms 远大于 8 个字节在 9600 波特率下所需的约 10ms不会提前返回发送完立刻恢复为接收状态是为了准备接收电表的响应帧。这套代码的毛病是阻塞等待在轮询 32 块表时主循环会被拖住但作为验证链路已经足够后面可换成中断或 DMA 版本。3.3 串口参数不是随便选的9600 8N1 在通讯协议文档里的依据电表数据采集 90% 的场景跑的是 9600 波特率、8 个数据位、无校验位、1 个停止位也就是“9600 8N1”。为什么是这个组合因为多数电表出厂默认就是 9600 8N1而且 9600 波特率下的差分信号沿变化较缓对线缆分布电容和终端电阻失配容忍度最高。Modbus 协议本身支持从 1200 到 115200 的波特率但通讯距离越长波特率越高越容易出 CRC 错误。即便协议文档标明支持多种波特率我建议前期联调也用默认值确认所有寄存器读取正常后再考虑统一的更高波特率。不要在不同电表之间混用波特率一个 485 总线上只能使用一组串口参数Modbus RTU 没有按地址区分波特率的机制。4. 电表数据采集调试Modbus RTU 请求帧、CRC 校验与现场报错对照硬件通了下一步就是按 cj.zip 里的通讯协议文档组织请求帧。这里最容易犯的错误是拿 03 功能码读遍所有寄存器实际上电表协议文档里会区分 03保持寄存器和 04输入寄存器电压、电流、功率这类实时数据通常是输入寄存器用 04 读只有需要写电表参数时才用 06 或 16 功能码。4.1 一个完整的读电压请求帧拆解假设文档给的电压寄存器地址是 0x0000读取一块地址为 1 的电表的两个连续寄存器电压、电流请求帧由 8 个字节组成字节序数值含义10x01从站地址范围 1~24720x04功能码读输入寄存器3~40x00 0x00起始寄存器地址高字节在前5~60x00 0x02寄存器数量2 个7~80x71 0xCBCRC16-Modbus 低字节在前把这 8 个字节用串口发送后电表如果正常应答会返回类似 01 04 04 09 C4 02 1E 的帧地址、功能码、字节数4 字节、两个寄存器共 4 字节有效数据、2 字节 CRC。有效数据的解析方式要注意字节序多数电表是大端模式也就是高字节在前09 C4 表示电压 2500除以文档标注的变比 10得到 250.0V。4.2 CRC16-Modbus 的计算与 3.5 字符空闲间隔CRC 错是整个电表数据采集链路里排名第一的报错原因。写代码时不要自己造轮子CRC16-Modbus 的标准计算过程是初值 0xFFFF多项式 0xA001低字节在前。下面这个查表法版本比逐位计算快得多uint16_t ModbusCRC16(uint8_t *buffer, uint16_t length) { uint16_t crc 0xFFFF; for (uint16_t i 0; i length; i) { crc ^ buffer[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }逻辑说明每一个字节都要和 crc 寄存器异或 8 次异或结果最低位为 1 时执行右移和多项式异或否则只右移。调用完成后函数返回的 crc 已经是低字节在前正好可以直接填入请求帧的第 7、8 字节。除了 CRCModbus RTU 还强制要求帧与帧之间的间隔大于 3.5 个字符时间。9600 波特率下一个字符含起始位、8 数据位、停止位共 11 位3.5 个字符时间约为 3.5 × 11 / 9600 ≈ 4.01ms。这也是为什么很多采集程序里主站发送完请求后至少要等 10ms 再切换为接收状态——UART 本身发送完最后一个字节移位寄存器还需要一点时间把最后的停止位推出去如果急着切方向最后一个字节会被截断电表自然不回。4.3 原理图上的三个坑与排查对照表即使代码完全按文档写的现场还是可能读不上来。结合电表原理图和 RS485 物理层特性按错误现象分三类排查现象特征直接原因检查点总线上完全抓不到发出去的帧DE/RE 方向控制接反或悬空用万用表量 PA8 电平发送期间应为 3.3VA、B 反接设备不回差分信号极性接反对调 485 A/B 两根线或用示波器看波形是否 180° 反相有请求帧但无响应帧120Ω 终端电阻缺失或位置错误确认总线两端各有一个 120Ω 电阻而不是集中在一端20 个节点以内的短距离柜内通讯不接 120Ω 终端电阻有时也能工作但波形边沿会过冲波特率一旦提到 19200就会开始随机掉帧。原理图上如果画了 R120 的封装但 BOM 里空着大概率就是省料了。这种情况下与其飞线补电阻不如把波特率降回 9600 并缩短总线长度。5. 用模拟从站验证一主多从轮询参数超时与抖动窗口的一个可复现方案最后一环是验证不要用真实电表做开发期的压力测试。拿真实表调一次两次可以接受但如果你要测试 32 块表的轮询逻辑实物接线费时且容易烧设备。我习惯的做法是先在 PC 上用 Python 起一个 Modbus RTU 从站模拟器把电表的关键寄存器和超时行为模拟出来等代码在假从站上跑稳定再带电表联调。5.1 一个用 pyserial 写的最小从站模拟程序这个程序监听一个串口收到请求后解析功能码、寄存器地址然后按预设数据回电表格式帧。开发时我用虚拟串口把 COM3 和 COM4 对接这个脚本监听 COM3STM32 采集终端通过 USB 转 485 连到 COM4或直接用 USB 转串口。安装依赖用pip install pyserial脚本如下import serial import struct PORT COM3 BAUD 9600 # 模拟电表地址 1 的电压寄存器值 250.0V乘以变比 10 存储 REGISTERS {0x0000: 2500, 0x0001: 1002} CRC_POLY 0xA001 def crc16_modbus(data): crc 0xFFFF for b in data: crc ^ b for _ in range(8): if crc 0x0001: crc (crc 1) ^ CRC_POLY else: crc 1 return crc.to_bytes(2, byteorderlittle) def main(): ser serial.Serial(PORT, BAUD, timeout0.2) while True: frame ser.read(8) # 主站请求固定为 8 字节 if len(frame) ! 8: continue addr, func, start_hi, start_lo, cnt_hi, cnt_lo frame[:6] start (start_hi 8) | start_lo cnt (cnt_hi 8) | cnt_lo if addr 0x01 and func 0x04 and start in REGISTERS: values [REGISTERS[start i] for i in range(cnt)] payload bytes([addr, func, cnt * 2]) for v in values: payload v.to_bytes(2, byteorderbig) ser.write(payload crc16_modbus(payload)) # 收到非 04 功能码不回用于模拟异常 else: pass if __name__ __main__: main()说明这段脚本假设请求帧里寄存器地址连续且都落在预设字典内完整的从站响应还需要处理异常码 0x02非法数据地址生产级模拟器建议直接用 pymodbus 写但在现场的应急验证中这个最小脚本足够判断主站的轮询时序是否正常。脚本回帧的 CRC 用同一个标准算法计算主站收到后若报 CRC 错误问题不在电表而在主站的接线或 UART 配置。5.2 一主多从轮询的三个关键参数计算主站轮询多块表时真正决定稳定性的参数不是请求内容而是超时时间、帧间间隔和重试策略。参数推荐值计算与依据响应超时200ms电表内部最长处理时间约 50ms取 4 倍以上余量轮询间隔50ms必须大于 4.01ms 的 3.5 字符时间给总线留出净空连续失败重试2 次超过 2 次标记该表离线转下一块避免卡死循环超时这块有些协议栈把 timeout 和 inter_char_timeout 混成同一个参数我一般在串口接收中断里只按字节间隔判断帧结束超过 4ms 没收到新字节就认为一帧结束响应超时放在主站状态机里单独计时收到任意合法从站帧后立即清空计时器。5.3 验证丢帧的一个偏方看两块表切换时的总线空闲窗口所有参数设完后用模拟从站验证一主多从最值得观察的是两块表切换的瞬间。如果第一块表响应后 1ms 内主站就发了第二块表请求而模拟从站的收发切换慢了半拍总线就会撞帧。验证方法是在脚本的ser.read前加一个时间戳打印对比两个请求之间的时间差是否稳定大于 3.5 字符时间。还可以用示波器 A/B 通道同时抓差分波形和 PA8 的方向控制引脚正常情况下方向脚先出现一个高电平脉冲约 2ms 后差分总线出现数据。如果方向高电平出现后总线仍长时间空闲说明主站发送函数里串口等待和方向切换之间有阻塞。本文还有配套的精品资源点击获取