ARTICLE DETAIL

资讯详情

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

DL/T645-2007电表通信协议实战:三相电压读取5个关键步骤

DL/T645-2007电表通信协议实战:三相电压读取5个关键步骤 DL/T645-2007这个协议凡是做过电表数据采集的应该都不陌生。它看起来特别简单——发一帧十六进制报文收一帧拆一下数据就完事。可真到了现场拿串口去读三相电压我见过太多人在几个固定坑位上反复折腾表号抄错一位、地址顺序搞反、校验和怎么算都对不上、返回的数据解出来电压能到几百上千伏。这篇文章不打算把协议规范逐条念一遍我按自己调表时真正会走的路径从电表地址解析到三相电压读取拆成5个关键步骤每个步骤都配上实操报文和避坑说明。适合正在写采集程序、或者拿着串口工具在配电房里对着电表发呆的工程师参考。1. 五个关键步骤逐个拆解1.1 步骤一确认物理链路与通信参数DL/T645-2007的物理层基本就是RS-485半双工差分通信。很多人一上来就急着拼报文结果连一帧数据都发不出去问题全出在链路和参数上。接线是最先要确认的事。RS-485通常是A、B两根线对应电表端子上的RS485 A和RS485 B。USB转485模块的标识可能不同有的标A/B有的标D/D-碰到标法不一致的最好翻一下模块手册别凭感觉猜。如果总线上只有一块表A/B接反不会烧设备但一定通信不上调换一下就能验证。模块和电表之间尽量用双绞线屏蔽层单端接地现场有变频器或大功率设备时这能省掉很多莫名其妙的干扰问题。通信参数这块是重灾区。DL/T645-2007表计最常见的出厂默认是1200bps、偶校验、8数据位、1停止位也就是常说的“1200 E 8 1”。但很多串口调试助手默认是9600、8N1第一次打开工具就按默认配置去读电表当然不会理你。部分电表支持自适应波特率不过那是表侧行为主站侧最好还是先把参数设成1200 E 8 1去试通了再考虑别的波特率。判断链路通没通不要急着读电压先用一个最简单的命令验证读通信地址。主站发送下面这帧广播报文如果总线上只有一只表它应该会返回自己的通信地址68 AA AA AA AA AA AA 13 00 77 16其中地址域AAAAAAAAAAAA是广播地址13是读通信地址控制码00是数据域长度77是校验和。返回帧的控制码是93数据域里就带表地址。这一步能通说明接线、串口参数、模块都正常后面就可以放心往下走。注意广播读地址只适合总线上只挂一只表的情况。如果总线上有多只表多只同时响应会冲突这时候还是得按铭牌上的地址去读。1.2 步骤二解析电表地址DL/T645-2007的地址域固定6个字节每字节存两位BCD码整个地址就是12位十进制数。电表铭牌上那一串编号比如“202405120001”在报文里不会按从左到右的顺序直接出现。具体的处理方法先把12位数字从高位开始两两分组得到20 24 05 12 00 01然后把这6组倒序发送帧内地址域就是 01 00 12 05 24 20。换句话说地址域就是“从右往左每两位一组依次填进6个字节”。很多人在这一步直接把十六进制字符串反转比如把202405120001转成0x202405120001后倒过来结果字节顺序和BCD位序全乱了这是最常见的一个坑。现场还有另一种情况铭牌上的编号未必就是通信地址。有的表通信地址是表号有的表是出厂编号有的还带地区码或厂商代码前缀。所以我到一个新现场第一件事就是抄下标牌号然后用步骤一里那条读通信地址命令把表真正在用的地址确认一遍。报文解析出来的地址和铭牌对得上就继续对不上就以表返回的为准再去翻表计说明书。广播地址AAAAAAAAAAAA只能用于广播校时或读通信地址不能拿它去读电压、电量这类具体数据。如果向广播地址发读电压命令表不知道你要读哪块表自然不会有响应。1.3 步骤三构造读取三相电压的报文DL/T645-2007的帧结构不算复杂固定由起始符、地址域、控制码、数据域长度、数据域、校验和、结束符组成。我习惯记成下面这张表组成部分长度说明起始符1字节固定0x68地址域6字节通信地址低字节在前控制码1字节表示这次是读还是写、是请求还是应答数据域长度1字节数据域的实际字节数数据域L字节数据标识、数据内容校验和1字节从起始符到数据域末尾累加取低8位结束符1字节固定0x16控制码是很多人第一次看会绕晕的地方。主站请求读数据用0x11从站正常应答用0x91从站异常应答用0xD1。这三个值要记住调程序判断返回值时频繁用到。0x11的最高位是0表示主站发起的请求从站应答时会把方向位翻成1所以读数据应答就成了0x91。当表侧检测到命令有问题应答控制码是0xD1收到这个值别继续按正常数据解析先去查错误信息字。数据标识用4个字节表示协议本身给出了编码结构但不同厂家表计的具体数据项定义可能不同。以常见的国网电表为例三相电压一般这么定数据项数据标识帧内发送顺序DI0 DI1 DI2 DI3单位A相电压0x0201010000 01 01 020.1VB相电压0x0201020000 02 01 020.1VC相电压0x0201030000 03 01 020.1VAB线电压0x0201040000 04 01 020.1VBC线电压0x0201050000 05 01 020.1VCA线电压0x0201060000 06 01 020.1V数据标识在帧内不是按0x02010100这个整体直接发的而是先发最低字节。这也是一个高发错误点。建议第一次调表时把表计附带的《数据标识编码表》翻出来核对别直接照抄网上的表除非你能确认手里的表和对方用的是同一个固件版本。现在把前面几步串起来假设表地址是前面提到的202405120001要读A相电压完整请求帧就是68 01 00 12 05 24 20 11 04 00 01 01 02 D1 16计算校验和时把从0x68开始一直到数据域最后一个字节0x02的所有字节累加结果0x1D1取低8位得到0xD1。注意这里校验和恰好是0xD1和异常应答控制码0xD1数值一样纯属巧合别把它当成错误帧。提示任何调试工具发送报文时记得把显示和输入都切到HEX模式不要用ASCII模式否则你看到一堆乱码表也完全不会应答。1.4 步骤四接收返回帧并解析电压数据以刚才那条A相电压请求为例正常返回帧大致是68 01 00 12 05 24 20 91 07 00 01 01 02 59 23 00 DC 16看懂这帧返回值的结构等于掌握了解析所有DL/T645-2007数据的套路。前7个字节的地址域和请求帧一致0x91说明是正常应答0x07是数据域长度表示后面跟7个字节数据域前4个字节是数据标识00 01 01 02和请求里的标识一致后3个字节59 23 00就是A相电压数据。电压数据是压缩BCD码而且是低字节在前。实际计算时先把字节逆序得到00 23 59再按BCD码解析成十进制数002359也就是2359然后乘以单位0.1V得出235.9V。如果你直接用十六进制转换工具去算59 23 00一定会得到完全错误的数值这是新手最容易挂在最后一步的地方。BCD码和普通十六进制是两回事。0x59表示的是十进制59不是十进制的89。读电压、电流、电量这类数据时拿到字节先确定字节顺序再确定BCD解析方式然后乘上对应倍率顺序不能乱。解析时还要养成看数据域长度的习惯。有的表返回的数据域里除了数据标识和数据还会附加状态字或时间戳导致长度不是固定的7字节。正确做法是先取出长度字段L再按L把数据域整体切出来之后才去判断前4字节是不是请求的数据标识、后面哪些字节是我们要的数据值。不要按固定偏移去截取否则遇到长度变化就会错位。如果你收到控制码0xD1的返回帧说明表侧认为请求有异常。常见原因包括数据标识不存在、读取条件不满足、表内采集模块故障等。数据域的第一个字节是错误信息字具体每位含义要看表计手册不同厂家定义有差异。1.5 步骤五连续读取与多只电表的轮询设计单帧能读通之后剩下的问题就是怎么稳定地把三相电压都读出来以及怎么在总线上带多只表。读三相电压我一般直接发三帧单独读分别改数据标识里的第二字节A相: 68 01 00 12 05 24 20 11 04 00 01 01 02 D1 16 B相: 68 01 00 12 05 24 20 11 04 00 02 01 02 ? 16 C相: 68 01 00 12 05 24 20 11 04 00 03 01 02 ? 16不要试图找“一条命令读三相电压”的通用标识这个在不同厂家表计上差异很大有的话也未必按你预想的方式返回。最稳的做法就是一相一帧轮询帧与帧之间留至少50ms的间隔。RS-485是半双工总线发完一帧后立刻发下一帧可能上一帧的应答还没回来总线冲突就发生了。轮询多只表时每一帧都要先切到对应表的地址然后独立组帧、发送、等待、解析。每只表给200到500ms的超时窗口比较合理。超时后不要立刻重发同一帧至少等一个超时周期再重试否则表正在忙或总线不稳定时重发只会加剧拥塞。连续重试2到3次仍然无响应就跳到下一只表把故障记录下来别让一只失联表卡死整个采集周期。2. 校验和与BCD码两个最容易翻车的细节2.1 校验和为什么经常算不对DL/T645-2007的校验和范围要特别注意从起始符0x68开始一直累加到数据域的最后一个字节结束符0x16不参与计算。我见过不止一个人把结束符也加进去或者从地址域开始累加结果算出来的校验和怎么都对不上。校验和算法本身很简单就是所有参与字节的二进制累加超过0xFF的部分直接截断取低8位。比如A相电压请求帧0x68 0x01 0x00 0x12 0x05 0x24 0x20 0x11 0x04 0x00 0x01 0x01 0x02 0x1D1取低8位得到0xD1。用程序写就是一个循环求和加一个按位与def calc_cs(data: bytes) - int: return sum(data) 0xFF发送端组帧时先临时把校验和位置填0计算完再填进去。接收端校验时也要用同样的范围去算再和帧里的校验和字节比对。这里有个实用的小技巧接收端把整帧包括校验和字节但不含结束符0x16所有字节累加取低8位结果应该是0x00。如果不是0直接判定校验失败不需要单独取出原校验和再比较。这个判断方式在写程序时特别省事。2.2 BCD解码的方向和倍率压缩BCD码每字节表示两位十进制数高4位是高位数字低4位是低位数字。以刚才的电压数据59 23 00为例如果按“高位字节在前”去读就是0x592300对应十进制592300乘以0.1V就是59230.0V这个数值显然不对。所以解析电压类数据时第一步永远是先按协议规定的字节顺序把数据逆序然后再做BCD解码。电表返回的电压数据一般是3字节单位0.1V。拿到数据先把字节逆序成00 23 59再按BCD解析成2359最后乘以0.1得到235.9V。如果你读的是电流、功率单位可能是0.0001A、0.0001kW等具体倍率同样以表计手册为准别默认所有数据都乘0.1。我在程序里会专门写一个BCD解码函数避免和普通十六进制转换混用def bcd_to_int(data: bytes) - int: val 0 for b in data: hi (b 4) 0x0F lo b 0x0F val val * 100 hi * 10 lo return val如果读到的数据类型带符号比如功率方向、潮流方向协议里会用补码表示符号。遇到这种数据BCD解码之前要先看最高字节的最高位是0还是1是1就说明是负数要按补码规则还原。但三相电压本身是无符号数据不需要这步属于题外话。3. 常见问题与排查技巧实录3.1 一张问题速查表我把自己在现场和实验室里碰到过的问题整理成了一张表基本覆盖了从地址解析到三相电压读取的绝大多数故障现象可能原因处理动作发任何帧都无响应接线A/B反了、串口参数不对、模块没供电先发读通信地址0x13验证链路再查接线和参数读通信地址能通读电压不通广播地址读电压、数据标识错误改用具体表地址核对DI数据标识返回帧校验和总不对结束符被算进去、校验范围没包含起始符确认校验范围是68到数据域末尾不含16能收到返回帧但电压值离谱BCD按hex解了、字节顺序没逆序、倍率不对先逆序再BC D解码按0.1V换算返回帧控制码是D1数据标识非法、表内故障、读取条件不满足停止解析读取错误信息字查手册同一台工具换表后不通通信地址不是铭牌号、表固件参数不同用0x13读实际地址核对厂家手册多只表轮询偶尔丢帧帧间隔太短、超时太短、总线冲突帧间隔加到50ms以上超时给足300ms这张表看着简单每一条背后都是真实的调试图。尤其是“读通信地址能通读电压不通”这条很多人会怀疑协议哪里理解错了其实往往只是拿着广播地址去读数据或者数据标识里的DI顺序发反了。3.2 一个真实的排查过程有一次在现场调试对方给的地址是“888800000001”我按规则倒序组帧发出去一直没有应答。用0x13广播读地址返回的地址是“000000000001”。原来这只表把前4位厂商代码固定成了0000通信地址并没有完全采用铭牌上的12位编号。改回表返回的地址后一帧就通了。这个例子想说明的是地址解析不是机械地把铭牌号倒序而是要以表计实际存储的通信地址为准。铭牌上印的编号可能已经包含了营销编号、资产编号等信息不一定等于通信地址。调任何一只新表之前先发一次读通信地址既能验证链路又能拿到最可靠的地址来源。就算读出来的地址和铭牌一致也花不了几秒钟这个习惯能省掉大量无意义的排查时间。还有一次对方说“能收到返回帧但电压算出来是两万多伏”。我让他把原始报文发过来一看数据域最后一个字节是0x59开头但数据标识多了一个字节长度字段是08而不是07。他按固定偏移去取电压字节取到了错误位置。从那以后我解析代码的第一行一定是先读长度字段再按L切数据域绝不假设返回长度固定。4. 调试工具与脚本示例4.1 硬件选型与串口工具读DL/T645-2007电表硬件上最常用的就是USB转RS-485模块。选型时优先选带隔离的模块价格贵一点但能在现场意外共地、浪涌时保护电脑和表计。那种几块钱的TTL转USB小板直接怼电表不仅抗干扰差还容易烧模块。串口调试工具我习惯用支持HEX收发、能定时发送的。SSCOM、友善串口调试助手这类都行设置好串口号、1200 E 8 1的参数把HEX报文粘进去发送返回的HEX帧一眼就能看清。手动调几帧没问题后建议把验证过的路径写成脚本避免后续靠人工敲十六进制组帧累且容易错。4.2 一个可直接运行的Python读取脚本下面这段脚本是我平时单点调试的简化版用pyserial发请求、收响应、校验和、解BCD直接能跑。注意这只是在单只表、单帧请求的场景下用生产环境的轮询逻辑要再补超时重试和异常处理。import serial import time # 假设表地址为 202405120001帧内地址域为 01 00 12 05 24 20 ADDR bytes.fromhex(01 00 12 05 24 20) # A相电压数据标识帧内发送顺序为 00 01 01 02 DI_A bytes.fromhex(00 01 01 02) def calc_cs(data: bytes) - int: # 校验和从起始符0x68到数据域末尾所有字节累加取低8位 return sum(data) 0xFF def build_read_frame(addr: bytes, di: bytes) - bytes: # 帧 起始符 地址 控制码11 数据域长度 数据标识 校验和 结束符 head bytes([0x68]) addr bytes([0x11, len(di)]) di return head bytes([calc_cs(head), 0x16]) def bcd_to_int(data: bytes) - int: # 压缩BCD转十进制data按高字节在前传入 val 0 for b in data: hi (b 4) 0x0F lo b 0x0F val val * 100 hi * 10 lo return val if __name__ __main__: ser serial.Serial( portCOM3, baudrate1200, bytesize8, parityE, stopbits1, timeout1 ) req build_read_frame(ADDR, DI_A) print(发送:, req.hex().upper()) ser.write(req) # 预留通信处理时间RS-485半双工要等表返回 time.sleep(0.2) resp ser.read(64) if len(resp) 10: print(响应太短可能超时或链路异常) ser.close() raise SystemExit(1) # 校验长度len(resp)应至少是 起始1地址6控制1长度1数据域L校验1结束1 length resp[8] # 校验和范围是 0 到 8length-1即不包含结束符 cs_calc calc_cs(resp[:9 length]) if cs_calc ! 0: print(校验和错误) ser.close() raise SystemExit(1) # 数据域从第9字节开始 data resp[9:9 length] # 判断数据标识是否与请求一致 if data[:4] ! DI_A: print(数据标识不匹配) ser.close() raise SystemExit(1) # 电压数据为3字节低字节在前先逆序再BCD解码 raw data[4:7] voltage bcd_to_int(raw[::-1]) * 0.1 print(A相电压: %.1f V % voltage) ser.close()脚本思路是组一个最简单的读A相电压请求发出去之后读取响应收到响应先判断长度再按校验和是否为零判断帧完整性最后取出数据域确认数据标识匹配再把3字节电压逆序、BCD解码、乘以0.1。这个流程套到B相、C相只需要改DI完全可以复用。5. 写在后面一点个人经验协议栈调不通的时候我始终坚持一个原则先退到最小验证。什么是最小验证就是总线上只留一只表发一条读通信地址0x13。这条命令能通链路和串口参数就没问题剩下的坑只可能在地址解析、数据标识、校验和这些协议细节里这条命令都不通就先别碰协议去查接线、供电和串口设置。我靠这个原则排查过很多“看起来像协议问题”的现场最后发现有一半根本不是协议的事。再啰嗦一句现场安全问题。DL/T645-2007表计一般在配电柜、表箱里读表前先确认作业环境安全不要带电插拔RS-485线不要碰电压端子非专业人员不要打开表盖操作。技术问题可以反复试安全和合规问题不能试。协议读通了、电压数据能稳定解出来了后面再去做多表轮询、数据入库、异常告警就只是工程量的积累没有本质门槛了。
返回列表