1. 舵机控制原理与STM32 PWM实现基础

舵机(Servo Motor)是嵌入式系统中最为常见的执行机构之一,其核心价值在于将数字控制信号精确映射为机械角度输出。在工业控制、机器人关节、云台稳定系统等场景中,舵机以结构紧凑、响应明确、成本低廉的特点成为首选执行单元。理解其底层时序特性与STM32的PWM生成机制,是构建可靠运动控制系统的第一步。

1.1 舵机标准控制协议:20ms周期脉宽调制

绝大多数模拟舵机(如SZ90、SG90等常见型号)遵循统一的脉冲控制协议: 固定20ms周期(50Hz)的方波信号,通过调节高电平持续时间(即脉宽)来设定目标角度 。该协议不依赖电压幅值或电流大小,仅对脉宽敏感,因此具有极强的抗干扰能力与硬件兼容性。

脉宽(ms) 对应角度 说明
0.5 最小角度位置(通常为左极限)
1.0 45° 中间偏左位置
1.5 90° 中心零点位置(标准中位)
2.0 135° 中间偏右位置
2.5 180° 最大角度位置(通常为右极限)

该映射关系并非线性函数,而是由舵机内部电位器反馈与比较电路共同决定。实际应用中,0.5–2.5ms范围覆盖了绝大多数舵机的物理行程,超出此范围可能导致电机堵转、过热甚至损坏。值得注意的是, 20ms周期本身具有严格容差要求 ——若周期偏离过大(如低于15ms或高于25ms),舵机内部控制芯片将无法正确识别指令,表现为抖动、失步或完全无响应。

1.2 STM32定时器PWM生成机制解析

STM32的高级定时器(TIM1/TIM8)与通用定时器(TIM2–TIM5、TIM15–TIM17)均支持PWM输出功能,其本质是利用计数器与比较寄存器的协同工作实现占空比可调的方波。关键寄存器包括:

  • PSC(Prescaler) :预分频器,用于降低计数器时钟频率,扩展计数周期范围;
  • ARR(Auto-Reload Register) :自动重装载值,决定计数器溢出周期(即PWM周期);
  • CCRx(Capture/Compare Register x) :捕获/比较寄存器,决定通道x的比较匹配时刻(即PWM高电平结束点)。

PWM周期计算公式为:
$$ T_{PWM} = \frac{(PSC + 1) \times (ARR + 1)}{f_{CLK}} $$

其中 $ f_{CLK} $ 为定时器输入时钟频率(通常为APB1或APB2总线频率)。对于SYSCLK=72MHz、APB1=36MHz的典型配置,选择TIM3(挂载于APB1总线)时,$ f_{CLK} = 36\text{MHz} $。

为实现20ms周期,需满足:
$$ (PSC + 1) \times (ARR + 1) = 36\text{MHz} \times 20\text{ms} = 720000 $$

工程实践中常采用“分步配置”策略:先固定PSC使计数器频率便于计算(如设PSC=71,则计数器频率为500kHz),再根据所需周期反推ARR。例如:
- PSC = 71 → 计数器时钟 = 36MHz / (71+1) = 500kHz
- 周期20ms对应计数值 = 500kHz × 20ms = 10000 → ARR = 9999

此时,脉宽0.5ms对应CCR值 = 500kHz × 0.5ms = 250;脉宽1.5ms对应CCR值 = 750。该整数映射关系直接决定了角度控制的分辨率(本例中理论分辨率为0.018°/LSB)。

1.3 复用功能与GPIO配置要点

舵机信号线需连接至具备复用功能的GPIO引脚。以STM32F103C8T6为例,TIM3_CH1默认映射至PA6,TIM3_CH2映射至PA7。此类引脚需配置为 复用推挽输出(Alternate Function Push-Pull) ,而非普通GPIO输出模式。原因在于:

  • 定时器外设通过AFIO(Alternate Function I/O)模块直接驱动引脚电平,绕过GPIO数据寄存器;
  • 推挽输出提供足够驱动能力(通常>20mA),确保信号边沿陡峭,减少传输延迟;
  • 复用功能需显式使能AFIO时钟(RCC_APB2ENR |= RCC_APB2ENR_AFIOEN),否则复用功能不可用。

