野火STM32Modbus主机读取寄存器/线圈失败(一)-解决接收中断不触发的问题
接收中断不触发
前情提要
在自己的开发板上移植了野火的modbus主机程序。
野火主机程序移植
野火主机代码理解与使用
问题背景
我使用STM32显示板作为Modbus主机连接电脑,并在电脑上运行Modbus Slave软件。测试中发现,读取保持寄存器和输入寄存器均失败,但写入操作正常。例如,当我的板子作为主机发送读取从机1的40、41地址(其中预先写入了4000和6500)的请求时,Modbus Slave可以正确接收到请求帧:
Rx: 001205-01 03 00 28 00 02 44 03
Tx: 001206-01 03 04 0F A0 19 64 B1 97
这说明主机发出的命令没有问题。然而,在我的代码中,用于存储保持寄存器的数组 usMRegHoldBuf[][]
始终为0,未能更新。进一步排查发现,程序并未进入回调函数 eMBMasterRegHoldingCB(UCHAR * pucRegBuffer, USHORT usAddress, USHORT usNRegs, eMBRegisterMode eMode)
。
后续测试表明,Modbus主机的接收中断从未被触发,因此初步判断问题出在Modbus主机的接收中断部分,可能存在配置或实现上的错误。
现象
在portserial_m.c中添加调试代码,变量tx_int_count递增,但是rx_int_count始终为0,说明没触发接收中断。
/* * Create an interrupt handler for the transmit buffer empty interrupt* (or an equivalent) for your target processor. This function should then* call pxMBFrameCBTransmitterEmpty( ) which tells the protocol stack that* a new character can be sent. The protocol stack will then call * xMBPortSerialPutByte( ) to send the character.*/
void prvvUARTTxReadyISR(void)
{/* 发送状态机 */extern volatile uint32_t tx_int_count;tx_int_count++;pxMBMasterFrameCBTransmitterEmpty();
}/* * Create an interrupt handler for the receive interrupt for your target* processor. This function should then call pxMBFrameCBByteReceived( ). The* protocol stack will then call xMBPortSerialGetByte( ) to retrieve the* character.*/
void prvvUARTRxISR(void)
{/* 接收状态机 */extern volatile uint32_t rx_int_count;rx_int_count++;pxMBMasterFrameCBByteReceived();
}
解决
将portserial_m.c中的void vMBMasterPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)进行修改。
把
void vMBMasterPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{/* If xRXEnable enable serial receive interrupts. If xTxENable enable* transmitter empty interrupts.*/if(xRxEnable){/* 串口2接收中断使能 */__HAL_USART_ENABLE_IT(&huart2,USART_IT_RXNE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN) /* 485低电平接收 */HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_LOW);#endif}else{/* 串口2接收中断关闭 */__HAL_USART_DISABLE_IT(&huart2,USART_IT_RXNE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN)/* 485高电平发送 */HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_HIGH);#endif}if(xTxEnable){/* 串口2发送中断使能 */__HAL_USART_ENABLE_IT(&huart2,USART_IT_TXE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN)/* 485高电平发送*/HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_HIGH);#endif}else{/* 串口2发送中断关闭 */__HAL_USART_DISABLE_IT(&huart2,USART_IT_TXE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN) /* 485低电平接收*/HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_LOW);#endif}
}
改成
void vMBMasterPortSerialEnable(BOOL xRxEnable, BOOL xTxEnable)
{/* If xRXEnable enable serial receive interrupts. If xTxENable enable* transmitter empty interrupts.*/if(xRxEnable){#if defined(MODBUS_MASTER_USE_CONTROL_PIN) /* 485低电平接收 */HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_LOW);/* 添加小延时确保485切换完成 */for(volatile int i = 0; i < 100; i++);#endif/* 串口2接收中断使能 */__HAL_USART_ENABLE_IT(&huart2,USART_IT_RXNE); }else{/* 串口2接收中断关闭 */__HAL_USART_DISABLE_IT(&huart2,USART_IT_RXNE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN)/* 485高电平发送 */HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_HIGH);#endif}if(xTxEnable){#if defined(MODBUS_MASTER_USE_CONTROL_PIN)/* 485高电平发送*/HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_HIGH);/* 添加小延时确保485切换完成 */for(volatile int i = 0; i < 100; i++);#endif/* 串口2发送中断使能 */__HAL_USART_ENABLE_IT(&huart2,USART_IT_TXE); }else{/* 串口2发送中断关闭 */__HAL_USART_DISABLE_IT(&huart2,USART_IT_TXE); #if defined(MODBUS_MASTER_USE_CONTROL_PIN) /* 485低电平接收*/HAL_GPIO_WritePin(MODBUS_MASTER_GPIO_PORT,MODBUS_MASTER_GPIO_PIN,MODBUS_MASTER_GPIO_PIN_LOW);#endif}
}
就可以触发了,rx_int_count递增。
原因分析
您遇到的问题非常典型,涉及到RS-485半双工通信中一个关键且容易忽视的细节:方向控制引脚(DE/RE)切换与UART中断使能的时序问题。
问题根本原因分析
(暂不清楚,以下为ai的解释)
中断使能(__HAL_USART_ENABLE_IT
)的操作在GPIO方向切换(HAL_GPIO_WritePin
)之前。这会引发一个竞态条件(Race Condition):
- 使能接收中断的时刻:当调用
__HAL_USART_ENABLE_IT(&huart2, USART_IT_RXNE)
后,USART2的接收中断立即被打开。此时,USART硬件已经开始监听总线上的数据。 - 切换485方向的时刻:紧接着,代码才去设置GPIO引脚为接收模式(低电平)。从CPU执行完第一条指令到GPIO电平实际稳定、收发器切换到接收状态,存在一个极短但不可忽略的延迟。
在这个短暂的延迟窗口内,总线状态是不确定的:
- 如果总线上恰好有数据(例如,上一帧数据的残留、噪声、或者其他设备的发送),USART的接收寄存器(RXNE)会立即被置位。
- 由于接收中断已经使能,CPU会立刻响应并进入接收中断服务程序(ISR)。
- 然而,此时485收发器可能还未完全切换到接收模式,导致它无法正确地将总线差分信号传递给USART,或者传递的是错误的电平。
- 最终结果是,ISR读取到的可能是无效数据、帧错误(FE)、或噪音。更常见的是,这个过早触发的中断会清空RXNE标志,但并没有读到有效数据。而当Modbus协议栈真正开始等待响应时,由于标志位已被清除,无法再次触发中断,导致接收超时。
修改后的代码为什么能工作?
修改后的代码调整了时序:
- 先切换485方向:首先通过GPIO将收发器稳稳地设置为目标模式(接收或发送)。
- 添加短暂延时:使用
for
循环提供一个短暂的阻塞延时。这个延时确保了GPIO电平有足够的时间稳定下来,并且485收发器内部的电路已经完成了切换(通常需要几百纳秒到几微秒)。虽然用循环实现延时不是最优雅的方式(更好的做法是使用硬件定时器或精确延时函数),但它简单有效,解决了硬件切换的时序问题。 - 最后使能中断:在确保硬件已经完全准备好之后,再打开USART中断。这样,当中断使能时,总线状态和收发器状态都是稳定和正确的,只有在使能之后到来的新数据才会触发中断,从而避免了“旧”数据或噪音的误触发。
简单比喻:
- 修改前:就像先打开麦克风的开关(中断使能),然后再告诉别人“现在你可以对我说话了”(485切接收)。在你告诉他之前,任何环境噪音(总线数据)都会被麦克风录进去。
- 修改后:先告诉别人“现在你可以对我说话了”(485切接收),确保自己准备好了,然后再打开麦克风开关(中断使能)。这样录到的才是对方真正要对你说的话。
总结与建议
总结: 接收中断不触发的根本原因不是中断没打开,而是中断被过早地触发并在错误的时刻被处理掉了。修改后的代码通过调整GPIO操作和中断使能的顺序,并加入延时确保硬件稳定,解决了这个问题。
改进建议:
- 替换循环延时:
for
循环延时受编译器优化和CPU频率影响,不精确。建议使用HAL_Delay(1)
(延时1ms,对于485切换绰绰有余)或者查询硬件定时器来实现更可靠的微秒级延时。 - 检查中断优先级:确保USART中断的优先级设置合理,不会被其他高优先级中断长时间阻塞。
- 检查中断服务程序(ISR):确保在USART2的IRQHandler中正确处理了
USART_IT_RXNE
事件,并且清除了相应的标志位。