资讯动态

STM32F1三通道ADC连续转换:规则组扫描+DMA配置与避坑指南

发布时间:2026/10/16 22:59:24 来源:尧图企业网站定制
简介本资源为STM32F1x系列ADC三通道连续转换的完整工程代码面向具备一定嵌入式基础的开发者与学习者帮助解决多通道模拟信号采集与DMA高效搬运的实战问题。工程围绕12位ADC的初始化、通道选择、DMA配置、软件触发、中断处理及数据读取等关键环节展开并涉及功耗优化与输入电压安全等注意事项可应用于工业自动化、环境监测、电机控制等实时数据采集场景。压缩包为zip格式大小约309KB文件总数与类型明细上游暂未提供。目前已有423人学习下载适合希望掌握STM32多通道ADC连续转换与DMA协同工作方式的读者参考通过完整工程结构快速理解采样序列配置、转换结果自动存储及中断响应流程为后续移植到实际项目提供可复用的代码基础。1. 三通道 ADC 连续转换为什么你采到的电压总在跳如果你用 STM32F1 做过电压采集大概率遇到过这个场景单通道轮询读出来挺稳一旦改成三个通道轮流采数据就开始飘通道之间还互相串。很多人第一反应是硬件滤波没做好其实问题常常出在采样时序和规则组配置上。STM32F1 的 ADC1 有三个规则通道配合 DMA 做连续转换是工业采集里最省 CPU 的方案之一但它的坑集中在「采样时间分配」和「DMA 缓冲区对齐」这两处。这份 STM32F1x ADC 三通道连续转换完整工程代码就是围绕 ADC1 的 IN0、IN1、IN2 三个通道用规则组扫描加 DMA 循环搬运把三路电压稳定地落到内存数组里。它适合正在做多路模拟量采集、又不想被中断频繁打断主循环的嵌入式开发者也适合刚接触 STM32 标准外设库、想找一个能直接编译下载的 ADC 工程模板的人。下面我按「先讲清配置逻辑再落到代码最后说坑」的顺序拆一遍。2. 规则组扫描加 DMA三通道连续转换的配置逻辑2.1 为什么选规则组扫描而不是注入组STM32F1 的 ADC 有两组通道规则组和注入组。注入组优先级高、可打断规则组适合做紧急采样规则组则是按顺序依次转换配合 DMA 可以把结果自动搬到内存。三通道连续采集这种场景规则组扫描是更自然的选择——你只需要在 ADC_RegularChannelConfig 里把三个通道按顺序排好启动一次转换硬件就会自己按 IN0、IN1、IN2 的顺序走完一轮每转完一个通道触发一次 DMA 请求。注入组不是不能用但它一次最多四个通道且转换结果放在独立的 JDRx 寄存器里DMA 支持不如规则组直接。常见做法是需要和规则组同步采样的场合才用注入组纯多路轮询采集一律走规则组加 DMA。这份工程用的就是规则组三通道扫描DMA 循环模式主循环完全不碰 ADC 数据寄存器。2.2 采样时间怎么定三个通道不能一刀切采样时间决定了 ADC 内部采样保持电容的充电窗口。STM32F1 的采样时间从 1.5 到 239.5 个 ADC 时钟周期可调。很多人图省事三个通道全设成 55.5 周期结果高阻抗通道采不准低阻抗通道又浪费时间。我一般这样分配先看信号源内阻。如果某一路前面串了较大分压电阻比如 100k 以上采样时间至少给到 71.5 或 239.5 周期如果是运放跟随输出内阻很低1.5 或 7.5 周期就够。这份工程里三个通道的采样时间是在 ADC_RegularChannelConfig 的最后一个参数里分别指定的不是全局统一值。这一点很关键——规则组每个通道的采样时间可以独立设置别被「统一配置」的写法带偏。提示采样时间总和加上转换时间必须小于你期望的两次 DMA 请求间隔否则会出现数据覆盖。三通道 55.5 周期加 12.5 周期转换在 14MHz ADC 时钟下大约 14.6 微秒一轮这个量级心里要有数。2.3 DMA 缓冲区大小与循环模式DMA 这边要盯三个参数外设地址、内存地址、数据长度。外设地址固定是 ADC1-DR内存地址是你自己定义的 uint16_t 数组数据长度等于规则组通道数也就是 3。方向设为外设到内存模式设为循环这样一轮转完自动回到数组头部不需要手动重启 DMA。有个细节容易被忽略DMA 的存储器数据宽度和外设数据宽度都设成半字16 位因为 ADC_DR 是 16 位寄存器结果也是 12 位右对齐。如果你设成字节高位会被截掉读出来的值永远偏小。这份工程在 DMA_Init 里把 PeripheralDataSize 和 MemoryDataSize 都写成 DMA_PeripheralDataSize_HalfWord 和 DMA_MemoryDataSize_HalfWord这是必须对齐的。3. 从工程结构到关键代码三通道 ADC 怎么落地3.1 工程文件构成与时钟使能顺序拿到这份工程先看目录结构。它基于 STM32F1 标准外设库核心文件是 main.c、adc.c、dma.c 和 stm32f10x_it.c。adc.c 里放 ADC 初始化和通道配置dma.c 里放 DMA 初始化main.c 负责调用和读数据。时钟使能顺序不能乱先开 DMA1 时钟再开 ADC1 时钟最后配 GPIO。因为 DMA 要先于 ADC 准备好否则 ADC 第一次触发 DMA 请求时通道还没使能会丢第一个数据。// 时钟使能顺序影响首次转换是否丢数 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 先开 DMA1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 再开 ADC1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 最后开 GPIOA逻辑说明DMA1 挂在 AHB 总线上ADC1 和 GPIOA 挂在 APB2 上。先使能 DMA 是为了保证 ADC 配置完成、启动转换之前DMA 通道已经处于可用状态。参数上RCC_AHBPeriph_DMA1 对应 DMA1 控制器RCC_APB2Periph_ADC1 对应 ADC1 外设这两个宏在 stm32f10x_rcc.h 里定义。3.2 GPIO 模拟输入配置与 ADC 校准三个通道对应 PA0、PA1、PA2都要配成模拟输入模式。这里有个血泪经验模拟输入模式下GPIO 的输出驱动和上拉下拉都要关掉否则外部信号会被内部电路拉偏。配置完 GPIO 之后ADC 使能之前必须做一次校准否则零点和增益误差会让三路数据整体偏移。// PA0/PA1/PA2 配置为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入非浮空非上拉 GPIO_Init(GPIOA, GPIO_InitStructure); // ADC 校准使能后必须先校准再启动转换 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));逻辑说明GPIO_Mode_AIN 会关闭施密特触发器和上下拉让引脚直接连到 ADC 采样网络。校准分两步——复位校准寄存器和启动校准每次都要等状态位清零。参数上ADC_ResetCalibration 和 ADC_StartCalibration 是标准库函数内部操作的是 ADC_CR2 的 RSTCAL 和 CAL 位。跳过校准的后果是三路读数可能整体偏高或偏低几十个 LSB且通道之间偏差不一致。3.3 规则组三通道排序与 DMA 绑定这是整个工程的核心配置段。规则组的顺序决定了 DMA 缓冲区里哪个元素对应哪个通道。比如你把 IN0 排第一、IN1 排第二、IN2 排第三那么 DMA 数组的 [0] 就是 IN0[1] 是 IN1[2] 是 IN2。顺序一旦写错你读到的「通道1」其实是通道2的电压。// 规则组三通道排序顺序即 DMA 缓冲区索引顺序 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_71Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_7Cycles5); // 开启扫描模式和连续转换模式 ADC_InitStructure.ADC_ScanConvMode ENABLE; // 多通道必须开扫描 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续转换不停止 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发 ADC_Init(ADC1, ADC_InitStructure); // 绑定 DMA 并启动 ADC_DMACmd(ADC1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE);逻辑说明ADC_RegularChannelConfig 的第三个参数是规则组内的排名从 1 开始不是通道号。第四个参数是采样时间三个通道分别给了 71.5、55.5、7.5 周期对应不同内阻的信号源。ADC_ScanConvMode 必须 ENABLE否则只会转第一个通道。ADC_ContinuousConvMode 设为 ENABLE 后一轮三通道转完自动开始下一轮DMA 循环模式正好接住。参数上ADC_ExternalTrigConv_None 表示不用外部触发靠 ADC_SoftwareStartConvCmd 启动一次后持续运行。3.4 主循环读取与电压换算DMA 在后台搬运主循环只需要读数组。但要注意DMA 是循环覆盖的你读的时候可能正好读到一半新一半旧。常见做法是读两次做比较或者用 DMA 传输完成中断做双缓冲。这份工程为了简洁主循环里直接读配合一个简单的软件滤波。// 主循环读取三通道 DMA 缓冲区并换算电压 uint16_t adc_buf[3]; // DMA 目标数组必须全局或静态 float voltage[3]; const float vref 3.3f; while(1) { for(int i 0; i 3; i) { voltage[i] (float)adc_buf[i] * vref / 4095.0f; // 12位ADC满量程4095 } // 此处可加均值滤波或中值滤波 Delay_ms(100); }逻辑说明adc_buf 必须是全局或静态变量因为 DMA 在后台持续写入局部变量地址在栈上DMA 访问会出错。换算公式里 4095 是 12 位满量程vref 取 3.3V如果实际参考电压是 3.3V 以外的值要按实际修改。参数上adc_buf 的类型是 uint16_t和 DMA 半字配置对应。主循环延时 100ms 只是示例实际采集节奏由你的应用决定。4. 避坑排查三通道 ADC 最容易翻车的四个地方4.1 通道数据串位现象是「通道1的电压跟着通道2变」现象读出来的三路数据里某两路明显相关一路变化另一路跟着动。原因通常是规则组排序和 DMA 缓冲区索引没对应上或者 ADC_RegularChannelConfig 的排名参数写重复了。比如两个通道都写排名 1硬件只会转其中一个另一个位置的数据是残留值。解决把三个 ADC_RegularChannelConfig 调用的排名参数依次写成 1、2、3并在代码注释里标清「排名 N 对应 adc_buf[N-1]」。改完重新编译下载用固定电压源分别接三个通道验证。4.2 采样时间不够高内阻通道读数偏低且跳现象某一通道接的是分压电阻输出的信号读数比万用表低一截且波动大。原因是采样时间太短采样保持电容没充到信号电压。STM32F1 的采样保持电容约 8pF信号源内阻 100k 时时间常数约 0.8 微秒要充到 12 位精度至少需要 7 到 8 个时间常数也就是 6 微秒以上。55.5 周期在 14MHz 下约 4 微秒不够。解决把该通道采样时间改成 239.5 周期或者在前级加电压跟随器降低输出阻抗。改完用示波器看 ADC 输入引脚确认信号在采样窗口内稳定。4.3 DMA 缓冲区被编译器优化现象是数据永远不变现象程序下载后adc_buf 里的值一直是初始值ADC 寄存器明明在变。原因是 adc_buf 定义成了局部变量或者被编译器优化到寄存器里DMA 写的是内存地址但 CPU 读的是寄存器缓存。解决把 adc_buf 定义成全局变量并加 volatile 关键字告诉编译器每次从内存读。常见写法是volatile uint16_t adc_buf[3];。另外检查 DMA_InitStructure.DMA_MemoryBaseAddr 是否取了 adc_buf[0]取错地址 DMA 会写到别处。4.4 连续转换模式下停止位没处理现象是功耗偏高现象系统待机电流比预期大ADC 一直在转。原因是 ADC_ContinuousConvMode 设为 ENABLE 后ADC 不会自动停即使你不再读数据它也在消耗电流。解决如果应用需要间歇采集把连续转换关掉改用单次转换加软件触发每次采完自动停。或者在不需要采集时调用 ADC_Cmd(ADC1, DISABLE) 关掉 ADC。注意关之前先关 DMA 请求否则残留的 DMA 请求可能触发一次空搬运。5. 进阶技巧用 DMA 传输完成中断做双缓冲采集主循环直接读 adc_buf 有个隐患你读的时候 DMA 可能正在写同一块内存读到的三个值可能来自不同轮次。对于慢速采集无所谓但如果你要做 50Hz 工频信号分析这个时间差会引入相位误差。我一般会开 DMA 传输完成中断在中断里切换缓冲区实现双缓冲。具体做法定义两个数组 adc_buf_a[3] 和 adc_buf_b[3]DMA 初始化时指向 adc_buf_a传输完成中断里把 DMA_MemoryBaseAddr 改成 adc_buf_b同时置一个标志位告诉主循环「adc_buf_a 已满可以读」。下一轮中断再切回 adc_buf_a。这样主循环读的永远是已经搬完的完整一轮数据不会读到半新半旧的值。// DMA 传输完成中断服务函数中的双缓冲切换 void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); if(dma_target 0) { DMA_SetCurrDataCounter(DMA1_Channel1, 3); DMA1_Channel1-CMAR (uint32_t)adc_buf_b; // 切到缓冲区B dma_target 1; buf_ready 1; // 标志A已满 } else { DMA_SetCurrDataCounter(DMA1_Channel1, 3); DMA1_Channel1-CMAR (uint32_t)adc_buf_a; // 切回缓冲区A dma_target 0; buf_ready 2; // 标志B已满 } } }逻辑说明DMA1_Channel1 对应 ADC1 的 DMA 请求通道。DMA_SetCurrDataCounter 重新装载传输数量因为循环模式下计数器会自动重载但手动改 CMAR 后需要确保计数器正确。CMAR 是 DMA 通道的内存地址寄存器直接写寄存器比调库函数快适合中断里用。buf_ready 标志告诉主循环哪个缓冲区可读。参数上DMA1_IT_TC1 是传输完成中断标志清除后才能响应下一次。这个双缓冲结构配合定时器触发 ADC就能做等间隔同步采集。我习惯把定时器触发频率设成采样率的整数倍ADC 外部触发选 TIM2_CC2 或 TIM3_TRGO这样三个通道的采样时刻由硬件定时器锁定比软件启动的抖动小一个数量级。从那以后我每次做多通道 ADC 采集都强制走一遍「排序核对、采样时间分通道、DMA 地址 volatile、双缓冲切换」这四步再也没出现过数据串位和跳变。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