導言
《[[STM32F103_LL庫+寄存器學習筆記09 - DMA串口接收與DMA串口發送,串口接收空閑中斷]]》上一章節完成DMA發送與接收。此時,有一個致命的問題可能會導致數據包丟失。原因是USART1接收只開啟了接收空閑中斷(IDLE),DMA在連續模式下,如果數據一直持續發送(或者數據包的大小比接收緩存區要大),將會出現數據被覆蓋,被覆蓋的數據等于丟失的數據。
實現更加健壯的串口接收程序是同時利用USART空閑中斷來判斷數據包的結束,并配合DMA半傳輸和傳輸完成中斷及時處理數據,從而避免數據溢出或被覆蓋。 總的來說就是接收空閑中斷 + DMA傳輸過半中斷 + DMA傳輸完成中斷。
方案如下:
- 開啟接收空閑中斷
- 當數據傳輸過程中出現空閑(即一段數據傳輸結束后總線靜默),USART空閑中斷就會觸發。這時,立即關閉DMA,讀取DMA剩余計數器(或利用緩沖區總大小減去剩余計數得到實際接收字節數),將這部分數據交給上層處理,然后清除中斷標志并重新啟動DMA。這樣即使數據包不足一個完整緩沖區,也能準確提取數據。
- 開啟半傳輸和全傳輸中斷
- 半傳輸中斷:當DMA接收到緩沖區前半部分的數據時觸發。此時可以先處理前半部分的數據,避免數據不斷寫入后覆蓋還未處理的數據。
- 傳輸完成中斷:當DMA完成整個緩沖區的數據接收時觸發,同樣可處理后半部分數據。這種“雙緩沖”技術允許你在數據接收的同時就將已接收部分取出處理,降低丟包風險。
總的來說:
- 對于連續且數據量較大的情況,DMA半傳輸和傳輸完成中斷能保證數據能及時被輪換處理,不會因緩沖區滿而丟失數據。
- 對于數據量較小或者數據傳輸間有間隔的情況,空閑中斷能夠準確捕獲數據包結束時刻,防止因數據不足而不觸發DMA半/全傳輸中斷而導致數據滯留。
另外,中斷的處理必須保證簡單,盡可能保證快進快出。 所以,數據的處理必須留在主循環來做(配合高效的數據結構ringbuffer)。后續章節會介紹ringbuffer的移植與使用。
以115200波特率計算(假設每字節約10位,即包括起始、數據、停止位),理論上每秒可以傳輸約11520字節,也就是每毫秒大約11.5個字節。512個字節大概需要44~46毫秒填滿。當然,這個值會依據具體的幀配置(比如數據位、停止位、校驗位)略有不同。所以,如果數據量很大(一直連續)當觸發傳輸過半中斷時,要在大概44ms內把數據搬運出去,否則會被覆蓋。
效果如下所示:
項目地址:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_ll_library10_usart_dma_rx_interrupt
一、代碼(LL庫)
1.1、main.c
1.2、usart.c
1.3、stm32f1xx_it.c
1.4、編譯、下載
如上所示,編譯通過。
效果如上所示,大量發送數據包,不丟包!
二、調試傳輸過半中斷與傳輸完成中斷
調試傳輸過半中斷與傳輸完成中斷之前,先被USART1的接收空閑中斷關閉掉,從而方便你單獨調試這兩個中斷。如下所示:
2.1、傳輸過半中斷
如上所示,從串口助手發送512個字節到STM32F103,觸發了DMA1通道5的傳輸過半中斷。從寄存器表格看到,HTIF5確認被置1了。
如上所示,代碼LL_DMA_ClearFlag_HT5(DMA1)
運行完,HTIF5標志位確實被清0了。
如上所示,串口助手發送512個字節給STM32F103,讓它觸發DMA1通道5的傳輸過半中斷。在中斷里將前半段512個字節讓DMA1通道4回傳給電腦的串口助手。數據傳輸數量寄存器CNDTR5從0x400(1024)變成0x200(512),寫指針指向接收緩存區的偏移512。從整個過程看來,傳輸過半中斷程序調試OK。
2.2、傳輸完成中斷
如上所示,串口助手再一次發送512個字節給STM32F103后,進入傳輸完成中斷。此時,TCIF5被置1。
如上所示,代碼LL_DMA_ClearFlag_TC5(DMA1)
運行之后,TCIF5標志被清0。
如上所示,將斷點放開后,STM32F103將第二次收到的512個字節回傳給電腦的串口助手。此時,寄存器CNDTR5從0x200(512)變回最開始的0x400(1024)。從整個過程看來,傳輸完成中斷正常!
三、寄存器梳理
3.1、啟動傳輸過半中斷、傳輸完成中斷
如上所示,寄存器DMA_CCR5的位2與位1置1就可以打開半傳輸中斷與傳輸完成中斷。
// 增加傳輸完成與傳輸過半中斷
DMA1_Channel5->CCR |= (1UL << 1); // 傳輸完成中斷 (TCIE)
DMA1_Channel5->CCR |= (1UL << 2); // 傳輸過半中斷 (HTIE)
3.2、全局中斷DMA1_Channel5_IRQHandler()里判斷傳輸過半標志與傳輸完成標志
void DMA1_Channel5_IRQHandler(void) {// 半傳輸中斷if (DMA1->ISR & (1UL << 18)) {DMA1->IFCR |= (1UL << 18); // 清除標志}// 傳輸完成中斷if (DMA1->ISR & (1UL << 17)) {DMA1->IFCR |= (1UL << 17); // 清除標志}// 改為if...else if,中斷運行的時間更短
}
如上所示,通過寄存器ISR判斷是否進入該中斷,接著使用寄存器IFCR來清除中斷標志位。
四、代碼(寄存器)
4.1、main.c
4.2、stm32f1xx_it.c
4.3、編譯、下載
4.4、串口助手調試
如上所示,效果跟LL庫一樣。
五、細節補充
5.1、當接收緩存區大小1024bytes,剛好收到一幀大小512bytes數據時,會怎樣???
當接收緩存區大小1024bytes,剛好收到一幀大小512bytes數據時,將會進入傳輸過半中斷,然后再進入接收空閑中斷。 然后,傳輸過半中斷與接收空閑中斷都有將接收緩存區復制到發送緩存區的功能,會復制兩遍數據給發送緩存區嗎?代碼處理好了,不會! 以下是接收空閑中斷里的某部分代碼。
// 根據剩余字節判斷當前正在哪個半區
// 還有,避免當數據長度剛好512字節與1024字節時,傳輸過半中斷與空閑中斷復制兩遍數據,與傳輸完成中斷與空閑中斷復制兩遍數據。
if (remaining > (RX_BUFFER_SIZE/2)) {// 還在接收前半區:接收數據量 = (1K - remaining),但肯定不足 512 字節count = RX_BUFFER_SIZE - remaining;if (count != 0) { // 避免與傳輸完成中斷沖突,多復制一次memcpy((void*)tx_buffer, (const void*)rx_buffer, count);}
} else {// 前半區已寫滿,當前在后半區:后半區接收數據量 = (RX_BUFFER_SIZE/2 - remaining)count = (RX_BUFFER_SIZE/2) - remaining;if (count != 0) { // 避免與傳輸過半中斷沖突,多復制一次memcpy((void*)tx_buffer, (const void*)(rx_buffer + RX_BUFFER_SIZE/2), count);}
}
if (count != 0) {recvd_length = count;rx_complete = 1;
}
假設RX_BUFFER_SIZE為1024,當串口助手發送512個字節時,DMA剩余計數remaining正好為512。此時remaining不大于512,所以進入else分支,計算得到count = (1024/2) - 512 = 512 - 512 = 0。因此不會再復制數據,也就避免了重復拷貝前半段數據。
當接收緩存區大小1024bytes,剛好收到一幀大小1024bytes數據時,發生順序大致如下:
- 當接收到前512字節時,DMA觸發半傳輸中斷,此時會調用對應中斷服務程序,將前半區(偏移0~511)的數據復制到發送緩沖區,并設置接收完成標志。
- 當接收到后512字節時,DMA觸發傳輸完成中斷,同樣將后半區(偏移512~1023)的數據復制到發送緩沖區(注意,如果使用同一個tx_buffer,則需要根據應用需求處理兩次復制的數據是合并還是分別處理)。
- 如果此時串口沒有繼續接收數據,USART1空閑中斷也可能觸發。此時,在空閑中斷處理代碼中,會先禁用DMA,然后通過下面這段代碼來計算“新增”的數據長度:
// 禁用 DMA1 通道5,防止數據繼續寫入
LL_DMA_DisableChannel(DMA1, LL_DMA_CHANNEL_5);
uint16_t remaining = LL_DMA_GetDataLength(DMA1, LL_DMA_CHANNEL_5); // 獲取剩余的容量
uint16_t count = 0;
// 根據剩余字節判斷當前正在哪個半區
// 還有,避免當數據長度剛好512字節與1024字節時,傳輸過半中斷與空閑中斷復制兩遍數據,與傳輸完成中斷與空閑中斷復制兩遍數據。
if (remaining > (RX_BUFFER_SIZE/2)) {// 還在接收前半區:接收數據量 = (1K - remaining),但肯定不足 512 字節count = RX_BUFFER_SIZE - remaining;if (count != 0) { // 避免與傳輸完成中斷沖突,多復制一次memcpy((void*)tx_buffer, (const void*)rx_buffer, count);}
} else {// 前半區已寫滿,當前在后半區:后半區接收數據量 = (RX_BUFFER_SIZE/2 - remaining)count = (RX_BUFFER_SIZE/2) - remaining;if (count != 0) { // 避免與傳輸過半中斷沖突,多復制一次memcpy((void*)tx_buffer, (const void*)(rx_buffer + RX_BUFFER_SIZE/2), count);}
}
if (count != 0) {recvd_length = count;rx_complete = 1;
}
// 重新設置 DMA 傳輸長度并使能 DMA
LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_5, RX_BUFFER_SIZE);
LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_5);
對于1024字節的情況,DMA工作在循環模式下,當完整數據傳輸完成后,DMA的計數器會重新加載為 RX_BUFFER_SIZE(即1024),因此當空閑中斷進入處理時:
- remaining 將等于1024,
- 如果進入第一個分支,則計算得到 count = RX_BUFFER_SIZE - remaining = 1024 - 1024 = 0,
- 如果進入第二個分支(一般不會出現,因為remaining不小于512),同樣結果為0。
因此,空閑中斷處理代碼不會再復制數據,也不會重復處理前半區數據。總結來說,當剛好發送1024字節時: - 半傳輸中斷處理了前512字節,
- 傳輸完成中斷處理了后512字節,
- 空閑中斷因計算出的新接收字節數為0而不再進行復制。
這就避免了數據重復拷貝的問題!