通用 PWM 原理基础教学

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 会随着时间按固定节拍递增(或递减)

  • 在常见的向上计数模式中:

  1. CNT 从 0 开始计数

  2. 一直加到 ARR

  3. 到达 ARR 后清零(或更新后回到 0)

  4. 如此循环 → 得到周期性的"锯齿/斜坡"计数过程

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 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. 实用配置示例(便于快速上手)

如果你希望计算更直观,可以让定时器计数"单位时间"更整齐,例如:

  1. 让计数频率变成 1MHz(1us 计一次数)
  • 72MHz / (PSC+1) = 1MHz

  • PSC = 71(因为 72MHz / 72 = 1MHz)

  1. 让周期变成 20ms
  • 20ms = 20,000us

  • 计数从 0 数到 ARR,一共 (ARR+1) 次

  • ARR = 20000 - 1 = 19999

  1. 通过 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);


			
			
		 }

}
相关推荐
小张程序人生21 小时前
ShardingJDBC读写分离详解与实战
数据库
木风小助理21 小时前
三大删除命令:MySQL 核心用法解析
数据库·oracle
tc&21 小时前
redis_cmd 内置防注入功能的原理与验证
数据库·redis·bootstrap
麦聪聊数据21 小时前
MySQL 性能调优:从EXPLAIN到JSON索引优化
数据库·sql·mysql·安全·json
Facechat21 小时前
视频混剪-时间轴设计
java·数据库·缓存
lalala_lulu21 小时前
MySQL中InnoDB支持的四种事务隔离级别名称,以及逐级之间的区别?(超详细版)
数据库·mysql
曹牧1 天前
Oracle:大量数据删除
数据库·oracle
小四的快乐生活1 天前
大数据SQL诊断(采集、分析、优化方案)
大数据·数据库·sql
CV工程师的自我修养1 天前
你的SQL为什么慢?看懂MySQL EXPLAIN执行计划,快速定位性能瓶颈
数据库·mysql