1. PWM 是什么
PWM(Pulse Width Modulation)中文常称"脉冲宽度调制"。
它的核心思想是:在固定周期 内,通过改变信号处于高电平的时间长度 (也就是"脉冲宽度"),来等效地改变输出的平均电压/平均功率,从而实现对能量的可控输出。
你可以把 PWM 理解为一种"用数字开关模拟模拟量"的方法:
-
开关只有 0/1(低/高电平)
-
但通过"高电平持续多久"的变化,就能得到不同的"平均效果"
2. 占空比、周期、频率
2.1 周期(Period)
PWM 信号每重复一次的时间长度记为 T。
2.2 频率(Frequency)
频率 f 与周期 T 的关系:
- f = 1 / T
2.3 占空比(Duty Cycle)
占空比 D 指在一个周期内,信号为高电平的时间占整个周期的比例:
- D = Thigh / T × 100%
例如:
-
占空比 50%:高电平时间与低电平时间相同(常见"方波"效果)
-
占空比越大:高电平越"宽",平均能量越高
-
占空比越小:高电平越"窄",平均能量越低
3. PWM 能做什么(常见应用)
PWM 在单片机/嵌入式里非常常用,典型用途包括:
-
呼吸灯/LED 亮度调节(改变平均电流/亮度)
-
直流电机调速(改变平均电压/功率)
-
舵机控制(用固定周期 + 特定脉宽表达角度)
-
蜂鸣器/音调输出(通过频率控制音高,通过占空比/开关控制响度等)
-
电源控制、加热控制(功率调节)
4. 用定时器产生 PWM 的基本思想(以常见定时器为例)
很多 MCU(例如 STM32)都是通过定时器 Timer 硬件来输出 PWM。
在 PWM 模式下,通常由两个关键寄存器决定 PWM 的形态:
-
TIMx_ARR(Auto-Reload Register,自动重装载寄存器)
-
决定计数器的"上限"
-
直接决定 周期/频率
-
-
TIMx_CCRx(Capture/Compare Register,捕获比较寄存器)
-
决定"比较阈值"
-
直接决定 占空比(高电平宽度)
-
一句话总结:
ARR 定周期(频率),CCR 定占空比(高电平持续时间)
5. 图示对应的工作过程解释(计数器 + 比较)
5.1 计数器 CNT 的行为
-
横坐标:时间
-
纵坐标:CNT 计数值
-
CNT 会随着时间按固定节拍递增(或递减)
-
在常见的向上计数模式中:
-
CNT 从 0 开始计数
-
一直加到 ARR
-
到达 ARR 后清零(或更新后回到 0)
-
如此循环 → 得到周期性的"锯齿/斜坡"计数过程
5.2 比较值 CCR 的作用
-
CCRx 是一个固定(或可动态修改)的比较阈值
-
定时器把 CNT 与 CCRx 进行比较
-
根据 PWM 输出模式配置,决定输出在比较前后是高电平还是低电平
以最常见的 **PWM Mode 1(边沿对齐,上计数)**为例(常见默认逻辑):
-
当 CNT < CCRx 时:输出为 高电平
-
当 CNT ≥ CCRx 时:输出为 低电平
于是就得到:
-
CNT 从 0 增长到 CCRx 的这段时间 → 输出高电平
-
CNT 从 CCRx 增长到 ARR 的这段时间 → 输出低电平
-
下一次 CNT 清零 → 输出重新回到高电平段
最终形成连续 PWM 波形
6. 频率与占空比如何由 ARR、CCR 决定
6.1 周期/频率与 ARR 的关系
若定时器计数时钟频率为 f_cnt(它通常由系统时钟经过分频得到):
-
PWM 周期 T ≈ (ARR + 1) / f_cnt
-
PWM 频率 f_pwm ≈ f_cnt / (ARR + 1)
所以:ARR 越大 → 周期越长 → 频率越低
ARR 越小 → 周期越短 → 频率越高
(很多 MCU 里还会有一个 PSC 预分频器,进一步决定 f_cnt,但这里先抓住"ARR 控周期"这一核心即可。)
6.2 占空比与 CCR 的关系(边沿对齐上计数常见情况)
- 占空比 D ≈ CCRx / (ARR + 1) × 100%
所以:CCR 越大 → 高电平越宽 → 占空比越大
CCR 越小 → 高电平越窄 → 占空比越小
7. 如何调参(最实用的结论)
你想控制 PWM 的两个关键指标:
7.1 想改频率(周期) → 改 ARR(以及可能的 PSC)
- 频率太高/太低,优先调整 ARR(必要时配合 PSC)
7.2 想改占空比 → 改 CCR
- 亮度/转速需要线性变化时,通常就是动态更新 CCR
8. 小结
-
PWM 通过改变高电平持续时间 来改变输出的平均能量
-
占空比 D = 高电平时间 / 周期
-
定时器 PWM 常用两个核心量:
-
ARR:决定周期/频率
-
CCR:决定占空比/脉宽
-
-
CNT 在 0~ARR 之间循环计数,CNT 与 CCR 比较后产生高低电平,从而形成 PWM 波形
-
实际使用中:
-
调频率:动 ARR(或 PSC)
-
调占空比:动 CCR
-

