基于单片机的智能电表DL/T 645抄表与多表轮询实践
2026/9/20 19:15:34 网站建设 项目流程

简介:这是一份面向电子信息、电气工程及嵌入式方向学生与开发者的智能电表抄表系统设计文档,适合用作课程设计、毕业设计或电表抄表项目入门参考。资源围绕STM32F103C8T6单片机与ST7540电力载波通信芯片展开,结合电力载波电路、电平转换电路及MAX232等外围器件,完整呈现从电路原理图绘制、电路板制作调试到单片机代码编写与FSK调制解调通信实现的软硬件流程,并整理出可靠性高、可扩展性强、成本低、计量准确等系统特点,以及居民住宅电量自动检测、收费与管理等应用场景。包内共1个docx文档,压缩包约1.57MB,内容含摘要、总体方案、元器件介绍、硬件电路设计等章节,可直接按目录检索阅读。已有84人学习,适合需要快速理解抄表系统架构、对照原理图与模块设计思路的读者参考。

1. 智能电表抄表要解决什么问题,谁在用这套方案

小区配电间里挂着两百多块单相电表,物业每月要出一份分户电量清单。派人挨家挨户按表键、抄数字,两天抄不完,抄完还得手工敲进 Excel,错一位就得整行重来。智能电表抄表要解决的就是这件事:让单片机定时把电表里的电量字读出来,经总线送到集中器或上位机,最后落成可查询、可对账的数据。落到工程上,它是一套「计量前端 + 单片机主控 + 通信链路 + 数据存储」的组合,并不神秘。适合看这套东西的人有三类:做电力终端和智能家居网关的嵌入式工程师,用 51 单片机或 32 单片机做课程设计和毕业设计的学生,以及准备把实验室玩具搬进配电箱的开发者。反直觉的一点是,抄不准的锅很少出在通信上,八成是计量系数没标定、掉电没保存、或者地址域少加了 33H。

2. 智能电表抄表终端的硬件选型与单片机最小系统

2.1 主控选型:STC 单片机、51 单片机和 STM32 各守哪块阵地

单表本地显示和简单上报,STC89C52RC 这类 51 单片机够用,资料多、烧录简单、成本压得下来。但它的 RAM 只有 512 字节,串口和定时器各两三个,轮询十几台表时收发缓冲很快吃紧,还会因为中断嵌套丢帧。真要做多表轮询,STM32F103 这类 32 单片机更合适:多路 USART、DMA、硬件 RTC、72MHz 主频,一帧一帧收发几乎不占 CPU。STC15/STC8 系列卡在中间,1T 内核、内部 EEPROM、多路 CCP,做小规模轮询性价比很高,很多蓝桥杯单片机板子用的就是这一类,拿来练手很顺。选型不用追高,先数清楚要挂几台表、要不要本地显示、要不要断网续传,答案基本就出来了。

主控关键资源擅长场景主要局限
STC89C52RC8KB Flash、512B RAM、1 路 UART、2 个定时器单表、本地显示RAM 小,多表缓冲吃紧
STC15/STC81T 内核、内部 EEPROM、多路 CCP单表到小规模轮询无 DMA,高波特率易丢字节
STM32F103多路 USART、DMA、RTC、72MHz多表轮询、协议栈成本与开发门槛略高

2.2 计量前端:脉冲计数、专用计量芯片还是计量 SoC

计量前端决定了抄回来的数准不准。最早的做法是电表输出电能脉冲,单片机用定时器/计数器数脉冲,一个脉冲代表固定电量,累计起来就是总电能。这条路成本最低,但拿不到电压、电流、功率因数,精度也受脉冲宽度和抖动影响,只适合教学演示。工业终端一般用专用计量芯片,比如 ADE7758 走 SPI,BL0937、HLW8032 走脉冲或 UART,芯片内部做了有功功率的积分和校准,精度能到 0.5~1 级。再往上是一体化计量 SoC,把计量和 MCU 集成在一颗芯片里,外围最干净,但算法和标定接口通常绑定厂商工具链,灵活度打折。

方案典型器件接口方式精度适用
脉冲计数光电脉冲表 + 计数器GPIO / 定时器低,仅累计电能教学、低成本
专用计量芯片ADE7758 / BL0937 / HLW8032SPI / UART / 脉冲0.5~1 级工业终端
计量 SoC单相计量 MCU内置1 级单表一体化

2.3 通信链路与存储介质怎么排

RS-485 是抄表总线的主力,差分传输、抗共模干扰、一条总线挂 32 台表不费劲;红外口留给现场手持抄表器;载波和微功率无线用于不方便布线的老楼改造。存储分两级:秒级数据先落内部 EEPROM 或 AT24C02,防止掉电丢数;日报表、月冻结数据按 CSV 追加到 TF 卡,拔卡就能进 Excel 对账。IO 不够用时,用 74HC165 扩展并行输入读按键或拨码,比重新画板省事。

