vet的中文怎么写STM32F407VET6 的 ADC 教程:多通道扫描

新闻资讯2026-04-21 00:51:02

你有没有遇到过这样的场景?

项目需要同时读取温度、湿度、光照和电流信号,结果写了一堆轮询代码,CPU 被 ADC 占满,主程序卡顿,响应延迟严重……最后只能靠加延时、关中断来“凑合”解决问题?

其实,STM32F407VET6 早就为你准备了更优雅的解决方案——

ADC 多通道扫描模式 + DMA

。它能让硬件自动完成多个模拟通道的采集,数据直接送进内存,CPU 几乎零参与。

今天我们就来彻底搞懂这套机制,不讲空话,不套模板,只聊工程师真正关心的事:怎么用、为什么这么用、踩过哪些坑、如何避雷⚡️


在嵌入式系统中,传感器越来越多,但 MCU 的资源却不会无限增长。比如一个智能农业监测节点,可能要接:

  • 土壤温湿度(模拟输出)
  • 光照强度
  • 空气温湿度
  • CO₂ 浓度
  • 风速风向(部分型号为模拟量)

如果每个通道都单独启动 ADC、等待转换完成、读取结果……那你的

while(1)

很快就会变成“伪实时”系统——看着在跑,实则处处卡顿。

而使用

扫描模式(Scan Mode)+ DMA

,你可以做到:

✅ 所有通道按顺序自动采集

✅ 数据自动存入数组,无需中断服务频繁处理

✅ 主程序继续执行其他任务,完全不受干扰

✅ 实现接近“并行”的多路采样体验(虽然物理上仍是串行)

这不仅是效率问题,更是系统架构设计的分水岭。用得好,系统流畅稳定;用不好,轻则数据失真,重则死机重启。


先别急着写代码,咱们先把芯片的能力摸清楚。STM32F407VET6 可不是普通单片机,它的 ADC 模块相当硬核:



  • 3 个独立 ADC

    (ADC1/2/3),可单独或协同工作
  • ✅ 每个 ADC 支持

    16 个外部通道 + 2 个内部通道

    • 外部通道对应 GPIO 引脚(如 PA0 → ADC123_IN0)
    • 内部通道包括:温度传感器、内部参考电压(Vrefint)


  • 12 位分辨率

    ,数字输出范围 0~4095
  • ✅ 可编程采样时间:

    3 ~ 480 个 ADC 时钟周期
  • ✅ 支持多种触发方式:软件启动、定时器触发、外部信号等
  • ✅ 支持

    DMA 请求

    ,每次转换后自动搬运数据
  • ✅ 提供

    扫描模式



    间断模式

    ,灵活应对不同需求

这些特性组合起来,意味着你可以在不牺牲性能的前提下,构建出高效、低功耗、高精度的多路采集系统。

🤔 小知识:为什么是“最多 16 个外部通道”?

因为 STM32 的 ADC 输入通道编号 IN0~IN18 中,有些是共用引脚的。例如 ADC1 的 IN0 同时对应 PA0、PB0 等(取决于具体封装),但在同一时间只能启用其中一个作为输入。


我们常说“轮询 ADC”,但如果这个“轮询”是由 CPU 来做的,那就太累了。真正的高手,是让硬件自己动起来。

它是怎么工作的?

想象一下,你有一个待办清单(conversion sequence),上面写着:

  1. 去厨房测温度
  2. 去阳台测光照
  3. 去客厅测湿度

如果你亲自跑一趟,每到一处都要记录数据、返回起点再出发——这就是传统的“逐个启动 + 查询”方式,效率极低。

而扫描模式相当于雇了个助手,你把清单交给他,说:“按顺序走一遍,把数据记下来放桌上。”然后你就去干别的了。等他回来告诉你“搞定了”,你再去桌上拿数据就行。

📌 这个“助手”就是 ADC 控制器,“清单”就是你在 CubeMX 或代码里配置的

转换序列(Rank)

,“桌子”就是内存中的数组。

