STM32两路ADC测量实战:从原理到滤波算法与国赛真题解析
2026/8/29 21:56:38 网站建设 项目流程

1. 项目概述:从国赛真题看ADC测量的核心价值

在蓝桥杯嵌入式国赛的赛场上,两路ADC测量这个题目可以说是一个“常客”,也是一个检验选手嵌入式系统基本功的“试金石”。乍一看,题目要求似乎很简单:用单片机采集两路模拟电压信号。但真正做过的朋友都知道,这里面门道可深了。它绝不仅仅是调用一下HAL库的HAL_ADC_Start那么简单,而是综合考察了你对STM32(国赛常用平台)ADC外设的理解深度、对实时性与精度的平衡能力,以及对嵌入式系统资源管理的全局观。

为什么国赛如此青睐这个题目?因为ADC是连接物理世界与数字世界的桥梁。无论是智能车的光敏电阻、角度传感器,还是工业控制中的温度、压力变送器,最终都需要通过ADC将连续的模拟量转换为单片机可以处理的数字量。两路测量,则引入了“同步”、“交替”、“优先级”等更复杂的应用场景。你不仅要让ADC工作起来,还要让它稳定、准确、高效地工作,同时处理好两路数据可能存在的相互干扰、采样时机冲突等问题。这背后涉及时钟配置、采样时间计算、DMA传输、数据处理算法等一系列硬核知识点。

这篇文章,我就以一个过来人的身份,结合国赛真题的典型要求和实际项目中的经验,带你彻底吃透STM32平台下的两路ADC测量。我会从最基础的原理和配置讲起,一直深入到滤波算法、误差分析和实战避坑指南。无论你是正在备赛的蓝桥杯选手,还是刚接触STM32的嵌入式开发者,相信这篇长文都能给你带来实实在在的帮助。

2. 核心需求与方案设计拆解

面对“两路ADC测量”这个任务,我们首先要摒弃“拿到就写代码”的冲动。一个好的开始是成功的一半,在动手之前,我们必须把需求拆解清楚,并据此设计一个稳健的方案。

2.1 需求深度解析

国赛题目通常不会直接告诉你所有细节,需要我们根据描述推断出隐含的高阶需求:

  1. 基本功能需求:这是明面上的要求。使用STM32的ADC模块,对两路指定的模拟输入通道(例如PA5和PA6)进行电压采集,并将结果通过串口发送到上位机显示,或者用于后续的控制逻辑。
  2. 性能与精度需求:这是国赛拉开差距的关键。题目可能要求测量范围(如0-3.3V)、精度(如误差小于±0.05V),或者对电压变化的响应速度有要求。这直接决定了ADC的分辨率(12位)、采样率以及参考电压的稳定性。
  3. 实时性与同步性需求:两路测量,它们之间的关系是什么?
    • 独立异步采样:两路信号变化缓慢且无关联,可以分时复用ADC,轮流采样。这是最简单的方式,但无法获取同一时刻的两路信号值。
    • 定时同步采样:需要获取同一时刻的两路信号状态(例如,计算功率时需要瞬时电压和电流)。这通常需要利用ADC的扫描模式+DMA,或者双ADC模式(如果MCU支持)。
    • 触发采样:ADC的启动由外部事件(如定时器、引脚)触发,这对于需要与系统其他部分严格同步的采样至关重要。
  4. 系统资源与稳定性需求:在比赛中,CPU资源非常宝贵。如果使用阻塞式的轮询读取ADC,会大量占用CPU时间。因此,采用DMA(直接存储器访问)将ADC数据自动搬运到内存,是提升系统效率、保证其他任务(如按键扫描、显示刷新)流畅运行的标准做法。此外,电源噪声、PCB布局也会影响ADC的稳定性,这属于硬件设计范畴,但在软件上我们可以通过滤波来弥补。

2.2 方案选型与决策逻辑

基于以上需求,我们可以形成一个清晰的方案决策树:

第一步:确定采样模式对于大多数蓝桥杯国赛场景,两路信号通常不需要严格的同步到纳秒级,但要求稳定的周期性采样。因此,规则组扫描模式 + DMA传输是首选方案。我们将两路通道配置到一个ADC的同一个规则组序列里,ADC会自动按顺序转换它们,转换完成后通过DMA一次性将两个结果搬运到指定的数组。这种方式不占用CPU,且能保证两路采样的间隔极短(取决于ADC的转换时间),对于大多数应用可以近似认为是“同步”的。

