一、前言
在單片機中,USART通信是最常用也是最先去接觸的串口外設,在小數據量應用中一般不需要考慮USART串口(以下簡稱為串口)的高負載能力,比如打印一下log,接收幾個其他設備的指令或者發送幾個指令控制其他設備。但是在高速的大數據量的通信場合,串口可能會承載較高的數據負載,如果不合理地進行單片機的資源利用,則有可能造成各種問題。比如使用串口接收中斷接收大量的數據,頻繁地進入中斷,會占用太多的CPU資源。這時可能會想到【空閑中斷+DMA傳輸完成中斷】的方式接收大量數據,但是這是一個極具風險的行為,假設一下,DMA數據傳輸結束之后,此時CPU開始讀取DMA緩存中的數據,此時又有新的數據進來,新的數據就會覆蓋之前的數據導致異常。
二、如何啟用串口的DMA功能
在討論如何實現串口的高負載通信之前,我們得先明白如何啟用串口的DMA通信。
DMA(DirectMemoryAccess)直接儲存器訪問,是一個CPU用于數據從一個地址空間到另一個地址空間的搬運組件,該過程無需CPU的干預,不占用CPU的資源,可以使單片機這種單線程CPU實現“偽多線程”。只需在數據搬運結束后通知CPU即可。
在國民技術的資料中是有串口+DMA的例程的,但是官方為了用戶調試方便,例程相對簡單,就是實現了兩個MCU串口間的DMA通信,在開發時具有一定借鑒意義,但是不具備高負載能力,同時移植性不是很好,這里我在例程的基礎上進行簡化,同時例程不具備的功能也會一一展開。
1.串口+DMA發送
#defineTxBufferSize1 (countof(TxBuffer1) - 1) #definecountof(a) (sizeof(a) / sizeof(*(a))) USART_InitTypeUSART_InitStructure; uint8_tTxBuffer1[20] ={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a};
首先是定義一些相關的變量,數據和結構體啥的,TxBufferSize1發送數量,TxBuffer1[20]發送的數組。
/** *[url=home.php?mod=space uid=247401]@brief[/url] Configures thedifferent system clocks. */ voidRCC_Configuration(void) { /*DMA clock enable */ RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_DMA,ENABLE); /*Enable GPIO clock */ RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOB,ENABLE); /*Enable USARTy and USARTz Clock */ RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_USART1,ENABLE); } /** *[url=home.php?mod=space uid=247401]@brief[/url] Configures thedifferent GPIO ports. */ voidGPIO_Configuration(void) { GPIO_InitTypeGPIO_InitStructure; /*Initialize GPIO_InitStructure */ GPIO_InitStruct( GPIO_InitStructure); /*Configure USARTy Tx as alternate function push-pull */ GPIO_InitStructure.Pin = GPIO_PIN_6; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Alternate= GPIO_AF0_USART1; GPIO_InitPeripheral(GPIOB, GPIO_InitStructure); /*Configure USARTy Rx as alternate function push-pull and pull-up */ GPIO_InitStructure.Pin = GPIO_PIN_7; GPIO_InitStructure.GPIO_Pull = GPIO_Pull_Up; GPIO_InitStructure.GPIO_Alternate= GPIO_AF0_USART1; GPIO_InitPeripheral(GPIOB, GPIO_InitStructure); }
對相關的時鐘和串口的引腳進行初始化,這里是直接用的官方例程,只不過將官方例程的宏定義換成了實際的值,便于看代碼,不然還需跳轉,但是官方的例程這方面的可移植性會更好。
voidDMA_Configuration(void) { DMA_InitTypeDMA_InitStructure; /*USARTy TX DMA1 Channel (triggered by USARTy Tx event) Config */ DMA_DeInit(DMA_CH4); DMA_StructInit( DMA_InitStructure); DMA_InitStructure.PeriphAddr = (USART1_BASE + 0x04); DMA_InitStructure.MemAddr = (uint32_t)TxBuffer1; DMA_InitStructure.Direction = DMA_DIR_PERIPH_DST; DMA_InitStructure.BufSize = TxBufferSize1; DMA_InitStructure.PeriphInc = DMA_PERIPH_INC_DISABLE; DMA_InitStructure.DMA_MemoryInc = DMA_MEM_INC_ENABLE; DMA_InitStructure.PeriphDataSize= DMA_PERIPH_DATA_SIZE_BYTE; DMA_InitStructure.MemDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.CircularMode = DMA_MODE_NORMAL; DMA_InitStructure.Priority = DMA_PRIORITY_VERY_HIGH; DMA_InitStructure.Mem2Mem = DMA_M2M_DISABLE; DMA_Init(DMA_CH4, DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_TX,DMA, DMA_CH4, ENABLE); }
DMA的初始化采用NORMAL模式,即只發送一次,當計數器為0時便不再搬運數據。
voidUART_Init(USART_Module* USARTx,uint32_t BaudRate) { /*USARTy and USARTz configuration ---------------------------*/ USART_StructInit( USART_InitStructure); USART_InitStructure.BaudRate = BaudRate; USART_InitStructure.WordLength = USART_WL_8B; USART_InitStructure.StopBits = USART_STPB_1; USART_InitStructure.Parity = USART_PE_NO; USART_InitStructure.HardwareFlowControl= USART_HFCTRL_NONE; USART_InitStructure.Mode = USART_MODE_RX | USART_MODE_TX; /*Configure USARTy and USARTz */ USART_Init(USARTx, USART_InitStructure); /*Enable USARTy DMA Rx and TX request */ USART_EnableDMA(USARTx,USART_DMAREQ_RX | USART_DMAREQ_TX, ENABLE); /*Enable the USARTy and USARTz */ USART_Enable(USARTx,ENABLE); }
串口的初始化。
voidDMA_send(uint8_t* pBuffer,uint16_t BufferLength) { DMA_EnableChannel(DMA_CH4,DISABLE); DMA_SetCurrDataCounter(DMA_CH4,BufferLength); DMA_EnableChannel(DMA_CH4,ENABLE); while(USART_GetFlagStatus(USART1, USART_FLAG_TXDE) == RESET) { } }
DMA的發送函數,先失能DMA通道,再重新設置傳輸長度,再使能DMA通道,這里是檢測while是檢測串口的發送完成編制位,在官方的demo中檢測的是DMA的通道完成標志,這個在這里面是不可以的,因為DMA的搬運速度是遠大于串口的通信速度的,如果檢測DMA通道完成標志,會導致DMA已經將數據搬運到串口的數據寄存器,但是因為串口的速度不夠,導致此時數據還未送出,而因為例程只循環一次,在測試例程時看不出問題,但是這里會出問題。
intmain(void) { /*System Clocks Configuration */ RCC_Configuration(); /*Configure the GPIO ports */ GPIO_Configuration(); /*Configure the DMA */ DMA_Configuration(); UART_Init(USART1,115200); while(1) { DMA_send(TxBuffer1,20); Delay(10000000); } }
最后在主函數調用各初始化函數,在while(1)中循環發送便可實現最簡單的串口+DMA發送。
2.串口+DMA接收
在上面發送的基礎上我們加上DMA的接收功能,此處需要解釋一下下面的操作:為了對應手冊,上面的串口發送DMA通道原來是CH4,我下面全部改成CH1。
uint8_tRxBuffer1[20];
定義一個數組用于接收串口數據。
USART_ConfigInt(USARTx,USART_INT_IDLEF, ENABLE);
添加串口中斷定義。
voidNVIC_Configuration(void) { NVIC_InitTypeNVIC_InitStructure; /*Enable the USARTz Interrupt */ NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 0; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init( NVIC_InitStructure); }
添加NVIC配置。
voidDMA_Configuration(void) { DMA_InitTypeDMA_InitStructure; /*USARTy TX DMA1 Channel (triggered by USARTy Tx event) Config */ DMA_DeInit(DMA_CH1); DMA_StructInit( DMA_InitStructure); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)TxBuffer1; DMA_InitStructure.Direction= DMA_DIR_PERIPH_DST; DMA_InitStructure.BufSize= TxBufferSize1; DMA_InitStructure.PeriphInc= DMA_PERIPH_INC_DISABLE; DMA_InitStructure.DMA_MemoryInc= DMA_MEM_INC_ENABLE; DMA_InitStructure.PeriphDataSize= DMA_PERIPH_DATA_SIZE_BYTE; DMA_InitStructure.MemDataSize= DMA_MemoryDataSize_Byte; DMA_InitStructure.CircularMode= DMA_MODE_NORMAL; DMA_InitStructure.Priority= DMA_PRIORITY_VERY_HIGH; DMA_InitStructure.Mem2Mem= DMA_M2M_DISABLE; DMA_Init(DMA_CH1, DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_TX,DMA, DMA_CH1, ENABLE); DMA_DeInit(DMA_CH2); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)RxBuffer1; DMA_InitStructure.Direction= DMA_DIR_PERIPH_SRC; DMA_InitStructure.BufSize= TxBufferSize1; DMA_Init(DMA_CH2, DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_RX,DMA, DMA_CH2, ENABLE); }
添加DMA的接收,并將通道設置為CH2。
voidDMA_Revice(uint16_t BufferLength) { DMA_EnableChannel(DMA_CH2,DISABLE); DMA_SetCurrDataCounter(DMA_CH2,BufferLength); DMA_EnableChannel(DMA_CH2,ENABLE); }
添加DMA接收函數
voidUSART1_IRQHandler(void) { if(USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET) { /*軟件先讀USART_STS,再讀USART_DAT清除空閑中斷標志。*/ USART1->STS; USART1->DAT; for(inti=0;i<20;i++) { TxBuffer1[i]= RxBuffer1[i]; } DMA_send(20); DMA_Revice(20); } }
添加串口中斷函數,在串口中斷函數中將接收的數據傳給DMA發送數組,再通過DMA的方式發送出來用于校驗結果。
通過串口助手可觀測數據正確。至此,常見的串口+DMA的發送與接收完成。后文將實現高負載的通信。
三、高負載情況下的DMA如何實現
在串口數據量較大時,一般使用雙BUF,很多單片機有硬件雙緩沖,DMA的目標儲存區域有兩個,當一次完整的數據傳輸結束后,也就是counter值變為0時,DMA會自動將數據指向另一塊區域。這樣用戶就有時間去處理剛存滿的buf,而不會被覆蓋。就是“乒乓緩存”。
普通DMA
DMA雙緩沖
大致流程如下:
1.串口有數據到來,DMA現將數據儲存在內存1,完成后通知CPU過來處理數據。
2.此時DMA不停下,開始將后續的數據搬運到內存2。
3.內存2的數據搬運完成,通知CPU開始處理內存2中的數據。
4.如果數據傳輸還未結束,此時DMA會將數據儲存在內存1。如此循環,直至沒有數據到來。
但是遺憾的是N32G435這塊芯片不具備雙緩沖模式,那么我們可以主動控制DMA跳轉內存區域。利用“傳輸過半中斷”來模擬雙緩沖模式。
大致流程如下:
1.DMA完成搬運一半的數據時,產生一個傳輸過半中斷,此時我們讓CPU來處理上一半數據。
2.DMA數據搬運未停止,此時繼續搬運后一半數據,此操作不會影響前面一半的數據處理。
3.DMA數據搬運完,觸發傳輸完成中斷,這時CPU可以處理后半數據。
4.如果數據傳輸還未結束,DMA繼續將數據向前半搬運,如此循環。
代碼講解如下:
以下代碼完整流程如下:
1.配置串口波特率2.5M,DMA的BufSize設置為40,開啟傳輸過半中斷,傳輸完成中斷,串口空閑中斷。
2.啟動DMA接收。
3.通過串口助手發送80個數據到串口。
4.當DMA接收數組接收到20個數據觸發傳輸過半中斷,跳轉中斷函數將20個數據存放到數組中。
5.此時DMA仍在運行,但是數據存放在DMA接收數組的后20個地址空間。
6.當DMA接收數組填滿,觸發DMA傳輸完成中斷,跳轉中斷函數將后20個數據保存,此時DMA一共搬運了40個數據。
7.DMA繼續搬運數據到接收數組里,此時會覆蓋之前的前二十個數據,跳轉到步驟4.
8.接收完80個數據,此時觸發串口空閑中斷,將接收到的數據打印出來。
在上面代碼基礎上做如下操作:
1.將DMACH2通道設置為循環模式,測試階段將BufSize設置為40,開啟傳輸過半中斷和傳輸完成中斷。同時為了測試高速場景,串口波特率設置為2.5M:
DMA_DeInit(DMA_CH2); DMA_InitStructure.PeriphAddr= (USART1_BASE + 0x04); DMA_InitStructure.MemAddr= (uint32_t)buffer; DMA_InitStructure.Direction= DMA_DIR_PERIPH_SRC; DMA_InitStructure.BufSize= 40; DMA_InitStructure.CircularMode= DMA_MODE_CIRCULAR; DMA_Init(DMA_CH2, DMA_InitStructure); DMA_RequestRemap(DMA_REMAP_USART1_RX,DMA, DMA_CH2, ENABLE); DMA_ConfigInt(DMA_CH2,DMA_INT_HTX,ENABLE);//半傳輸中斷 DMA_ConfigInt(DMA_CH2,DMA_INT_TXC,ENABLE);//傳輸完成中斷 DMA_ClearFlag(DMA_FLAG_HT2,DMA);//清除標志位,避免第一次傳輸出錯 DMA_ClearFlag(DMA_FLAG_TC2,DMA); DMA_ClrIntPendingBit(DMA_INT_HTX2,DMA); DMA_ClrIntPendingBit(DMA_INT_TXC2,DMA);
2.NVIC設置DMA通道中斷
voidNVIC_Configuration(void) { NVIC_InitTypeNVIC_InitStructure; /*Enable the USARTz Interrupt */ NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 1; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init( NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel= DMA_Channel2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority= 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority= 0; NVIC_InitStructure.NVIC_IRQChannelCmd= ENABLE; NVIC_Init( NVIC_InitStructure); }
3.添加DMA的CH2中斷函數,num為全局變量,目的是將所有的數據保存進buf數組:
voidDMA_Channel2_IRQHandler(void) { //傳輸半滿 if(DMA_GetIntStatus(DMA_INT_HTX2,DMA)== SET) { DMA_ClrIntPendingBit(DMA_INT_HTX2,DMA); DMA_ClearFlag(DMA_FLAG_HT2,DMA); for(inti=0;i<20;i++) { buf[num]= buffer[i]; num++; } } //傳輸滿 if(DMA_GetIntStatus(DMA_INT_TXC2,DMA)== SET) { DMA_ClrIntPendingBit(DMA_INT_TXC2,DMA); DMA_ClearFlag(DMA_FLAG_TC2,DMA); for(inti=20;i<40;i++) { buf[num]= buffer[i]; num++; } } }
4.在串口空閑中斷中將收到的數據全部打印出來。
voidUSART1_IRQHandler(void) { if(USART_GetIntStatus(USART1, USART_INT_IDLEF) != RESET) { /*軟件先讀USART_STS,再讀USART_DAT清除空閑中斷標志。*/ USART1->STS; USART1->DAT; for(inti=0;i<80;i++) { TxBuffer1[i]= buf[i]; } DMA_send(80); num=0; } }
5.測試結果如下,在2.5M波特率的情況下保持數據完整。
寫在最后
這次主要討論了一種高負載情況下如何緩解CPU壓力的方法,所言所寫不盡完善,例如不定數據接收,就可以通過DMA_GetCurrDataCounter(DMA_CH2);函數進行傳輸數據的統計計算,這點大家可以自由發揮,現實可能遇到的問題是多種多樣的,主要在于關鍵能力的拓展。更多的還需要根據實際情況靈活配置。
來源:Nations加油站
審核編輯:湯梓紅
-
單片機
+關注
關注
6037文章
44563瀏覽量
635876 -
串口
+關注
關注
14文章
1555瀏覽量
76584 -
串口通信
+關注
關注
34文章
1626瀏覽量
55560 -
dma
+關注
關注
3文章
561瀏覽量
100622
發布評論請先 登錄
相關推薦
評論