【海棠派开发板】第八讲 ADC-规则组多通道采样实验

发布于 2024-05-14 09:29:23

8. ADC-规则组多通道采样实验

8.1 实验内容

通过本实验主要学习以下内容:

• ADC的简介

GD32FH757 ADC工作原理

• DMA和DMAMUX的原理

• 规则组多通道循环采样

8.1.1 ADC原理

我们知道,自然界中有非常多的模拟信号,比如光照强度,还有其他的例如温度、声音等等,那么人们是怎么来衡量一个模拟信号的呢?

我们通常会说今天光照度达到了3万Lux(照度单位),现在测量到的体温是36.5℃,我们所处的环境是40分贝,没错,人们就是通过将这些模拟信号数字化,从而达到衡量这些模拟信号的目的。那对于MCU来说,如果要测量一个模拟量,可以通过自带的ADC(Analog-to-Digital converters)模块,即模-数转换器将模拟量转化为可以被MCU读取到的数字量。

8.1.2 GD32H757 ADC工作原理

GD32H757有3个逐次逼近型ADC(SAR ADC),其中ADC0最大有效位为14bit,有20个外部通道,一个内部通道(DAC_OUT0 通道);ADC1最大有效位为14bit,有 18 个外部通道,3 个内部通道(电池电压(VBAT)通道、 参考电压输入通道(VREFINT) 和 DAC_OUT1 通道);ADC2最大有效位为12bit, 有 17 个外部通道, 4 个内部通道(电池电压(VBAT)通道、 参考电压输入通道(VREFINT)、内部温度传感通道(VSENSE)和高精度温度传感器通道(VSENSE2))。

这三个ADC可以独立工作,也可以让ADC0和ADC1工作在同步模式下。有最多42个外部ADC引脚可用于将连接到这些引脚的电压值转换为数字量,这些引脚号可以通过Datasheet获得。

表中ADC012_INx的意思是:该IO口可以作为通道x用于ADC0、ADC1和ADC2。如ADC012_IN10,表示PC0可以用于ADC0的通道10使用,也可以作为ADC1和ADC2的通道10使用。但要注意:不能在同一个时刻让不同的ADC去转换同一个通道,否则会有无法预料的结果

以下总结了GD32H757 ADC的特性:

• 高性能:

– ADC采样分辨率: ADC0/1可配置14位、 12位、 10位或者8位分辨率, ADC2可配置12位、 10位、 8位或者6位分辨率;

– ADC0/1采样率: 14位分辨率为4 MSPs, 12位分辨率为4.5 MSPs, 10位分辨率为5.14 MSPs, 8位分辨率为6 MSPs。分辨率越低,转换越快;

– ADC2采样率: 12位分辨率为5.3 MSPs, 10位分辨率为6.15 MSPs, 8位分辨率为7.27 MSPs, 6位分辨率为8.89 MSPs。分辨率越低,转换越快;

– 前置校准时间: 131个ADC时钟周期;

– 可编程采样时间;

– 数据存储模式:最高有效位对齐和最低有效位对齐;

– DMA请求。

• 模拟输入通道:

– ADC0有20个外部模拟输入通道, ADC1有18个外部模拟输入通道, ADC2有17个外部模拟输入通道;

– 1个内部温度传感通道(VSENSE);

– 1个内部参考电压输入通道(VREFINT);

– 1个外部监测电池VBAT供电引脚输入通道;

– 1个内部高精度温度传感器通道(VSENSE2);

– 与DAC内部通道连接。

• 转换开始的发起:

– 软件;

– TRIGSEL触发。

• 运行模式:

– 转换单个通道,或者扫描一序列的通道;

– 单次运行模式,每次触发转换一次选择的输入通道;

– 连续运行模式,连续转换所选择的输入通道;

– 间断运行模式;

– 同步模式(适用于具有两个或多个ADC的设备)。

• 转换结果阈值监测器功能: 模拟看门狗。

• 常规序列转换结束、模拟看门狗事件和溢出事件都可以产生中断。

• 过采样:

– ADC0/1为32位的数据寄存器, ADC2为16位数据寄存器;

– ADC0/1可调整的过采样率,从2x到1024x, ADC2可调整的过采样率,从2x到256x;

