STM32F407 从零上路 ⑤:ADC 采集——让 STM32 学会“感觉“
前言
前四篇我们处理的都是数字信号——要么是 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
-
左侧 Analog → ADC1
-
勾选 IN0(对应 PA0)
-
参数设置:
-
Mode:Independent mode
-
Clock Prescaler:PCLK2 divided by 4(ADC 时钟不要超过 36MHz)
-
Resolution:12 bits
-
Scan Conversion Mode:Disabled
-
Continuous Conversion Mode:Enabled(连续采集,不用每次手动触发)
-
EOC Selection:Single 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:
-
ADC1 设置中 → DMA Settings 标签 → Add
-
DMA Request:选择 ADC1
-
Direction:Peripheral To Memory
-
Mode:Circular(循环模式,持续搬运)
-
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 配置多通道
-
ADC1 → IN0 和 IN1(或更多)都勾上
-
Scan Conversion Mode:Enabled
-
Number of Conversion:2
-
在 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 协议——这是驱动墨水屏的关键技能。
练习:
-
把采样时间从 3 cycles 改到 480 cycles,对比读数的稳定性变化
-
用一个光敏电阻(几毛钱)替换电位器,做一个光控灯——ADC 读数低于阈值时打开 LED
-
如果有信号发生器,给 PA0 输入一个 1kHz 正弦波,用 VOFA+ 看实时波形
更多推荐


所有评论(0)