为什么不直接用双ADC?像STM32G4、H7等系列确实支持双ADC,可以实现真正的硬件同步采样。但对于国赛常用的中端型号(如STM32F103、STM32G431),通常只有一个ADC模块(可能有多个通道)。因此,扫描+DMA是最通用、最经济的实现方式。

第二步:确定触发源ADC何时开始一次转换?有三种选择:

  • 软件触发:调用HAL_ADC_StartHAL_ADC_Start_DMA。简单,但时序控制不精确。
  • 定时器触发:配置一个定时器(如TIM2)产生更新事件,并将其连接到ADC的触发输入端。这是强烈推荐的方式。它可以产生固定频率的采样时钟,是构建数据采集系统的基础。例如,配置定时器每1ms触发一次ADC,那么你的采样频率就是稳定的1kHz。

第三步:确定数据搬运与处理方式

  • 搬运方式:毫无悬念,选择DMA。在CubeMX中配置DMA为循环模式(Circular),这样ADC就会源源不断地采样,DMA源源不断地将数据写入一个环形缓冲区,CPU只需定期去缓冲区读取处理好的数据即可。
  • 处理方式:ADC的原始值需要转换为电压值。公式很简单:电压 = (原始值 / 4095) * 参考电压Vref+。但一次采样值噪声很大,必须进行软件滤波。常用的有递推平均滤波(窗口滑动平均)、中值滤波、一阶滞后滤波等。在比赛中,递推平均滤波实现简单、效果明显,是性价比最高的选择。

注意:在CubeMX中配置DMA时,数据宽度要匹配。ADC是12位,结果通常存放在16位变量中。因此DMA的外设地址(ADC数据寄存器)宽度设为Half Word(半字,16位),内存地址(你的数组)宽度也设为Half Word。同时,要开启DMA的循环模式,并配置好数据长度(等于你规则序列的通道数,这里是2)。

3. 硬件连接与CubeMX工程配置详解

理论分析完毕,我们进入实战环节。假设我们使用STM32G431RB(蓝桥杯第十一届国赛指定板)的PA5和PA6作为两路ADC输入。

3.1 硬件电路注意事项

在将信号接入MCU之前,有几点硬件上的“坑”需要提前避开:

  1. 参考电压源:ADC的精度极大程度依赖于参考电压VREF+的稳定性。STM32G431的VREF+引脚默认内部连接到VDDA(模拟电源)。务必确保VDDA供电干净、稳定。在板子上,VDDA通常通过一个磁珠或0欧电阻从VCC(3.3V)隔离而来,并需要搭配一个10uF钽电容和一个0.1uF陶瓷电容进行去耦。在比赛中,如果发现ADC值跳动大,首先检查电源。
  2. 信号调理:如果输入信号可能超过ADC的量程(0-3.3V),必须增加钳位保护电路(如使用两个二极管钳位到地和VDDA)。如果信号源内阻很大,会导致采样电容充电时间不足,产生误差。必要时可以在输入端增加一个电压跟随器(运放)作为缓冲。
  3. PCB布局:模拟信号走线应尽量短,远离数字信号线(特别是时钟、PWM线),避免耦合噪声。在原理图上,通常会将ADC输入引脚标注为“ANALOG”以作提醒。

3.2 CubeMX图形化配置全流程

接下来,我们一步步使用STM32CubeMX完成所有初始化代码的生成。这是现代STM32开发的基础,务必熟练掌握。

1. 引脚配置在Pinout & Configuration视图下:

  • 找到PA5PA6,将其功能设置为ADC1_IN8ADC1_IN9(具体通道号需查数据手册,G431的PA5对应IN8,PA6对应IN9)。
  • 同时,检查VREF+VDDAVSSA的连接,确保它们已正确连接到电源和地。

