尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 ADC实战:从基础原理到DMA与定时器触发的高效数据采集

STM32 ADC实战:从基础原理到DMA与定时器触发的高效数据采集 1. 项目概述从模拟世界到数字世界的桥梁做嵌入式开发尤其是和传感器、信号采集打交道你肯定绕不开ADC。ADC全称模数转换器是连接现实物理世界和数字计算世界的核心桥梁。它把连续变化的电压、电流这些模拟信号转换成单片机能够识别和处理的离散数字值。我最近在调试一个基于STM32的环境监测节点需要采集光照、温湿度等多个传感器的数据ADC的稳定性和精度直接决定了整个系统的可靠性。在这个过程中我重新梳理了一遍STM32的ADC模块从基础原理到实战配置踩了不少坑也总结了一些心得。这篇文章我就以一个典型的“STM32 ADC数模转换实验”为线索把整个流程掰开揉碎了讲清楚无论你是刚接触STM32的新手还是想优化现有采集系统的老手相信都能找到有用的东西。这个实验的核心目标很明确让STM32单片机能够正确、稳定地读取外部模拟电压并将其转换为我们可以编程使用的数字量。我们会从最基础的轮询模式开始逐步深入到更高效、更实用的DMA传输和定时器触发最后聊聊精度提升和常见故障排查。我会基于最常用的STM32F1系列和HAL库来讲解但原理和方法是通用的适配F4、H7等其他系列时只需稍作调整。准备好了吗我们开始。2. 核心原理与STM32 ADC模块解析2.1 ADC是如何工作的从采样到量化的微观过程很多人会用“尺子量长度”来类比ADC这个比喻很形象但我想再深入一层。假设你要测量一个不断波动的水位ADC的工作可以分为三步采样、保持、量化与编码。采样就像用相机快速给水位拍一张照片。奈奎斯特采样定理告诉我们拍照的频率采样率必须至少是水位波动最高频率的两倍才能完整记录其变化否则就会产生“混叠失真”导致采集到的数据完全失真。在STM32中ADC的时钟频率和采样周期共同决定了这个“拍照”的速度。保持就是在拍下照片的瞬间把当前的水位值“冻结”住以便后续仔细测量。ADC内部有一个采样保持电路来完成这个动作其保持电容的大小和漏电流会影响精度。量化与编码则是把“冻结”的水位高度对照一把有刻度的尺子参考电压Vref进行读数。这把尺子的最小刻度1 LSB决定了分辨率。对于一个12位的ADC尺子被分成了2^124096个刻度。如果Vref是3.3V那么1 LSB就对应约0.8mV3.3V / 4096。ADC最终输出的就是当前电压落在第几个刻度上的编号也就是我们读到的0-4095之间的数字值。注意这里的Vref非常关键。对于STM32Vref可能是独立的引脚Vref也可能是直接连接到VDDA模拟电源。务必确保Vref稳定、干净任何波动都会直接导致转换结果的整体缩放误差。2.2 STM32 ADC模块的关键特性与工作模式不同系列的STM32其ADC模块能力差异很大。以经典的STM32F103C8T6为例它通常有1-2个12位精度的ADC支持最多16个外部通道和2个内部通道温度传感器、内部参考电压。它的主要工作模式有几种单次转换模式触发一次转换一个通道然后停止。最简单适合低速、非连续采集。连续转换模式启动后ADC会不停地对一个通道进行转换。需要配合DMA或频繁读取数据寄存器否则数据会被覆盖。扫描模式这是多通道采集的核心。ADC会按照预置的序列比如通道1, 2, 3, ...自动依次转换多个通道。扫描模式可以和单次/连续模式结合形成“单次扫描”或“连续扫描”。触发源的选择是配置的难点和重点。触发决定了ADC何时开始一次转换。除了软件触发手动启动更常用的是硬件触发例如定时器触发用定时器的更新事件、比较匹配事件来精准地、周期性地触发ADC采样。这是实现固定采样率如1kHz的黄金标准能解放CPU并且采样间隔极其均匀对后续的信号处理如FFT至关重要。外部引脚触发可以用一个外部信号如GPIO上升沿来启动转换。转换时间的计算是配置的基础。总转换时间 采样时间 12.5个ADC时钟周期对于12位分辨率。采样时间是可以配置的单位是ADC时钟周期时间越长从外部信号源“汲取”电荷越充分精度越高但速度越慢。你需要根据信号源的内阻来权衡。官方数据手册会给出一个参考表格例如信号源内阻为10kΩ时建议采样时间不低于7.5个周期。3. 基础实战单通道轮询采集我们从最简单的开始让PA1引脚上的电压通过轮询的方式被读取并打印出来。3.1 硬件连接与CubeMX基础配置首先进行硬件连接。找一个电位器中间引脚接PA1另外两个引脚分别接3.3V和GND。这样旋转电位器PA1的电压就在0-3.3V之间变化。打开STM32CubeMX创建工程选择你的芯片型号。配置时钟树确保系统时钟和APB2总线时钟ADC挂载在此正确设置。ADC时钟不能超过芯片规定的最大值如STM32F1通常为14MHz。配置GPIO将PA1设置为模拟输入模式Analog Mode。这是必须的其他模式如浮空输入会导致采样不准确。配置ADC1在“Analog”标签下找到ADC1。“Resolution”选择12位。“Scan Conversion Mode”选择Disable单通道不用扫描。“Continuous Conversion Mode”选择Disable单次转换。“DMA Continuous Requests”选择Disable轮询模式不用DMA。“End of Conversion Selection”选择EOC flag after each conversion每次转换后产生标志。在“Channel Configuration”子标签为Rank1选择通道1对应PA1并设置一个合适的“Sampling Time”比如“239.5 Cycles”这个值较大能保证精度适合初学调试。配置串口为了打印数据配置一个USART如USART1为异步模式波特率115200。生成代码。3.2 轮询模式代码实现与解析在生成的工程中我们主要在主循环里添加代码。// 在main.c的私有变量区定义 ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart1; uint32_t adc_value 0; float voltage 0.0f; // 在main函数的while(1)循环中添加 HAL_ADC_Start(hadc1); // 启动ADC转换 if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) // 等待转换完成超时100ms { adc_value HAL_ADC_GetValue(hadc1); // 读取转换结果 voltage (float)adc_value * 3.3f / 4095.0f; // 计算实际电压值 printf(ADC Value: %lu, Voltage: %.3f V\r\n, adc_value, voltage); } HAL_Delay(500); // 延时500ms降低打印频率这段代码的逻辑非常直接启动-等待-读取-计算-打印。HAL_ADC_PollForConversion函数会阻塞在这里直到转换完成或超时。3.3f是你的参考电压如果你使用了外部精准基准源这里需要替换。实操心得在调试阶段可以用printf打印原始ADC值。如果旋转电位器时ADC值在0和4095之间平滑变化说明硬件和基础配置基本正确。如果值卡在0或4095不动首先检查硬件连接和GPIO模式如果值跳动剧烈可能是电源噪声或采样时间太短。轮询模式的致命缺点HAL_ADC_PollForConversion和HAL_Delay都是阻塞函数。在这几百毫秒里CPU几乎干不了别的活效率极低。任何对实时性有要求的系统都不能容忍这种浪费。因此这仅仅是入门验证真实项目必须升级到更高级的模式。4. 效率升级多通道扫描与DMA传输当需要采集多个传感器如温度、光照、电池电压时轮询模式就完全不可行了。这时扫描模式DMA的组合是标准解决方案。4.1 DMA原理与在ADC中的应用DMA直接存储器访问是一个可以独立于CPU工作的外设它的任务就是在两个地址之间搬运数据。对于ADC多通道扫描工作流程如下ADC配置为扫描模式并设置好通道序列Rank1: 通道1(温度), Rank2: 通道2(光照) ...。配置DMA将源地址设为ADC数据寄存器ADC-DR的地址目标地址设为我们定义的一个数组如uint16_t adc_buffer[4]。设置DMA为循环模式Circular数据宽度为半字对应12位数据。启动DMA和ADC的连续转换模式。此后每当ADC完成一个通道或所有通道的转换DMA会自动把数据搬到数组里完全不需要CPU干预。CPU可以定期来检查这个数组获取最新数据。4.2 CubeMX配置与代码实现在刚才的CubeMX工程基础上进行修改启用扫描模式在ADC1配置中将“Scan Conversion Mode”设置为Enabled。启用连续模式将“Continuous Conversion Mode”设置为Enabled。添加多个通道在“Channel Configuration”下为Rank1, Rank2, Rank3分别选择通道1, 2, 3对应PA1, PA2, PA3并设置采样时间。配置DMA在“DMA Settings”标签页点击Add。选择ADC1模式选择“Circular”循环模式。数据宽度都选择“Half Word”半字16位。增量地址外设地址不增量存储器地址增量。生成代码。在代码中// 私有变量区 uint16_t adc_dma_buffer[3]; // 存放3个通道的数据 // 在main函数初始化部分启动ADC和DMA HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, 3); // 主循环中可以直接使用adc_dma_buffer数组中的数据它会被DMA自动更新 while (1) { // 无需手动启动或读取ADC temp_voltage (float)adc_dma_buffer[0] * 3.3f / 4095.0f; // 通道1数据在索引0 light_voltage (float)adc_dma_buffer[1] * 3.3f / 4095.0f; // 通道2数据在索引1 // ... 处理数据 HAL_Delay(100); }关键细节HAL_ADC_Start_DMA函数调用后ADC和DMA就开始协同工作源源不断地把转换结果填入缓冲区。这里有一个非常重要的点DMA的传输完成中断Half/Full Transfer Complete。我们可以启用这些中断在DMA缓冲区半满或全满时通知CPU来处理一批数据这是实现高速、连续、无丢失采集的关键避免了CPU频繁轮询。5. 精准控制定时器触发采样对于需要固定采样频率的应用比如音频采集、振动分析软件延时或主循环控制采样的间隔是极不稳定的。这时必须使用定时器触发。5.1 定时器触发配置详解我们使用一个通用定时器如TIM2来产生一个固定频率的更新事件Update Event用这个事件作为ADC的外部触发源。CubeMX配置步骤配置定时器选择TIM2将其时钟源设为内部时钟。在“Parameter Settings”中设置“Prescaler”预分频器和“Counter Period”自动重装载值ARR。例如如果系统时钟72MHz我们希望产生1kHz的触发频率。预分频器设为72-1这样定时器时钟为1MHz。将ARR设为1000-1这样计数器从0数到999产生一个更新事件频率为1MHz / 1000 1kHz。配置ADC触发源回到ADC1配置在“External Trigger Conversion Source”中选择“Timer 2 Trigger Out event”。同时将“External Trigger Conversion Edge”选为“Rising Edge”上升沿触发。调整ADC模式由于由定时器精准触发我们可以将“Continuous Conversion Mode”改回Disable。每次定时器更新事件到来ADC就执行一次扫描转换或多个通道。5.2 代码整合与数据同步配置生成代码后启动顺序变得很重要// 启动定时器产生触发脉冲 HAL_TIM_Base_Start(htim2); // 启动ADC并等待外部触发 HAL_ADC_Start_IT(hadc1); // 或者使用HAL_ADC_Start_DMA // ADC转换完成中断回调函数如果用了中断 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 在这里读取或处理数据。由于触发是定时的这里被调用的频率也是固定的1kHz。 // 如果配合DMA可以在DMA传输完成中断里处理一批数据。 }这种模式下采样间隔由定时器硬件保证精度极高CPU只需在数据准备好后通过中断进行处理实现了精准的“采样-处理”流水线。6. 精度提升实战与常见问题深潜即使配置正确ADC读数也可能跳动、不准。下面分享几个提升精度和稳定性的实战技巧。6.1 电源、参考源与PCB布局的隐性影响1. 模拟电源VDDA/VSSA的纯净度 这是影响ADC精度的头号因素。必须确保VDDA由干净的LDO供电并且与数字电源VDD通过磁珠或0Ω电阻隔离。在VDDA和VSSA引脚附近务必放置一个10uF的钽电容和一个100nF的陶瓷电容进行退耦电容尽可能靠近引脚。2. 参考电压Vref 如果芯片有独立的Vref引脚务必连接一个外部精密基准源如REF30332.5ppm/°C这比使用内部VDDA作为参考要稳定得多。即使使用VDDA也要测量其实际电压而不是简单地认为它是3.300V。3. PCB布局走线 模拟信号线连接到ADC输入引脚的走线要远离数字信号线特别是时钟、PWM线最好用地线包裹隔离。模拟地AGND和数字地DGND通常采用单点连接。6.2 软件滤波算法从简单到实用硬件是基础软件滤波则是消除随机噪声的利器。这里介绍几种常用方法1. 均值滤波 最简单连续采样N次求和后取平均。能有效抑制白噪声但会降低响应速度。#define SAMPLE_TIMES 32 uint32_t adc_sum 0; for(int i0; iSAMPLE_TIMES; i) { adc_sum HAL_ADC_GetValue(hadc1); // 可以加一个小延时避免采样间隔过近 } uint16_t adc_average adc_sum / SAMPLE_TIMES;2. 滑动平均滤波 维护一个固定长度的队列每次新采样值进入最老的采样值移出始终计算队列的平均值。兼顾了平滑度和实时性。#define FILTER_LEN 10 uint16_t adc_buf[FILTER_LEN] {0}; uint8_t buf_index 0; uint32_t buf_sum 0; // 每次采样后调用 buf_sum - adc_buf[buf_index]; // 减去即将被覆盖的旧值 adc_buf[buf_index] HAL_ADC_GetValue(hadc1); buf_sum adc_buf[buf_index]; // 加上新值 buf_index (buf_index 1) % FILTER_LEN; uint16_t adc_filtered buf_sum / FILTER_LEN;3. 中值滤波 连续采样N次N为奇数然后取这N个值的中间值。对脉冲干扰尖峰噪声有奇效。#define MEDIAN_LEN 5 // 必须是奇数 uint16_t adc_temp[MEDIAN_LEN]; for(int i0; iMEDIAN_LEN; i) adc_temp[i] HAL_ADC_GetValue(hadc1); bubble_sort(adc_temp, MEDIAN_LEN); // 需要一个简单的排序函数 uint16_t adc_median adc_temp[MEDIAN_LEN/2];在实际项目中我常常将中值滤波和滑动平均滤波结合先用中值滤波去掉可能的异常尖峰再用滑动平均进行平滑效果非常好。6.3 典型故障排查清单当你发现ADC值不对时可以按以下清单逐项排查现象可能原因排查方法读数始终为0或接近01. GPIO未配置为模拟模式。2. 外部信号电压确实为0。3. ADC未正确启动或触发。4. DMA配置错误数据未搬运到正确内存。1. 检查CubeMX GPIO配置。2. 用万用表测量输入引脚电压。3. 单步调试检查HAL_ADC_Start等函数返回值。4. 检查DMA缓冲区地址和传输大小。读数始终为4095或接近满量程1. 输入电压超过Vref。2. 输入引脚浮空或外部电路开路。3. Vref引脚接地或电压异常低。1. 测量输入电压。2. 检查外部电路确保信号源正常工作。3. 测量Vref引脚电压。读数跳动剧烈噪声大1. 采样时间太短。2. 模拟电源噪声大。3. 信号源内阻高驱动能力不足。4. 电路板布局不佳受到数字干扰。1. 增加ADC采样周期数。2. 用示波器观察VDDA和输入信号添加滤波电容。3. 在ADC输入前增加电压跟随器运放。4. 检查布线模拟线远离数字线。多通道采集通道间互相干扰1. ADC内部通道切换时的电荷注入效应。2. 扫描序列中相邻通道采样时间不足。1. 在扫描序列中在两个实际通道之间插入一个“哑元通道”如连接到GND的通道让ADC有时间稳定。2. 适当增加所有通道的采样时间。DMA模式数据错位DMA传输的数据宽度或内存增量设置错误。检查CubeMX中DMA配置ADC数据寄存器是16位半字缓冲区应为uint16_t数组内存地址增量需开启。一个我踩过的坑曾经用DMA做多通道采集发现第一个通道数据总是对的后面通道的数据像是随机值。折腾了半天才发现在CubeMX中配置DMA时“Data Width”选项外设和存储器都必须选择“Half Word”16位。如果存储器端误选为“Word”32位DMA会以32位为单位搬运数据导致内存地址错乱数据全乱了套。这个错误非常隐蔽因为编译和运行都不会报错。7. 进阶话题内部通道与校准7.1 内部温度传感器与Vrefint的妙用STM32芯片内部集成了两个非常有用的ADC通道温度传感器通道可以粗略测量芯片结温。公式通常为温度(°C) (V_sense - V_25) / Avg_Slope 25。其中V_25是25度时的传感器电压Avg_Slope是温度系数这些值在芯片数据手册的电气特性章节。注意这个传感器精度不高±1°C到±5°C适合监测芯片大致温升不能做高精度温度测量。内部参考电压通道Vrefint这是一个出厂时校准过的、相对稳定的内部基准电压约1.2V。它的最大用途是计算实际的VDDA电压从而补偿因电源波动带来的ADC误差。计算实际VDDA的流程在代码中读取Vrefint通道的ADC原始值ADC_RAW_VREFINT。从芯片的系统存储区通常在0x1FFFF7BA地址具体见参考手册读取出厂校准值VREFINT_CAL这个值是在VDDA3.3V、特定温度下测量并存储的。计算实际VDDAV_DDA_ACTUAL 3.3V * VREFINT_CAL / ADC_RAW_VREFINT。用这个V_DDA_ACTUAL去计算其他外部通道的实际电压可以显著提高测量精度尤其当电池供电电压下降时。7.2 ADC校准流程解析STM32的ADC模块存在偏移和线性度误差。上电后执行一次校准可以显著减少这些误差。HAL库提供了校准函数// 在ADC初始化之后启动之前执行校准 HAL_ADCEx_Calibration_Start(hadc1);这个函数内部会执行以下操作将ADC置于一种特定的校准模式。进行一系列内部转换来计算校准系数。将计算出的系数写入ADC的校准寄存器。重要提示每次芯片上电或ADC模块的供电/时钟发生重大变化后都应重新校准。校准期间不能进行任何转换操作。对于多ADC模式如双ADC交替采样每个ADC都需要单独校准。8. 项目实战构建一个简易数字电压表让我们把上面所有的知识点串起来做一个综合性的小项目一个用STM32和OLED屏幕实现的简易数字电压表要求能测量0-3.3V电压显示数值和一个小条形图并且通过按键切换测量模式单次/连续。硬件清单STM32F103C8T6核心板0.96寸OLED显示屏I2C接口电位器一个用于提供可变电压两个按键电阻、杜邦线若干软件设计要点ADC配置采用单通道PA112位分辨率使用定时器2以100Hz频率触发单次转换。这样既能保证采样率稳定又比连续模式更省电。DMA配置使用DMA在每次转换完成后将数据搬运到一个uint16_t的变量中。采用“单次传输”模式每次定时器触发转换完成DMA搬运一次。滤波算法在DMA传输完成中断中将新数据填入一个长度为5的滑动平均滤波器队列并计算当前平均值。电压计算与显示在主循环中读取滤波后的ADC值利用内部Vrefint通道计算当前精确的VDDA电压再换算出输入引脚的实际电压。将电压值和对应的条形图用*号长度表示通过I2C发送到OLED显示。按键处理一个按键用于切换“单次测量”按一次采一次和“连续测量”自动以100Hz采样模式。这可以通过使能或失能定时器触发来实现。另一个按键用于清零最大值/最小值记录。核心代码片段DMA中断处理与电压计算// 私有变量 uint16_t adc_raw 0; uint16_t adc_filter_buf[5] {0}; uint8_t filter_idx 0; uint32_t filter_sum 0; float actual_vdda 3.3f; // 初始假设值 // 在初始化中读取Vrefint校准值并启动ADC校准 uint16_t vrefint_cal *((uint16_t *)0x1FFFF7BA); // F1系列地址 HAL_ADCEx_Calibration_Start(hadc1); HAL_TIM_Base_Start(htim2); // 启动定时器触发 // ADC DMA传输完成中断回调函数每次转换完成触发一次 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { // 读取DMA目标地址的值即最新的ADC数据 // 注意如果是多通道这里需要解析缓冲区 adc_raw adc_dma_buffer; // 假设是单通道DMA // 滑动平均滤波 filter_sum - adc_filter_buf[filter_idx]; adc_filter_buf[filter_idx] adc_raw; filter_sum adc_raw; filter_idx (filter_idx 1) % 5; } // 在主循环中定期计算并显示 void update_voltage_display(void) { uint16_t filtered_adc filter_sum / 5; // 每隔一段时间如1秒重新计算实际VDDA static uint32_t vdda_update_tick 0; if(HAL_GetTick() - vdda_update_tick 1000) { // 切换到Vrefint通道读取其原始值这里需要临时改变ADC通道简化起见略去切换过程 uint16_t adc_vrefint_raw read_vrefint_channel(); if(adc_vrefint_raw ! 0) { actual_vdda 3.3f * vrefint_cal / adc_vrefint_raw; } vdda_update_tick HAL_GetTick(); } // 计算输入电压 float input_voltage (float)filtered_adc * actual_vdda / 4095.0f; // 在OLED上显示 input_voltage 和条形图 // ... OLED显示代码 }通过这个项目你将实战运用定时器触发、DMA传输、软件滤波、内部参考电压校准以及OLED驱动等多个知识点。当旋转电位器看到屏幕上的电压值平稳变化条形图随之舞动时你会对STM32的ADC有一个全面而深刻的理解。这远比单纯阅读手册要来得扎实。
返回列表