1. 舵机控制原理与硬件选型

舵机(Servo Motor)是嵌入式系统中最为基础且高频使用的执行机构之一,其核心价值在于将数字控制信号精确映射为机械角度输出。与普通直流电机不同,舵机内部集成了电位器反馈、比较电路和驱动H桥,构成一个闭环控制系统。用户无需关心电机转速、电流或负载变化,只需提供符合协议的PWM信号,即可实现0°–180°范围内任意角度的稳定定位。

本节所采用的舵机型号为SG90,属于标准微型模拟舵机,具有成本低、接口简洁、响应可靠等特点,广泛应用于教学实验、小型机器人关节及原型验证场景。其物理接口为三线制:
- 橙色线(Signal) :PWM控制信号输入端,接收单片机输出的脉宽调制波;
- 红色线(VCC) :电源正极,工作电压范围为4.8 V–6.0 V;
- 棕色线(GND) :电源地,必须与控制器共地。

需特别强调: 共地是系统可靠运行的前提 。若舵机电源地与STM32的地未连接,控制信号将因参考电平缺失而完全失效,表现为舵机无响应或抖动。实践中常见错误即单独使用USB供电STM32、而用外部5 V适配器独立供电舵机,却忽略两地短接,导致调试数小时仍无法驱动。

SG90遵循标准的50 Hz PWM协议,即周期固定为20 ms(T = 20 ms),脉宽(Ton)在0.5 ms–2.5 ms范围内线性对应0°–180°机械角度:

脉宽(ms) 对应角度 占空比(%) 计数值(ARR=19999, PSC=71)
0.5 2.5% 500
1.0 45° 5.0% 1000
1.5 90° 7.5% 1500
2.0 135° 10.0% 2000
2.5 180° 12.5% 2500

该映射关系由舵机内部模拟电路决定,不可通过软件修改。因此,MCU端的任务并非“生成任意PWM”,而是 严格维持20 ms周期,并在该周期内精确控制高电平持续时间 。任何偏离都将导致角度偏移、抖动甚至堵转过热。

值得注意的是,SG90的额定扭矩为1.8 kg·cm(4.8 V)/2.2 kg·cm(6.0 V),最大承重约90 g——这并非指可悬吊90 g物体,而是指在90°位置施加90 g垂直力矩时仍能保持定位。实际应用中,若负载存在惯性或冲击,建议留出30%–50%余量。曾有项目在云台俯仰轴直接使用SG90驱动120 g摄像头模组,连续运行2小时后舵机内部齿轮磨损,角度漂移达±5°,更换为MG90S(金属齿轮)后问题解决。

2. STM32定时器PWM输出配置详解

在STM32平台上实现舵机控制,本质是配置一个通用定时器(如TIM3)工作于PWM模式,并将其通道输出引脚复用至GPIO。本节以STM32F103C8T6(主流入门型号)为例,基于HAL库展开工程化配置,所有参数均依据芯片数据手册与时钟树设计推导得出,杜绝“试出来”式配置。

2.1 系统时钟与定时器时基计算

STM32F103默认使用内部HSI(8 MHz)或外部HSE(8 MHz)作为PLL输入源。本工程采用HSE+PLL倍频方案,将系统时钟(SYSCLK)配置为72 MHz。此值决定了APB1总线(TIM2–TIM4挂载于此)的最高频率:当AHB预分频为1时,APB1预分频为2,故PCLK1 = 36 MHz。

定时器时钟源为PCLK1的倍频(TIM2–TIM4为1x,即36 MHz)。要生成20 ms周期的PWM,需设置定时器自动重装载值(ARR)与预分频系数(PSC),满足:

$$
T_{PWM} = (ARR + 1) \times (PSC + 1) \times T_{CLK}
$$

其中 $T_{CLK} = \frac{1}{36\,\text{MHz}} \approx 27.78\,\text{ns}$。代入目标周期20 ms:

$$
20 \times 10^{-3} = (ARR + 1) \times (PSC + 1) \times \frac{1}{36 \times 10^6}
$$

解得 $(ARR + 1) \times (PSC + 1) = 720000$。

为兼顾精度与寄存器操作便利性,通常选择PSC使计数频率落入1–100 kHz区间。此处取PSC = 71,则:

$$
PSC + 1 = 72,\quad ARR + 1 = \frac{720000}{72} = 10000,\quad ARR = 9999
$$

但实际工程中常将ARR设为19999(即20000计数),PSC设为35(36分频),此时:

$$
T_{PWM} = 20000 \times 36 \times \frac{1}{36 \times 10^6} = 20\,\text{ms}
$$

