概述
STM32中的ADC
STM32F103系列提供了3个ADC,精度为12位,每个ADC最多有16个通道和2个内部信号源。
STM32F103的ADC是一种逐次逼近型模拟数字转换器。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阈值。ADC的输入时钟不得超过14MHz,它是由PCLK2经分频产生。
ADC类型
逐次逼近型工作原理
类比天平称重简化过程
类比称量未知重量的物体的最优方法:先上大砝码,后上小砝码;如果某次增加砝码后发现砝码端较重,则将该砝码替换为较小一点的砝码;如果某次增加砝码后发现砝码端较轻,则继续增加一个较小一点的砝码。直至天平两端平衡。
电路原理
转换过程分析
ADC功能分析
模拟电源 & 模拟参考电压
输入通道
通道组(若干通道的逻辑组织序列)
[!NOTE]
类比一下歌曲和歌单的关系:
- 可以将每个通道的转换任务类比为一首歌曲的播放
- 我们可以将若干首歌按照自己爱好的播放顺序组织为一个惯常歌单列表(常规组),并进行顺序/循环放
- 有时突然兴起,特别想听几首歌,于是将它们插队到当前播放列表中(例如“下一首播放”功能),这样它们能够比惯常列表中的歌曲优先播放
常规组
插入组
触发源
软件触发
转换方式
[!NOTE]
类比歌单播放:
- 扫描模式:开启则自上而下顺序播放歌单列表;关闭则仅播放列表中第一首歌
- 连续转换:是否循环播放
- 扫描+连续:循环播放歌单
- 非扫描+连续:循环播放歌单第一首歌
事件 & 中断
转换结束中断
数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。其中转换结束中断很好理解,跟我们平时接触的中断一样,有相应的中断标志位和中断使能位,我们还可以根据中断类型写相应配套的中断服务程序。
模拟看门狗中断
当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是我们开启了模拟看门狗中断,其中低阈值和高阈值由 ADC_LTR 和 ADC_HTR 设置。例如我们设置高阈值是2.5V,那么模拟电压超过 2.5V 的时候,就会产生模拟看门狗中断,反之低阈值也一样。
DMA请求
规则和注入通道转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据直接存储在内存里面。要注意的是只有ADC1和ADC3可以产生DMA请求。
数据对齐
转换时间计算
案例-采集可变电阻电压
硬件电路
寄存器分析
RCC时钟
[!NOTE]
虽然挂载在APB2上,但是ADC的最大输入频率为14MHz
ADC输入时钟预分频
由于APB2为72MHz,要满足ADC输入时钟最大14MHz的要求,我们可以设置预分频为6。
GPIO工作模式
GPIO工作模式需要配置模拟信号输入,由于不需要经过TTL采样到IDR中,因此GPIO的时钟可以不用开启。
[!TIP]
但为了统一编程,最好还是养成用到了GPIO则开启对应时钟的习惯,除非有特殊的考量。
编排常规组通道序列
现在外部模拟信号能够通过PC0到达ADC1通道10了,我们需要将通道组织到常规组中,这样ADC才能知道按照什么流程来执行这些通道的转换任务。
序列数量
这里我们只使用通道10来采集可变电阻电压,因此序列只需包含通道10即可,数量为1.
序列顺序
通过SQ1可以指定常规组中第一个要转换的通道编号(从0到17),SQ2则用来指定第二个要转换的通道,以此类推。
ADC控制相关
// ADC控制相关
// 关闭扫描模式,常规组只有一个通道
ADC1->CR1 &= ~ADC_CR1_SCAN;
// 开启连续模式,循环采样可调电阻电压
ADC1->CR2 |= ADC_CR2_CONT;
// 数据对齐:右对齐
ADC1->CR2 &= ~ADC_CR2_ALIGN;
// 使能外部触发ADC采样
ADC1->CR2 |= ADC_CR2_EXTTRIG;
// 外部触发源选择软件触发 => 111
ADC1->CR2 |= ADC_CR2_EXTSEL;
采样时间
采样时间的作用
- ADC 内部有一个采样保持电路,由采样电容和开关组成。在采样过程中,采样电容需要通过输入信号的驱动完成充电,采样时间就是给采样电容充电到目标电压所需要的时间。
- 如果采样时间太短,采样电容未充满,电压达不到稳定值,会导致 ADC 输出的数字值不准确。
采样时间由什么决定
- 输入阻抗: 输入信号源的阻抗越高,采样电容充电越慢,需要更长的采样时间。
- ADC 采样电容: STM32 的采样电容通常在几十 pF 到几百 pF 范围。
- 信号频率: 高频信号需要更快的采样时间,但仍需足够时间充电。
ADC电气特性
采样时间设置
ADC唤醒 & 校准
ADC唤醒 & 关闭 & 触发AD转换
[!NOTE]
需要注意的是,通过ADON开启转换针对的是常规组。
ADC校准
ADC校准能够显著减少由于内部电容组变化导致的精度错误。
[!NOTE]
- 建议每次唤醒ADC后,都执行一次校准
- 必须在ADC上电至少两个ADC时钟周期后,才开始校准
软件触发AD转换
状态寄存器
时序控制
示例代码
adc.h
#ifndef __ADC_H__
#define __ADC_H__
#include "stm32f10x.h"
void ADC1_Init(void);
void ADC1_StartConvert(void);
double ADC1_ReadVoltage(void);
#endif /* __ADC_H__ */
adc.c
#include "adc.h"
#include "delay.h"
#include "logger.h"
void ADC1_Init(void) {
// ADC输入时钟配置
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// APB2 6分频 => 10
RCC->CFGR &= ~RCC_CFGR_ADCPRE;
RCC->CFGR |= RCC_CFGR_ADCPRE_1;
RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
// GPIO工作模式配置为模拟输入 MODE=00 CNF=00
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
// 编排常规组通道序列
// 序列数量为1 => 0000
ADC1->SQR1 &= ~ADC_SQR1_L;
// 将通道10配置为转换序列第一个 => 01010
ADC1->SQR3 &= ~ADC_SQR3_SQ1;
ADC1->SQR3 |= ADC_SQR3_SQ1_3;
ADC1->SQR3 |= ADC_SQR3_SQ1_1;
// 配置通道10的采样时间 13.5 cycles => 010
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10_2;
ADC1->SMPR1 |= ADC_SMPR1_SMP10_1;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10_0;
// ADC控制相关
// 关闭扫描模式,常规组只有一个通道
ADC1->CR1 &= ~ADC_CR1_SCAN;
// 开启连续模式,循环采样可调电阻电压
ADC1->CR2 |= ADC_CR2_CONT;
// 数据对齐:右对齐
ADC1->CR2 &= ~ADC_CR2_ALIGN;
// 使能外部触发ADC采样
ADC1->CR2 |= ADC_CR2_EXTTRIG;
// 外部触发源选择软件触发 => 111
ADC1->CR2 |= ADC_CR2_EXTSEL;
}
void ADC1_StartConvert(void) {
// 唤醒ADC
ADC1->CR2 |= ADC_CR2_ADON;
// 等待至少两个ADC时钟周期后开始执行校准
delay_us(2);
ADC1->CR2 |= ADC_CR2_CAL;
// 等待校准结束
while (ADC1->CR2 & ADC_CR2_CAL) {
}
// 软件触发开始转换,由于配置了连续模式,因此触发一次后常规组的转换会循环不断
// 也可以通过ADON触发常规组转换
ADC1->CR2 |= ADC_CR2_SWSTART;
// 等待第一次转换完成
while ((ADC1->SR & ADC_SR_EOC) == 0) {
}
}
double ADC1_ReadVoltage(void) {
uint16_t adc_val = ADC1->DR;
double voltage = adc_val * 3.3 / 4095;
LOG_DEBUG("adc_val = %d, voltage = %.2fV", adc_val, voltage);
return voltage;
}
main.c
#include "stm32f10x.h"
#include "uart.h"
#include "logger.h"
#include "adc.h"
#include "delay.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {
uart_send_bytes(buf, size);
}
int main() {
uart_init();
ADC1_Init();
LOG_DEBUG("main start")
ADC1_StartConvert();
while (1) {
ADC1_ReadVoltage();
delay_s(1);
}
}
HAL实现
Cube配置
[!TIP]
我们可以学习Cube图形化界面中对各个寄存器的编排形式来模块化理解寄存器。
main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// perform calibration before start adc
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
double v = HAL_ADC_GetValue(&hadc1) * 3.3 / 4095;
printf("v = %.2fV\n", v);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
案例-多通道+DMA
需求
PC0是10通道,采集的是可变电阻器的电压。PC2对应的是12通道,使用杜邦线连接到电源或地,测试他们的电压。
当多个通道同时采集时,一般就需要使用DMA来传输数据,否则数据如果来不及取出,则会导致数据被覆盖。
示例代码
adc.h
#ifndef __ADC_H__
#define __ADC_H__
#include "stm32f10x.h"
void ADC1_Init(void);
void ADC1_StartConvert_DMA(uint32_t bufAddr, uint16_t dataSize);
#endif /* __ADC_H__ */
adc.c
#include "adc.h"
#include "delay.h"
#include "logger.h"
void ADC1_Init(void) {
// ADC输入时钟配置
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
// APB2 6分频 => 10
RCC->CFGR &= ~RCC_CFGR_ADCPRE;
RCC->CFGR |= RCC_CFGR_ADCPRE_1;
RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
// GPIO工作模式配置为模拟输入 MODE=00 CNF=00
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
GPIOC->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2);
// 编排常规组通道序列
// 序列数量为2 => 0001
ADC1->SQR1 &= ~ADC_SQR1_L;
ADC1->SQR1 |= ADC_SQR1_L_0;
// 将通道10配置为转换序列第一个 => 01010
ADC1->SQR3 &= ~ADC_SQR3_SQ1;
ADC1->SQR3 |= ADC_SQR3_SQ1_3;
ADC1->SQR3 |= ADC_SQR3_SQ1_1;
// 将通道12配置为转换序列第二个 => 01100
ADC1->SQR3 &= ~ADC_SQR3_SQ2;
ADC1->SQR3 |= ADC_SQR3_SQ2_3;
ADC1->SQR3 |= ADC_SQR3_SQ2_2;
// 配置通道10的采样时间 7.5 cycles => 001
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10_2;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP10_1;
ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
// 配置通道12的采样时间 7.5 cycles => 001
ADC1->SMPR1 &= ~ADC_SMPR1_SMP12;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP12_2;
ADC1->SMPR1 &= ~ADC_SMPR1_SMP12_1;
ADC1->SMPR1 |= ADC_SMPR1_SMP12_0;
// ADC控制相关
// 开启扫描模式,常规组有两个通道
ADC1->CR1 |= ADC_CR1_SCAN;
// 开启连续模式
ADC1->CR2 |= ADC_CR2_CONT;
// 数据对齐:右对齐
ADC1->CR2 &= ~ADC_CR2_ALIGN;
// 使能外部触发ADC采样
ADC1->CR2 |= ADC_CR2_EXTTRIG;
// 外部触发源选择软件触发 => 111
ADC1->CR2 |= ADC_CR2_EXTSEL;
// 使能DMA模式
ADC1->CR2 |= ADC_CR2_DMA;
}
static void Init_DMA_Channel(uint32_t bufAddr, uint16_t dataSize) {
// 使能DMA时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// ADC1使用DMA1通道1
// 从外设读到内存
DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;
// 源地址和目标地址
DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);
DMA1_Channel1->CMAR = bufAddr;
// 数据宽度和数量,注意DMA的DR是16bit
DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;
DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0;
DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0;
DMA1_Channel1->CNDTR = dataSize;
// ADC.DR地址不变,内存地址自增
DMA1_Channel1->CCR &= ~DMA_CCR1_PINC;
DMA1_Channel1->CCR |= DMA_CCR1_MINC;
// 开启循环模式,和ADC连续模式配合
DMA1_Channel1->CCR |= DMA_CCR1_CIRC;
// 开启DMA通道
DMA1_Channel1->CCR |= DMA_CCR1_EN;
}
void ADC1_StartConvert_DMA(uint32_t bufAddr, uint16_t dataSize) {
// 唤醒ADC
ADC1->CR2 |= ADC_CR2_ADON;
// 等待至少两个ADC时钟周期后开始执行校准
delay_us(2);
ADC1->CR2 |= ADC_CR2_CAL;
// 等待校准结束
while (ADC1->CR2 & ADC_CR2_CAL) {
}
// 开始转换前建立DMA通道
Init_DMA_Channel(bufAddr, dataSize);
// 软件触发开始转换
ADC1->CR2 |= ADC_CR2_SWSTART;
// 等待常规组首次转换完成
while ((ADC1->SR & ADC_SR_EOC) == 0) {
}
}
main.c
#include "adc.h"
#include "delay.h"
#include "logger.h"
#include "stm32f10x.h"
#include "uart.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {
uart_send_bytes(buf, size);
}
int main() {
uart_init();
ADC1_Init();
LOG_DEBUG("main start")
uint16_t buf[2] = {0};
ADC1_StartConvert_DMA((uint32_t)buf, 2);
while (1) {
LOG_DEBUG("v1 = %.2fV, v2 = %.2fV", buf[0] * 3.3 / 4095,
buf[1] * 3.3 / 4095)
delay_ms(1000);
}
}
DMA通道建立时机问题
[!TIP]
应该在ADC上电后,再建立DMA通道;在ADC掉电状态建立DMA通道则可能失败。
在建立DMA通道时,应该确保双方是活跃状态。
HAL库实现
常规组通道编排
开启DMA
禁用DMA中断
ADC时钟预分频
main.c
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
uint16_t buf[2] = {0};
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)buf, 2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(1000);
printf("V1 = %.2fV, V2 = %.2fV\n", buf[0] * 3.3 / 4095, buf[1] * 3.3 / 4095);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
源码分析
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length)
{
...
/* Enable the ADC peripheral */
tmp_hal_status = ADC_Enable(hadc);
...
/* Start the DMA channel */
HAL_DMA_Start_IT(hadc->DMA_Handle, (uint32_t)&hadc->Instance->DR, (uint32_t)pData, Length);
}