
《極海芯得》系列內容為用戶使用極海系列產品的經驗總結,均轉載自21ic論壇極海半導體專區,全文未作任何修改,未經原文作者授權禁止轉載。
一、前言
1.1 應用場景
UART是一種應用廣泛的通信接口,使用簡單,絕大部分MCU都有這種接口。
MCU端如果想接收串口的數據,簡單的做法是在“接收寄存器非空”中斷中一個字節一個字節的接收,這種方式代碼處理起來簡單些,一般情況下夠用,但是如果接收的數據量非常大就不太合適,此外,這種方式需要CPU頻繁處理中斷,占用大量CPU時間不說,MCU性能低點還容易丟數據。
這是就需要用DMA來進行批量數據的接收,APM32的大部分UART都配有DMA,利用UART的空閑中斷和DMA傳輸完成中斷實現一次中斷接收多個數據。
1.2 APM32的相關資源
想用DMA接收前提是改UART支持DMA功能,對于APM32E103來說除了UART5,其他串口都是有DMA功能的。

這次我們用DMA進行多個數據接收,串口中斷源只需要用到"空閑中斷"即可。DMA傳輸完成中斷只有在傳輸完特定數量數據后才觸發,接收到的數據不足則不觸發;而空閑中斷則在超過1個字節時間沒有收到數據時觸發,空閑中斷還是很有必要,因為對方發過來的數據往往是隨機的。

其中,“總線空閑”(IDLE)正是我們所要使用的關鍵中斷源。手冊里對空閑幀的處理機制說明如下:

對應的狀態標志位定義如下,當檢測到空閑總線時硬件會置位該標志:

這次我們以USART1為例,USART1的RX對應是DMA1的通道5。

二、乒乓操作算法
在用DMA接收數據如果只有一個數組進行緩存,那存在一個問題,在處理這些剛數據時,后面發過來的數據會覆蓋這個緩存,導致數據錯亂;如果直接在中斷服務中先處理數據再開啟下一次DMA傳輸,則很可能因為DMA開晚了導致丟失部分數據。
想解決以上問題,最好就是使用雙緩存組成乒乓操作,DMA傳輸使用一個緩存,數據的解析解析處理使用另一個緩存,每次DMA傳輸完成修改地址切換成另一個緩存,這樣一個緩存用于寫入,另一個緩存用于讀取,解決了接收速度和解析速度不匹配的問題,這種操作也叫乒乓操作。

弄懂乒乓操作后,下面就來編寫代碼,先建立一個乒乓數據類型,用來存儲該算法需要的所有相關信息。
#define MaxBufferSize 100 //單次最大接收長度
typedef struct
{
unsigned char buffer[2][MaxBufferSize+4];
unsigned int length[2];
unsigned char w_index;
unsigned char r_index;
}PingpongDef;
為了防止某些變量默認值不是0而產生一些BUG,編寫一個初始化函數進行關鍵變量清零:
//乒乓緩存初始化
void pingpong_init(void)
{
comBuf.w_index = 0;
comBuf.r_index = 0;
comBuf.length[0] = 0;
comBuf.length[1] = 0;
}
編寫緩存的寫入函數,實現DMA傳輸完成中斷或串口空閑中斷的乒乓操作,寫操作主要是要記錄此次接收數據長度并切換DMA傳輸地址。
//乒乓緩存寫
//unsigned int len: 本次寫入的數據長度
//返回:下次寫入的緩存地址
unsigned char *pingpong_write(unsigned int len)
{
comBuf.length[comBuf.w_index] = len;
comBuf.r_index = comBuf.w_index;
comBuf.w_index ^= 0x01;
return comBuf.buffer[comBuf.w_index];
}
編寫緩存的讀取函數,實現在while(1)中的數據解析前的乒乓操作,讀操作時先判斷這次的數據長度,如果確實有數據則輸出這次數據的長度和指針,給后面代碼使用。
//乒乓緩存讀
//unsigned int *len:讀出的數據長度
//返回:讀出的數據地址
unsigned char *pingpong_read(unsigned int *len)
{
unsigned char *p;
*len = comBuf.length[comBuf.r_index];
if (*len == 0)
{
p = NULL;
}
else
{
p = comBuf.buffer[comBuf.r_index];
comBuf.length[comBuf.r_index] = 0;
}
return p;
}
這三個函數屬于純算法,可以用于后面代碼調用。
void pingpong_init(void);
unsigned char *pingpong_write(unsigned int len);
unsigned char *pingpong_read(unsigned int *len);
三、UART和DMA的代碼
3.1、UART和DMA的初始化
首先是USART1的配置,這部分和正常的USART配置相同。
接著是DMA的配置,也就是 DMA1_Channel5 的配置,外設地址設為USART的基地址+0x04。

