接收中斷不觸發
前情提要
在自己的開發板上移植了野火的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
事件,并且清除了相應的標志位。