– ADC0/1高达11位的可编程数据移位, ADC2为8位的可编程数据移位,。

• ADC0/1供电要求: 1.8V到3.6V,一般电源电压为3.3V, ADC2供电要求: 1.71V到3.6V,一般电源电压为3.3V;

• 通道输入范围: VREF- ≤VIN ≤VREF+;

• 数据可以路由到HPDF进行后期处理。

下面介绍下GD32H757的ADC框图:

标注1:输入电压和参考电压

输入电压引脚定义如下表:

GD32H757的ADC是14/12bit有效位的,14bit满量程对应的转换值为16383,12bit为4095,以14bit为例,当采样引脚上的电压等于ADC参考电压时,得到的转换值即为16383。故理论采样是指可通过以下公式得到:

采样数值=实际电压/参考电压*16383

标注2:输入通道

前面提到,ADC0有20个外部模拟输入通道, ADC1有18个外部模拟输入通道, ADC2有17个外部模拟输入通道。其中外部通道可以通过Datasheet进行查询。

标注3:规则组

GD32H757的ADC转换组称为规则组,也叫常规序列。

规则组有三个重要的参数,其一为转换的个数,其二为转换的序列,其三为转换周期,规定好这三个参数后,一旦开始规则组的转换,则ADC就按照转换序列一个一个的进行模-数转换,直到达到要求的转换个数。

规则组的转换个数由ADC_RSQ0寄存器的RL[3:0]位规定,转换的总数目为RL[3:0]+1,转换总数目最大为16个;转换序列和转换周期由ADC_RSQ0~ADC_RSQ8共同决定,我们以RSQ8寄存器为例来看下:

ADC_RSQ0寄存器:

举个例子,现需要按照CH3->CH2->CH1的顺序进行规则组转换,则设定RL[3:0] = 2,然后设定RSQ0为2(CH3),RSQ1为1(CH2),RSQ2为0(CH1),则当开始规则组转换时,ADC首先进行RSQ0规定的通道即CH3的转换,再进行RSQ1规定的通道即CH2的转换,最后进行RSQ2规定的通道即CH1转换,当这三个通道转换完后,规则组转换结束。

需要注意的是,每转换一个规则组通道,转换结果都会放在寄存器ADC_RDATA中,所以CPU一定要在下一个通道转换完成前将上一个通道转换结果读走,否则会导致上一个通道数据被新的数据覆盖。所以在多通道规则组转换时,为了保证能读到所有通道的数据,一定要使用DMA(直接存储器访问控制器),每个通道转换结束后,都会给DMA发送请求,DMA就会将最新的ADC_RDATA中的数据搬走。关于ADC配合DMA的使用,后面会详细介绍。

标注4:触发源

ADC的规则组需要选特定的触发源用于触发ADC转换,注意,ADC的Enable(即ADC_CTL1寄存器的ADC_ON位置“1”)不会触发ADC转换,而是当选定的触发源来临后ADC才开始转换。

触发源分为内部触发和外部触发,内部触发是指软件触发;外部触发源是除了内部触发源以外的触发源,外部触发源可以通过TRISEL选择。TRIGSEL触发相关内容,请读者参考GD32H7系列用户手册TRIGSEL章节。

标注5:规则组数据寄存器

如标注3规则组的表述,每个ADC的规则组只有一个数据寄存器ADC_RDATA,每转换一个通道,转换结果放在这个寄存器中,在下一通道转换结束前必须要将上一个通道的转换结果取走。

标注6:ADC中断及标志位

ADC的中断总共有两种:规则组转换结束中断以及模拟看门狗,可以通过将ADC_CTL0中的EOCIE、WDE0IE、WDE2IE、WDE2IE置“1”来开启相应中断。

ADC_STAT寄存器中的EOC和WDE0,1,2表示相应事件发生,EOC置“1”表示规则组的转换已经结束。

8.1.3 DMA原理

本实验中ADC通道有两个,分别为PC2_C和PC3_C,所以我们用规则组多通道采样实现双电压读取,从上一节内容中可以知道,ADC规则组实现多通道转换时,必须要用到DMA。下面我们介绍下DMA原理。

