PWM舵机实验

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

137.5.9PWM舵机实验

/*
 * 整体目的:用 TIM4_CH1 在 PB6 输出约 50 Hz 的舵机控制脉冲,并让高电平宽度循环变化。
 * 参数依据:TIM4CLK=72 MHz,PSC=7199 后每个计数为 100 μs;ARR=200 时周期为
 *           (200+1)×100 μs=20.1 ms,比较值 5~25 对应约 0.5~2.5 ms 高电平。
 * 硬件依据:PB6 只输出逻辑控制信号;舵机由独立稳定 5 V 电源供电,并与 STM32 共地。
 * 上下文:配套工程把舵机更新挂在上一节 PWM_Value 的取余条件上,因此更新间隔并不完全均匀。
 * 代码范围:摘自 CubeIDE/07PWM 的 tim.c 和 main.c;HAL PWM 库内部不逐行展开。
 */

/* ==================== Core/Src/tim.c:TIM4_CH1 舵机 PWM ==================== */

TIM_HandleTypeDef htim4;                    /* 定义 TIM4 句柄,保存舵机 PWM 的时基和通道状态。 */

void MX_TIM4_Init(void)                    /* 配置约 50 Hz 的 TIM4_CH1 PWM。 */
{                                           /* TIM4 初始化函数体开始。 */
    TIM_ClockConfigTypeDef sClockSourceConfig = {0}; /* 清零时钟源配置结构体。 */
    TIM_MasterConfigTypeDef sMasterConfig = {0}; /* 清零主模式配置结构体。 */
    TIM_OC_InitTypeDef sConfigOC = {0};    /* 清零 PWM 输出比较配置结构体。 */

    htim4.Instance = TIM4;                 /* 让句柄指向 APB1 上的 TIM4 寄存器块。 */
    htim4.Init.Prescaler = 7199U;          /* 实际 7200 分频,使 CNT 每 100 μs 加 1。 */
    htim4.Init.CounterMode = TIM_COUNTERMODE_UP; /* 选择从 0 向 ARR 递增的计数方式。 */
    htim4.Init.Period = 200U;              /* ARR=200,周期包含 201 拍,即约 20.1 ms。 */
    htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 不额外分频滤波采样时钟。 */
    htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* ARR 在更新边界装载。 */

    if (HAL_TIM_Base_Init(&htim4) != HAL_OK) /* 配置 TIM4 基础时基。 */
    {                                       /* 基础初始化失败分支开始。 */
        Error_Handler();                    /* 失败时进入工程统一错误处理。 */
    }                                       /* 基础初始化结果判断结束。 */

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 使用内部定时器时钟。 */
    if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK) /* 应用时钟源。 */
    {                                       /* 时钟源配置失败分支开始。 */
        Error_Handler();                    /* 配置失败时停止正常业务。 */
    }                                       /* 时钟源配置结果判断结束。 */

    if (HAL_TIM_PWM_Init(&htim4) != HAL_OK) /* 初始化 TIM4 的 PWM 功能状态。 */
    {                                       /* PWM 初始化失败分支开始。 */
        Error_Handler();                    /* 失败时进入错误处理。 */
    }                                       /* PWM 初始化结果判断结束。 */

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; /* 不用 TIM4 触发其他外设。 */
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; /* 不使用主从同步。 */
    if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK) /* 应用主模式设置。 */
    {                                       /* 主模式配置失败分支开始。 */
        Error_Handler();                    /* 失败时进入错误处理。 */
    }                                       /* 主模式配置结果判断结束。 */

    sConfigOC.OCMode = TIM_OCMODE_PWM1;    /* 选择 PWM1,向上计数时 CNT<CCR 为高有效。 */
    sConfigOC.Pulse = 0U;                  /* CCR1 初始为 0,刚启动时没有有效高脉冲。 */
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; /* 参考有效状态输出高电平。 */
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; /* 关闭输出比较快速模式。 */
    if (HAL_TIM_PWM_ConfigChannel(&htim4, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) /* 配置 CH1。 */
    {                                       /* 通道配置失败分支开始。 */
        Error_Handler();                    /* 失败时停止正常业务。 */
    }                                       /* 通道配置结果判断结束。 */

    HAL_TIM_MspPostInit(&htim4);           /* 把 PB6 配置为 TIM4_CH1 复用推挽输出。 */
}                                           /* TIM4 初始化函数结束。 */

/* ==================== Core/Src/main.c:启动并循环改变舵机脉宽 ==================== */

uint8_t PWM_Value = 0U;                    /* 沿用呼吸灯循环的 0~199 相位,课程用它决定舵机更新时间。 */
uint8_t SG_PWM_Value = 5U;                 /* 舵机比较值初始设为 5,对应约 0.5 ms。 */

MX_TIM4_Init();                             /* 把 TIM4 的时基和 CH1 PWM 参数写入硬件。 */
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);  /* 启动 PB6/TIM4_CH1 输出;此时初始 CCR1 仍为 0。 */

if ((PWM_Value % 30U) == 0U)              /* 相位为 0、30、60……时更新舵机;回绕处间隔较短。 */
{                                           /* 舵机位置更新分支开始。 */
    SG_PWM_Value += 5U;                    /* 比较值每次增加 5,即脉宽增加约 0.5 ms。 */
    if (SG_PWM_Value > 25U)                /* 超过约 2.5 ms 的课程上限后回到起点。 */
    {                                       /* 舵机比较值回绕分支开始。 */
        SG_PWM_Value = 5U;                 /* 回到约 0.5 ms 的端点值。 */
    }                                       /* 舵机比较值回绕分支结束。 */
    __HAL_TIM_SET_COMPARE(                 /* 把新脉宽写入 TIM4 通道 1 的 CCR1。 */
        &htim4,                             /* 选择 TIM4 句柄。 */
        TIM_CHANNEL_1,                     /* 选择连接 PB6 的通道 1。 */
        SG_PWM_Value);                     /* 5~25 个 100 μs 节拍对应约 0.5~2.5 ms。 */
}                                           /* 本次舵机位置更新结束。 */