SG90 舵机 PWM 控制基础教学文档
1. SG90 引脚与接线说明
SG90 舵机一般有 3 个引脚(3 根线):
-
红线(VCC) :电源正极,通常接 5V
-
棕线(GND):电源地
-
橙线(信号线):PWM 信号输入端,接单片机的 PWM 输出引脚,用来接收单片机发送的 PWM
注意:舵机供电电流可能较大,建议电源地与单片机地 共地,供电尽量稳定。
2. 控制方法(核心思路)
控制 SG90 舵机旋转比较简单:
只需要给它输入 PWM 波形 ,通过修改 PWM 的 高电平持续时间(脉宽)/占空比,就可以改变舵机角度。
SG90 的控制一般使用:
-
周期 T ≈ 20ms(约等于 50Hz)
-
**高电平持续时间(脉宽)**一般在 0.5ms ~ 2.5ms
3. 高电平时间与角度的对应关系(示例表)
| 高电平持续时间(ms) | 舵机角度(°) | 占空比 |
|---|---|---|
| 0.5 | 0° | 2.5% |
| 1.0 | 45° | 5% |
| 1.5 | 90° | 7.5% |
| 2.0 | 135° | 10% |
| 2.5 | 180° | 12.5% |
占空比计算方式(周期 20ms 时):
- 占空比 = 高电平时间 / 20ms × 100%
例如:1.5ms / 20ms = 7.5%
4. 用定时器配置 20ms 周期(ARR/PSC 关系)
当定时器时钟为 72MHz 时(示例),周期可用下式描述:
T = 20ms = (ARR + 1) × (PSC + 1) / 72,000,000
含义:
-
PSC:预分频系数(Prescaler)
-
ARR:自动重装载值(Auto-Reload Register)
-
T:PWM 周期(这里要求约 20ms)
结论:
调 ARR / PSC → 调 PWM 周期(频率)
调 CCR → 调 PWM 高电平宽度(占空比/舵机角度)
5. 实用配置示例(便于快速上手)
如果你希望计算更直观,可以让定时器计数"单位时间"更整齐,例如:
- 让计数频率变成 1MHz(1us 计一次数)
-
72MHz / (PSC+1) = 1MHz
-
取 PSC = 71(因为 72MHz / 72 = 1MHz)
- 让周期变成 20ms
-
20ms = 20,000us
-
计数从 0 数到 ARR,一共 (ARR+1) 次
-
取 ARR = 20000 - 1 = 19999
- 通过 CCR 控制角度(高电平时间)
-
若 1us/计数,则:
-
0.5ms = 500us → CCR = 500
-
1.0ms = 1000us → CCR = 1000
-
1.5ms = 1500us → CCR = 1500
-
2.0ms = 2000us → CCR = 2000
-
2.5ms = 2500us → CCR = 2500
-
这样你只需要改 CCR,就能让舵机转到不同角度。
例
技术周期20ms,频率50hz
20ms = 0.02s = (200-1) *(7200-1)/72000000
共200次计数
|--------|------|-------|
| | | 占空比 |
| 195/5 | 0° | 2.5% |
| 190/10 | 45° | 5% |
| 185/15 | 90° | 7.5% |
| 180/20 | 135° | 10% |
| 175/25 | 180° | 12.5% |
代码
用的是定时器3下通道2的PB5

