news 2026/10/26 16:58:43

南瑞101规约C代码解析:从RTU101.rar到报文调试实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
南瑞101规约C代码解析:从RTU101.rar到报文调试实战

简介:这份资源围绕IEC 101规约与南瑞101实现展开,面向电力自动化方向的开发者、RTU调试人员及电力系统通信协议学习者,帮助理解远动通信中RTU与主站之间的数据交互机制。压缩包为rar格式,共2个文件,包含1个cpp源码与1个txt说明文档,整体约13KB,体量轻便,便于快速查阅与二次开发。内容涉及101报文结构、控制域与地址域含义、遥信变位20报文上报、遥测数据实时传输,以及南瑞设备在具体厂站中的厂家标志配置等实践细节,可帮助读者建立从协议标准到工程实现的完整认知。目前已有789人学习下载,适合需要结合源码理解101规约报文解析、排查通信问题或开展电力远动调试的技术人员参考。

1. 南瑞101规约报文解析:从RTU101.rar里翻出一套能跑的C代码

手里拿到一个叫rtu101.rar的压缩包,解压后是一堆.c和.h,文件名里带着101、南瑞、报文这些字眼,这是很多做电力自动化的工程师都会遇到的场景。IEC 101 规约(也就是常说的南瑞101)是国内变电站、配电终端和主站之间最常用的串口通信协议之一,RTU 侧要按这个规约组帧、应答、上送遥测遥信。问题在于,网上流传的这套 C 代码往往缺文档、缺注释,变量名还是拼音缩写,直接编译能过,但报文对不上就对不上,串口一抓全是乱码。这篇笔记就围绕这套rtu101.rar里的 C 101 规约实现,把报文结构、代码怎么读、参数怎么调、调试时哪里最容易翻车讲清楚。适合手里已经有这套代码、或者正准备用 C 语言对接南瑞101主站的嵌入式工程师,读完能自己把一条完整报文从组帧到解析跑通。

2. 南瑞101规约的报文骨架:为什么你的C代码总是对不上帧

2.1 固定帧长与可变帧长:两种格式别混用

IEC 101 规约的报文分两种基本格式,这是读任何一份 C 101 代码前必须先在脑子里立住的东西。固定帧长格式只有 5 个字节,结构是:启动字符0x10、控制域 C、链路地址 A、校验和 CS、结束字符0x16。它用来做链路层的确认、请求链路状态、复位链路这些不需要传数据的场合。可变帧长格式则是:启动字符0x68、长度域 L、再一个0x68、控制域 C、链路地址 A、应用层数据、校验和 CS、结束字符0x16。长度域 L 表示的是从控制域到应用层数据结束的字节数,不含启动字符、长度域本身、校验和和结束符。

很多南瑞101的 C 代码翻车,第一个原因就是把这两种格式的判断条件写错。常见做法是在接收状态机里先判断首字节:收到0x10走固定帧分支,收到0x68走可变帧分支。但有些代码为了省事,统一按可变帧处理,结果固定帧的链路确认报文被当成可变帧解析,长度域读到了控制域的值,整个缓冲区错位。我一般会在接收函数入口就做一次显式分支,并且把两种帧的最小长度校验写死,固定帧必须正好 5 字节,可变帧至少 8 字节,不满足直接丢弃并重新同步。

2.2 控制域:I格式、S格式、U格式的判定逻辑

控制域 C 是南瑞101规约里信息量最大的一个字节,它决定了这条报文是干什么的。按最低两位(有些实现按最高两位,取决于位序约定,南瑞101通常按低位在前)区分:I 格式帧最低位为 0,携带应用层数据,发送序号 N(S) 和接收序号 N(R) 各占 7 位;S 格式帧最低两位是01,只做确认,带接收序号 N(R);U 格式帧最低两位是11,用于链路控制,比如启动链路、停止链路、测试链路。

在 C 代码里,这个判定通常写成宏或者内联函数。下面这段是我从类似rtu101.rar的代码里整理出来的典型写法,逻辑清晰,可以直接对照你手里的版本:

/* 控制域格式判定,南瑞101按低位在前约定 */ #define CTRL_IS_I(c) (((c) & 0x01) == 0x00) #define CTRL_IS_S(c) (((c) & 0x03) == 0x01) #define CTRL_IS_U(c) (((c) & 0x03) == 0x03) /* 从I格式控制域提取发送序号和接收序号 */ static void parse_i_ctrl(unsigned char c1, unsigned char c2, unsigned short *ns, unsigned short *nr) { /* 第一个控制字节低7位是N(S),第二个控制字节低7位是N(R) */ *ns = (unsigned short)((c1 >> 1) & 0x7F); *nr = (unsigned short)((c2 >> 1) & 0x7F); }

