摘要:在嵌入式开发中,传感器是感知世界的“五官”,而 ADC(模数转换器) 则是将这些模拟信号(电压、温度、光照)转化为单片机能听懂的数字信号的“翻译官”。
本文将以 STM32F103C8T6 为核心,拒绝枯燥的寄存器手册,通俗讲解 SAR ADC 的工作原理,手把手教你使用 STM32CubeMX 配置单通道与多通道扫描,并对比 查询模式(Polling) 与 DMA模式 的实战代码区别。附带电压换算公式与常见避坑指南。


一、 ADC 的“前世今生”:核心原理通俗解

1.1 为什么要用 ADC?

我们的单片机是数字芯片,它只认识 0 (0V) 和 1 (3.3V)。但现实世界是模拟的,比如电池电压是 2.5V,光敏电阻的电压是 1.2V。
ADC(Analog-to-Digital Converter)的作用就是把这些连续变化的电压值,量化成离散的数字值

1.2 STM32F103 的 ADC 特性

STM32F103C8T6 内部集成了 2 个 12位 ADC(ADC1 和 ADC2),共有 10 个外部通道

  • 12位精度:意思是它把 0V ~ 3.3V 的电压切成了 2^12=4096。
    • 最小分辨率 = 3.3V/4096≈0.8mV。
  • 转换速度:最快 1us 一次(1MHz)。
  • 输入范围:0≤VIN≤VREF+(通常是 3.3V)。

1.3 核心原理:SAR(逐次逼近型)

STM32 的 ADC 属于 SAR(Successive Approximation Register) 类型。
通俗理解
想象你在玩“猜数字”游戏,数字范围是 0-4095,你必须用最少的次数猜对。

  1. ADC 先猜中间值 2048(2.5V的一半)。
  2. 如果实际电压 > 猜的电压,ADC 就保留 1,猜下一个高位。
  3. 如果实际电压 < 猜的电压,ADC 就填 0,猜下一个低位。
  4. 经过 12 次比较(因为是 12 位),最终确定数字。

这就是为什么 ADC 转换需要时间(采样时间 + 转换时间)。


二、 关键参数与“避坑指南”(非常重要)

在配置 CubeMX 之前,必须理解这几个概念,否则代码跑不起来:

2.1 ADC 时钟(ADCCLK)

【坑点 1】:STM32F103 的 ADC 时钟最大不能超过 14MHz

  • 系统主频通常是 72MHz。
  • ADC 挂载在 APB2 总线上。
  • 如果不分频,72MHz 直接给 ADC,芯片会罢工或数据乱跳
  • 正确做法:必须设置分频系数为 6分频 (12MHz) 或 8分频 (9MHz)

2.2 采样时间 (Sampling Time)

这是 ADC 对内部电容充电的时间。

  • 信号源内阻大(如光敏电阻):需要长采样时间(如 239.5 Cycles),否则电压充不满,读数偏小。
  • 信号源内阻小(如运放输出):可以用短采样时间(如 1.5 Cycles),速度更快。

2.3 左对齐 vs 右对齐

  • 右对齐(Right Alignment)(推荐) 12位数据存在 16位寄存器的低12位,直接读就是真实值。
  • 左对齐:通常用于只需要 8 位精度的场景(直接读高8位)。

三、 实战一:单通道轮询采集(测量电位器电压)

我们先从最简单的开始:使用 CPU 傻等(Polling)方式读取 PA1 引脚的电压。

3.1 CubeMX 配置步骤

  1. 引脚设置
    • 点击 PA1 -> 选择 ADC1_IN1

  1. ADC配置 (Analog -> ADC1):
    • Mode:勾选 IN1
    • Configuration -> Parameter Settings
      • Data Alignment: Right (右对齐)。
      • Scan Conversion Mode: Disable (只测一个,不用扫描)。
      • Continuous Conversion ModeEnable (开启后,它会不停地采集,不用每次都按开始键)。
      • Discontinuous Conversion Mode: Disable。

  1. 时钟配置 (Clock Configuration)
    • 重点:找到 ADC Prescaler,设置为 /6 (12MHz) 或 /8 (9MHz)。千万别选 /2 或 /4!

时钟源选择外部晶振

  1. 生成代码。

3.2 代码编写

在 main.c 中添加变量和逻辑:

/* USER CODE BEGIN PV */
uint16_t ADC_Value = 0;   // 存放原始值 (0-4095)
float Voltage = 0.0;      // 存放换算后的电压
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
// 1. 开启 ADC 自校准 (建议加上,提高精度)
HAL_ADCEx_Calibration_Start(&hadc1);
// 2. 启动 ADC 转换
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */

/* USER CODE BEGIN 3 */
while (1)
{
    // 3. 等待转换完成 (轮询方式,第二个参数是超时时间)
    if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
    {
        // 4. 获取原始值
        ADC_Value = HAL_ADC_GetValue(&hadc1);
        
        // 5. 换算电压: V = (ADC / 4095) * 3.3
        Voltage = (float)ADC_Value / 4095.0 * 3.3;
        
        // 打印调试 (需要重定向printf)
        // printf("ADC: %d, Vol: %.2fV\r\n", ADC_Value, Voltage);
    }
    HAL_Delay(100);
}
/* USER CODE END 3 */


四、 实战二:多通道 DMA 采集(进阶必备)

痛点:如果你要同时测 4 个传感器,用上面的轮询法,CPU 就要一直等 ADC 忙完,效率极低。
解法DMA (Direct Memory Access)。让 DMA 搬运工在后台把 ADC 数据直接搬到内存数组里,CPU 只要去数组里取现成的数就行了!

4.1 CubeMX 配置步骤

假设我们要测两个通道:PA1 (电位器) 和 内部温度传感器

  1. ADC设置
    • Mode:勾选 IN1 和 Temperature Sensor Channel

    • Parameter Settings
      • 找到 ADC_Regular_ConversionMode(规则组转换模式)这一栏。
      • 找到 Number of Conversion(转换数量)。它现在的默认值肯定是 1
      • 手动把 1 改成 2(或者你勾选的通道总数)
      • Scan Conversion ModeEnabled (自动开启,因为选了多通道)。
      • Continuous Conversion ModeEnabled (不停地轮询扫描)。
      • DMA Continuous RequestsEnabled (关键!让 DMA 也不停地搬运)。

      • Rank (采样顺序)
        • Rank 1: Channel 1 (PA1)。
        • Rank 2: Temperature Sensor。
        • 注意:温度传感器采样时间建议设最大 (239.5 Cycles)
  1. DMA设置 (System Core -> DMA):
    • 点击 Add -> Select ADC1
    • ModeCircular (循环模式,这很重要,否则搬运一次就停了)。
    • Data WidthHalf Word (16bit),因为 ADC 数据是 12位的,需要用 16位存。
    • Memory Increment: 勾选 (每次搬运后,内存地址+1,存到数组下一个位置)。

4.2 代码编写

无需在 while(1) 里写任何 ADC 代码,全自动!

/* USER CODE BEGIN PV */
// 定义一个数组存放数据,长度=通道数
uint16_t ADC_DMA_Buffer[2]; 
// Buffer[0] 存放 PA1 电位器
// Buffer[1] 存放 内部温度
/* USER CODE END PV */

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);

// 启动 ADC 的 DMA 模式
// 这里的参数是:句柄、接收数组地址、数据长度
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_DMA_Buffer, 2);
/* USER CODE END 2 */

/* USER CODE BEGIN 3 */
while (1)
{
    // 直接读数组就是最新数据!CPU 零负担!
    float vol_pa1 = (float)ADC_DMA_Buffer[0] / 4095.0 * 3.3;
    
    // STM32F103 内部温度计算公式 (V25=1.43V, Avg_Slope=4.3mV/C)
    // Temp = (V25 - V_sense) / Avg_Slope + 25
    float vol_temp = (float)ADC_DMA_Buffer[1] / 4095.0 * 3.3;
    float temperature = (1.43 - vol_temp) / 0.0043 + 25.0;
    
    // printf("Vol: %.2f V, Temp: %.1f C\r\n", vol_pa1, temperature);
    
    HAL_Delay(500);
}
/* USER CODE END 3 */

五、 常见问题 FAQ

Q1: 读出来的电压为什么会波动?

  • 电源纹波:USB 供电不稳,建议并联 100nF 电容。
  • 采样时间太短:信号源阻抗匹配没做好,增加 Sampling Time。
  • 软件滤波:在软件里做“均值滤波”,采集 10 次取平均值。

Q2: 多通道采集数据错位了?

  • 检查 DMA 是否开启了 Circular 模式。
  • 检查代码中 HAL_ADC_Start_DMA 的第三个参数长度是否和通道数一致。

Q3: 为什么读到的值一直是 4095?

  • 引脚可能悬空了。
  • 输入电压超过了 3.3V(这可能烧坏 IO 口!)。

六、 总结

通过本文,我们掌握了:

  1. SAR ADC 原理:逐次逼近,12位精度。
  2. CubeMX 核心配置时钟分频 (<14MHz) 是关键。
  3. 两种模式
    • 轮询模式:适合低频、单次采集。
    • DMA 模式:适合多通道、高速、连续采集(推荐)。

掌握了 ADC,你就可以连接光敏、热敏、摇杆、火焰传感器等各种模拟器件,真正打通单片机与物理世界的桥梁!

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