DMA(直接存储器访问控制器)是一个非常好用的外设,它提供了一种硬件的方式在外设和存储器之间或者存储器和存储器之间传输数据,而无需 CPU 的介入,从而使 CPU 可以专注在处理其他系统功能上。GD32H757有两个DMA,其中DMA0有8个通道,DMA1也有8个通道。

GD32H757支持DMA的单一传输和多拍传输模式。这里只讲解单一传输。DMA实现很简单,只要配置好以下几要素即可。

  1. 源地址和目标地址:DMA进行数据搬运过程为从源地址读取到数据,再搬运到目标地址。本实验中,需要把ADC转换结果搬运到自定义的buffer中,所以源地址就要设置为ADCx_RDATA寄存器地址,目标地址为buffer地址。
  2. 源和目标的地址增量方式:地址增量方式有固定模式和增量模式两种,固定模式是指进行一次DMA搬运后,下次搬运的源地址或目标地址保持不变;增量模式指进行一次DMA搬运后,下次搬运的源地址或目标地址会加1。本实验中,源地址始终都应该为ADCx_RDATA地址,所以源地址增量方式需要设置为固定模式,而目标地址为自定义buffer,我们需要用buffer[0]存储x轴数据,buffer[1]存储y轴数据,所以目标地址增量方式需要设置为增量模式。
  3. DMA传输方向:DMA传输方向有三种,分别为外设地址->存储器地址、存储器地址->外设地址以及存储器->存储器。本实验中源地址是外设地址,目标地址为自定义buffer地址即存储器地址,故传输方向需设置为外设地址->存储器地址。
  4. 源和目标数据位宽:源和目标数据位宽表示每次搬运的数据长度,可以设置为8bit、16bit和32bit。本实验中ADC的数据只占用ADCx_RDATA寄存器的低半字即16bit,所以源和目标位宽选择16bit即可。
  5. DMA传输个数和循环模式:传输个数表示一轮DMA传输可以搬运的次数。循环模式表示当一轮DMA传输结束后,是否直接进行下一轮搬运,当开启循环模式后,当上一轮DMA传输结束后,源地址和目标地址会恢复到最开始的状态。本实验中,需要转换2个通道ADC,故DMA传输个数设置为2,循环模式开启。
  6. DMA通道优先级:DMA的每个通道都有一个软件优先级,当DMA控制器在同一时间接收到多个外设请求时,仲裁器将根据外设请求的优先级来决定响应哪一个外设请求。优先级包括软件优先级和硬件优先级,优先级规则如下:

软件优先级:分为4级,低,中,高和极高。可以通过寄存器DMA_CHxCTL的PRIO位域来配置。
硬件优先级:当通道具有相同的软件优先级时,编号低的通道优先级高。例:通道0和通道2配置为相同的软件优先级时,通道0的优先级高于通道2。

上面描述了DMA配置的一些要素,那么DMA是如何被触发的呢,这里就要讲到另外一个外设:DMAMUX。

8.1.4 DMAMUX原理

我们来看下DMA系统架构:

我们可以看到,DMA0/1的每个通道都有一个输入源Peri_reqx,而这些输入源则是由DMAMUX输出。下图为DMAMUX的框图:

将以上两个图放在一起看就更好理解:

可以看到DMAMUX的请求路由器总共有16个通道,其中通道0~7用于DMA0的通道0~7;通道8~15用于DMA1的通道0~7。

那么请求路由器的触发源是什么呢?这个可以通过GD32H7用户手册DMAMUX章节中查出。比如本实验中使用PC2_C和PC3_C进行ADC转换,从datasheet中查出这两个引脚分别对应为ADC2_IN0和ADC2_IN1:

所以DMAMUX的请求路由器触发源选择ADC2即可,下图为DMAMUX请求输入源输入信号表部分截图:

讲到这里可能有的读者,特别是用过GD前期产品(如GD32F3/F4系列)的读者就会有疑问了,如果DMA用于ADC2,我需要选择哪个DMA通道呢?答案就是:任意一个没有被使用的通道即可!因为GD32H7产品的DMA通道没有绑定到特定外设上,所以只需要选择任意一个没有被使用的通道就可以了,这种设计方式,让DMA的使用更加的灵活方便。

8.2 硬件设计

