ADC 学习笔记
ADC 学习笔记

ADC 学习笔记

记录 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;
}