此配置优势在于: ARR=19999时,1 μs对应计数值为2,0.5 ms即为1000,1.5 ms为3000,数值整除性好,避免浮点运算与舍入误差 。因此最终确定:

  • htim3.Init.Prescaler = 35; (PSC = 35,分频后定时器时钟 = 36 MHz / 36 = 1 MHz)
  • htim3.Init.Period = 19999; (ARR = 19999,计数满值为20000,周期 = 20000 × 1 μs = 20 ms)

2.2 GPIO与AFIO时钟使能

TIM3的PWM输出通道对应多个GPIO引脚,需查阅《STM32F103xx Reference Manual》第9章“Alternate function I/O and debug configuration”。TIM3_CH1默认复用至PA6,TIM3_CH2至PA7,二者均位于GPIOA端口。

关键约束在于: 所有复用功能(AFIO)必须显式开启AFIO时钟 。这是初学者高频踩坑点——仅开启GPIOA和TIM3时钟,却遗漏RCC_APB2ENR寄存器中的AFIOEN位,导致复用功能无法生效,引脚始终处于默认输入状态。

正确时钟使能顺序如下(按依赖关系排列):

__HAL_RCC_AFIO_CLK_ENABLE();   // 必须最先开启,为后续重映射和复用准备
__HAL_RCC_GPIOA_CLK_ENABLE();  // GPIOA提供PA6/PA7物理引脚
__HAL_RCC_TIM3_CLK_ENABLE();   // TIM3提供PWM时基与通道逻辑

2.3 GPIO初始化:复用推挽输出模式

PA6与PA7需配置为复用功能推挽输出(Alternate Function Push-Pull),而非普通推挽(GPIO_MODE_OUTPUT_PP)。区别在于:
- 普通推挽:输出由GPIO_BSRR/BSRR寄存器直接控制;
- 复用推挽:输出由定时器捕获/比较寄存器(CCR)经AFIO模块驱动,GPIO仅提供物理通路。

配置代码要点:

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;      // 核心:必须为AF_PP
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速确保PWM边沿陡峭
GPIO_InitStruct.Pull = GPIO_NOPULL;           // 舵机信号为强驱动,无需上下拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

若误设为 GPIO_MODE_OUTPUT_PP ,则TIM3的PWM输出将被GPIO寄存器覆盖,实测现象为:PA6/PA7输出恒定高电平或低电平,示波器观测不到PWM波形。

2.4 定时器基础参数与高级控制寄存器

TIM3初始化不仅需设定时基(PSC/ARR),还需明确计数模式与重复计数器(RCR):

htim3.Instance = TIM3;
htim3.Init.Prescaler = 35;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数,匹配CCRx时置高/低
htim3.Init.Period = 19999;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim3.Init.RepetitionCounter = 0; // 高级定时器才用,通用定时器设为0

TIM_COUNTERMODE_UP 是PWM生成的基础——计数器从0递增,当等于CCR1时触发通道1动作(如OC1REF置高),等于ARR时溢出并清零,同时触发更新事件(UEV)。此模式下,占空比由CCRx/ARR决定。

2.5 PWM通道配置与预装载使能

每个定时器通道(CH1–CH4)需独立配置输出比较模式。舵机仅需PWM高有效(Active High),即高电平持续时间决定角度:

TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;     // PWM模式1:计数器<CCRx时输出有效电平
sConfigOC.Pulse = 1000;                 // 初始脉宽1000 → 0.5 ms → 0°
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;  // 关闭快速模式,确保预装载生效
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);

TIM_OCMODE_PWM1 是关键:当CNT < CCRx时,OCxREF为高;CNT ≥ CCRx时为低。若误用 TIM_OCMODE_PWM2 (反相模式),则脉宽与角度关系将完全颠倒。

预装载(Preload)必须启用,否则CCRx寄存器更新会立即生效,导致PWM波形在非计数周期边界跳变,产生毛刺。启用方式:

__HAL_TIM_ENABLE_OCxPRELOAD(&htim3, TIM_CHANNEL_1); // 使能CH1预装载
__HAL_TIM_ENABLE_OCxPRELOAD(&htim3, TIM_CHANNEL_2); // 使能CH2预装载

最后启动定时器与通道:

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 启动CH1 PWM输出
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); // 启动CH2 PWM输出
HAL_TIM_Base_Start(&htim3);               // 启动计数器(必要!)

HAL_TIM_Base_Start() 易被忽略,但若未调用,计数器不运行,CCRx值再正确也无PWM输出。

3. 舵机角度控制函数设计与抗干扰实践

硬件配置完成后,软件层需提供直观、鲁棒的角度控制接口。直接操作CCR寄存器虽可行,但存在两大缺陷:一是脉宽与角度映射需重复计算,二是缺乏对舵机动态特性的适配。本节提出经过量产项目验证的封装方案。

