
1. 项目缘起从“拧旋钮”到“读电压”的嵌入式入门最近在整理工作室的物料翻出来一堆B5K的电位器。这玩意儿在电子爱好者手里简直是“万金油”从调音量到调亮度再到各种需要手动调节参数的场合都能看到它的身影。但很多时候我们只是把它当作一个简单的分压器接上电源和地中间抽头输出一个模拟电压然后用万用表量一下了事。这其实有点“暴殄天物”了因为电位器真正的价值在于它能将一个物理上的旋转或滑动动作转化为一个可以被微控制器MCU精确感知和处理的连续变化的电信号。这就是我们今天要聊的核心如何用STM32这颗强大的“大脑”去“读懂”B5K电位器这个“手指”的动作。听起来很简单不就是读个ADC模数转换器值嘛但实操过的人都知道从电路连接、代码配置到数值处理每一步都有细节。比如B5K这个阻值有什么讲究STM32的ADC参考电压怎么选读回来的值跳动怎么办怎么把ADC的原始值映射成我们想要的0-100%或者0-360°这些问题正是新手从“点灯”迈向“感知世界”的关键一步。所以这篇文章就围绕“STM32读取B5K电位器”这个具体任务掰开揉碎了讲。我会假设你已经有基本的STM32开发环境比如Keil或STM32CubeIDE并且会点灯、会用串口打印。咱们的目标是让你不仅能照着步骤把代码跑起来更能理解每一步背后的“为什么”最终能举一反三把这种读取模拟量的能力应用到自己的项目里无论是做个调光台灯、遥控小车还是更复杂的控制系统。2. 硬件基石理解B5K电位器与STM32的ADC接口在写代码之前我们必须把硬件电路理清楚。硬件是软件的根基连接错了或者理解有偏差代码调得再漂亮也是白搭。2.1 B5K电位器不只是个“可调电阻”首先我们得认清手里的这个“B5K”到底是什么。B5K其实包含了两个关键信息5K 指的是电位器的总电阻值单位是欧姆Ω。也就是说当你用万用表测量电位器外侧两个引脚通常标记为1和3时阻值应该是5kΩ左右。这个值决定了电位器在电路中的电流消耗。根据欧姆定律I V / R在3.3V电压下流经电位器的最大电流约为0.66mA对STM32的IO口来说是安全的。B 这个字母代表了电位器的阻值变化曲线Taper。最常见的是B型线性型和A型对数型。B型线性 阻值的变化与旋转角度或滑动位移成线性关系。旋转到中间位置时中间抽头对两端的电阻比就是1:1。这是我们做参数调节、位置传感时最常用的类型。A型对数 阻值变化与旋转角度的对数成比例常用于音量调节以符合人耳对声音响度的感知特性等响度曲线。对于我们读取位置信息的需求必须使用B型线性电位器。如果你用一个A型电位器来当角度传感器那么读出来的数值和实际角度之间的关系将是非线性的需要复杂的软件校准徒增烦恼。电位器通常有三个引脚。接线时两端的引脚分别接电源VCC 如3.3V和地GND。中间的引脚是滑动抽头Wiper它的输出电压V_out会随着旋转而变化计算公式为V_out VCC * (R_bottom) / (R_top R_bottom)其中R_bottom是抽头到GND端的电阻R_top是抽头到VCC端的电阻。当我们旋转电位器时R_top和R_bottom此消彼长但总和始终是5kΩ。2.2 STM32的ADC外设将模拟世界数字化STM32内部集成了高精度的ADCAnalog-to-Digital Converter模数转换器它的任务就是把电位器抽头上那个连续的电压信号转换成微控制器能处理的数字值。这里有几个关键概念需要理解分辨率 常见的STM32的ADC是12位的。这意味着它能把参考电压范围划分成 2^12 4096 个等级。每个等级对应一个数字量从0到4095或写作0x000到0xFFF。所以12位ADC的最小可分辨电压变化V_ref / 4096。参考电压V_ref 这是ADC测量的基准。STM32的ADC参考电压源可以来自内部如VDDA也可以接外部精密基准源。对于大多数通用应用我们直接使用芯片的VDDA引脚电压作为参考。务必确保VDDA引脚连接了一个稳定、干净的3.3V电源并且通常需要在VDDA和VSSA模拟地之间就近放置一个1uF和10nF的滤波电容这是ADC稳定工作的前提。采样时间 ADC不是瞬间完成转换的它需要一小段时间来“采样”输入引脚上的电压。对于像电位器这样信号变化相对缓慢的源我们可以设置较长的采样时间让ADC内部的采样电容有充足的时间充电到目标电压从而提高转换精度。STM32允许我们为每个通道独立配置采样时间。硬件连接示意图B5K电位器 引脚1 (VCC端) ---- STM32的3.3V电源 (例如 3V3) 引脚2 (抽头 Wiper) ---- STM32的某个ADC输入引脚 (例如 PA0) 引脚3 (GND端) ---- STM32的GND 注意STM32的VDDA引脚也必须接3.3VVSSA接GND。注意有些开发板为了简化已经将VDDA内部连接到3.3V。但为了获得最佳性能尤其是当你的3.3V电源来自线性稳压器LDO时最好还是按照数据手册要求独立且良好地处理VDDA/VSSA的供电与滤波。3. 软件驱动从CubeMX配置到代码实现硬件连接妥当后我们进入软件环节。这里以STM32CubeMX工具配合HAL库为例这是目前最主流、最快捷的开发方式。即使你用的是标准库或LL库原理也完全相通。3.1 使用STM32CubeMX进行图形化配置创建工程与选型 打开CubeMX选择你使用的具体STM32型号例如STM32F103C8T6。配置系统核心SYS 在SYS选项卡下将Debug设置为Serial Wire如果你要用ST-Link调试。对于某些型号如果你计划使用PA15、PB3、PB4等引脚做GPIO可能需要在这里禁用JTAG功能选择Disable释放这些引脚。配置时钟RCC 在RCC选项卡下将HSE高速外部时钟设置为Crystal/Ceramic Resonator如果你的板子有外部晶振。然后转到Clock Configuration标签页配置系统时钟SYSCLK。对于F1系列通常用8MHz HSE通过PLL倍频到72MHz。确保给ADC的时钟ADCCLK不超过数据手册规定的最大值例如14MHz。通常系统时钟72MHz下给ADC分频到12MHz或更低是安全的。配置ADC引脚在芯片引脚图上找到你计划使用的ADC输入引脚例如PA0点击它选择ADC1_IN0对于PA0通常是通道0。在左侧分类视图中找到Analog下的ADC1。在Parameter Settings标签页Resolution 选择12-bit。这是精度和速度的平衡。Scan Conversion Mode 因为我们只用一个通道选择Disabled。Continuous Conversion Mode 选择Enabled这样ADC就会自动连续转换我们只需要定时去读取结果即可。Discontinuous Conversion ModeDisabled。DMA Continuous Requests 如果不用DMA就Disabled。使用DMA可以大大减轻CPU负担我们稍后讨论。End Of Conversion Selection 保持默认。在NVIC Settings标签页可以勾选ADC1 and ADC2 interrupts的全局中断。如果使用轮询方式读取则不需要开启。配置ADC通道切换到ADC1的Configuration标签页点击Add按钮添加一个Regular Conversions。在Rank中选择Channel 0对应PA0。Sampling Time 这是关键参数对于电位器这种高阻抗源建议设置较长的采样时间以确保采样电容充分充电。可以选择Cycles 239.5或Cycles 480.5具体选项因型号而异。时间越长抗噪声能力越强但转换速度会变慢。对于手动调节的电位器速度要求极低所以选一个较长的周期如239.5个周期是很好的选择。生成代码 配置好工程名、路径和工具链MDK-ARM for Keil后点击GENERATE CODE。3.2 编写应用程序代码CubeMX生成代码后我们主要在main.c的/* USER CODE BEGIN */和/* USER CODE END */之间添加业务逻辑。方案一轮询方式最简单/* 在main函数初始化部分之后开启ADC */ HAL_ADC_Start(hadc1); // 启动ADC连续转换 /* 在main的while循环中 */ while (1) { uint32_t adc_value; if (HAL_ADC_PollForConversion(hadc1, 100) HAL_OK) // 等待转换完成超时100ms { adc_value HAL_ADC_GetValue(hadc1); // 获取12位原始值范围0-4095 // 此时adc_value就是ADC的原始读数 // 可以在这里进行数值映射、滤波或通过串口发送 // 例如映射到0-100% // uint32_t percentage (adc_value * 100) / 4095; } HAL_Delay(50); // 延时一段时间再读避免过于频繁 }轮询方式的优缺点优点 代码简单直观易于理解。缺点HAL_ADC_PollForConversion函数会阻塞CPU直到转换完成或超时。在while循环中频繁调用会导致CPU大量时间在空等效率低下且影响系统实时性。方案二中断方式更高效在CubeMX中启用ADC全局中断前面已提。在代码中处理中断回调/* 在main函数中启动ADC并开启中断 */ HAL_ADC_Start_IT(hadc1); /* 在main.c文件末尾用户代码区重写转换完成回调函数 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { uint32_t adc_value HAL_ADC_GetValue(hadc1); // 在这里处理adc_value例如存入全局变量、触发其他操作 // 注意回调函数中不宜进行耗时操作 } }中断方式的优缺点优点 非阻塞。ADC转换完成后自动触发中断CPU在转换期间可以处理其他任务效率高。缺点 编程稍复杂需要注意中断服务函数要简短快速。如果转换频率非常高中断频繁触发也可能成为系统负担。方案三DMA方式专业之选强烈推荐对于需要连续、高速、稳定采集ADC数据的应用DMA直接存储器访问是终极解决方案。它可以在完全不占用CPU的情况下自动将ADC转换结果搬运到指定的内存数组中。CubeMX配置在ADC1的Parameter Settings中将DMA Continuous Requests设置为Enabled。在Project Manager的Advanced Settings中确保为ADC1生成了DMA初始化代码。在DMA Settings标签页可能在ADC1或System Core下点击Add选择ADC1模式为Circular循环模式数据宽度为Word对应32位但实际数据是12位右对齐存储的。代码实现#define ADC_BUFFER_SIZE 256 // 定义DMA缓冲区大小 uint32_t adc_dma_buffer[ADC_BUFFER_SIZE]; // DMA目标缓冲区 /* 在main函数初始化部分之后 */ HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_SIZE); /* 此后adc_dma_buffer数组会被ADC自动、循环地填充 */ /* 在主循环或定时器中断中你可以直接读取这个数组的最新值或进行平均滤波 */ while (1) { // 例如取缓冲区中最新的一次转换结果 uint32_t latest_adc_value adc_dma_buffer[0]; // 注意在循环模式下索引0不一定是最新值需要根据实际情况处理 // 更常见的做法是设置一个“缓冲区已满”的半传输或传输完成中断在中断中处理一批数据。 HAL_Delay(100); }DMA方式的优缺点优点 CPU零开销。ADC以设定速率连续转换DMA自动搬运你可以一次性处理一批数据非常适合进行数字滤波如滑动平均。缺点 配置和理解起来最复杂需要处理缓冲区、指针和可能的中断。实操心得 对于读取电位器这种低速应用轮询方式完全够用且最简单。但如果你正在学习STM32我强烈建议你从中断方式开始尝试然后挑战DMA方式。理解DMA是提升嵌入式开发能力的重要一环。在实际产品中为了系统稳定性和低功耗DMA通常是首选。4. 数据处理滤波、映射与校准直接从ADC读回来的原始值0-4095往往不能直接使用因为它带有噪声并且它代表的是电压而不是我们最终需要的物理量如角度、百分比。4.1 软件滤波对抗噪声与抖动电位器本身有噪声电源也可能有纹波导致ADC读数在小范围内跳动。即使电位器静止读数也可能在±2个LSB最低有效位内变化。软件滤波是必须的。1. 多次采样取平均最基础有效#define SAMPLE_TIMES 16 // 采样次数必须是2的幂次方便用移位代替除法 uint32_t read_adc_average(void) { uint32_t sum 0; for(int i0; iSAMPLE_TIMES; i) { // 假设使用轮询方式单次读取函数为 read_adc_once() sum read_adc_once(); HAL_Delay(1); // 每次采样间隔一小段时间 } return (sum / SAMPLE_TIMES); // 或使用 sum 4 (因为16是2的4次方) }2. 滑动平均滤波更适用于连续采样 适用于DMA方式连续采集的场景。维护一个固定长度的队列数组每次新数据进来替换掉最老的数据然后计算队列中所有数据的平均值。#define FILTER_WINDOW_SIZE 8 uint32_t adc_filter_buffer[FILTER_WINDOW_SIZE] {0}; uint8_t filter_index 0; uint32_t sliding_average_filter(uint32_t new_value) { uint32_t sum 0; adc_filter_buffer[filter_index] new_value; // 新值替换旧值 filter_index (filter_index 1) % FILTER_WINDOW_SIZE; // 索引循环 for(int i0; iFILTER_WINDOW_SIZE; i) { sum adc_filter_buffer[i]; } return sum / FILTER_WINDOW_SIZE; }3. 一阶低通滤波惯性滤波 这种方法模拟了硬件RC低通滤波器的效果计算量小能很好地平滑数据。float alpha 0.2; // 滤波系数0alpha1越小越平滑但响应越慢 float filtered_value 0; uint32_t low_pass_filter(uint32_t new_value) { filtered_value filtered_value alpha * (new_value - filtered_value); return (uint32_t)filtered_value; }注意事项 滤波在平滑噪声的同时也会引入滞后延迟。对于电位器这种手动操作滞后几十毫秒到一百毫秒人眼几乎无法察觉所以可以大胆使用较强的滤波如大的窗口或小的alpha。但如果是用于高速反馈系统就需要在平滑度和响应速度之间仔细权衡。4.2 数值映射从ADC值到有用信息滤波后的稳定ADC值需要映射成有意义的物理量。1. 线性映射到百分比0-100% 这是最常见的需求比如控制PWM占空比、设置阈值等。uint32_t adc_raw read_adc_filtered(); // 假设这是滤波后的值0-4095 uint32_t percentage (adc_raw * 100) / 4095; // 注意先乘后除避免整数运算归零2. 线性映射到特定范围如0-360° 假设电位器是单圈旋转型通常300°有效电气角度我们可以映射到角度。#define ADC_MIN 80 // 实测得到的最小ADC值对应0° #define ADC_MAX 4000 // 实测得到的最大ADC值对应300° uint32_t angle (adc_raw - ADC_MIN) * 300 / (ADC_MAX - ADC_MIN); if(angle 300) angle 300; // 限幅 if(angle 0) angle 0;3. 非线性校正 如果你的电位器线性度不好或者你需要一个特殊的响应曲线如指数音量调节可以查表法LUT。预先测量一组ADC值和目标输出值的对应关系存入数组。使用时根据当前ADC值在表中查找最近的两个点进行线性插值得到最终输出。这对于高精度应用或校正传感器非线性非常有效。4.3 校准消除硬件误差理想情况下电位器拧到一端是0VADC值0拧到另一端是3.3VADC值4095。但现实很骨感硬件公差 电位器两端可能无法接触到真正的端点电阻。参考电压误差 VDDA可能不是精确的3.300V。 因此上电校准或手动校准是提升精度的好习惯。简单的两点校准法上电后提示用户将电位器拧到最小值位置然后按下一个“校准键”。单片机连续采样一段时间记录下稳定的ADC值作为adc_min。提示用户将电位器拧到最大值位置再次按下校准键。记录稳定的ADC值作为adc_max。后续所有的映射计算都使用adc_min和adc_max作为范围而不是理论上的0和4095。uint32_t map_calibrated(uint32_t adc_raw, uint32_t cal_min, uint32_t cal_max, uint32_t out_min, uint32_t out_max) { if(adc_raw cal_min) return out_min; if(adc_raw cal_max) return out_max; // 防止除零 if(cal_max cal_min) return out_min; return ( (adc_raw - cal_min) * (out_max - out_min) / (cal_max - cal_min) out_min ); }5. 进阶优化与问题排查把基本功能跑通只是第一步要让项目更稳健、更专业还需要考虑以下方面。5.1 电源与参考源的稳定性ADC的精度极度依赖参考电压的稳定性。如果系统的3.3V电源是从USB的5V通过LDO如AMS1117-3.3降压得来当数字部分如LED、电机工作时会引起电源纹波导致ADC读数波动。对策一 为模拟部分VDDA使用独立的LDO供电或者至少使用LC电感-电容滤波器从数字电源中隔离出模拟电源。对策二 使用STM32内部的参考电压缓冲器如果型号支持。它可以提供一个更稳定的内部参考电压给ADC。对策三 软件上可以采集一个已知的、稳定的内部参考电压如内部带隙电压VREFINT通过比例计算来抵消VDD变化的影响。STM32的HAL库提供了HAL_ADCEx_Calibration_GetValue等函数来辅助此过程。5.2 多通道采集与扫描模式如果你需要读取多个电位器不必为每个电位器配置一个ADC。STM32的ADC支持多通道扫描。在CubeMX中为多个引脚如PA0, PA1, PA2配置ADC通道。在ADC1的Parameter Settings中将Scan Conversion Mode设置为Enabled。在Configuration的Regular Conversions中按顺序添加多个Rank每个Rank对应一个通道并设置好采样时间。使用DMA方式启动转换。DMA的目标缓冲区需要是一个数组其长度等于通道数。ADC会按照Rank顺序自动转换每个通道结果依次存入DMA缓冲区。读取时缓冲区adc_dma_buffer[0]是第一个通道的值adc_dma_buffer[1]是第二个通道的值以此类推。5.3 常见问题与排查思路问题ADC读数始终为0或4095。排查 首先用万用表测量电位器抽头的电压看是否在0-VCC之间正常变化。如果电压正常检查STM32的ADC引脚配置是否正确是否配置为模拟输入模式Analog。检查VDDA和VSSA是否已正确连接。问题ADC读数跳动很大即使电位器不动。排查硬件 检查电源纹波。用示波器看VDDA和ADC输入引脚上的波形。确保电位器抽头与ADC引脚之间的连线短且牢固。在ADC输入引脚到地之间加一个10nF - 100nF的陶瓷电容可以很好地滤除高频噪声。软件 增加ADC采样时间Sampling Time。这是对付高阻抗信号源噪声最有效的软件手段。其次实施软件滤波如前面所述的滑动平均。问题读数范围不全达不到0或4095。排查 这是正常现象由电位器的端点电阻和PCB走线电阻导致。使用前面介绍的两点校准法即可完美解决无需纠结硬件。问题使用DMA时数据好像不对或溢出。排查 检查DMA缓冲区大小是否足够。在循环模式下ADC会一直写如果CPU读取速度跟不上新数据会覆盖旧数据。可以考虑使用DMA的“半传输完成”和“传输完成”中断在中断中处理半缓冲或全缓冲数据这是更可靠的双缓冲机制。另外检查DMA的数据宽度Word和ADC的数据对齐方式通常为右对齐Right alignment是否匹配。6. 项目实战构建一个可调光LED灯理论最终要服务于实践。让我们用一个简单的综合项目来串联所有知识点用B5K电位器控制一个LED的亮度。硬件清单STM32最小系统板 x1B5K线性电位器 x1LED灯珠或开发板上的用户LED x1220Ω限流电阻 x1如果LED没有内置杜邦线若干电路连接电位器两端接3.3V和GND抽头接PA0ADC。LED正极通过220Ω电阻接PB0或其他GPIO负极接GND。软件设计CubeMX配置配置PA0为ADC1_IN0。配置PB0为GPIO_Output推挽输出低电平有效取决于你的LED接法。配置一个定时器如TIM2用于产生PWM信号。将PB0映射为该定时器的PWM输出通道如TIM2_CH3。配置ADC12位连续转换长采样时间。生成代码。代码逻辑在main函数中启动ADC轮询或中断方式启动PWM定时器。在while循环或ADC中断回调中读取滤波后的ADC值。将ADC值0-4095线性映射到PWM的占空比设置值如0-1000对应0%-100%。更新PWM的占空比寄存器如__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_3, duty_cycle)。效果 旋转电位器LED的亮度会平滑地变化。通过调整软件滤波参数你可以让亮度变化非常平滑毫无闪烁感。这个项目虽然小但它完整地实践了模拟信号采集ADC、数字信号处理滤波、映射和模拟信号输出PWM的闭环。理解了它你就掌握了嵌入式系统与物理世界交互的一项核心技能。你可以轻易地将它扩展为控制电机转速、舵机角度、蜂鸣器音调等等。