2. ADC1配置在Analog -> ADC1中:

  • Resolution:选择12-bit。这是精度和速度的平衡点。
  • Scan Conversion ModeEnable。这是多通道扫描的关键。
  • Continuous Conversion ModeDisable。我们不希望ADC不停地转换,而是由定时器触发。
  • Discontinuous Conversion Mode:Disable。
  • DMA Continuous RequestsEnable。这样DMA请求会连续发生,配合循环DMA实现不间断传输。
  • End Of Conversion Selection:选择EOC after each conversion(每个通道转换后产生EOC)或EOC after sequence(整个序列转换后产生一个EOC)。对于DMA传输,通常选后者,让DMA在一次扫描完成后搬运数据。
  • Low Power Auto Wait:Disable。
  • Clock Prescaler:选择PCLK2 divided by 2by 4。ADC时钟(ADCCLK)不能超过手册规定的最大值(例如G431是60MHz)。根据系统时钟计算,选择合适的分频,确保ADCCLK不超频。
  • Sampling Time:这是关键参数!点击Channel 8Channel 9,为它们选择采样时间。采样时间 = (采样周期数 + 12.5) / ADCCLK。采样周期数越长,ADC内部的采样电容充电越充分,精度越高,但转换速度越慢。对于中等阻抗(<10kΩ)的信号源,选择47.5 Cycles92.5 Cycles是比较稳妥的。在比赛中,如果时间充裕,可以选长一些以求稳定。

3. 规则组配置在ADC1的Regular Conversion Mode下:

  • 点击Add,将Channel 8Channel 9加入规则组。
  • 可以调整它们的顺序,这决定了DMA搬运数据的数组顺序。
  • 每个通道的采样时间在这里也可以单独设置。

4. 触发源配置Trigger Source中,选择Timer 2 Trigger Out event。这意味着ADC将由TIM2的更新事件触发。

5. DMA配置DMA Settings中点击Add

  • DMA Request选择ADC1
  • Mode选择Circular(循环模式)。
  • Data Width选择Half Word(与ADC数据寄存器宽度一致)。

6. 定时器TIM2配置我们需要一个定时器来定期触发ADC。

  • 在Timers -> TIM2中,选择Clock SourceInternal Clock
  • Parameter Settings中:
    • Prescaler(预分频器):根据所需采样频率计算。假设系统时钟为80MHz,我们希望ADC每秒触发1000次(1kHz)。
    • 计算公式:定时器时钟 / ((Prescaler+1) * (Counter Period+1)) = 触发频率
    • 例如:定时器时钟80MHz,欲得1kHz。令Prescaler = 7999Counter Period = 9。则频率 = 80,000,000 / (8000 * 10) = 1000 Hz。
  • Trigger Output (TRGO) Parameters中,将Master Mode Selection设置为Update Event。这样,每次定时器更新(溢出)时,都会产生一个触发信号输出给ADC。

7. 时钟树配置检查系统时钟配置,确保给ADC和定时器的时钟源正确且未超频。

8. 生成代码在Project Manager中设置好项目名称、路径和IDE(如MDK-ARM),然后点击GENERATE CODE。CubeMX会生成所有初始化代码,我们在main.c的用户代码区添加应用逻辑即可。

4. 软件逻辑与滤波算法实现

CubeMX帮我们搭建好了舞台,现在需要我们来编写“剧本”。代码主要添加在main.c/* USER CODE BEGIN 2 *//* USER CODE BEGIN 3 */区域。

4.1 变量定义与初始化

首先,定义存储ADC数据的缓冲区。由于我们使用DMA循环模式,需要定义一个足够大的数组作为缓冲区。但为了简化,我们可以定义一个二维数组或两个一维数组,用于存放滤波后的最终数据。

/* USER CODE BEGIN PV */ #define ADC_BUFFER_SIZE 2 // 规则组里有2个通道 #define FILTER_WINDOW_SIZE 10 // 滤波窗口大小 volatile uint16_t adc_raw_buffer[ADC_BUFFER_SIZE]; // DMA会直接写入这个数组 uint32_t adc_channel1_sum = 0, adc_channel2_sum = 0; uint16_t adc_filtered_value1 = 0, adc_filtered_value2 = 0; uint8_t filter_index = 0; uint16_t filter_window1[FILTER_WINDOW_SIZE] = {0}; uint16_t filter_window2[FILTER_WINDOW_SIZE] = {0}; /* USER CODE END PV */

main函数的初始化部分(/* USER CODE BEGIN 2 */)启动ADC和DMA:

/* USER CODE BEGIN 2 */ // 启动ADC,配合DMA。参数:ADC句柄,目标数组,长度(转换次数) if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_raw_buffer, ADC_BUFFER_SIZE) != HAL_OK) { Error_Handler(); } // 启动触发ADC的定时器 HAL_TIM_Base_Start(&htim2); /* USER CODE END 2 */

4.2 主循环与数据处理

在主循环/* USER CODE BEGIN 3 */中,我们不再需要轮询ADC是否转换完成,因为DMA在后台默默工作。我们的任务是定期处理adc_raw_buffer中的数据,并进行滤波。

