STM32 ADC读取电位器与PWM控制舵机:从原理到实战的闭环控制方案
2026/8/7 7:01:40 网站建设 项目流程

在嵌入式开发中,如何用最直观的物理交互方式(比如旋转一个旋钮)来精确控制一个执行机构(比如舵机)的转动角度,是很多智能硬件项目的基础需求。最近在做一个基于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 系统工作流程

整个系统的工作流程可以概括为以下四步:

  1. 采集:STM32的ADC模块周期性读取连接电位器的GPIO引脚电压,得到一个12位的原始数字量(例如ADC_Value,范围0-4095)。
  2. 映射:CPU将ADC_Value通过线性映射公式,转换为目标脉宽时间(单位可以是微秒us)或对应的定时器比较寄存器值(CCR)。
  3. 输出:STM32的定时器PWM模块根据上一步计算出的值,实时调整输出波形的脉宽,同时保持周期固定为20ms。
  4. 执行:舵机接收到新的PWM信号,驱动内部电机转动到对应的角度位置。

这样就实现了一个“旋钮拧到哪,舵机转到哪”的实时闭环控制系统。

2. 环境准备与版本说明

在开始编码前,请确保你的开发环境已就绪。以下配置是本文示例的基础,不同型号的STM32可能存在引脚差异,但整体思路一致。

硬件清单:

  • 主控芯片:STM32F103C8T6(蓝色战舰最小系统板),这是最常用的入门型号。其他F1系列或F4系列芯片同样适用,配置方法类似。
  • 电位器:10kΩ线性电位器一个。
  • 舵机:SG90(180度)或MG996R舵机一个。注意供电需求,SG90可由开发板5V引脚驱动,MG996R负载大时建议外接5V/2A以上电源。
  • 连接线:杜邦线若干。
  • 开发/调试工具:ST-LINK V2下载调试器。

软件环境:

  • 集成开发环境(IDE):STM32CubeIDE 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 系统时钟与项目创建

  1. 打开STM32CubeIDE,新建STM32项目。
  2. 在芯片选择器中输入STM32F103C8,选择STM32F103C8Tx,点击下一步。
  3. 设置项目名称(如Servo_PWM_Control)和存储路径,选择C语言和TrueSTUDIO工具链(默认即可)。
  4. 进入CubeMX图形界面。

3.2 ADC1 通道配置(用于读取电位器)

我们的目标是使用ADC1的某个通道来采样电位器电压。假设使用PA1引脚。

  1. Pinout & Configuration标签页左侧,找到Analog下的ADC1
  2. IN1对应的引脚PA1上勾选,将其配置为ADC1的通道1。
  3. 在左侧的System Core>ADC1配置界面中,进行如下设置:
    • 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: 1
      • Sampling Time: 选择Cycles 55.5Cycles 239.5。采样时间越长,精度越高,但转换速度越慢。对于电位器这种慢变信号,选择较长的采样时间(如239.5周期)可以获得更稳定的读数。
  4. Parameter Settings选项卡,保持Resolution12-bit(0-4095)。其他参数默认。

3.3 定时器TIM2配置(用于产生舵机PWM)

我们使用通用定时器TIM2的通道1(PA0引脚)输出PWM。注意,舵机控制需要非常精确的周期。

  1. 找到Timers下的TIM2
  2. TIM2Clock Source设为Internal Clock
  3. Channel1设为PWM Generation CH1,此时PA0引脚会自动被配置为复用输出。
  4. 进入Parameter Settings选项卡,进行关键计算和配置:
    • 预分频器(Prescaler, PSC):用于对定时器时钟进行分频。我们需要先计算。
    • 计数器周期(Counter Period, ARR):决定PWM波形的频率。

计算步骤:假设系统主频(APB1 Timer Clocks)为72MHz(STM32F103C8T6常见配置)。舵机需要50Hz频率,即周期T=20ms=20,000us。

  1. 确定定时器计数频率:我们希望定时器每个计数的单位时间为1us,这样方便计算脉宽。因此,定时器时钟应为1MHz。
    • 预分频器值(PSC) = 定时器时钟 / 目标计数频率 - 1 = 72MHz / 1MHz - 1 = 71。
    • 在CubeMX中设置Prescaler (PSC - 16 bits value)71
  2. 确定自动重装载值(ARR):ARR决定了PWM的周期。
    • 周期T = (ARR + 1) * 计数单位时间。
    • 计数单位时间 = 1 / 计数频率 = 1us。
    • 所以,ARR + 1 = 周期T / 计数单位时间 = 20,000us / 1us = 20000。
    • 因此,Counter Period (ARR - 16 bits value)设置为19999(因为ARR是从0开始计数的)。
  3. 其他设置
    • Counter Mode:Up(向上计数)。
    • auto-reload preload:Enable(使能自动重载预装载,保证波形稳定)。
    • 下方PWM Generation Channel 1Mode保持PWM mode 1Pulse(脉宽初始值)先设为1500,对应1.5ms脉宽(90度)。CH Polarity设为High(高电平有效)。