这段代码的关键在于位运算的移位方向。南瑞101的序号是 15 位循环,实际只用低 7 位,发送时 N(S) 放在第一个控制字节的 bit1 到 bit7,N(R) 放在第二个控制字节的 bit1 到 bit7。参数上要注意,0x7F这个掩码不能写成0xFF,否则会把控制位带进来,序号直接错乱。如果你手里的代码用的是>> 1之后没做掩码,短时间跑不出问题,一旦序号超过 127 回绕就会对不上,这是血泪经验。

2.3 链路地址与校验和:南瑞101的隐形门槛

链路地址 A 在南瑞101里通常是一个字节,但实际项目里可能扩展成两个字节,取决于主站配置。校验和 CS 是算术和校验,把控制域、链路地址、应用层数据所有字节相加,取低 8 位,再取反加一或者直接取反,具体看主站约定。南瑞的很多主站用的是「和校验取反」,也就是所有字节累加后按位取反。

这里有个容易被忽略的点:校验和的计算范围。固定帧和可变帧的计算范围不一样。固定帧是从控制域到链路地址,可变帧是从控制域到应用层数据最后一个字节,都不包含启动字符、长度域、校验和自身和结束符。我见过有人把长度域也算进去,结果主站一直回否定确认。下面是一个通用的校验和计算函数,参数说明写在注释里:

/* 计算南瑞101校验和,buf指向控制域起始,len为参与计算的字节数 */ static unsigned char calc_cs(const unsigned char *buf, int len) { unsigned char sum = 0; int i; for (i = 0; i < len; i++) { sum += buf[i]; } return (unsigned char)(~sum); /* 按位取反,南瑞主站常用约定 */ }

调用的时候,固定帧传len = 2(控制域加链路地址),可变帧传len = 2 + 应用层数据长度。这个len怎么来的,就是从长度域 L 减去 2(控制域和链路地址各占一字节)得到应用层数据长度,再加 2。参数改错,校验和必错,主站直接丢帧,连否定确认都不给,这是调试时最让人抓狂的黑匣子。

3. 把RTU101.rar里的C代码跑起来:从工程结构到最小可运行

3.1 先理清代码目录:哪些文件是规约核心

拿到rtu101.rar解压后的目录,通常会有这么几类文件:规约核心文件(名字里带101、proc、frame的)、串口驱动文件(带uart、serial的)、应用层映射文件(带yc、yx、yk的,对应遥测遥信遥控)、以及主循环和配置文件。不要一上来就通读,先找规约核心里的帧解析函数和组帧函数,这两个是命脉。

我一般会先看头文件里的结构体定义,尤其是帧缓冲区结构体和链路状态结构体。帧缓冲区通常是一个unsigned char数组加一个长度变量,链路状态里会有发送序号、接收序号、链路状态标志。把这些结构体成员和前面讲的报文格式一一对应上,代码就理解了一半。如果代码里用了全局变量来存这些状态,注意多任务环境下要加锁,裸机跑就没这个问题。

3.2 串口参数配置:南瑞101的默认档位

南瑞101规约跑在串口上,物理层参数通常是:波特率 9600、数据位 8、停止位 1、校验位偶校验(EVEN)。有些现场用 1200 或 2400 波特率,校验位也可能改成无校验,这个必须和主站侧完全一致,差一个参数就是全乱码。在 Linux 下用 termios 配置,在 Windows 下用 DCB,在裸机上进寄存器。下面给一个 Linux 下的配置片段,参数含义逐行说明:

#include <termios.h> #include <fcntl.h> #include <unistd.h> int open_101_port(const char *dev) { int fd = open(dev, O_RDWR | O_NOCTTY); struct termios opt; tcgetattr(fd, &opt); cfsetispeed(&opt, B9600); /* 输入波特率9600 */ cfsetospeed(&opt, B9600); /* 输出波特率9600 */ opt.c_cflag |= (CLOCAL | CREAD); /* 使能接收,忽略调制解调器控制线 */ opt.c_cflag &= ~CSIZE; opt.c_cflag |= CS8; /* 8位数据位 */ opt.c_cflag |= PARENB; /* 使能校验 */ opt.c_cflag &= ~PARODD; /* 偶校验 */ opt.c_cflag &= ~CSTOPB; /* 1位停止位 */ opt.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /* 原始模式 */ opt.c_cc[VMIN] = 1; /* 至少读1字节 */ opt.c_cc[VTIME] = 10; /* 超时1秒 */ tcsetattr(fd, TCSANOW, &opt); return fd; }

