我在我的 BMS 系統(tǒng)中使用了隔離的串口來(lái)進(jìn)行通信,BMS 的主控芯片我選擇了芯源的 CW32L031,這是一款 TSSOP20 封裝的 M0 內(nèi)核的低功耗單片機(jī),資源不多,主頻也不是很高,但是功耗低確是我最重要的需求。
為了降低功耗,我將主頻設(shè)置的比較低,因此在走串口通信的時(shí)候,如果使用串口的中斷來(lái)進(jìn)行發(fā)送和接收,那么每接收一個(gè)字符就會(huì)產(chǎn)生一個(gè)中斷,這樣頻繁的中斷肯定是 CPU 不厭其煩的,于是我選擇使用 DMA 充當(dāng)一次串口的緩沖助手。
一、 什么是 DMA
DMA(Direct Memory Access,直接存儲(chǔ)器訪(fǎng)問(wèn))提供在外設(shè)與內(nèi)存、存儲(chǔ)器和存儲(chǔ)器、外設(shè)與外設(shè)之間的高速數(shù)據(jù)傳輸使用。它允許不同速度的硬件裝置來(lái)溝通,而不需要依賴(lài)于 CPU ,在這個(gè)時(shí)間中,CPU 對(duì)于內(nèi)存的工作來(lái)說(shuō)就無(wú)法使用。
其實(shí),我們可以簡(jiǎn)化一下對(duì) DMA 功能的描述,DMA 的主要工作就是搬磚,我只需要設(shè)置好幾個(gè)簡(jiǎn)單的參數(shù),DMA 就可以幫我們把數(shù)據(jù)從一個(gè)地址搬運(yùn)到另一個(gè)地址,就是這么簡(jiǎn)單。
這里有三種情況,分別是內(nèi)存到內(nèi)存,內(nèi)存到外設(shè),外設(shè)到內(nèi)存。
要想讓這個(gè)這個(gè)蘑菇頭好好的幫我們搬磚,首先我們需要設(shè)置以下幾個(gè)參數(shù):
源地址(從哪里取磚塊)目的地址(把磚塊放到哪里)傳輸帶寬(一次搬幾塊)源地址是否增加(磚塊是排列好的,還是一塊接著一塊吐出來(lái)的)目的地址是否增加(磚塊是排列碼好,還是一塊一塊的丟進(jìn)一個(gè)洞里)觸發(fā)源(誰(shuí)發(fā)指令開(kāi)始搬)一次性傳輸量(一共搬多少次)
下面,我們就從一個(gè)工地的場(chǎng)景來(lái)了解一下,蘑菇頭是如何執(zhí)行任務(wù)來(lái)減輕總工的工作負(fù)荷的。首先,對(duì)于蘑菇頭來(lái)說(shuō),他的主要任務(wù)就是把磚從一個(gè)地方搬運(yùn)到另一個(gè)地方,只要總工開(kāi)始給他講好怎么搬,蘑菇頭就能摒棄一切雜念,任勞任怨的快樂(lè)搬磚。最簡(jiǎn)單的是內(nèi)存搬運(yùn),也就是把磚從一塊空地搬運(yùn)到另一塊空地,這時(shí)候我們只需要跟蘑菇頭說(shuō):蘑菇頭,你過(guò)來(lái),你今天的任務(wù)是把 A 區(qū)域(源地址)的500 塊(傳輸量)磚搬運(yùn)到 B 區(qū)域(目的地址),你每次搬 2 塊(傳輸帶寬),同時(shí)要保證他們?cè)?AB 區(qū)域的碼放是相同的(源和目的地址同步增加)?,F(xiàn)在我只要一喊:“開(kāi)始”(軟件觸發(fā)),你就馬上按我說(shuō)的給我搬磚,我會(huì)先去忙點(diǎn)別的事。
這不,總工就可以有喝茶的時(shí)間了嘛!似乎只是這么倒騰磚的意義不大,我們假設(shè)現(xiàn)在有一個(gè)機(jī)器來(lái)負(fù)責(zé)把磚頭從車(chē)上卸下來(lái),然后我們讓蘑菇頭把機(jī)器卸下來(lái)的磚整整齊齊的碼放在 B 區(qū)域。這個(gè)時(shí)候我們可以告訴蘑菇頭,要從機(jī)器出口那里搬(源地址不增加),這次因?yàn)闄C(jī)器一次只能吐出一塊磚來(lái),所以你一次只搬一塊(傳輸帶寬),然后按順序碼放到 B 區(qū)域。今天我也不陪你了,機(jī)器出磚的時(shí)候會(huì)鳴笛(硬件觸發(fā)),你聽(tīng)到鳴笛就開(kāi)始搬就可以了。哦,對(duì)了,你搬完 500 塊排滿(mǎn)一層后,就重新開(kāi)始在上面再排一層(循環(huán)模式),我回來(lái)之前你不許停。
二、串口的問(wèn)題
上面我們了解了 DMA 的工作過(guò)程,然后我們就用它來(lái)幫我們解決一下我們的串口問(wèn)題。假設(shè)我們讓蘑菇頭幫我們把串口接收的數(shù)據(jù)先搬運(yùn)到內(nèi)存中的一個(gè)緩沖區(qū),比如這個(gè)緩沖區(qū)是 64 個(gè)字節(jié),那么我們就可以在主循環(huán)中區(qū)查詢(xún)這個(gè)緩沖區(qū)而不用每個(gè)字節(jié)都進(jìn)入中斷處理。即便我們的 DMA 沒(méi)有循環(huán)模式,我們也只需要在 DMA 傳輸完成中斷中區(qū)重新給蘑菇頭發(fā)一遍指令,這樣也可以把系統(tǒng)的中斷頻率降低 64 倍。下面代碼是我在 CW32L031 上實(shí)現(xiàn)的DMA 緩沖接收串口數(shù)據(jù)的代碼。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); //開(kāi)啟 DMA 的傳輸完成中斷
}
DMA 中斷中重置 DMA 參數(shù):
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)中進(jìn)行查詢(xún)處理:
void uart_check_recv(void)
{
s32 DAMCnt = 0;
s32 MaxDataLen = DMA_BUFFSIZE;
//判斷是否正在接受
if(rx_fifo.bRecving)
return;
rx_fifo.bRecving = 1; // 加鎖,防止多線(xiàn)程調(diào)用
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 接收到的數(shù)據(jù)長(zhǎng)度
//LOG("dma recv %d rn",DAMCnt);
while( rx_fifo.rx_ptr != DAMCnt && MaxDataLen > 0) //緩存中還有數(shù)據(jù)就循環(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é)數(shù)
MaxDataLen--;
}
rx_fifo.bRecving = 0; //解鎖
}
三、注意事項(xiàng)
在 CW32L031 平臺(tái)上,其 DMA 設(shè)計(jì)的比較簡(jiǎn)單,他沒(méi)有循環(huán)模式,因此我們需要開(kāi)啟 DMA 的傳輸完成中斷,然后在中斷中重新設(shè)定目的地址和傳輸量,這里一定要注意設(shè)置完整,不然程序很容易跑飛,因?yàn)?蘑菇頭的頭腦還是比較簡(jiǎn)單的,如果地址不重新設(shè)定,那么他會(huì)一直往后累加,把磚搬的工地上到處都是,等總工喝完茶回來(lái)就徹底崩潰了。
另外,蘑菇頭搬磚的時(shí)候,走的路也是工地上的路,因此還是有很大概率會(huì)和總工在路線(xiàn)上起沖突的,因此他和總工也并不是完全不相干了,如果蘑菇頭一次性搬磚數(shù)量太多,也會(huì)堵住工地上的道路,從而阻礙到總工去上個(gè)廁所啥的也不一定哦。
你學(xué)廢了嗎?