这里介绍一种高效的递推平均滤波算法。它不需要保存所有历史数据,只需一个累加和,每次减去最旧的值,加上最新的值。

/* USER CODE BEGIN WHILE */ while (1) { // 简单的延时,控制数据处理频率。这里假设定时器触发ADC是1kHz,我们每10ms处理一次数据(100Hz) HAL_Delay(10); // 1. 读取DMA缓冲区中的原始值(注意:DMA在持续更新它,但一次读取是原子的) uint16_t raw_ch1 = adc_raw_buffer[0]; // 对应规则组中第一个通道 uint16_t raw_ch2 = adc_raw_buffer[1]; // 对应规则组中第二个通道 // 2. 应用递推平均滤波 // 减去窗口中最旧的值 adc_channel1_sum -= filter_window1[filter_index]; adc_channel2_sum -= filter_window2[filter_index]; // 存入最新的值 filter_window1[filter_index] = raw_ch1; filter_window2[filter_index] = raw_ch2; // 加上最新的值 adc_channel1_sum += raw_ch1; adc_channel2_sum += raw_ch2; // 计算当前平均值 adc_filtered_value1 = adc_channel1_sum / FILTER_WINDOW_SIZE; adc_filtered_value2 = adc_channel2_sum / FILTER_WINDOW_SIZE; // 更新窗口索引 filter_index++; if (filter_index >= FILTER_WINDOW_SIZE) { filter_index = 0; } // 3. 将滤波后的值转换为电压(假设Vref=3.3V) float voltage_ch1 = (float)adc_filtered_value1 / 4095.0f * 3.3f; float voltage_ch2 = (float)adc_filtered_value2 / 4095.0f * 3.3f; // 4. 可以通过串口打印出来,或者用于其他控制逻辑 printf("CH1: %.3f V, CH2: %.3f V\r\n", voltage_ch1, voltage_ch2); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */

实操心得FILTER_WINDOW_SIZE(窗口大小)的选择是门艺术。窗口越大,滤波效果越好,噪声越小,但响应速度越慢(延迟增大)。在蓝桥杯比赛中,你需要根据题目要求权衡。如果题目要求快速响应变化,窗口就设小点(如5);如果要求测量稳定直流电压,窗口可以设大点(如20或50)。可以通过实验,观察不同窗口大小下,输出波形的平滑度和阶跃响应速度来决定。

4.3 进阶:定时器触发与DMA的完美配合

上述代码中,我们用了HAL_Delay(10)来模拟一个处理周期。在实际的高性能应用中,更好的做法是使用另一个定时器中断或者DMA传输完成半满/全满中断来触发数据处理。

使用DMA传输完成中断CubeMX配置DMA时,可以开启传输完成中断(TC)或半传输完成中断(HT)。这样,当DMA搬运了半缓冲区或整个缓冲区的数据后,会进入中断服务函数。我们可以在中断中设置一个标志位,然后在主循环中检查这个标志位来处理数据。这种方式数据处理更及时,且能精确知道数据块的时间戳。

代码片段示例(中断方式):

// 在CubeMX中使能DMA的传输完成中断 // 在stm32g4xx_it.c中找到DMA的中断服务函数 void DMA1_Channel1_IRQHandler(void) { if(__HAL_DMA_GET_FLAG(&hdma_adc1, DMA_FLAG_TC1)) { __HAL_DMA_CLEAR_FLAG(&hdma_adc1, DMA_FLAG_TC1); // 设置一个数据就绪标志 adc_data_ready = 1; } } // 在主循环中 while(1) { if(adc_data_ready) { adc_data_ready = 0; // 处理 adc_raw_buffer 中的数据... process_adc_data(); } // ... 其他任务 }

5. 精度提升技巧与常见问题排查

即使配置看起来都正确,ADC的读数也可能不尽如人意。下面分享一些提升精度和稳定性的实战技巧,以及常见问题的排查思路。

5.1 软件校准与硬件补偿

  1. 偏移与增益校准:STM32的ADC模块内部提供了偏移和增益校准寄存器。在上电初始化ADC之后、开始转换之前,可以调用HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);函数进行自校准。这个操作能显著减少内部电路引起的零漂和增益误差。
  2. 过采样技术:如果你不需要很高的采样率,但需要更高的分辨率(例如,想要得到14位或16位的有效结果),可以使用过采样。原理是以更高的频率采样,然后对多个样本进行累加和平均。例如,4倍过采样可以将信噪比提高6dB,等效于增加1位分辨率。STM32的ADC硬件支持过采样,可以在CubeMX中直接配置,非常方便。
  3. 参考电压测量:我们公式里的3.3V是理想值。实际上,VDDA可能不是精确的3.300V。如果你的系统对绝对精度要求极高,可以:
    • 使用一个外部高精度基准源(如REF3033)连接到VREF+引脚。
    • 或者,用单片机另一个已知精确的ADC通道(如果有多余的)去测量一个已知的精准电压(例如,通过电阻分压得到的1.65V),然后反推出实际的VREF+电压,用于后续计算。这是一种“自举”校准法。