具体流程如下:

  1. 启动 ADC(软件或定时器触发)
  2. ADC 自动选择第一个通道(比如 Rank 1 对应 IN0)
  3. 开始采样 → 转换 → 得到结果
  4. 如果启用了 DMA,立刻将结果搬移到指定内存地址
  5. 切换到下一个通道(Rank 2),重复步骤 3~4
  6. 所有通道完成后,产生 EOC(End of Conversion Sequence)标志
  7. (可选)触发中断或 DMA 完成回调

整个过程完全由硬件控制,CPU 只需在开始时喊一声“开始!”,结束时知道“好了!”即可。

💡 关键点:扫描模式的核心价值不是“能采多个通道”,而是“

减少 CPU 干预

”。这才是嵌入式系统追求的目标。


HAL 库确实简化了开发,但也隐藏了很多细节。很多人按照默认配置走下来,发现数据不准、顺序错乱、DMA 溢出……其实问题往往出在几个关键参数上。

下面我们拆开来看,

哪些设置必须小心对待

1.

ScanConvMode = ENABLE

这是开启扫描模式的开关。没有它,即使你配置了多个通道,ADC 也只会转换第一个就停下来。

hadc1.Init.ScanConvMode = ENABLE;

⚠️ 注意:一旦启用扫描模式,就必须设置

NbrOfConversion

表示总共多少个通道参与转换。

2.

NbrOfConversion = 2

(或其他数值)

表示本次扫描中有几个通道会被依次转换。这个值必须和你在后续调用

HAL_ADC_ConfigChannel()

时配置的 Rank 数量一致。

hadc1.Init.NbrOfConversion = 2; // 必须等于实际使用的 Rank 总数

❌ 错误示范:

hadc1.Init.NbrOfConversion = 1;
// 然后却配置了 Rank_1 和 Rank_2 —— 第二个根本不会被执行!

3.

ContinuousConvMode

:单次 vs 连续

这个选项决定 ADC 是否循环采集。

设置 行为
DISABLE
执行完一轮扫描后停止,需重新调用

Start_DMA

才能再次采集
ENABLE
完成一轮后立即从头开始下一轮,持续不断

👉 推荐做法:

一般设为 DISABLE

,配合定时器触发使用,便于精确控制采样频率。

例如你想每 10ms 采集一次所有通道,可以用 TIM2 触发 ADC,这样既能保证等间隔,又能避免 DMA 缓冲来不及处理的问题。

4.

EOCSelection = ADC_EOC_SEQ_CONV

这个参数决定了“什么时候算转换结束”。


  • ADC_EOC_SINGLE_CONV

    :每个通道转换完都置位 EOC 标志 → 中断频繁

  • ADC_EOC_SEQ_CONV

    :只有当所有通道全部转换完成后才置位 EOC → 更适合扫描模式

✅ 正确选择:

ADC_EOC_SEQ_CONV

,否则你会被一堆中断打断得怀疑人生。

5.

DMAContinuousRequests = ENABLE

是否允许 ADC 在每次转换后都发出 DMA 请求。

  • 启用 ✔️:每一个通道转换完成都会触发一次 DMA 搬运
  • 禁用 ❌:只在最后一次转换时请求 DMA → 前面的数据会丢失!

所以,只要用了多通道扫描 + DMA,这一项必须打开。


下面这段代码,是你真正能用在项目里的版本,我已经把它优化到了“拿来即用”的程度,并加上了详细注释和工程建议。

#include "main.h"
#include "stm32f4xx_hal.h"

// 存储两个通道的原始数据
uint16_t adc_raw[2]; 

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);

int main(void)


    while (1)
    {
        // 主循环自由运行
        // adc_raw[0] -> PA0 数据
        // adc_raw[1] -> PA1 数据

        // 示例:每 500ms 打印一次数据(通过串口或其他方式)
        HAL_Delay(500);

        // 实际项目中应通过标志位判断数据是否更新
        // 而不是盲目延时
    }
}

初始化函数详解