本实验是使用PC2_C和PC3_C来进行电压采集,读者可以采用飞线方式外接电压到这两个引脚进行测试。

8.3 代码解析

本实验用到两个ADC2通道,使用ADC2规则组搭配DMA1通道0进行数据转换和搬运,ADC2规则组和DMA1通道0都开启循环模式,一旦开始ADC2规则组转换,会持续PC2_C和PC3_C上的电压进行转换和数据搬运。

8.3.1 DMA和ADC初始化

在driver_adc.c中定义driver_adc_regular_ch_dma_config函数,该函数实现DMA和ADC的初始化。

C
void driver_adc_regular_ch_dma_config(typdef_adc_ch_general *ADC, typdef_adc_ch_parameter *ADC_CH,void *buffer)
{
dma_single_data_parameter_struct dma_single_data_parameter;
rcu_periph_clock_enable(ADC->dma_parameter.rcu_dma);                                                                                                        /*DMA时钟开启*/
rcu_periph_clock_enable(RCU_DMAMUX);
dma_deinit(ADC->dma_parameter.dma_periph, ADC->dma_parameter.dma_channel);                /*DMA通道参数复位*/
/*DMA源地址、目标地址、增量方式、传输位宽、传输方向、传输个数、优先级设置*/
dma_single_data_parameter.request = ADC->dma_parameter.request;
dma_single_data_parameter.periph_addr  = (uint32_t)(&ADC_RDATA(ADC->adc_port));
dma_single_data_parameter.periph_inc   = DMA_PERIPH_INCREASE_DISABLE;
dma_single_data_parameter.memory0_addr  = (uint32_t)(buffer);
dma_single_data_parameter.memory_inc   = DMA_MEMORY_INCREASE_ENABLE;
if(ADC->adc_mode == ADC_DAUL_REGULAL_PARALLEL)
{
dma_single_data_parameter.periph_memory_width = DMA_PERIPH_WIDTH_32BIT;
}
else
{
dma_single_data_parameter.periph_memory_width = DMA_PERIPH_WIDTH_16BIT;
}
dma_single_data_parameter.direction    = DMA_PERIPH_TO_MEMORY;
dma_single_data_parameter.number       = ADC->dma_parameter.dma_number;
dma_single_data_parameter.priority     = ADC->dma_parameter.dma_priority;
dma_single_data_mode_init(ADC->dma_parameter.dma_periph, ADC->dma_parameter.dma_channel, &dma_single_data_parameter);
/*DMA循环模式设置*/
if(ADC->dma_parameter.dma_circulation_mode == ENABLE)
{
dma_circulation_enable(ADC->dma_parameter.dma_periph, ADC->dma_parameter.dma_channel);
}
else
{
dma_circulation_disable(ADC->dma_parameter.dma_periph, ADC->dma_parameter.dma_channel);
}
dma_channel_enable(ADC->dma_parameter.dma_periph, ADC->dma_parameter.dma_channel);        /* 使能DMA */
driver_adc_config(ADC,ADC_CH);                                                                                                                                                                                                                        /* ADC初始化 */
}

ADC初始化函数定义如下:

