笔者在进行不少项目开发时,都遇到了需要多通道多次采样的需求。由于STM32片上12位ADC的精度不少很高,通常需要对每个通道多次采样,然后计算平均值作为采样的结果。如果采用常规的读取ADC数值并计算平均值的方法,会占用大量CPU时间,极大地增加了MCU的负担(如6个通道采样20个数据计算平均值,需要采样120次才能刷新一次数据)。而采用DMA传输可以使ADC采样和数据刷新放在“后台”自动进行,可以节省大量的单片机运行时间。
笔者将以STM32F103为例,简单介绍一下采用DMA传输使用ADC的一般方法。
DMA(Direct Memory Access)——直接存储器存取,是单片机的一个外设,它的主要功能是用来搬数据,但是不需要占用 CPU,即在传输数据的时候,CPU 可以干其他的事情,好像是多线程一样。数据传输支持从外设到存储器或者存储器到存储器,这里的存储器可以是 SRAM 或者是 FLASH。DMA 控制器包含了 DMA1 和 DMA2,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,这里的通道可以理解为传输数据的一种管道。要注意的是 DMA2 只存在于大容量的单片机中。下图所示是STM32F103的DMA结构框图:
DMA 控制器独立于内核,属于一个单独的外设,结构比较简单。从应用的角度来说,它主要有以下三点值得注意的地方:
如果外设要想通过 DMA 来传输数据,必须先给 DMA控制器发送 DMA请求,DMA收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
DMA 有 DMA1 和 DMA2 两个控制器,DMA1 有 7 个通道,DMA2 有 5 个通道,不同的 DMA 控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置。
DMA 具有 12 个独立可编程的通道,其中 DMA1 有 7 个通道,DMA2 有 5 个通道,每
个通道对应不同的外设的 DMA 请求。虽然每个通道可以接收多个外设的请求,但是同一
时间只能接收一个,不能同时接收多个。
下图所示为DMA1请求映射表:
下图所示为DMA2请求映射表:
当发生多个 DMA 通道请求时,就意味着有先后响应处理的顺序问题,这个就由仲裁器也管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
使用 DMA,最核心就是配置要传输的数据,包括数据从哪里来,要到哪里去,传输的
数据的单位是什么,要传多少数据,是一次传输还是循环传输等等。
DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。
当我们配置好数据要从哪里来到哪里去之后,我们还需要知道我们要传输的数据是多少,数据的单位是什么。以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由DMA_CNDTR 配置,这是一个 32 位的寄存器,一次最多只能传输 65535 个数据。要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8 位的,所以我们定义的要发送的数据也必须是 8 位。外设的数据宽度由 DMA_CCRx 的PSIZE[1:0]配置,可以是8/16/32位,存储器的数据宽度由DMA_CCRx的MSIZE[1:0]配置,可以是8/16/32位。
在 DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。外设的地址指针由 DMA_CCRx 的 PINC 配置,存储器的地址指针由 MINC 配置。以串口向电脑发送数据为例,要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。
数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来鉴别。每个DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器DMA_ISR 的详细描述。
传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即是传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCRx 寄存器的 CIRC 循环模式位控制。
在正确配置后,DMA传输与ADC采样会启动。笔者的代码中使能了6个ADC通道进行采样,存储深度为20组。由于设置了循环传输模式,二维数组
ADC_DMA_Value[ADC_DMA_CHANNEL_DEEPTH][ADC_DMA_CHANNEL_NUM]
中的数据会被不断的刷新。
通过函数uint16_t ADC_GetData_DMA(uint8_t channel)可以读取某个通道的采样数值。
通过函数uint16_t ADC_GetAverageData_DMA(uint8_t channel)可以读取以深度20为单位计算出来的平均采样数值。
void GPIO_Config()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_AFIODeInit();
/*JTAG-DP Disabled and SW-DP Enabled, use PB3, PB4 as GPIO*/
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
///ADC GPIO - PA3, PA4, PA5, PA6
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
#define ADC_DMA_CHANNEL_NUM 6
#define ADC_DMA_CHANNEL_DEEPTH 20
#define ADC_DMA_BUFFER_SIZE (ADC_DMA_CHANNEL_NUM * ADC_DMA_CHANNEL_DEEPTH)
volatile uint16_t ADC_Data[20];
volatile uint16_t ADC_DMA_Value[ADC_DMA_CHANNEL_DEEPTH][ADC_DMA_CHANNEL_NUM];
void ADC_Config()
{
/**
* Configuration of ADC
*/
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
ADC_DeInit(ADC1); //复位ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //通道扫描
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 6;
ADC_Init(ADC1, &ADC_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); //通道1转换结果保存到ADCConvertedValue[0~10][0]
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_239Cycles5); //通道2转换结果保存到ADCConvertedValue[0~10][1]
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 3, ADC_SampleTime_239Cycles5); //通道3转换结果保存到ADCConvertedValue[0~10][2]
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 4, ADC_SampleTime_239Cycles5); //通道4转换结果保存到ADCConvertedValue[0~10][3]
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 5, ADC_SampleTime_239Cycles5); //通道5转换结果保存到ADCConvertedValue[0~10][4]
ADC_RegularChannelConfig(ADC1, ADC_Channel_6, 6, ADC_SampleTime_239Cycles5); //通道6转换结果保存到ADCConvertedValue[0~10][5]
ADC_DMACmd(ADC1, ENABLE); //开启ADC的DMA支持
ADC_Cmd(ADC1, ENABLE);
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1))
;
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1))
;
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
void DMA_Config()
{
/**
* Configuration of DMA
*/
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能时钟
DMA_DeInit(DMA1_Channel1); //将通道一寄存器设为默认值
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &(ADC1->DR); //该参数用以定义DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) &ADC_DMA_Value; //该参数用以定义DMA内存基地址(转换结果保存的地址)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //该参数规定了外设是作为数据传输的目的地还是来源,此处是作为来源
DMA_InitStructure.DMA_BufferSize = ADC_DMA_BUFFER_SIZE; //定义指定DMA通道的DMA缓存的大小,单位为数据单位。这里也就是ADC_DMA_Value的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //设定外设地址寄存器递增与否,此处设为不变 Disable
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //用来设定内存地址寄存器递增与否,此处设为递增,Enable
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //DMA通道拥有高优先级 分别4个等级 低、中、高、非常高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //使能DMA通道的内存到内存传输
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道
// DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE);
DMA_Cmd(DMA1_Channel1, ENABLE); //启动DMA通道一
}
uint16_t ADC_GetData_DMA(uint8_t channel)
{
return ADC_DMA_Value[0][channel];
}
uint16_t ADC_GetAverageData_DMA(uint8_t channel)
{
uint16_t sum = 0;
for (uint8_t j = 0; j < ADC_DMA_CHANNEL_DEEPTH; j++)
{
sum += ADC_DMA_Value[j][channel];
}
return sum / ADC_DMA_CHANNEL_DEEPTH; //求平均值并转换成电压值
}