SG90.C
#include "stm32f10x.h"
#include "SG90.h"
/*
* ========================= SG90_init 说明 =========================
* 功能:使用 TIM3_CH2 通过"部分重映射"输出到 PB5,产生 50Hz PWM 驱动 SG90 舵机
*
* 你的参数:
* PSC = 7200-1 -> 计数频率 = 72MHz / 7200 = 10kHz -> 1 tick = 0.1ms = 100us
* ARR = 200-1 -> 周期 = 200 * 0.1ms = 20ms -> 50Hz
*
* 你使用的是:PWM1 + 低极性 TIM_OCPolarity_Low
* CNT < CCR2 -> 低电平
* CNT >= CCR2 -> 高电平
* 所以 高电平脉宽 = (200 - CCR2) * 0.1ms
*
* 你 sg90_angel_set() 中:
* CCR2=195 -> 高电平 0.5ms
* CCR2=185 -> 高电平 1.5ms
* CCR2=175 -> 高电平 2.5ms
*
* 注意:SG90 一般要求 5V 供电,并且舵机 GND 必须与 STM32 GND 共地。
* ================================================================
*/
void SG90_init(void)
{
/* ===================== 1. 定义配置结构体 ===================== */
// 舵机引脚(PB5)GPIO 配置结构体
GPIO_InitTypeDef sg90_init_struct;
// TIM3 基本定时器(时基)配置结构体
TIM_TimeBaseInitTypeDef sg90time_initstruct;
// TIM3 输出比较(PWM)配置结构体
TIM_OCInitTypeDef oc_initstruct;
/* ===================== 2. 使能外设时钟 ======================= */
// 使能 GPIOB 时钟(PB5 在 GPIOB)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 使能 AFIO 时钟(使用重映射必须开启 AFIO)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// 使能 TIM3 时钟(TIM3 在 APB1)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
/* ===================== 3. 引脚重映射 ========================= */
// 将 TIM3 的通道输出做"部分重映射",使 TIM3_CH2 输出到 PB5
//(注意:需要先开 AFIO 时钟)
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
/* ===================== 4. GPIO 配置 ========================== */
// PB5 配置为复用推挽输出 AF_PP,用于输出 TIM3_CH2 PWM
sg90_init_struct.GPIO_Mode = GPIO_Mode_AF_PP;
sg90_init_struct.GPIO_Pin = GPIO_Pin_5;
sg90_init_struct.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOB, &sg90_init_struct);
/* ===================== 5. TIM3 时基配置(50Hz) =============== */
// 计数时钟分频
sg90time_initstruct.TIM_ClockDivision = TIM_CKD_DIV1;
// 向上计数
sg90time_initstruct.TIM_CounterMode = TIM_CounterMode_Up;
// 自动重装载 ARR = 200-1 -> 200 tick -> 20ms 周期(50Hz)
sg90time_initstruct.TIM_Period = 200 - 1;
// 预分频 PSC = 7200-1 -> 10kHz 计数频率 -> 0.1ms 每 tick
sg90time_initstruct.TIM_Prescaler = 7200 - 1;
// 普通定时器无重复计数器(该字段对 TIM3 无意义,但写出来更清晰)
// sg90time_initstruct.TIM_RepetitionCounter = 0;
// 将时基配置写入 TIM3
//(注意:StructInit 可用,但你这里直接赋值完整关键字段也可用)
TIM_TimeBaseInit(TIM3, &sg90time_initstruct);
/* ===================== 6. PWM 输出比较配置(CH2) ============== */
// PWM1 模式:比较输出为 PWM 波形
oc_initstruct.TIM_OCMode = TIM_OCMode_PWM1;
// 输出极性:Low
// Low 的含义:CNT < CCR2 输出低;CNT >= CCR2 输出高
// 所以高电平宽度 = (ARR+1 - CCR2) * tick_time
oc_initstruct.TIM_OCPolarity = TIM_OCPolarity_Low;
// 使能输出比较通道输出
oc_initstruct.TIM_OutputState = TIM_OutputState_Enable;
// 初始比较值(CCR2)=185 -> 高电平 (200-185)*0.1ms = 1.5ms(中位)
oc_initstruct.TIM_Pulse = 185;
// 初始化 TIM3 通道2(CH2)输出比较为 PWM
TIM_OC2Init(TIM3, &oc_initstruct);
// 使能 CCR2 预装载(写 CCR2 时不会立刻影响输出,在更新事件时生效,更平滑)
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
/* ===================== 7. 启动定时器 ========================== */
// 开启 TIM3 开始计数并输出 PWM
TIM_Cmd(TIM3, ENABLE);
}
/*
* ===================== sg90_angel_set 说明 =====================
* 你现在是"离散档位"角度控制:
* 0° -> CCR2=195 -> 高电平 0.5ms
* 45° -> CCR2=190 -> 高电平 1.0ms
* 90° -> CCR2=185 -> 高电平 1.5ms
* 135° -> CCR2=180 -> 高电平 2.0ms
* 180° -> CCR2=175 -> 高电平 2.5ms
*
* 注意:
* 1) SG90 不是每个都能安全到 0.5ms~2.5ms,有些会顶死/抖动,常用更安全范围是 1.0ms~2.0ms
* 2) 如果你把极性改成 High,那么 CCR2 就应该是 5~25(或 10~20),不能再用 175~195
* ==============================================================
*/
void sg90_angel_set(uint16_t angle)
{
switch(angle)
{
case 180:
// 2.5ms 高电平
TIM_SetCompare2(TIM3, 175);
break;
case 135:
// 2.0ms 高电平
TIM_SetCompare2(TIM3, 180);
break;
case 90:
// 1.5ms 高电平
TIM_SetCompare2(TIM3, 185);
break;
case 45:
// 1.0ms 高电平
TIM_SetCompare2(TIM3, 190);
break;
case 0:
// 0.5ms 高电平
TIM_SetCompare2(TIM3, 195);
break;
default:
// 传入非 0/45/90/135/180 时不处理(保持当前角度)
// 更好的写法见下方"改进建议"
break;
}
}
/*
* ========================= 改进建议(不改你的命名) =========================
*
* 1) 建议在 init 中补充"结构体初始化函数",避免未赋值字段导致潜在问题:
* GPIO_StructInit(&sg90_init_struct);
* TIM_TimeBaseStructInit(&sg90time_initstruct);
* TIM_OCStructInit(&oc_initstruct);
* 你现在虽然手动填了关键字段,但结构体里还有其它字段,初始化会更稳。
*
* 2) 建议加上 ARR 预装载(周期更稳定):
* TIM_ARRPreloadConfig(TIM3, ENABLE);
*
* 3) 建议 sg90_angel_set 支持任意 0~180 的角度(用公式映射),避免 switch 只有 5 档:
* - 仍保持你的 Low 极性逻辑:CCR2 = 200 - pulse_ticks
* - pulse_ticks 建议在 10~20(1ms~2ms)更安全
*
* 4) 建议加"限幅/保护",避免舵机顶到机械限位抖动或发热:
* 如果你的舵机在 0.5/2.5ms 会抖,就把范围改成 1.0/2.0ms 对应 CCR2=190..180
*
* 5) 硬件层面必须确认:
* - SG90 供电 5V(电流要够,USB/板载 LDO 可能不够)
* - 舵机 GND 与 STM32 GND 共地(否则信号无参考)
* - PB5 确实接到舵机信号线,并确认你用的是 TIM3_CH2 的"部分重映射"引脚
* =======================================================================
*/
SG90.h
#ifndef _SG90_H
#define _SG90_H
#include "stm32f10x.h"
void SG90_init(void);
void sg90_angel_set(uint16_t angle);
#endif
main.c
#include "stm32f10x.h"
#include "main.h"
#include "stdio.h"
#include "bsp_time.h"
#include "SG90.h"
void delay(uint16_t time)
{
uint16_t i=0;
while(time--)
{
i=12000;
while(i--);
}
}
int main()
{
//\r\n回车、换行两个动作
SG90_init();
//sg90_angel_set();这种函数不用在这写,这里是调用函数,没用到就不写
while(1)
{
sg90_angel_set(180);
delay(1000);
sg90_angel_set(135);
delay(1000);
sg90_angel_set(90);
delay(1000);
sg90_angel_set(45);
delay(1000);
sg90_angel_set(0);
delay(1000);
}
}