一、舵机控制原理回顾

1.1 舵机控制原理

常见 SG90 舵机使用约 20 ms 一帧的控制脉冲,即约 50 Hz。它主要读取高电平脉宽,而不是把高电平占空比当作普通电机调速量:

高电平脉宽本实验比较值(100 μs/格)常见位置含义
0.5 ms5一端
1.0 ms10靠近一端
1.5 ms15中位
2.0 ms20靠近另一端
2.5 ms25另一端

不同舵机的安全端点和角度范围可能不同。首次调试应从 1.5 ms 中位开始,逐渐扩大脉宽,若出现堵转、抖动或异常电流,应立即收窄范围。

舵机内部可理解为一个闭环系统:

flowchart LR
    P[输入脉宽] --> D[目标位置译码]
    S[电位器反馈实际角度] --> E{误差比较}
    D --> E
    E --> H[内部H桥]
    H --> M[直流电机与齿轮]
    M --> S

二、实验演示

2.1 定时器与PWM初始化设置

TIM4_CH1 使用 PWM 模式 1,预分频器为 7199。72 MHz 经分频后,计数频率为 10 kHz,每格 100 μs。代码先初始化,再调用 HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1) 启动输出。

2.2 定时器与引脚选择及配置

TIM4_CH1 默认输出到 PB6。三线舵机的连接通常是:

外部稳定5 V ───────── 红线(V+)
外部电源GND ───────── 棕/黑线(GND)
STM32 PB6/TIM4_CH1 ── 橙/黄线(信号)
STM32 GND ─────────── 外部电源GND

舵机电流远大于 GPIO 能力,启动和堵转时还会产生电源跌落与噪声。不要从 GPIO 供电,也不宜直接依赖开发板较弱的 3.3 V 稳压输出。使用独立 5 V 电源并共地,在舵机电源接口附近放置合适的电解电容和陶瓷去耦电容。

2.3 定时器参数配置与计算

工程配置 PSC=7199、ARR=200:

计数周期 = (7199+1) / 72 MHz = 100 μs
PWM周期  = (200+1) × 100 μs = 20.1 ms
PWM频率  ≈ 49.75 Hz

因为计数器从 0 计到 ARR,周期包含 ARR+1 个计数。若需要严格的 20.0 ms,应把 ARR 设置为 199。舵机通常能接受 20.1 ms,但理解这个加 1 对后续精确定时很重要。

2.4 定时器与PWM代码实现

工程把 SG_PWM_Value 初始化为 5,并在触发更新时先增加 5:

SG_PWM_Value += 5;
if (SG_PWM_Value > 25) {
    SG_PWM_Value = 5;
}
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, SG_PWM_Value);

于是变量在 0.5、1.0、1.5、2.0、2.5 ms 对应值之间循环。比较值改变的是高电平宽度,帧周期仍由 PSC 与 ARR 决定。

需要注意,初始化通道时 Pulse=0,启动 PWM 后代码没有立刻把初值 5 写入 CCR1;第一次满足更新条件时又先把 5 加到 10,所以引脚最初输出 0 脉宽,随后直接从 1.0 ms 开始。若希望上电后立即处于 0.5 ms 位置,应在启动 TIM4_CH1 后先执行一次 __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, SG_PWM_Value)。

2.5 例题1: 舵机控制实验演示

观察舵机时应确认三个现象:

  1. 每次脉宽改变后,舵机移动到新位置并保持;
  2. 中位附近应对应约 1.5 ms;
  3. 到端点时不应持续顶死、啸叫或明显发热。

若方向与预期相反,不要简单反接舵机电源;应在软件中反向映射脉宽。反接电源可能损坏舵机。

编译、下载并确认供电无误后再连接信号线。舵机或驱动模块底部若有裸露金属,不能直接放在开发板焊点上,应使用绝缘垫片,防止短路。

三、代码编写

3.1 定义舵机变量并赋值

uint8_t SG_PWM_Value=5 足以保存 5~25 的比较值。0 == 表达式 这类写法能让误写成赋值时更容易被编译器发现,但更重要的是开启编译器警告并认真处理警告。

工程用 PWM_Value % 30 == 0 作为舵机更新条件。由于 PWM_Value 在 0~199 间回绕,它会在 30、60、90、120、150、180、0 时触发;180 到回绕 0 只有 20 个 25 ms 步长,即 500 ms,不是稳定的 750 ms。

3.2 利用pw值控制

若要求每 750 ms 稳定更新一次,应使用独立时间基准:

uint32_t servo_last = HAL_GetTick();

if ((uint32_t)(HAL_GetTick() - servo_last) >= 750U) {
    servo_last += 750U;
    SG_PWM_Value += 5U;
    if (SG_PWM_Value > 25U) {
        SG_PWM_Value = 5U;
    }
    __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, SG_PWM_Value);
}

无符号减法写法能够自然处理 HAL_GetTick() 回绕。若主循环可能长时间阻塞,可以在条件满足时用循环补偿漏掉的节拍,或把舵机更新放入周期任务。