我在我的 BMS 系统中使用了隔离的串口来进行通信,BMS 的主控芯片我选择了芯源的 CW32L031,这是一款 TSSOP20 封装的 M0 内核的低功耗单片机,资源不多,主频也不是很高,但是功耗低确是我最重要的需求。
为了降低功耗,我将主频设置的比较低,因此在走串口通信的时候,如果使用串口的中断来进行发送和接收,那么每接收一个字符就会产生一个中断,这样频繁的中断肯定是 CPU 不厌其烦的,于是我选择使用 DMA 充当一次串口的缓冲助手。
一、 什么是 DMA
DMA(Direct Memory Access,直接存储器访问)提供在外设与内存、存储器和存储器、外设与外设之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU ,在这个时间中,CPU 对于内存的工作来说就无法使用。
其实,我们可以简化一下对 DMA 功能的描述,DMA 的主要工作就是搬砖,我只需要设置好几个简单的参数,DMA 就可以帮我们把数据从一个地址搬运到另一个地址,就是这么简单。
这里有三种情况,分别是内存到内存,内存到外设,外设到内存。
要想让这个这个蘑菇头好好的帮我们搬砖,首先我们需要设置以下几个参数:
源地址(从哪里取砖块)目的地址(把砖块放到哪里)传输带宽(一次搬几块)源地址是否增加(砖块是排列好的,还是一块接着一块吐出来的)目的地址是否增加(砖块是排列码好,还是一块一块的丢进一个洞里)触发源(谁发指令开始搬)一次性传输量(一共搬多少次)
下面,我们就从一个工地的场景来了解一下,蘑菇头是如何执行任务来减轻总工的工作负荷的。首先,对于蘑菇头来说,他的主要任务就是把砖从一个地方搬运到另一个地方,只要总工开始给他讲好怎么搬,蘑菇头就能摒弃一切杂念,任劳任怨的快乐搬砖。最简单的是内存搬运,也就是把砖从一块空地搬运到另一块空地,这时候我们只需要跟蘑菇头说:蘑菇头,你过来,你今天的任务是把 A 区域(源地址)的500 块(传输量)砖搬运到 B 区域(目的地址),你每次搬 2 块(传输带宽),同时要保证他们在 AB 区域的码放是相同的(源和目的地址同步增加)。现在我只要一喊:“开始”(软件触发),你就马上按我说的给我搬砖,我会先去忙点别的事。
这不,总工就可以有喝茶的时间了嘛!似乎只是这么倒腾砖的意义不大,我们假设现在有一个机器来负责把砖头从车上卸下来,然后我们让蘑菇头把机器卸下来的砖整整齐齐的码放在 B 区域。这个时候我们可以告诉蘑菇头,要从机器出口那里搬(源地址不增加),这次因为机器一次只能吐出一块砖来,所以你一次只搬一块(传输带宽),然后按顺序码放到 B 区域。今天我也不陪你了,机器出砖的时候会鸣笛(硬件触发),你听到鸣笛就开始搬就可以了。哦,对了,你搬完 500 块排满一层后,就重新开始在上面再排一层(循环模式),我回来之前你不许停。
二、串口的问题
上面我们了解了 DMA 的工作过程,然后我们就用它来帮我们解决一下我们的串口问题。假设我们让蘑菇头帮我们把串口接收的数据先搬运到内存中的一个缓冲区,比如这个缓冲区是 64 个字节,那么我们就可以在主循环中区查询这个缓冲区而不用每个字节都进入中断处理。即便我们的 DMA 没有循环模式,我们也只需要在 DMA 传输完成中断中区重新给蘑菇头发一遍指令,这样也可以把系统的中断频率降低 64 倍。下面代码是我在 CW32L031 上实现的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; //硬件触发模式
DMA_InitStructure.HardTrigSource = USART_RX_SRC; //UART3作为触发源
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 */
}
主循环中进行查询处理:
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) //缓存中还有数据就循环
{
//LOG("(%d)",rx_fifo.rx_ptr);
parse_data(TxRxBuffer[rx_fifo.rx_ptr]);
rx_fifo.rx_ptr++;
if( rx_fifo.rx_ptr >= DMA_BUFFSIZE ) //指针循环
{
//LOG("DMA rn");
rx_fifo.rx_ptr = 0;
}
DAMCnt = DMA_BUFFSIZE - (USART_RX_DMA->CNT_f.CNT); //更新一下缓冲区剩余待处理的字节数
MaxDataLen--;
}
rx_fifo.bRecving = 0; //解锁
}
三、注意事项
在 CW32L031 平台上,其 DMA 设计的比较简单,他没有循环模式,因此我们需要开启 DMA 的传输完成中断,然后在中断中重新设定目的地址和传输量,这里一定要注意设置完整,不然程序很容易跑飞,因为 蘑菇头的头脑还是比较简单的,如果地址不重新设定,那么他会一直往后累加,把砖搬的工地上到处都是,等总工喝完茶回来就彻底崩溃了。
另外,蘑菇头搬砖的时候,走的路也是工地上的路,因此还是有很大概率会和总工在路线上起冲突的,因此他和总工也并不是完全不相干了,如果蘑菇头一次性搬砖数量太多,也会堵住工地上的道路,从而阻碍到总工去上个厕所啥的也不一定哦。
你学废了吗?