2.4 上电自检与串口初始化的最小 C 代码

下面这段是 51 单片机在 11.0592MHz 晶振下把串口配成 9600bps、8 位数据、偶校验的初始化,同时把 485 方向脚置成接收态。

#include <reg52.h> #define RS485_DE P1_0 /* 485 收发切换:1 发送,0 接收 */ void uart_init(void) { SCON = 0xD0; /* 模式3:9位UART(8数据+1校验),允许接收 */ TMOD |= 0x20; /* 定时器1工作于模式2,8位自动重装 */ TH1 = 0xFD; /* 9600bps @ 11.0592MHz,SMOD=0 */ TL1 = 0xFD; PCON &= 0x7F; /* 确认 SMOD = 0,波特率不翻倍 */ TR1 = 1; /* 启动定时器1作为波特率发生器 */ ES = 1; /* 使能串口中断 */ EA = 1; RS485_DE = 0; /* 默认挂在接收态,避免占住总线 */ } void rs485_send(unsigned char *buf, unsigned char len) { unsigned char i; RS485_DE = 1; /* 先切发送,再写 SBUF */ for (i = 0; i < len; i++) { SBUF = buf[i]; while (!TI); /* 等这一字节移完 */ TI = 0; } RS485_DE = 0; /* 最后一个字节发完才切回接收 */ }

SCON = 0xD0里的 SM0/SM1 选模式 3,这一位 TB8 会被硬件当作第 9 位校验位输出,所以电表端必须也配偶校验,否则读回来全是乱码。TH1的计算方式是 256 − 11059200 ÷ (384 × 9600) = 253,换晶振或改波特率都得重算。rs485_send里最关键的是RS485_DE的切换时机:写 SBUF 之前拉高,最后一个字节的 TI 置位之后再拉低。少等这一个 TI,最后一个字节会被截断,表现就是「前几字节正常、校验永远不过」。

3. DL/T 645 抄表报文拆解与单片机端组帧解析

3.1 帧结构逐字段过一遍

国内单相电表普遍遵循 DL/T 645-2007,帧定长头尾、变长数据域,结构相当规整。

字段长度(字节)取值说明
起始符10x68帧头,前后各一个
地址域6BCD低字节在前,逐字节 +0x33
起始符10x68
控制码10x11 / 0x91 / 0xD1读请求 / 读应答 / 异常应答
数据长度1N数据域字节数
数据域N数据标识 + 数据,逐字节 +0x33
校验码1CS从第一个 0x68 到数据域末尾的算术和 mod 256
结束符10x16

要记住的只有一条:地址域、数据标识、数据本身全部做了加 0x33 处理,而控制码、长度、校验码不加。很多人调试半天读不出数,就是把数据域也当成了明文。

3.2 组一帧读正向有功总电能的命令

以 12 位表号000000000123、读数据标识00010000(正向有功总电能)为例,用 Python 把帧拼出来。

def addr_to_wire(addr12: str) -> bytes: """12 位十进制表号 -> 6 字节 BCD,低字节在前""" raw = bytes(int(addr12[i:i + 2], 16) for i in range(0, 12, 2)) return raw[::-1] def build_read_cmd(addr12: str, di4: bytes) -> bytes: """组读数据请求帧,di4 为 4 字节数据标识,低字节在前""" a = bytes((b + 0x33) & 0xFF for b in addr_to_wire(addr12)) d = bytes((b + 0x33) & 0xFF for b in di4) body = b'\x68' + a + b'\x68' + bytes([0x11, len(d)]) + d cs = sum(body) & 0xFF return body + bytes([cs, 0x16]) print(build_read_cmd("000000000123", bytes([0x00, 0x00, 0x01, 0x00])).hex(' ').upper()) # 68 34 33 33 33 33 33 68 11 04 33 33 34 33 E5 16

addr_to_wire里用int(x, 16)而不是int(x, 10),是因为 BCD 码的字节值本身就等于十进制数字对的十六进制写法,"23"要得到0x23而不是23。数据标识00010000是 4 字节、低字节在前,所以传进去是00 00 01 00,加 0x33 后变成33 33 34 33。校验码 E5 是对帧头到数据域末尾做算术和再取低 8 位得到的,这个算法比 CRC 简单,实现时不容易出错,但也意味着它没有纠错能力,只能查错。

3.3 解析应答帧:控制码 0x91、BCD 与小数位

电表正常应答的控制码是 0x91,数据域前 4 字节是数据标识(加了 0x33),后面才是数值。

