RS485电表数据采集:原理图识读与STM32 Modbus RTU实现
2026/9/16 18:07:25 网站建设 项目流程

简介:面向电力计量、智能电表与数据采集相关软硬件开发者,这份资源整合了电表数据采集系统的原理图与可参考源码,可用于理解硬件电路设计、采集流程以及数据上报机制。包体共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 RX,DI 连 TX,RE 和 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 模块引脚作用
PA9(USART1_TX)DI发送数据
PA10(USART1_RX)RO接收数据
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~247
20x04功能码,读输入寄存器
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 字节。

除了 CRC,Modbus RTU 还强制要求帧与帧之间的间隔大于 3.5 个字符时间。9600 波特率下,一个字符含起始位、8 数据位、停止位共 11 位,3.5 个字符时间约为 3.5 × 11 / 9600 ≈ 4.01ms。这也是为什么很多采集程序里主站发送完请求后至少要等 10ms 再切换为接收状态——UART 本身发送完最后一个字节,移位寄存器还需要一点时间把最后的停止位推出去,如果急着切方向,最后一个字节会被截断,电表自然不回。

4.3 原理图上的三个坑与排查对照表

即使代码完全按文档写的,现场还是可能读不上来。结合电表原理图和 RS485 物理层特性,按错误现象分三类排查:

现象特征直接原因检查点
总线上完全抓不到发出去的帧DE/RE 方向控制接反或悬空用万用表量 PA8 电平,发送期间应为 3.3V
A、B 反接,设备不回差分信号极性接反对调 485 A/B 两根线,或用示波器看波形是否 180° 反相
有请求帧但无响应帧120Ω 终端电阻缺失或位置错误确认总线两端各有一个 120Ω 电阻,而不是集中在一端

20 个节点以内的短距离柜内通讯,不接 120Ω 终端电阻有时也能工作,但波形边沿会过冲,波特率一旦提到 19200,就会开始随机掉帧。原理图上如果画了 R120 的封装但 BOM 里空着,大概率就是省料了。这种情况下,与其飞线补电阻,不如把波特率降回 9600 并缩短总线长度。

5. 用模拟从站验证一主多从轮询参数:超时与抖动窗口的一个可复现方案

最后一环是验证,不要用真实电表做开发期的压力测试。拿真实表调一次两次可以接受,但如果你要测试 32 块表的轮询逻辑,实物接线费时且容易烧设备。我习惯的做法是先在 PC 上用 Python 起一个 Modbus RTU 从站模拟器,把电表的关键寄存器和超时行为模拟出来,等代码在假从站上跑稳定,再带电表联调。

5.1 一个用 pyserial 写的最小从站模拟程序

这个程序监听一个串口,收到请求后解析功能码、寄存器地址,然后按预设数据回电表格式帧。开发时我用虚拟串口把 COM3 和 COM4 对接:这个脚本监听 COM3,STM32 采集终端通过 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, byteorder='little') def main(): ser = serial.Serial(PORT, BAUD, timeout=0.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, byteorder='big') 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 后差分总线出现数据。如果方向高电平出现后总线仍长时间空闲,说明主站发送函数里串口等待和方向切换之间有阻塞。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询