简介:面向电力行业嵌入式开发与运维人员,XHPD_C.rar是一套基于C51微控制器的故障指示器源代码,用于实现线路短路、接地等瞬时故障检测、记录与远程上传。该套代码虽不涉及录波采样,但聚焦故障快速定位与信息上报,适合具有C51基础的开发者进行二次开发或对照学习。资源共37个文件,包含7个C源码、9个头文件,并辅以中断、串口、E2ROM等模块文件,以及Keil工程配置(uvproj/uvopt)、链接映射(lnp/map)、汇编启动(a51)和编译生成的hex、lst、obj等类型,整体仅80KB,目录按lib、src、User、Objects等划分,结构清晰。当前已有152人学习下载。通过阅读源码可掌握电流突变检测思路、故障信息存储与上报流程,理解定时器、中断、串口外设在电力监测装置中的典型应用,对提升电力系统自动化故障处理能力具有实用参考价值。
1. 这套 C51 工程到底在解决什么问题
拿到XHPD_C.rar这个压缩包,第一眼看到的是Keil工程文件.uvproj/.uvopt和一堆.c/.h源码,很多人会误以为它是一个带录波功能的完整录波器项目。但把源码结构和功能模块过一遍就会发现,它本质上是一套基于 C51 单片机的故障指示器核心控制程序,实现的是「故障检测 + 就地指示 + 数据上报」这条链路,并不包含录波数据的采集与存储。所谓录波器,通常要能连续采样电压电流并保存故障前后的波形;而这个工程的定位是快速定位故障,用指示灯和通信报文告诉你「哪条线路、什么时刻、发生了什么类型的故障」,至于故障瞬间的电气量细节,它不负责。
这个区别决定了我们后续看代码和移植时的重点。你不需要去找波形文件怎么解析,也不需要关心采样率够不够高,真正值得读的是三块:一是 AD 采样和故障判据怎么配合,二是故障记录用什么结构存、存在哪,三是上传协议怎么组织。这套代码对做电力自动化终端、智能配网设备、故障指示器产品化的工程师特别有参考价值,尤其是 C51 平台上资源受限时怎么把「检测、存储、通信」三件事塞进一个 8 位 MCU 里。下面直接按源码工程src目录里的模块展开,边讲原理边给可复现的代码和参数。
2. 从工程结构拆解看各模块的职责边界
2.1 启动与主循环:STARTUP 和 main 的分工
工程根目录下能看到STARTUP.A51、main.c、interrupt.c等文件。STARTUP.A51是 Keil C51 编译器配套的启动文件,负责在进入main()之前完成数据存储器清零、寄存器初始化、堆栈指针设置。对于 C51 来说,这一步不是可有可无的,因为 8051 内核的片内 RAM 上电后是随机值,如果不清零,全局变量的初值就会出问题。
main.c里的主循环通常是一个while(1)大循环,配合定时器中断来做任务调度。我一般不建议在裸机循环里写死 delay 等到天荒地老,常见做法是用一个系统 tick(比如 1ms 中断里置标志位),主循环里扫描标志位来轮询各个子任务。这样 AD 采样、故障判断、通信处理就不会互相卡住。代码结构大致如下:
// main.c 片段 void main(void) { System_Init(); // 时钟、IO、串口、ADC 初始化 Timer0_Init(1000); // 1ms 定时中断,用于系统 tick while (1) { if (Tick_1ms_Flag) { Tick_1ms_Flag = 0; ADC_Process(); // 采集电流电压,做故障判据 Key_Scan(); // 面板按钮,用于翻看故障记录 Comm_Process(); // 处理上位机下发命令和主动上报 } } }这段代码的逻辑很简单,但参数上有个容易被忽略的点:Timer0_Init(1000)的 1000 表示定时器重装值,具体对应多少毫秒取决于晶振频率和定时器工作模式。C51 的定时器是 16 位,最大计数 65535,如果晶振是 11.0592MHz,12T 模式下每个机器周期是 12 个时钟周期,定时器每 1.085us 计一次数。要产生 1ms 中断,重装值大约是 65536 - 922 = 64614。这个值改错,整个系统的 tick 就会偏,AD 采样间隔和故障判断的时间窗全部跟着错,所以在移植到别的板子时一定要重新算。
2.2 AD 采样与故障检测的配合方式
故障指示器检测的是线路电流,判据一般是「电流突变」或「越限」。src下的 AD 采样代码通过 C51 操作 P1 口或专用 ADC 引脚读取模拟量,采样结果经过滑动滤波后与阈值比较。这里最常见的一个坑是:故障瞬间电流是突变的,直接用单次采样值判断容易误动;但如果滤波窗口太长,又会导致动作变慢。实际工程里常取 5~10 个周波的 RMS 值作为基准,然后用当前瞬时值跟基准值做比例比较。
// fault_detect.c 片段 #define FAULT_CURRENT_RATIO 5 // 当前电流 / 基准电流 > 5 判为故障 #define SAMPLE_BUF_SIZE 20 // 滑动滤波窗口 unsigned int sample_buf[SAMPLE_BUF_SIZE]; unsigned char buf_index = 0; unsigned long sample_sum = 0; unsigned int ADC_Get_Average(void) { unsigned char i; unsigned int avg; sample_sum -= sample_buf[buf_index]; sample_buf[buf_index] = ADC_Read(); // 读 AD 转换结果 sample_sum += sample_buf[buf_index]; buf_index = (buf_index + 1) % SAMPLE_BUF_SIZE; avg = sample_sum / SAMPLE_BUF_SIZE; return avg; } unsigned char Fault_Judge(unsigned int base_current, unsigned int current) { if (current > (base_current * FAULT_CURRENT_RATIO)) { return 1; // 发生过流故障 } return 0; }这段代码里sample_sum是滑动窗口的和,每次采样替换掉最旧的数据,避免每次重新累加 20 个点的开销。对于 8 位机来说,用unsigned long保存和值是必要的,因为 20 个 16 位采样值累加最大到 20 * 65535,约 130 万,必须超过 16 位才能存下。FAULT_CURRENT_RATIO取 5 是一个经验值,根据现场线路负荷和设备 CT 变比不同,实际整定时可能要调到 3 到 8 之间。整定值太小,负荷波动就会误报;太大,短路故障可能检测不到。整定这件事没有标准答案,只能在现场用继保测试仪做故障模拟来标定。
2.3 中断服务:为什么故障检测要放在中断里
中断服务函数集中在interrupt.c里,一般是用外部中断(INT0/INT1)来响应故障信号,或者用定时器中断做周期性采样。故障检测放中断里是必要的,因为主循环里的任务可能会被通信阻塞,如果故障发生时不立刻响应,等到主循环轮询到可能已经过去好几毫秒,故障电流可能已经导致设备损坏。
// interrupt.c 片段 void EXT_INT0_ISR(void) interrupt 0 { unsigned int current_now; current_now = ADC_Get_Average(); if (Fault_Judge(base_current, current_now)) { Fault_Record_Save(FAULT_TYPE_OVERCURRENT, current_now); Indicate_LED_On(); UART_Send_Fault_Packet(); } }注意 C51 的中断函数用interrupt 0来声明,数字对应中断向量号,0 是外部中断 0,1 是定时器 0,2 是外部中断 1,3 是定时器 1,4 是串口。这个编号写错,中断永远不会触发,而且编译器不会报错,这是 C51 里特别容易踩的坑。中断服务函数里做的事情越少越好,这里只做采样、判据、存记录和发报文,不做耗时的打印或延时。如果确实需要传大量数据,应该置一个标志位,等主循环来处理。
3. 故障记录的非易失存储与掉电保护
3.1 E2rom 模块的读写封装与地址分配
工程中出现的E2rom.c对应的是 AT24C02 或类似 I2C EEPROM 芯片,用来保存故障记录。不带录波功能的故障指示器,掉电后至少要保留最近几条故障信息,包括故障类型、发生时间、动作电流值,这些数据就是存在 E2ROM 里的。C51 通过模拟 I2C 时序来读写 EEPROM,因为 8051 没有硬件 I2C 外设。
// E2rom.c 片段 #define E2ROM_I2C_ADDR 0xA0 // AT24C02 的 I2C 地址 #define FAULT_RECORD_ADDR 0x10 // 故障记录起始地址 void E2rom_Write_Record(unsigned char record_id, FAULT_RECORD_T *record) { unsigned int addr = FAULT_RECORD_ADDR + record_id * sizeof(FAULT_RECORD_T); unsigned char i; unsigned char *p = (unsigned char *)record; I2C_Start(); I2C_SendByte(E2ROM_I2C_ADDR | 0x00); // 写模式 I2C_SendByte((addr >> 8) & 0xFF); I2C_SendByte(addr & 0xFF); for (i = 0; i < sizeof(FAULT_RECORD_T); i++) { I2C_SendByte(*p++); } I2C_Stop(); Delay_Ms(5); // EEPROM 写周期,AT24C02 典型为 5ms }这里sizeof(FAULT_RECORD_T)是这个封装的关键,如果结构体里有字节对齐的问题,写入和读取的长度不一致,故障记录就会错乱。C51 默认是 1 字节对齐,但如果你用了#pragma pack或者编译器设置改了,就要注意。另一个参数是 EEPROM 写周期,AT24C02 的数据手册标称写周期最大 5ms,写完后必须延时才能进行下一次写操作,否则数据会丢失。这个延时不能省,也不可能靠查询 ACK 位来替代,因为 ACK 只表示芯片收到了字节,不代表内部擦写完成。
3.2 循环覆盖策略:故障记录怎么保证不丢最新
故障记录条数不可能无限增长,EEPROM 的空间就那么大,所以要设计一个循环覆盖机制。简单做法是固定存 N 条,每次写入新的就把最旧的一条覆盖掉。工程里通常会有一个记录计数器和当前写指针,在系统初始化时扫描 EEPROM 里的记录,找到最新的位置再开始写。
// 记录管理逻辑 #define MAX_RECORD_COUNT 16 unsigned char record_write_index = 0; void Fault_Record_Save(unsigned char type, unsigned int current) { FAULT_RECORD_T record; record.type = type; record.current = current; record.time_stamp = Get_RTC_Time(); // 从外部 RTC 或内部定时器读取 E2rom_Write_Record(record_write_index, &record); record_write_index = (record_write_index + 1) % MAX_RECORD_COUNT; }record_write_index在初始化时要恢复。常见做法是在 EEPROM 里单独存一个地址放当前索引,每次上电读出来。如果这个索引值因为掉电写坏变成非法值(超过 MAX_RECORD_COUNT),要在代码里做合法性校验,否则数组就越界了。这个「上电恢复索引」的逻辑很多人会忽略,结果故障记录存了几条之后,重启覆盖位置就乱了。另外一个细节是,EEPROM 写入次数有限(AT24C02 标称 100 万次),如果故障频繁,单点反复写会加速老化。进阶方案是把记录分散写到 16 个不同的起始地址,磨损均衡,但这个工程只有 16 条记录,空间不足以做复杂均衡,所以不要过度设计。
4. 故障信息上传:串口通信与协议解析
4.1 UART 波特率配置与中断接收
故障指示器要把故障信息上报到集中器或手持终端,最常见的物理层就是 RS485 串口。C51 的串口工作在模式 1,波特率由定时器 1 溢出率决定。这里的参数计算要精确,因为波特率误差超过 2% 就会导致通信误码。
// UART_Init:以 9600bps 为例,晶振 11.0592MHz void UART_Init(void) { SCON = 0x50; // 模式1,8位UART,允许接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1,模式2,8位自动重装 TH1 = 0xFD; // 波特率重装值,对应 9600bps TL1 = 0xFD; TR1 = 1; // 启动定时器1 ES = 1; // 使能串口中断 EA = 1; // 使能总中断 }TH1 = 0xFD 的计算公式是:波特率 = 晶振频率 / (32 × 12 × (256 - TH1))。代入 11.0592MHz 和 0xFD(十进制 253),得到 11059200 / (32 × 12 × 3) = 9600,正好是标准值。为什么要强调晶振必须是 11.0592MHz?因为只有这个频率才能整除出标准的 9600、19200 等波特率。如果板子上用的是 12MHz 晶振,9600 波特率的误差会达到 2.5% 左右,短距离可能还能通,但长距离 RS485 就悬了。所以移植代码时,第一件事就是确认晶振频率和 TH1 是否匹配。
4.2 报文格式设计与校验:怎么让上位机认得出故障
故障信息不能裸发一个电流值了事,要有帧头、地址、命令、数据、校验。常见做法是 Modbus-RTU 或者自定义的类似协议。工程源码里用的是自定义协议,核心是防止总线上的干扰导致误动作。我给出一个典型的上报帧格式:
| 字节偏移 | 内容 | 长度 | 说明 |
|---|---|---|---|
| 0 | 帧头 0xAA | 1 | 同步字节 |
| 1 | 设备地址 | 1 | 1~247,0xFF 为广播 |
| 2 | 命令字 | 1 | 0x01 实时数据,0x02 故障上报 |
| 3 | 故障类型 | 1 | 0x01 过流,0x02 接地 |
| 4-5 | 故障电流 | 2 | 高字节在前,单位 A |
| 6-7 | 故障时间戳 | 2 | 相对时间,单位分钟 |
| 8 | CRC 校验 | 1 | 异或校验或 CRC-8 |
// 发送故障上报帧 void UART_Send_Fault_Packet(void) { unsigned char packet[9]; unsigned char i; unsigned char crc = 0; packet[0] = 0xAA; // 帧头 packet[1] = DEVICE_ADDR; // 本机地址 packet[2] = 0x02; // 故障上报命令 packet[3] = fault_type; packet[4] = fault_current >> 8; packet[5] = fault_current & 0xFF; packet[6] = time_stamp >> 8; packet[7] = time_stamp & 0xFF; for (i = 0; i < 8; i++) { crc ^= packet[i]; // 异或校验 } packet[8] = crc; UART_Send_Bytes(packet, 9); }这里用异或校验而不是 CRC-16,是因为帧长度短、实时性要求不高,异或校验足以发现单比特错误,而且 C51 上计算 CRC-16 的耗时对主循环影响较大。设备地址DEVICE_ADDR通常通过拨码开关读取,对应到 C51 是读 P1 口的某几位。如果多个故障指示器挂在同一条 RS485 总线上,地址冲突会导致上报数据互相干扰,这是现场最常见的问题。协议里还应该有一个上位机主动查询的命令字 0x01,用于运维人员手动读取故障记录,而不是只能等设备主动上报。
4.3 串口收发状态机:避免丢包和粘包的处理
串口接收不能用简单的「收到一字节就处理一字节」的方式,因为完整的故障帧有 9 个字节,中间被其他数据打断就会解析错乱。标准做法是用状态机接收,按字节逐个转移状态,直到收满一帧再校验处理。这在 4.2 节代码里没有体现,但在src/usart.c中通常是这么实现的:
// 串口接收状态机 #define FRAME_STATE_IDLE 0 #define FRAME_STATE_HEADER 1 #define FRAME_STATE_BODY 2 #define FRAME_STATE_CRC 3 unsigned char rx_state = FRAME_STATE_IDLE; unsigned char rx_buf[9]; unsigned char rx_index = 0; void UART_ISR(void) interrupt 4 { unsigned char ch; if (RI) { RI = 0; ch = SBUF; switch (rx_state) { case FRAME_STATE_IDLE: if (ch == 0xAA) { rx_buf[0] = ch; rx_index = 1; rx_state = FRAME_STATE_BODY; } break; case FRAME_STATE_BODY: rx_buf[rx_index++] = ch; if (rx_index >= 8) { rx_state = FRAME_STATE_CRC; } break; case FRAME_STATE_CRC: rx_buf[8] = ch; if (Check_Frame_CRC(rx_buf, 8)) { Process_Command(rx_buf); // 解析命令并执行 } rx_state = FRAME_STATE_IDLE; break; } } }状态机的好处是:总线上的噪声字节不会导致误入解析流程,只有收到完整的帧头后才开始积累数据。如果一个帧收到了 8 个数据字节但 CRC 不对,整个帧丢掉并回到 IDLE 状态,等待下一个 0xAA。这样即使总线上有多个设备同时发送造成叠加,也能最大限度地保证帧同步。注意rx_state和rx_index必须定义为全局变量,因为中断函数和主循环都有可能访问它们。如果某个版本里把状态变量放在函数内部定义,中断处理返回后状态就丢了,这个问题排查起来非常隐蔽。
5. 源码改造:无录波功能怎么看波形数据
5.1 缺什么和能补什么:从故障指示器到简易录波
摘要里明确指出这个工程不含录波功能。但实际应用中,运维人员看故障记录时往往想要故障前后的波形。既然main.c里的 AD 采样是周期性的,理论上在故障触发前后各保存 N 个采样点就能实现简易录波。C51 的 RAM 只有 128 字节或 256 字节,直接存波形数据不现实,需要外扩 RAM 或者用串口把原始采样点实时上传给上位机存储。
常见改造方案是:故障触发前,AD 采样值先存入一个环形缓冲区;当故障触发发生时,停止写入并保留缓冲区里最后 N 个点作为故障前波形,然后继续采集 M 个点作为故障后波形。这个「前 N 后 M」的数据加起来约 1~2K 字节,C51 片内放不下,一般放在外部 RAM(如 62256 SRAM)里,故障结束后再通过串口传给上位机。
// 简易录波缓冲:环形缓冲区保存故障前 64 点 #define PRE_FAULT_POINTS 64 #define POST_FAULT_POINTS 128 unsigned int wave_buf[PRE_FAULT_POINTS + POST_FAULT_POINTS]; unsigned char wave_index = 0; unsigned char wave_triggered = 0; void ADC_Sample_For_Wave(void) { unsigned int cur; cur = ADC_Read(); if (!wave_triggered) { wave_buf[wave_index] = cur; wave_index = (wave_index + 1) % PRE_FAULT_POINTS; } else if (wave_index < PRE_FAULT_POINTS + POST_FAULT_POINTS) { wave_buf[wave_index++] = cur; } }这段代码里,故障前数据存在一个环形缓冲区里,wave_index循环覆盖,始终保持最近 64 个点的值。故障触发后,wave_triggered置 1,停止覆盖,继续往后追加 128 个点。等 MSC51 的工作完成之后,上位机通过命令读取这 192 个点,就能还原出故障前后的电流波形。这个方案虽然没有专门的录波仪采样率高,但它利用了已有的 AD 模块,不需要额外硬件,对分析短路电流的大致形状和衰减过程是有帮助的。
5.2 采样率与缓冲区大小的权衡
采样率直接决定故障波形的分辨能力。C51 的 AD 采样率受限于指令周期,一般能做到每毫秒采几次到几十次。如果 20ms 一个工频周波内能采 20 个点,那就是每周波 20 点,能勉强看出正弦波的畸变。要分辨高次谐波,至少需要每周波 64 点以上。这个工程没有外扩高速 AD,所以不要期望它能做谐波分析,它的价值在于观察故障发生时刻电流的突变趋势和衰减过程。
缓冲区大小的选择跟 RAM 有直接关系。C51 用xdata关键字访问外部 RAM,例如:
unsigned int xdata wave_buf[PRE_FAULT_POINTS + POST_FAULT_POINTS];如果不加xdata,编译器默认把wave_buf分配到片内data段,而片内 RAM 总共只有 256 字节,192 个unsigned int直接溢出编译失败。加上xdata后,数据放在外部 RAM,访问速度稍慢,但容量可以到 64K。这个关键字是 C51 区别于标准 C 最需要记住的一个点,很多人移植代码时把变量丢在片内 RAM 导致编译不过,就是忘了区分存储类型。
6. 借助故障录波分析软件验证上报帧的正确性
6.1 用串口助手模拟集中器验证协议
代码写完不能直接上屏,先用 PC 串口工具验证故障上报帧是不是符合协议。把设备的串口 TX/RX 通过 USB 转 TTL 接到电脑,打开串口助手,设置 9600、8、N、1,然后把设备上的测试按钮按下强制触发一次故障记录。正常情况下,串口助手里应当收到一帧 9 字节数据,例如AA 01 02 01 00 64 00 0A 68。这帧的含义是:帧头 AA,地址 01,命令 02(故障上报),类型 01(过流),电流 0x0064(100A),时间戳 0x000A(10分钟),校验 0x68。
如果收到的数据不对,先从波特率查起。用示波器量 TX 引脚的电平宽度,1 位的时间是 104us 左右(9600bps),如果偏差超过 5%,就是 TH1 重装值或者晶振频率的问题。然后查帧的 CRC 校验,把前 8 字节做异或,结果应该等于第 9 字节。如果 CRC 不对,多半是发送函数里把校验字节也算进去了,导致递归错误。
6.2 上位机波形还原的矩阵:如何从故障指示器数据拼出录波效果
故障指示器本身不带录波,但集中器或后台系统可以收集多台故障指示器的上报数据,配合故障录波分析软件拼出一个简化版的故障过程。思路是:故障发生时,同一线路上的多个指示器各上报一个电流值和动作时间,后台按时间线把这些离散点排列出来,就能大致看出故障电流沿线路的传播和衰减趋势。相比独立录波仪,这种方式胜在布点密度高、成本低,适合配电网这种分支多、故障点分散的场景。
具体到代码验证这一步,可以在发送函数里加一个测试帧标志,让设备周期性地发送0x01命令的实时电流值。上位机把这些数据存储为文本,每行一个值,再用 MATLAB 或 Python 的 matplotlib 画出来,就是一条连续的电流趋势线。这个方法能间接验证设备的 AD 采样是否稳定,以及采样值是否随真实电流变化。对没有原厂测试台的开发者来说,这是最快验证硬件链路是否通畅的手段。
关于故障录波分析软件选型,常见的做法是先用开源或厂家提供的调试助手确认物理层和协议层无误,再用支持 IEC 61850 或 Comtrade 格式的录波分析工具来导入波形数据。但要注意,故障指示器输出的是极简协议帧,不是标准 Comtrade 文件,所以导入之前必须写一个转换脚本,把设备上报的十六进制帧转换成c的a值对。这个转换逻辑你可以放在后台的上位机程序里,不要指望设备端能做这件事,C51 的资源做不了这种文件级转换。
本文还有配套的精品资源,点击获取