Files
DTU-LCD/Middlewares/Modbus/MODBUS.c

386 lines
17 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) /**< 按键事件命令类型 */
/* ============================================================================
* 图片与 Flash 宏定义
* ============================================================================ */
#define BMP_ADDR (0x0803F000) /**< BMP 在 Flash 中的存储地址(扇区 126 */
#define BMP_LEN (3208) /**< BMP 数据长度(字节) */
/* ============================================================================
* 轮询与超时常量
* ============================================================================ */
#define RS485_RT_COUNT_MAX (55) /**< 实时轮询计数上限,约 275ms55×5ms */
#define RS485_ACK_COUNT_MAX (40) /**< 应答超时计数上限,约 200ms40×5ms */
#define RS485_ACK_OVERTIME_MAX (5) /**< 连续应答超时次数上限,超过则重初始化 RS485 */
/* ============================================================================
* 数据结构定义
* ============================================================================ */
/**
* @brief LOGO 图片数据结构体
* @note 用于存储从 138 端下发的 BMP 数据及尺寸,或从 Flash 读取的 LOGO
*/
typedef struct
{
uint8_t bmpdata[3200]; /**< BMP 点阵数据,最大 3200 字节 */
uint32_t biWidth; /**< 图片宽度(像素) */
uint32_t biHeight; /**< 图片高度(像素) */
} logo_type;
/**
* @brief RS485 轮询与应答状态结构体
* @note 用于 Modbus 主站轮询、应答超时检测及命令应答标志管理
*/
typedef struct
{
volatile uint8_t RT_count; /**< 实时命令计数,用于定时发送刷新命令 */
volatile uint16_t CMD_ACK; /**< 命令接收完成标志SET=可发下一帧) */
volatile uint8_t ACK_COUNT_ABLE; /**< 应答计时使能(发令后置位) */
volatile uint8_t ACK_count; /**< 应答等待计数,超 RS485_ACK_COUNT_MAX 即超时 */
volatile uint8_t ACK_OverTime; /**< 本次应答超时标志 */
volatile uint8_t ACK_OverTimeCnt; /**< 连续应答超时次数,超 RS485_ACK_OVERTIME_MAX 则重初始 */
} RS485_POLL_TYPE;
RS485_POLL_TYPE RS485POLL = {0}; /**< 全局轮询状态 */
/* ============================================================================
* 私有变量定义
* ============================================================================ */
static logo_type logo = {0}; /**< LOGO 图片数据BMP + 宽高) */
static uint8_t Picture[3200] = {0}; /**< 接收图片缓冲,前 3200 字节为点阵数据 */
/* ============================================================================
* Modbus 命令与屏幕刷新
* ============================================================================ */
/**
* @brief 发送屏幕刷新命令(特殊格式的 Modbus 命令)
* @param func 功能码(通常为读寄存器 READ_REGISTER = 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 端发送屏幕刷新命令;发送后重置 CMD_ACK、RT_count
* 使能 ACK_COUNT_ABLE 并清零 ACK_count。
* @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; /* 重置应答计数值 */
}
/* ============================================================================
* LOGO 与 BMP 处理
* ============================================================================ */
/**
* @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;
}
/**
* @brief 从 Flash 读取 LOGO 并显示,延时约 2 秒
* @note 调用 BMP_Get 从 Flash 读取 LOGO再 LOGO_Printf 显示;
* 每次显示后 delay 4×500ms = 2s
* @retval 无
*/
void NL_LOGO_Printf(void)
{
BMP_Get();
LOGO_Printf();
delay_ms(500);
delay_ms(500);
delay_ms(500);
delay_ms(500);
}
/* ============================================================================
* Modbus 主处理与轮询
* ============================================================================ */
/**
* @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 无
*/
void RS485_Process(KEY_TYPE key)
{
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错误计数静态变量 */
/* 按键处理:保存按键值 */
if(key != KEY_NONE)
{
key_bk = key;
}
if(RS485REG.NewMessageFlag) /* 接收到新消息 */
{
RS485REG.NewMessageFlag = RESET; /* 清除控制字 */
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_DeInit();
// delay_ms(500);
// delay_ms(500);
// 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); /* 发送实时刷新命令 */
}
}
/* 应答超时处理:应答计数超限 */
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)) /* 连续超时次数达到上限 */
{
RS485POLL.ACK_OverTimeCnt = RS485_ACK_OVERTIME_MAX; /* 限制最大计数(防止溢出) */
RS485POLL.CMD_ACK = SET; /* 设置命令应答标志(允许重新发送) */
}
}
/**
* @brief 轮询计数处理(需在 5ms 周期定时器中调用)
* @note RT_count 递增,用于定时发实时刷新命令;若 ACK_COUNT_ABLE 置位,
* 则 ACK_count 递增,用于应答超时判断。
* @retval 无
*/
void Process_Count(void)
{
RS485POLL.RT_count++;
if(RS485POLL.ACK_COUNT_ABLE)
{
RS485POLL.ACK_count++;
}
}
/**
* @brief Modbus 轮询与应答相关变量初始化
* @note 清零 ACK_OverTime、ACK_count、ACK_OverTimeCnt关闭 ACK_COUNT_ABLE
* 置位 CMD_ACK 表示可发送。上电或通信异常恢复后调用。
* @retval 无
*/
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; /* 设置接收完成标志 */
}