记录 STM32 和 GD32 中 ADC 的学习笔记和心得。本文中主要解析 ADC 的规则转换,不涉及注入转…
写在前边的话
本文中所有代码示例均部署在 GD32F303 平台。
ADC 值与电压的关系
ADC,即模数转换,是利用数字量来表示模拟量。ADC 值与模拟量的关系可以用下图来表示,以 12 位 ADC 为例。这里的“12 位”是指 ADC 的分辨率是 12 位,在 2 进制中,12 位无符号整型的取值范围为 0~4095,即 0~2^12-1。如果 ADC 的分辨率是 8,那么 ADC 值范围为 0~255,如果是 16 位 ADC,那么 ADC 值范围在 0~65535 之间。
使用 ADC 值表达电压值的过程更像是用尺子丈量,每一个 ADC 值都唯一对应一个电压值,因为是比例关系,所以 ADC 与电压值的换算关系为:电压值=VREF- + (VREF+ – VREF-) * ADC / 4095。这里的 VREF 即为参考电压,有正有负。有些单片机比较简单,例如 GD32F303RCT6,VREF- 等于 GND,VREF+ 等于供电电压,如果单片机的供电电压为 3.3V,那么 ADC 与电压值的换算关系为:电压值= 3.3 * ADC / 4095。有些单片机会有额外的引脚来单独配置 VREF+ 和 VREF-,如此一来用户可以用一个高精度的参考电压电路来为 ADC 提供参考电压,这样做可以避免 ADC 测量收到供电端的影响。

实际项目中,产品里可能包含电机等器件,这些器件启动的一瞬间会消耗大量的电量,造成供电电压瞬间跌落。此时如果 VREF+ 与供电电压绑到一起,就会干扰到 ADC 转换。还有一种情况是电源部分设计不好,噪声很大,此时没有独立的参考电压的话,同样也会影响到单片机的 ADC 采样。
对于 STM32 和 GD32, ADC 分辨率一般是 12 位,可以配置成 8 位甚至是 6 位。在 12 位分辨率的情况下,假定 VREF- 为 0V,VREF+ 为 3.3V,那么此时 ADC 最小分辨电压为 3.3V / 4095 = 0.8mV。这对于大多数项目是足够用了,对于一些高精度场合,可以外挂一些高精度 ADC 芯片。如果 ADC 芯片的分辨率为 16 位,同样 3.3V 参考电压下,最小分辨电压为 3.3V / 65535 = 80μV;如果是 24 位分辨率,那么最小分辨电压甚至能达到 196.7nV。当然,ADC 分辨率越高,价格也就越贵。
ADC 触发源
STM32 和 GD32 的 ADC 可以由软件触发,也可以由硬件定时器触发或外部中断触发,如下所示。

软件触发,即用户可以在软件上通过一条指令触发 ADC 转换。对于 GD32F303 系列,就是往 ADC_CTL1 寄存器的 SWRCST 位写 1,如下所示。简单应用中,用户可以直接用软件触发 ADC 转换,然后等待 ADC 转换完成,最后再获取 ADC 转换结果即可。

