ARTICLE DETAIL

资讯详情

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

STM32F103C8 ADC单通道采集实战:从原理到滤波与校准

STM32F103C8 ADC单通道采集实战:从原理到滤波与校准 1. 项目概述从零开始搞定STM32F103C8的ADC单通道采集如果你手头正好有一块经典的“蓝桥杯”或“正点原子”开发板核心是STM32F103C8T6想用它来读取一个电位器的电压或者监测一下电池电量那么你大概率会用到它的ADC功能。ADC模数转换器是连接模拟世界和数字世界的桥梁对于单片机来说这是最基础也最核心的外设之一。STM32F103C8的ADC功能强大但初次接触时面对时钟配置、通道选择、采样时间这些参数很容易一头雾水代码跑起来要么没数据要么数据跳得跟心电图一样。这个内容就是为你准备的。我将以一个最典型的场景——使用STM32F103C8的ADC1单次转换模式读取PA1引脚ADC通道1上的模拟电压——作为主线手把手带你从原理到代码从配置到调试完整走一遍流程。我们不仅会写出能用的代码更会搞清楚每一个配置项背后的“为什么”比如为什么采样时间会影响精度为什么需要校准以及当你的ADC值像得了“多动症”一样不稳定时该怎么给它“治病”。无论你是刚接触STM32的新手还是想巩固ADC基础的老手这篇内容都能让你获得可以直接“抄作业”的代码和真正解决问题的思路。2. ADC基础与STM32F103C8的硬件资源解析在动手写代码之前我们必须先和手里的“兵器”——STM32F103C8的ADC模块——打个招呼了解它的脾气和能力边界。这能避免很多后续的“想当然”错误。2.1 ADC是什么我们为什么需要它单片机是数字世界的居民它只认识0和1。但现实世界是连续的、模拟的比如温度、压力、声音、光线强度它们的变化是平滑的曲线。ADCAnalog-to-Digital Converter就像一个翻译官它的工作就是测量一个引脚上的模拟电压比如0V到3.3V之间的任意值然后把它转换成一个单片机可以理解的数字值。对于STM32F103C8的12位ADC来说这个数字值的范围是0到4095因为2^12 4096。假设我们单片机的供电电压VDDA是标准的3.3V那么ADC的转换关系理论上就是线性的0V对应数字值03.3V对应数字值4095。你测到1.65V理论上ADC值就应该是2048左右。这个“翻译”过程就是我们整个项目的核心目标。2.2 STM32F103C8的ADC家底盘点STM32F103C8T6这颗芯片内部集成了2个12位精度的ADC模块ADC1和ADC2。它们非常灵活支持多种工作模式但对于我们单通道采集这个入门任务需要关注以下几个关键特性分辨率与量程12位分辨率意味着它能将参考电压范围分成4096份。量程由参考电压决定通常VREF接VDDA3.3VVREF-接VSSA0V所以输入电压必须在0V到3.3V之间超过这个范围不仅测不准还可能损坏ADC输入引脚。通道与引脚ADC1有16个外部模拟输入通道ADC2有10个。它们复用在GPIO口的特定引脚上。例如ADC1的通道1对应PA1通道2对应PA2以此类推。这是第一个容易踩的坑你必须确保你使用的GPIO引脚确实复用了你想要的ADC通道。比如你想用ADC1通道1就必须把PA1配置为模拟输入模式而不是简单的浮空输入。转换模式单次转换触发一次只转换一个通道一次然后停止。适合低速、非连续的应用比如读取一个电位器。连续转换开启后ADC会不停地自动转换指定的通道。适合需要实时监控的场景。扫描模式配合多通道使用ADC按顺序自动转换一组预先设置好的通道。 对于单通道我们通常从单次转换模式开始逻辑最清晰。触发源ADC需要一个“开始转换”的信号这个信号就是触发。可以是软件触发你写一条命令让它开始也可以是硬件触发比如一个定时器到时了自动让它开始。入门阶段用软件触发最简单。采样时间这是影响精度和速度的关键参数。ADC不是瞬间“抓住”电压值的它需要一小段时间称为采样时间让内部采样保持电容上的电压稳定到与外部输入电压一致。时间太短电容充电不足转换结果就会不准时间太长转换速度就慢。STM32的ADC采样时间以“时钟周期”为单位可配置。对于高阻抗的信号源比如用一个很大的电阻分压需要更长的采样时间。了解这些你就知道配置ADC时那些下拉菜单里的选项大概是什么意思了。接下来我们进入实战环节。3. 开发环境搭建与CubeMX图形化配置我强烈推荐使用ST官方的STM32CubeMX工具进行初始化配置。它通过图形界面生成底层硬件初始化代码HAL库能极大减少因寄存器配置错误导致的低级问题让我们更专注于应用逻辑。这里我们以使用ADC1单次转换读取PA1电压为例。3.1 工程创建与核心时钟配置首先打开CubeMX新建工程选择MCU型号为STM32F103C8Tx。在Pinout Configuration视图下开始配置系统核心SYS在Debug下拉菜单中选择Serial Wire。这开启了SWD下载和调试接口对于C8T6是必须的否则第一次下载程序后可能就无法再次连接了。时钟RCC在High Speed Clock (HSE)选择Crystal/Ceramic Resonator。我们的开发板外部通常有一个8MHz的晶振这里就是告诉芯片使用这个外部晶振作为时钟源。时钟配置Clock Configuration这是关键一步。我们需要为ADC提供一个时钟。STM32F103的ADC时钟最大不能超过14MHz。通常的配置路径是HSE8MHz → PLL倍频到72MHz作为系统时钟SYSCLK → 然后通过分频给APB2总线APB2时钟也是72MHz → ADC预分频器。在时钟树图上找到APB2确保它是72MHz。然后找到ADC Prescaler选择PCLK2 divided by 6。这样ADC时钟 72MHz / 6 12MHz符合小于14MHz的要求保证了ADC的正常工作。3.2 ADC1单通道参数详细配置转到Analog-ADC1进行配置。模式Mode因为我们只用一个通道所以Independent mode独立模式即可。参数设置Parameter SettingsData Alignment: 选择Right alignment右对齐。这样12位的转换结果存储在16位变量的低12位我们直接读取即可比较直观。左对齐需要额外处理。Scan Conversion Mode: 选择Disabled。我们不是多通道扫描。Continuous Conversion Mode: 选择Disabled。我们使用单次转换需要时手动启动。Discontinuous Conversion Mode: 选择Disabled。这是用于扫描模式下分组触发的我们不用。DMA Continuous Requests: 选择Disabled。单次单通道用查询或中断方式读取数据更简单暂时不用DMA。End Of Conversion Selection: 保持默认。通道配置在左侧的芯片引脚图上找到PA1点击它选择ADC1_IN1。这步操作会自动将PA1的GPIO模式设置为模拟输入Analog。回到ADC1配置页面下方会出现IN1的配置行。点击Rank选择Regular channel 1规则通道1。关键参数Sampling Time采样时间这里我们先设置为Cycles 239.5。这是最长的采样时间约等于239.5个ADC时钟周期。在ADC时钟为12MHz时采样时间约为20微秒。为什么一开始设这么长为了稳定。在调试初期我们优先保证转换结果的正确性避免因采样时间不足导致数据乱跳。后续优化时可以根据信号源特性调小这个值以提高转换速率。触发配置在External Trigger Conversion Source中选择Regular Conversion launched by software。这意味着我们将通过调用HAL_ADC_Start()函数来软件触发一次转换。3.3 生成工程代码配置完成后转到Project Manager页面设置好工程名称、存储路径、IDE比如MDK-ARM V5。在Code Generator选项卡勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral这样ADC的代码会独立成文件结构更清晰。点击GENERATE CODECubeMX会生成完整的初始化代码。用你的IDE如Keil打开工程在main.c的/* USER CODE BEGIN 2 */和/* USER CODE END 2 */之间就是我们添加自己应用代码的地方。4. 单次转换模式下的ADC数据采集代码实现生成了代码骨架现在我们来注入灵魂——编写读取ADC值的应用程序。我们采用“启动转换-等待转换完成-读取结果”的简单轮询模式。4.1 ADC初始化的补充检查在开始写主循环代码前先确认一下CubeMX生成的初始化是否正确。打开main.c找到MX_ADC1_Init()函数或者直接查看adc.c文件。你应该能看到类似以下的配置重点是采样时间和触发源// 在 adc.c 的 MX_ADC1_Init 函数中应有类似代码 hadc1.Instance ADC1; hadc1.Init.ScanConvMode ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); } // 配置通道1的采样时间 ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_1; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_239CYCLES_5; // 239.5周期采样 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); }4.2 主循环中的采集逻辑在main.c的while (1)主循环里我们编写采集代码。思路是启动转换 - 等待转换完成 - 读取值 - 延时 - 循环。/* USER CODE BEGIN WHILE */ while (1) { uint32_t adc_value 0; float voltage 0.0f; // 1. 启动ADC单次转换 HAL_ADC_Start(hadc1); // 2. 等待转换完成超时时间设为10ms if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { // 3. 读取转换结果12位右对齐值范围0-4095 adc_value HAL_ADC_GetValue(hadc1); // 4. 将ADC值转换为电压值 (假设VDDA3.3V) voltage (float)adc_value * 3.3f / 4095.0f; // 5. 通过串口打印结果需要事先配置好USART并重定向printf printf(ADC Value: %lu, Voltage: %.3f V\r\n, adc_value, voltage); } else { printf(ADC Conversion Timeout!\r\n); } // 6. 停止ADC单次转换模式下一次转换完成后自动停止也可显式调用 HAL_ADC_Stop(hadc1); // 7. 延时一段时间避免打印刷屏太快 HAL_Delay(500); // 延时500ms /* USER CODE END WHILE */代码逻辑拆解与注意事项HAL_ADC_Start(): 这个函数会启动ADC并等待触发。因为我们配置的是软件触发所以调用它之后转换就立即开始了。HAL_ADC_PollForConversion(): 这是一个轮询等待函数。它会阻塞在这里直到ADC转换完成标志被置位或者超过你设置的超时时间这里是10毫秒。对于单次转换一个通道在12MHz ADC时钟和239.5周期采样时间下一次转换时间大约为(239.5 12.5) / 12MHz ≈ 21us远小于10ms所以通常不会超时。如果超时可能是ADC根本没启动成功或者时钟配置有问题。HAL_ADC_GetValue(): 读取数据寄存器中的值。这里有一个细节这个函数会清除“转换完成”标志位EOC。所以如果你用中断方式在中断服务函数里读取一次后标志位就清了不会导致中断不断触发。电压计算公式电压 ADC值 * 参考电压 / 4095是理想情况。这里的3.3f是你实际测量的VDDA电压。如果板子供电不稳这个值会变影响测量绝对精度。对于需要高绝对精度的场合需要测量真实的VDDA或者使用芯片内部的参考电压VREFINT进行校准计算。串口打印为了方便观察我用了printf。你需要确保工程中已经实现了串口重定向例如重写_write函数。如果还没做可以先用调试器查看变量值或者点个LED灯来简单指示。4.3 编译、下载与初步测试将代码编译无误后下载到开发板。用一根杜邦线将PA1引脚连接到开发板的3.3V电源引脚观察串口输出。你应该看到ADC值稳定在4095附近电压值在3.3V附近。再将PA1连接到GND值应接近0。如果接一个电位器的中间抽头转动电位器数值应平滑变化。如果此时数值跳动很大或者根本不变别急我们接下来就要解决这些典型问题。5. ADC值不稳定跳动的根源分析与实战滤波“我的ADC读数怎么一直在跳”这是新手遇到最多的问题。即使是接一个稳定的电压源读出来的值也可能在±几个LSB最低有效位范围内波动。这其中有正常噪声也有可以改善的问题。5.1 识别跳动类型噪声还是错误首先要量化“跳动”。接一个固定电压比如用板载的3.3V连续读取100个ADC值观察变化范围。小范围跳动±2~5个数字这很可能是正常的白噪声来源于电源纹波、ADC本身的量化噪声、PCB板上的数字信号串扰等。需要通过软件滤波来处理。大范围跳动几十甚至上百个数字或固定偏差这通常是硬件或配置问题需要优先排查。5.2 硬件层面的排查与优化治本软件滤波是“治标”硬件优化才是“治本”。在加滤波算法前先检查这些电源质量ADC的参考电压VDDA必须干净、稳定。如果VDDA随着数字电路工作而波动ADC结果必然跳动。检查确保开发板上VDDA和VSSA模拟地已经通过磁珠或0欧电阻与VDD和VSS数字电源和地连接并且靠近MCU处有足够的去耦电容通常是一个10uF钽电容并联一个100nF陶瓷电容。实操用示波器探头打在AC耦合档测量VDDA引脚对地的波形观察纹波大小。好的电源纹波应该在几十mV以内。信号源阻抗与采样时间这是最容易被忽略的一点。ADC输入端内部可以等效为一个开关加一个小电容。采样阶段开关闭合外部信号源需要在这个时间内给这个电容充电到稳定电压。如果信号源阻抗太高比如用一个1MΩ的电阻分压充电就会很慢。问题采样时间不足电容充电未完成转换值就不准且每次充电程度随机导致跳动。解决方案增加采样时间在CubeMX里把Sampling Time调大比如从239.5调到71.5或55.5试试。对于高阻抗源可能需要最长的采样时间。降低信号源阻抗在分压电路后加一个电压跟随器运算放大器进行缓冲它可以提供极低的输出阻抗完美驱动ADC。或者减小分压电阻的阻值比如从1MΩ降到10kΩ但要注意功耗。PCB布局与走线模拟走线远离数字走线特别是时钟线、PWM线、数据总线等高速数字信号它们会产生强烈的电磁干扰。模拟地AGND的单点接地确保模拟部分的地最终只有一个点连接到数字地避免数字噪声电流在模拟地路径上产生压降。在ADC输入引脚就近对地加一个小电容例如100pF~10nF的陶瓷电容。这个电容可以作为电荷池在采样瞬间提供瞬时电流同时也能滤除一部分高频噪声。但要注意电容会和信号源阻抗形成一个低通滤波器可能影响信号带宽。对于直流或慢变信号利大于弊。5.3 软件滤波算法实战治标硬件优化后剩余的微小跳动就需要软件滤波来平滑了。这里介绍几种简单有效的方法算术平均滤波连续采样N次求和后取平均值。这是最常用、最简单的。#define SAMPLE_TIMES 10 // 采样次数 uint32_t ADC_Filter_Average(void) { uint32_t sum 0; for(uint8_t i0; iSAMPLE_TIMES; i) { HAL_ADC_Start(hadc1); if(HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { sum HAL_ADC_GetValue(hadc1); } HAL_Delay(1); // 可选让每次采样有点间隔 } HAL_ADC_Stop(hadc1); return (sum / SAMPLE_TIMES); }优缺点能有效抑制周期性干扰和随机噪声。但平滑度和速度是矛盾的N越大越平滑速度越慢。对于缓变信号效果好对快速变化信号会产生滞后。滑动平均滤波维护一个长度为N的队列每次新采样值入队最老的出队然后计算队列中所有值的平均值。相当于一个不断移动的窗口。#define FILTER_LEN 8 uint32_t adc_buf[FILTER_LEN] {0}; uint8_t buf_index 0; uint32_t ADC_Filter_MovingAverage(uint32_t new_val) { uint32_t sum 0; adc_buf[buf_index] new_val; // 新值替换旧值 buf_index (buf_index 1) % FILTER_LEN; // 索引循环 for(uint8_t i0; iFILTER_LEN; i) { sum adc_buf[i]; } return (sum / FILTER_LEN); } // 在主循环中读取原始值new_val然后 filtered_val ADC_Filter_MovingAverage(new_val);优缺点对数据的平滑效果好实时性比算术平均好每次只更新一个值。但同样有滞后且需要占用固定的内存空间。一阶滞后滤波低通滤波这是一种递归滤波本次输出结果 α * 本次采样值 (1-α) * 上次输出结果。其中α是滤波系数0α1。float alpha 0.2f; // 系数越小越平滑滞后越大 float filtered_voltage 0.0f; // 初始值 uint32_t ADC_Filter_LowPass(uint32_t new_val) { float new_voltage (float)new_val * 3.3f / 4095.0f; filtered_voltage alpha * new_voltage (1 - alpha) * filtered_voltage; return (uint32_t)(filtered_voltage * 4095.0f / 3.3f); }优缺点计算量小能很好地抑制周期性干扰适用于高频波动大的场合。但相位滞后明显α的选择需要权衡响应速度和平滑度。我的经验是对于大多数读取电位器、电池电压等慢变信号的应用“硬件上确保采样时间足够软件上使用滑动平均滤波”是性价比最高的组合。采样时间通过实验确定逐步减小采样时间直到跳动开始明显增大然后退回一级。滑动平均的长度N取4、8、16等2的幂次方便于编译器优化除法运算。6. 进阶话题精度校准、内部参考与DMA传输当你的单通道采集稳定工作后可以探索这些进阶内容来提升项目的专业性和效率。6.1 ADC校准消除出厂误差STM32的ADC在出厂时会有微小的增益和偏移误差。HAL库提供了校准函数来修正这些误差提升绝对精度。校准通常在ADC初始化之后、开始使用之前进行一次。CubeMX生成的代码里可能没有自动加入校准我们需要手动添加。在main.c的MX_ADC1_Init()函数调用之后添加校准代码/* USER CODE BEGIN 2 */ // 启动ADC校准 if (HAL_ADCEx_Calibration_Start(hadc1) ! HAL_OK) { Error_Handler(); } // 校准完成后ADC会自动处于关闭状态 printf(ADC Calibration Done.\r\n); /* USER CODE END 2 */注意校准过程会消耗几十到几百微秒的时间且会修改ADC的内部校准寄存器。它主要修正的是ADC模块自身的误差对于由外部参考电压VDDA不准、PCB布局、信号源阻抗等引入的误差无效。6.2 使用内部参考电压VREFINT计算真实VDDA前面我们计算电压时默认VDDA3.3V。但实际上USB供电、LDO的精度都会导致VDDA在3.2V到3.4V之间波动。为了获得高精度的绝对电压测量值我们可以利用芯片内部的一个已知精度的参考电压源VREFINT来反向推算当前的VDDA。STM32F103内部有一个1.2V典型值的参考电压连接到ADC的一个内部通道通常是通道17。它的值在芯片出厂时被测量并存储在系统存储区Flash特定地址中称为VREFINT_CAL。在3.3V供电、30°C条件下这个校准值对应ADC对VREFINT的测量结果。计算真实VDDA的步骤读取校准值从指定Flash地址读取VREFINT_CAL。对于F103这个地址通常是0x1FFFF7BA2字节。#define VREFINT_CAL_ADDR 0x1FFFF7BA uint16_t vrefint_cal *(uint16_t*)VREFINT_CAL_ADDR;测量VREFINT的ADC值配置ADC去采样内部通道17具体通道号查数据手册获取ADC原始值adc_vrefint_raw。计算VDDA利用公式VDDA 3.3V * VREFINT_CAL / adc_vrefint_raw。这里3.3V是设计值用于计算比例。计算外部通道的真实电压有了真实的VDDA再计算外部通道电压Vexternal VDDA * adc_external_raw / 4095。代码示例片段float Get_Real_VDDA(void) { uint16_t vrefint_cal *(uint16_t*)0x1FFFF7BA; // 假设已经配置并读取了内部参考通道的ADC值到变量 adc_vrefint float vdda 3.3f * (float)vrefint_cal / (float)adc_vrefint; return vdda; // 返回计算得到的真实VDDA电压 }这个方法能有效消除供电电压波动带来的测量误差特别适合用于电池供电设备中精确测量电池电压。6.3 尝试DMA传输解放CPU在我们之前的轮询或中断方式中每次ADC转换完成都需要CPU介入去读取数据。当需要高速、连续采样时这会大量占用CPU时间。DMA直接存储器访问就是为了解决这个问题而生的它可以在外设ADC和内存数组之间直接搬运数据无需CPU参与。配置DMA单通道连续传输的思路CubeMX配置在ADC1配置中启用DMA Continuous Requests。在DMA Settings中添加一个DMA请求方向设为Peripheral To Memory模式设为Circular循环模式缓冲区满了自动从头开始数据宽度都设为Word半字也行对应16位。在Parameter Settings里将Continuous Conversion Mode设为Enabled连续转换模式。代码修改定义一个全局数组作为DMA传输的目标缓冲区uint32_t adc_dma_buf[BUFFER_SIZE];在初始化后启动ADC的DMA传输HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buf, BUFFER_SIZE);此后ADC会以最快的速度受采样时间限制连续转换并将结果自动存入adc_dma_buf。你可以通过计算两次读取缓冲区数据的时间差来获取采样率或者处理缓冲区中的数据。可以开启DMA传输完成中断或半传输中断在中断里处理数据实现“双缓冲”机制避免处理数据时覆盖新数据。对于单通道采集DMA的优势可能不那么明显但这是通往多通道、高速采集的必经之路。理解了这个流程以后再面对复杂的应用就会从容很多。走到这里你已经从一个ADC的“使用者”变成了“理解者”。从最基础的引脚配置、采样时间理解到硬件噪声排查、软件滤波算法实现再到利用内部参考进行校准和探索高效的DMA传输这套组合拳足以应对绝大多数基于STM32F103C8的单通道ADC应用场景。记住嵌入式开发中稳定可靠的数据是一切上层应用的基础而ADC正是获取这基础数据的关键门户。多动手测试多思考数据背后的物理意义你的项目就会越来越扎实。
返回列表