手把手教你使用STM32的ADC(原创)

模数转换器ADC是STM32单片机芯片自带的关键外设,其将模拟信号转换为数字信号,从而使单片机能够测量电压信号。本文将主要介绍STM32 ADC的使用情景、基于cube mx+hal库平台的使用、基本程序设计模式。

本文采用的是最经典常用的方案,即cube mx+hal库+ADC+DMA+滑动窗口滤波,最后基于这一方案实现了一个经典案例,即设计一个PI控制器控制Buck电路的输出电压。

1. STM32 ADC的使用情景

当你所开发的硬件设备需要测量电压信号时,STM32单片机自带的ADC貌似是一个最低成本与最高效的选择。但实际上STM32 ADC性能很有限:

  • 测量范围只有0V-3.3V;
  • 分辨率理论上最高为12位,也就是3.3/2^12=8e-4V,但实测误差显然远比这大;
  • 采样率理论上最高为 ADC时钟频率/12,也就是6位,9+3个周期。假设你的ADC时钟频率为8MHz,那么最高采样率为0.67MHz。处理这样的数据流对于STM32的CPU来说也是一个很大的挑战。

尽管如此,STM32 ADC方案比外接专用采样芯片、或者换成DSP来实现便宜的多。而且,STM32的ADC有个显著优势就是其通道数特别多,对于STM32F429来说,它有3个ADC外设,每个都可以测量15个通道。这也正是大多数学生项目都喜欢使用STM32 ADC的原因。

然而,如果你的项目需要很高频率、或者幅值精度较高的电压采样的话,比如实现一个数字示波器、要实时测量10kHz的正弦信号中的纹波、输入音频数据等。那就可能需要采用其他专用的ADC芯片了,STM32 的ADC的表现往往令人失望。在此列举一下其他更高级的ADC方案:

  • 音频处理:Analog Devices ADAU1761,Texas Instruments PCM5242
  • 数字示波器:Texas Instruments ADS54J60,Analog Devices AD9680
  • 对于更自由的数据处理需求,使用直接接电脑PCIe接口的DAQ数据采样板也是不错的选择,例如使用NI的PCIe-6320等。

有部分场景,比如做过零检测、测PWM波占空比、测范围在0-3.3V之外的信号等,这些都可以通过额外设计电路实现,还是可以使用STM32 ADC的。

2. 在Cube MX中配置ADC+DMA

本文采用的是最经典常用的方案,即cube mx+hal库+ADC+DMA+滑动窗口滤波。 DMA是一种快速将ADC转换结果读入内存的外设,在需要高速采样的时候格外重要。滑动窗口滤波是一种常见滤波手段,用于减少ADC高速采样时的噪声信号。

2.1 打开ADC选项

在左侧Analog中选择ADC1,右侧勾选你想要的通道。最右边就可以显示对应的引脚。

以下是基本参数设置,对照着设置即可,注意Number of conversion指通道数,选择之后要配置各通道的优先级。

2.2. DMA 设置

按照如下配置即可:

2.3. ADC 采样率计算与设置

笔者有个师兄在基于STM32 ADC实现控制器的时候,直接把ADC采样语句和后续控制都写在了主函数大while语句里面,导致最终无法确定ADC的具体采样速率,审稿人问控制频率的时候束手无策。因此需要引以为鉴。

计算并设置ADC采样频率是很关键的,因为其直接影响我们后续做处理与控制的节奏。在此给出基本公式:

f_s = \frac{f_\mathrm{PCLK2}/\mathrm{ClockPrescaler}}{(\mathrm{Resolution+SamplingTime})(\mathrm{ChannelNum}))}

下面给出该式中每一项对应的值的设置。

2.3.1 ADC时钟频率,PCLK2与ClockPrescaler

STM32 ADC挂载在APB2外设总线上,其采样率与APB2总线时钟频率f_PCLK2有关,看中文参考手册P250:

f_PCLK2在STM32 cubemx中有以下时钟设置:

这里看出APB2总线时钟频率f_PCLK2是16MHz。ADC的时钟是在其基础上分出来的,预分频器ClockPrescaler对应于以下选项:

2.3.2 Resolusion与SamplingTime

不同的分辨率对应不同的采样周期,对应于以下选项:

SamplingTime与通道数对应于以下选项:

如果按照以上的选项,那么最终ADC的采样率就是

f_s=\frac{16e6/8}{(15+480)\times2}\approx2020\mathrm{Hz}

实践中,应当依照自己想要的采样率配置这些参数。如果后续的操作比较消耗时间,那么采样率不宜设置太大。

3. 在代码中使用ADC

3.1 设置结果数组并开启ADC

在cubemx中生成代码之后。首先,在main.c中的私有变量部分添加一个32位usigned int数组作为全局变量,用于储存测量值:

/* USER CODE BEGIN PD */
#define ADC_CHANNEL_NUM 2
/* USER CODE END PD */

/* USER CODE BEGIN PV */
uint32_t measures[ADC_CHANNEL_NUM];
MovingAverageFilter filter;  //滤波器
...

注意,这里C语言定义数组用的是栈内存,STM32下不建议使用malloc动态申请堆内存。

接着,在主函数中的用户代码2添加启动语句:

/* USER CODE BEGIN 2 */
InitMovingAverageFilter(&filter); // 初始化滤波器
HAL_ADC_Start_DMA(&hadc1, measures, ADC_CHANNEL_NUM);

STM32外设的启动语句往往很长,加上IDE代码提示功能很辣鸡,十分容易忘记。但其实这一方法在在stm32f4xx_hal_adc.c中有所实现。除此以外还有另外几个函数:

// Enables ADC and starts conversion of the regular channels.
HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef* hadc);

// Enables the interrupt and starts ADC conversion of regular channels.
HAL_StatusTypeDef HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc);

// Enables ADC DMA request after last transfer (Single-ADC mode) and enables ADC peripheral  
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length);

// Gets the converted value from data register of regular channel.
uint32_t HAL_ADC_GetValue(ADC_HandleTypeDef* hadc)

3.2 编写回调函数

STM32 ADC回调函数的签名在stm32f4xx_hal_adc.c中都有所定义,使用的是一种弱实现的方式,就像java里的抽象函数一样。

在主函数中实现这个回调函数,每次ADC对所有通道采样一遍的时候都会执行这个函数:

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) 
{
    HAL_ADC_Start_DMA(&hadc1, measures, ADC_CHANNEL_NUM);
	UpdateMovingAverageFilter(&filter, measures[0]);  //滑动窗口滤波,只过滤1通道
	voltage_mea = (float) ComputeMovingAverage(&filter) * 3.3f / 4095.0f;  // 12bits 4095 real voltage
	
}

在回调函数开头必须要把之前的启动语句重新写一遍,否则这个回调函数可能只会执行一遍。

3.3 编写滑动窗口滤波

滑动窗口滤波是一种常见滤波手段,用于减少ADC高速采样时的噪声信号。首先定义一个结构体:

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#define WINDOW_SIZE 100
typedef struct {
  uint32_t buffer[WINDOW_SIZE];
  uint32_t index;
} MovingAverageFilter;

再定义滑动窗口滤波相关方法:

uint32_t ComputeMovingAverage(MovingAverageFilter *filter)  
{
  uint32_t sum = 0;
  for (int i = 0; i < WINDOW_SIZE; ++i) 
  {
    sum += filter->buffer[i];
  }
  return sum / WINDOW_SIZE;
}

void InitMovingAverageFilter(MovingAverageFilter *filter) 
{
  filter->index = 0;
  for (int i = 0; i < WINDOW_SIZE; ++i) 
  {
    filter->buffer[i] = 0;
  }
}

void UpdateMovingAverageFilter(MovingAverageFilter *filter, uint32_t newValue)
{
  filter->buffer[filter->index] = newValue;
  filter->index = (filter->index + 1) % WINDOW_SIZE;
  if (filter->index % WINDOW_SIZE == 0) filter->index = 0;
}

其实这可以封装成一个滤波器类,但遗憾的是C语言中没有类和对象,所以只能定义一个结构体和几个独立的方法。

3.4 异步或同步地处理结果

如果要同步地处理结果,只需要在ADC的回调函数中执行后续任务。

但若后续操作需要的时间比采样周期长,则需要异步地处理结果。这涉及配置一个定时器,周期性地执行后续任务。有关定时器的内容我会在以后更新。

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值