前言

前四篇我们处理的都是数字信号——要么是 0(低电平),要么是 1(高电平)。但现实世界是模拟的:温度是连续变化的、音量是连续变化的、电池电压也是连续变化的。

ADC(模数转换器) 就是连接数字世界和模拟世界的桥梁。这篇我们让 STM32 读取一个电位器的电压,并通过串口打印出来。

这是后面示波器项目的核心前置知识。


一、F407 的 ADC 规格

参数
分辨率 12 位(0~4095)
通道数 3 个 ADC,共 16+ 通道
转换速度 最快 2.4MHz(0.42μs)
模式 单次、连续、扫描、间断
电压范围 0 ~ 3.3V(VREF+ 接 VDDA)

12 位分辨率的含义:

3.3V ÷ 4096 ≈ 0.8mV/步

也就是说 ADC 读到 1 对应约 0.8mV 的变化——对于大多数应用足够了。


二、硬件准备

最简单的方式:一个电位器(10kΩ 可调电阻)做分压。

接线:

电位器:
引脚 1 ──── VDD(3.3V)
引脚 2 ──── PA0(ADC1 通道 0)
引脚 3 ──── GND

没有电位器的话,也可以用两个电阻(比如 10kΩ + 10kΩ)做分压,得到一个固定的 1.65V 中点电压。


三、CubeMX 配置

3.1 配置 ADC

  1. 左侧 AnalogADC1

  2. 勾选 IN0(对应 PA0)

  3. 参数设置:

    • ModeIndependent mode

    • Clock PrescalerPCLK2 divided by 4(ADC 时钟不要超过 36MHz)

    • Resolution12 bits

    • Scan Conversion ModeDisabled

    • Continuous Conversion ModeEnabled(连续采集,不用每次手动触发)

    • EOC SelectionSingle conversion

    • 其他保持默认

3.2 配置 PA0 为 Analog

ADC 输入引脚必须配成 Analog 模式(不是浮空输入!),CubeMX 选了 IN0 后会自动设置。

3.3 配置 USART1(用于打印数据)

参考第②篇,配好 USART1(115200 波特率),加好 printf 重定向。

3.4 生成代码


四、代码:读取 ADC 并打印

4.1 最简单的单次采集

uint32_t adc_value;
​
HAL_ADC_Start(&hadc1);                          // 启动 ADC
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)  // 等待转换完成
{
    adc_value = HAL_ADC_GetValue(&hadc1);       // 读取结果(0~4095)
}
HAL_ADC_Stop(&hadc1);                           // 停止 ADC

4.2 连续采集 + 串口打印

CubeMX 已经勾选了 Continuous Conversion Mode,启动一次就连续采集:

/* USER CODE BEGIN 2 */
HAL_ADC_Start(&hadc1);   // 启动连续采集
printf("ADC Test Started\r\n");
/* USER CODE END 2 */
​
/* USER CODE BEGIN 3 */
while (1)
{
    if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)
    {
        uint32_t adc_val = HAL_ADC_GetValue(&hadc1);
        float voltage = adc_val * 3.3f / 4096.0f;  // 用 4096(2^12)而非 4095,与前文分辨率解释一致
​
        printf("ADC = %4lu  (%.2fV)\r\n", adc_val, voltage);
    }
    HAL_Delay(200);  // 每 200ms 打印一次
}
/* USER CODE END 3 */

旋转电位器,串口打印的数字应该跟着变化:

ADC = 2048  (1.65V)
ADC = 1024  (0.83V)
ADC = 3072  (2.48V)
ADC = 4095  (3.30V)

4.3 ADC + DMA(不占 CPU)

上面的 PollForConversion 是轮询方式——CPU 在等待 ADC 转换时是阻塞的。如果用 DMA 方式,ADC 转换完自动把数据搬进内存,CPU 全程不参与。

CubeMX 配置 DMA:

  1. ADC1 设置中 → DMA Settings 标签 → Add

  2. DMA Request:选择 ADC1

  3. DirectionPeripheral To Memory

  4. ModeCircular(循环模式,持续搬运)

  5. Data Width:Peripheral = Half Word,Memory = Half Word

代码:

#define ADC_BUF_SIZE 100
uint16_t adc_buf[ADC_BUF_SIZE];
​
/* USER CODE BEGIN 2 */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE);
/* USER CODE END 2 */
​
// ADC 转换完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1)
    {
        // adc_buf 里装满了最新的采样值
        // 可以在这里处理或设置标志位让主循环处理
    }
}

如果用 Circular 模式 + DMA,ADC 会不停地采集并把数据写入数组,CPU 可以安心做其他事情,完全不参与转换过程。


