|
- /**
- * @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) {
- }
- }
|