def parse_read_resp(frame: bytes): if frame[0] != 0x68 or frame[7] != 0x68 or frame[-1] != 0x16: raise ValueError("帧定界符错误") ctrl = frame[8] ln = frame[9] if ctrl == 0xD1: raise IOError(f"从站异常应答,错误码 {frame[10]:02X}") if ctrl != 0x91: raise ValueError(f"控制码不是读应答: {ctrl:02X}") body = frame[:10 + ln] if (sum(body) & 0xFF) != frame[10 + ln]: raise ValueError("校验和错误") data = bytes((b - 0x33) & 0xFF for b in frame[10:10 + ln]) di = data[:4].hex().upper() digits = data[4:].hex().upper() # 8 位 BCD 数字 value = float(digits[:-2] + "." + digits[-2:]) return di, value resp = bytes.fromhex("68 34 33 33 33 33 33 68 91 08 " "33 33 34 33 33 33 45 33 47 16") print(parse_read_resp(resp)) # ('00010000', 12.0)

数值部分是 4 字节 BCD、固定两位小数,所以digits[:-2]是整数位、digits[-2:]是小数位。这个例子里数据字节减 0x33 后是00 00 12 00,拼成00001200,即 12.00 kWh。解析前一定要先验长度和校验和,现场最常见的现象就是半截帧被当成完整帧送进解析,结果数值离谱却查不出原因。收到 0xD1 时要单独处理:它代表电表明确拒绝了这个数据标识,通常是标识写错或该表不支持这一项。

3.4 备选链路:Modbus RTU 的 CRC16 怎么算

一部分采集器走 Modbus RTU 读电表的保持寄存器,校验方式换成 CRC16,多项式 0xA001,低字节先发。

def crc16_modbus(data: bytes) -> bytes: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return bytes([crc & 0xFF, crc >> 8]) # 低字节在前 print(crc16_modbus(bytes.fromhex("01 03 00 00 00 02")).hex(' ')) # 校验值随帧内容变化,低位字节先上总线

两种协议不要混用同一套收发时序:DL/T 645 靠帧长和结束符 0x16 定界,Modbus 则依赖 3.5 个字符的静默间隔。串口超时统一按最慢的那种配。

4. 单片机抄表主循环:多表轮询、485 收发与数据落盘

4.1 用定时器分时轮询,一张表一个时间片

多表抄表不能用阻塞式while死等,否则一台表掉线就把整个循环卡住。常见做法是把轮询拆成状态机,10ms 进一次,靠millis()计时器判超时。

#define METER_MAX 16 #define FRAME_MAX 64 #define RETRY_MAX 3 #define TIMEOUT_MS 300 typedef enum { ST_IDLE, ST_WAIT, ST_PARSE, ST_STORE } state_t; static state_t st = ST_IDLE; static uint8_t cur = 0, retry = 0; static uint32_t t_tx = 0; static uint8_t rx_buf[FRAME_MAX], rx_len = 0; static float last_value; void poll_tick(void) /* 每 10ms 调用一次 */ { switch (st) { case ST_IDLE: rx_len = 0; build_read_cmd(cur); /* 组帧写入 tx_buf */ rs485_send(tx_buf, tx_len); t_tx = millis(); st = ST_WAIT; break; case ST_WAIT: if (rx_len >= 12 && rx_buf[rx_len - 1] == 0x16) { st = ST_PARSE; /* 收到以 0x16 结尾的完整帧 */ } else if (millis() - t_tx > TIMEOUT_MS) { if (++retry < RETRY_MAX) { st = ST_IDLE; } /* 重发 */ else { retry = 0; cur = (cur + 1) % METER_MAX; st = ST_IDLE; } } break; case ST_PARSE: if (parse_frame(rx_buf, rx_len, &last_value) == 0) st = ST_STORE; else if (++retry < RETRY_MAX) st = ST_IDLE; else { retry = 0; cur = (cur + 1) % METER_MAX; st = ST_IDLE; } break; case ST_STORE: save_record(cur, last_value); retry = 0; cur = (cur + 1) % METER_MAX; st = ST_IDLE; break; } }

TIMEOUT_MS = 300是按 9600bps 估的:32 字节帧在线上约 33ms,电表内部处理再加几十毫秒,留 300ms 已经有近十倍余量。RETRY_MAX = 3是现场比较稳的经验值,再多会把总线占满。串口中断只负责往rx_buf里塞字节,主循环负责判帧,两者之间用rx_len做同步就够了,别在中断里解析。

4.2 数据落盘:EEPROM 循环缓冲与 TF 卡 CSV

秒级电量先写 EEPROM,做环形缓冲,写满一圈覆盖最旧的一页,避免频繁擦写同一个地址。AT24C02 每页 8 字节,跨页写会回卷覆盖,必须按页对齐。

