十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 ADC从入门到精通:配置、DMA采集与滤波实战

STM32 ADC从入门到精通:配置、DMA采集与滤波实战 1. 项目概述从模拟世界到数字世界的桥梁做嵌入式开发尤其是涉及到传感器数据采集、电池电压监控或者音频信号处理时我们总会遇到一个核心问题如何让只认识0和1的微控制器去理解和处理现实世界中连续变化的电压信号这就是ADC模数转换器要干的活儿。STM32作为业界最流行的ARM Cortex-M内核微控制器家族其内置的ADC外设功能强大且灵活但相应的配置选项也多新手很容易在采样率、分辨率、触发方式这些参数上绕晕。这个实验的核心就是带你亲手打通STM32的ADC功能把外部一个可变的模拟电压比如用电位器分压得到的转换成单片机内部可以处理的数字值并通过串口打印出来观察。这不仅是学习STM32的必修课更是后续所有涉及数据采集、智能控制项目的基石。无论你是刚接触STM32的学生还是需要快速回顾ADC要点进行产品开发的工程师通过这个从原理到代码、从配置到调试的完整流程你都能建立起清晰、实用的认知避开那些我早年踩过的坑。2. ADC核心原理与STM32的ADC外设解析2.1 ADC是如何工作的从连续到离散的魔法要玩转ADC不能只停留在调用库函数的层面得先明白它底层是怎么“变魔术”的。模拟信号在时间上和幅度上都是连续的而数字信号是离散的。ADC的转换过程可以通俗地理解为“量化”和“编码”两步。想象一下你要用一把刻度尺去测量一段连续变化的长度。首先你需要确定尺子的最小刻度比如1毫米这就是分辨率对应ADC的位数。一个12位的ADC意味着它能把参考电压范围分成 2^12 4096 个离散的等级。如果参考电压是3.3V那么每个数字等级1 LSB代表的电压就是 3.3V / 4096 ≈ 0.8mV。你测得的长度值最终会以最接近的那个刻度值来报告这个过程就是量化。量化必然带来误差这个误差就是量化误差理论上最大为 ±0.5 LSB。STM32的ADC主要采用逐次逼近型SAR架构。它的工作原理很像天平称重先用一半的参考电压相当于最大的砝码与输入电压比较如果输入电压更大则保留这个“砝码”对应数字位输出1并加上一半的剩余电压下一个更小的砝码继续比较如果更小则丢弃这个“砝码”输出0用剩余电压的一半继续比较。如此反复从最高位MSB到最低位LSB逐位确定最终得到一串二进制数字这就是编码。注意量化误差是原理性误差无法消除。高分辨率ADC可以减少这个误差但成本、转换时间也会增加。对于STM32常见的12位ADC在要求不高的场合完全够用但在需要高精度测量如精密仪表时需要额外注意基准电压源、PCB布局和软件滤波。2.2 STM32 ADC的关键特性与模式选择不同系列的STM32如F1、F4、H7其ADC外设能力有差异但核心概念相通。这里以最常见的STM32F1/F4系列为例解析几个你必须掌握的关键特性。1. 分辨率与转换时间分辨率有6位、8位、10位、12位可选通常我们用12位以获得最佳精度。转换时间是一个关键参数它决定了ADC能跑多快。转换时间 采样时间 逐次逼近时间固定为12.5个ADC时钟周期。采样时间可以软件配置单位是ADC时钟周期时间越长采样保持电容充电越充分对抗输入信号源阻抗的能力越强结果越准确但整体转换速率就越慢。你需要根据信号频率和精度要求来权衡。2. 扫描模式与连续模式这是配置时最容易混淆的两个概念。扫描模式Scan Mode针对多通道采集。使能后ADC会按照规则序列寄存器SQR里设定的通道顺序一个接一个地自动转换。单次扫描完成后停止。连续模式Continuous Mode针对单通道或多通道的循环采集。使能后ADC在完成一次或一轮扫描转换后立即自动开始下一次转换周而复始除非你手动停止它。通常如果是多通道采集我们会使能扫描模式。至于是否使能连续模式则取决于你需要单次触发还是持续不断地获取数据。3. 触发源ADC转换可以由软件触发调用HAL_ADC_Start也可以由硬件事件触发这才是ADC发挥威力的地方。硬件触发源包括定时器触发这是最常用的方式。可以用一个定时器的更新事件UEV、比较匹配事件CHx等来精确地、周期性地触发ADC采样。例如做音频采样时就需要一个精确的定时器来产生固定的采样率如44.1kHz。外部引脚触发通过特定的GPIO引脚上升沿/下降沿来触发适合响应外部事件。4. 数据对齐与DMA12位的转换结果默认是右对齐的存放在一个16位的数据寄存器里。你也可以选择左对齐方便后续进行某些计算。当进行高速、多通道连续采样时CPU频繁进入中断去读取数据会成为瓶颈并且可能丢失数据。此时必须启用DMA直接存储器访问。DMA可以在ADC转换完成后自动将数据搬运到你指定的内存数组Buffer中搬运完成后才通知CPU极大减轻CPU负担实现高效稳定的数据流。3. 实验环境搭建与CubeMX基础配置3.1 硬件连接与电路设计要点我们以一个最简单的实验为例使用一个10kΩ的多圈电位器将其两端分别接到3.3V和GND中间抽头滑动端接到STM32的某个ADC输入通道引脚例如PA0。这样旋转电位器PA0上的电压就在0~3.3V之间线性变化。实操心得别小看这个简单的电路。首先确保你的3.3V电源是干净的噪声要小。如果直接从板载的LDO取电要留意当数字部分功耗大时3.3V上可能会有毛刺影响ADC读数稳定性。对于精度要求高的场合建议使用独立的基准电压芯片如REF3033为ADC的参考电压引脚VREF供电。其次如果信号源阻抗较高比如用长导线连接可以在ADC输入引脚对地加一个几十到几百皮法的小电容起到滤波和提供瞬时电荷的作用但电容太大会影响信号变化速度。硬件清单STM32最小系统板如STM32F103C8T6蓝色小板ST-Link V2调试下载器10kΩ电位器一个杜邦线若干电脑安装Keil MDK或STM32CubeIDE3.2 使用STM32CubeMX进行图形化配置STM32CubeMXHAL库极大地简化了配置过程但理解其生成的代码至关重要。创建项目与选择ADC引脚打开CubeMX选择你的MCU型号。在Pinout视图中找到你想用的ADC通道比如PA0将其功能设置为ADC1_IN0。CubeMX会自动将其配置为模拟输入模式这是正确的ADC引脚必须设为模拟输入以避免额外的电流消耗和干扰。配置ADC参数在左侧“Analog”分类下找到ADC1。Mode选择“Independent mode”独立模式。对于双ADC的复杂模式如交替、同步初期先不用。Parameter Settings标签页Resolution选择“12-bit”12位分辨率。Scan Conversion Mode因为我们只用一个通道选择“Disabled”。如果是多通道则需Enable。Continuous Conversion Mode选择“Disabled”。我们采用软件单次触发。Discontinuous Conversion Mode禁用。DMA Continuous Requests禁用单次转换不需要DMA。End Of Conversion Selection选择“EOC after each conversion”每次转换后产生EOC事件。Analog WatchDog模拟看门狗可以先禁用。它的作用是当转换结果高于或低于你设定的阈值时触发中断常用于电池电压监控。Sampling Time在“Channel Configuration”子标签下选择我们配置的Rank1规则序列中的第一个设置其Sampling Time。对于电位器这种慢速信号选择“239.5 Cycles”可以获得很好的噪声抑制效果。对于高速信号则需要减少这个时间。配置时钟树ADC的时钟ADCCLK由APB2时钟分频而来。STM32F1的ADC时钟最高14MHzF4系列更高。确保你的ADCCLK不超过芯片手册规定的最大值。在Clock Configuration页面合理配置系统时钟和APB2分频使ADCCLK在安全范围内。生成代码在Project Manager页面设置好工程名、路径、IDEKeil或CubeIDE然后点击“Generate Code”。CubeMX会生成完整的初始化代码。4. 软件编程从单次采样到DMA连续采集4.1 基础单次采样与软件触发生成了代码后我们打开工程在main.c的用户代码区编写采集逻辑。// 在main函数初始化部分之后进入主循环之前可以开始一次转换 HAL_ADC_Start(hadc1); // 启动ADC转换软件触发 // 等待转换完成超时时间设为10ms if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { // 读取转换结果12位右对齐所以值范围是0-4095 uint16_t adc_value HAL_ADC_GetValue(hadc1); // 将数字值转换为电压值 (假设Vref 3.3V) float voltage (float)adc_value / 4095.0f * 3.3f; // 通过串口打印出去 printf(ADC Value: %d, Voltage: %.3f V\r\n, adc_value, voltage); } HAL_ADC_Stop(hadc1); // 停止ADC这段代码放在while(1)循环里就能不断采样并打印。但HAL_ADC_PollForConversion是阻塞式的会占用CPU时间等待。在实际产品中更推荐使用中断方式或DMA方式。4.2 中断方式采集中断方式避免了CPU空等转换完成后自动调用回调函数。在CubeMX中使能ADC全局中断在ADC配置的NVIC Settings标签页勾选ADC1 and ADC2 global interrupts。编写代码// 启动ADC中断模式转换 HAL_ADC_Start_IT(hadc1); // 转换完成中断回调函数需重写 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { uint16_t adc_value HAL_ADC_GetValue(hadc1); // 处理数据例如存入缓冲区 // 注意在中断回调函数中做耗时操作如printf是坏习惯 // 通常只设置标志位或拷贝数据。 g_adc_value adc_value; g_conversion_done_flag 1; } } // 在主循环中检查标志位 while (1) { if (g_conversion_done_flag) { g_conversion_done_flag 0; float voltage (float)g_adc_value / 4095.0f * 3.3f; printf(Voltage: %.3f V\r\n, voltage); // 如果需要连续采样再次启动中断转换 HAL_ADC_Start_IT(hadc1); } // ... 其他任务 }4.3 DMA多通道连续采集实战精华这是工业级应用中最常用的模式。假设我们需要以10kHz的速率循环采集通道0PA0和通道1PA1的电压。CubeMX配置在ADC1的Parameter Settings中使能Scan Conversion Mode和Continuous Conversion Mode。在DMA Settings标签页点击Add选择ADC1模式选择Circular循环模式数据宽度都选择Word半字对应16位。这样DMA会周而复始地将数据搬运到指定数组。在Rank配置中添加两个Rank分别选择通道0和通道1设置相同的采样时间。配置一个定时器如TIM2来触发ADC。在ADC1的Trigger Configuration中选择Trigger Source为Timer 2 Trigger Out event。然后去配置TIM2使其产生一个10kHz的更新事件例如系统时钟72MHz分频后计数到7200-1更新一次即可得到10kHz频率。软件编写// 定义缓冲区DMA会循环填充它 // 我们定义两个通道每个通道的数据是uint16_t缓冲区长度设为每个通道10个样本 #define ADC_BUFFER_SIZE 20 // 2通道 * 10样本 uint16_t adc_buffer[ADC_BUFFER_SIZE] {0}; // 在main函数初始化后启动DMA HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE); // 然后启动定时器ADC便会以10kHz的速率被定时器自动触发DMA自动搬运数据。 // 主循环中我们可以直接访问adc_buffer来获取最新数据 // 缓冲区排列顺序取决于Rank顺序假设Rank1是CH0Rank2是CH1 // 那么 buffer[0] CH0样本1, buffer[1] CH1样本1, buffer[2] CH0样本2, buffer[3] CH1样本2... while (1) { // 计算最近一次CH0和CH1的转换结果平均值 uint32_t sum_ch0 0, sum_ch1 0; for (int i 0; i 10; i) // 取10个样本平均 { sum_ch0 adc_buffer[i * 2]; // 偶数索引是CH0 sum_ch1 adc_buffer[i * 2 1]; // 奇数索引是CH1 } float voltage_ch0 (float)(sum_ch0 / 10) / 4095.0f * 3.3f; float voltage_ch1 (float)(sum_ch1 / 10) / 4095.0f * 3.3f; // 打印或处理... HAL_Delay(100); // 每100ms处理一次 }核心技巧使用Circular模式的DMA时缓冲区就像一个“滑动的窗口”CPU可以随时从里面读取最新的数据而无需担心数据覆盖冲突因为DMA的写指针和CPU的读指针是独立运行的。这是实现高速实时数据流处理的关键。5. 数据滤波与校准让读数更精准可靠直接从ADC读出的值往往是“毛躁”的跳动几个LSB是常有的事。硬件上的噪声无法完全消除但我们可以通过软件滤波来获得更稳定、更可信的结果。5.1 常用软件滤波算法算术平均滤波最简单有效。连续采样N次取平均值。N越大平滑效果越好但响应速度越慢。适用于一般性信号。#define N 10 uint32_t filter_average(uint16_t new_sample) { static uint16_t buf[N] {0}; static uint8_t index 0; static uint32_t sum 0; sum - buf[index]; // 减去最旧的值 buf[index] new_sample; // 存入新值 sum new_sample; // 加上新值 index (index 1) % N; // 更新索引 return sum / N; // 返回平均值 }滑动平均滤波与算术平均类似但通常用一个固定长度的队列FIFO来实现每次加入新值并剔除旧值计算当前队列内所有值的平均。上述代码其实就是滑动平均的一种实现。中值滤波对采样窗口内的数据排序取中间值。对脉冲性干扰偶尔的尖峰毛刺有奇效。比如采样5次排序后取第三大的值作为输出。uint16_t filter_median(uint16_t new_sample) { static uint16_t buf[5] {0}; static uint8_t idx 0; uint16_t temp_buf[5]; buf[idx] new_sample; if (idx 5) idx 0; // 拷贝到临时数组排序 memcpy(temp_buf, buf, sizeof(buf)); bubble_sort(temp_buf, 5); // 需要一个简单的排序函数 return temp_buf[2]; // 返回中值 }一阶滞后滤波低通滤波Y(n) α * X(n) (1-α) * Y(n-1)。其中α是滤波系数0α1X(n)是新采样值Y(n-1)是上次滤波输出。α越小滤波越平滑但滞后越严重。计算量小适合MCU。#define ALPHA 0.2f // 滤波系数 float filter_lowpass(float new_sample) { static float output 0; output ALPHA * new_sample (1 - ALPHA) * output; return output; }经验之谈没有一种滤波算法是万能的。对于缓慢变化的信号如温度、电池电压滑动平均或一阶滞后滤波效果很好。对于存在突发尖峰的信号如按键检测可能伴有抖动中值滤波更适合。在实际项目中我常常会组合使用例如先进行中值滤波去除野值再进行滑动平均进一步平滑。5.2 ADC的校准与参考电压要让ADC读数准确除了滤波校准也至关重要。内部参考电压STM32芯片内部有一个参照电压VREFINT连接到ADC的一个特殊通道如STM32F103是通道17。这个电压在出厂时经过校准典型值约1.2V但每个芯片有细微差异其精确值存放在芯片的系统存储区Flash特定地址。我们可以通过读取这个内部参考电压通道的值来反推当前实际的VDDA模拟供电电压是多少从而对其他通道的测量值进行比例校准消除因VDDA波动带来的误差。这是提高精度的低成本方法。偏移校准HAL库提供了HAL_ADCEx_Calibration_Start()函数用于执行ADC的偏移校准。它应该在ADC初始化之后、开始转换之前调用。校准过程会测量内部电路的误差并存储校准值后续转换会自动补偿。特别注意校准前必须确保ADC已经上电并稳定一段时间并且不要有任何外部信号输入到被校准的ADC通道。6. 典型问题排查与调试技巧实录即使配置看起来正确ADC也可能出现各种“诡异”现象。下面是我在多年调试中总结的一些常见问题及解决方法。6.1 问题一ADC读数不稳定跳动大现象输入一个固定电压读出的数字值在±10个LSB甚至更大范围内跳动。排查思路检查电源和地这是首要怀疑对象。用示波器查看模拟电源VDDA和地VSSA的波形看是否有明显的噪声或纹波。确保VDDA通过磁珠或电感与数字电源VDD隔离并接有足够的去耦电容如10uF钽电容100nF陶瓷电容靠近MCU引脚。检查信号源信号源本身是否稳定电位器接触不良也会导致跳动。可以尝试用稳压源直接给一个固定电压测试。调整采样时间增加采样时间给采样保持电容更充分的充电时间特别是当信号源阻抗较高时。这是软件上最直接的改善手段。检查PCB布局模拟信号走线要远离数字信号线、时钟线等高速噪声源。最好在模拟部分周围铺上完整的模拟地平面。软件滤波如上节所述施加合适的软件滤波。6.2 问题二ADC读数永远为0或4095满量程现象无论输入电压如何变化读出的值始终是0x0000或0x0FFF。排查思路引脚配置错误确认GPIO是否已正确配置为模拟输入模式。如果配置成了推挽输出等模式ADC无法正常工作。参考电压问题检查VREF和VREF-通常VREF-接VSSA的连接。如果VREF悬空或接地可能导致此现象。对于没有独立VREF引脚的型号VDDA即作为参考正端。ADC未正确启动或触发检查HAL_ADC_Start或HAL_ADC_Start_IT或HAL_ADC_Start_DMA是否被成功调用。如果是硬件触发检查触发源如定时器是否已使能并正常工作。DMA溢出在DMA模式下如果DMA缓冲区设置太小或者CPU处理速度跟不上DMA填充速度可能导致数据被覆盖读出异常值。检查DMA缓冲区大小和CPU处理逻辑。6.3 问题三多通道采样通道间相互串扰现象当只有一个通道有输入信号时其他无信号的通道读数也会跟着变化。排查思路采样时间不足这是最主要的原因。在切换ADC多路复用器的通道时需要足够的时间让内部电容放电并建立新通道的电压。必须增加采样时间尤其是在高阻抗信号源的情况下。可以尝试将采样时间设置为所有可用选项中的最大值。通道切换顺序如果可能在扫描序列中将高阻抗通道安排在低阻抗通道之后并确保有足够的时间间隔通过采样时间控制。6.4 调试利器利用串口与逻辑分析仪串口打印最基础的调试方法。将原始ADC值、计算后的电压、滤波后的值实时打印出来可以直观看到数据变化趋势和问题。注意打印频率不要太高以免影响程序实时性。逻辑分析仪/示波器高级调试必备。可以抓取ADC的触发信号如定时器输出、转换完成信号EOC以及DMA请求信号精确分析时序是否满足要求转换间隔是否恒定有没有丢失触发脉冲等。这对于调试基于定时器触发的DMA连续采集模式至关重要。ADC的稳定性是嵌入式系统感知外界的基础它往往需要软硬件协同优化。从原理理解到配置实践再到问题排查每一步都藏着细节。我个人的体会是把ADC玩透你的嵌入式项目就成功了一半。下次当你需要采集温度、压力、光照或者音频时你会感谢现在耐心调试ADC的自己。最后一个小技巧在最终产品中如果ADC精度要求极高不妨在代码初始化后加入一个短暂的延时比如100ms让模拟电源和ADC内部电路完全稳定再进行校准和采样往往会有意想不到的改善效果。
返回列表