/** * @file main.c * @brief 基于 uC/OS-II 的 Modbus RTU 从站主程序。 * @version 2.0 * @author zengbingjie * @date 2026-07-29 * @copyright 版权所有信捷科技股份有限公司 * @note 修改记录:2.0 增加帧隔离、连接状态和可靠收发处理。 */ #include "main.h" #include "modbus.h" #include "modbus_backup.h" #define APP_MODBUS_FUNCTION_INDEX (1U) #define APP_MODBUS_RTU_TIMER_PHASE_T15 (0U) #define APP_MODBUS_RTU_TIMER_PHASE_T35 (1U) static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE]; static OS_STK AppTaskModbusStk[APP_TASK_MODBUS_STK_SIZE]; static OS_STK AppTaskT35Stk[APP_TASK_T35_STK_SIZE]; UART_HandleTypeDef Uart1Handle; /* 保留真实数据区供 IAR Live Watch 调试;不维护请求、应答或状态镜像副本。 */ volatile uint16_t holdingRegisters[MODBUS_DATA_POINT_COUNT]; volatile uint8_t coilStorage[MODBUS_COIL_STORAGE_SIZE]; volatile uint16_t extendedHoldingRegister; /* UART 诊断变量保留为全局可见,便于在 IAR Live Watch 中观察断线和重连测试。 */ volatile uint32_t ModbusUartLastErrorCode; volatile uint32_t ModbusUartLastRxState; volatile uint32_t ModbusUartRecoveryCount; volatile uint32_t ModbusUartRecoveryFailureCount; volatile uint32_t ModbusUartRxArmCount; volatile uint32_t ModbusUartRxArmFailureCount; volatile uint8_t ModbusUartLastRxByte; volatile uint16_t ModbusLastCompleteFrameLength; static MODBUS_SLAVE ModbusSlave; static OS_EVENT *ModbusFrameSem; static OS_EVENT *ModbusTxSem; /* 接收缓冲区由 UART 中断逐字节填充。只有 RTU 定时器置位 * ModbusFrameReady 后,Modbus 任务才读取它,因此解析器不会读到仍在接收中的帧。 */ static uint8_t ModbusRxBuffer[MODBUS_RX_BUF_SIZE]; static uint8_t ModbusTxBuffer[MODBUS_TX_BUF_SIZE]; /* 以下状态由中断和任务共享;更新应保持简短,读取完整帧快照时使用已有的临界区保护。 */ static volatile uint16_t ModbusRxLength; static volatile uint16_t ModbusFrameLength; static volatile uint8_t ModbusFrameReady; static volatile uint8_t ModbusRxOverflow; static volatile uint8_t ModbusRtuTimerPhase; static volatile uint8_t ModbusInterCharacterTimeout; static volatile uint8_t ModbusRxDiscard; static volatile uint8_t ModbusReceptionNeedsRecovery; static uint32_t ModbusLastActivityTick; static uint8_t ModbusHasActivity; static MODBUS_STATUS ModbusLastStatus; static GPIO_TypeDef *const AppUserOutputPorts[APP_MODBUS_USER_OUTPUT_COIL_COUNT] = { PLC_Q1_PORT, PLC_Q2_PORT, PLC_Q3_PORT, PLC_Q4_PORT, PLC_Q5_PORT, PLC_Q6_PORT, PLC_Q7_PORT }; static const uint16_t AppUserOutputPins[APP_MODBUS_USER_OUTPUT_COIL_COUNT] = { PLC_Q1_PIN, PLC_Q2_PIN, PLC_Q3_PIN, PLC_Q4_PIN, PLC_Q5_PIN, PLC_Q6_PIN, PLC_Q7_PIN }; void SystemClock_Config(void); static void AppTaskStart(void *argument); static void AppTaskModbus(void *argument); static void AppTaskT35(void *argument); static void AppInitSystemTick(void); static void AppInitGpio(void); static void AppInitUart1(void); static void AppInitModbusRtuTimer(void); static INT8U AppCreateTasks(void); static HAL_StatusTypeDef AppStartUartReception(void); static HAL_StatusTypeDef AppRecoverUartReception(void); static HAL_StatusTypeDef AppRecoverPendingUartReception(void); static HAL_StatusTypeDef AppEnsureUartReception(void); static HAL_StatusTypeDef AppTransmitResponse(uint16_t txLength); static void AppStartModbusRtuTimer(void); static void AppStopModbusRtuTimer(void); static void AppSetModbusRtuTimer(uint16_t timeoutUs); static void AppHandleModbusRtuTimer(void); static uint16_t AppGetReadyFrameLength(void); static INT8U AppPostModbusFrameSem(void); static uint8_t AppIsModbusStatusAccepted(MODBUS_STATUS status); static uint8_t AppIsModbusRetentionWriteFunction(uint8_t functionCode); static MODBUS_STATUS AppUpdateLinkStatus(void); static MODBUS_STATUS AppSyncCoilsToGpio(void); static void AppProcessFrame(uint16_t rxLength); /** * @brief 应用程序入口。 * @return 该函数不返回。 */ int main(void) { INT8U osError; /* 启动调度器前必须先完成硬件初始化。尤其要先初始化 USART1 和 TIM5, * 这样任务才能安全地启动接收和使用 RTU 帧间隔定时器。 */ HAL_Init(); AppInitSystemTick(); SystemClock_Config(); AppInitGpio(); AppInitUart1(); AppInitModbusRtuTimer(); /* OSStart() 之后由 uC/OS-II 负责任务调度;启动任务会创建下面的 * Modbus 工作任务和定时监测任务。 */ OSInit(); osError = OSTaskCreateExt( AppTaskStart, NULL, &AppTaskStartStk[APP_TASK_START_STK_SIZE - 1U], APP_TASK_START_PRIO, APP_TASK_START_PRIO, &AppTaskStartStk[0], APP_TASK_START_STK_SIZE, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); if (osError != OS_ERR_NONE) { Error_Handler(); } OSStart(); while (1) { } } /** * @brief 配置 STM32F407 的 168 MHz 系统时钟。 * @note 使用板载 12 MHz 外部晶振作为 PLL 时钟源。 */ void SystemClock_Config(void) { RCC_OscInitTypeDef oscillatorConfig = {0}; RCC_ClkInitTypeDef clockConfig = {0}; __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); oscillatorConfig.OscillatorType = RCC_OSCILLATORTYPE_HSE; oscillatorConfig.HSEState = RCC_HSE_ON; oscillatorConfig.PLL.PLLState = RCC_PLL_ON; oscillatorConfig.PLL.PLLSource = RCC_PLLSOURCE_HSE; oscillatorConfig.PLL.PLLM = 12U; oscillatorConfig.PLL.PLLN = 336U; oscillatorConfig.PLL.PLLP = RCC_PLLP_DIV2; oscillatorConfig.PLL.PLLQ = 7U; if (HAL_RCC_OscConfig(&oscillatorConfig) != HAL_OK) { Error_Handler(); } clockConfig.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clockConfig.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clockConfig.AHBCLKDivider = RCC_SYSCLK_DIV1; clockConfig.APB1CLKDivider = RCC_HCLK_DIV4; clockConfig.APB2CLKDivider = RCC_HCLK_DIV2; if (HAL_RCC_ClockConfig(&clockConfig, FLASH_LATENCY_5) != HAL_OK) { Error_Handler(); } } /** * @brief 将 SysTick 配置为可调用 uC/OS-II 的内核感知中断优先级。 */ static void AppInitSystemTick(void) { HAL_NVIC_SetPriority(SysTick_IRQn, CPU_CFG_KA_IPL_BOUNDARY, 0U); } /** * @brief 初始化 XDM-60T4-E 的连接指示灯和 Q1 至 Q7 输出引脚。 * @note 板卡输出为低电平有效,初始状态全部关闭。 */ static void AppInitGpio(void) { GPIO_InitTypeDef gpioConfig = {0}; __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOI_CLK_ENABLE(); gpioConfig.Mode = GPIO_MODE_OUTPUT_PP; gpioConfig.Pull = GPIO_PULLUP; gpioConfig.Speed = GPIO_SPEED_FREQ_LOW; gpioConfig.Pin = PLC_Q0_PIN | PLC_Q1_PIN | PLC_Q2_PIN | PLC_Q3_PIN; HAL_GPIO_Init(GPIOF, &gpioConfig); gpioConfig.Pin = PLC_Q4_PIN; HAL_GPIO_Init(GPIOI, &gpioConfig); gpioConfig.Pin = PLC_Q5_PIN | PLC_Q6_PIN | PLC_Q7_PIN; HAL_GPIO_Init(GPIOE, &gpioConfig); HAL_GPIO_WritePin(GPIOF, PLC_Q0_PIN | PLC_Q1_PIN | PLC_Q2_PIN | PLC_Q3_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOI, PLC_Q4_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOE, PLC_Q5_PIN | PLC_Q6_PIN | PLC_Q7_PIN, GPIO_PIN_SET); } /** * @brief 初始化 Modbus RTU 使用的 USART1。 */ static void AppInitUart1(void) { Uart1Handle.Instance = USART1; Uart1Handle.Init.BaudRate = APP_MODBUS_BAUD_RATE; /* 串口参数必须与触摸屏/PLC 主站完全一致。当前工程使用 9 位字长和偶校验 * (HAL 对“8 位数据 + 1 位校验”的配置方式);不能只修改从站而不修改主站。 */ Uart1Handle.Init.WordLength = UART_WORDLENGTH_9B; Uart1Handle.Init.StopBits = UART_STOPBITS_1; Uart1Handle.Init.Parity = UART_PARITY_EVEN; Uart1Handle.Init.Mode = UART_MODE_TX_RX; Uart1Handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; Uart1Handle.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&Uart1Handle) != HAL_OK) { Error_Handler(); } } /** * @brief 将 TIM5 配置为 Modbus RTU 帧间隔单次定时器。 */ static void AppInitModbusRtuTimer(void) { __HAL_RCC_TIM5_CLK_ENABLE(); TIM5->CR1 = 0U; TIM5->PSC = (APP_MODBUS_RTU_TIMER_CLOCK_HZ / APP_MODBUS_RTU_TIMER_TICK_HZ) - 1U; TIM5->ARR = APP_MODBUS_RTU_T35_US - 1U; TIM5->CNT = 0U; TIM5->EGR = TIM_EGR_UG; TIM5->SR = 0U; TIM5->DIER = TIM_DIER_UIE; HAL_NVIC_SetPriority(TIM5_IRQn, 5U, 0U); HAL_NVIC_EnableIRQ(TIM5_IRQn); } /** * @brief 创建 Modbus 工作任务与帧间隔检测任务。 * @return uC/OS-II 错误码。 */ static INT8U AppCreateTasks(void) { INT8U osError; osError = OSTaskCreateExt( AppTaskModbus, NULL, &AppTaskModbusStk[APP_TASK_MODBUS_STK_SIZE - 1U], APP_TASK_MODBUS_PRIO, APP_TASK_MODBUS_PRIO, &AppTaskModbusStk[0], APP_TASK_MODBUS_STK_SIZE, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); if (osError != OS_ERR_NONE) { return osError; } osError = OSTaskCreateExt( AppTaskT35, NULL, &AppTaskT35Stk[APP_TASK_T35_STK_SIZE - 1U], APP_TASK_T35_PRIO, APP_TASK_T35_PRIO, &AppTaskT35Stk[0], APP_TASK_T35_STK_SIZE, NULL, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR); return osError; } /** * @brief 启动任务,创建同步对象并启动应用任务。 * @param[in] argument 未使用。 */ static void AppTaskStart(void *argument) { INT8U osError; (void)argument; ModbusFrameSem = OSSemCreate(0U); ModbusTxSem = OSSemCreate(0U); if ((ModbusFrameSem == NULL) || (ModbusTxSem == NULL)) { Error_Handler(); } osError = AppCreateTasks(); if (osError != OS_ERR_NONE) { Error_Handler(); } OSTaskSuspend(OS_PRIO_SELF); while (1) { } } /** * @brief 清空接收状态并启动 USART1 单字节中断接收。 * @return HAL 状态码。 */ static HAL_StatusTypeDef AppStartUartReception(void) { OS_CPU_SR cpu_sr = 0U; HAL_StatusTypeDef halStatus; /* 新帧必须从干净的软件状态开始。这里使用临界区,避免定时器或 UART 中断 * 看到只清除了一半的状态字段。 */ OS_ENTER_CRITICAL(); AppStopModbusRtuTimer(); ModbusRxLength = 0U; ModbusFrameLength = 0U; ModbusFrameReady = 0U; ModbusRxOverflow = 0U; ModbusRtuTimerPhase = APP_MODBUS_RTU_TIMER_PHASE_T15; ModbusInterCharacterTimeout = 0U; ModbusRxDiscard = 0U; OS_EXIT_CRITICAL(); /* 每次只挂接一个字节的接收。接收完成回调负责推进缓冲区并重新挂接下一个 * 字节,使组帧完全由中断驱动且不会阻塞 Modbus 任务。 */ halStatus = HAL_UART_Receive_IT(&Uart1Handle, &ModbusRxBuffer[0], 1U); ModbusUartRxArmCount++; if (halStatus != HAL_OK) { ModbusUartRxArmFailureCount++; } ModbusUartLastRxState = (uint32_t)Uart1Handle.RxState; return halStatus; } /** * @brief 在 Modbus 任务上下文中恢复 USART1 单字节中断接收。 * @return HAL 状态码。 */ static HAL_StatusTypeDef AppRecoverUartReception(void) { HAL_StatusTypeDef halStatus; HAL_StatusTypeDef abortStatus; ModbusUartRecoveryCount++; /* 出错的接收仍由 HAL 持有时,HAL_UART_Receive_IT() 会返回 BUSY。 * 先终止旧接收并清除外设状态,下一次挂接才能从 READY 状态开始。 */ abortStatus = HAL_UART_AbortReceive_IT(&Uart1Handle); /* STM32 HAL 宏会按外设要求读取 SR 和 DR,以清除奇偶校验、帧格式和溢出错误。 * 该操作必须在中断之外执行。 */ __HAL_UART_CLEAR_PEFLAG(&Uart1Handle); halStatus = AppStartUartReception(); if ((abortStatus != HAL_OK) || (halStatus != HAL_OK)) { (void)HAL_UART_AbortReceive_IT(&Uart1Handle); __HAL_UART_CLEAR_PEFLAG(&Uart1Handle); halStatus = AppStartUartReception(); } if (halStatus != HAL_OK) { ModbusUartRecoveryFailureCount++; } ModbusUartLastRxState = (uint32_t)Uart1Handle.RxState; return halStatus; } /** * @brief 处理一个待执行的 UART 接收恢复请求。 * @return 本次恢复操作返回的 HAL 状态。 * @note 接收已经挂接时 UART 中断仍可能设置待恢复标志。只能清除本次尝试前 * 已取出的请求,不能清除恢复过程中并发产生的新请求。 */ static HAL_StatusTypeDef AppRecoverPendingUartReception(void) { HAL_StatusTypeDef halStatus; halStatus = HAL_OK; if (ModbusReceptionNeedsRecovery != 0U) { ModbusReceptionNeedsRecovery = 0U; halStatus = AppRecoverUartReception(); if (halStatus != HAL_OK) { ModbusReceptionNeedsRecovery = 1U; } } return halStatus; } /** * @brief 即使 HAL 状态丢失,也保持 UART 接收中断处于挂接状态。 * @return 恢复操作的 HAL 状态;接收已经挂接时返回 HAL_OK。 * @note RS-485 断线时可能完全没有 UART 中断,因此这里的检查可以补充错误回调, * 同时处理接收被打断后 HAL 停留在 READY/BUSY 状态的情况。 */ static HAL_StatusTypeDef AppEnsureUartReception(void) { if (ModbusFrameReady != 0U) { return HAL_OK; } /* 线缆断开可能完全不产生 UART 中断。任务每次唤醒时检查 HAL 状态,可以补充 * ErrorCallback 的处理,即使硬件没有上报错误标志也能重新挂接接收。 */ if ((ModbusReceptionNeedsRecovery != 0U) || (Uart1Handle.RxState != HAL_UART_STATE_BUSY_RX)) { ModbusReceptionNeedsRecovery = 1U; return AppRecoverPendingUartReception(); } return HAL_OK; } /** * @brief 通过 USART1 发送 Modbus 从站响应帧。 * @param[in] txLength 响应帧长度。 * @return HAL 状态码。 */ static HAL_StatusTypeDef AppTransmitResponse(uint16_t txLength) { HAL_StatusTypeDef halStatus; INT8U osError; if ((txLength == 0U) || (txLength > MODBUS_TX_BUF_SIZE)) { return HAL_ERROR; } /* 发送是异步进行的。任务等待 ModbusTxSem,确保 USART1 发出最后一个字节前 * 不会复用同一个发送缓冲区。 */ halStatus = HAL_UART_Transmit_IT(&Uart1Handle, ModbusTxBuffer, txLength); if (halStatus != HAL_OK) { return halStatus; } OSSemPend(ModbusTxSem, APP_MODBUS_TX_TIMEOUT_TICKS, &osError); if (osError != OS_ERR_NONE) { (void)HAL_UART_AbortTransmit_IT(&Uart1Handle); return HAL_TIMEOUT; } return HAL_OK; } /** * @brief 设置正在运行的 RTU 定时器单次超时时间,单位为微秒。 * @param[in] timeoutUs 单次超时时间,单位为微秒。 */ static void AppSetModbusRtuTimer(uint16_t timeoutUs) { /* TIM5 作为单次定时器使用。装载新间隔前先停止、清零并确认状态, * 否则残留的更新标志可能立即产生错误的帧边界。 */ TIM5->CR1 &= (uint32_t)(~TIM_CR1_CEN); TIM5->CNT = 0U; TIM5->ARR = (uint32_t)timeoutUs - 1U; TIM5->SR = 0U; TIM5->CR1 |= TIM_CR1_CEN; } /** * @brief 启动 Modbus RTU T1.5/T3.5 定时器的第一阶段。 */ static void AppStartModbusRtuTimer(void) { /* 每收到一个字节就重新开始 T1.5。如果线路静默超过 T1.5,当前帧会被标记为 * 无效,随后第二阶段继续等待 T3.5,再决定丢弃帧还是交给任务处理。 */ ModbusRtuTimerPhase = APP_MODBUS_RTU_TIMER_PHASE_T15; ModbusInterCharacterTimeout = 0U; AppSetModbusRtuTimer(APP_MODBUS_RTU_T15_RX_TIMEOUT_US); } /** * @brief 停止并确认 Modbus RTU 帧间隔定时器。 */ static void AppStopModbusRtuTimer(void) { TIM5->CR1 &= (uint32_t)(~TIM_CR1_CEN); TIM5->SR = 0U; } /** * @brief 在中断上下文中处理 T1.5 和 T3.5 单次定时到期事件。 */ static void AppHandleModbusRtuTimer(void) { if (ModbusRtuTimerPhase == APP_MODBUS_RTU_TIMER_PHASE_T15) { /* T1.5 表示帧内出现了字符间隔,此时不能立即解析;还要等待后续 T3.5, * 用它把这条不完整帧与下一条合法 RTU 帧分隔开。 */ ModbusInterCharacterTimeout = 1U; ModbusRtuTimerPhase = APP_MODBUS_RTU_TIMER_PHASE_T35; AppSetModbusRtuTimer(APP_MODBUS_RTU_T35_AFTER_T15_US); return; } AppStopModbusRtuTimer(); ModbusInterCharacterTimeout = 0U; if (ModbusRxDiscard != 0U) { ModbusRxDiscard = 0U; ModbusRxLength = 0U; return; } if ((ModbusRxLength == 0U) || (ModbusFrameReady != 0U)) { return; } /* T3.5 表示帧结束。通知任务前先保存帧长度,任务随后解析稳定的缓冲区, * 完成后再重置接收状态。 */ ModbusFrameLength = ModbusRxLength; ModbusLastCompleteFrameLength = ModbusFrameLength; ModbusFrameReady = 1U; (void)HAL_UART_AbortReceive_IT(&Uart1Handle); (void)AppPostModbusFrameSem(); } /** * @brief 原子读取已封存 Modbus 帧的长度。 * @return 完整帧长度;无完整帧时返回 0。 */ static uint16_t AppGetReadyFrameLength(void) { OS_CPU_SR cpu_sr = 0U; uint16_t frameLength; /* 定时器中断将 ModbusFrameLength 和 ModbusFrameReady 作为一个逻辑事件发布。 * 这里原子地读取快照,避免任务拿到属于另一种缓冲区状态的长度。 */ frameLength = 0U; OS_ENTER_CRITICAL(); if (ModbusFrameReady != 0U) { frameLength = ModbusFrameLength; } OS_EXIT_CRITICAL(); return frameLength; } /** * @brief 通知 Modbus 任务处理完整帧或刷新通信状态。 * @return uC/OS-II 错误码。 */ static INT8U AppPostModbusFrameSem(void) { INT8U osError; /* RTU 定时器、UART 错误路径和周期链路检查等多个来源共用一个信号量。 * 任务会再次检查帧长度,因此没有完整帧的唤醒也是安全的,并能刷新链路指示。 */ osError = OSSemPost(ModbusFrameSem); return osError; } /** * @brief 判断协议处理结果是否代表已寻址的有效通信。 * @param[in] status Modbus 协议处理结果。 * @return 1 表示有效通信;0 表示无效通信。 */ static uint8_t AppIsModbusStatusAccepted(MODBUS_STATUS status) { if ((status == MODBUS_STATUS_OK) || (status == MODBUS_STATUS_BROADCAST) || (status == MODBUS_STATUS_EXCEPTION)) { return 1U; } return 0U; } /** * @brief 判断功能码是否为需要保存到 RTC 的写操作。 * @param[in] functionCode Modbus 功能码。 * @return 写操作返回 1,否则返回 0。 * @note 仅 0x42 修改 0x00010000;标准寄存器和线圈写入不具备掉电保持承诺。 */ static uint8_t AppIsModbusRetentionWriteFunction(uint8_t functionCode) { switch (functionCode) { case MODBUS_FC_WRITE_EXTENDED_HOLDING_REGS: return 1U; default: return 0U; } } /** * @brief 更新连接线圈和 PLC 连接指示灯。 * @return 成功返回 MODBUS_STATUS_OK;失败返回错误状态。 * @note 仅已寻址的正常响应、广播写或异常响应刷新活动时间;CRC 错误和 * 非本机帧不会误报链路在线。 */ static MODBUS_STATUS AppUpdateLinkStatus(void) { MODBUS_STATUS status; uint32_t currentTick; uint8_t linkState; GPIO_PinState pinState; currentTick = OSTimeGet(); linkState = APP_MODBUS_LINK_DISCONNECTED; if ((ModbusHasActivity != 0U) && ((currentTick - ModbusLastActivityTick) < APP_MODBUS_LINK_TIMEOUT_TICKS)) { linkState = APP_MODBUS_LINK_CONNECTED; } status = ModbusSetCoil(&ModbusSlave, APP_MODBUS_LINK_STATE_COIL, linkState); if (status != MODBUS_STATUS_OK) { return status; } if (linkState == APP_MODBUS_LINK_CONNECTED) { pinState = GPIO_PIN_RESET; } else { pinState = GPIO_PIN_SET; } HAL_GPIO_WritePin(PLC_LINK_LED_PORT, PLC_LINK_LED_PIN, pinState); return MODBUS_STATUS_OK; } /** * @brief 将线圈 1 至 7 的状态同步到 PLC Q1 至 Q7 输出。 * @return 成功返回 MODBUS_STATUS_OK;失败返回错误状态。 * @note 板级输出为低有效,因此逻辑线圈置 1 时向 GPIO 写入 GPIO_PIN_RESET。 */ static MODBUS_STATUS AppSyncCoilsToGpio(void) { MODBUS_STATUS status; uint16_t coilOffset; uint16_t coilAddress; uint8_t coilValue; GPIO_PinState pinState; for (coilOffset = 0U; coilOffset < APP_MODBUS_USER_OUTPUT_COIL_COUNT; coilOffset++) { coilAddress = coilOffset + APP_MODBUS_FIRST_USER_OUTPUT_COIL; status = ModbusGetCoil(&ModbusSlave, coilAddress, &coilValue); if (status != MODBUS_STATUS_OK) { return status; } pinState = (coilValue != 0U) ? GPIO_PIN_RESET : GPIO_PIN_SET; HAL_GPIO_WritePin(AppUserOutputPorts[coilOffset], AppUserOutputPins[coilOffset], pinState); } return MODBUS_STATUS_OK; } /** * @brief 处理已封存的 Modbus RTU 请求帧。 * @param[in] rxLength 请求帧长度。 * @note 此函数在 Modbus 任务上下文执行。它负责协议分发、链路活动刷新、 * 线圈 GPIO 同步、0x42 成功后的 RTC 保存和异步 UART 应答启动。 */ static void AppProcessFrame(uint16_t rxLength) { MODBUS_STATUS status; uint16_t txLength; uint8_t functionCode; uint8_t isWriteOperation; txLength = 0U; functionCode = 0U; isWriteOperation = 0U; if (rxLength > APP_MODBUS_FUNCTION_INDEX) { functionCode = ModbusRxBuffer[APP_MODBUS_FUNCTION_INDEX]; } if ((rxLength == 0U) || (rxLength > MODBUS_RX_BUF_SIZE)) { ModbusLastStatus = MODBUS_STATUS_FRAME_ERROR; return; } if (ModbusRxOverflow != 0U) { ModbusLastStatus = MODBUS_STATUS_FRAME_ERROR; return; } /* 协议解析固定放在任务上下文中执行。这样解析器可以更新线圈/寄存器并构造 * 响应,而不必在 UART 或定时器中断中执行较长的处理。 */ status = ModbusProcessFrame(&ModbusSlave, ModbusRxBuffer, rxLength, ModbusTxBuffer, MODBUS_TX_BUF_SIZE, &txLength); ModbusLastStatus = status; /* 只有 CRC 正确且寻址到本站的帧(包括合法广播写入或已生成的异常响应)才算 * 有效链路活动,线路上的随机干扰不能错误点亮连接指示灯。 */ if (AppIsModbusStatusAccepted(status) != 0U) { ModbusLastActivityTick = OSTimeGet(); ModbusHasActivity = 1U; isWriteOperation = AppIsModbusRetentionWriteFunction(functionCode); if (((ModbusLastStatus == MODBUS_STATUS_OK) || (ModbusLastStatus == MODBUS_STATUS_BROADCAST)) && (isWriteOperation != 0U)) { (void)ModbusBackupSave(&ModbusSlave); } } /* 线圈值是协议数据模型;这里是唯一的同步桥,将用户线圈 1..7 映射到板卡 * 低电平有效的 Q1..Q7 输出。 */ if ((status == MODBUS_STATUS_OK) || (status == MODBUS_STATUS_BROADCAST)) { (void)AppSyncCoilsToGpio(); } (void)AppUpdateLinkStatus(); if (txLength > 0U) { (void)AppTransmitResponse(txLength); } } /** * @brief Modbus 协议处理任务。 * @param[in] argument 未使用。 */ static void AppTaskModbus(void *argument) { HAL_StatusTypeDef halStatus; INT8U osError; uint16_t rxLength; (void)argument; /* 先将协议模型绑定到 RAM 数据区,再恢复由 RTC 备份的扩展寄存器,确保第一条 * 请求到达前数据已经准备好。 */ if (ModbusSlaveInit(&ModbusSlave, MODBUS_SLAVE_ADDR_DEFAULT, holdingRegisters, coilStorage) != MODBUS_STATUS_OK) { Error_Handler(); } if (ModbusConfigureExtendedHoldingRegister(&ModbusSlave, &extendedHoldingRegister) != MODBUS_STATUS_OK) { Error_Handler(); } if (ModbusBackupRestore(&ModbusSlave) != MODBUS_STATUS_OK) { Error_Handler(); } if (AppSyncCoilsToGpio() != MODBUS_STATUS_OK) { Error_Handler(); } if (AppUpdateLinkStatus() != MODBUS_STATUS_OK) { Error_Handler(); } halStatus = AppRecoverUartReception(); if (halStatus != HAL_OK) { ModbusReceptionNeedsRecovery = 1U; } while (1) { /* 完整帧解析和 UART 恢复由本任务负责。中断回调只收集字节、标记帧边界或 * 错误,并投递此信号量。 */ halStatus = AppEnsureUartReception(); if (halStatus != HAL_OK) { OSTimeDly(1U); continue; } OSSemPend(ModbusFrameSem, 0U, &osError); if (osError != OS_ERR_NONE) { continue; } rxLength = AppGetReadyFrameLength(); if (rxLength > 0U) { AppProcessFrame(rxLength); /* 帧定时器已经终止了本次完整帧的接收。正常挂接下一帧时不能再次清除 * SR/DR,因为主站可能在 T3.5 后已经开始发送下一条请求。 */ halStatus = AppStartUartReception(); if (halStatus != HAL_OK) { ModbusReceptionNeedsRecovery = 1U; } } else { (void)AppUpdateLinkStatus(); } } } /** * @brief 定期检测接收静默时间并判断 Modbus RTU 帧结束。 * @param[in] argument 未使用。 */ static void AppTaskT35(void *argument) { uint16_t updateElapsedTicks; (void)argument; updateElapsedTicks = 0U; while (1) { /* 这个低频任务不解析帧,只按周期唤醒 Modbus 任务,使空闲线路在达到配置 * 的超时时间后将连接指示灯恢复为断开状态。 */ OSTimeDly(APP_MODBUS_STATUS_CHECK_TICKS); updateElapsedTicks += APP_MODBUS_STATUS_CHECK_TICKS; if (updateElapsedTicks >= APP_MODBUS_STATUS_UPDATE_TICKS) { updateElapsedTicks = 0U; (void)AppPostModbusFrameSem(); } } } /** * @brief USART1 单字节接收完成回调。 * @param[in] uartHandle 触发回调的串口句柄。 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *uartHandle) { HAL_StatusTypeDef halStatus; if ((uartHandle == NULL) || (uartHandle->Instance != USART1)) { return; } /* 此回调每次只处理一个字节,不解析功能码,也不写入数据模型;这些操作由 * AppTaskModbus 任务完成。 */ if (ModbusFrameReady != 0U) { return; } /* T1.5 间隔会使当前不完整帧失效。继续接收并丢弃字节直到后续 T3.5 边界, * 防止迟到的字节被误认为新帧的首字节。 */ if ((ModbusRxDiscard != 0U) || (ModbusInterCharacterTimeout != 0U)) { ModbusRxDiscard = 1U; ModbusRxLength = 0U; AppStartModbusRtuTimer(); halStatus = HAL_UART_Receive_IT(&Uart1Handle, &ModbusRxBuffer[0], 1U); if (halStatus != HAL_OK) { ModbusReceptionNeedsRecovery = 1U; (void)AppPostModbusFrameSem(); } return; } if (ModbusRxLength >= MODBUS_RX_BUF_SIZE) { ModbusRxOverflow = 1U; return; } /* HAL 已经将当前字节放入 ModbusRxBuffer[ModbusRxLength]。 */ ModbusRxLength++; ModbusUartLastRxByte = ModbusRxBuffer[ModbusRxLength - 1U]; AppStartModbusRtuTimer(); if (ModbusRxLength >= MODBUS_RX_BUF_SIZE) { ModbusRxOverflow = 1U; return; } halStatus = HAL_UART_Receive_IT(&Uart1Handle, &ModbusRxBuffer[ModbusRxLength], 1U); if (halStatus != HAL_OK) { ModbusReceptionNeedsRecovery = 1U; (void)AppPostModbusFrameSem(); } } /** * @brief USART1 通信错误回调。 * @param[in] uartHandle 触发回调的串口句柄。 * @note 中断仅停止计时、标记当前帧无效并通知任务;接收重挂接由 * AppRecoverPendingUartReception 在任务上下文执行。 */ void HAL_UART_ErrorCallback(UART_HandleTypeDef *uartHandle) { if ((uartHandle == NULL) || (uartHandle->Instance != USART1)) { return; } /* UART 错误会使当前整帧失效。中断只记录错误并唤醒任务,不在这里终止或重新 * 进入 HAL 接收流程,从而避免 HAL 重入,并让恢复操作集中在一个上下文中。 */ AppStopModbusRtuTimer(); ModbusUartLastErrorCode = uartHandle->ErrorCode; ModbusUartLastRxState = (uint32_t)uartHandle->RxState; __HAL_UART_CLEAR_PEFLAG(uartHandle); ModbusRxOverflow = 1U; ModbusReceptionNeedsRecovery = 1U; (void)AppPostModbusFrameSem(); } /** * @brief 处理 TIM5 的 Modbus RTU T1.5/T3.5 单次定时到期事件。 * @note 本函数只更新接收状态和投递信号量,不解析帧内容。 */ void AppModbusRtuTimerIrqHandler(void) { if ((TIM5->SR & TIM_SR_UIF) == 0U) { return; } TIM5->SR &= (uint32_t)(~TIM_SR_UIF); AppHandleModbusRtuTimer(); } /** * @brief USART1 中断发送完成回调。 * @param[in] uartHandle 触发回调的串口句柄。 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *uartHandle) { if ((uartHandle == NULL) || (uartHandle->Instance != USART1)) { return; } /* 只有最后一个字节已经移出串口后,才唤醒发送任务。 */ (void)OSSemPost(ModbusTxSem); } /** * @brief 进入不可恢复错误状态。 */ void Error_Handler(void) { __disable_irq(); while (1) { } }