C++
void driver_adc_config(typdef_adc_ch_general *ADC,typdef_adc_ch_parameter *ADC_CH)
{
uint8_t i;
adc_idx_enum idx_adc;
adc_deinit(ADC->adc_port);
/* ADC clock config */

if(ADC->adc_port==ADC0){
idx_adc=IDX_ADC0;
}else if(ADC->adc_port==ADC1){
idx_adc=IDX_ADC1;
}else{
idx_adc=IDX_ADC2;
}
rcu_adc_clock_config(idx_adc, RCU_ADCSRC_PER);

adc_clock_config(ADC->adc_port, ADC->adc_psc);                                         /*配置ADC时钟频率*/
rcu_periph_clock_enable(ADC->rcu_adc);                                 /*使能ADC时钟*/

/*配置ADC相关IO口,先配置时钟,再将IO口设置为模拟输入*/
for(i=0 ;i<ADC->ch_count; i++)
{
if(ADC_CH[i].adc_channel < ADC_CHANNEL_17)
{
rcu_periph_clock_enable(ADC_CH[i].rcu_port);
gpio_mode_set(ADC_CH[i].port, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, ADC_CH[i].pin);
}
else
{
if(ADC->adc_port==ADC2)
{
if(ADC_CH[i].adc_channel == ADC_CHANNEL_17)
{
adc_internal_channel_config(ADC_CHANNEL_INTERNAL_VBAT, ENABLE);
}
else if(ADC_CH[i].adc_channel == ADC_CHANNEL_18)
{
adc_internal_channel_config(ADC_CHANNEL_INTERNAL_TEMPSENSOR, ENABLE);
}
else if(ADC_CH[i].adc_channel == ADC_CHANNEL_19)
{
adc_internal_channel_config(ADC_CHANNEL_INTERNAL_VREFINT, ENABLE);
}
else if(ADC_CH[i].adc_channel == ADC_CHANNEL_20)
{
adc_internal_channel_config(ADC_CHANNEL_INTERNAL_HP_TEMPSENSOR, ENABLE);
}
}
else
{
rcu_periph_clock_enable(ADC_CH[i].rcu_port);
gpio_mode_set(ADC_CH[i].port, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, ADC_CH[i].pin);
}
}

}
adc_sync_mode_config(ADC->adc_mode);                                                                /*配置ADC工作模式,如独立模式,规则并行模式等*/
adc_special_function_config(ADC->adc_port, ADC_SCAN_MODE, ADC->adc_scan_function);        /*配置规则组的扫描模式和连续转换模式*/
if(ADC->adc_channel_group == ADC_REGULAR_CHANNEL)
{
adc_special_function_config(ADC->adc_port, ADC_CONTINUOUS_MODE, ADC->adc_continuous_function);
}
adc_data_alignment_config(ADC->adc_port, ADC_DATAALIGN_RIGHT);                                                                                        /*选择数据右对齐*/
adc_channel_length_config(ADC->adc_port, ADC->adc_channel_group, ADC->ch_count);                /*配置转换通道数*/
if(ADC->adc_channel_group == ADC_REGULAR_CHANNEL)                                                                                                                                                /*配置转换顺序*/
{
for(i = 0;i< ADC->ch_count;i++)
{
adc_regular_channel_config(ADC->adc_port, i, ADC_CH[i].adc_channel,ADC_CH[i].sample_time);
}
}
else if(ADC->adc_channel_group == ADC_INSERTED_CHANNEL)
{
for(i = 0;i< ADC->ch_count;i++)
{
adc_inserted_channel_config(ADC->adc_port, i, ADC_CH[i].adc_channel,ADC_CH[i].sample_time);
}
}
/*选择触发源及使能外部触发模式*/
adc_external_trigger_config(ADC->adc_port, ADC->adc_channel_group, ADC->adc_external_trigger_mode);
/*选择是否需要使用DMA*/
if(ADC->DMA_mode == ENABLE)
{
adc_dma_request_after_last_enable(ADC->adc_port);
adc_dma_mode_enable(ADC->adc_port);
}
/*ADC的使能和自校准,ADC使能后需要经过一定的ADC_CLK后才能校准,本示例中直接使用1ms延时*/
adc_enable(ADC->adc_port);
delay_ms(1);
/* ADC calibration mode config */
adc_calibration_mode_config(ADC->adc_port, ADC_CALIBRATION_OFFSET_MISMATCH);
/* ADC calibration number config */
adc_calibration_number(ADC->adc_port, ADC_CALIBRATION_NUM32);
adc_calibration_enable(ADC->adc_port);
}

在driver_adc.h中声明了ADC DMA的结构体:

C
typedef struct __typdef_adc_dma_parameter
{
rcu_periph_enum rcu_dma;                //DMA时钟
uint32_t dma_periph;                                //DMA号
dma_channel_enum dma_channel;//DMA通道号
uint32_t request;//DMA请求
uint32_t dma_number;                                //DMA传输个数
uint32_t dma_priority;                        //DMA通道优先级
EventStatus dma_circulation_mode;//循环模式
}typdef_adc_dma_parameter;

这段代码比较简单,请读者按照前面介绍的DMA原理进行解析。

8.3.2 BSP_ADC设置所需要的参数及IO口结构体定义