/* AT24C02 页写:一页 8 字节,返回 0 表示成功 */ uint8_t eeprom_page_write(uint8_t page, const uint8_t *buf, uint8_t len) { if (len == 0 || len > 8) return 1; /* 超出页容量直接拒绝 */ i2c_start(); i2c_write_byte(0xA0); /* 设备地址 + 写方向 */ i2c_write_byte(page * 8); /* 页内起始字地址 */ for (uint8_t i = 0; i < len; i++) i2c_write_byte(buf[i]); i2c_stop(); delay_ms(6); /* 等内部写周期,典型 5ms */ return 0; }

需要按月出报表时,再挂一张 TF 卡用 FatFs 追加 CSV,拔卡就能在电脑上打开。

FRESULT log_append(const char *line) { FIL f; UINT bw; FRESULT r; r = f_open(&f, "0:/meter_log.csv", FA_OPEN_ALWAYS | FA_WRITE); if (r != FR_OK) return r; r = f_lseek(&f, f_size(&f)); /* 定位到文件尾实现追加 */ if (r == FR_OK) { r = f_write(&f, line, strlen(line), &bw); if (r == FR_OK) r = f_sync(&f); /* 立即落盘,防掉电丢行 */ } f_close(&f); return r; }

每次写完必须f_sync,否则数据还在 FatFs 缓存里,一断电整块丢失。CSV 行格式建议固定成「时间, 表号, 数据标识, 数值」,方便上位机按列解析。

4.3 超时与重试参数怎么定

参数典型值说明
波特率2400 / 9600 bps645-2007 默认 2400,现场多改为 9600
数据位/校验/停止位8 / E / 1偶校验,多数电表只认这一种
帧间间隔≥ 20 ms从站需要处理时间,连发会丢帧
单帧超时200~500 ms按帧长和波特率算,留 5~10 倍余量
重试次数3连续失败则跳过该表,下一轮再试
单表轮询周期0.4~1 s16 张表全轮一遍约 10~16s

参数之间是有耦合的:把超时调小、重试调大,看似提高了可靠性,实际会拖长全轮周期,反而让日报表生成变慢。先把帧间间隔和超时按最慢的那台表定死,再谈优化。

5. 智能电表抄表终端的校准与现场排错技巧

5.1 用电能表常数做一次现场校准

计量芯片出厂有默认增益,但互感器、采样电阻的公差会让读数偏移。最省事的校准办法是用电表常数:表面标 1200 imp/kWh,就接一个稳定负载让它走 1 kWh,理论应产生 1200 个脉冲。实际计数 N 个,修正系数就是 1200/N,写回计量芯片的增益寄存器。

/* 简化示意:按脉冲数修正在用增益,g_gain 为 16 位定点 */ uint32_t calib_gain(uint16_t rated_imp, uint16_t real_imp, uint32_t gain) { if (real_imp == 0) return gain; /* 没脉冲,保持原值 */ return (uint32_t)((uint64_t)gain * rated_imp / real_imp); }

负载要稳定,用纯阻性负载最好,别在电机频繁启停的支路上标。校准完把系数写进 EEPROM 保存,下次上电直接加载,别每次开机重算。

5.2 抄不到数时的排查顺序

出问题别急着改代码,按物理层往上查效率最高。

现象优先排查工具
所有表都无应答A/B 差分线接反、终端电阻缺失、DE 时序万用表、示波器
单台表无应答表号、波特率、校验位不匹配手持抄表器
偶发校验错波特率偏差、共模干扰、地线没共地逻辑分析仪
数值明显不对加 0x33 处理漏做、BCD 字节序、小数位上位机回放

第一步永远是自环测试:把 485 收发器的 A、B 短接,自己发一帧自己收,通了说明 MCU 侧和收发器没问题,问题在总线或从站。第二步用万用表量 A、B 之间的空闲差分电压,正常在 200mV 以上;第三步再拿手持抄表器直连单表,确认表和表号本身是好的。

5.3 用逻辑分析仪看 485 差分波形

最容易踩的坑是「收得到帧但最后一个字节总错」。把逻辑分析仪夹在 DE 脚和 RXD 上,看 DE 的下降沿是不是落在最后一个字节停止位之后。如果 DE 提前拉低,收发器会把这半个字节掐掉;如果拖得太晚,从站的应答前几个字节会被自己吃掉。另一个常见现象是波形上升沿明显变缓、幅值不足 1.5V,那是总线负载过重或缺少偏置电阻,先减挂载数量、再补 120Ω 终端电阻。抓到波形以后对着帧结构一个字节一个字节核,比盯着代码猜要快得多。

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

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

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

立即咨询