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

资讯详情

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

STM32F103C8T6 ADC配置全解析:从硬件原理到软件滤波实战

STM32F103C8T6 ADC配置全解析:从硬件原理到软件滤波实战 1. 项目概述为什么STM32F103C8T6的ADC值得深挖如果你手头正好有一块“蓝色药丸”Blue PillSTM32F103C8T6最小系统板的昵称并且想用它来读取一个电位器的电压、监测电池电量或者采集传感器信号那么ADC模数转换器绝对是你绕不开的第一个核心外设。我接触过很多初学者拿到这块芯片后面对数据手册里ADC那一章复杂的时序图、一堆的寄存器往往感到无从下手。网上教程虽多但要么是简单的库函数调用知其然不知其所以然要么是过于底层的寄存器操作让人望而生畏。结果就是代码看似能跑但ADC数值抖动得厉害采样速度上不去或者多通道切换时数据错乱。STM32F103C8T6内部集成了2个12位的ADC最高采样率可达1MHz支持多达16个外部通道具体取决于型号和封装还支持扫描、间断、注入、DMA等多种高级模式。它的性能对于大多数嵌入式传感应用来说绰绰有余但要想把它用“稳”、用“精”里面门道不少。比如ADC的参考电压怎么选直接用VDD3.3V稳不稳采样时间设置多少才合适如何用DMA实现后台不间断采集而不占用CPU采集到的数据跳动怎么办该用哪种滤波函数这些问题都是把一个ADC功能从“能用”提升到“好用”甚至“可靠”的关键。这篇文章我就以一个多年嵌入式开发者的视角带你彻底吃透STM32F103C8T6的ADC。我们不只讲怎么配置更要讲清楚为什么这么配置背后的硬件原理是什么不同配置会带来什么影响。我会结合常见的电位器电压读取、多路温度传感器巡检等场景从原理图设计、CubeMX配置、代码编写到数据处理给出完整的、可落地的方案并分享那些在官方手册里找不到的“踩坑”经验和调试技巧。2. 核心硬件原理与设计要点在写第一行代码之前理解ADC的硬件原理和设计约束能帮你避开至少一半的坑。STM32F103C8T6的ADC模块是SAR逐次逼近型架构这是一种在精度、速度和成本之间取得很好平衡的经典结构。2.1 ADC参考电压的选择与优先级这是影响ADC精度最根本的因素没有之一。STM32F103C8T6的ADC模块有专门的**VREF和VREF-**引脚在64脚及以上的封装中独立引出。但在我们常用的48脚或更小的封装如LQFP48中VREF内部连接到VDDA模拟电源VREF-内部连接到VSSA模拟地。注意VDDA和VSSA必须被妥善处理。一个常见的错误是直接将VDDA接到数字电源VDD3.3V并且忽略VSSA的连接。这会导致数字电路的高频噪声串扰到敏感的模拟供电上引起ADC读数底噪增大、数值跳动。正确的设计应该是电源隔离即使使用同一个3.3V LDO也建议使用磁珠或0欧电阻将VDD和VDDA在物理上隔开并在VDDA引脚就近放置一个10uF的钽电容和一个100nF的陶瓷电容到VSSA形成RC滤波。参考电压源对于精度要求高的应用如电子秤、高精度测温强烈建议使用外部基准电压芯片如REF3033输出3.3V单独为VDDA/REF供电。这时ADC的输入范围就是0V至这个基准电压如3.3V其稳定性和噪声水平直接决定了ADC的精度上限。优先级当存在外部基准时其优先级最高。如果没有则使用VDDA。在代码中我们需要通过配置ADC的参考电压选择如果支持或确保硬件连接正确来体现这一选择。我的实操心得在一般的电池供电设备中我用一个低压差线性稳压器LDO如AMS1117-3.3同时给VDD和VDDA供电但在两者之间串一个10uH的磁珠VSS和VSSA在芯片下方单点连接。对于电池电压监测这种本身精度要求不极致的场景完全够用。但如果做的是一个需要校准的测量仪表外部基准芯片是必须的。2.2 采样时间与输入阻抗的匹配ADC不是瞬间完成采样的。它内部有一个采样保持电容需要时间通过外部信号源的阻抗对其充电到稳定值。这个时间就是采样时间。STM32的ADC允许你编程设置这个时间单位是ADC时钟周期。这里的关键计算信号源阻抗Rs和ADC内部采样开关电阻RADC会形成一个RC电路。采样电容CADC的充电时间常数 τ (Rs RADC) * CADC。为了达到N位精度通常需要充电时间 N * ln(2) * τ。STM32F103的CADC典型值是8pFRADC约1kΩ。假设你用一个电位器阻值10kΩ直接接在ADC引脚上那么Rs10kΩ中心抽头到两端的最大等效阻抗。总电阻约11kΩ时间常数τ ≈ 11kΩ * 8pF 88ns。要达到12位精度约7个时间常数需要至少616ns。如果你的ADC时钟ADCCLK是12MHz周期83.3ns那么你需要设置采样时间 616ns / 83.3ns ≈ 7.4个周期。所以选择采样时间为7.5个周期或更久是安全的。CubeMX中的配置在Channel Settings里你可以为每个通道单独设置“Sampling Time”。对于高阻抗源要设置更长的采样时间如239.5个周期否则采样值会偏低且不稳定。对于低阻抗源如运放输出可以用较短的采样时间如1.5个周期来提高采样速率。2.3 多通道与扫描模式的理解STM32F103C8T6的ADC1支持最多16个外部通道。如果你需要轮流采集多个传感器的值就需要用到扫描模式。扫描模式ADC会按照你在“Sequence”中排好的通道顺序自动地一个接一个进行转换。你可以选择在单次模式下启动一次则扫描转换一轮后停止或者在连续模式下一轮结束后自动开始下一轮持续不断。这里最容易混淆的是DMA的配合在扫描模式下每个通道转换完成后都会产生一个EOC转换结束事件。如果你开启了DMA那么每个EOC事件都会触发DMA将ADC数据寄存器DR里的值搬运到你指定的内存数组中。数组的顺序必须和通道扫描顺序完全一致。例如你扫描通道0,1,2,3那么DMA目标数组adc_buffer[0]存的就是通道0的值[1]存通道1的值以此类推。一个常见的坑只开启了扫描模式但没有开启连续模式也没有在定时器中断里反复触发ADC。结果ADC只扫描了一次就停止了你以为没工作。正确的做法是要么开启“连续转换模式”让ADC自己循环扫描要么使用定时器触发在定时器中断里用HAL_ADC_Start()或HAL_ADC_Start_IT()来启动一次扫描。3. 从零构建CubeMX配置与HAL库驱动详解理论懂了我们开始动手。我以最常见的场景为例用ADC1的通道0PA0和通道1PA1采集两路电压使用扫描模式并由DMA将数据搬运到内存用定时器3以100Hz的频率触发转换。3.1 CubeMX工程初始化引脚配置在Pinout视图找到PA0和PA1将其功能设置为ADC1_IN0和ADC1_IN1。检查VDDA和VSSA的供电网络是否已正确连接在原理图阶段完成。ADC1配置在Analog-ADC1中进入参数设置。Clock PrescalerADC时钟预分频器。系统时钟72MHz经过APB2分频通常不分频后到ADC的时钟最大不能超过14MHz。我们选择PCLK2 divided by 6得到12MHz的ADCCLK这是稳定工作的常用值。Resolution12-bit。这是硬件支持的最高精度。Data AlignmentRight alignment。右对齐方便我们直接读取16位变量的低12位。左对齐在某些需要快速比较的场景有用但这里不讨论。Scan Conversion ModeEnabled。启用扫描模式。Continuous Conversion ModeDisabled。我们不希望ADC自动连续转换而是由定时器触发。Discontinuous Conversion ModeDisabled。间断模式用于从序列中选一部分通道转换这里不用。DMA Continuous RequestsEnabled。在DMA搬运完一轮数据后自动等待下一轮数据实现循环搬运。End Of Conversion SelectionEOC after each conversion。每个通道转换完都产生EOC事件方便DMA搬运。Low Power Auto WaitDisabled。Channel Settings在Rank中添加两个规则通道。Rank1: ChannelIN0, Sampling Time 设为239.5 Cycles针对高阻抗源保守设置。Rank2: ChannelIN1, Sampling Time 同样设为239.5 Cycles。DMA配置在DMA Settings标签页点击Add。DMA Request选择ADC1。ModeCircular。循环模式DMA会周而复始地搬运数据填满缓冲区后回到开头非常适合连续数据流。Data WidthWord32位。因为ADC数据寄存器是32位的虽然我们只用了低16位。也可以选Half Word16位但Word更通用。定时器3触发配置在Trigger选项选择Timer 3 Trigger Out event。然后去配置TIM3。将TIM3的时钟源设为内部时钟预分频器和计数器周期Period根据你的需求计算。假设系统时钟72MHz我们希望100Hz触发10ms一次。设置Prescaler为7200-1这样计数器时钟为10kHz。设置Counter Period为100-1这样每100个计数10ms产生一次更新事件。最关键的一步在TIM3的Trigger Output (TRGO) Parameters中将Trigger Event Selection设置为Update Event。这样每次定时器溢出更新时就会产生一个TRGO信号这个信号被连接到ADC作为转换触发源。生成代码配置好时钟树确保系统时钟正确生成工程代码。3.2 关键代码编写与解析CubeMX生成的代码搭建了框架我们还需要添加一些关键逻辑。// 在main.c的USER CODE BEGIN PV区域定义变量 #define ADC_BUFFER_SIZE 2 // 对应两个通道 uint32_t adc_value[ADC_BUFFER_SIZE]; // DMA目标缓冲区 volatile uint8_t adc_data_ready 0; // 数据就绪标志位 // USER CODE BEGIN 4 // DMA传输完成中断回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { adc_data_ready 1; // 设置标志位通知主循环数据已更新一轮 } } // USER CODE END 4 int main(void) { // ... CubeMX生成的初始化代码 (HAL_Init(), SystemClock_Config()等) // MX_ADC1_Init(); MX_DMA_Init(); MX_TIM3_Init(); 这些都会被调用 // 启动DMA将ADC与内存缓冲区关联起来 // 注意HAL_ADC_Start_DMA的第三个参数是数据长度对于扫描模式是“通道数”乘以“每次触发转换的组数”我们这里就是通道数2。 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_value, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); } // 启动定时器让它开始产生触发信号 HAL_TIM_Base_Start(htim3); while (1) { if (adc_data_ready) { adc_data_ready 0; // 清除标志位 // 此时 adc_value[0] 是通道0PA0的值adc_value[1] 是通道1PA1的值 uint16_t ch0_raw adc_value[0] 0xFFF; // 取低12位 uint16_t ch1_raw adc_value[1] 0xFFF; // 转换为电压值 (假设VREF3.3V) float voltage_ch0 (ch0_raw * 3.3f) / 4095.0f; float voltage_ch1 (ch1_raw * 3.3f) / 4095.0f; // 这里可以加入滤波、显示、上传等操作 // 注意由于DMA是循环搬运主循环处理数据的速度必须快于ADC产生数据的速度 // 否则缓冲区会被覆盖。我们的设计是100Hz主循环处理这点数据绰绰有余。 // 示例简单的移动平均滤波 static float filter_buf_ch0[5] {0}; static uint8_t filter_idx 0; filter_buf_ch0[filter_idx] voltage_ch0; filter_idx (filter_idx 1) % 5; float filtered_voltage 0; for(int i0; i5; i) filtered_voltage filter_buf_ch0[i]; filtered_voltage / 5.0f; // 使用filtered_voltage进行后续操作... } // 主循环可以执行其他任务 HAL_Delay(1); } }代码要点解析HAL_ADC_Start_DMA这个函数启动了ADC并关联了DMA。一旦有外部触发定时器3ADC开始转换转换完的数据会自动由DMA搬运到adc_value数组。HAL_ADC_ConvCpltCallback这是一个弱定义的回调函数。当DMA搬运完一轮数据即我们设定的长度ADC_BUFFER_SIZE后会进入此中断。我们在这里设置一个标志位通知主循环数据已准备好。这是典型的“生产者-消费者”模型DMA中断是生产者主循环是消费者。电压计算公式电压 (原始值 / 4095) * 参考电压。这里的4095是12位ADC的最大值2^12 - 1。务必使用浮点数或进行定点数运算否则整数除法会丢失精度。滤波在中断回调里只做最简单的标志位设置把耗时的滤波、计算等操作放到主循环。这是保持系统实时性的好习惯。上面示例是一个简单的移动平均滤波能有效抑制随机抖动。4. 精度提升与噪声抑制实战技巧即使配置正确ADC读数可能依然存在抖动。别急这不是芯片问题而是模拟电路的常态。下面分享几个立竿见影的优化技巧。4.1 硬件层面的“必修课”电源去耦重申一遍在VDDA和VSSA引脚最近的地方放置一个10uF钽电容和一个100nF陶瓷电容。VDD引脚同样需要。这是抑制电源噪声性价比最高的方法。信号调理低通滤波在ADC输入引脚前串联一个100Ω的电阻并对地接一个100nF的电容形成一个截止频率约16kHz的RC低通滤波器可以滤除大部分高频噪声。限流保护如果信号来自外部这个串联电阻还能起到一定的限流保护作用防止意外高压损坏ADC输入引脚ADC引脚通常能承受少量高于VDDA的电压但有严格限制。接地艺术模拟地VSSA和数字地VSS应在芯片下方或电源入口处单点连接。PCB布局时模拟部分和数字部分尽量分开避免数字信号线特别是高频时钟、PWM线从ADC输入线或模拟电源线下方穿过。4.2 软件滤波算法选型当硬件做到位后剩余的抖动就需要软件滤波来平滑。限幅滤波消除粗大误差#define MAX_DELTA 50 // 允许两次采样间的最大差值 uint16_t LastValue 0; uint16_t Filter_Limit(uint16_t new_sample) { if((new_sample - LastValue MAX_DELTA) || (LastValue - new_sample MAX_DELTA)) { return LastValue; // 变化过大认为是干扰返回旧值 } else { LastValue new_sample; return new_sample; } }这种方法能有效剔除偶尔出现的尖峰脉冲干扰但无法平滑小幅度连续抖动。移动平均滤波最常用 如前文示例。优点是简单有效能平滑周期性噪声。缺点是会产生相位滞后且窗口越大滞后越严重响应变慢。适用于变化缓慢的信号如温度、压力。一阶滞后滤波低通滤波器的数字实现float Filter_FirstOrder(float new_sample, float old_value, float alpha) { // alpha 采样周期T / (滤波时间常数 T) 0alpha1 // alpha越小滤波效果越强响应越慢。 return alpha * new_sample (1 - alpha) * old_value; }这种方法计算量小效果类似模拟RC滤波器。alpha的选择需要权衡平滑度和响应速度。中位值平均滤波防脉冲干扰平均滤波法 连续采样N次去掉一个最大值和一个最小值然后计算剩余N-2个值的算术平均。这种方法既能滤除脉冲干扰又能抑制小幅波动效果很好但需要排序计算量稍大。我的选择策略对于缓慢变化的传感器如NTC热敏电阻我通常先用一个小的移动平均窗口如5点再配合一个限幅滤波效果和实时性都不错。对于需要快速响应的信号如音频采集一阶滞后滤波或更复杂的IIR/FIR滤波器是更好的选择。4.3 ADC校准与提高采样率的权衡STM32的ADC出厂时有微小的偏移和增益误差。HAL库提供了HAL_ADCEx_Calibration_Start()函数进行校准。校准必须在ADC上电初始化之后开始第一次转换之前进行。校准值会被存储在芯片内部后续转换会自动补偿。关于采样率有一个重要公式总转换时间 采样时间 12.5个周期其中12.5个周期是12位逐次逼近转换的固定时间。采样时间是你设置的Sampling Time。在ADCCLK12MHz时若采样时间设为239.5周期总时间 239.5 12.5 252周期。单通道最高采样率 12MHz / 252 ≈ 47.6 kHz。如果是双通道扫描完成一轮扫描需要 252 * 2 504周期采样率约为 23.8 kHz。如果你想提高采样率提高ADCCLK但不能超过14MHz。减少采样时间前提是信号源阻抗足够低。减少扫描的通道数。一个常见的误区盲目追求最短采样时间。如果信号源阻抗高采样电容充电不足转换出来的数字值会远低于实际电压值而且每次转换值都不一样抖动剧烈。在速度和精度之间优先保证精度。通过计算或实验找到能满足精度的最短采样时间。5. 进阶应用与故障排查实录掌握了基础我们可以玩点更高级的并看看那些年我踩过的坑。5.1 双ADC同步模式STM32F103C8T6有两个ADCADC1和ADC2。在一些高级封装上它们可以工作在同步模式例如交替模式一个ADC转换时另一个采样提高吞吐率或同步注入模式同时测量多个相关信号。这在CubeMX配置双ADC同步模式的热词中有所体现。配置起来相对复杂需要精确的触发同步。通常需要一个主ADC如ADC1工作在定时器触发模式从ADCADC2工作在“外部触发-ADC1”模式。数据可以通过双ADC模式下的DMA进行搬运。这个功能在需要极高采样率或同步采样的场合如电机相电流采样非常有用但对于大多数应用单ADC扫描DMA已经足够。5.2 典型问题排查清单当你发现ADC工作不正常时可以按这个清单逐一检查现象可能原因排查方法读数始终为0或40951. 引脚配置错误未设为模拟输入。2. 外部信号超出参考电压范围0V或VDDA。3. DMA配置错误数据未搬运到正确地址。1. 检查CubeMX引脚配置和生成的初始化代码。2. 用万用表测量实际输入电压。3. 在调试器中查看adc_value数组内存看是否有变化。读数跳动剧烈1. 采样时间太短针对高阻抗源。2. 电源/地噪声大。3. 外部信号本身噪声大。4. PCB布局不佳数字噪声耦合。1. 增大采样时间如设为239.5。2. 检查去耦电容用示波器看电源纹波。3. 在信号输入端增加RC低通滤波。4. 优化布线模拟数字部分隔离。多通道数据错位1. DMA缓冲区大小与扫描通道数不匹配。2. 扫描序列Rank顺序与DMA数组读取顺序不一致。1. 确认HAL_ADC_Start_DMA的第三个参数等于扫描的通道总数。2. 检查CubeMX中Rank的顺序并确保程序按相同顺序解析DMA数组。ADC不启动/无转换1. 未使能ADC时钟或DMA时钟。2. 触发源未工作如定时器未启动。3. 在单次模式下只触发了一次。1. 检查__HAL_RCC_ADC1_CLK_ENABLE()和__HAL_RCC_DMA1_CLK_ENABLE()是否被调用。2. 检查定时器是否HAL_TIM_Base_Start()。3. 如果是单次扫描需要在每次需要转换时重新调用HAL_ADC_Start()或HAL_ADC_Start_IT()。DMA中断不进入1. DMA中断未使能。2. DMA传输完成中断TC标志未正确清除。3. 中断优先级冲突被更高优先级中断阻塞。1. 在CubeMX的NVIC设置中勾选DMA通道的中断。2. 确保没有在其他地方错误地清除了中断标志。3. 检查NVIC优先级分组和设置。5.3 低功耗场景下的ADC使用在电池供电设备中ADC可能是耗电大户。STM32的ADC支持多种低功耗特性自动关机在单次转换模式转换结束后ADC自动进入低功耗状态。低功耗等待模式配合Low Power Auto Wait功能可以在转换间隙自动进入低功耗状态等待下一次触发。分时供电对于间歇性采样的传感器如每分钟采一次温湿度可以在需要采样时才通过一个GPIO控制MOS管给传感器和其信号调理电路供电采样完成后彻底断电。同时MCU的ADC模块也在采样间隙保持关闭。一个省电的编程模式void ADC_SingleMeasurement_LowPower(void) { // 1. 唤醒传感器给传感器供电 Sensor_Power_On(); HAL_Delay(10); // 等待传感器稳定 // 2. 使能ADC时钟和模块如果之前被关闭 __HAL_RCC_ADC1_CLK_ENABLE(); // 重新初始化ADC如果深度睡眠下上下文丢失 // MX_ADC1_Init(); // 3. 执行一次转换 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t adc_val HAL_ADC_GetValue(hadc1); // 4. 关闭ADC时钟以省电 HAL_ADC_Stop(hadc1); __HAL_RCC_ADC1_CLK_DISABLE(); // 5. 关闭传感器电源 Sensor_Power_Off(); // 处理adc_val... }最后关于stm32f103c8t6国产替代和加密这是两个延伸话题。国产替代芯片如GD32、APM32通常引脚和寄存器兼容但ADC性能可能有细微差异需要仔细阅读其数据手册并测试。加密功能则涉及读保护、芯片唯一ID等用于保护知识产权与ADC功能本身无关但可以在产品化时考虑。折腾STM32的ADC就像在微型的模拟世界里探险每一个细节的调整都可能带来结果的改善。从最初的读数跳动到后来的稳定精准这个过程本身就是嵌入式工程师的乐趣所在。希望这篇长文能成为你手边一份可靠的指南当你再遇到ADC相关问题时能在这里找到思路和答案。
返回列表