3.4 时钟树配置

确保系统时钟正确。在Clock Configuration标签页,通常STM32F103C8T6的外部晶振(HSE)为8MHz,经过PLL倍频后系统时钟(SYSCLK)可设置为72MHz。确认APB1 Timer Clocks(TIM2的时钟源)也为72MHz。如果之前未配置,可以在此处将HSE选为时钟源,并将PLL倍频因子设为9,使SYSCLK=72MHz。

3.5 生成代码

  1. Project Manager>Code Generator中,勾选Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral,这样代码更模块化。
  2. 点击右上角的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) // 定时器计数频率为1MHz,1计数=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 */

代码关键点解析:

  1. HAL_ADC_PollForConversion:这是一个轮询方式的ADC值获取函数。参数10表示超时时间为10毫秒。在连续转换模式下,第一次调用会启动转换并等待,后续调用会等待上一次转换完成。对于实时性要求不极高的场景,轮询方式简单可靠。
  2. 线性映射公式:这是整个项目的数学核心。adc_value * 2000 / 4095实现了将0-4095映射到0-2000的范围,再加上500的基值,最终得到500-2500的脉宽值。务必使用整数运算,在嵌入式系统中效率远高于浮点数。
  3. __HAL_TIM_SET_COMPARE:这是一个HAL库宏,用于安全地设置指定定时器通道的比较寄存器(CCR)值。当CCR值改变,下一个PWM周期就会立即采用新的脉宽,从而实现舵机角度的实时控制。
  4. 延时HAL_Delay(10):此延时决定了系统对电位器变化的响应速度。10ms的延时意味着控制更新频率为100Hz,远高于舵机50Hz的信号频率,足够流畅。如果去掉延时,循环会以极高速率运行,虽然响应更快,但会无谓地消耗CPU资源。可以根据实际需求调整或使用非阻塞式定时器来优化。

4.3 可选:添加串口打印调试信息

为了直观看到ADC值的变化,可以初始化串口并重定向printf。在CubeMX中使能USART1(PA9-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电源为其供电,并将此电源的地线与开发板地线连接在一起。避免因电机启动瞬间电流过大导致开发板复位或损坏。

上电测试:

  1. 给系统上电。
  2. 旋转电位器旋钮,观察舵机是否随之转动。
  3. 如果连接了串口,打开串口助手,可以看到不断打印的ADC值和计算出的脉宽值。旋转电位器时,这些数值应平滑变化。

预期现象:将电位器逆时针旋到底(接近GND),ADC值接近0,舵机应转到0度(或最小角度)。将电位器顺时针旋到底(接近3.3V),ADC值接近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.5A,MG996R空载约需0.7A,带负载可能超过1A)。
2. 用示波器测量PWM周期,确认是否为准确的20ms(50Hz)。检查定时器时钟和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中):

  1. 在ADC1配置中,开启DMA Continuous Requests,并添加一个DMA通道(如ADC1Memory,循环模式,数据宽度为半字)。
  2. External Trigger Conversion Source设为Timer 2 Trigger Out event或其他定时器事件。
  3. 在代码中启动带DMA的ADC:HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_buffer, buffer_size);
  4. 在主循环中直接读取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、定时器)协同工作的绝佳范例。

关键知识点回顾:

  1. ADC配置:重点是采样时间的设置,影响精度和稳定性。
  2. 定时器PWM配置:核心在于根据系统时钟计算预分频器(PSC)和自动重装载值(ARR),以产生精确的50Hz基准频率。
  3. 线性映射算法:将ADC的数值范围(0-4095)线性变换到舵机的控制范围(500-2500us),是软件层的核心。
  4. 硬件设计要点:舵机务必单独供电,信号地需共地,这是稳定工作的前提。

下一步学习方向:

  • 深入定时器:尝试使用定时器的编码器模式读取舵机反馈(部分舵机支持),或使用输入捕获模式测量PWM脉宽。
  • 加入通信接口:通过串口(UART)接收上位机指令来控制舵机角度,实现串口调试助手控制。
  • 移植到实时操作系统(RTOS):如FreeRTOS,将ADC采样、控制算法、PWM更新等任务放在不同线程中,并加入信号量、队列等机制,构建更复杂的多任务控制系统。
  • 探索其他控制策略:例如加入PID控制算法,让舵机能够更快速、平稳、准确地到达指定位置,并具备抗负载扰动的能力。

希望这篇详细的教程能帮助你打通STM32控制舵机的全链路。在实际项目中,硬件电路的稳定性、软件的鲁棒性和算法的效率需要不断权衡和优化。动手实践,遇到问题耐心排查,是嵌入式工程师成长的必经之路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询