一、DMA基本概念
直接存储器访问(Direct Memory Access),简称DMA。DMA是CPU一个用于数据从一个地址空间到另一地址空间“搬运”(拷贝)的组件,数据拷贝过程不需CPU干预,数据拷贝结束则通知CPU处理。因此,大量数据拷贝时,使用DMA可以释放CPU资源。DMA数据拷贝过程,典型的有:
- 内存—>内存,内存间拷贝
- 外设—>内存,如uart、spi、i2c等总线接收数据过程
- 内存—>外设,如uart、spi、i2c等总线发送数据过程
二、DMA一些应用场景
1、ADC与DMA
(1)AD单次启动+软件启动+查询/中断方式
-------------------------------------------------------------------------
var = Get_Adc_Average(ADC_CHANNEL_0, 10); //放大倍数为0.635
Sys.adc1 = var * 3300 / 4096 * 1.574 + 10;
----------------------------------------------------------------------------
u16 Get_Adc(u32 ch)
{
ADC_ChannelConfTypeDef ADC1_ChanConf;
ADC1_ChanConf.Channel=ch;
ADC1_ChanConf.Rank=1;
ADC1_ChanConf.SamplingTime=ADC_SAMPLETIME_480CYCLES;
ADC1_ChanConf.Offset=0;
HAL_ADC_ConfigChannel(&hadc1,&ADC1_ChanConf);
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1,10);
return (u16)HAL_ADC_GetValue(&hadc1);
}
u16 Get_Adc_Average(u32 ch,u8 times)
{
u32 temp_val=0;
u8 t;
for(t=0;t<times;t++)
{
temp_val+=Get_Adc(ch);
osDelay(5);
}
return temp_val/times;
}
(2)连续转换+DMA+手动启动/定时器启动
在方法1里面,每次转换完成,需要我们手动去读一下AD值;启动DMA之后,完全省掉了这个过程,只需要等待设定好的值全部转换完成之后触发一个中断,再进行数据处理。
/**
* @brief adc01的配置 规则通道并行 扫描和连续转换模式
*/
static void bsp_adc01_cfg(void)
{
/* enable ADC0 and ADC1 clock */
rcu_periph_clock_enable(RCU_ADC0);
rcu_periph_clock_enable(RCU_ADC1);
/* config ADC clock */
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV16);
/* ADC continous function enable */
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE);
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE);
adc_special_function_config(ADC1, ADC_SCAN_MODE, ENABLE);
adc_special_function_config(ADC1, ADC_CONTINUOUS_MODE, ENABLE);
/* ADC trigger config */
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_REGULAR_NONE);//ADC0_1_EXTTRIG_REGULAR_NONE
adc_external_trigger_source_config(ADC1, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_REGULAR_NONE);//ADC0_1_EXTTRIG_INSERTED_NONE
/* ADC data alignment config */
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT);
adc_data_alignment_config(ADC1, ADC_DATAALIGN_RIGHT);
/* ADC mode config,使用规则同步模式,ADC0是主,ADC1是从, 同时转换两个通道(同时转换的通道不能相同) */
adc_mode_config(ADC_DAUL_REGULAL_PARALLEL);
/* ADC分辨率 12B */
adc_resolution_config(ADC0,ADC_RESOLUTION_12B);
adc_resolution_config(ADC1,ADC_RESOLUTION_12B);
/* ADC channel length config */
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 6);
adc_channel_length_config(ADC1, ADC_REGULAR_CHANNEL, 0);
/* ADC regular channel config,一个通道转换时长是2.06us */
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_6, ADC_SAMPLETIME_239POINT5);
adc_regular_channel_config(ADC0, 1, ADC_CHANNEL_7, ADC_SAMPLETIME_239POINT5);
adc_regular_channel_config(ADC0, 2, ADC_CHANNEL_14, ADC_SAMPLETIME_239POINT5);
//adc_regular_channel_config(ADC0, 3, ADC_CHANNEL_7, ADC_SAMPLETIME_55POINT5);
//adc_regular_channel_config(ADC0, 4, ADC_CHANNEL_8, ADC_SAMPLETIME_55POINT5);
adc_regular_channel_config(ADC0, 3, ADC_CHANNEL_15, ADC_SAMPLETIME_239POINT5);
adc_regular_channel_config(ADC0, 4, ADC_CHANNEL_8, ADC_SAMPLETIME_239POINT5);
adc_regular_channel_config(ADC0, 5, ADC_CHANNEL_9, ADC_SAMPLETIME_239POINT5);
// adc_regular_channel_config(ADC1, 3, ADC_CHANNEL_15, ADC_SAMPLETIME_55POINT5);
//adc_regular_channel_config(ADC1, 4, ADC_CHANNEL_5, ADC_SAMPLETIME_55POINT5);
/* ADC external trigger enable */
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE);
adc_external_trigger_config(ADC1, ADC_REGULAR_CHANNEL, ENABLE);
/* enable ADC0 interface */
adc_enable(ADC0);
//delay_ms(0xFFFF);
/* ADC0 calibration and reset calibration */
adc_calibration_enable(ADC0);
/* enable ADC1 interface */
adc_enable(ADC1);
//delay_ms(0xFFFF);
/* ADC1 calibration and reset calibration */
adc_calibration_enable(ADC1); /* ADC校准复位 */
/* ADC DMA function enable */
adc_dma_mode_enable(ADC0);
/* ADC0 software trigger enable */
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
adc_software_trigger_enable(ADC1, ADC_REGULAR_CHANNEL);
}
uint16_t adc01_fifo[100];
/**
* @brief ADC01 DMA配置
* @retval none
* @author Mr.W
* @date 2020-11-10
*/
void bsp_adc01_dma_cfg(void)
{
/* ADC_DMA_channel configuration */
dma_parameter_struct dma_data_parameter;
/* enable DMA0 clock */
rcu_periph_clock_enable(RCU_DMA0);
/* ADC DMA_channel configuration */
dma_deinit(DMA0, DMA_CH0);
/* initialize DMA data mode */
dma_data_parameter.periph_addr = (uint32_t)(&ADC_RDATA(ADC0));
dma_data_parameter.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_data_parameter.memory_addr = (uint32_t)(g_sys.adcs.fifo);
dma_data_parameter.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_data_parameter.periph_width = DMA_PERIPHERAL_WIDTH_16BIT;
dma_data_parameter.memory_width = DMA_MEMORY_WIDTH_16BIT;
dma_data_parameter.direction = DMA_PERIPHERAL_TO_MEMORY;
dma_data_parameter.number = ADCFILTER_NUM*ADCCHANNEL_NUM*2; /* 搬运的数据个数,完成后会触发中断*/
dma_data_parameter.priority = DMA_PRIORITY_HIGH;
dma_init(DMA0, DMA_CH0, &dma_data_parameter);
dma_circulation_enable(DMA0, DMA_CH0);
//设置dma中断优先级
nvic_irq_enable(DMA0_Channel0_IRQn, 0, 0);
/* enable DMA transfer complete interrupt */
dma_interrupt_enable(DMA0,DMA_CH0, DMA_INT_HTF|DMA_INT_FTF);//半传输/全传输中断都开启
/* enable DMA channel */
dma_channel_enable(DMA0, DMA_CH0);
}
void DMA0_Channel0_IRQHandler(void)
{
if(dma_interrupt_flag_get(DMA0,DMA_CH0, DMA_INT_HTF))//半传输完成
{
g_sys.adcs.dma_half1=1;
}
if(dma_interrupt_flag_get(DMA0,DMA_CH0, DMA_INT_FTF))//全部传输完成
{
g_sys.adcs.dma_half2=1;
}
//清除全部标志位
dma_interrupt_flag_clear(DMA0,DMA_CH0, DMA_INT_FLAG_G);
rt_sem_release(&sem_adc_dma); /* 释放信号量 */
}
void calcaulate_anlog(void)
{
uint32_t sum_data[ADCCHANNEL_NUM] = {0};
uint16_t i = 0;
uint16_t j = 0;
// int16_t var1=0;
static uint8_t cnt_tsk = 0;
static uint8_t cnt_cluth = 0;
static uint16_t cnt2_cluth = 0;
static int16_t judge_cluth_data_before = 0;
static int16_t judge_cluth_data_now = 0;
if (g_sys.adcs.dma_half1 == 1)
{
for (i = 0; i < ADCFILTER_NUM; i++)
{
for (j = 0; j < ADCCHANNEL_NUM; j++)
sum_data[j] += g_sys.adcs.fifo[j + i * 6];
}
for (j = 0; j < ADCCHANNEL_NUM; j++)
{
g_sys.adcs.average_adc[j] = sum_data[j] / ADCFILTER_NUM;
sum_data[j] = 0;
}
}
if (g_sys.adcs.dma_half2 == 1) // half2数据暂时舍弃
{
// for(i=0;i<ADCFILTER_NUM;i++)
// {
// for(j=0;j<ADCCHANNEL_NUM;j++)
// sum_data[j]+=g_sys.adcs.fifo[j+i*6+ADCCHANNEL_NUM*ADCFILTER_NUM];
// }
// for(j=0;j<ADCCHANNEL_NUM;j++)
// {
// g_sys.adcs.average_adc[j]= sum_data[j]/ADCFILTER_NUM;
// sum_data[j]=0;
// }
}
}
}
2、串口与DMA
串口(uart)是一种低速的串行异步通信,适用于低速通信场景,通常使用的波特率小于或等于115200bps。对于小于或者等于115200bps波特率的,而且数据量不大的通信场景,一般没必要使用DMA,或者说使用DMA并未能充分发挥出DMA的作用。
对于数量大,或者波特率提高时,必须使用DMA以释放CPU资源,因为高波特率可能带来这样的问题:
- 对于发送,使用循环发送,可能阻塞线程,需要消耗大量CPU资源“搬运”数据,浪费CPU
- 对于发送,使用中断发送,不会阻塞线程,但需浪费大量中断资源,CPU频繁响应中断;以115200bps波特率,1s传输11520字节,大约69us需响应一次中断,如波特率再提高,将消耗更多CPU资源
- 对于接收,如仍采用传统的中断模式接收,同样会因为频繁中断导致消耗大量CPU资源
如下为某项目的环形DMA传输;
for(;;)
{
xResult = xSemaphoreTake(g_xSemCntUart1_rxHandle, portMAX_DELAY) ; // 任务代码中获取二值信号量 成功
if(xResult == pdTRUE)
{
taskENTER_CRITICAL() ;
memset((uint8_t *)g_uart1_temp_buf,0,U1_BUFSIZE);
printf("串口1的接收数据位置 : %d %d\r\n",g_uart1_recv.tail, g_uart1_recv.head);
size= ring_used_buf_get(&g_uart1_recv,(uint8_t *)g_uart1_temp_buf);
taskEXIT_CRITICAL() ;
//printf("串口1接收数据为 : %s\r\n",g_uart1_temp_buf);
uart_send_data(&huart3,(uint8_t *)g_uart1_temp_buf,size);
}
else
{
Error_Handler();
}
}
-----------------------------------------------------------------------
/**
串口1相关DMA部分初始化代码
**/
void init_related_usart1(void)
{
/* USART1 DMA Init */
/* USART1_RX Init */
hdma_usart1_rx.Instance = DMA2_Stream2;
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);
/* USART1_TX Init */
hdma_usart1_tx.Instance = DMA2_Stream7;
hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart1_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);
#if UART1_RECV_EN == 1
g_uart1_recv.rxbuf = (uint8_t *)g_uart1_dma_rxbuf; /* STM32 串口设备 */
g_uart1_recv.totalleng =U1_RXSIZE;
g_uart1_recv.head =0;
g_uart1_recv.tail =0;
g_uart1_recv.used =0;
HAL_UART_Receive_DMA(&huart1, g_uart1_recv.rxbuf , U1_RXSIZE) ;
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
__HAL_UART_CLEAR_IDLEFLAG(&huart1) ; //清除空闲标志
#endif
}
--------------------------------------------------------
void USART1_IRQHandler(void)
{
usart_recv_idle(&huart1, &g_uart1_recv,&g_xSemCntUart1_rxHandle) ;
HAL_UART_IRQHandler(&huart1);
}
/**
*********************************************************************************************************
* 函数功能: 串口1 接收 环形DMA+空闲中断
*********************************************************************************************************
*/
typedef struct
{
uint8_t *rxbuf;
uint16_t totalleng; // 数据总长
uint16_t head; // 这次接收数据 队列头
uint16_t tail; // 这次接收数据 队列尾
uint16_t used; // 接收数据 解析标志 //
// UART_STATUSE statuse;
}USART_RECV;
void usart_recv_idle(UART_HandleTypeDef *huart,USART_RECV *pUart, osSemaphoreId_t *pSem)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
BaseType_t xSaveIntStatus ;
if((__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(huart) ; //清除空闲标志
xSaveIntStatus = taskENTER_CRITICAL_FROM_ISR() ; /* 进入中断服务程序里面临界区 */
pUart->tail=pUart->head;
pUart->head=pUart->totalleng - __HAL_DMA_GET_COUNTER(huart->hdmarx);
//printf("si is %d\r\n",( pUart->head));
taskEXIT_CRITICAL_FROM_ISR(xSaveIntStatus) ; /* 退出中断服务程序里面临界区 */
xSemaphoreGiveFromISR(*pSem, &xHigherPriorityTaskWoken) ; //用于中断服务程序中释放 二值 信号量。
if(xHigherPriorityTaskWoken == pdTRUE) /* 如果 xHigherPriorityTaskWoken = pdTRUE,那么退出中断后切到当前最高优先级任务执行 */
{
portYIELD_FROM_ISR(xHigherPriorityTaskWoken) ;
}
}
}
/** *********************************************************************************************************
* 函数功能: 从环形缓冲区中 从used到 head 长度的数据
*********************************************************************************************************
*/
uint16_t ring_used_buf_get(USART_RECV *pUart,uint8_t *buf)
{
uint16_t lenght=0 ;
while(pUart->used!=pUart->head)
{
*buf++= pUart->rxbuf[pUart->used++];
lenght++;
if(pUart->used>=pUart->totalleng)
pUart->used= 0;
}
return lenght ;
}
/**
*********************************************************************************************************
* 函数功能: 从环形缓冲区中 从tail到 head 长度的数据
*********************************************************************************************************
*/
uint16_t ring_tail_buf_get( USART_RECV *pUart,uint8_t *buf)
{
uint16_t lenght=0 ;
while(pUart->tail!=pUart->head)
{
*buf++= pUart->rxbuf[pUart->tail++];
lenght++;
if(pUart->tail>=pUart->totalleng)
pUart->tail= 0;
}
return lenght ;
}
3、其他
- 如在一些大量数据需要多个算法处理,如摄像头数据需要分别进行二值化处理、几何变换、色度变换,则必须使用到DMA进行数据搬运来提升效率;
- 再如越来越多的多核的处理器,xilinx ZYNQ 7000: Cortex-A9 + fpga;Ti TMS320C6678:8核;STM32MP1/iMX7 : Cortex-A7 和 Cortex-M4 等其核心间通信除了共享的部分空间和外设,大部分的数据量都是要靠DMA进行传送来达到协程。
- DMA与网口。
HAL_ETH_DMATxDescListInit();
HAL_ETH_DMARxDescListInit(); - DMA与SPI、SDIO
如下图为stm32f4的DMA请求映射
三、DMA几个相关误区
1、DMA与cahe一致性问题
- 高性能的微控制器会在主存储器和 CPU 之间增加高速缓冲存储器(Cache),目的是提高对存储器的平均访问速度,从而提高存储系统的性能。
- CPU在访问内存时,首先判断所要访问的内容是否在Cache中,如果在,就称为“命中(hit)”,此时CPU直接从Cache中调用该内容;否则,就 称为“ 不命中”,CPU只好去内存中调用所需的子程序或指令了。CPU不但可以直接从Cache中读出内容,也可以直接往其中写入内容。由于Cache的存取速 率相当快,使得CPU的利用率大大提高,进而使整个系统的性能得以提升。
- Cache的一致性就是Cache中的数据,与对应的内存中的数据是一致的。 DMA是直接操作总线地址的,这里先当作物理地址来看待吧(系统总线地址和物理地址只是观察内存的角度不同)。如果cache缓存的内存区域不包括DMA分配到的区域,那么就没有一致性的问题。但是如果cache缓存包括了DMA目的地址的话,会出现什么什么问题呢?问题出在,经过DMA操作,cache缓存对应的内存数据已经被修改了,而CPU本身不知道(DMA传输是不通过CPU的),它仍然认为cache中的数 据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用旧的Cache数据。这样就发生Cache与内存的数据“不一致性”错误。
- 一般而言要么禁用此项功能,要么在使用过程中注意该段空间是否存在多个host的操作,如有,则实时需要刷新cahe 和memory之间的交互。
2、DMA与CCM内存单元
相较于F2,F4新加的一个特殊内部SRAM。64 KB CCM (内核耦合存储器)数据 RAM 不属于总线矩阵请参见图 1 : STM32F405xx/07xx和 STM32F415xx/17xx 器件的系统架构)。
因此,如果DMA的源地址或者目标地址分配到的是CCM部分的空间,则无法完成搬运。解决方式是要么禁用该段空间,要么修改分散加载sct文件