此外,舵机供电需注意共地设计:舵机VCC(通常5V)、GND必须与MCU的VDDA/VSSA及数字地形成低阻抗回路。若电源地未共接,信号参考电平漂移将导致舵机误动作或失控。

2. 基于HAL库的TIM3双通道PWM初始化详解

本节以STM32F103系列为平台,使用STM32CubeMX生成的HAL库框架,实现TIM3_CH1与CH2的同步PWM输出。该设计兼顾舵机单路控制与后续直流电机H桥驱动的扩展需求,体现嵌入式系统模块化设计思想。

2.1 硬件资源规划与时钟树配置

根据前述分析,选定TIM3作为PWM源,其时钟来自APB1总线。在STM32F103中,APB1最大频率为36MHz,TIM3默认使用该时钟。需确保RCC初始化中已使能TIM3时钟:

__HAL_RCC_TIM3_CLK_ENABLE();  // 使能TIM3时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟
__HAL_RCC_AFIO_CLK_ENABLE();  // 使能AFIO时钟(复用功能必需)

GPIO引脚分配如下:
- PA6 → TIM3_CH1(舵机信号输出)
- PA7 → TIM3_CH2(预留电机驱动通道)

2.2 GPIO初始化:复用功能配置

PA6与PA7需配置为复用推挽输出,速度设为50MHz(满足50Hz PWM信号完整性要求):

GPIO_InitTypeDef GPIO_InitStruct = {0};

// 配置PA6、PA7为复用推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;     // 复用推挽
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 50MHz速度
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 重映射设置(若需):F1系列TIM3_CH1/CH2默认已在PA6/PA7,无需重映射
// 但需确认AFIO_MAPR寄存器中无冲突配置

关键点在于 GPIO_MODE_AF_PP 模式的选择。若误设为 GPIO_MODE_OUTPUT_PP ,则引脚将由GPIO寄存器而非TIM3外设控制,导致PWM失效。

2.3 TIM3基础定时器参数配置

采用前述计算结果:PSC=71(分频72倍),ARR=9999(计数10000次),实现20ms周期:

TIM_HandleTypeDef htim3;
TIM_OC_InitTypeDef sConfigOC = {0};

htim3.Instance = TIM3;
htim3.Init.Prescaler = 71;           // (71+1)=72分频 → 36MHz/72 = 500kHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 9999;            // (9999+1)=10000计数 → 500kHz/10000 = 50Hz
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.RepetitionCounter = 0;
if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) {
    Error_Handler(); // 初始化失败处理
}

此处 Period 即ARR值, Prescaler 即PSC值。 CounterMode 设为向上计数,符合标准PWM生成逻辑。

2.4 PWM通道初始化与使能

双通道需分别配置CH1与CH2。核心参数为 Pulse (即CCR值),它直接决定脉宽:

// 配置CH1(PA6)
sConfigOC.OCMode = TIM_OCMODE_PWM1;        // PWM模式1:计数器<CCR时输出有效电平
sConfigOC.Pulse = 250;                     // 初始脉宽0.5ms → 0°
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) {
    Error_Handler();
}

// 配置CH2(PA7)
sConfigOC.Pulse = 250;                     // 初始同CH1
if (HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2) != HAL_OK) {
    Error_Handler();
}

// 启动PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);

TIM_OCMODE_PWM1 表示:当计数器值小于CCR时,输出高电平;到达CCR时翻转为低电平;溢出后重新计数。此模式下, Pulse 值即为高电平持续的计数值,与脉宽呈严格线性关系。

