文章目录
- 0. 概要
- 1. 录波功能简介
- 1.1 功能框架
- 1.2 录波控制逻辑
- 1.3 手动录波数据传输流程
- 1.4 故障录波传输流程
- 2 C语言应用程序接口(API)
- 2.1 EasyModbus接口
- 2.2 PESdk
- 3 录波功能的实现
- 3.1 功能码定义
- 3.1.1 公共功能码
- 3.1.2 用户自定义功能码
- 3.1.3 保留功能码
- 3.2 录波功能寄存器定义
- 3.2.1 公共寄存器
- 3.2.2 手动录波寄存器
- 3.2.3 故障录波寄存器
- 3.3 示例代码
- 3.3.1 Modbus初始化和回调函数
- 3.3.2 录波器初始化和回调函数
- 3.3.3. 录波器API调用和数据填充
- 4 录波数据展示
0. 概要
本文介绍C语言库PESdk和EasyModbus在没有文件系统的嵌入式设备上实现手动录波、故障录波数据存储的功能,配合上位机软件DeMate进行录波数据的传输、展示,将录波数据保存到Comtrade文件中。
PESdk是可用于电力电子设备控制开发的C语言库,对一些比较通用的功能进行抽象和开发,手动录波、故障录波是其中的一部分功能。EasyModbus支持RTU和TCP/IP,实现了Modbus协议的公共功能码,扩展了功能码对录波数据和工控数据寄存器进行隔离,扩大可用于录波数据存储的寄存器地址空间。PESdk和EasyModbus不依赖于具体的硬件或操作系统。
1. 录波功能简介
1.1 功能框架
整个系统和录波相关的功能架构如下图。
DeMate是上位机,运行于Windows操作系统,DSP或ARM是下位机,EasyModbu和PESdk向上提供通信和录波的基础功能。
- DeMate
配置手动录波通道、数据收发、波形展示、故障录波文件的保存和加载,响应用户手动录波指令,配置下位机手动录波通道,启动录波并自动读取录波数据,自动读取故障录波数据; - 应用程序
串口设备的初始化、数据转发、填充录波数据; - EasyModbus
接收Modbus RTU数据帧,读写寄存器,响应上位机的请示; - PESdk
录波初始化,录波控制逻辑;
1.2 录波控制逻辑
启动手动录波器时,从数据缓存首地址开始向后填充数据,数据填满时置完成标志位,等待上位机来读数据。
- 手动录波控制逻辑
故障录波器在系统启动的时候就开始缓存数据,故障发生时记录当前时刻的数据缓存位置,继续缓存由寄存器PostFault指定的数据个数后停止录波,等待上位机读取数据。如果读取数据之前系统故障消除,则故障录波器自动启动,之前的缓存将会被新数据覆盖。
故障录波器在系统启动的时候就开始缓存数据,故障发生时记录当前时刻的数据缓存位置,继续缓存由寄存器PostFault指定的数据个数后停止录波,等待上位机读取数据。如果读取数据之前系统故障消除,则故障录波器自动启动,之前的缓存将会被新数据覆盖。
1.3 手动录波数据传输流程
手动录波由用户发起,DeMate按照协议和流程给下位机发送手动录波指令、录波通道对应的modbus点表地址,经过一定的延时后读取录波器状态位,决定是否要发起读取数据的流程,数据读取完毕后以曲线的形式向用户展示,用户可以将录波数据保存到本地的Comtrade文件中。
1.4 故障录波传输流程
在正常运行的情况下,PESdk的故障录波控制逻辑在应用程序的回调函数中循环刷新故障数据缓存,当应用程序检测到故障时,PESdk自动计算停止录波的时刻,以满足存储故障前、后数据点数的要求。
假设在t8时刻发生了故障,在t10时刻停止录波,上位机从t4开始读取数据,读取数据完成后需要做数据移位处理,才能从t1~t10依次保存或展示数据。
上位机在后台线程中不间断地读取寄存器状态,当录波标志位置位时自动发起读故障的流程,读故障录波数据的时序图如下。
2 C语言应用程序接口(API)
EasyModbus和PESdk提供一系列API,支持数据的传输和录波控制逻辑的实现。
2.1 EasyModbus接口
接口 | 描述 |
---|---|
modbus_rtu_init | 初始化一个modbus结构体变量,指定数据接收完成、校验失败、发送数据的回调,等。 |
modbus_rtu_push | 接收modbus数据,内部做了帧长度合法性校验,接收完成后进入回调函数进行处理 |
modbus_rtu_reset | 应用程序处理完modbus数据帧后复位结构体的相关变量,准备接收下一帧 |
modbus_rtu_response | 从机的响应处理,在内部进行数据的拷贝和组包,如果在modbus_rtu_init中指定了发送数据的回调函数,则自己调用回调函数进行发送 |
mbtable_get_item_value | 从Modbus点表中读取数据项的值 |
mbtable_set_item_value | 设置Modbus点表某个数据项的值,内部有数据超上、下限的校验 |
mbtable_get_item_index | 获取数据项在Modbus点表中的下标 |
mbtable_reset_item_to_default | 设置Modbus点表中某个数据项为默认值 |
mbtable_reset_all_to_default | 设置Modbus点表中所有数据项为默认值 |
2.2 PESdk
接口 | 描述 |
---|---|
recorder_get_profile | 读录波寄存器 |
recorder_set_profile | 写录波寄存器 |
recorder_reset_fault | 复位故障录波相关寄存器 |
recorder_reset_manual | 复位手动录波相关寄存器 |
recorder_fault_record | 故障录波入口 |
recorder_manual_record | 手动录波入口 |
3 录波功能的实现
3.1 功能码定义
Modbus功能码占用一个字节,取值范围是 1~127(即 0x01~0x7F)。同时,使用功能码+0x80 表示异常状态,即129~255 代表异常码。功能码可分为位操作和字操作两类,位操作的最小单位为1位(bit),字操作的最小单位为两个字节。
在 Modbus 标准协议中,一共规定了三类Modbus功能码:公共功能码、用户自定义功能码和保留功能码。
3.1.1 公共功能码
公共功能码有如下特点:(1)被明确定义的功能码;(2)保证唯一性;(3)由 Modbus 协会确认,并提供公开的文档;(4)可进行一致性测试;(5)包括协议定义的功能码和保留将来使用的功能码。
常用的部分公共功能码见下表。
代码 | 名称 | 地址 | 位/字操作 | 数量 |
---|---|---|---|---|
0x01 | 读线圈状态 | 00001~09999 | 位 | 单个/多个 |
0x02 | 读离散输入状态 | 10001~19999 | 位 | 单个/多个 |
0x03 | 读保持寄存器 | 40001~49999 | 字 | 单个/多个 |
0x04 | 读输入寄存器 | 30001~39999 | 字 | 单个/多个 |
0x05 | 写单个线圈 | 00001~09999 | 位 | 单个 |
0x06 | 写单个保持寄存器 | 40001~49999 | 字 | 单个 |
0x0F | 写多个线圈 | 00001~09999 | 位 | 多个 |
0x10 | 写多个保持寄存器 | 40001~49999 | 字 | 多个 |
3.1.2 用户自定义功能码
Modbus有两个用户自定义功能码区域,分别是 65~72 和 100~110,用于录波的扩展功能码如下表。
代码 | 名称 | 地址 | 位/字操作 | 数量 |
---|---|---|---|---|
0x41 | 读录波设置 | 0x0000~0x0068 | 字 | 多个 |
0x42 | 写录波设置 | 0x0000~0x0068 | 字 | 多个 |
0x43 | 读保持寄存器 | 0x0000~0xFFFF | 字 | 多个 |
0x44 | 读输入寄存器 | 0x0000~0xFFFF | 字 | 多个 |
上表中的寄存器地址有重叠的部分,由上层应用程序通过功能码进行隔离,并不会冲突。
3.1.3 保留功能码
保留功能码是因为历史遗留原因,某些公司的传统产品上现行使用的功能码不作为公共使用。
3.2 录波功能寄存器定义
3.2.1 公共寄存器
名称 | 地址 | 字节 | R/W | 描述 |
---|---|---|---|---|
SampleInterval | 0x0000 | 2 | RO | 采样点间隔(us) |
SerialNum | 0x0000~0x0008 | 16 | RO | 序列号,占16个字节,不足的补0 |
Version | 0x0009~0x000E | 12 | RO | 版本号 |
ChannelCount | 0x000F | 2 | RO | 低8位:最大的手动录波通道数;高8位:最大的故障录波通道数 |
PostFault | 0x0010 | 2 | RO | 故障发生后继续缓存的数据个数 |
RSV | 0x0011~0x0013 | 6 | RO | 预留 |
3.2.2 手动录波寄存器
名称 | 地址 | 字节 | R/W | 描述 |
---|---|---|---|---|
Start | 0x0014 | 2 | RO | 启动录波控制,可由上位机触发或故障自动触发启动录波 |
Ready | 0x0015 | 2 | RO | 录波就绪状态,0-录波未完成1-录波完成 |
TimeStamp | 0x0016~0x0019 | 8 | RO | 时间戳 |
ValidChannel | 0x001A | 2 | RO | 实际使用的录波通道数 |
DataCount | 0x001B | 2 | RO | 每个通道的数据个数 |
DataAddr_0~DataAddr_15 | 0x001C~0x002B | 32 | RW | 录波通道对应的数据的寄存器地址,对应于用公共功能码读写的寄存器地址,默认最多支持16个通道,应用程序可根据存储空间的大小确定使用的通道数 |
RSV | 0x002C~0x002F | 8 | RO | 预留 |
3.2.3 故障录波寄存器
名称 | 地址 | 字节 | R/W | 描述 |
---|---|---|---|---|
Start | 0x0030 | 2 | RO | 启动录波控制,可由上位机触发或故障自动触发启动录波 |
Ready | 0x0031 | 2 | RO | 录波就绪状态,0-录波未完成1-录波完成 |
TimeStamp | 0x0032~0x0035 | 8 | RO | 时间戳 |
ValidChannel | 0x0036 | 2 | RO | 实际使用的录波通道数 |
DataCount | 0x0037 | 2 | RO | 每个通道的数据个数 |
DataAddr_0~DataAddr_15 | 0x0038~0x0057 | 64 | RW | 录波通道对应的数据的寄存器地址,对应于用公共功能码读写的寄存器地址,默认最多支持32个通道,应用程序可根据存储空间的大小确定使用的通道数 |
Additional Information | 0x0058~0x0067 | 32 | RO | 故障录波的附加信息,由上层应用定义 |
FaultPoint | 0x0068 | 2 | RO | 故障时的数据下标 |
3.3 示例代码
3.3.1 Modbus初始化和回调函数
// 需要包含两个头文件
#include "pesdk.h"
#include "modbusrtu.h"
// 初始化Modbus数据点表
const struct modbus_table_item_t mb_table_items[] =
{
// {寄存器地址,指向实际的变量指针,{最小值,最大值,默认值},格式}
{RW_ITEM_START + 0, &mydevice.command.start_pwm, {0, 1, 1}, TDF_U16_RW},
{RW_ITEM_START + 1, &mydevice.command.reset, {0, 1, 2}, TDF_U16_RW},
{RW_ITEM_START + 2, &mydevice.command.save_param, {0, 1, 3}, TDF_U16_RW},
{RW_ITEM_START + 3, &mydevice.command.load_default_param, {0, 1, 0}, TDF_U16_RW},
{RO_ITEM_START + 0, &mydevice.data.rms.ugrid.a, {0, 10000, 0}, TDF_FLT_RO},
{RO_ITEM_START + 2, &mydevice.data.rms.ugrid.b, {0, 10000, 0}, TDF_FLT_RO},
{RO_ITEM_START + 4, &mydevice.data.rms.ugrid.c, {0, 10000, 0}, TDF_FLT_RO},
{RO_ITEM_START + 6, &mydevice.data.rms.ugridline.ab, {0, 1, 0}, TDF_FLT_RO},
{RO_ITEM_START + 8, &mydevice.data.rms.ugridline.bc, {0, 1, 0}, TDF_FLT_RO},
{RO_ITEM_START + 10, &mydevice.data.rms.ugridline.ca, {0, 1, 0}, TDF_FLT_RO},
...
};
void init_system(void)
{
// ...
// modbus rtu结构体变量,ID,本机地址,客户端,接收完成回调,错误回调,发送数据回调
modbus_rtu_init(&mbhost, 0, 0x01, MODBUS_INST_TYPE_CLIENT, modbus_frame_recv, modbus_frame_error, &write_scia_bytes);
// ...
recorder_init();
// ...
}
// 接收数据帧完成的回调
void modbus_frame_recv(struct modbus_rtu_inst_t *inst, char *data, int len)
{
switch(MODBUS_RTU_REG_CMD(inst))
{
case MODBUS_CMD_RD_REGS: // 0x03功能码
modbus_resp_cmd03(inst);
break;
case MODBUS_CMD_WR_MULT_REGS: // 0x10功能码
modbus_resp_cmd10(inst);
break;
case MODBUS_CMD_RD_RECORD_PROFILE: // 读录波寄存器
modbus_resp_read_record_profile(inst);
break;
case MODBUS_CMD_WR_RECORD_PROFILE: // 写录波寄存器
modbus_resp_write_record_profile(inst);
break;
case MODBUS_CMD_RD_FAULT_RECORD_DATA: // 读故障录皮数据
modbus_resp_read_record_data(inst, &fault_record_buff[0][0]);
break;
case MODBUS_CMD_RD_MANUAL_RECORD_DATA: // 读手动录波数据
modbus_resp_read_record_data(inst, &manual_record_buff[0][0]);
break;
default:
break;
}
modbus_rtu_reset(inst); // 复位相关变量,准备接收新数据
return;
}
// 接收错误数据帧的回调
void modbus_frame_error(struct modbus_rtu_inst_t *inst)
{
if(inst->frame_handler == NULL)
{
return;
}
inst->frame_handler(inst, inst->data, inst->framelen);
return;
}
// 串口接收回调
void serial_data_handler(char data)
{
modbus_rtu_push(&mbhost, &data, 1); // 接收modbus rtu数据
}
3.3.2 录波器初始化和回调函数
主要完成录波器信息寄存器、录波通道对应的数据指针的初始化,故障录波通道一般固定,而手动录波通道可通过上位机配置。
void recorder_init(void)
{
memset(&recorder_info, 0, sizeof(recorder_info_t));
memset(&manual_recorder, 0, sizeof(struct record_manual_t));
memset(&fault_recorder, 0, sizeof(struct record_fault_t));S
memcpy(&recorder_info.serial, deviceserial, sizeof(deviceserial) > RECORDER_MAX_SERIAL_LEN?RECORDER_MAX_SERIAL_LEN:sizeof(deviceserial));
recorder_info.sample_interval = 1000000/(RATE_GRID_FREQ*SAMPLE_PER_CYCLE);
recorder_info.version[0] = VERSION_MAJOR;
recorder_info.version[1] = VERSION_MINOR;
recorder_info.version[2] = VERSION_REV;
recorder_info.version[3] = VERSION_BUILD;
recorder_info.postfault = RECORDER_POST_FAULT_COUNT;
manual_recorder.common.start = 0; // 不启动录波
manual_recorder.common.ready = 0; // 录波完成状态位清零
manual_recorder.common.timestamp = 0; // 时间戳清零
manual_recorder.common.datacount = RECORDER_BUFF_LEN; // 每个手动录波通道的数据长度
manual_recorder.common.validchannel = sizeof(manual_record_buff)/(sizeof(float)*RECORDER_BUFF_LEN); // 实际起用的故障录波通道数
for(int i = 0; i < RECORDER_MAX_MANUAL_CHANNEL_COUNT; i++)
{
manual_recorder.regaddr[i] = 3 + RO_ITEM_START + 2*i; // 初始化手动录波数据地址
}
manual_recorder.buff = manual_record_buff; // 手动录波数据的缓存
manual_recorder.push_data_callback = record_manual; // 填充录波数据的回调
fault_recorder.common.start = 0; // 不启动录波
fault_recorder.common.ready = 0; // 录波完成状态位清零
fault_recorder.common.timestamp = 0; // 时间戳清零
fault_recorder.common.datacount = RECORDER_BUFF_LEN; // 每个故障录波通道的数据长度
fault_recorder.common.validchannel = sizeof(fault_record_buff)/(sizeof(float)*RECORDER_BUFF_LEN); // 实际起用的故障录波通道数
for(int i = 0; i < RECORDER_MAX_FAULT_CHANNEL_COUNT; i++)
{
fault_recorder.regaddr[i] = 3 + RO_ITEM_START + 2*i; // 初始化故障录波数据地址
}
fault_recorder.buff = fault_record_buff; // 故障录波数据的缓存
fault_recorder.push_data_callback = record_fault; // 填充录波数据的回调
return;
}
3.3.3. 录波器API调用和数据填充
在PWM或定时器中断中调用PESdk的录波API,PESdk内部做录波逻辑控制,在应用程序的回调函数中填充录波数据。
// 计算ADC数据
void calc_data(void)
{
// ...
#ifdef TEST_RECORDER // 生成模拟数据
static int index = 0;
mydevice.data.inst.ugridline.ab = 100*sin(2*index*PI/SAMPLE_PER_CYCLE);
mydevice.data.inst.ugridline.bc = 100*sin(2*index*PI/SAMPLE_PER_CYCLE - 2.0*PI/3.0) + 110;
mydevice.data.inst.ugridline.ca = 100*sin(2*index*PI/SAMPLE_PER_CYCLE + 2.0*PI/3.0) + 220;
index++;
if(index >= SAMPLE_PER_CYCLE)
{
index = 0;
}
#endif
// ...
}
// 填充故障数据的回调
void record_fault(struct record_fault_t *recorder)
{
struct modbus_table_item_t *pitem = mbtable_get_item_by_addr(recorder->regaddr[0]);
memcpy(&fault_record_buff[0][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
pitem = mbtable_get_item_by_addr(recorder->regaddr[1]);
memcpy(&fault_record_buff[1][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
pitem = mbtable_get_item_by_addr(recorder->regaddr[2]);
memcpy(&fault_record_buff[2][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
// ...
// 填充一个异常数据
if(postfaultcounter == RECORDER_POST_FAULT_COUNT)
{
fault_record_buff[0][recorder->index] = 11;
fault_record_buff[1][recorder->index] = 12;
fault_record_buff[2][recorder->index] = 13;
}
}
// 填充手动录波数据的回调
void record_manual(struct record_manual_t *recoder)
{
struct modbus_table_item_t *pitem = mbtable_get_item_by_addr(recorder->regaddr[0]);
memcpy(&manual_record_buff[0][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
pitem = mbtable_get_item_by_addr(recorder->regaddr[1]);
memcpy(&manual_record_buff[1][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
pitem = mbtable_get_item_by_addr(recorder->regaddr[2]);
memcpy(&manual_record_buff[2][recoder->index], pitem->pointer, pitem->format.bits.regcount << 1)
// ...
}
// 读ADC数据
void read_adc(void)
{
mydevice.data.raw.ugrid.ab = (int16)(AdcRegs.ADCRESULT0 >> 4);
mydevice.data.raw.ugrid.bc = (int16)(AdcRegs.ADCRESULT1 >> 4);
mydevice.data.raw.ugrid.ca = (int16)(AdcRegs.ADCRESULT2 >> 4);
...
}
// 有PWM中断中调用录波器
__interrupt void epwm_isr(void)
{
// 启动ADC...
// ...
read_adc(); // 读ADC数据
calc_data(); // 计算模拟量
// ...
#ifdef TEST_RECORDER
// 调用PESdk的录波API,sysfault为故障标志位
recorder_fault_record(&fault_recorder, recorder_info.postfault, sysfault);
recorder_manual_record(&manual_recorder);
#endif
}
4 录波数据展示
在示例代码中模拟发生了一个故障,在故障数据填充回调函数中写入一个异常数据,DeMate读到数据后做数据对齐,将数据保存为Comtrade文件,向用户展示了正确的故障录波数据。
后续继续升级PESdk,增加传输存储在flash中的故障录波文件列表和数据、固件升级等功能。
原文发表于个人公众号:E路臻品,欢迎关注