在bsp_adc.c中,对BSP_ADC设置所需要的参数及IO扩结构体数组进行了定义:

C
typdef_adc_ch_general  BSP_ADC= {
.rcu_adc = RCU_ADC2,                                                                                                                        /* ADC2的时钟 */
.adc_psc = ADC_CLK_SYNC_HCLK_DIV6,                                                                /* ADC2设置为HCLK 6分频 */
.adc_port = ADC2,                                                                                                                                        /* ADC口为ADC2 */
.adc_mode = ADC_SYNC_MODE_INDEPENDENT,                                                /* ADC模式为独立模式 */
.adc_channel_group = ADC_REGULAR_CHANNEL,                                        /* 使用规则组 */
.adc_scan_function = ENABLE,                                                                                        /* 开启扫描模式 */
.adc_continuous_function = ENABLE,                                                                /* 开启循环模式 */
.ch_count = 2,                                                                                                                                                /* 转换长度为2 */
.adc_external_trigger_mode = EXTERNAL_TRIGGER_DISABLE,
.dma_parameter =
{
.rcu_dma = RCU_DMA1,                                                                                                                /* DMA1的时钟 */
.dma_periph = DMA1,                                                                                                                        /* 使用DMA1 */
.dma_channel = DMA_CH0,                                                                                                        /* 使用通道4 */
.dma_number = 2,                                                                                                                                /* DMA传输长度为2 */
.request = DMA_REQUEST_ADC2,
.dma_priority = DMA_PRIORITY_HIGH,                                                        /* DMA通道优先级 */
.dma_circulation_mode = ENABLE                                                                        /* DMA循环模式打开 */
},
.DMA_mode = ENABLE                                                                                                                                /* 使用DMA */
};

typdef_adc_ch_parameter BSP_ADC_ch[2] =
{
{
.rcu_port = RCU_GPIOC,                                                                                                                /* GPIOC时钟 */
.port = GPIOC,                                                                                                                                                /* GPIO port */
.pin = GPIO_PIN_2,                                                                                                                                /* PC2 */
.gpio_speed = GPIO_OSPEED_12MHZ,                                                                        /* PC2速度设置为12MHz */
.adc_channel = ADC_CHANNEL_0,                                                                                        /* PC2是ADC2的通道0 */
.sample_time = 240                                                                                                                                /* 设置采样周期为240 */
}
,
{
.rcu_port = RCU_GPIOC,                                                                                                                /* GPIOC时钟 */
.port = GPIOC,                                                                                                                                                /* GPIO port */
.pin = GPIO_PIN_3,                                                                                                                                /* PC3 */
.gpio_speed = GPIO_OSPEED_12MHZ,                                                                                /* PC3速度设置为12MHz */
.adc_channel = ADC_CHANNEL_1,                                                                                        /* PC3是ADC2的通道1 */
.sample_time = 240                                                /* 设置采样周期为55.5 */
}
};/* ADC通道参数配置,包括IO口,和对应通道以及采样周期 */

8.3.3 BSP_ ADC初始化和触发ADC转换的具体实现函数

在bsp_adc.c中定义了 DMA和ADC初始化和触发ADC转换的函数:

C
uint16_t BSP_ADC_data[2] ;
void bsp_ADC_config()
{
driver_adc_regular_ch_dma_config(&BSP_ADC,BSP_ADC_ch,(uint16_t*)BSP_ADC_data);
driver_adc_software_trigger_enable(&BSP_ADC);
}

8.3.4 main函数实现

C
int main(void)
{
driver_init();//延时函数初始化
bsp_uart_init(&BOARD_UART);//BOARD_UART串口初始化
bsp_ADC_config();//bsp ADC配置
while (1)
{
delay_ms(100);//延时100ms
printf_log(" the BSP_ADC data is %d,%d \r\n", BSP_ADC_data[0],BSP_ADC_data[1]);//打印ADC数据
}
}

本例程main函数首先进行了延时函数初始化,为了演示实验结果,这里初始化了BOARD_UART串口,关于串口的使用,请读者参考串口章节,然后是BSP_ADC配置。在主循环中,每100ms打印一次PC2_C和PC3_C的ADC转换数据。

8.4 实验结果

如上main函数实现说明。

0 条评论