static void MX_ADC1_Init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4;     // PCLK2 = 84MHz → ADCCLK = 21MHz
    hadc1.Init.Resolution            = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode          = ENABLE;                       // 必须开!
    hadc1.Init.ContinuousConvMode    = DISABLE;                      // 单次模式,推荐
    hadc1.Init.DiscontinuousConvMode = DISABLE;                     // 不使用间断模式
    hadc1.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;// 软件触发
    hadc1.Init.DataAlign             = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion       = 2;                           // 两个通道
    hadc1.Init.DMAContinuousRequests = ENABLE;                      // 每次转换都发 DMA 请求
    hadc1.Init.EOCSelection          = ADC_EOC_SEQ_CONV;             // 整个序列结束后才算完成

    if (HAL_ADC_Init(&hadc1) != HAL_OK)
    {
        Error_Handler();
    }

    // 配置通道 0 (PA0)
    sConfig.Channel         = ADC_CHANNEL_0;
    sConfig.Rank            = ADC_REGULAR_RANK_1;
    sConfig.SamplingTime    = ADC_SAMPLETIME_480CYCLES;  // 高阻抗源必须长采样!
    sConfig.SingleDiff      = ADC_SINGLE_ENDED;
    sConfig.OffsetNumber    = ADC_OFFSET_NONE;
    sConfig.Offset          = 0;

    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }

    // 配置通道 1 (PA1)
    sConfig.Channel         = ADC_CHANNEL_1;
    sConfig.Rank            = ADC_REGULAR_RANK_2;
    // 注意:除了 Channel 和 Rank,其他参数最好显式重设一遍
    sConfig.SamplingTime    = ADC_SAMPLETIME_480CYCLES;
    sConfig.SingleDiff      = ADC_SINGLE_ENDED;
    sConfig.OffsetNumber    = ADC_OFFSET_NONE;
    sConfig.Offset          = 0;

    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
    {
        Error_Handler();
    }
}

MSP 层初始化(底层驱动挂钩)

这部分很多人忽略,但它决定了 GPIO 和 DMA 能不能正常工作。

void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if(adcHandle->Instance == ADC1)
    

        // 把 DMA 句柄绑定到 ADC 句柄
        __HAL_LINKDMA(adcHandle, DMA_Handle, hdma_adc1);
    }
}

🎯 关键点提醒:


  • MemInc = ENABLE

    :确保每次 DMA 把数据写入数组的不同位置

  • Mode = DMA_CIRCULAR

    :非常适合连续采集场景,缓冲区自动回绕

  • Priority = MEDIUM

    :避免被低优先级任务阻塞,影响实时性

你以为启动了

HAL_ADC_Start_DMA()

就万事大吉?Too young.

最大的陷阱来了:

你怎么知道

adc_raw[]

里的数据是不是最新的?

因为 DMA 是后台默默搬运的,当你在

while(1)

里读

adc_raw[0]

的时候,可能它正在被覆盖,也可能还是上次的老数据。

解决方案一:使用半传输中断(Half Transfer Interrupt)

DMA 支持两种中断:

  • HT(Half Transfer):一半数据传完时触发
  • TC(Transfer Complete):全部传完时触发

我们可以利用这两个事件来通知主程序:“新数据来了!”

修改 DMA 配置:
hdma_adc1.Init.Mode = DMA_CIRCULAR;
// 启用中断
if (HAL_DMA_Start_IT(&hdma_adc1, 
                     (uint32_t)&hadc1.Instance->DR, 
                     (uint32_t)adc_raw, 
                     2) != HAL_OK)
{
    Error_Handler();
}
添加回调函数:
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc)

}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)

}

然后在主循环中检测标志位,而不是盲目读取数组。

解决方案二:双缓冲模式(Double Buffer Mode)

如果你启用了 DMA 的双缓冲功能(

FIFOMode = ENABLE

+

DoubleBufferMode

),那么 DMA 会在两个内存区域之间切换,进一步提升安全性。

不过对于仅两个通道的小数组来说,有点杀鸡用牛刀了。但在音频采集、高速波形记录等场景中非常有用。


别以为照着例程就能一帆风顺,以下是我在实际项目中踩过的坑,现在免费送给你👇

🔹 问题 1:前一个通道影响后一个通道的读数?

