1.简介
多通道采集数据时,为了使采样过程尽可能不占用CPU资源,采用定时器触发的多通道ADC扫描采样,利用ADC+DMA可实时,有序的转存多通道数据至程序内存(数组),再加一个定时中断,用来定时读取内存中的数据。
实现双通道采集时,使用ADC(模数转换器)和DMA(直接内存访问)结合的方式可以提高采集效率。而为了触发ADC的采样和将采样数据传输到指定内存区域,通常需要配置定时器。
配置步骤:
-
配置ADC通道:设置ADC的工作模式、采样率、采样精度等参数,并选择要使用的ADC通道。
-
配置DMA通道:设置DMA的传输模式、传输方向、数据长度等参数,并选择要使用的DMA通道。
-
配置定时器:设置定时器的工作模式、时钟源、定时周期等参数。定时器的触发事件可以作为ADC开始采样的触发源。
-
配置ADC触发源:将定时器的触发事件与ADC的采样触发源关联起来,以便定时器定时到达后触发ADC的采样动作。
-
配置DMA传输触发源:将ADC转换完成的数据传输触发源与DMA关联起来,以便ADC完成一次采样后自动触发DMA传输数据到指定的内存区域。
2 ADC
ADC具有独立模式,双重模式和三重模式。由于ADC的规则通道有16个,规则数据寄存器只有一个,如果使用多通道转换,转换的数据就全部在DR中,前一个时间点转换的通道数据就会被新的数据覆盖,开启DMA模式传输可以避免这种问题。对于独立模式的多通道ADC转换使用DMA传输非常有必要。
ADC通道介绍
在ADC中,规则通道(Regular Channels)和注入通道(Injected Channels)是两种不同的采样方式。
- 规则通道是ADC常用的采样方式,它通过配置一组规则通道来进行采样。每个规则通道可以连接一个模拟信号源,并设置相应的采样时间和分辨率。ADC按照规则通道的顺序依次进行采样,并将采样结果保存在相应的数据寄存器中。规则通道适用于周期性或连续的采样。
- 注入通道是一种特殊的采样方式,它用于在规则通道采样过程中临时插入额外的通道。注入通道通常用于对事件性信号的快速采样,如瞬态信号或突发事件。注入通道的采样过程不会打断规则通道的采样,因此可以在不中断正常采样的情况下,对某些特定事件进行快速响应。
使用规则通道和注入通道时,需要进行相应的配置:
-
规则通道配置:设置规则通道的采样顺序、通道号、采样时间、分辨率等参数。规则通道的采样结果可以通过数据寄存器或DMA传输保存。
-
注入通道配置:设置注入通道的通道号、采样时间、分辨率等参数。注入通道的采样结果可以通过数据寄存器或DMA传输保存。
注意: 常见一般使用ADC规则通道。注入通道是在规则通道转换时强行插入转换的通道,注入通道转换完成后,规则通道才能工作,这点与中断程序很像。注入通道只有在规则通道存在时才会出现。
转换顺序
单通道模式:只转换规则序列中的一个通道。可以选择转换任一规则通道或注入通道。
单排模式:依次转换规则序列中的每个通道,直到转换完所有通道。转换的顺序是按照其在规则序列中的顺序进行。
扫描模式:循环转换规则序列中的所有通道。转换顺序是按照其在规则序列中的顺序进行,然后再次从第一个通道开始。
注入顺序模式:如果使用了注入通道,在完成规则序列的转换之后,可以选择继续转换注入通道,从而形成注入通道的转换顺序。
规则序列
如下图所示,规则序列寄存器有3个,分别为SQR3、SQR2、SQR1,依次用于配置通道1~16的转换顺序(SQ1–SQ16),以及配置有多少个通道需要转换(SQL)
举个例子,我们要配置3个转换通道依次是CH1、CH5、CH7、CH16,则
SQ1 寄存器设置为 0(代表CH1)
SQ2 寄存器设置为 4(代表CH5)
SQ3 寄存器设置为 6(代表CH7)
SQ4 寄存器设置为 15(代表CH16)
设定顺序长度: ADC模块通常有一个名为L的寄存器(SQRL),用于指定转换序列的长度。假设L的设置是基于0的计数,如果您想要进行4个通道的转换,您将寄存器设置为3(用二进制表示即0011,因为这通常是从0开始计数的,所以设置为3意味着进行4次转换)。如果设置是基于1的计数,则设置为4。
这样的配置将使ADC按照CH1、CH5、CH7和CH16的顺序进行转换。
对应的关键配置代码如下:
ADC_InitStructure.ADC_NbrOfConversion = 4; // 设置通道数为4
ADC_Init(ADC1, &ADC_InitStructure);
// 配置ADC通道1、5、7和16
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles); // ADC通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 2, ADC_SampleTime_3Cycles); // ADC通道5
ADC_RegularChannelConfig(ADC1, ADC_Channel_7, 3, ADC_SampleTime_3Cycles); // ADC通道7
ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 4, ADC_SampleTime_3Cycles); // ADC通道16
注入序列
注入序列寄存器含义与上面规则序列类似,此处不再表述怎么配置。
触发源
ADC转换可以由软触发
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
也可以由外部事件触发转换,包括内部定时器触发和外部IO触发。
外部触发检测有4种状态:禁止触发检测、上升沿检测、下降沿检测以及上升沿和下降沿均检测。
3 DMA
DMA可以在不需要CPU干预的情况下实现外设直接和存储器之间的数据传输。在STM32中,DMA可以用于加速数据传输,减少CPU占用率,提高系统性能。DMA控制器是一个独立的外设,它可以与其他外设相连,如ADC、DAC、USART、SPI、I2C等。DMA控制器可以直接访问存储器,而不必通过CPU,从而大大提高数据传输效率。
DMA数据流通道如下所示。
4 ADC+DMA双通道采集代码实现
4.1 DMA配置
配置对应的数据流通道,配置外设地址及存储器地址,设置方向为外设到存储器。
注意搬运模式要设置为循环搬运DMA_Mode_Circular
uint16_t adc_data[20];//存储采集值的缓冲区
void ADC_DMA_Config(void){ //DMA收
DMA_InitTypeDef DMA_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE);//DMA2时钟使能
DMA_InitStructure.DMA_Channel =DMA_Channel_2; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&(ADC3->DR));//par;//DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)adc_data;//DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;//外设到存储器模式
DMA_InitStructure.DMA_BufferSize = 20;//数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据长度:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器数据长度:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;// 设置循环搬运
DMA_InitStructure.DMA_Priority = DMA_Priority_High;//优先级设置
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;//存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;//外设突发单次传输
DMA_Init(DMA2_Stream0, &DMA_InitStructure);//初始化DMA Stream
DMA_Cmd(DMA2_Stream0, ENABLE);
}
4.2 ADC配置
触发源选择定时器3,TIM3_TRGO事件触发。
//初始化ADC
void Adc_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_CommonInitTypeDef ADC_CommonInitStructure;
ADC_InitTypeDef ADC_InitStructure;
ADC_DMA_Config();
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);//使能GPIOF时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3, ENABLE); //使能ADC3时钟
//先初始化ADC3通道6,7 IO口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN; //模拟输入
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ;//不带上下拉
GPIO_Init(GPIOF, &GPIO_InitStructure);
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; //独立模式
ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;//两个采样阶段之间的延迟5个时钟
ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; //DMA失能
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;//预分频4分频。ADCCLK=PCLK2/4=84/4=21Mhz,ADC时钟最好不要超过36Mhz
ADC_CommonInit(&ADC_CommonInitStructure); //初始化
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; //12位模式
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //关闭连续转换
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;//外部触发极性设置为上升沿
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; //触发源选择TIM3_TRGO
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据右对齐
ADC_InitStructure.ADC_NbrOfConversion = 2; //两个转换通道
ADC_Init(ADC3, &ADC_InitStructure);
//设置规则通道转换顺序
ADC_RegularChannelConfig(ADC3,ADC_Channel_4 , 1, ADC_SampleTime_3Cycles);
ADC_RegularChannelConfig(ADC3,ADC_Channel_5 , 2, ADC_SampleTime_3Cycles);
ADC_DMARequestAfterLastTransferCmd(ADC3, ENABLE); //ADC在DMA传输结束后发出DMA请求
ADC_DMACmd(ADC3, ENABLE);
ADC_Cmd(ADC3, ENABLE);
ADC_SoftwareStartConv(ADC3); //启动 ADC3 的软件转换
}
4.3 定时器配置
定时器配置代码如下,要记得调用TIM_ClearITPendingBit(TIM3,TIM_IT_Update)清除中断标志位。
void timer3_init(void){
TIM_TimeBaseInitTypeDef TimeBaseInitStructrue;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
/*
*定时器时钟源频率为:TIMxCLK=84MHz
*定时器频率为:TIMxCLK/(TIM_Prescaler+1)=84/8400=10000Hz
*即计一个数的耗时为0.1ms
*总计数10次,则计时周期为1ms
*/
//配置预分频系数
TimeBaseInitStructrue.TIM_Prescaler=8400-1;
//配置装载值
TimeBaseInitStructrue.TIM_Period=10-1;
TimeBaseInitStructrue.TIM_CounterMode=TIM_CounterMode_Up;
TimeBaseInitStructrue.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3,&TimeBaseInitStructrue);
TIM_SelectOutputTrigger(TIM3,TIM_TRGOSource_Update);
//配置中断
NVIC_InitTypeDef NVIC_InitStructrue;
//选择中断源
NVIC_InitStructrue.NVIC_IRQChannel=TIM3_IRQn;
//设置抢占优先级
NVIC_InitStructrue.NVIC_IRQChannelPreemptionPriority=0;
//设置子优先级
NVIC_InitStructrue.NVIC_IRQChannelSubPriority=3;
//使能中断
NVIC_InitStructrue.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructrue);
//清除定时器更新中断标志位
TIM_ClearFlag(TIM3,TIM_FLAG_Update);
//开启定时器更新中断标志
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
//使能定时器
TIM_Cmd(TIM3,ENABLE);
}
//定时器3中断
void TIM3_IRQHandler(void){
if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET){
/***********************************/
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
}
4.4 处理采集数据
我们建立的缓冲区如下所示
uint16_t adc_data[20];//存储采集值的缓冲区
上面用到了两个通道转换,依次是CH4、CH5,则每次触发转换都是先转换CH4再转换CH5以执行完一次完整转换,也就是说,转换得到的数据是按照CH4、CH5、CH4、CH5…交替存放在adc_data数组里。
我们使用数据时,可以将采集的数据取平均值(由于数组大小为20,因此每个通道各有10个数据)。
void get_rocker_value(uint16_t* rocker1,uint16_t* rocker2){
uint32_t va1=0,va2=0;
for(uint8_t i=0;i<20;i+=2){
va1+=adc_data[i];
va2+=adc_data[i+1];
}
*rocker1=va1/10;
*rocker2=va2/10;
}