ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DL/T645-2007电表通信实战:从RS485接线到三相电压读取

DL/T645-2007电表通信实战:从RS485接线到三相电压读取 去年做能耗管理平台的时候客户要求直接采集一块国网规约电表的电压数据。我对着DL/T645-2007的协议文档照着示例拼命令结果串口调试助手那边一片死寂一条应答都收不到。后来折腾了大半天才发现问题不在协议文档上而是几个细节我根本没注意地址域要倒序、数据标识返回有两种格式、校验和从第一个68就开始算。这些在标准里写得很细可是第一次碰这个协议的人真的很容易漏。这篇文章就把我调通DL/T645-2007的全过程拆开讲重点放在电表地址解析和三相电压读取这两个高频需求上从链路参数一路说到正式的解析代码。做嵌入式、物联网数据采集、电力计量相关方向的朋友看完应该能少走不少弯路。1. 先别急着写代码链路参数和调试环境里的隐性要求很多新人拿到协议文档第一件事就是按示例发报文但发送之前有几件事没确认后面的所有排查都像是在盲人摸象。尤其DL/T645-2007这套协议跑在RS485半双工总线上硬件链路和串口参数不过关报文写得再对也不会有人理你。1.1 RS485接线A/B反接是最容易犯的低级错误电表侧一般是一个两芯或三芯的RS485端子标着A、B部分表还有PE接地端子。电脑这边用USB转485模块连接。接线原则特别简单A对A、B对B不能像RS232那样交叉。但我实测下来A/B反接的概率比我以为的高得多。尤其是电表端子排上A和B的丝印不够清楚的时候用手头现有的双绞线随便一夹十有八九就反了。反接的直接表现是发任何命令都没有响应因为485是差分信号极性反了接收端解析出来的全是乱码或电平错误。另外两个细节短距离十几米以内调试不接终端电阻一般也能通。但现场走线超过几十米或者总线上挂了很多块表最好在总线两端各接一个120欧姆终端电阻否则反射信号会把报文打花。如果现场电表和控制设备之间距离远、共地条件差优先用带隔离的USB转485模块。隔离模块能切断地环路避免共模电压把某个设备的485芯片烧掉。我见过不止一次因为地电位差导致通信时好时坏的情况换成隔离模块后问题立刻消失。1.2 串口参数2400-E-8-1不是可选项是电表默认项DL/T645-2007在物理层默认的串口参数是波特率2400bps、偶校验Even、8位数据位、1位停止位也就是常说的2400-E-8-1。这里的坑在于很多通用串口工具的默认波特率是9600如果你没改就直接发帧电表那边收到的全是时序错乱的bit流自然不会有正常应答。我建议调试的第一步就是把串口工具参数明确设成2400、偶校验、8数据位、1停止位并用十六进制显示模式。还有一点部分新型电表支持自适应波特率但它的自适应逻辑通常是上电后等待主机发送一帧特定格式的数据来“试探”。如果你一上来就按9600发表可能还在等待识别。总之先老老实实按2400-E-8-1来能通之后再研究自适应。调试工具软件本身没有特殊要求常见的串口助手、MobaXterm、minicom都可以关键是十六进制收发。1.3 FE前导符的作用唤醒休眠中的电表DL/T645-2007帧本身从68开始但在实际总线上电表为了低功耗经常在一段时间没有通信后进入“休眠”状态。这时直接发68开头的完整帧电表可能根本没在听。标准允许发送方在帧前面加前导符也就是连续几个FE典型是2到4个用来唤醒接收端。我自己的习惯是每次读数据前先发4个FE再接完整的请求帧。前导符本身不是帧的一部分不计入校验和。这个细节在贴代码和调工具时特别容易踩因为很多网上贴的报文不带FE照抄之后在模拟器上能通到现场却不行。不过也要提醒一句如果表一直保持活跃FE前导是可选的。但既然加上没有任何副作用我建议始终加上。2. 电表地址解析6字节BCD码为什么总让人栽跟头DL/T645-2007的每一帧都要带上目标电表的地址地址域是6个字节位于第一个68起始符和第二个68起始符之间。对这个地址域的解析是新手出错率最高的地方。2.1 地址在帧里的真实排列低位字节在前先看帧结构字段长度取值说明前导符0字节或若干通常为FE FE FE FE帧起始符1字节68H地址域6字节A0~A5BCD码表示低字节在前帧起始符1字节68H控制码1字节如11H读数据、91H正常应答、D1H异常应答数据域长度1字节L表示数据域字节数数据域L字节含数据标识或具体数据校验和1字节CS从第一个68到CS之前所有字节累加忽略进位取低8位结束符1字节16H地址域本身是BCD码每个字节代表两位十进制数字所以6个字节总共能表示12位十进制数。难点在于传输顺序A0是最低位A5是最高位。也就是说电表地址在帧里是反着放的。2.2 从表号到地址域一步步换算假设现场电表的通信地址是200612345678一共12位数字。按两位一组拆成BCD码20 06 12 34 56 78。送到帧里时要反向排列所以地址域的6个字节是78 56 34 12 06 20。这个换算看起来简单但实操里很容易错。最典型的错误是表号是8位数字比如12345678有人会直接把8位数字按BCD拆成12 34 56 78然后倒序变成78 56 34 12填进地址域的后4个字节。这样本地看好像没问题但协议规定地址域必须是6字节12位前两个字节要补零补零的位置也有讲究。正确的做法是先把8位表号补成12位001234567890不对12345678补成12位应该是0012345678等一下8位数字补成6个BCD字节是在高位补0也就是00 12 34 56 78其实是这样的12位十进制数应该是0012345678不对12345678是8位补成12位需要4个前导0即000012345678。按两位一组是00 00 12 34 56 78倒序后是78 56 34 12 00 00。我见过有人把8位表号按12 34 56 78 00 00来填充地址域再倒序成00 00 78 56 34 12结果地址就错位了。所以一定要注意补0补在高位不是低位。2.3 广播地址与全零地址的区分地址域为AA AA AA AA AA AA时表示广播地址用于广播校时、广播冻结这类一对多的命令。广播地址不能用来读电表数据因为所有表都会响应的话总线就乱套了所以正常的广播读数据请求会被表忽略。还有一个坑地址域为00 00 00 00 00 00并不是广播地址它表示一块“地址全0”的具体表。有些平台为了省事把所有表的地址域都填成全0结果所有表都会认为主机在喊自己响应帧的地址域也全是0一旦总线上挂了两块以上的表冲突就不可避免。所以现场务必确保每块表的通信地址唯一并且不要用全0。2.4 表号含字母的情况标准地址域用BCD码表示十进制数字但实际有些电表的资产编号带字母。遇到这种情况先看电表通信地址是否可以用按键或厂家工具改成纯数字如果不能就要翻一下对应厂家的规约补充说明看看它这个字段是当ASCII用还是当十六进制数用。不同厂家的处理不一定一致不要拿标准BCD去硬套。3. 请求帧逐字节拆解以读A相电压为例地址搞清楚之后就可以拼一帧完整的读数据请求了。这里用读A相电压做例子把每个字节的作用和长度域、校验和怎么算一起讲透。3.1 一个完整读数据请求的示例假设表地址是12345678前面已经推出来地址域是78 56 34 12 00 00要读A相电压数据标识是02 01 01 00。请求帧如下FE FE FE FE 68 78 56 34 12 00 00 68 11 04 02 01 01 00 D9 16逐个字段来看FE FE FE FE前导符用于唤醒。68帧起始符。78 56 34 12 00 00目标电表地址BCD倒序。68第二个帧起始符。11控制码表示“读数据”请求。04数据域长度。注意这里不是整个帧减掉头尾的长度而是数据域本身的长度。这次的数据域是4字节的数据标识所以L04。02 01 01 00数据标识。02 01表示变量类中的电压数据。01相别01是A相、02是B相、03是C相。00扩展位一般置0。D9校验和。16结束符。3.2 校验和CS的计算不是“从地址到数据”而是从第一个68开始我看过不少人写的计算函数是从地址域开始算甚至把FE前导符也累加进去这两种都会导致校验错。正确算法是从帧起始符68开始一直到校验和之前的一个字节也就是完整包含68 78 56 34 12 00 00 68 11 04 02 01 01 00逐字节累加超过0xFF就丢弃进位只保留低8位。上面的累加结果我用Python验证一下data bytes.fromhex(68 78 56 34 12 00 00 68 11 04 02 01 01 00) cs sum(data) 0xFF print(fCS 0x{cs:02X})输出CS 0xD9和示例里的D9一致。写代码时可以直接用这种“先把帧拼完整、再对前面所有字节求和取低8位”的方式避免漏算第二个68或者把结束符16也算进去。3.3 收到响应时的帧结构请求发出去之后电表正常情况下会返回一帧控制码为91正常应答。读A相电压的应答帧长这样68 78 56 34 12 00 00 68 91 06 02 01 01 00 11 05 95 16注意这里数据域长度变成了06也就是4字节数据标识加2字节电压数据。也就是说单相电压读回来时数据域里没有相别标志直接就是数据。再往后看两字节11 05这是电压值小端序实际值是0x0511换算成十进制是1297分辨率是0.1V所以A相电压是129.7V。这个“单相直接带数据、组合量带标志块”的差异就是下一章要说的重点。4. 三相电压读取组合数据块的返回结构比单相复杂如果一块表要显示三相电压最直观的做法是发三次单相请求分别读A相、B相、C相电压。这样确实能工作但占用总线时间长轮询周期也慢。合理做法是用组合数据标识02 01 00 00一次把三相电压全读回来。4.1 组合标识请求帧请求帧如下FE FE FE FE 68 78 56 34 12 00 00 68 11 04 02 01 00 00 D8 16数据标识02 01 00 00中02 01还是电压变量00表示组合量即一次返回三相。L依旧等于04因为数据域就是4字节数据标识。4.2 返回报文逐字节解析电表返回的报文可能是这样具体数值不代表所有表都这样68 78 56 34 12 00 00 68 91 10 02 01 00 00 01 02 11 05 02 02 10 05 03 02 0F 05 D3 16先看数据域长度10也就是16字节。从02 01 00 00开始数据域结构为字节偏移内容含义0~302 01 00 00数据标识原样返回401数据块标志A相502数据块长度2字节6~711 05A相电压小端0x0511129.7V802数据块标志B相902数据块长度2字节10~1110 05B相电压小端0x0510129.6V1203数据块标志C相1302数据块长度2字节14~150F 05C相电压小端0x050F129.5V数据域总长度 4字节数据标识 3个数据块 × 4字节 16字节对应L0x10完全对得上。校验和CS用同样的方式验证前面那串报文累加低8位结果是0xD3。4.3 和单相返回格式的差异千万不能混用解析刚才说过单相读取的应答数据域是02 01 01 00 11 05也就是数据标识后面直接是2字节数据没有数据块标志和长度。而组合读取的应答数据域是02 01 00 00 01 02 11 05 02 02 10 05 03 02 0F 05每个相别前面都带了一个标志字节和长度字节。我见过有人直接把组合返回的数据从第4字节开始按2字节一截去解析电压结果第一相读出来是01 02即0x0201换算成51.3V完全错误。所以解析之前必须先判断如果数据标识第3字节是01、02、03说明是单相直接取数据如果是00说明是组合要先读“数据块标志长度”再取数据。4.4 电压值的换算和越界判断DL/T645-2007里瞬时电压数据统一用2字节有符号或无符号整型表示单位是0.1V。读取时要先按小端序拼回整数def parse_voltage(lo, hi): raw lo | (hi 8) # 如果按有符号处理超过0x7FFF表示负值但交流电压一般不会出现负值 return raw * 0.1比如11 05拼成0x0511等于1297乘以0.1就是129.7V。这里有两个注意点小端序不要拼反。拼成0x1105就变成435.7V了明显不合理。数据块返回时如果某相无效有的电表会用FF FF填充。此时按0.1V换算会得到6553.5V这种离谱值。解析时要判断原始值是不是0xFFFF如果是就标记该相无效不要硬参与计算。实际调试时最好先用串口工具手动发一次组合读取命令亲眼确认返回帧里数据块标志的顺序和长度再写解析代码。不同厂家电表对组合量的实现可能存在少量差异比如某些老表在组合量返回时每个数据块不带长度字节只带相别标志这种情况下只能按实际抓到的报文调整解析逻辑。5. 异常应答D1和无应答状态完整排查链路有时候电表是respond的但返回的是异常帧有时候干脆不搭理你。这两种情况要分开排查。5.1 控制码D1让错误信息字说话当请求有语义错误时电表会返回控制码D1H数据域只有1个字节即错误信息字ERR。比如你发了一个表不支持的电压数据标识可能收到68 78 56 34 12 00 00 68 D1 01 10 CE 16这里L01数据域是10即ERR0x10。错误信息字常用位如下BIT含义D7其他错误D6当前状态不能执行D5无请求数据D4数据标识错误D3密码错误D2通信速率不能改变D1保留D0时间不准确/超范围实测中最常见的两个值0x10数据标识错误。说明你发的02 01 01 00之类标识超出了这块表的支持范围。此时不要怀疑电表坏了去翻表厂家的规约手册确实有很多表只支持单相标识不支持组合标识。0x20无请求数据。意味着控制码和数据标识对应不上比如要求“读”但标识是“写”类型。还有一种情况某些电表对于组合读取里某相数据不存在或无效不会返回ERR而是把对应数据块的值填成FF FF。所以拿到FF FF先当成无效数据处理不要当成电压值。5.2 校验和不匹配的排查思路如果电表返回了一帧但解析端校验和不通过问题通常出在这几处前导符FE被算进了校验和。前导符只是唤醒信号不计入CS。漏算第二个68或者把结束符16也算进去了。CS的计算范围是从第一个68开始到校验和之前的一个字节结束。数据域长度L数错。L是数据域的实际字节数不是整个帧的字节数也不是数据标识加数据的数量少算或多算。建议在调试初期打印每一个字节的十六进制和预期的CS逐字节对一遍。比如上面三相电压返回帧如果解析端算出来是0xD2而不是0xD3那基本就是漏算或者多算了某个字节。5.3 无应答时的排查顺序无应答是最让人头疼的。我的排查顺序是固定的按可能性从高到低来检查RS485接线A/B是否接反是否共地。检查串口参数是不是2400-E-8-1。很多工具箱打开默认9600-N-8-1差一个校验位都不通。检查是否加前导符总线上如果有休眠表FE至少发2个我习惯发4个。检查地址域是不是BCD倒序是不是补0位置错了。这一个坑最隐蔽。检查数据标识组合对特定表型组合标识可能不受支持此时换单相标识试试。检查电表地址是否被改过有些项目现场电表做过密钥或地址变更用出厂地址发就会一直超时。确认方法是看电表铭牌或由厂家工具读取当前地址。5.4 响应慢、半包与粘包电表不是高速设备从收到完整请求到输出应答几十毫秒到一两秒都有可能。我做批量读取时超时时间一般设在2到3秒而不是常见的1秒。设太短慢一点的表会被误判为离线。另外RS485总线上如果一次轮询多块表或者某帧响应很长应用层可能会收到“半包”或“粘包”。比如数据域还没接收完或者一次read包含了上一帧的尾部。这种情况靠串口读取函数直接按固定长度读是不够的最好在缓冲区里按帧结构找68起始符和16结束符把完整的一帧切出来再解析。这个逻辑直接引出了下一章的解析器设计。6. 从串口调试到正式代码一个能用的645帧解析骨架调通了一条命令之后接下来就是把它固化到正式程序里。这里给出一个最精简的解析框架帮你避开“在串口read里直接解码”的坑。6.1 接收缓冲与状态机思路不建议每收到一个字节就立刻尝试解析。因为串口是流式的一帧数据可能分多次到达。比较稳妥的做法是把收到的字节放进一个环形缓冲区然后不断在里面寻找符合以下特征的最短帧从数据流的68位置开始。读6字节地址遇到第二个68。读控制码和长度域L。根据L确定数据域长度拿到CS和16。校验CS等于前面所有字节的累加和低8位。我习惯用状态机来处理# 简化版状态机 states {WAIT_68: 0, ADDR: 1, ADDR_68: 2, CTRL_LEN: 3, DATA: 4, CS: 5}当然这只是思路。真正实现时可以先从一个最简单的函数开始接收完整的一帧到列表再做解析。6.2 完整解析函数示例下面是一段Python示例做了三件事校验CS、判断是正常应答还是异常应答、解析三相电压组合数据块。实际工程里可以用C或C实现逻辑相同。def parse_645_frame(frame: bytes): # frame 是不含前导符FE的完整帧从68开始到16结束 if len(frame) 11 or frame[0] ! 0x68 or frame[-1] ! 0x16: raise ValueError(bad frame boundary) # 校验和从第0字节到倒数第3字节 cs_calc sum(frame[:-2]) 0xFF if cs_calc ! frame[-2]: raise ValueError(fCS mismatch, calc{cs_calc:02X}, recv{frame[-2]:02X}) addr frame[1:7] # 地址域BCD倒序 ctrl frame[7] # 控制码 length frame[8] # 数据域长度 data frame[9:9 length] if ctrl 0xD1: err data[0] raise ValueError(fmeter error: ERR0x{err:02X}) if ctrl ! 0x91: raise ValueError(funexpected ctrl: 0x{ctrl:02X}) # 数据标识 di data[:4] payload data[4:] if di[:2] bytes([0x02, 0x01]): # 电压 if di[2] 0x00: # 组合量三相电压 # 按 相别标志 长度 数据 解析 result {} idx 0 while idx len(payload): phase payload[idx] blen payload[idx 1] raw payload[idx 2: idx 2 blen] val int.from_bytes(raw, little, signedFalse) if raw b\xff\xff or val 0xFFFF: result[fU{phase}] None else: result[fU{phase}] round(val * 0.1, 1) idx blen 2 return result else: # 单相比如 02 01 01 00 raw payload[:2] val int.from_bytes(raw, little, signedFalse) return {U: round(val * 0.1, 1)} return data这里面有几个容易写错的地方校验和计算用的是frame[:-2]也就是从68到CS之前的所有字节CS本身和结束符16都不参与。地址域我没有立即转成十进制串因为很多业务场景需要保留原始hex用于排查。实际需要时再做BCD反转转换。组合数据块的长度字段是真实数据字节数。有的表返回2字节有的返回3字节所以不要硬编码blen2一定按长度字段来。6.3 批量轮询时的节奏控制现场通常不止一块表。批量轮询的节奏有讲究每块表请求之间加一点间隔比如50到100ms。不要极快地连续发帧很多电表判断“总线拥塞”后会自动忽略请求。超时控制在2到3秒超时后重试一次。连续两次超时才标记离线。记录每块表上一次成功应答的时间。如果多块表共享一条485总线某块表长时间无应答会影响整个队列最好把它从轮询列表里暂时摘除每隔一段时间再试探一次。我实际项目里用这套逻辑跑过32块表的总线轮询10秒周期内全部轮询一遍没有任何问题。把组合读取用上之后三相电压一次拿全总线流量也降了不少。写在最后的调试心得如果让我总结这次调645协议的经验就一条先让串口助手把十六进制收发跑通再动代码。串口工具能看到最原始的报文这比任何高级调试手段都直接。在工具上亲眼看到请求帧和应答帧逐字节对应的那一瞬间你对数据标识、地址倒序、校验和的理解才会真正落下来。等一条命令在工具上通了再写正式代码把组合读取、异常应答、超时重试一步步加进去。这样遇到问题时候好定位到底是硬件、地址、还是解析逻辑的错不会所有变量搅在一起猜。DL/T645-2007这套协议本身不复杂但各种表型之间的细微差异确实不少留一点耐心、多抓几帧报文坑就一个个填平了。
返回列表