模数转换ADC(STM32F103ZE)


概述

image-20241207132628019

image-20241207132958431

STM32中的ADC

STM32F103系列提供了3个ADC,精度为12位,每个ADC最多有16个通道和2个内部信号源。

STM32F103的ADC是一种逐次逼近型模拟数字转换器。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阈值。ADC的输入时钟不得超过14MHz,它是由PCLK2经分频产生。

image-20241207133434436

ADC类型

image-20241207141746294

逐次逼近型工作原理

类比天平称重简化过程

类比称量未知重量的物体的最优方法:先上大砝码,后上小砝码;如果某次增加砝码后发现砝码端较重,则将该砝码替换为较小一点的砝码;如果某次增加砝码后发现砝码端较轻,则继续增加一个较小一点的砝码。直至天平两端平衡。

image-20241207144028375

image-20241207144527257

image-20241207144601805

image-20241207144649256

image-20241207144659421

电路原理

image-20241207142920535

image-20241207143812523

image-20241207143952014

image-20241207144920604

转换过程分析

image-20241207145340136

image-20241207145417112

ADC功能分析

模拟电源 & 模拟参考电压

image-20241207150154532

image-20241207150859556

image-20241207150113218

输入通道

image-20241207151028730

通道组(若干通道的逻辑组织序列)

[!NOTE]

类比一下歌曲和歌单的关系:

  • 可以将每个通道的转换任务类比为一首歌曲的播放
  • 我们可以将若干首歌按照自己爱好的播放顺序组织为一个惯常歌单列表(常规组),并进行顺序/循环放
  • 有时突然兴起,特别想听几首歌,于是将它们插队到当前播放列表中(例如“下一首播放”功能),这样它们能够比惯常列表中的歌曲优先播放

image-20241207151700153

常规组

image-20241207152657556

插入组

image-20241207153813447

触发源

软件触发

image-20241207154500929

转换方式

[!NOTE]

类比歌单播放:

  • 扫描模式:开启则自上而下顺序播放歌单列表;关闭则仅播放列表中第一首歌
  • 连续转换:是否循环播放
    • 扫描+连续:循环播放歌单
    • 非扫描+连续:循环播放歌单第一首歌

image-20241207154823940

事件 & 中断

image-20241207155421171

image-20241207211313719

转换结束中断

数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。其中转换结束中断很好理解,跟我们平时接触的中断一样,有相应的中断标志位和中断使能位,我们还可以根据中断类型写相应配套的中断服务程序。

模拟看门狗中断

当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是我们开启了模拟看门狗中断,其中低阈值和高阈值由 ADC_LTR 和 ADC_HTR 设置。例如我们设置高阈值是2.5V,那么模拟电压超过 2.5V 的时候,就会产生模拟看门狗中断,反之低阈值也一样。

DMA请求

规则和注入通道转换结束后,除了产生中断外,还可以产生DMA请求,把转换好的数据直接存储在内存里面。要注意的是只有ADC1和ADC3可以产生DMA请求

image-20241207155811981

image-20241207155845368

数据对齐

image-20241207160310466

转换时间计算

image-20241207160652894

案例-采集可变电阻电压

硬件电路

image-20241207170951408

image-20241207170929018

寄存器分析

RCC时钟

[!NOTE]

虽然挂载在APB2上,但是ADC的最大输入频率为14MHz

image-20241207171058355

image-20241207171350684

image-20241207171330944

ADC输入时钟预分频

image-20241207172036548

image-20241207172049933

由于APB2为72MHz,要满足ADC输入时钟最大14MHz的要求,我们可以设置预分频为6。

GPIO工作模式

image-20241207172607090

GPIO工作模式需要配置模拟信号输入,由于不需要经过TTL采样到IDR中,因此GPIO的时钟可以不用开启。

[!TIP]

但为了统一编程,最好还是养成用到了GPIO则开启对应时钟的习惯,除非有特殊的考量。

编排常规组通道序列

现在外部模拟信号能够通过PC0到达ADC1通道10了,我们需要将通道组织到常规组中,这样ADC才能知道按照什么流程来执行这些通道的转换任务。

序列数量

image-20241207173824003

这里我们只使用通道10来采集可变电阻电压,因此序列只需包含通道10即可,数量为1.

序列顺序

image-20241207174007845

通过SQ1可以指定常规组中第一个要转换的通道编号(从0到17),SQ2则用来指定第二个要转换的通道,以此类推。

ADC控制相关

image-20241207181439887

image-20241207181613976

// 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电气特性

image-20241207212034901

image-20241207181039478

采样时间设置

image-20241207182324177

ADC唤醒 & 校准

ADC唤醒 & 关闭 & 触发AD转换

image-20241207190825998

image-20241207190616679

[!NOTE]

需要注意的是,通过ADON开启转换针对的是常规组。

ADC校准

ADC校准能够显著减少由于内部电容组变化导致的精度错误。

image-20241207191313952

[!NOTE]

  • 建议每次唤醒ADC后,都执行一次校准
  • 必须在ADC上电至少两个ADC时钟周期后,才开始校准

软件触发AD转换

image-20241207193334202

状态寄存器

image-20241207194215560

时序控制

image-20241207193043135

image-20241207192938318

示例代码

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配置

image-20241207202209344

[!TIP]

我们可以学习Cube图形化界面中对各个寄存器的编排形式来模块化理解寄存器。

image-20241207202257369

image-20241207202232215

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来传输数据,否则数据如果来不及取出,则会导致数据被覆盖。

image-20241207214851385

示例代码

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库实现

常规组通道编排

image-20241208083218931

开启DMA

image-20241208083357152

禁用DMA中断

image-20241208083449124

ADC时钟预分频

image-20241208083304844

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);
}

THE END


文章作者: 安文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 安文 !
  目录