Files
DTU-LCD/Middlewares/Modbus/MODBUS.c
2026-01-24 20:03:14 +08:00

358 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/******************************************************************************
* @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 0x45LOGO图片保存并显示
* - 其他普通图片显示图片和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; /* 设置接收完成标志 */
}