2.5 预装载寄存器(ARR/CCR预装载)的作用

HAL_TIM_PWM_Init() 中,HAL库默认启用ARR与CCR的预装载功能( TIM_AUTORELOAD_PRELOAD_ENABLE )。该机制的意义在于:

  • 避免计数过程中参数突变 :若直接写入ARR/CCR,可能在计数中途生效,导致当前周期异常(如非预期的窄脉冲);
  • 保证波形连续性 :新参数在下一个更新事件(UEV)时同步加载,确保每个PWM周期参数一致;
  • 简化多通道同步 :所有通道参数在同一个UEV时刻更新,消除相位偏差。

因此,在运行时动态修改占空比,只需调用 __HAL_TIM_SET_COMPARE() HAL_TIM_PWM_SetCompare() 即可安全更新CCR值,无需担心时序毛刺。

3. 舵机角度控制算法与实时参数更新策略

将数字PWM值映射为物理角度,并在主循环中安全更新,是舵机控制的核心环节。本节深入剖析映射关系、防抖策略及工程实践中的关键考量。

3.1 角度-PWM值映射模型构建

舵机的0.5–2.5ms脉宽范围对应0–180°机械行程,建立线性映射模型:

$$ CCR = CCR_{min} + \frac{angle}{180^\circ} \times (CCR_{max} - CCR_{min}) $$

其中:
- $ CCR_{min} = 250 $ (0.5ms @ 500kHz)
- $ CCR_{max} = 1250 $ (2.5ms @ 500kHz)
- $ angle \in [0^\circ, 180^\circ] $

该模型假设舵机内部反馈为理想线性,实际应用中需校准。例如,某批次SZ90舵机实测0°对应0.48ms,180°对应2.52ms,则应修正为:

#define CCR_MIN_CAL 240   // 0.48ms * 500kHz
#define CCR_MAX_CAL 1260  // 2.52ms * 500kHz
#define ANGLE_RANGE 180.0f

uint16_t AngleToCCR(float angle) {
    if (angle < 0.0f) angle = 0.0f;
    if (angle > 180.0f) angle = 180.0f;
    return (uint16_t)(CCR_MIN_CAL + (angle / ANGLE_RANGE) * (CCR_MAX_CAL - CCR_MIN_CAL));
}

3.2 主循环中的安全更新机制

直接在 while(1) 中频繁调用 HAL_TIM_PWM_SetCompare() 存在风险:舵机机械响应存在惯性,若更新间隔短于其响应时间(典型值100–500ms),将导致电机剧烈抖动、发热甚至失步。因此需引入 最小更新间隔约束

#include "main.h"
#include "tim.h"
#include "math.h"

static uint32_t last_update_ms = 0;
static const uint32_t MIN_UPDATE_INTERVAL_MS = 150; // 最小更新间隔150ms

void Servo_UpdateAngle(uint8_t channel, float angle) {
    uint32_t current_ms = HAL_GetTick();

    // 检查最小更新间隔
    if ((current_ms - last_update_ms) < MIN_UPDATE_INTERVAL_MS) {
        return; // 间隔不足,跳过本次更新
    }

    uint16_t ccr_val = AngleToCCR(angle);

    switch(channel) {
        case TIM_CHANNEL_1:
            HAL_TIM_PWM_SetCompare(&htim3, TIM_CHANNEL_1, ccr_val);
            break;
        case TIM_CHANNEL_2:
            HAL_TIM_PWM_SetCompare(&htim3, TIM_CHANNEL_2, ccr_val);
            break;
        default:
            return;
    }

    last_update_ms = current_ms;
}

HAL_GetTick() 基于SysTick中断,精度为1ms,足以满足舵机控制需求。该策略确保每次角度更新有充足机械响应时间,显著提升运行稳定性。

3.3 典型测试序列:0°→90°→180°摆动