外設地址不增,儲存地址自增,數據大小都設置為字節,循環模式設置為禁止,內存地址設為之前的乒乓緩存的第1個數組,內存大小也就是DMA單次最大傳輸長度,這里設為100,傳輸方向設為從外設到內存。
最后使能USART的DMA接收功能,使能DMA通道,使能DMA的傳輸完成中斷,使能串口的空閑中斷。
//UART + DMA初始化
void uart_dma_init(void)
{
GPIO_Config_T GPIO_ConfigStruct;
USART_Config_T USART_ConfigStruct;
RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOA | RCM_APB2_PERIPH_USART1);
//USART config
GPIO_ConfigStruct.mode = GPIO_MODE_AF_PP;
GPIO_ConfigStruct.pin = GPIO_PIN_9;
GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;
GPIO_Config(GPIOA, &GPIO_ConfigStruct);
GPIO_ConfigStruct.mode = GPIO_MODE_IN_FLOATING;
GPIO_ConfigStruct.pin = GPIO_PIN_10;
GPIO_ConfigStruct.speed = GPIO_SPEED_10MHz;
GPIO_Config(GPIOA, &GPIO_ConfigStruct);
USART_ConfigStruct.baudRate = UartBaudrate;
USART_ConfigStruct.hardwareFlow = USART_HARDWARE_FLOW_NONE;
USART_ConfigStruct.mode = USART_MODE_TX_RX;
USART_ConfigStruct.parity = USART_PARITY_NONE;
USART_ConfigStruct.stopBits = USART_STOP_BIT_1;
USART_ConfigStruct.wordLength = USART_WORD_LEN_8B;
USART_Config(USART1, &USART_ConfigStruct);
USART_Enable(USART1);
//DMA config
RCM_EnableAHBPeriphClock(RCM_AHB_PERIPH_DMA1);
DMA_Reset(DMA1_Channel4);
DMA_Reset(DMA1_Channel5);
DMA_Config_T DMA_ConfigStruct;
DMA_ConfigStruct.peripheralBaseAddr = (uint32_t)(USART1_BASE + 0x04);
DMA_ConfigStruct.peripheralInc = DMA_PERIPHERAL_INC_DISABLE;
DMA_ConfigStruct.memoryInc = DMA_MEMORY_INC_ENABLE;
DMA_ConfigStruct.peripheralDataSize = DMA_PERIPHERAL_DATA_SIZE_BYTE;
DMA_ConfigStruct.memoryDataSize = DMA_MEMORY_DATA_SIZE_BYTE;
DMA_ConfigStruct.loopMode = DMA_MODE_NORMAL;
DMA_ConfigStruct.M2M = DMA_M2MEN_DISABLE;
DMA_ConfigStruct.priority = DMA_PRIORITY_HIGH;
//USART1_RX DMA1_CH5
DMA_ConfigStruct.memoryBaseAddr = (uint32_t)comBuf.buffer[0];;
DMA_ConfigStruct.bufferSize = MaxBufferSize;
DMA_ConfigStruct.dir = DMA_DIR_PERIPHERAL_SRC;
DMA_Config(DMA1_Channel5, &DMA_ConfigStruct);
USART_EnableDMA(USART1, USART_DMA_RX);
DMA_Enable(DMA1_Channel5); //使能DMA通道
DMA_EnableInterrupt(DMA1_Channel5, DMA_INT_TC);
USART_EnableInterrupt(USART1, USART_INT_IDLE);
NVIC_EnableIRQRequest(USART1_IRQn, 0, 1);
NVIC_EnableIRQRequest(DMA1_Channel5_IRQn, 0, 0);
}
3.2 中斷服務函數的處理
在兩個中斷服務函數中還要實現數據的接收操作,首先是串口空閑中斷,進入串口空閑中斷后,先清除空閑中斷標志,然后讀取DMA的傳輸長度,接著獲取下一個待切換的緩存地址,重新配置長度并開啟DMA通道,中斷中的代碼盡量少,避免執行時間過長而錯過數據。
//串口接收空閑中斷
void USART1_IRQHandler(void)
{
if (USART_ReadStatusFlag(USART1, USART_FLAG_IDLE) != RESET)
{
USART_ClearIntFlag(USART1, USART_INT_IDLE);
USART_RxData(USART1);
DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
unsigned short len = MaxBufferSize - DMA1_Channel5->CHNDATA; //獲取本次DMA傳輸長度
unsigned char *p = pingpong_write(len);
DMA1_Channel5->CHMADDR = (unsigned int)p;
DMA1_Channel5->CHNDATA = MaxBufferSize;
DMA1_Channel5->CHCFG_B.CHEN = ENABLE;
}
}
DMA傳輸完成中斷的操作和串口空閑中斷差不多,不同的只是前面是清除DMA的中斷標志。
//DMA接收傳輸完成中斷
void DMA1_Channel5_IRQHandler(void)
{
if (DMA_ReadIntFlag(DMA1_INT_FLAG_TC5) != RESET)
{
DMA_ClearIntFlag(DMA1_INT_FLAG_TC5);
DMA1_Channel5->CHCFG_B.CHEN = DISABLE;
unsigned short len = MaxBufferSize - DMA1_Channel5->CHNDATA; //獲取本次DMA傳輸長度
unsigned char *p = pingpong_write(len);
DMA1_Channel5->CHMADDR = (unsigned int)p;
DMA1_Channel5->CHNDATA = MaxBufferSize;
DMA1_Channel5->CHCFG_B.CHEN = ENABLE;
}
}
四、批量數據接收測試
上面已經實現乒乓寫入操作,剩下的就在while(1)中實現乒乓的讀取操作,例如對數據進行解析。這里只演示數據的接收數量,把本次接收的數據量和總計接收的數據量通過printf打印出來。
//接收數據的處理..
void buffer_process(void)
{
static unsigned int total_len = 0;
unsigned int cur_len;
unsigned char *buf;
buf = pingpong_read(&cur_len);
if (buf)
{
total_len += cur_len;
printf("%d/%d\r\n", cur_len, total_len); //打印輸出本次讀取長度和總長度
}
}
最后用串口助手發送一些數據進行測試,每一次都能收數據量都正確。
最重要的測試是發送一個文件,模擬批量數據的發送,以便能測試APM32的串口接收能力。發送一個64KB的文件,最終接收到的數據總數是65536,剛好不多不少。

注:文章作者在原帖中提供了代碼文件,有需要請至原文21ic論壇
原文地址:
https://bbs.21ic.com/icview-3499758-1-3.html?_dsign=b03b3d40
或點擊下方 閱讀原文 跳轉
↑↑↑ 點擊上方卡片關注極海 ↑↑↑