这段代码里VMIN和VTIME的组合很关键。VMIN=1, VTIME=10表示至少读到 1 个字节,且字节间超时 1 秒。南瑞101的报文帧内字节间隔通常要求小于 1 秒,帧间可以长一些。如果VTIME设成 0,读操作会一直阻塞;设成太大,帧内超时判断会失效。现场调试时如果发现报文被拆成两段读上来,先查这两个参数。

3.3 最小可运行:手动构造一条固定帧并解析

在把整个工程跑起来之前,我习惯先写一个最小测试,手动构造一条固定帧,喂给解析函数,看输出对不对。固定帧「请求链路状态」的典型内容是:10 49 01 4A 16。其中10是启动,49是 U 格式控制域(测试链路功能),01是链路地址,4A是校验和,16是结束。校验和计算:0x49 + 0x01 = 0x4A,取反是0xB5,但这里4A是直接和,说明这套代码用的是「和校验不取反」的约定。这就是为什么前面强调校验和约定必须和主站一致,同一个规约不同厂家实现可能不同。

构造好之后,调用解析函数,打印出帧类型、控制域格式、链路地址。如果解析出来是 U 格式、地址 1,说明接收状态机和格式判定没问题。这一步过了,再接入真实串口,用主站模拟器发报文,观察应答。很多新手跳过这一步,直接上现场,结果分不清是代码问题还是接线问题,浪费大量时间。

4. 报文解析与组帧的C实现细节:遥测遥信怎么塞进应用层

4.1 应用层数据单元:类型标识与传送原因

可变帧的应用层数据部分,第一个字节是类型标识 TI,第二个字节是可变结构限定词 VSQ,然后是传送原因 COT 和公共地址,之后才是信息对象。类型标识决定了这条报文是遥测、遥信还是总召唤。南瑞101常用的类型标识:单点遥信是 1,双点遥信是 3,归一化遥测是 9,标度化遥测是 11,总召唤是 100,时钟同步是 103。

在 C 代码里,这些通常定义成枚举或宏。解析时先读 TI,然后根据 TI 决定后续信息对象的解析方式。比如遥测是每个对象 2 字节值加 1 字节品质,遥信是每个对象 1 字节状态加 1 字节品质。VSQ 的低 7 位是信息对象数量,最高位表示是否带时标。如果带时标,每个对象后面还要跟 7 个字节的时标。这个逻辑如果写错,解析出来的遥测值会整体偏移,看起来像随机数。

4.2 组帧函数:从结构体到字节流

组帧是解析的逆过程。给定类型标识、传送原因、信息对象数组,拼出一条完整的可变帧。下面是一个简化的组帧函数,重点看长度域和校验和的处理:

/* 组一条可变帧,ti类型标识,cot传送原因,data应用层数据,dlen数据长度 */ int build_var_frame(unsigned char *out, unsigned char ti, unsigned char cot, const unsigned char *data, int dlen) { int idx = 0; int i; unsigned char cs; out[idx++] = 0x68; /* 启动字符 */ out[idx++] = (unsigned char)(dlen + 4); /* 长度域:控制域1+地址1+ASDU头2+数据 */ out[idx++] = 0x68; /* 第二个启动字符 */ out[idx++] = 0x00; /* 控制域,I格式,序号由调用者填充 */ out[idx++] = 0x01; /* 链路地址,按现场配置改 */ out[idx++] = ti; /* 类型标识 */ out[idx++] = 0x01; /* VSQ,假设1个信息对象,不带时标 */ out[idx++] = cot; /* 传送原因 */ out[idx++] = 0x01; /* 公共地址 */ for (i = 0; i < dlen; i++) { out[idx++] = data[i]; } cs = calc_cs(&out[3], idx - 3); /* 从控制域到数据末尾 */ out[idx++] = cs; out[idx++] = 0x16; /* 结束字符 */ return idx; }

长度域的计算是dlen + 4,这 4 个字节是控制域、链路地址、类型标识、VSQ、传送原因、公共地址里的前四个?这里要仔细:长度域 L 统计的是控制域、链路地址、应用层数据的总字节数。应用层数据包括 TI、VSQ、COT、公共地址、信息对象。所以 L = 1(控制域)+ 1(链路地址)+ 1(TI)+ 1(VSQ)+ 1(COT)+ 1(公共地址)+ dlen = dlen + 6。上面代码里写dlen + 4是错的,这是一个典型的翻车点。正确的应该是dlen + 6。校验和从控制域开始,到数据末尾,长度是idx - 3(减去启动、长度、启动三个字节),这个是对的。

