我在我的 BMS 系統(tǒng)中使用了隔離的串口來進行通信,BMS 的主控芯片我選擇了芯源的 CW32L031,這是一款 TSSOP20 封裝的 M0 內核的低功耗單片機,資源不多,主頻也不是很高,但是功耗低確是我最重要的需求。
為了降低功耗,我將主頻設置的比較低,因此在走串口通信的時候,如果使用串口的中斷來進行發(fā)送和接收,那么每接收一個字符就會產生一個中斷,這樣頻繁的中斷肯定是 CPU 不厭其煩的,于是我選擇使用 DMA 充當一次串口的緩沖助手。
一、 什么是 DMA
DMA(Direct Memory Access,直接存儲器訪問)提供在外設與內存、存儲器和存儲器、外設與外設之間的高速數據傳輸使用。它允許不同速度的硬件裝置來溝通,而不需要依賴于 CPU ,在這個時間中,CPU 對于內存的工作來說就無法使用。
其實,我們可以簡化一下對 DMA 功能的描述,DMA 的主要工作就是搬磚,我只需要設置好幾個簡單的參數,DMA 就可以幫我們把數據從一個地址搬運到另一個地址,就是這么簡單。
這里有三種情況,分別是內存到內存,內存到外設,外設到內存。
要想讓這個這個蘑菇頭好好的幫我們搬磚,首先我們需要設置以下幾個參數:
源地址(從哪里取磚塊)目的地址(把磚塊放到哪里)傳輸帶寬(一次搬幾塊)源地址是否增加(磚塊是排列好的,還是一塊接著一塊吐出來的)目的地址是否增加(磚塊是排列碼好,還是一塊一塊的丟進一個洞里)觸發(fā)源(誰發(fā)指令開始搬)一次性傳輸量(一共搬多少次)
下面,我們就從一個工地的場景來了解一下,蘑菇頭是如何執(zhí)行任務來減輕總工的工作負荷的。首先,對于蘑菇頭來說,他的主要任務就是把磚從一個地方搬運到另一個地方,只要總工開始給他講好怎么搬,蘑菇頭就能摒棄一切雜念,任勞任怨的快樂搬磚。最簡單的是內存搬運,也就是把磚從一塊空地搬運到另一塊空地,這時候我們只需要跟蘑菇頭說:蘑菇頭,你過來,你今天的任務是把 A 區(qū)域(源地址)的500 塊(傳輸量)磚搬運到 B 區(qū)域(目的地址),你每次搬 2 塊(傳輸帶寬),同時要保證他們在 AB 區(qū)域的碼放是相同的(源和目的地址同步增加)。現(xiàn)在我只要一喊:“開始”(軟件觸發(fā)),你就馬上按我說的給我搬磚,我會先去忙點別的事。
這不,總工就可以有喝茶的時間了嘛!似乎只是這么倒騰磚的意義不大,我們假設現(xiàn)在有一個機器來負責把磚頭從車上卸下來,然后我們讓蘑菇頭把機器卸下來的磚整整齊齊的碼放在 B 區(qū)域。這個時候我們可以告訴蘑菇頭,要從機器出口那里搬(源地址不增加),這次因為機器一次只能吐出一塊磚來,所以你一次只搬一塊(傳輸帶寬),然后按順序碼放到 B 區(qū)域。今天我也不陪你了,機器出磚的時候會鳴笛(硬件觸發(fā)),你聽到鳴笛就開始搬就可以了。哦,對了,你搬完 500 塊排滿一層后,就重新開始在上面再排一層(循環(huán)模式),我回來之前你不許停。
二、串口的問題
上面我們了解了 DMA 的工作過程,然后我們就用它來幫我們解決一下我們的串口問題。假設我們讓蘑菇頭幫我們把串口接收的數據先搬運到內存中的一個緩沖區(qū),比如這個緩沖區(qū)是 64 個字節(jié),那么我們就可以在主循環(huán)中區(qū)查詢這個緩沖區(qū)而不用每個字節(jié)都進入中斷處理。即便我們的 DMA 沒有循環(huán)模式,我們也只需要在 DMA 傳輸完成中斷中區(qū)重新給蘑菇頭發(fā)一遍指令,這樣也可以把系統(tǒng)的中斷頻率降低 64 倍。下面代碼是我在 CW32L031 上實現(xiàn)的DMA 緩沖接收串口數據的代碼。DMA 的初始化:
static void dma_init(void)
{
DMA_InitTypeDef DMA_InitStructure = {0};
RCC_AHBPeriphClk_Enable( RCC_AHB_PERIPH_DMA , ENABLE); //Open DMA Clk
//初始化DMA RX
DMA_InitStructure.DMA_Mode = DMA_MODE_BLOCK; //Block 為可被打斷的dma傳輸,bulk為不可被打斷傳輸
DMA_InitStructure.DMA_TransferWidth = DMA_TRANSFER_WIDTH_8BIT; // dma的傳輸帶寬,8bit
DMA_InitStructure.DMA_SrcInc = DMA_SrcAddress_Fix; // DMA 源地不變,接受寄存器
DMA_InitStructure.DMA_DstInc = DMA_DstAddress_Increase; //目的地址增加,緩存
DMA_InitStructure.TrigMode = DMA_HardTrig; //硬件觸發(fā)模式
DMA_InitStructure.HardTrigSource = USART_RX_SRC; //UART3作為觸發(fā)源
DMA_InitStructure.DMA_TransferCnt = DMA_BUFFSIZE;
DMA_InitStructure.DMA_SrcAddress = (uint32_t)&USARTX->RDR;
DMA_InitStructure.DMA_DstAddress = (uint32_t)TxRxBuffer;
DMA_Init(USART_RX_DMA, &DMA_InitStructure);
DMA_Cmd(USART_RX_DMA, ENABLE);
DMA_ITConfig(USART_RX_DMA, DMA_IT_TC, ENABLE); //開啟 DMA 的傳輸完成中斷
}
DMA 中斷中重置 DMA 參數:
void DMACH1_IRQHandler(void)
{
/* USER CODE BEGIN */
if(DMA_GetITStatus(DMA_IT_TC1) == SET)
{
DMA_ClearITPendingBit(DMA_IT_TC1);
USART_RX_DMA->CNT_f.CNT = DMA_BUFFSIZE;
USART_RX_DMA->DSTADDR_f.DSTADDR = (uint32_t)TxRxBuffer;
USART_RX_DMA->CNT_f.REPEAT = 1;
USART_RX_DMA->CSR_f.EN = 1;
}
/* USER CODE END */
}
主循環(huán)中進行查詢處理:
void uart_check_recv(void)
{
s32 DAMCnt = 0;
s32 MaxDataLen = DMA_BUFFSIZE;
//判斷是否正在接受
if(rx_fifo.bRecving)
return;
rx_fifo.bRecving = 1; // 加鎖,防止多線程調用
if(USART_RX_DMA->CNT_f.CNT >= DMA_BUFFSIZE) //如果 DMA 接收為空,退出, 初始值為最大,接收遞減
{
rx_fifo.bRecving = 0;
return;
}
DAMCnt = DMA_BUFFSIZE - (USART_RX_DMA->CNT_f.CNT); // 這是 DMA 接收到的數據長度
//LOG("dma recv %d rn",DAMCnt);
while( rx_fifo.rx_ptr != DAMCnt && MaxDataLen > 0) //緩存中還有數據就循環(huán)
{
//LOG("(%d)",rx_fifo.rx_ptr);
parse_data(TxRxBuffer[rx_fifo.rx_ptr]);
rx_fifo.rx_ptr++;
if( rx_fifo.rx_ptr >= DMA_BUFFSIZE ) //指針循環(huán)
{
//LOG("DMA rn");
rx_fifo.rx_ptr = 0;
}
DAMCnt = DMA_BUFFSIZE - (USART_RX_DMA->CNT_f.CNT); //更新一下緩沖區(qū)剩余待處理的字節(jié)數
MaxDataLen--;
}
rx_fifo.bRecving = 0; //解鎖
}
三、注意事項
在 CW32L031 平臺上,其 DMA 設計的比較簡單,他沒有循環(huán)模式,因此我們需要開啟 DMA 的傳輸完成中斷,然后在中斷中重新設定目的地址和傳輸量,這里一定要注意設置完整,不然程序很容易跑飛,因為 蘑菇頭的頭腦還是比較簡單的,如果地址不重新設定,那么他會一直往后累加,把磚搬的工地上到處都是,等總工喝完茶回來就徹底崩潰了。
另外,蘑菇頭搬磚的時候,走的路也是工地上的路,因此還是有很大概率會和總工在路線上起沖突的,因此他和總工也并不是完全不相干了,如果蘑菇頭一次性搬磚數量太多,也會堵住工地上的道路,從而阻礙到總工去上個廁所啥的也不一定哦。
你學廢了嗎?