3.1 角度-脉宽映射函数

建立线性映射关系,规避浮点运算:

#define SERVO_MIN_PULSE    500   // 0.5 ms → 0°
#define SERVO_MAX_PULSE    2500  // 2.5 ms → 180°
#define SERVO_ANGLE_MIN    0
#define SERVO_ANGLE_MAX    180

uint16_t Servo_AngleToPulse(uint8_t angle) {
    if (angle < SERVO_ANGLE_MIN) angle = SERVO_ANGLE_MIN;
    if (angle > SERVO_ANGLE_MAX) angle = SERVO_ANGLE_MAX;
    // 整数运算:pulse = min + (angle * (max-min)) / 180
    return SERVO_MIN_PULSE + (uint32_t)(angle) * (SERVO_MAX_PULSE - SERVO_MIN_PULSE) / 180;
}

该函数经编译器优化后生成纯整数指令,执行时间<1 μs。相比 pulse = 500 + angle * 11.11f ,避免了FPU依赖与舍入误差,在无硬件浮点单元的Cortex-M3上尤为关键。

3.2 带延迟补偿的舵机驱动函数

舵机响应非瞬时:从接收新脉宽到转子停稳需50–300 ms(取决于负载与电压)。若主循环以10 ms间隔连续调用 __HAL_TIM_SET_COMPARE() ,将导致舵机频繁启停、电流尖峰及机械磨损。实测数据显示,SG90在120°大角度阶跃时,稳定时间约220 ms;小角度(如10°)约80 ms。

因此,必须引入最小更新间隔(Minimum Update Interval)机制:

static uint32_t last_update_ms = 0;

void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle) {
    uint32_t now = HAL_GetTick();
    // 强制最小间隔200 ms,防止过快更新
    if ((now - last_update_ms) < 200) {
        HAL_Delay(200 - (now - last_update_ms));
    }
    last_update_ms = HAL_GetTick();

    uint16_t pulse = Servo_AngleToPulse(angle);
    __HAL_TIM_SET_COMPARE(htim, channel, pulse);
}

此设计在保证响应速度(200 ms内可达目标角度)的同时,彻底消除因软件刷新过快引发的机械抖动。某四足机器人项目曾因忽略此点,导致舵机在站立姿态微调时发出高频“哒哒”声,更换为带延时的驱动函数后问题消失。

3.3 主循环调用示例

main() 的无限循环中,按需调用舵机控制:

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

    // 初始化舵机至中位(90°)
    Servo_SetAngle(&htim3, TIM_CHANNEL_1, 90);
    Servo_SetAngle(&htim3, TIM_CHANNEL_2, 90);

    while (1) {
        // 示例:0°→90°→180°循环摆动
        Servo_SetAngle(&htim3, TIM_CHANNEL_1, 0);
        HAL_Delay(1500);
        Servo_SetAngle(&htim3, TIM_CHANNEL_1, 90);
        HAL_Delay(1500);
        Servo_SetAngle(&htim3, TIM_CHANNEL_1, 180);
        HAL_Delay(1500);
    }
}

HAL_Delay() 在此处承担双重角色:既是角度切换的视觉缓冲,也是舵机物理运动的等待窗口。若需更高实时性,可改用FreeRTOS任务延时或SysTick回调,但对教学级应用, HAL_Delay() 已足够。

4. 硬件连接规范与调试技巧

软件逻辑正确,硬件连接失误仍会导致系统失效。本节总结SG90与STM32F103C8T6(如Blue Pill开发板)连接的黄金法则,并提供低成本调试方案。

4.1 接线图与电气要求

舵机引脚 连接目标 注意事项
橙色(S) STM32 PA6 (TIM3_CH1) 信号线,长度建议<20 cm,避免与电机电源线平行布线以防串扰
红色(VCC) 外部5 V电源正极 严禁使用STM32的5 V引脚供电! Blue Pill的USB 5 V输出能力仅500 mA,SG90堵转电流达1 A,必烧USB接口或LDO
棕色(GND) STM32 GND + 外部电源GND 必须短接! 使用杜邦线将外部电源GND与开发板GND引脚可靠连接

典型错误接法:
- ✘ 将舵机VCC接开发板5 V引脚 → 开发板重启或USB端口损坏;
- ✘ 舵机GND未与MCU共地 → 示波器测PA6有波形,舵机无反应;
- ✘ 信号线过长且靠近电机驱动线 → PWM波形叠加高频噪声,舵机乱转。

4.2 舵机测试仪的使用方法

在烧录代码前,务必验证舵机本体是否完好。推荐使用专用舵机测试仪(如TowerPro SG90 Tester),其优势在于:
- 提供0°–180°连续可调PWM,旋钮调节直观;
- 内置LED指示当前脉宽,便于校准;
- 可快速区分故障源:若测试仪能驱动舵机,说明硬件正常,问题必在MCU代码或接线。

