ARTICLE DETAIL

资讯详情

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

STM32 ADC读取电位器与PWM控制舵机:从原理到实战的闭环控制方案

STM32 ADC读取电位器与PWM控制舵机:从原理到实战的闭环控制方案 在嵌入式开发中如何用最直观的物理交互方式比如旋转一个旋钮来精确控制一个执行机构比如舵机的转动角度是很多智能硬件项目的基础需求。最近在做一个基于STM32的机械臂原型时就遇到了这个问题需要手动调节多个关节的角度。网上资料要么只讲ADC读取电位器要么只讲PWM控制舵机将两者结合起来的完整闭环教程却不多见导致调试时信号匹配和线性映射总出问题。本文将系统性地拆解如何利用STM32的ADC模块读取电位器模拟电压并实时转换为PWM信号的占空比从而精准控制舵机角度。从核心原理、CubeMX配置、HAL库代码编写到线性映射校准提供一套完整的、可复现的实战方案。无论你是刚接触STM32的新手还是需要快速实现交互控制的开发者都能从中获得可直接移植的代码和清晰的调试思路。1. 背景与核心概念在开始动手之前我们需要理解这个系统中三个核心部件的工作原理以及它们是如何协同工作的。1.1 STM32微控制器系统的“大脑”STM32是意法半导体ST推出的基于ARM Cortex-M内核的32位微控制器。它集成了丰富的外设其中两个对本项目至关重要的外设是ADC (Analog-to-Digital Converter模数转换器)负责将连续的模拟电压信号如电位器输出的0-3.3V转换为离散的数字量如0-4095供CPU读取和处理。定时器TIM的PWM输出功能通用定时器可以产生精确的脉冲波形。通过配置定时器的捕获/比较寄存器可以轻松生成频率和占空比均可调的PWM信号。1.2 电位器模拟信号的“提供者”电位器本质上是一个可变电阻器。通过旋转旋钮可以改变中间抽头与两端之间的电阻值从而在抽头上得到一个分压电压。当电位器两端分别接在STM32的VCC如3.3V和GND时中间抽头的输出电压就在0V到3.3V之间连续变化。这个连续的电压变化就是我们用来控制舵机的“模拟量手柄”。1.3 舵机PWM信号的“执行者”舵机是一种位置角度伺服的驱动器它内部包含直流电机、减速齿轮组、控制电路和位置反馈电位器。舵机的控制信号是周期为20ms频率50Hz的PWM脉冲其转动角度由脉冲的高电平宽度脉宽决定。典型控制关系以180度舵机如SG90/MG996R为例0.5ms脉宽 —— 对应0度位置。1.5ms脉宽 —— 对应90度位置。2.5ms脉宽 —— 对应180度位置。重要特性舵机不是通过占空比的平均电压来工作而是识别每个周期内高电平脉冲的绝对宽度。因此必须保证PWM信号的频率稳定在50Hz周期20ms然后通过改变脉宽0.5ms ~ 2.5ms来改变角度。1.4 系统工作流程整个系统的工作流程可以概括为以下四步采集STM32的ADC模块周期性读取连接电位器的GPIO引脚电压得到一个12位的原始数字量例如ADC_Value范围0-4095。映射CPU将ADC_Value通过线性映射公式转换为目标脉宽时间单位可以是微秒us或对应的定时器比较寄存器值CCR。输出STM32的定时器PWM模块根据上一步计算出的值实时调整输出波形的脉宽同时保持周期固定为20ms。执行舵机接收到新的PWM信号驱动内部电机转动到对应的角度位置。这样就实现了一个“旋钮拧到哪舵机转到哪”的实时闭环控制系统。2. 环境准备与版本说明在开始编码前请确保你的开发环境已就绪。以下配置是本文示例的基础不同型号的STM32可能存在引脚差异但整体思路一致。硬件清单主控芯片STM32F103C8T6蓝色战舰最小系统板这是最常用的入门型号。其他F1系列或F4系列芯片同样适用配置方法类似。电位器10kΩ线性电位器一个。舵机SG90180度或MG996R舵机一个。注意供电需求SG90可由开发板5V引脚驱动MG996R负载大时建议外接5V/2A以上电源。连接线杜邦线若干。开发/调试工具ST-LINK V2下载调试器。软件环境集成开发环境IDESTM32CubeIDE v1.11.0。它集成了STM32CubeMX图形化配置工具和基于Eclipse的代码编辑/调试环境一站式解决配置和编码问题。固件库STM32CubeF1 Firmware Package。在CubeIDE中创建项目时会自动管理。串口调试助手如XCOM、Putty等用于打印ADC采样值辅助调试。版本兼容性说明本文的核心逻辑基于STM32 HAL库该库在不同系列和版本的CubeMX/CubeIDE中保持高度一致的API风格。因此即使你使用的是更新版本的CubeIDE如1.12或不同型号的芯片如STM32G0, F4只需根据芯片数据手册调整对应的GPIO和定时器引脚代码主体无需大改。3. 核心原理与CubeMX工程配置本节是项目的基石我们将使用STM32CubeMX进行图形化配置生成初始化代码框架。3.1 系统时钟与项目创建打开STM32CubeIDE新建STM32项目。在芯片选择器中输入STM32F103C8选择STM32F103C8Tx点击下一步。设置项目名称如Servo_PWM_Control和存储路径选择C语言和TrueSTUDIO工具链默认即可。进入CubeMX图形界面。3.2 ADC1 通道配置用于读取电位器我们的目标是使用ADC1的某个通道来采样电位器电压。假设使用PA1引脚。在Pinout Configuration标签页左侧找到Analog下的ADC1。在IN1对应的引脚PA1上勾选将其配置为ADC1的通道1。在左侧的System CoreADC1配置界面中进行如下设置Scan Conversion Mode:Disabled单通道无需扫描。Continuous Conversion Mode:Enabled使能连续转换自动循环采样。Discontinuous Conversion Mode:Disabled。End Of Conversion Selection:EOC flag at the end of single conversion。Low Power Auto Wait:Disabled。Channel 1 (PA1) Configuration:Rank: 1Sampling Time: 选择Cycles 55.5或Cycles 239.5。采样时间越长精度越高但转换速度越慢。对于电位器这种慢变信号选择较长的采样时间如239.5周期可以获得更稳定的读数。在Parameter Settings选项卡保持Resolution为12-bit0-4095。其他参数默认。3.3 定时器TIM2配置用于产生舵机PWM我们使用通用定时器TIM2的通道1PA0引脚输出PWM。注意舵机控制需要非常精确的周期。找到Timers下的TIM2。将TIM2的Clock Source设为Internal Clock。将Channel1设为PWM Generation CH1此时PA0引脚会自动被配置为复用输出。进入Parameter Settings选项卡进行关键计算和配置预分频器Prescaler, PSC用于对定时器时钟进行分频。我们需要先计算。计数器周期Counter Period, ARR决定PWM波形的频率。计算步骤假设系统主频APB1 Timer Clocks为72MHzSTM32F103C8T6常见配置。舵机需要50Hz频率即周期T20ms20,000us。确定定时器计数频率我们希望定时器每个计数的单位时间为1us这样方便计算脉宽。因此定时器时钟应为1MHz。预分频器值PSC 定时器时钟 / 目标计数频率 - 1 72MHz / 1MHz - 1 71。在CubeMX中设置Prescaler (PSC - 16 bits value)为71。确定自动重装载值ARRARR决定了PWM的周期。周期T (ARR 1) * 计数单位时间。计数单位时间 1 / 计数频率 1us。所以ARR 1 周期T / 计数单位时间 20,000us / 1us 20000。因此Counter Period (ARR - 16 bits value)设置为19999因为ARR是从0开始计数的。其他设置Counter Mode:Up向上计数。auto-reload preload:Enable使能自动重载预装载保证波形稳定。下方PWM Generation Channel 1的Mode保持PWM mode 1Pulse脉宽初始值先设为1500对应1.5ms脉宽90度。CH Polarity设为High高电平有效。3.4 时钟树配置确保系统时钟正确。在Clock Configuration标签页通常STM32F103C8T6的外部晶振HSE为8MHz经过PLL倍频后系统时钟SYSCLK可设置为72MHz。确认APB1 Timer ClocksTIM2的时钟源也为72MHz。如果之前未配置可以在此处将HSE选为时钟源并将PLL倍频因子设为9使SYSCLK72MHz。3.5 生成代码在Project ManagerCode Generator中勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral这样代码更模块化。点击右上角的GENERATE CODE生成工程代码并打开CubeIDE进行编辑。4. 核心代码编写与解析CubeMX已经为我们生成了ADC和TIM2的初始化代码MX_ADC1_Init()和MX_TIM2_Init()。我们只需要在main.c的主循环中添加业务逻辑。4.1 启动ADC与PWM在main()函数的初始化部分while(1)循环之前我们需要启动ADC的连续转换和PWM输出。/* USER CODE BEGIN 2 */ HAL_ADC_Start(hadc1); // 启动ADC1开始连续转换 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); // 启动TIM2的通道1输出PWM /* USER CODE END 2 */4.2 主循环逻辑读取、映射、设置核心逻辑在while (1)循环中。我们周期性地读取ADC值将其映射为舵机脉宽并更新定时器的比较寄存器CCR。/* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ uint32_t adc_value 0; uint16_t servo_pulse 0; // 1. 读取ADC值 if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) // 等待转换完成超时10ms { adc_value HAL_ADC_GetValue(hadc1); // 获取12位ADC原始值 (0-4095) // 可选通过串口打印ADC值用于调试 // printf(ADC: %lu\r\n, adc_value); } // 2. 将ADC值线性映射到舵机脉宽单位us // 舵机脉宽范围500us (0度) ~ 2500us (180度) // ADC值范围0 ~ 4095 // 映射公式servo_pulse 500 (adc_value * (2500-500) / 4095) // 为避免浮点数使用整数运算 servo_pulse 500 (adc_value * 2000 / 4095); // 3. 将脉宽us转换为定时器的比较值CCR // 定时器计数频率为1MHz1计数1us。 // 所以脉宽多少微秒CCR就设置为多少。 // 注意CCR值不能超过ARR值19999。 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, servo_pulse); // 4. 添加一个小延时避免循环过快。HAL_Delay会阻塞可根据需要调整。 HAL_Delay(10); // 延时10ms即控制更新率约为100Hz } /* USER CODE END 3 */代码关键点解析HAL_ADC_PollForConversion这是一个轮询方式的ADC值获取函数。参数10表示超时时间为10毫秒。在连续转换模式下第一次调用会启动转换并等待后续调用会等待上一次转换完成。对于实时性要求不极高的场景轮询方式简单可靠。线性映射公式这是整个项目的数学核心。adc_value * 2000 / 4095实现了将0-4095映射到0-2000的范围再加上500的基值最终得到500-2500的脉宽值。务必使用整数运算在嵌入式系统中效率远高于浮点数。__HAL_TIM_SET_COMPARE这是一个HAL库宏用于安全地设置指定定时器通道的比较寄存器CCR值。当CCR值改变下一个PWM周期就会立即采用新的脉宽从而实现舵机角度的实时控制。延时HAL_Delay(10)此延时决定了系统对电位器变化的响应速度。10ms的延时意味着控制更新频率为100Hz远高于舵机50Hz的信号频率足够流畅。如果去掉延时循环会以极高速率运行虽然响应更快但会无谓地消耗CPU资源。可以根据实际需求调整或使用非阻塞式定时器来优化。4.3 可选添加串口打印调试信息为了直观看到ADC值的变化可以初始化串口并重定向printf。在CubeMX中使能USART1PA9-TX PA10-RX模式为Asynchronous波特率115200。然后在代码中集成以下内容/* 在main.c文件顶部添加 */ #include stdio.h /* 重写fputc函数使printf输出到串口 */ int __io_putchar(int ch) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; } /* 在while循环中取消注释printf语句 */ printf(ADC: %lu, Pulse: %u us\r\n, adc_value, servo_pulse);5. 硬件连接与测试代码编写完成后编译并下载到STM32开发板。接下来进行硬件连接。接线示意图电位器 - 左侧引脚 - 3.3V (开发板3.3V引脚) - 中间引脚 - PA1 (ADC输入) - 右侧引脚 - GND 舵机 - 信号线橙色/黄色 - PA0 (PWM输出) - 电源线红色 - 5V (开发板5V引脚或外部5V电源正极) - 地线棕色/黑色 - GND (与开发板共地)重要提醒如果使用MG996R等大扭矩舵机务必使用独立5V电源为其供电并将此电源的地线与开发板地线连接在一起。避免因电机启动瞬间电流过大导致开发板复位或损坏。上电测试给系统上电。旋转电位器旋钮观察舵机是否随之转动。如果连接了串口打开串口助手可以看到不断打印的ADC值和计算出的脉宽值。旋转电位器时这些数值应平滑变化。预期现象将电位器逆时针旋到底接近GNDADC值接近0舵机应转到0度或最小角度。将电位器顺时针旋到底接近3.3VADC值接近4095舵机应转到180度或最大角度。旋转过程应平稳无抖动或卡顿。6. 常见问题与排查思路在实际操作中你可能会遇到一些问题。下表列出了常见现象、原因及解决方法问题现象可能原因排查思路与解决方案舵机完全不转动1. 供电问题。2. PWM信号未输出。3. 信号线接错。1. 检查舵机电源5V和地线是否接好用万用表测量电压。2. 用示波器或逻辑分析仪检查PA0引脚是否有50Hz的PWM波形。如果没有检查CubeMX中TIM2配置和HAL_TIM_PWM_Start函数是否调用。3. 确认舵机信号线连接到了正确的PWM输出引脚。舵机抖动或吱吱响1. 电源功率不足。2. PWM频率不准。3. 机械负载过重或卡住。1.最常见原因换用独立5V电源给舵机供电确保电流足够SG90约需0.5AMG996R空载约需0.7A带负载可能超过1A。2. 用示波器测量PWM周期确认是否为准确的20ms50Hz。检查定时器时钟和ARR、PSC计算是否正确。3. 卸下负载手动转动舵机盘看是否顺畅。舵机角度范围不对如只能转90度1. PWM脉宽映射范围错误。2. 舵机型号不同如270度舵机。3. ADC采样值范围非0-4095。1. 打印出servo_pulse的值旋转电位器时看其是否在500-2500之间平滑变化。检查映射公式。2. 查阅你的舵机数据手册确认其脉宽与角度的对应关系并修改代码中的最小/最大脉宽值。3. 确认ADC分辨率配置为12位并打印adc_value看其是否在0-4095范围内。ADC读数不稳定舵机抖动1. 电位器接触不良或噪声。2. ADC采样时间太短。3. 电源纹波大。1. 更换电位器或在电位器输出端与地之间并联一个0.1uF的电容滤波。2. 在CubeMX中增加ADC通道的Sampling Time采样时间例如设为Cycles 239.5以提高精度和稳定性。3. 在模拟电源3.3V和地之间增加滤波电容。控制响应有延迟主循环中HAL_Delay时间过长。减小HAL_Delay的参数例如改为HAL_Delay(5)或HAL_Delay(2)提高控制频率。注意舵机本身响应有延迟过快的更新无意义。旋转电位器到两端舵机不动或打齿脉宽超出了舵机安全范围。在代码中对计算出的servo_pulse进行限幅处理确保其值在安全范围内例如500-2500。servo_pulse (servo_pulse 500) ? 500 : ((servo_pulse 2500) ? 2500 : servo_pulse);7. 优化与进阶实践基础功能实现后可以从以下几个方面进行优化和扩展让项目更健壮、更专业。7.1 软件滤波消除ADC采样抖动直接读取的ADC值可能存在毛刺导致舵机轻微抖动。可以添加简单的软件滤波算法。移动平均滤波示例#define FILTER_LEN 10 // 滤波窗口长度 uint32_t adc_filter_buffer[FILTER_LEN] {0}; uint8_t filter_index 0; uint32_t adc_filter_sum 0; // 在while循环中替换原来的adc_value获取部分 if (HAL_ADC_PollForConversion(hadc1, 10) HAL_OK) { uint32_t raw_adc HAL_ADC_GetValue(hadc1); // 移动平均滤波 adc_filter_sum - adc_filter_buffer[filter_index]; // 减去最旧的值 adc_filter_buffer[filter_index] raw_adc; // 存入新值 adc_filter_sum raw_adc; // 加上新值 filter_index (filter_index 1) % FILTER_LEN; // 更新索引 adc_value adc_filter_sum / FILTER_LEN; // 计算平均值 }此方法用过去10次采样的平均值作为有效值能有效平滑数据但会引入少量延迟。FILTER_LEN可根据需要调整。7.2 使用DMA定时器触发ADC解放CPU轮询方式占用CPU。对于更复杂的系统可以使用DMA直接存储器访问将ADC转换结果自动搬运到内存并用定时器定时触发ADC转换实现“后台”自动采样。配置思路在CubeMX中在ADC1配置中开启DMA Continuous Requests并添加一个DMA通道如ADC1到Memory循环模式数据宽度为半字。将External Trigger Conversion Source设为Timer 2 Trigger Out event或其他定时器事件。在代码中启动带DMA的ADCHAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, buffer_size);在主循环中直接读取adc_buffer中的值即可无需等待转换。这种方式下ADC采样完全由硬件定时触发并通过DMA传输CPU几乎零开销可以处理其他任务。7.3 非线性映射与校准上述线性映射假设电位器旋转角度与输出电压呈完美线性关系且舵机响应也是线性的。实际情况可能存在偏差。电位器非线性中低档电位器在两端可能存在非线性区。可以通过制作“查找表”的方式进行校准实测电位器在不同位置时的ADC值并记录对应的理想舵机角度或脉宽在程序中用查表法或插值法进行映射。舵机死区与非线性有些舵机在脉宽接近极限值时响应不灵敏。可以通过实验找到该舵机实际有效的脉宽范围如520-2480us并在代码中使用这个范围进行映射。7.4 多路控制与工程结构优化如果需要控制多个舵机如机械臂只需重复配置多个ADC通道和多个定时器PWM通道即可。但要注意定时器资源分配和代码组织。代码模块化将ADC读取和PWM设置封装成独立的函数或文件。使用结构体管理状态为每个舵机创建一个结构体包含其对应的ADC通道、目标角度、当前脉宽、滤波数据等使程序更清晰。注意定时器选择一个通用定时器通常有4个通道可以输出4路同频率的PWM非常适合控制多个舵机。只需为每个通道设置不同的CCR值。8. 总结通过本项目我们完成了一个典型的STM32嵌入式系统闭环控制案例模拟信号采集ADC → 数字信号处理映射 → 控制信号输出PWM。这不仅是一个具体的舵机控制项目更是一个理解STM32核心外设ADC、定时器协同工作的绝佳范例。关键知识点回顾ADC配置重点是采样时间的设置影响精度和稳定性。定时器PWM配置核心在于根据系统时钟计算预分频器PSC和自动重装载值ARR以产生精确的50Hz基准频率。线性映射算法将ADC的数值范围0-4095线性变换到舵机的控制范围500-2500us是软件层的核心。硬件设计要点舵机务必单独供电信号地需共地这是稳定工作的前提。下一步学习方向深入定时器尝试使用定时器的编码器模式读取舵机反馈部分舵机支持或使用输入捕获模式测量PWM脉宽。加入通信接口通过串口UART接收上位机指令来控制舵机角度实现串口调试助手控制。移植到实时操作系统RTOS如FreeRTOS将ADC采样、控制算法、PWM更新等任务放在不同线程中并加入信号量、队列等机制构建更复杂的多任务控制系统。探索其他控制策略例如加入PID控制算法让舵机能够更快速、平稳、准确地到达指定位置并具备抗负载扰动的能力。希望这篇详细的教程能帮助你打通STM32控制舵机的全链路。在实际项目中硬件电路的稳定性、软件的鲁棒性和算法的效率需要不断权衡和优化。动手实践遇到问题耐心排查是嵌入式工程师成长的必经之路。
返回列表