模数转换器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采样频率是很关键的,因为其直接影响我们后续做处理与控制的节奏。在此给出基本公式:
下面给出该式中每一项对应的值的设置。
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的采样率就是
实践中,应当依照自己想要的采样率配置这些参数。如果后续的操作比较消耗时间,那么采样率不宜设置太大。
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的回调函数中执行后续任务。
但若后续操作需要的时间比采样周期长,则需要异步地处理结果。这涉及配置一个定时器,周期性地执行后续任务。有关定时器的内容我会在以后更新。