基于PESdk和EasyModbus实现录波控制逻辑和数据传输

2023-11-02 03:36

本文主要是介绍基于PESdk和EasyModbus实现录波控制逻辑和数据传输,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 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,支持数据的传输和录波控制逻辑的实现。
和录波相关的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描述
SampleInterval0x00002RO采样点间隔(us)
SerialNum0x0000~0x000816RO序列号,占16个字节,不足的补0
Version0x0009~0x000E12RO版本号
ChannelCount0x000F2RO低8位:最大的手动录波通道数;高8位:最大的故障录波通道数
PostFault0x00102RO故障发生后继续缓存的数据个数
RSV0x0011~0x00136RO预留
3.2.2 手动录波寄存器
名称地址字节R/W描述
Start0x00142RO启动录波控制,可由上位机触发或故障自动触发启动录波
Ready0x00152RO录波就绪状态,0-录波未完成1-录波完成
TimeStamp0x0016~0x00198RO时间戳
ValidChannel0x001A2RO实际使用的录波通道数
DataCount0x001B2RO每个通道的数据个数
DataAddr_0~DataAddr_150x001C~0x002B32RW录波通道对应的数据的寄存器地址,对应于用公共功能码读写的寄存器地址,默认最多支持16个通道,应用程序可根据存储空间的大小确定使用的通道数
RSV0x002C~0x002F8RO预留
3.2.3 故障录波寄存器
名称地址字节R/W描述
Start0x00302RO启动录波控制,可由上位机触发或故障自动触发启动录波
Ready0x00312RO录波就绪状态,0-录波未完成1-录波完成
TimeStamp0x0032~0x00358RO时间戳
ValidChannel0x00362RO实际使用的录波通道数
DataCount0x00372RO每个通道的数据个数
DataAddr_0~DataAddr_150x0038~0x005764RW录波通道对应的数据的寄存器地址,对应于用公共功能码读写的寄存器地址,默认最多支持32个通道,应用程序可根据存储空间的大小确定使用的通道数
Additional Information0x0058~0x006732RO故障录波的附加信息,由上层应用定义
FaultPoint0x00682RO故障时的数据下标

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));Smemcpy(&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路臻品,欢迎关注

这篇关于基于PESdk和EasyModbus实现录波控制逻辑和数据传输的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/327824

相关文章

使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)

《使用Java解析JSON数据并提取特定字段的实现步骤(以提取mailNo为例)》在现代软件开发中,处理JSON数据是一项非常常见的任务,无论是从API接口获取数据,还是将数据存储为JSON格式,解析... 目录1. 背景介绍1.1 jsON简介1.2 实际案例2. 准备工作2.1 环境搭建2.1.1 添加

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

java如何分布式锁实现和选型

《java如何分布式锁实现和选型》文章介绍了分布式锁的重要性以及在分布式系统中常见的问题和需求,它详细阐述了如何使用分布式锁来确保数据的一致性和系统的高可用性,文章还提供了基于数据库、Redis和Zo... 目录引言:分布式锁的重要性与分布式系统中的常见问题和需求分布式锁的重要性分布式系统中常见的问题和需求

SpringBoot基于MyBatis-Plus实现Lambda Query查询的示例代码

《SpringBoot基于MyBatis-Plus实现LambdaQuery查询的示例代码》MyBatis-Plus是MyBatis的增强工具,简化了数据库操作,并提高了开发效率,它提供了多种查询方... 目录引言基础环境配置依赖配置(Maven)application.yml 配置表结构设计demo_st

python使用watchdog实现文件资源监控

《python使用watchdog实现文件资源监控》watchdog支持跨平台文件资源监控,可以检测指定文件夹下文件及文件夹变动,下面我们来看看Python如何使用watchdog实现文件资源监控吧... python文件监控库watchdogs简介随着Python在各种应用领域中的广泛使用,其生态环境也

el-select下拉选择缓存的实现

《el-select下拉选择缓存的实现》本文主要介绍了在使用el-select实现下拉选择缓存时遇到的问题及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的... 目录项目场景:问题描述解决方案:项目场景:从左侧列表中选取字段填入右侧下拉多选框,用户可以对右侧

Python pyinstaller实现图形化打包工具

《Pythonpyinstaller实现图形化打包工具》:本文主要介绍一个使用PythonPYQT5制作的关于pyinstaller打包工具,代替传统的cmd黑窗口模式打包页面,实现更快捷方便的... 目录1.简介2.运行效果3.相关源码1.简介一个使用python PYQT5制作的关于pyinstall

使用Python实现大文件切片上传及断点续传的方法

《使用Python实现大文件切片上传及断点续传的方法》本文介绍了使用Python实现大文件切片上传及断点续传的方法,包括功能模块划分(获取上传文件接口状态、临时文件夹状态信息、切片上传、切片合并)、整... 目录概要整体架构流程技术细节获取上传文件状态接口获取临时文件夹状态信息接口切片上传功能文件合并功能小

python实现自动登录12306自动抢票功能

《python实现自动登录12306自动抢票功能》随着互联网技术的发展,越来越多的人选择通过网络平台购票,特别是在中国,12306作为官方火车票预订平台,承担了巨大的访问量,对于热门线路或者节假日出行... 目录一、遇到的问题?二、改进三、进阶–展望总结一、遇到的问题?1.url-正确的表头:就是首先ur

C#实现文件读写到SQLite数据库

《C#实现文件读写到SQLite数据库》这篇文章主要为大家详细介绍了使用C#将文件读写到SQLite数据库的几种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下... 目录1. 使用 BLOB 存储文件2. 存储文件路径3. 分块存储文件《文件读写到SQLite数据库China编程的方法》博客中,介绍了文