/****************************************************************************** * @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 #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) /**< 实时轮询计数上限,约 275ms(55×5ms) */ #define RS485_ACK_COUNT_MAX (40) /**< 应答超时计数上限,约 200ms(40×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 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; } /** * @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; /* 设置接收完成标志 */ }