DLT645-2007电表采集实战:STM32单片机帧解析与RS485通信 1. 项目缘起与整体设计思路1.1 为什么我要啃DLT645-2007这块硬骨头前阵子接了个能耗监测的小活儿甲方现场有十几块2007版规约的电表要求把电压、电流、功率、电量这些数据每15秒轮询一次汇总到网关再上传。我原本以为用Modbus抄表那套经验直接套就行结果一上手就发现完全不是一回事——DLT645-2007是国网系电表专用的通讯规约帧结构、地址域、控制码、数据标识编码全都有自己的玩法跟Modbus RTU那种直来直去的寄存器读写完全是两个路子。说白了DLT645-2007就是电表和采集设备之间约定的一套“暗号”。电表只认这套暗号你用Modbus去问它它理都不理你。这套规约规定了帧怎么起头、怎么结尾、地址怎么编、数据怎么问、电表怎么答、校验怎么算。搞嵌入式采集的兄弟只要碰国网的电表这一关绕不过去。这篇文章我打算把整个实战过程完整拆一遍从帧结构逐字节解析到串口参数配置到读数据标识的编码规则再到单片机上的收发状态机怎么写最后把我踩过的坑和排查经验全倒出来。适合正在做电表采集的嵌入式工程师、做能耗监测的物联网开发者以及被毕业设计卡住的同学。看完你至少能做到拿到一块2007规约的电表知道怎么把电量读出来。1.2 整体方案选型为什么用单片机而不是现成模块市面上确实有现成的DLT645转Modbus网关模块几百块一个插上就能用。但我这个项目有几个特殊要求一是现场要同时挂十几块表每块表的地址不同轮询时序要自己控二是采集上来的数据要在本地做一次预处理和缓存断网时不能丢数据三是成本压得比较死网关模块乘以十几路预算扛不住。所以最终方案是自己用单片机做多路采集。主控我选的是STM32F103C8T6原因很直接串口资源够用3个USART价格便宜资料铺天盖地出问题好查。如果你手头只有51单片机也能做但多路串口切换会比较吃力后面我会提一下51方案的限制。串口收发芯片用的是常见的隔离型RS485模块因为电表那边是RS485接口现场电磁环境又比较脏不隔离容易烧片子。整个系统的数据流是这样的单片机通过RS485总线轮询各块电表按DLT645-2007的帧格式发请求电表回帧后单片机解析出数据存到本地缓冲区再通过另一路串口或无线模块上传。核心难点全在“发请求”和“解析回帧”这两步上下面逐层拆开讲。1.3 帧结构总览先建立整体认知再抠细节DLT645-2007的帧格式是变长的但结构很规整。一帧完整的报文由这几部分组成起始符、地址域、起始符、控制码、数据域长度、数据域、校验码、结束符。注意这里有两个起始符中间夹着地址域这是2007版跟97版一个很明显的区别很多人第一次看会懵。我用一个读电量的实际帧给你直观感受一下。假设表地址是000000000001读A相电压请求帧长这样FE FE FE FE 68 01 00 00 00 00 00 68 11 04 33 34 34 35 CS 16前面四个FE是唤醒字节因为电表平时可能处于休眠状态需要先发几个FE把它叫醒。然后是68起始符接着是6字节BCD码地址低位在前再来一个68然后是控制码11读数据数据域长度04数据域是数据标识33343435CS是校验和16是结束符。回帧的结构类似但控制码会变成91读数据应答数据域里带上具体数值。理解了这个骨架剩下的就是往每个字段里填规则。我建议你先把这一帧背下来后面所有调试都围绕它展开。2. 核心细节逐字段拆解与实操要点2.1 地址域BCD码和低位在前的坑地址域占6个字节用的是压缩BCD码也就是每个字节存两位十进制数。比如表号是123456789012那地址域就是12 34 56 78 90 12。但这里有个大坑传输顺序是低位在前。也就是说实际发出去的字节顺序是90 12 78 56 34 12这种倒过来的形式。我第一次调的时候就是栽在这里。表号明明抄对了发出去就是没回应。后来拿示波器抓波形逐字节比对才发现地址字节顺序反了。电表收到地址后是把它当成一个整体来比对的顺序错了它认为不是在叫它自然不理你。还有一个细节广播地址是AA AA AA AA AA AA用这个地址发请求总线上所有表都会响应但会冲突一般只在特殊场景用。单表操作老老实实用实际地址。地址的BCD编码转换在代码里要写个函数把字符串形式的表号转成6字节数组注意高低位和字节序两层翻转很容易绕晕。提示调试阶段建议先用广播地址确认总线和串口参数没问题再换成单表地址。如果广播有回应、单表没回应百分之百是地址编码或字节序的问题。2.2 控制码读懂电表在说什么控制码是1个字节它决定了这帧是干什么的。请求帧和应答帧的控制码不一样这是判断帧类型的关键。常用的几个值我列个表控制码方向含义0x11主站发读数据0x91电表回读数据应答正常0x91的最高位D71电表回从站异常应答0x14主站发写数据0x94电表回写数据应答0x08主站发广播校时判断应答是否正常不能只看控制码等于0x91还要看最高位。如果电表返回的控制码D7位是1说明它拒绝了这次请求数据域里第一个字节是错误码。错误码的含义也有讲究比如01是其他错误02是无请求数据04是密码错等。我遇到过返回02的情况查了半天发现是数据标识写错了电表里根本没有这个数据项。控制码的D6位表示是否有后续帧D5位表示是否从站应答这些位在复杂场景下会用到但常规读数据基本用不上。新手先把0x11和0x91这对搞熟就够了。2.3 数据标识读什么数据全靠它数据标识是4个字节决定了你要读电表里的哪个数据。这是整个规约里最需要查手册的部分因为标识编码有一套自己的规则。以A相电压为例标识是0x33343435拆开看33是电压类34是A相34是相电压35是具体项。这套编码是分层级的前两个字节定大类后两个字节定具体项。常用的数据标识我整理了一份速查表实际项目里这几个覆盖了八成需求数据项数据标识十六进制数据类型A相电压33 34 34 35BCD1位小数B相电压33 35 34 35BCD1位小数C相电压33 36 34 35BCD1位小数A相电流33 34 35 35BCD3位小数瞬时总有功功率33 33 34 33BCD4位小数正向有功总电量00 00 00 00BCD2位小数反向有功总电量00 01 00 00BCD2位小数注意数据标识在帧里也是低位在前传输的。比如A相电压标识33343435实际发出去的字节顺序是35 34 34 33。这个跟地址域一个道理都是小端序。我建议在代码里统一写个字节翻转函数所有多字节字段都过一遍省得每个地方单独处理。数据类型也要留意。电压是1位小数电流是3位小数电量是2位小数解析出来的BCD码要按对应的小数位数还原成实际值。比如电压返回的是2200实际是220.0V。这个小数点位置搞错了数据就全废了。2.4 校验码别小看这一个字节校验码是1个字节算法很简单从第一个起始符68开始到数据域最后一个字节为止所有字节累加取低8位。注意是第一个68开始算前面的四个FE不算最后的16也不算。这个边界一定要卡准多算一个少算一个都会校验失败。我写校验函数的时候习惯这样传入一个指向帧头的指针和帧长度从索引0开始累加到倒数第二个字节倒数第一个是16结束符。但要注意帧头指针应该指向第一个68而不是FE。如果你把FE也算进去校验值就错了。电表对校验是很严格的校验错了直接丢帧不回应。调试时如果发现发出去的帧石沉大海先检查校验。我一般会在代码里把发送帧的每个字节打印出来手动算一遍校验跟程序算的对比很快就能定位问题。3. 实操过程与核心环节实现3.1 串口参数配置一个都不能错DLT645-2007的串口参数是固定的波特率2400bps数据位8位停止位1位偶校验。注意是偶校验不是无校验。很多人用Modbus的习惯默认无校验这里必须改成偶校验否则电表不认。在STM32上配置偶校验除了设置USART的CR1寄存器里的PCE位和PS位还要注意数据位长度。因为校验位占了一位所以实际上要配置成9位数据8位数据1位校验在HAL库里体现为WordLength设置成UART_WORDLENGTH_9B。这个细节如果没注意到发出去的波形校验位就是错的。2400bps这个速率不高一帧十几个字节大概要几十毫秒。轮询十几块表的时候时序要算好别让总线冲突。我一般给每块表留200毫秒的响应窗口超时就跳过下一轮再试。这样即使某块表临时没响应也不会拖垮整个轮询周期。3.2 发送流程从组帧到发出发送一帧的完整流程我拆成这几步准备唤醒字节连续4个0xFE填入起始符0x68填入6字节地址BCD编码低位在前再填一个0x68填入控制码0x11填入数据域长度0x04填入4字节数据标识低位在前计算并填入校验码填入结束符0x16代码里我用一个发送缓冲区数组按顺序往里填最后统一通过串口发出去。发送前记得把RS485的收发方向引脚切到发送模式发完再切回接收模式。这个方向切换的时机很关键切早了数据没发完切晚了会丢掉电表的第一字节回应。我的做法是等串口的发送完成标志位置位后再切稳妥。void build_read_frame(uint8_t *buf, uint8_t *addr, uint32_t di) { uint8_t i, cs 0; for(i 0; i 4; i) buf[i] 0xFE; buf[4] 0x68; for(i 0; i 6; i) buf[5i] addr[5-i]; // 地址低位在前 buf[11] 0x68; buf[12] 0x11; buf[13] 0x04; buf[14] di 0xFF; buf[15] (di 8) 0xFF; buf[16] (di 16) 0xFF; buf[17] (di 24) 0xFF; for(i 4; i 18; i) cs buf[i]; buf[18] cs; buf[19] 0x16; }这段代码里地址和数据标识都做了字节翻转校验从索引4第一个68开始累加到索引17结束正好覆盖到数据域末尾。你可以直接拿去用改改地址和标识就行。3.3 接收解析状态机比阻塞等待靠谱接收这边我强烈建议用状态机别用阻塞式等待。因为电表响应时间不确定阻塞等待要么等太久浪费CPU要么等太短丢帧。状态机的思路是每收到一个字节就喂给状态机状态机根据当前状态判断这个字节合不合法合法就推进到下一状态不合法就复位重新找帧头。状态机的状态我定义了这几个等第一个FE、等第二个FE、等第三个FE、等第四个FE、等第一个68、收地址6字节、等第二个68、收控制码、收长度、收数据域、收校验、等16结束符。每个状态只做一件事逻辑清晰出问题也好定位。收到完整帧后先验校验校验过了再解析数据域。解析的时候根据控制码判断是正常应答还是异常应答正常的话把BCD码转成数值按小数位数还原。这里要注意数据域的长度是变长的读电压返回2字节读电量可能返回4字节解析函数要根据数据标识动态处理。3.4 数据还原BCD转数值的细节电表返回的数据是压缩BCD码每个字节两位十进制数。比如返回字节0x22 0x00表示2200。转成数值的代码很简单uint32_t bcd_to_u32(uint8_t *bcd, uint8_t len) { uint32_t val 0; for(uint8_t i 0; i len; i) { val val * 100 (bcd[i] 4) * 10 (bcd[i] 0x0F); } return val; }注意BCD码里如果出现A到F说明数据异常要丢弃。正常情况下每个半字节都是0到9。还原出整数后再根据数据项的小数位数除以10的幂。电压除以10电流除以1000电量除以100。这个小数位数一定要跟数据标识对应上我建议在代码里维护一张表把数据标识和小数位数关联起来解析时查表别硬编码。4. 常见问题与排查技巧实录4.1 发了没反应从这五个地方查电表不回应是最常见的问题我按排查优先级列一下排查项检查方法常见原因串口参数示波器看波形波特率、校验位不对地址编码打印发送字节字节序反了校验码手动算一遍对比累加范围错了RS485方向测方向引脚切换时机不对接线万用表测AB线A/B接反了我遇到最多的是校验码算错和地址字节序反了。校验码那个坑在于累加起点一定要从第一个68开始别把FE算进去。地址那个坑在于两层翻转字符串转BCD是一层字节序倒置是另一层两层都做对了才行。4.2 收到乱码多半是波特率或干扰如果收到数据但全是乱码先确认波特率是不是2400。有时候程序里配置的是9600但电表是2400收到的就是一堆垃圾。如果波特率没错那可能是RS485总线干扰检查一下终端电阻有没有接A/B线有没有屏蔽接地。现场电磁环境差的话加个磁环或者换屏蔽双绞线。还有一种情况是收到了自己的发送回显。这是因为RS485方向切换太慢发送的数据被自己接收到了。解决办法是发送完成后延时一小会儿再切接收或者干脆在接收状态机里把发送期间的数据丢弃。4.3 数据对不上小数点和符号位数据能读出来但数值不对八成是小位数搞错了。电压读出来2200你当成2200V就离谱了实际是220.0V。每个数据项的小数位数在规约手册里都有别凭感觉。另外功率类数据可能带符号位最高位是符号标志解析时要单独处理。电量类数据一般是累计值不带符号。4.4 多表轮询冲突时序和超时挂多块表的时候如果两块表同时响应就会冲突。解决办法是严格轮询发完一块表的请求后等它回应或者超时再发下一块。超时时间我设的是300毫秒实测大部分表在100毫秒内就回了。如果某块表连续几轮都超时可以标记为故障降低它的轮询频率别让它拖累整个总线。提示轮询间隔不要太短电表也需要处理时间。我试过50毫秒一轮结果丢帧率很高改成200毫秒后就稳了。4.5 51单片机方案的现实约束如果你用的是51单片机单路采集没问题但多路串口切换会比较麻烦。51一般只有一个硬件串口多路要靠模拟串口或者外部切换芯片。模拟串口在2400bps下勉强能用但时序精度要求高中断一多就容易丢帧。我的建议是51方案只做单表或者双表超过三块表还是上STM32或者类似的32位单片机省心得多。5. 调试工具与效率提升经验5.1 串口助手怎么用才高效调试DLT645串口助手是必备的。我习惯用能显示十六进制的助手发送和接收都开hex模式。发送区提前把组好的帧存成快捷发送项点一下就能发。接收区开自动换行和时间戳方便看响应间隔。如果助手支持校验和计算就更好了可以直接验证。我一般会先在电脑上用USB转RS485模块直接连电表把帧调通了再把代码移植到单片机。这样能把硬件问题和协议问题分开排查效率高很多。电脑上调通了单片机这边基本就是串口配置和方向控制的活儿。5.2 逻辑分析仪抓波形当串口助手都调不通的时候就得上逻辑分析仪了。抓RS485的A/B线波形看发送的字节流对不对波特率准不准校验位是什么电平。我那次地址字节序的问题就是靠逻辑分析仪抓出来的一眼就看出字节顺序反了。逻辑分析仪不用买太贵的几十块的那种抓2400bps绰绰有余。5.3 代码分层协议层和硬件层分开写代码的时候我把协议层和硬件层分开。协议层只负责组帧、解析帧、算校验不碰任何寄存器。硬件层负责串口收发、方向控制、定时器。这样协议层可以在电脑上单独测试用gcc编译跑单元测试不用每次都烧到板子上。这个习惯帮我省了大量时间尤其是协议逻辑复杂的时候。6. 写在最后的一点个人体会这个项目做下来我最大的感受是DLT645-2007看着复杂但它的复杂是“规则多”而不是“逻辑难”。帧结构、地址编码、数据标识、校验算法每一条都是死规则查手册就能搞定。真正花时间的是调试是那些手册上不会写的细节——字节序、校验范围、方向切换时机、小数点位置。我踩过的坑基本都在上面写了你照着做能少走不少弯路。如果让我给新手一个建议那就是先用电脑加USB转485把单表读通把帧的每个字节都搞明白再上单片机做多路。别一上来就焊板子写代码那样出了问题你都不知道是硬件还是协议。另外规约手册一定要备一份在电脑里数据标识那部分经常要查。我习惯把常用的数据标识整理成自己的速查表贴在显示器边上比翻PDF快多了。这套代码我现在已经用在好几个现场了2400bps偶校验STM32F103跑状态机十几块表轮询稳得很。你要是也在做类似的东西希望这篇能帮上忙。