main() 函数中实现三位置摆动逻辑,验证控制精度:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM3_Init();

    uint8_t pos_state = 0;
    uint32_t state_start_ms = HAL_GetTick();

    while (1) {
        uint32_t now_ms = HAL_GetTick();

        // 每2秒切换一次目标角度
        if (now_ms - state_start_ms >= 2000) {
            switch(pos_state) {
                case 0:
                    Servo_UpdateAngle(TIM_CHANNEL_1, 0.0f);   // 0°
                    pos_state = 1;
                    break;
                case 1:
                    Servo_UpdateAngle(TIM_CHANNEL_1, 90.0f);  // 90°
                    pos_state = 2;
                    break;
                case 2:
                    Servo_UpdateAngle(TIM_CHANNEL_1, 180.0f); // 180°
                    pos_state = 0;
                    break;
            }
            state_start_ms = now_ms;
        }

        // 可添加其他任务...
        HAL_Delay(10);
    }
}

此逻辑每2秒驱动舵机移动至预设角度,形成清晰可见的摆动效果,是硬件验证的黄金标准。

4. 硬件连接、调试工具与故障排查指南

软件逻辑的正确性最终需通过硬件行为验证。本节聚焦实际工程中的连接规范、低成本调试手段及典型故障定位方法,这些经验往往比代码本身更具实践价值。

4.1 舵机三线制连接规范

舵机标准接口为三线:信号线(Signal)、电源正(VCC)、电源地(GND)。以SZ90为例,线序通常为:

颜色 功能 连接目标 注意事项
橙色/黄色 Signal MCU PWM引脚(PA6) 信号线长度建议<20cm,避免长线辐射干扰
红色 VCC 独立5V电源(非USB) 严禁使用ST-Link或USB端口直接供电! 舵机启动电流可达500mA,远超调试器供电能力
棕色/黑色 GND MCU GND(共地) 必须共地! 若未连接,信号电平无参考,舵机不响应

关键原则: 舵机电源与MCU电源分离,但地线必须单点共接 。推荐接线顺序:先接GND,再接VCC,最后接Signal。上电前务必断开所有电源,接线完成后再上电,可避免因短路导致MCU或舵机损坏。

4.2 舵机测试仪:硬件验证的第一道防线

在烧录代码前,使用专用舵机测试仪(Servo Tester)进行硬件验证,是高效开发的关键习惯。其工作原理为:内置可调电位器生成标准20ms PWM信号,通过旋钮直观调节脉宽。

  • 接线方式 :测试仪输出端标有“+”、“-”、“S”,分别接舵机VCC、GND、Signal;
  • 验证流程
    1. 连接舵机,旋转旋钮观察舵机是否平滑转动至极限位置;
    2. 检查0°与180°位置是否对应旋钮两端;
    3. 若舵机无反应,检查电源电压(应为4.8–6.0V)、接线极性、舵机自身故障。

该步骤可快速隔离问题:若测试仪下舵机正常,则问题必在MCU软硬件;若测试仪下亦不工作,则舵机或电源故障。避免陷入“代码没错,硬件却不动”的无效调试循环。

4.3 常见故障现象与根因分析

现象 可能原因 排查步骤
舵机完全不动作 ① 电源未接或电压不足(<4.5V)
② GND未共接
③ PWM引脚配置错误(非AF_PP模式)
④ TIM3时钟未使能
① 万用表测VCC-GND电压
② 查证PCB走线或杜邦线GND连接
③ 检查 MX_GPIO_Init() 中PA6模式
④ 确认 __HAL_RCC_TIM3_CLK_ENABLE() 已调用
舵机轻微抖动(嗡嗡声) ① PWM周期非50Hz(如误设为100Hz)
② 信号线受干扰(长线、邻近电机线)
③ 电源纹波过大
① 示波器测PA6波形,确认周期20ms
② 缩短信号线,远离动力线
③ 用电容滤波(100μF电解+0.1μF陶瓷并联)
舵机只能转半圈(如0°→90°) ① 脉宽范围未校准(如仅发送0.5–1.5ms)
② 舵机机械限位或损坏
① 修改 CCR_MIN_CAL / CCR_MAX_CAL 扩大范围测试
② 用测试仪验证全行程
烧录后舵机乱转 ① 初始化顺序错误(如先启动TIM再配置GPIO)
② 全局变量未初始化(CCR初值随机)
① 确保 MX_GPIO_Init() MX_TIM3_Init() 之前调用
② 在 MX_TIM3_Init() 中显式设置 sConfigOC.Pulse 初值

