358 lines
15 KiB
C
358 lines
15 KiB
C
/******************************************************************************
|
||
* @file MODBUS.c
|
||
* @brief Modbus RTU协议通信处理模块
|
||
* @details 本文件实现了基于Modbus RTU协议的RS485通信功能,包括:
|
||
* - Modbus命令帧的构建和发送
|
||
* - 接收数据的解析和处理
|
||
* - 实时数据轮询机制
|
||
* - 屏幕刷新命令处理
|
||
* - 保护定值、控制字等数据的更新
|
||
* @author 阜阳师范大学物电学院
|
||
* @version V0.01
|
||
* @date 2026-01-23
|
||
* @note 通信协议:Modbus RTU
|
||
* 通信接口:RS485
|
||
* 主站地址:0x01
|
||
******************************************************************************/
|
||
#include "MODBUS.h"
|
||
#include <string.h>
|
||
#include "rs485.h"
|
||
#include "CRC16.h"
|
||
#include "led.h"
|
||
#include "160160D.h"
|
||
#include "filehandle.h"
|
||
/* ============================================================================
|
||
* 命令类型宏定义
|
||
* ============================================================================ */
|
||
#define RT_CMD (1) /**< 实时数据刷新命令类型 */
|
||
#define KEY_CMD (0) /**< 按键事件命令类型 */
|
||
|
||
/* ============================================================================
|
||
* 图片宏定义
|
||
* ============================================================================ */
|
||
#define BMP_ADDR (0x0803F000) /**< BMP数据在Flash中的存储地址(扇区126) */
|
||
#define BMP_LEN (3208) /**< BMP数据长度,单位:字节 */
|
||
|
||
|
||
typedef struct
|
||
{
|
||
uint8_t bmpdata[3200];
|
||
uint32_t biWidth;
|
||
uint32_t biHeight;
|
||
} logo_type;
|
||
|
||
typedef struct
|
||
{
|
||
volatile uint8_t RT_count; /* 实时命令计数器 */
|
||
volatile uint16_t CMD_ACK; /* 普通命令接收完成标志位 */
|
||
volatile uint8_t ACK_COUNT_ABLE; //命令发出后计时使能
|
||
volatile uint8_t ACK_count; /* 应答计时计数器 */
|
||
volatile uint8_t ACK_OverTime; /* 超时标志位 */
|
||
volatile uint8_t ACK_OverTimeCnt; /* 超时次数技术器 */
|
||
} RS485_POLL_TYPE;
|
||
|
||
RS485_POLL_TYPE RS485POLL = {0};
|
||
|
||
#define RS485_RT_COUNT_MAX (55) //轮询限值 275ms
|
||
#define RS485_ACK_COUNT_MAX (40) //应答超时限值 200ms
|
||
#define RS485_ACK_OVERTIME_MAX (5) // 75ms 应答超时计数最大次数,超过该次数显示通讯异常
|
||
|
||
|
||
|
||
/* ============================================================================
|
||
* 私有变量定义
|
||
* ============================================================================ */
|
||
static logo_type logo; /**< LOGO图片数据结构体,包含BMP图片数据和尺寸信息 */
|
||
static uint8_t Picture[3200];
|
||
/* ============================================================================
|
||
* MODBUS协议处理部分
|
||
* ============================================================================ */
|
||
/**
|
||
* @brief 发送屏幕刷新命令(特殊格式的Modbus命令)
|
||
* @param func 功能码(通常为READ_RIGISTER = 0x03)
|
||
* @param type 命令类型:RT_CMD(1)=实时刷新,KEY_CMD(0)=按键事件
|
||
* @param key 按键值(当type=KEY_CMD时有效)
|
||
* @note 命令帧格式(6字节):
|
||
* [0] = 从站地址(Master_Address = 0x01)
|
||
* [1] = 功能码(func)
|
||
* [2] = 命令类型(type)
|
||
* [3] = 按键值(key)
|
||
* [4] = CRC校验高字节
|
||
* [5] = CRC校验低字节
|
||
* CRC校验范围:前4字节
|
||
* 发送后重置命令应答标志
|
||
* @retval 无
|
||
*/
|
||
void RS485_RefreshScreenCMD(uint8_t func, uint8_t type, uint8_t key)
|
||
{
|
||
uint16_t crc = 0;
|
||
uint8_t cmdbuf[6];
|
||
cmdbuf[0] = 0x01; /* 从站地址 */
|
||
cmdbuf[1] = func; /* 功能码 */
|
||
cmdbuf[2] = type; /* 命令类型 */
|
||
cmdbuf[3] = key; /* 按键值 */
|
||
crc = Calculate_CRC(&cmdbuf[0],4); /* 计算CRC校验(前4字节) */
|
||
cmdbuf[4] = crc >> 8; /* CRC校验高字节 */
|
||
cmdbuf[5] = (uint8_t) crc; /* CRC校验低字节 */
|
||
|
||
RS485_SendBuff(cmdbuf, 6); /* 发送6字节命令帧 */
|
||
RS485POLL.CMD_ACK = RESET; /* 重置命令应答标志 */
|
||
}
|
||
|
||
|
||
///* ============================================================================
|
||
// * 屏幕刷新处理 远程处理图片
|
||
// * ============================================================================ */
|
||
/**
|
||
* @brief 发送屏幕刷新命令
|
||
* @param type 命令类型:RT_CMD(1)=实时刷新,KEY_CMD(0)=按键事件
|
||
* @param key 按键值(当type=KEY_CMD时有效)
|
||
* @note 发送屏幕刷新命令到138端,请求更新屏幕显示
|
||
* 命令发送后:
|
||
* - 重置命令应答标志
|
||
* - 重置实时数据计数
|
||
* - 使能应答计数
|
||
* - 重置应答计数值
|
||
* @retval 无
|
||
*/
|
||
void RefreshScreen(uint8_t type, uint8_t key)
|
||
{
|
||
RS485_RefreshScreenCMD(0x03, type, key); /* 发送刷新屏幕命令 */
|
||
|
||
RS485POLL.CMD_ACK = RESET; /* 重置命令响应标志 */
|
||
RS485POLL.RT_count = 0; /* 重置实时召唤计数 */
|
||
|
||
RS485POLL.ACK_COUNT_ABLE = SET; /* 使能应答计数 */
|
||
RS485POLL.ACK_count = 0; /* 重置应答计数值 */
|
||
}
|
||
/**
|
||
* @brief 在LCD屏幕上显示LOGO图片
|
||
* @note 调用Display_BMP函数将全局变量logo中的BMP图片数据显示在LCD屏幕上
|
||
* 显示位置:屏幕居中(由Display_BMP函数自动计算)
|
||
* 图片尺寸:使用logo结构体中的biWidth和biHeight
|
||
* 图片数据:使用logo结构体中的bmpdata数组
|
||
* 注意:显示前需要确保logo数据已通过BMP_Get()从Flash读取或已初始化
|
||
* @retval 无
|
||
*/
|
||
void LOGO_Printf(void)
|
||
{
|
||
/* 调用Display_BMP函数显示LOGO图片 */
|
||
/* 参数:宽度、高度、图片数据指针 */
|
||
Display_BMP(logo.biWidth, logo.biHeight, (const uint8_t*)logo.bmpdata);
|
||
}
|
||
/**
|
||
* @brief 保存LOGO的BMP图片信息到Flash
|
||
* @note 将全局变量logo中的BMP图片数据保存到Flash的指定地址(BMP_ADDR)
|
||
* 保存的数据包括:
|
||
* - BMP图片数据(bmpdata数组,3200字节)
|
||
* - 图片宽度(biWidth)
|
||
* - 图片高度(biHeight)
|
||
* 注意:保存前Flash相关扇区会被自动擦除
|
||
* 保存地址:0x0803F000(扇区126)
|
||
* @retval 无
|
||
*/
|
||
void BMP_SAVE2False(void)
|
||
{
|
||
/* 调用Flash写入函数,将logo结构体数据写入Flash */
|
||
/* sizeof(logo)/2 将字节数转换为半字(16位)个数 */
|
||
FLASH_WriteMoreData(BMP_ADDR, (uint16_t*)(&logo), (sizeof(logo)/2));
|
||
}
|
||
/**
|
||
* @brief 从Flash中读取LOGO的BMP图片数据
|
||
* @note 从Flash的指定地址(BMP_ADDR)读取BMP图片数据到全局变量logo中
|
||
* 读取的数据包括:
|
||
* - BMP图片数据(bmpdata数组,3200字节)
|
||
* - 图片宽度(biWidth)
|
||
* - 图片高度(biHeight)
|
||
* 读取地址:0x0803F000(扇区126)
|
||
* 注意:使用volatile指针确保从Flash直接读取,避免缓存问题
|
||
* @retval 无
|
||
*/
|
||
void BMP_Get(void)
|
||
{
|
||
uint32_t i = 0;
|
||
/* 将Flash地址转换为 volatile 指针,确保直接从Flash读取数据 */
|
||
volatile logo_type* ptr = (volatile logo_type*)BMP_ADDR;
|
||
|
||
/* 循环读取BMP图片数据(3200字节) */
|
||
for(i = 0; i < 3200; i++)
|
||
{
|
||
logo.bmpdata[i] = ptr->bmpdata[i];
|
||
}
|
||
/* 读取图片宽度 */
|
||
logo.biWidth = ptr->biWidth;
|
||
/* 读取图片高度 */
|
||
logo.biHeight = ptr->biHeight;
|
||
}
|
||
|
||
void NL_LOGO_Printf(void)
|
||
{
|
||
BMP_Get();
|
||
LOGO_Printf();
|
||
delay_ms(500);
|
||
delay_ms(500);
|
||
delay_ms(500);
|
||
delay_ms(500);
|
||
}
|
||
/**
|
||
* @brief Modbus通信主处理函数-远程处理图片
|
||
* @param key 按键值(KEY_TYPE类型)
|
||
* @note 采用138端处理图片,整体传上来显示的方式刷图
|
||
* 处理流程:
|
||
* 1. 按键处理:
|
||
* - 保存按键值到静态变量key_bk
|
||
* - 等待可发送状态时发送按键命令
|
||
* 2. 接收新消息处理:
|
||
* - 设置连接标志
|
||
* - CRC校验通过:
|
||
* * 提取图片数据(3200字节)和命令信息(8字节)
|
||
* * 判断命令类型:
|
||
* - 0x89 0x45:LOGO图片,保存并显示
|
||
* - 其他:普通图片,显示图片和LED状态
|
||
* - CRC校验失败:
|
||
* * 累计错误计数
|
||
* * 连续3次错误后重启DMA接收
|
||
* - 清除控制字和应答相关标志
|
||
* 3. 定时刷新处理:
|
||
* - 实时数据计数超限时发送实时刷新命令
|
||
* 4. 应答超时处理:
|
||
* - 应答计数超限时设置超时标志
|
||
* - 连续超时次数达到上限时重启DMA接收
|
||
* 数据帧长度:
|
||
* 3000 8
|
||
* └──────┬──────┘ └─┬─┘
|
||
* Picture info
|
||
* info 组成:
|
||
* info[0]info[1] info[2]info[3] info[4]info[5]
|
||
* └──────┬─────┘ └───────┬────┘ └───────┬────┘
|
||
* 图片宽度 图片高度 图片类型标识
|
||
* info[0]info[1]info[2]info[3]
|
||
* └────────────┬─────────────┘
|
||
* LED 灯状态信息
|
||
* 图片类型:
|
||
* 1. info[4] = 0x89 info[5] = 0x45 logo 图片
|
||
* 2. 其他图片
|
||
* @retval 无
|
||
*/
|
||
uint8_t RS485_Process(KEY_TYPE key, uint8_t flag)
|
||
{
|
||
static KEY_TYPE key_bk = KEY_NONE; /* 按键备份(静态变量,保持状态) */
|
||
static uint8_t LED_BUFF[4]; /* LED状态缓冲区(静态变量) */
|
||
static uint8_t info[8]; /* 命令信息缓冲区(静态变量) */
|
||
static uint32_t CRC_ERR_COUNT = 0; /* CRC错误计数(静态变量) */
|
||
uint8_t ConnectFlg = flag;
|
||
|
||
/* 按键处理:保存按键值 */
|
||
if(key != KEY_NONE)
|
||
{
|
||
if(key_bk != key) /* 按键值变化时更新备份 */
|
||
{
|
||
key_bk = key;
|
||
}
|
||
}
|
||
|
||
if(RS485REG.NewMessageFlag) /* 接收到新消息 */
|
||
{
|
||
RS485REG.NewMessageFlag = RESET; /* 清除控制字 */
|
||
|
||
ConnectFlg = 1; /* 设置连接标志 */
|
||
if(Check_CRC((uint8_t*)&RS485REG.DR[0], UART_RX_LEN) == TRUE) /* CRC校验通过 */
|
||
{
|
||
CRC_ERR_COUNT = 0; /* 重置CRC错误计数 */
|
||
memcpy(Picture, (uint8_t*)RS485REG.DR, 3200); /* 提取图片数据(前3200字节) */
|
||
memcpy(info, (uint8_t*)RS485REG.DR + 3200, 8); /* 提取命令信息(后8字节) */
|
||
|
||
if(info[4] == 0x89 && info[5] == 0x45) /* LOGO图片命令(特殊标识) */
|
||
{
|
||
memcpy(logo.bmpdata, Picture, 3200); /* 复制图片到LOGO结构体 */
|
||
logo.biWidth = info[0] << 8 | info[1]; /* 解析图片宽度(高字节左移8位 + 低字节) */
|
||
logo.biHeight = info[2] << 8 | info[3]; /* 解析图片高度(高字节左移8位 + 低字节) */
|
||
BMP_SAVE2False(); /* 保存 BMP 图片到 Flash */
|
||
|
||
/* 显示LOGO */
|
||
BackLight_ON(); /* 开启背光 */
|
||
ClearScreen(); /* 清屏 */
|
||
LOGO_Printf(); /* 显示LOGO */
|
||
delay_ms(500); /* 延时500ms */
|
||
delay_ms(500); /* 延时500ms */
|
||
delay_ms(500); /* 延时500ms */
|
||
delay_ms(500); /* 延时500ms(总共2秒) */
|
||
}
|
||
else /* 普通图片命令 */
|
||
{
|
||
memcpy(LED_BUFF, (uint8_t*)RS485REG.DR + 3200, 4); /* 提取LED状态(后4字节) */
|
||
LED_Printf(LED_BUFF); /* 显示LED状态 */
|
||
ScreenPrintf(Picture); /* 显示图片 */
|
||
}
|
||
}
|
||
else /* CRC校验失败 */
|
||
{
|
||
CRC_ERR_COUNT++; /* CRC错误计数递增 */
|
||
if(CRC_ERR_COUNT >= 3) /* 连续3次CRC错误 */
|
||
{
|
||
/* 接收错位,重新初始化接收 */
|
||
RS485_DMA_init(); /* 重新初始化*/
|
||
CRC_ERR_COUNT = 0; /* 重置CRC错误计数 */
|
||
}
|
||
}
|
||
|
||
/* 清除控制字和应答相关标志 */
|
||
RS485POLL.ACK_OverTime = RESET; /* 重置应答超时标志 */
|
||
RS485POLL.ACK_COUNT_ABLE = RESET; /* 禁用应答计数 */
|
||
RS485POLL.ACK_count = 0; /* 重置应答计数 */
|
||
RS485POLL.ACK_OverTimeCnt = 0; /* 重置应答超时计数 */
|
||
RS485POLL.CMD_ACK = SET; /* 设置接收完成标志 */
|
||
}
|
||
if((RS485POLL.CMD_ACK)||(RS485POLL.ACK_OverTime)) /* 可以应答或者超时*/
|
||
{
|
||
/* 按键命令发送:有按键且处于可发送状态 */
|
||
if(key_bk != KEY_NONE) /* 有按键 */
|
||
{
|
||
RefreshScreen(KEY_CMD, key_bk); /* 发送按键刷新命令 */
|
||
key_bk = KEY_NONE; /* 清除按键备份 */
|
||
}
|
||
/* 定时刷新:实时数据计数超限且处于可发送状态 */
|
||
if(RS485POLL.RT_count > RS485_RT_COUNT_MAX) /* 定时到*/
|
||
{
|
||
RefreshScreen(RT_CMD, key_bk); /* 发送实时刷新命令 */
|
||
key_bk = KEY_NONE; /* 清除按键备份 */
|
||
}
|
||
}
|
||
|
||
/* 应答超时处理:应答计数超限 */
|
||
if(RS485POLL.ACK_count > RS485_ACK_COUNT_MAX) /* 应答超时 */
|
||
{
|
||
RS485POLL.ACK_OverTime = SET; /* 设置应答超时标志 */
|
||
RS485POLL.ACK_count = 0; /* 重置应答计数 */
|
||
RS485POLL.ACK_OverTimeCnt++; /* 应答超时计数递增 */
|
||
}
|
||
|
||
/* 连续超时处理:应答超时次数达到上限 */
|
||
if((RS485POLL.ACK_OverTimeCnt >= RS485_ACK_OVERTIME_MAX)) /* 连续超时次数达到上限 */
|
||
{
|
||
ConnectFlg = 0;
|
||
RS485POLL.ACK_OverTimeCnt = RS485_ACK_OVERTIME_MAX; /* 限制最大计数(防止溢出) */
|
||
RS485_DMA_init(); /* 重新初始化RS485 */
|
||
RS485POLL.CMD_ACK = SET; /* 设置命令应答标志(允许重新发送) */
|
||
}
|
||
return ConnectFlg;
|
||
}
|
||
|
||
void Process_Count(void)
|
||
{
|
||
RS485POLL.RT_count++;
|
||
if(RS485POLL.ACK_COUNT_ABLE)
|
||
{
|
||
RS485POLL.ACK_count++;
|
||
}
|
||
}
|
||
void Process_Init(void)
|
||
{
|
||
/* 清除控制字和应答相关标志 */
|
||
RS485POLL.ACK_OverTime = RESET; /* 重置应答超时标志 */
|
||
RS485POLL.ACK_COUNT_ABLE = RESET; /* 禁用应答计数 */
|
||
RS485POLL.ACK_count = 0; /* 重置应答计数 */
|
||
RS485POLL.ACK_OverTimeCnt = 0; /* 重置应答超时计数 */
|
||
RS485POLL.CMD_ACK = SET; /* 设置接收完成标志 */
|
||
}
|