五、ADC 采样值可视化

光看数字很枯燥。配合第②篇的串口方案,可以把 ADC 数据变成实时波形图

5.1 用 VOFA+ 画波形

VOFA+ 是常用的串口波形工具,支持"火线协议"。

打印格式(每行一个数据 + 换行):

printf("%.2f\r\n", voltage);
HAL_Delay(10);  // 100Hz 采样率

VOFA+ 设置:

  • 串口:选择你的 COM 口,波特率 115200

  • 协议:FireWater

  • 点击左上角波形图标 → 选择数据通道 → 实时曲线

5.2 用手摸一下

把 PA0 从电位器断开,改成用杜邦线悬空:

  • 手捏住杜邦线金属端:读到人体感应噪声(50Hz 工频干扰)

  • 用手指轻触 3.3V 引脚再碰 PA0:看到电压跳变

这个小实验让你直观感受到 ADC 的速度和灵敏度,也是示波器项目的基础。


六、多通道采集

实际项目中经常需要同时采集多个模拟信号。STM32 支持扫描模式自动切换通道。

6.1 CubeMX 配置多通道

  1. ADC1IN0IN1(或更多)都勾上

  2. Scan Conversion ModeEnabled

  3. Number of Conversion:2

  4. Rank 中分别设置每个通道的 Sampling Time

6.2 代码

uint32_t adc_vals[2];  // 两个通道的结果
​
HAL_ADC_Start(&hadc1);
​
for (int i = 0; i < 2; i++)
{
    HAL_ADC_PollForConversion(&hadc1, 100);
    adc_vals[i] = HAL_ADC_GetValue(&hadc1);
}
​
HAL_ADC_Stop(&hadc1);
​
printf("CH0 = %lu, CH1 = %lu\r\n", adc_vals[0], adc_vals[1]);

扫描模式下每一次 PollForConversion + GetValue 拿到的当前通道的数据,拿完后 ADC 自动切换到下一个通道。


七、ADC 参数调节与注意事项

7.1 采样时间

ADC 转换需要时间。CubeMX 中可以调节 Sampling Time

采样周期数 总转换时间(ADC 时钟 21MHz(PCLK2/4)) 适用场景
3 cycles 约 0.71μs 低阻抗信号源(< 10kΩ)
15 cycles 约 1.29μs 一般信号
28 cycles 约 1.90μs 高阻抗信号源(10k~50kΩ)
56 cycles 约 3.24μs 高阻抗、噪声大的信号
84 cycles 约 4.57μs 推荐初学者默认值
112 cycles 约 5.90μs 慢速高精度信号
144 cycles 约 7.43μs 最慢,最稳定
480 cycles 约 23.43μs 极慢信号(温度传感器)

采样时间不够长会导致 ADC 读数偏低,因为输入电压还没"充满"采样电容。如果发现 ADC 读数一直偏小,加大采样时间试试。

7.2 噪声抑制

方法 效果
加长采样时间 降低 ADC 输入噪声
多次采样取平均 天真的做法,去掉 3 个最高/最低再平均更好
硬件加滤波电容(0.1μF 到 GND) 效果最好
远离 PWM 走线 数字噪声耦合到 ADC 输入

八、关于分辨率与精度

一个小实验说明问题:

// 采集 100 次,看看读数有多"稳"
uint32_t sum = 0;
uint32_t min = 4096, max = 0;

for (int i = 0; i < 100; i++)
{
    HAL_ADC_PollForConversion(&hadc1, 100);
    uint32_t v = HAL_ADC_GetValue(&hadc1);
    sum += v;
    if (v < min) min = v;
    if (v > max) max = v;
}

printf("Avg = %lu, Min = %lu, Max = %lu, Range = %lu\r\n",
       sum / 100, min, max, max - min);

即使输入电压不变,ADC 的读数也会在小范围内跳动(1~3 个 LSB)。这是正常的——分辨率 ≠ 精度。12 位分辨率不代表 12 位精度,实际有效位数(ENOB)一般在 10~11 位。


九、应用:做一个简易电压表

把上面的代码整合一下,配上 OLED 或 LCD 屏幕(后话),就是一个简易电压表。配上两个电阻分压,量程可以扩展到 30V+。

下一篇我们回到数字通信,学习 SPI 协议——这是驱动墨水屏的关键技能。


练习:

  1. 把采样时间从 3 cycles 改到 480 cycles,对比读数的稳定性变化

  2. 用一个光敏电阻(几毛钱)替换电位器,做一个光控灯——ADC 读数低于阈值时打开 LED

  3. 如果有信号发生器,给 PA0 输入一个 1kHz 正弦波,用 VOFA+ 看实时波形

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