5.2 典型问题排查清单

当你发现ADC值跳动大、不准或者完全不变化时,可以按照以下清单逐一排查:

问题现象可能原因排查方法
ADC值始终为0或接近01. 信号未接入或接地。
2. ADC通道配置错误。
3. ADC时钟未使能或配置错误。
4. 采样时间太短,电容未充电。
1. 用万用表测量输入引脚电压。
2. 核对CubeMX中引脚和通道映射。
3. 在System Core > RCC中确认ADC时钟源已使能,检查时钟树配置。
4. 大幅增加采样周期数(如设为640.5 Cycles)测试。
ADC值始终为4095(满量程)或接近1. 输入电压超过VDDA(3.3V)。
2. 引脚配置为输出模式或其他功能,而非模拟输入。
3. VREF+未正确连接(浮空)。
1. 测量输入电压。
2. 检查引脚配置模式是否为Analog
3. 检查原理图,确保VREF+和VDDA连接稳定电源。
ADC值跳动剧烈(噪声大)1. 电源噪声大,VDDA不干净。
2. PCB布局不佳,数字噪声耦合到模拟走线。
3. 信号源内阻大,采样时间不足。
4. 未进行软件滤波。
1. 用示波器观察VDDA波形,增加去耦电容。
2. 检查布线,模拟线远离数字线。
3. 增加采样周期数,或在信号源和ADC引脚间加一个小的滤波电容(如100pF)。
4. 实现并调整软件滤波算法。
两路ADC值互相干扰1. ADC通道间串扰。在扫描模式下,切换通道时内部电容残留电荷影响。
2. 共地噪声。
1. 在规则组序列中,在两个通道之间插入一个额外的延迟(ADC_ExternalTrigConvEdge_None?不,更常用的是在CubeMX中调整Sampling Time,给足放电时间)。或者,在软件上,采样后丢弃第一个值(作为稳定值)。
2. 确保模拟地和数字地单点连接良好。
DMA数据不更新或错乱1. DMA缓冲区长度配置错误。
2. DMA未配置为循环模式,传输一次后停止。
3. 数组越界。
1. 核对HAL_ADC_Start_DMA的第三个参数,应等于规则序列中的通道数。
2. 在CubeMX中确认DMA模式为Circular
3. 确保定义的数组大小足够,且访问索引在范围内。
定时器触发不工作1. 定时器未启动。
2. ADC触发源选择错误。
3. 定时器分频计算错误,频率太高或太低。
1. 确认调用了HAL_TIM_Base_Start
2. 核对CubeMX中ADC的External Trigger Source是否对应了正确的定时器(如Timer 2 TRGO)。
3. 重新计算定时器预分频和周期值,用示波器或调试器查看定时器更新标志。

5.3 调试技巧:利用Keil的Logic Analyzer

如果你使用Keil MDK,其内置的Logic Analyzer功能是调试ADC的利器。你不需要示波器,就能在IDE里看到ADC数据的波形。

  1. 在调试模式下运行程序。
  2. 点击View -> Analysis Windows -> Logic Analyzer
  3. 点击Setup,添加要观察的变量,如adc_filtered_value1。设置类型为unsigned int
  4. 运行程序,你就能看到这个变量随时间变化的曲线图。这对于观察滤波效果、测量响应速度、发现异常跳动非常直观。

最后,我想说的是,ADC的稳定性是“软硬结合”的成果。软件滤波能解决一部分噪声,但干净的硬件设计才是根本。在备赛或做项目时,多动手测试,用示波器看看电源和信号,用逻辑分析仪看看时序,用调试器看看变量,遇到问题对照清单耐心排查。当你成功获得两条稳定、准确的ADC曲线时,那种成就感,就是嵌入式开发最纯粹的乐趣所在。希望这篇长文能成为你征服蓝桥杯ADC题目的得力助手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询