STM32F411定时器触发ADC采样:TIM2+DMA高精度连续采集配置与实战

发布时间:2026/9/9 5:29:10
STM32F411定时器触发ADC采样:TIM2+DMA高精度连续采集配置与实战 简介STM32F411CEU6(TIME2DMAADC).rar 是一份基于 STM32 HAL 库的嵌入式工程资源面向需要掌握 TIM2 定时器、DMA 与 ADC 协同工作的开发者重点解决每毫秒轮询三个 ADC 通道时的时序控制与数据搬运问题。压缩包共 1148 个文件、约 20.23MB其中包含大量 HAL 库源码与头文件.c/.h、IAR/Keil 工程配置文件.uvprojx/.ioc/.sct、编译输出.axf/.hex/.o及静态库.a/.lib等便于从源码到固件完整对照学习。目前已有 994 人浏览学习适合 STM32 中高级开发者或正在调试多通道模拟采集的工程师参考。通过该工程可以了解 TIM2 周期中断如何触发 ADC 启动DMA 如何在无需 CPU 介入的情况下将多通道转换结果搬移至内存并结合 HAL 库 API 完成初始化与回调处理为环境监测、传感器数据采集等实时应用提供可复用的实现思路。 STM32F411CEU6 这颗料在圈子里大家都不陌生尤其是玩 Black Pill 核心板的那批人几乎人手一片。但要说把这颗芯片的定时器触发采样链路TIM2 DMA ADC彻底玩明白我见过不少人在这一步卡了很久。今天就把我实际调试这套组合时的完整思路、代码细节和踩坑记录整理出来给正在折腾 ADC 多通道采样或者高频采集的朋友一个可直接抄作业的参考。先说说这套方案到底解决什么问题。如果你只是低速采集一两个模拟量比如查个电池电压、读个电位器那完全没必要上 DMA甚至轮询都够用。但一旦你涉及电机相电流采样、电源纹波监测、音频采集或者需要多个通道的数据保持同步采集CPU 逐个去读 ADC 寄存器就会造成两个麻烦一是 CPU 被频繁打断主循环被拖慢二是每次读到的数据之间有时间差多通道数据根本对不齐。TIM2 DMA ADC 这套组合的本质就是把定时触发采样和数据搬运全部交给硬件去完成CPU 只在 DMA 半满或全满时过来收一次数据整个流程对 CPU 的占用率几乎可以忽略不计。我自己实际跑过的配置是 STM32F411CEU6主频 100MHz其实这颗料最高能上 96MHz 才对但通过配置可以达到 100MHz 稳定运行这里不多展开用定时器 TIM2 的更新事件作为 ADC 的外部触发源ADC1 工作在扫描多通道模式DMA1 用循环模式把 ADC 转换结果持续搬运到内存数组里。整套链路跑起来之后ADC 的采样频率完全由 TIM2 决定想改采样率只需要改 TIM2 的预分频和自动重载值不用再去动 ADC 和 DMA 的配置这一点在后期调参时特别方便。1. 整体方案设计为什么要用 TIM2 去触发 ADC 而不是软件触发1.1 定时器触发模式的硬件逻辑STM32F411 的 ADC 支持多种触发源除了最基础的软件触发把 SWSTART 位置 1之外更常用的其实是外部触发。外部触发又可以分成两类一类来自定时器另一类来自片内外设的特定事件。用定时器触发 ADC 的好处是采样时刻完全确定不受主循环里代码执行顺序和分支跳转的影响。打个比方软件触发就像是你在忙别的事情时顺手想去点一下开始采样按键什么时候点完全看心情和手头忙不忙而定时器触发则是定了一个闹钟每隔固定时间闹钟响一次采样电路一听到闹钟响就立刻开工误差只在几个时钟周期以内。对于需要精确等间隔采样的应用比如后面要做 FFT 频谱分析定时器触发几乎是唯一正确的选择。TIM2 在这颗芯片上算是一个通用定时器挂在 APB1 总线上。在 100MHz 系统主频下APB1 定时器时钟默认是 50MHz因为 APB1 预分频不是 1 的时候定时器时钟会翻倍。这个细节非常关键因为你要算采样率的时候必须用实际的定时器时钟频率而不是想当然地拿系统主频去除。1.2 DMA 在这里扮演什么角色ADC 转换完成之后结果是放在 ADCx-DR 寄存器里的这个寄存器只有一个如果你不赶紧把数据读走下一次转换开始就会把上一次的数据覆盖掉。DMA 就是专门解决这个吞吐问题的ADC 每次转换结束会向 DMA 控制器发出一个请求DMA 直接把数据从外设寄存器搬到内存数组里不需要 CPU 干预。DMA1 的通道 1 是 ADC1 的专用请求通道这是硬件上固定的映射具体查看参考手册的 DMA 请求映射表不能乱接。我见过有些朋友在 CubeMX 里把 DMA 通道配错了导致数据死活不出来多半就是没查这张映射表。配置为循环模式Circular Mode之后DMA 搬运完一轮数据会自动把地址指针恢复到起始位置重新开始这样 ADC 就能永不停歇地连续工作这是实现长时间不间断采样的基础。1.3 三个外设协作的时序关系把三个外设串起来看TIM2 产生更新事件也可以理解成溢出中断事件但注意我们这里不是用中断而是事件ADC 检测到这个触发信号后开始一次转换转换完成后产生 DMA 请求DMA 把结果搬到内存然后 ADC 空闲等待下一次触发。这个流水线式的过程是紧密耦合的好处是一旦跑起来就完全不需要 CPU 在中间插手。需要注意的是ADC 的采样时间和转换时间必须小于定时器的触发周期。这句话是废话但很多人忽略假如你 ADC 配置的采样周期是 3 个周期加上 12 位转换本身的 12 个周期一共 15 个 ADC 时钟周期如果你 ADC 时钟是 10MHz单次转换时间就是 1.5 微秒。如果 TIM2 设置的触发周期比 1.5 微秒还短那 ADC 会漏掉触发事件数据就会出错或者对不齐。2. 核心配置细节CubeMX 图形化配置与关键参数选择2.1 CubeMX 中的具体配置步骤先说工具链。我知道圈子里分成两派一派是标准外设库死忠一派是 HAL 库拥趸。我的建议是如果你是在做产品原型验证或者学习阶段直接用 STM32CubeMX 生成初始化代码最省事但生成之后不要啥都不管一定要把生成的代码读一遍搞清楚每个外设的初始化顺序和参数含义。这样出了问题你才有方向去排查而不是像个无头苍蝇一样乱试。CubeMX 里的配置顺序我的习惯是这样的以 STM32F411CEU6 为例第一步配置时钟。外部高速晶振 HSE 我这里用的是 25MHz 的晶振Black Pill 板载可以通过 PLL 倍频到 100MHz。为什么不直接用内部 HSI因为内部 RC 振荡器精度不够对 ADC 采样时间会有影响尤其是在做音频类应用时时钟抖动会直接体现在采样数据上。在Clock Configuration标签页里把 HCLK 拉到 100MHz软件会自动帮你算好各总线分频系数。第二步配置 ADC1。在 Categories 里找到 ADC1开启通道 0 和通道 1这个按自己需求来我这里是双通道采样时间我一般设在 12 到 15 个周期左右。扫描模式Scan Mode选 Enable——注意如果你只开了一个通道就不用开扫描模式开了反而有问题后面常见问题里我会讲。连续转换模式Continuous Conversion Mode这里不要开因为我们要用定时器去触发如果开了连续转换ADC 就会不受控制地一直转下去定时器触发反而失去意义。外部触发源选择Timer 2 Capture Compare 2 event或者Timer 2 Trigger Out event具体选哪个要看你想用定时器的哪个事件来触发这里要注意配置一致性。第三步配置 DMA。在 ADC1 的配置页面里添加 DMA 请求选择 DMA1 Channel 1模式一定要选 Circular循环模式数据宽度外设和内存都选 Half Word也就是 16 位。为什么是 16 位因为 STM32F411 的 ADC 是 12 位的转换结果放在寄存器低 16 位里你用 16 位搬运效率和内存占用都是最优的用 32 位的话每次搬运多读两个字节的无效数据没必要。第四步配置 TIM2。预分频 PSC 和自动重载 ARR 的值要看目标采样频率来算。我们后面单独开一节专门讲计算。2.2 采样频率的计算逻辑与实例采样频率的公式其实特别简单如果你的定时器时钟是 50MHz那么采样频率就是 50MHz / (PSC 1) / (ARR 1)。这里加 1 是因为定时器是从 0 开始计数的。举个例子我想要 10kHz 的采样率也就是每 100 微秒采一次那我可以设 PSC 49分频到 1MHzARR 99从 0 数到 99共 100 个计数。这样 1MHz / 100 10kHz。这个方案意味着定时器时钟 1MHz每个计数周期是 1 微秒100 个计数就是 100 微秒正好 10kHz。我在之前实际音频采集项目中用了这个参数组合效果很稳定采样率误差在万分之几以内。当然如果你需要更高精度的采样率比如精确到个位数赫兹最好的做法是先确定 PSC然后反推 ARR 取整接受微小误差或者去调整系统时钟频率来让分频完全整除。对于大多数场景这点误差根本不会影响最终效果。还有一个注意事项如果你改了定时器时钟比如系统主频从 100MHz 改到 84MHzPSC 和 ARR 必须重新计算。我见过有人把工程从 F103 移植到 F411 上时钟树没改ADC 采样率直接翻倍还以为是程序问题。这种低级错误其实排查起来很耗时不如一开始就把配置逻辑理解透。2.3 DMA 中断的优先级选择数据搬运到内存之后你总得知道自己搬了多少数据吧有两种途径一是 DMA 传输完成中断Transfer Complete二是 DMA 半传输中断Half Transfer。Half Transfer 在这个场景里特别有用因为它是双缓冲的自然分割点当 DMA 搬运到一半时触发中断此时 CPU 可以去处理前一半数据同时 DMA 继续往后半段搬等后半段满了再触发一次中断。这样 CPU 的处理时间和 DMA 的搬运时间可以重叠吞吐率直接翻倍。在 NVIC 配置里我把 DMA1_Stream0 的中断优先级设成了抢占优先级 2、子优先级 0这个根据自己的工程需求来只要你没有实时性更强的中断就行并且用中断回调函数来设置一个标志位主循环检测到标志位就处理数据。这种抽象的好处是中断服务函数只做最简单的置位操作不进行耗时的数据处理保证中断退出足够快不阻塞系统。3. 实操过程从初始化代码到数据读取的完整链路3.1 初始化代码的关键片段解读用 CubeMX 生成代码之后核心初始化函数基本就是以下几个。我挑重点来讲不是让你无脑抄代码而是理解每段在干什么。ADC 初始化这里最重要的是下面这几行配置hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode ENABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T2_TRGO; hadc1.Init.DMAContinuousRequests ENABLE;ClockPrescaler 为什么要 DIV4因为在 100MHz 主频下ADC 时钟最高只能跑到 20MHz 左右具体看芯片数据手册F411 是 Internal ADC clock 最高 20MHz同步时钟模式下受 APB2 时钟影响。如果不分频直接给 50MHz 或者 100MHzADC 采样结果完全是乱的这是新手最容易踩的坑。ExternalTrigConvEdge 是上升沿触发这个对应着 TIM2 的 TRGO 事件。你需要保证 TIM2 的 TRGO 确实配成了 Update Event这个在 CubeMX 里 TIM2 配置页面的Trigger Output (TRGO)选项里选Update Event就行。DMA 初始化代码hdma_adc1.Instance DMA1_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; hdma_adc1.Init.Priority DMA_PRIORITY_HIGH;注意 DMA1_Stream0 Channel 0 的组合这对应 ADC1 的 DMA 请求。PeriphInc 必须 Disable因为外设地址始终是 ADC1-DR 这一个寄存器MemInc 必须 Enable因为我们要把数据依次放到数组的各个元素里。Mode 是 Circular这样 DMA 搬完一圈自动回到开头配合 ADC 连续触发实现不间断采集。Priority 这里我设 High但如果你系统里有其他 DMA 也在跑需要统筹考虑。3.2 HAL 库回调机制的正确使用姿势HAL 库的中断处理逻辑是外设中断触发后HAL 库会调用对应的中断回调函数。对于 ADC DMA 的场景我们需要关心的是 DMA 传输完成中断回调。有两种方式可以捕获这个事件第一种是直接在HAL_ADC_ConvCpltCallback里处理数据。这个回调函数会在一次完整的 ADC 转换序列结束后被调用也就是所有通道的数据都搬完了。第二种是使用 DMA 的半传输回调/完全传输回调。在 HAL 库中HAL_DMA_IRQHandler会被调用然后设置传输完成标志。我在实际工程里一般定义一个环形缓冲区处理#define ADC_BUF_SIZE 1024 volatile uint16_t adc_raw_buf[ADC_BUF_SIZE]; volatile uint8_t adc_data_ready 0; void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { adc_data_ready 1; } }在主循环里检测到 adc_data_ready 被置位就处理数据。注意处理完之后要手动清掉这个标志不然下次 DMA 搬完新数据之后你都不知道到底是新数据还是旧数据。更严谨的做法是用一个变量记录 DMA 当前写到了哪个位置通过__HAL_DMA_GET_COUNTER读取计数器的当前值反推当前写到了数组的哪个下标从而计算出新数据在数组里的区间。这个方法比较复杂但确实稳定适合追求极致时效的项目。3.3 一次完整的数据采集流程演示我直接给一个最简单的实验流程你跑通了就跑通了跑不通就看后面的常见问题部分排查。假设你配好了 CubeMX 工程初始化了 ADC1、DMA1、TIM2并且启动了它们HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_raw_buf, ADC_BUF_SIZE); HAL_TIM_Base_Start(htim2);第一行启动 ADC DMA第二行启动定时器。启动定时器之后TIM2 就开始产生更新事件ADC 收到触发信号开始转换DMA 搬运数据。就这么简单后面的数据全部由硬件自动完成你可以打开调试器在内存窗口里观察 adc_raw_buf 数组会发现数据在持续刷新。我最开始调试的时候还干过一件蠢事重复调用HAL_ADC_Start_DMA想重新开始采集结果发现数据乱了。后来查手册才明白在 Circular 模式下 DMA 根本不需要你手动重启它会一直循环搬运。如果你在某次想暂停采集只需要调用HAL_TIM_Base_Stop停止定时器ADC 没有触发信号了自然就停止采样。想恢复就再次启动定时器这套逻辑简单可靠。4. 常见问题与排查技巧实录4.1 数据全零或者同一个值不变遇到这个现象先别怀疑 ADC 坏了。我从经验来看最常见的原因是 DMA 根本没启动或者 DMA 配置的外设地址不对。检查步骤先看HAL_ADC_Start_DMA的返回值和HAL_DMA_GetState的状态如果是HAL_DMA_STATE_READY说明 DMA 还没进入传输状态此时检查你有没有调用HAL_ADC_Start_DMA之前先HAL_TIM_Base_Start——有些人代码顺序写反了DMA 还没准备好TIM 先跑起来了导致第一批转换事件丢失。另外检查 DMA 的数据宽度如果是 Byte8 位模式ADC 的 12 位结果会被截断数据看着就像坏了一样。正确配置是半字16 位。4.2 多通道模式下通道数据错位如果你开了 Scan Mode 扫描多个通道通道数据的顺序是由你初始化 Rank 时的顺序决定的。比如你先 Rank1 配置为 Channel0Rank2 配置为 Channel1那么 DMA 数组里的第 0 个元素是 Ch0 的数据第 1 个元素是 Ch1 的数据。这个顺序不能弄错。但有时候你明明配置顺序是对的数据还是错位那是因为你的 DMA 数组大小不够。举个例子2 个通道DMA 传输长度你配置成 512意思是一共搬 512 次每次搬一个半字那数组中 Ch0 和 Ch1 各占 256 个样本。如果你搞错了 DMA 传输长度的含义以为是单个通道的样本数那数据处理时就会错位。DMA 的传输长度是总传输次数不是每个通道的样本数这个一定要搞清楚。4.3 ADC 采样值跳变严重噪声大跳变的原因很多我按优先级从高到低列出排查思路第一ADC 参考电压是否稳定。如果你用板载 3.3V 供电且参考电压就是 VDD那么 VDD 上的纹波会直接进 ADC。BLACK PILL 板子上的 3.3V 一般质量还行但不排除你的电源本身有 100mV 级别的纹波这种情况下你可以改用内部参考如果有的话或者在 VREF 引脚上加一个高质量电容。第二采样电容的问题。如果你测的信号源内阻很大比如你直接用一个 10k 欧姆的电位器分压信号源内阻和 ADC 内部的采样电容会构成 RC 低通网络导致采样值不稳定或不准确。解决方法是加一个运放跟随器做阻抗变换或者把采样时间调得更长。第三时序干扰。如果 ADC 的触发源和某个大电流外设的开关频率接近比如 PWM 驱动电机的同时采电流那叠加噪声基本无法避免只能用软件滤波的方式来补救。4.4 采样率不稳数据有周期性毛刺这种情况我在做音频采集时遇到过最后定位到的原因是 DMA 中断和主循环里的某个耗时操作抢 CPU导致数据处理不及时环形缓冲区数据被覆盖。解决方法——提高 DMA 中断的抢占优先级确保数据搬运和中断处理不被其他中断打断。 ——把数据处理拆分成小块不要在中断里面做大数组的拷贝或者浮点运算。 ——如果是批量数据处理比如 FFT可以用双缓冲的思路DMA 搬后半段的时候处理前半段把时间重叠起来。4.5 一个常见误区DMA Continuous Requests 的作用有些人在配置 CubeMX 时看到 DMAContinuousRequests 这个选项不知道啥意思就一直默认 Disable。这个参数的含义是ADC 使能后DMA 请求是否持续产生。如果你用定时器触发 ADC这个参数要设成 Enable否则每个定时器触发事件只产生一次 DMA 请求下一次触发的时候 DMA 无法连续搬数据你会看到数组里只有一个位置的数据在刷新其他都是 0。这个坑让很多人卡过写出来给各位提个醒。5. 扩展应用从单 ADC 到多 ADC 与双缓冲优化5.1 多 ADC 同步采样的思路如果你需要两个 ADC 同时采样比如三相电流检测STM32F411 也支持双 ADC 模式Dual Mode两个 ADC 可以做到同步转换。DMA 的配置会更复杂一些因为双 ADC 模式的数据排列方式有几种Regular simultaneous mode 等生成的 32 位数据里高 16 位和低 16 位分别对应两个 ADC 的结果。这种模式下 DMA 的数据宽度要设为 32 位而且要特别小心数据对齐方式。我自己在电机控制项目里用这个模式采过两路电流采样率 20kHz效果非常好。但前提是中断优先级和双缓冲体系设计得足够好否则容易丢数据。5.2 DMA 双缓冲机制的进阶应用前面提到过 Half Transfer 中断这里展开讲一下双缓冲的具体实现。核心思想是用两个数组DMA 交替往两个数组里搬数据。当 DMA 在搬数组 A 的时候CPU 处理数组 B 的数据等 DMA 搬完 A 开始往 B 搬的时候CPU 正好处理完 B 开始处理 A。这样从宏观上看数据搬运和处理是完全重叠的不存在相互等待。在 HAL 库里实现双缓冲需要用到HAL_DMAEx_MultiBufferStart这类函数看具体芯片型号支持情况。F411 的 DMA 可以通过HAL_DMAEx_ChangeMemory来切换目标内存地址。这种进阶玩法对于需要持续高速采样的场景价值巨大。我在一个高速振动监测项目里把采样率提到了 125kHz双缓冲模式下 CPU 占用率依然很低主循环还有大量时间跑其他的业务逻辑。5.3 数据后处理滑动平均滤波与简单 FFT采集到原始 ADC 数据后很多人直接就拿来用了但原始数据往往带有高频噪声。最简单的处理方法是滑动平均滤波也就是取最近 N 个样本的平均值作为当前输出值。N 的取值要注意平衡N 太小滤波效果差N 太大会把有效信号的变化也抹平。uint32_t sum 0; uint16_t filter_buf[FILTER_SIZE]; uint8_t filter_index 0; void filter_push(uint16_t value) { sum - filter_buf[filter_index]; filter_buf[filter_index] value; sum filter_buf[filter_index]; filter_index (filter_index 1) % FILTER_SIZE; } uint16_t filter_get_average(void) { return (uint16_t)(sum / FILTER_SIZE); }这段代码用环形缓冲区和累积和每次操作都是 O(1) 复杂度性能非常高。但要注意 sum 变量要用 32 位装不然滤波窗口大了会溢出。如果你要做 FFT 频谱分析STM32F411 没有硬件 FFT 加速器F4 系列的 DSP 库也提供了软件 FFT 功能但用 CMSIS-DSP 库的 arm_cfft_q15 函数在 100MHz 主频下跑一个 1024 点的实序列 FFT耗时在毫秒级以内完全可接受。关键是 FFT 对采样率稳定性要求很高如果采样点的时间间隔不均匀FFT 结果会有明显的频谱泄漏。这就是为什么一开始我就强调要用定时器触发 ADC而不是软件触发——只有定时器触发才能保证严格等间隔采样。6. 写在最后的几个实操心得这套 TIM2 DMA ADC 的方案我前前后后在多个项目里用过从最开始的 F103 到现在的 F411每次重新搭这套链路都会花一些时间但属实是值回票价的。能让你在后期把注意力放在数据处理和算法逻辑上而不是被底层采样折腾得焦头烂额。想给刚开始接触这套玩意的朋友一个建议不要一上来就搞多通道、双缓冲、FFT第一步先把单通道定时器触发 DMA 搬运跑通用调试器观察数组里数据的刷新情况确认数据是活的而且是对的再去逐步叠加复杂度。我见过太多人在一次配置了太多功能出了问题完全不知道是哪个环节的锅反而耽误时间。还有一个很实际的小技巧调试 DMA 搬运的数据时用串口把这个数组的原始值打印出来配合一个示波器或者信号发生器给 ADC 输入端加一个已知电压看打印的数据是否和你预期的一致。这个方法虽然土但定位问题特别快。我之前在这个环节花了不少冤枉时间去反复检查代码后来总结出来先把信号源校准好再谈代码问题能省掉一半调试时间。本文还有配套的精品资源点击获取