示波器是终极调试工具。将探头接地夹接GND,针尖接PA6,可直观观测:
- 波形是否为标准方波(非畸变);
- 周期是否稳定20ms(光标测量);
- 高电平宽度是否随角度变化(如0°时250us,90°时750us)。

若波形正确而舵机不动作,则问题100%在硬件层(电源、接线、舵机本体)。

5. 扩展思考:从舵机到直流电机的PWM驱动演进

舵机控制是理解PWM的基础,而直流电机(DC Motor)驱动则是其自然延伸。二者虽同用PWM,但在控制逻辑与硬件接口上存在本质差异,理解此差异对系统架构设计至关重要。

5.1 单路PWM vs 双路互补PWM:控制维度的根本区别

  • 舵机 :单路PWM信号即完成全部控制。脉宽唯一决定角度,属 开环位置控制 ,无需电流或速度反馈。
  • 直流电机 :单路PWM仅能控制 转速 (平均电压),无法控制 转向 。要实现双向驱动,必须使用H桥电路,其核心是两路 相位相反 的PWM信号:

  • 正转:H桥上左管(Q1)与下右管(Q4)导通 → 电流从左至右流过电机;

  • 反转:上右管(Q2)与下左管(Q3)导通 → 电流从右至左流过电机。

因此,TIM3_CH1与CH2在此场景下需配置为 互补输出 (Complementary PWM),并加入死区时间(Dead Time)防止上下管直通短路。这解释了为何初始化中同时配置双通道——为后续电机驱动预留硬件基础。

5.2 电流保护与驱动芯片选型

舵机内部集成驱动电路,MCU仅需提供逻辑信号。而直流电机需外部驱动芯片(如L298N、TB6612FNG、DRV8871),其选型关键参数包括:

  • 持续电流能力 :需大于电机堵转电流(如12V/1A电机,堵转电流常达3–5A);
  • 逻辑电平兼容性 :DRV8871支持3.3V逻辑,L298N需5V;
  • 集成保护功能 :过流、过热、欠压锁定(UVLO)。

若直接用MCU GPIO驱动电机,将导致IO口烧毁。曾在一个项目中,工程师误将PA6(TIM3_CH1)直接连电机,上电瞬间PA6引脚永久击穿——这是新手最易踩的硬件深坑。

5.3 从开环到闭环:PID控制的引入契机

舵机自身构成闭环系统(内部电位器反馈+比较器),用户仅需发送目标角度。而直流电机是典型开环执行器,其转速受负载、电池电压影响极大。要实现精准速度或位置控制,必须引入外部传感器(编码器、霍尔元件)与PID算法。

这正是培训视频中“PID下篇”的伏笔:当TIM3双通道PWM驱动电机后,通过编码器获取实际转速,以PID算法动态调整两路PWM占空比,才能实现鲁棒的速度跟随。此时,TIM3不仅产生PWM,其编码器接口(ETR)或输入捕获(IC)功能也将被启用,系统复杂度跃升一个量级。

我在实际AGV小车项目中,曾因忽略电机供电压降导致PID积分饱和,小车在斜坡上持续加速直至撞墙。最终解决方案是在PID计算前加入电压前馈补偿——这类实战经验,永远无法从字幕中获得,而只能在一次次硬件联调中沉淀。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