单通道+软件触发
对于 GD32F303 系列,ADC 单通道输入,使用软件触发示例代码如下所示。这个方法简单方便,缺点是需要等待 ADC 转换完成,会造成 CPU 资源的浪费。此示例中,ADC 的采样率等于 ReadADC 函数被调用的频率。
#include "gd32f30x_conf.h"
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//配置ADC时钟源
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);
//GPIO配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
//所有ADC独立工作
adc_mode_config(ADC_MODE_FREE);
//配置ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
adc_special_function_config(ADC0, ADC_SCAN_MODE, DISABLE); //关闭扫描
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); //关闭连续转换
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); //规则组使用软件触发
adc_oversample_mode_disable(ADC0); //禁用过采样
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
}
/*********************************************************************************************************
* 函数名称: ReadADC
* 函数功能: 读取 ADC 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: ADC 转换结果
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
unsigned short ReadADC(void)
{
unsigned short adc;
//清除接收完成标志位
adc_flag_clear(ADC0, ADC_FLAG_EOC);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
//等待 ADC 转换完成
while(RESET == adc_flag_get(ADC0, ADC_FLAG_EOC)){}
//获取 ADC 值
adc = adc_regular_data_read(ADC0);
//返回转换结果
return adc;
}
多通道+软件触发
对于多通道输入,需要在读取之前配置采样顺序,即切换到特定的采样通道,代码如下所示。同样的,这个示例也会造成 CPU 资源的浪费,仅适合于简单项目。
#include "gd32f30x_conf.h"
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//配置ADC时钟源
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);
//GPIO配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_1);
//所有ADC独立工作
adc_mode_config(ADC_MODE_FREE);
//配置ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
adc_special_function_config(ADC0, ADC_SCAN_MODE, DISABLE); //关闭扫描
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); //关闭连续转换
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); //规则组使用软件触发
adc_oversample_mode_disable(ADC0); //禁用过采样
//配置默认通道采样顺序
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: ADC 转换结果
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
unsigned short ADC0ReadCH0(void)
{
unsigned short adc;
//切换到通道 0
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//清除接收完成标志位
adc_flag_clear(ADC0, ADC_FLAG_EOC);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
//等待 ADC 转换完成
while(RESET == adc_flag_get(ADC0, ADC_FLAG_EOC)){}
//获取 ADC 值
adc = adc_regular_data_read(ADC0);
//返回转换结果
return adc;
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH1
* 函数功能: 读取 ADC0 通道 1 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: ADC 转换结果
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
unsigned short ADC0ReadCH1(void)
{
unsigned short adc;
//切换到通道 1
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_1, ADC_SAMPLETIME_239POINT5);
//清除接收完成标志位
adc_flag_clear(ADC0, ADC_FLAG_EOC);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
//等待 ADC 转换完成
while(RESET == adc_flag_get(ADC0, ADC_FLAG_EOC)){}
//获取 ADC 值
adc = adc_regular_data_read(ADC0);
//返回转换结果
return adc;
}
多通道+DMA+软件触发
使用 DMA 实现 ADC 多通道输入如下所示。需要注意,为防止数据缓冲区中的 ADC 通道数据出现错位,DMA 配置需要在 ADC 配置之前。注意:使用 DMA 的情况下,无论通道数量是多少,必须开启扫描模式。
#include "gd32f30x_conf.h"
//通道数量
#define ADC_CH_NUM 2
//ADC 数据缓冲区,由 DMA 自动搬运
static unsigned short s_arrADCBuf[ADC_CH_NUM] = {0};
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//DMA 初始化结构体
dma_parameter_struct dma_init_struct;
//DMA 配置
rcu_periph_clock_enable(RCU_DMA0); //使能 DMA0 时钟
dma_deinit(DMA0, DMA_CH0); //初始化结构体设置默认值
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; //设置数据传输方向
dma_init_struct.memory_addr = (uint32_t)s_arrADCBuf; //内存地址设置
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //内存增长使能
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; //内存数据位数设置
dma_init_struct.number = ADC_CH_NUM; //内存数据量设置
dma_init_struct.periph_addr = (uint32_t)&(ADC_RDATA(ADC0)); //外设地址设置
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址增长失能
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; //外设数据位数设置
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级设置
dma_init(DMA0, DMA_CH0, &dma_init_struct); //初始化结构体
dma_circulation_enable(DMA0, DMA_CH0); //使能循环
dma_memory_to_memory_disable(DMA0, DMA_CH0); //禁用内存到内存
dma_channel_enable(DMA0, DMA_CH0); //使能 DMA
//配置ADC时钟源
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);
//GPIO 配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_1);
//所有 ADC 独立工作
adc_mode_config(ADC_MODE_FREE);
//配置 ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); //使能 ADC 扫描,即开启多通道转换
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); //使能连续采样
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, ADC_CH_NUM); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); //规则组使用软件触发
adc_oversample_mode_disable(ADC0); //禁用过采样
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
adc_regular_channel_config(ADC0, 1, ADC_CHANNEL_1, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
//使能 DMA
adc_dma_mode_enable(ADC0);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: ADC 转换结果
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
unsigned short ADC0ReadCH0(void)
{
return s_arrADCBuf[0];
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH1
* 函数功能: 读取 ADC0 通道 1 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: ADC 转换结果
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
unsigned short ADC0ReadCH1(void)
{
return s_arrADCBuf[1];
}
DMA+软件触发+高速采集
上述方法实现的 ADC 驱动,采样率完全由用户的读取频率来决定。然而,这种方式采样率很低,一般只能达到几 kHz。而且在系统高负荷工作情况下,采样率的精度得不到保障。例如采样任务放到了 2ms 任务中,每隔 2ms 采集一次数据,这样一来采样率为 500Hz。但是,由于系统高负荷运行,导致 2ms 任务不是那么准确,这就严重干扰到了 ADC 的采样。如果是心电数据,那么这个干扰会对滤波器造成很大的影响。
使用 DMA 高速采集 ADC 数据示例如下。这里,ADC 时钟为 APB2 的 8 分频,即 120MHz / 8 = 15MHz。所以 ADC 的转换率为 15MHz / (239.5 + 12.5) = 59.524kHz。
初始化时,DMA 传输数据量为 1024,所以 ADC 每转换完成一次,就会自动触发 DMA 传输,将 ADC 转换结果保存到 s_arrADCBuf 缓冲区中。此时,s_arrADCBuf 缓冲区中数据的采样率即为 ADC 的转换率,为 59.524kHz。用户可以通过调节 ADC 时钟和 ADC 通道的采样时间来调节采样率。
注意:在这里 DMA 被配置成了单次传输模式,并且开启了 DMA 传输完成中断。一旦数据采集完成,DMA 中断里会设定标志位,然后将数据移交到主线程去处理。数据处理完成后,用户需要调用 InitADC0 函数,重新配置 ADC,用以触发下一次采样。
#include "gd32f30x_conf.h"
//空指针定义
#ifndef NULL
#define NULL 0
#endif
//通道采样数量
#define ADC_CH_LEN 1024
//ADC 数据缓冲区,由 DMA 自动搬运
static unsigned short s_arrADCBuf[ADC_CH_LEN] = {0};
static unsigned char s_iADCFlag = 0;
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//DMA 初始化结构体
dma_parameter_struct dma_init_struct;
//DMA 配置
rcu_periph_clock_enable(RCU_DMA0); //使能 DMA0 时钟
dma_deinit(DMA0, DMA_CH0); //初始化结构体设置默认值
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; //设置数据传输方向
dma_init_struct.memory_addr = (uint32_t)s_arrADCBuf; //内存地址设置
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //内存增长使能
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; //内存数据位数设置
dma_init_struct.number = ADC_CH_LEN; //内存数据量设置
dma_init_struct.periph_addr = (uint32_t)&(ADC_RDATA(ADC0)); //外设地址设置
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址增长失能
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; //外设数据位数设置
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级设置
dma_init(DMA0, DMA_CH0, &dma_init_struct); //初始化结构体
dma_circulation_disable(DMA0, DMA_CH0); //关闭循环
dma_memory_to_memory_disable(DMA0, DMA_CH0); //禁用内存到内存
nvic_irq_enable(DMA0_Channel0_IRQn, 2, 2); //使能 DMA0 通道 0 的 NVIC
dma_interrupt_enable(DMA0, DMA_CH0, DMA_INT_FTF); //开启接收完成中断
dma_channel_enable(DMA0, DMA_CH0); //使能 DMA
//配置 ADC 时钟源
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);
//GPIO 配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
//所有 ADC 独立工作
adc_mode_config(ADC_MODE_FREE);
//配置 ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); //使能 ADC 扫描,即开启多通道转换
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); //使能连续采样
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); //规则组使用软件触发
adc_oversample_mode_disable(ADC0); //禁用过采样
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
//使能 DMA
adc_dma_mode_enable(ADC0);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
}
/*********************************************************************************************************
* 函数名称: DMA0_Channel0_IRQHandler
* 函数功能: DMA0 通道 0 中断服务函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void DMA0_Channel0_IRQHandler(void)
{
//清除中断标志位
dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF);
//标记已经接收到一批数据
s_iADCFlag = 1;
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: NULL:数据接收尚未完成,其它:ADC 数据缓冲区
* 创建日期: 2023年09月26日
* 注 意: 必须要重新初始化才能触发下一次转换
**********************************************************************************************************/
unsigned short* ADC0ReadCH0(void)
{
if(0 == s_iADCFlag)
{
return NULL;
}
s_iADCFlag = 0;
return s_arrADCBuf;
}
DMA+定时器更新事件触发+高速采集
使用 ADC 转换率做为采样率固然方便,可是不是那么灵活。这时候我们可以用一个定时器触发 ADC 转换,如下所示。通过参考手册可以知道,ADC0 可以被 TIMER2 的更新事件触发。此处定时器更新时间周期设定为 100us,采样率直接就是 10kHz,非常灵活方便。
#include "gd32f30x_conf.h"
//空指针定义
#ifndef NULL
#define NULL 0
#endif
//通道采样数量
#define ADC_CH_LEN 1024
//ADC 数据缓冲区,由 DMA 自动搬运
static unsigned short s_arrADCBuf[ADC_CH_LEN] = {0};
static unsigned char s_iADCFlag = {0};
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//DMA 初始化结构体
dma_parameter_struct dma_init_struct;
//定时器初始化结构体
timer_parameter_struct timer_initpara;
//DMA 配置
rcu_periph_clock_enable(RCU_DMA0); //使能 DMA0 时钟
dma_deinit(DMA0, DMA_CH0); //初始化结构体设置默认值
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; //设置数据传输方向
dma_init_struct.memory_addr = (uint32_t)s_arrADCBuf; //内存地址设置
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //内存增长使能
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; //内存数据位数设置
dma_init_struct.number = ADC_CH_LEN; //内存数据量设置
dma_init_struct.periph_addr = (uint32_t)&(ADC_RDATA(ADC0)); //外设地址设置
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址增长失能
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; //外设数据位数设置
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级设置
dma_init(DMA0, DMA_CH0, &dma_init_struct); //初始化结构体
dma_circulation_disable(DMA0, DMA_CH0); //关闭循环
dma_memory_to_memory_disable(DMA0, DMA_CH0); //禁用内存到内存
nvic_irq_enable(DMA0_Channel0_IRQn, 2, 2); //使能 DMA0 通道 0 的 NVIC
dma_interrupt_enable(DMA0, DMA_CH0, DMA_INT_FTF); //开启接收完成中断
dma_channel_enable(DMA0, DMA_CH0); //使能 DMA
//配置 TIMER2
rcu_periph_clock_enable(RCU_TIMER2); //使能 TIMER2 的时钟
timer_deinit(TIMER2); //设置 TIMER2 参数恢复默认值
timer_struct_para_init(&timer_initpara); //初始化 timer_initpara
timer_initpara.prescaler = 119; //设置预分频器值
timer_initpara.counterdirection = TIMER_COUNTER_UP; //设置向上计数模式
timer_initpara.period = 99; //设置自动重装载值
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; //设置时钟分割
timer_init(TIMER2, &timer_initpara); //根据参数初始化定时器
timer_master_output_trigger_source_select(TIMER2, TIMER_TRI_OUT_SRC_UPDATE); //定时器更新事件做为触发源
//GPIO 配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
//配置 ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8); //配置 ADC 时钟源
adc_mode_config(ADC_MODE_FREE); //所有 ADC 独立工作
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); //使能 ADC 扫描,即开启多通道转换
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); //关闭连续采样
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_REGULAR_T2_TRGO); //规则组使用 TIMER2 触发
adc_oversample_mode_disable(ADC0); //禁用过采样
adc_dma_mode_enable(ADC0); //使能 DMA
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
//开始转换
timer_enable(TIMER2);
}
/*********************************************************************************************************
* 函数名称: DMA0_Channel0_IRQHandler
* 函数功能: DMA0 通道 0 中断服务函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void DMA0_Channel0_IRQHandler(void)
{
//清除中断标志位
dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF);
//标记已经接收到一批数据
s_iADCFlag = 1;
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: NULL:数据接收尚未完成,其它:ADC 数据缓冲区
* 创建日期: 2023年09月26日
* 注 意: 必须要重新初始化才能触发下一次转换
**********************************************************************************************************/
unsigned short* ADC0ReadCH0(void)
{
if(0 == s_iADCFlag)
{
return NULL;
}
s_iADCFlag = 0;
return s_arrADCBuf;
}
DMA+定时器比较事件触发+高速采集
除了更新事件外,ADC 还可以通过定时器的比较事件输出触发,如下所示。注意:设定自动重装载值时,需要同步更新 TIMER_CHxCV 寄存器的值,使得输出的 PWM 保持 50% 的占空比。
#include "gd32f30x_conf.h"
//空指针定义
#ifndef NULL
#define NULL 0
#endif
//通道数量
#define ADC_CH_LEN 1024
//ADC 数据缓冲区,由 DMA 自动搬运
static unsigned short s_arrADCBuf[ADC_CH_LEN] = {0};
static unsigned char s_iADCFlag = {0};
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//DMA 初始化结构体
dma_parameter_struct dma_init_struct;
//定时器初始化结构体
timer_oc_parameter_struct timer_ocintpara;
timer_parameter_struct timer_initpara;
//DMA 配置
rcu_periph_clock_enable(RCU_DMA0); //使能 DMA0 时钟
dma_deinit(DMA0, DMA_CH0); //初始化结构体设置默认值
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; //设置数据传输方向
dma_init_struct.memory_addr = (uint32_t)s_arrADCBuf; //内存地址设置
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //内存增长使能
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; //内存数据位数设置
dma_init_struct.number = ADC_CH_LEN; //内存数据量设置
dma_init_struct.periph_addr = (uint32_t)&(ADC_RDATA(ADC0)); //外设地址设置
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址增长失能
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; //外设数据位数设置
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级设置
dma_init(DMA0, DMA_CH0, &dma_init_struct); //初始化结构体
dma_circulation_disable(DMA0, DMA_CH0); //关闭循环
dma_memory_to_memory_disable(DMA0, DMA_CH0); //禁用内存到内存
nvic_irq_enable(DMA0_Channel0_IRQn, 2, 2); //使能 DMA0 通道 0 的 NVIC
dma_interrupt_enable(DMA0, DMA_CH0, DMA_INT_FTF); //开启接收完成中断
dma_channel_enable(DMA0, DMA_CH0); //使能 DMA
//配置 TIMER0
rcu_periph_clock_enable(RCU_TIMER0);
timer_deinit(TIMER0);
timer_struct_para_init(&timer_initpara); //初始化 timer_initpara
timer_initpara.prescaler = 119; //设置预分频
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; //设置对齐模式
timer_initpara.counterdirection = TIMER_COUNTER_UP; //设置计数模式
timer_initpara.period = 99; //设置重装载值
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; //设置时钟分割
timer_init(TIMER0, &timer_initpara); //初始化结构体
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_LOW; //通道输出极性设置
timer_ocintpara.outputstate = TIMER_CCX_ENABLE; //通道输出状态设置
timer_channel_output_config(TIMER0, TIMER_CH_0, &timer_ocintpara); //通道输出初始化
timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_0, 49); //设置 50% 占空比
timer_channel_output_mode_config(TIMER0, TIMER_CH_0, TIMER_OC_MODE_PWM1); //通道输出模式配置
timer_channel_output_shadow_config(TIMER0, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE); //失能比较影子寄存器
timer_auto_reload_shadow_enable(TIMER0); //自动重载影子使能
timer_primary_output_config(TIMER0, ENABLE); //TIMER0 使能
//GPIO 配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
//配置 ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8); //配置 ADC 时钟源
adc_mode_config(ADC_MODE_FREE); //所有 ADC 独立工作
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); //使能 ADC 扫描,即开启多通道转换
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); //关闭连续采样
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_EXTTRIG_REGULAR_T0_CH0); //规则组使用 TIMER0 触发
adc_oversample_mode_disable(ADC0); //禁用过采样
adc_dma_mode_enable(ADC0); //使能 DMA
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
//开始转换
timer_enable(TIMER0);
}
/*********************************************************************************************************
* 函数名称: DMA0_Channel0_IRQHandler
* 函数功能: DMA0 通道 0 中断服务函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void DMA0_Channel0_IRQHandler(void)
{
//清除中断标志位
dma_interrupt_flag_clear(DMA0, DMA_CH0, DMA_INT_FLAG_FTF);
//标记已经接收到一批数据
s_iADCFlag = 1;
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: NULL:数据接收尚未完成,其它:ADC 数据缓冲区
* 创建日期: 2023年09月26日
* 注 意: 必须要重新初始化才能触发下一次转换
**********************************************************************************************************/
unsigned short* ADC0ReadCH0(void)
{
if(0 == s_iADCFlag)
{
return NULL;
}
s_iADCFlag = 0;
return s_arrADCBuf;
}
定时器驱动 DMA+高速采集
另外,我们还可以用定时器驱动 DMA 转换实现 ADC 高速采样。此时 ADC 使用软件触发即可,并且开启连续转换模式。利用定时器的更新事件,以一定频率触发 DMA 传输,将 ADC 转换结果保存到内存中。实现代码如下所示。
#include "gd32f30x_conf.h"
//空指针定义
#ifndef NULL
#define NULL 0
#endif
//通道采样数量
#define ADC_CH_LEN 1024
//ADC 数据缓冲区,由 DMA 自动搬运
static unsigned short s_arrADCBuf[ADC_CH_LEN] = {0};
static unsigned char s_iADCFlag = {0};
/*********************************************************************************************************
* 函数名称: InitADC0
* 函数功能: 初始化 ADC0
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void InitADC0(void)
{
//局部变量
dma_parameter_struct dma_init_struct; //DMA 初始化结构体
timer_parameter_struct timer_initpara; //定时器初始化结构体
//配置 TIMER1
rcu_periph_clock_enable(RCU_TIMER1); //使能 TIMER1 的时钟
timer_deinit(TIMER1); //设置 TIMER1 参数恢复默认值
timer_struct_para_init(&timer_initpara); //初始化 timer_initpara
timer_initpara.prescaler = 119; //设置预分频器值,1MHz
timer_initpara.counterdirection = TIMER_COUNTER_UP; //设置向上计数模式
timer_initpara.period = 99; //设置自动重装载值
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; //设置时钟分割
timer_init(TIMER1, &timer_initpara); //根据参数初始化定时器
timer_enable(TIMER1); //使能定时器
timer_dma_enable(TIMER1, TIMER_DMA_UPD); //定时器更新事件作为 DMA 触发源
//DMA 配置
rcu_periph_clock_enable(RCU_DMA0); //使能 DMA0 时钟
dma_deinit(DMA0, DMA_CH1); //初始化结构体设置默认值
dma_init_struct.direction = DMA_PERIPHERAL_TO_MEMORY; //设置数据传输方向
dma_init_struct.memory_addr = (uint32_t)s_arrADCBuf; //内存地址设置
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE; //内存增长使能
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_16BIT; //内存数据位数设置
dma_init_struct.number = ADC_CH_LEN; //内存数据量设置
dma_init_struct.periph_addr = (uint32_t)&(ADC_RDATA(ADC0)); //外设地址设置
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE; //外设地址增长失能
dma_init_struct.periph_width = DMA_PERIPHERAL_WIDTH_16BIT; //外设数据位数设置
dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH; //优先级设置
dma_init(DMA0, DMA_CH1, &dma_init_struct); //初始化结构体
dma_circulation_disable(DMA0, DMA_CH1); //关闭循环
dma_memory_to_memory_disable(DMA0, DMA_CH1); //禁用内存到内存
nvic_irq_enable(DMA0_Channel1_IRQn, 2, 2); //使能 DMA0 通道 1 的 NVIC
dma_interrupt_enable(DMA0, DMA_CH1, DMA_INT_FTF); //开启接收完成中断
dma_channel_enable(DMA0, DMA_CH1); //使能 DMA
//GPIO 配置
rcu_periph_clock_enable(RCU_GPIOA);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0);
//配置 ADC0
rcu_periph_clock_enable(RCU_ADC0); //使能 ADC0 的时钟
adc_deinit(ADC0); //复位 ADC0
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8); //配置 ADC 时钟源
adc_mode_config(ADC_MODE_FREE); //所有 ADC 独立工作
adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); //使能 ADC 扫描,即开启多通道转换
adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, ENABLE); //使能连续采样
adc_resolution_config(ADC0, ADC_RESOLUTION_12B); //规则组配置,12 位分辨率
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); //右对齐
adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, 1); //规则组长度,即 ADC 通道数量
adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); //规则组使能外部触发
adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_NONE); //规则组使用软件触发
adc_oversample_mode_disable(ADC0); //禁用过采样
adc_dma_mode_enable(ADC0); //使能 DMA
//配置通道采样顺序,采样顺序从 0 开始
//ADC 转换率:(239.5 + 12.5) / (120MHz / 8) = 16.8us
adc_regular_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_239POINT5);
//ADC0 使能
adc_enable(ADC0);
//使能 ADC0 校准
adc_calibration_enable(ADC0);
//规则组软件触发
adc_software_trigger_enable(ADC0, ADC_REGULAR_CHANNEL);
//开始转换
timer_enable(TIMER1);
}
/*********************************************************************************************************
* 函数名称: DMA0_Channel1_IRQHandler
* 函数功能: DMA0 通道 1 中断服务函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月26日
* 注 意:
**********************************************************************************************************/
void DMA0_Channel1_IRQHandler(void)
{
//清除中断标志位
dma_interrupt_flag_clear(DMA0, DMA_CH1, DMA_INT_FLAG_FTF);
//标记已经接收到一批数据
s_iADCFlag = 1;
}
/*********************************************************************************************************
* 函数名称: ADC0ReadCH0
* 函数功能: 读取 ADC0 通道 0 转换结果
* 输入参数: void
* 输出参数: void
* 返 回 值: NULL:数据接收尚未完成,其它:ADC 数据缓冲区
* 创建日期: 2023年09月26日
* 注 意: 必须要重新初始化才能触发下一次转换
**********************************************************************************************************/
unsigned short* ADC0ReadCH0(void)
{
if(0 == s_iADCFlag)
{
return NULL;
}
s_iADCFlag = 0;
return s_arrADCBuf;
}