现象:CH0 是 3.3V,CH1 是 0V,但读出来 CH1 居然有 200 多的值!

原因:

采样时间太短 + 信号源阻抗太高

ADC 的采样阶段就像给一个小电容充电。如果信号源内阻大(比如电位器、某些传感器),而采样时间又短,电容还没充到位就开始转换了,导致电压偏低或残留前值。

✅ 解法:

- 把

SamplingTime

改成

ADC_SAMPLETIME_480CYCLES


- 或者在 PCB 上靠近 MCU 引脚处加一个 100nF 陶瓷电容作局部储能

- 极端情况可外接运放缓冲器

🔹 问题 2:多个通道不是“同时”采集?

是的,STM32F4 的 ADC 是单个转换核心,轮流采集各通道,存在微小时间差(几微秒级)。

对大多数应用(温湿度监控)可以忽略;

但如果你要做三相电流采样用于电机控制,这就会影响相位计算。

✅ 解法:

- 使用

双 ADC 交错模式

(Dual Mode with Interleaved)

- 或改用外部同步采样 ADC 芯片(如 AD7606)

🔹 问题 3:DMA 缓冲被覆盖,数据错乱?

尤其是在

DMA_CIRCULAR

模式下,主程序没及时处理,新数据就把旧数据冲掉了。

✅ 解法:

- 使用

状态标志 + 双缓冲策略


- 或改为

DMA_NORMAL

模式,每次手动重启

- 更高级的做法:结合 FreeRTOS,用消息队列传递数据指针


项目 推荐做法
电源设计
AVDD 和 VDD 分离,中间加磁珠;AVSS 单独接地,避免数字噪声耦合
参考电压
条件允许时使用外部精密基准源(如 REF3033),比内部 Vref 稳定得多
输入保护
所有模拟输入串联 100Ω 电阻 + TVS 管(如 SMAJ3.3A),防静电和过压
PCB 布局
模拟走线尽量短,远离 CLK、SWD、PWM 等高频线;底层完整铺地
校准
上电时调用

HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED)

提升精度
功耗优化
空闲时关闭 ADC 时钟:

__HAL_RCC_ADC1_CLK_DISABLE()

调试技巧
用逻辑分析仪抓 DMA 请求信号,确认是否按时触发

掌握了这套机制之后,你可以轻松扩展到更复杂的系统:

🧩 场景 1:环境监测站

  • 采集 6 路传感器:温湿度 ×2、PM2.5、CO、NO₂、光照
  • 每 2 秒采集一次,通过 LoRa 发送到网关
  • 使用定时器触发 ADC,避免主循环延时不准

🧩 场景 2:电机控制系统

  • 三相电流采样(INA/INB/INC)+ 直流母线电压
  • 使用 TIM1 的 TRGO 触发 ADC,实现精准同步
  • 结合 DMA 和 DCMI 接口,甚至可以做简易示波器功能

🧩 场景 3:电池管理系统(BMS)

  • 多节锂电池电压采集(通过分压电阻接入)
  • 每节电池轮流扫描,配合窗口比较器快速识别异常
  • 数据经滤波算法后上传至上位机

你会发现,

一旦打通了“硬件自动采集 → DMA 搬运 → 主程序处理”这条链路,系统的扩展性和稳定性将大幅提升


很多初学者看到 ADC 配置一大堆结构体,DMA 又要设通道、又要对齐、还要优先级……一下子就懵了。

但你要记住:

所有复杂背后,都是为了实现一个简单的目标——让 CPU 少干活,让系统更高效

多通道扫描 + DMA 的本质,就是把原本属于 CPU 的“体力活”交给硬件去做。你只需要学会“下达命令”和“接收成果”,剩下的交给 STM32 去完成。

下次当你面对“我要同时读 5 个模拟量”的需求时,不要再写五个

HAL_ADC_Start...

了,试试这一套组合拳:

🔁 扫描模式 + 📦 DMA + 🕒 定时器触发 = 真·高效采集

你会发现,原来 STM32F407VET6 的潜力,远不止点亮一个 LED 那么简单。