4.3 序号管理与超时重传

I 格式帧的发送序号 N(S) 和接收序号 N(R) 是 15 位循环,每发一帧 N(S) 加一,每收一帧 N(R) 更新为对方期望的下一个序号。如果发送后在一定时间内没收到对方的 S 格式确认,需要重传。南瑞101的超时时间通常设 10 到 30 秒,具体看主站。重传次数一般 3 次,超过就报链路中断。

在 C 代码里,序号管理通常用一个结构体存ns、nr、ack_pending标志和重传计数。每次组 I 帧时把ns填进控制域,发送后ns = (ns + 1) & 0x7F。收到 S 帧时,用其中的 N(R) 确认自己已发送的帧。这里有个坑:N(R) 是累积确认,表示该序号之前的所有帧都收到了,不是只确认这一帧。如果代码里按单帧确认处理,重传逻辑会乱。

5. 调试南瑞101报文的避坑清单:串口抓包与常见故障

5.1 现象:主站不回确认,串口无任何输出

原因通常有三个:一是串口参数不匹配,尤其是校验位和停止位;二是 RS-485 收发方向控制没做对,发送时没使能发送,或者发送完没及时切回接收;三是链路地址配错,主站直接忽略。解决方法是先用示波器或串口监听工具看物理线上有没有波形,有波形但主站不回,查地址和校验和;没波形,查驱动和方向控制。RS-485 方向控制脚一般在发送前拉高,发送完最后一位后延时一小段时间再拉低,这个延时不够会导致最后一字节发不出去。

5.2 现象:报文能收到,但解析出来遥测值是乱码

原因多半是字节序或数据类型搞错。南瑞101的遥测值如果是归一化值,是 2 字节有符号数,范围 -1 到 1 对应 0x8000 到 0x7FFF,需要除以 32768 再乘量程。如果是标度化值,是 2 字节整数,直接乘系数。如果代码里把归一化值当标度化值处理,数值会差几个数量级。另外,信息对象的顺序必须和主站的点表一致,顺序错了,遥测值会串位,看起来像乱码。

5.3 现象:运行一段时间后序号对不上,通信中断

这是序号回绕处理不当的典型表现。15 位序号从 0 到 32767 循环,但实际只用低 7 位,也就是 0 到 127。如果代码里用unsigned short存序号但没做& 0x7F,序号会一直涨,超过 127 后控制域里的值就错了。解决方法是每次更新序号后都做掩码,比较序号时也要考虑回绕,不能用简单的>或<。常见做法是计算差值(a - b) & 0x7F,根据差值判断前后。

5.4 现象:总召唤时遥信全为 0 或全为 1

总召唤是主站问 RTU 要全部数据,RTU 需要按点表逐个上送。如果遥信全 0 或全 1,先查点表映射,看遥信状态有没有从硬件采集更新到规约层的数据结构。再查品质位,如果品质位是无效,主站可能显示为 0。还有一种可能是总召唤的激活确认和激活终止没处理好,主站以为召唤没完成,一直等。总召唤的流程是:主站发总召唤激活,RTU 回激活确认,然后 RTU 逐帧上送数据,最后发激活终止。少任何一步,主站都会超时。

5.5 现象:校验和偶尔错,重发后正常

偶发校验错通常是串口干扰或波特率偏差。先检查通信线是否屏蔽接地,波特率晶振是否匹配。如果硬件没问题,看代码里的校验和计算是否用了有符号char,累加时溢出导致结果不对。所有参与校验和的变量都应该用unsigned char,累加变量用unsigned int或unsigned short,最后取低 8 位再取反。这个细节在移植代码时经常被忽略。

6. 用Python脚本辅助验证南瑞101报文:抓包、解析、对比一条龙

调试现场不可能总带着主站模拟器,我习惯用 Python 写一个小脚本,从串口读原始字节,按南瑞101格式解析出来,打印成可读的表格。这样即使没有主站,也能验证 RTU 侧发出的报文对不对。下面这个脚本用pyserial读串口,核心解析逻辑和 C 代码一一对应:

import serial def calc_cs(data): """南瑞101校验和,按位取反""" return (~sum(data)) & 0xFF def parse_frame(buf): """解析一条完整帧,返回字典""" if buf[0] == 0x10 and len(buf) == 5: c, a, cs = buf[1], buf[2], buf[3] return {'type': 'fixed', 'ctrl': c, 'addr': a, 'cs_ok': calc_cs(buf[1:3]) == cs} if buf[0] == 0x68 and buf[2] == 0x68: l = buf[1] c, a = buf[3], buf[4] asdu = buf[5:3+l] cs = buf[3+l] return {'type': 'variable', 'len': l, 'ctrl': c, 'addr': a, 'asdu': asdu.hex(), 'cs_ok': calc_cs(buf[3:3+l]) == cs} return {'type': 'unknown'} ser = serial.Serial('/dev/ttyS0', 9600, parity=serial.PARITY_EVEN, bytesize=8, stopbits=1, timeout=1) while True: raw = ser.read(256) if raw: print(parse_frame(raw))