测试仪接线极简:仅需接入5 V电源(注意极性),舵机插头按S-VCC-GND顺序插入右侧接口(标注”S”+”VCC”+”GND”)。旋转电位器,舵机应平滑转动,无卡顿、异响或抖动。

若测试仪下舵机异常,可尝试:
- 更换电源(确认电压4.8–6.0 V且纹波<100 mV);
- 检查舵机齿轮是否进入死区(手动轻拨舵盘,听是否有“咔哒”声);
- 断电后短接信号线与GND 5秒,重置内部控制IC。

4.3 示波器波形诊断要点

当舵机不动作时,示波器是终极排查工具。测量PA6信号,预期波形特征:
- 周期 :严格20 ms(50 Hz),容差±100 μs;
- 高电平 :0.5–2.5 ms连续方波,边沿陡峭(上升/下降时间<100 ns);
- 低电平 :17.5–19.5 ms,无杂波;
- 占空比稳定性 :同一角度下,连续10个周期脉宽偏差<10 μs。

常见异常波形及对策:
- 无信号 :检查 HAL_TIM_PWM_Start() 是否调用;示波器探头接地夹是否接MCU GND;
- 周期错误(如10 ms) :PSC/ARR计算错误,或 HAL_TIM_Base_Start() 未调用;
- 脉宽跳变 :主循环中CCRx被意外修改,检查是否有其他任务或中断写入 __HAL_TIM_SET_COMPARE()
- 波形畸变(顶部塌陷) :PA6驱动能力不足,检查GPIO_Speed是否设为HIGH,或增加1 kΩ上拉电阻(虽非常规,但可临时验证)。

曾有一案例:舵机在0°与180°间振荡,示波器显示脉宽在500–2500间随机跳变。最终定位为 HAL_GetTick() 被错误地放在中断服务程序中调用,导致系统滴答计数器紊乱, last_update_ms 计算失准。修复中断中调用 HAL_GetTick() 的违规行为后恢复正常。

5. 扩展思考:双路PWM与电机控制衔接

本节配置的TIM3_CH1/CH2双通道,不仅服务于舵机,更为后续直流电机控制奠定基础。理解二者差异与协同,是嵌入式工程师进阶的关键。

5.1 舵机PWM vs 电机PWM的本质区别

特性 舵机(SG90) 直流电机(L298N驱动)
协议 50 Hz固定周期,脉宽编码角度 可变周期/频率,占空比编码转速
控制维度 单变量:角度(0–180°) 双变量:转速(0–100%)+ 方向(正/反)
输出需求 单路信号(S线) 两路互补信号(IN1/IN2)或一路PWM+方向电平

SG90仅需一路PWM,因其内部已集成方向逻辑;而H桥驱动的直流电机需两路信号协同:一路PWM决定功率,另一路电平决定流向。例如L298N典型控制:
- IN1=HIGH, IN2=LOW → 正转,PWM占空比决定速度;
- IN1=LOW, IN2=HIGH → 反转;
- IN1=IN2=LOW → 刹车;
- IN1=IN2=HIGH → 悬空(高阻态)。

5.2 TIM3双通道的复用策略

利用已配置的TIM3_CH1/CH2,可无缝扩展电机控制:
- CH1(PA6) → 电机PWM信号(接L298N的ENA);
- CH2(PA7) → 方向控制(接L298N的IN1),但需注意:CH2当前为PWM输出,需改为GPIO输出。

改造步骤:
1. 在 MX_TIM3_Init() 中,保留CH1的PWM配置;
2. 将CH2配置改为GPIO输出:
c HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); // 默认正转 HAL_GPIO_Init(GPIOA, &(GPIO_InitTypeDef){.Pin=GPIO_PIN_7, .Mode=GPIO_MODE_OUTPUT_PP});
3. 电机控制函数:
c void Motor_SetSpeed(int8_t speed) { // speed: -100 ~ +100 if (speed >= 0) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); // IN1=HIGH HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); // IN2=LOW } else { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET); // IN1=LOW HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET); // IN2=HIGH speed = -speed; } uint16_t pulse = (uint16_t)speed * 20; // 0-100% → 0-2000 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulse); }

此方案最大限度复用现有硬件资源,仅需软件重构,印证了嵌入式设计中“硬件一次设计,软件灵活定义”的工程哲学。在实际产品开发中,我们曾用同一块STM32F103C8T6同时驱动2个SG90舵机(云台俯仰/偏航)和1个直流电机(云台水平旋转),通过精细的时序调度与电源管理,实现了紧凑型智能云台的量产落地。

Logo

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

更多推荐