脚本里calc_cs的取反约定要和 C 代码一致,如果现场主站用的是不取反,这里也要改。parse_frame里可变帧的asdu切片是buf[5:3+l],因为长度域 L 从控制域算起,控制域在 buf[3],所以应用层数据从 buf[5] 开始,到3+l结束。这个索引关系在 C 代码里也经常写错,用 Python 先验证一遍,再回去改 C,效率高很多。

进阶一点,可以把解析出来的 ASDU 进一步拆成类型标识、传送原因、信息对象,和主站点表对比。我一般会在脚本里加一个点表字典,把信息对象地址映射成中文名,这样打印出来就是「遥测A相电压=220.5V」这种可读信息,现场排查时一眼就能看出哪个点不对。这个习惯帮我省下了大量和主站扯皮的时间。

最后说一个我自己的教训:不要相信任何没有经过校验和验证的报文。早期调试时看到串口有数据就以为通了,结果主站一直报通信中断,查了两天才发现是校验和约定搞反了。从那以后,我拿到任何一套南瑞101的 C 代码,第一件事就是写个脚本把校验和函数单独测一遍,确认和主站约定一致再往下走。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 16:56:59

人物音色档案加音频参考:构建统一且有辨识度的AI角色声音

一、为什么要看这份榜单 AI角色声音制作通常包含几个连续环节&#xff1a;确定人物设定、建立声音方向、准备音频参考、生成台词、结合画面和剧情进行调整。如果这些环节彼此割裂&#xff0c;常见问题包括&#xff1a; 同一角色在不同片段中音色变化明显&#xff1b;角色的年龄…

作者头像 李华
网站建设 2026/10/26 16:56:57

t3code:TypeScript+Tailwind+tRPC+Codegen跨端开发范式

1. 项目概述&#xff1a;t3code 是什么&#xff0c;它解决哪类开发者的实际痛点&#xff1f;t3code 这个名字乍看像某个新出的 CLI 工具或开源项目代号&#xff0c;但结合当前全网搜索热度与高频共现词——CLI、Electron、web app、iOS——它并非一个已发布、有官方文档的成熟产…

作者头像 李华
网站建设 2026/10/26 16:56:06

有限反馈预编码详解:从SVD到DFT码本与MMSE接收的完整实现

简介&#xff1a;面向MIMO无线通信与有限反馈预编码学习者的Matlab仿真资源包&#xff0c;围绕SVD预编码、MMSE接收机、DFT码本与码本设计展开&#xff0c;适合通信工程与电子信息类专业学生、研究人员对照学习。压缩包内共9个文件&#xff0c;以4个.m脚本和5个.mat数据文件为主…

作者头像 李华
网站建设 2026/10/26 16:54:38

AI Agent循环工程实战:手写Loop Engineering避免死循环与失控

写AI Agent最让人头疼的是什么&#xff1f;不是Prompt写不好&#xff0c;也不是模型选不对——而是你明明给了它一堆工具和任务&#xff0c;它却在执行过程中反复调用同一个工具&#xff0c;或者在一个环节里绕来绕去&#xff0c;最后既没完成任务&#xff0c;也没告诉你发生了…

作者头像 李华
网站建设 2026/10/26 16:54:23

开源版workbuddy替代:基于Rust与MCP的Agent工作台实战

1. 从“workbuddy 替代”这个念头说起&#xff1a;我到底想解决什么问题最早动这个念头&#xff0c;是因为我在好几个项目里反复遇到同一个尴尬场景&#xff1a;团队里有人在用 workbuddy 这类工作台工具管理日常任务、文档和 Agent 调用&#xff0c;但一旦涉及私有化部署、数据…

作者头像 李华
网站建设 2026/10/26 16:51:42

Rust与Lean 4结合的账本形式化验证实践

1. 项目背景与核心价值 在分布式系统和区块链技术蓬勃发展的今天&#xff0c;账本实现的正确性验证成为了一个关键挑战。传统测试方法往往难以覆盖所有边界条件&#xff0c;而形式化验证又存在学习曲线陡峭的问题。verified-ledger项目提出了一种创新性的解决方案——将Lean 4定…

作者头像 李华