PWM详解
一 PWM基本介绍
本章简单介绍PWM基本特征
1 PWM基础简介
PWM(脉冲宽度调制)是一种通过调整脉冲宽度
来控制输出信号的技术。(常用于通过设置数字引脚的高低电平持续时间
,来控制各种设备的运行状态
,速度亮度声音等)
(1) 基本原理
- PWM信号是一种
周期固定
、占空比可调的方波信号。其波形由一系列等幅不等宽的矩形脉冲组成,脉冲的宽度(即高电平持续时间)是可调节的,而周期保持不变 - 产生原理:通过改变脉冲的宽度和周期来模拟不同的电压或功率级别。在
一个周期内
,通过改变高电平
的持续时间
占整个周期的比例,即占空比,来实现对输出信号
的调制
。例如,若占空比为 50%,则在一个周期内高电平时间和低电平时间相等,平均电压为电源电压的一半。
(2) 基础参数
- 频率:指1秒内
多少个
完整的PWM周期,单位为赫兹(Hz)。它决定了PWM信号的更新速度,较高的频率可以使负载响应更平滑。 - 周期:PWM输出一个完整信号的
时长
,为频率Hz的倒数,单位为秒
。 - 占空比:表示高电平在一个周期内所占的比例,通常用百分比表示。例如,如果高电平持续时间为8ms,周期为10ms,那么占空比就是80%。通过调整占空比,可以控制输出信号的平均功率或电压(一般不需要直接设小数比例)
- 实现原理:
微控制器
可以基于定时器
,精确地控制数字引脚输出高电平或低电平的时间,例如在一个10ms的周期,高电平持续时间为2毫秒,低电平持续时间为8毫秒,依靠对定时器的设定,即可满足不同的PWM频率,控制不同外设。
(4) 应用领域
- 电机控制:通过调整 PWM 信号的占空比,可以精确地控制电机的
转速
和扭矩。例如,在直流电机调速中,占空比越大,电机的平均电压越高,转速就越快。 - LED 调光:利用 PWM 技术可以实现 LED 灯的
亮度调节
。通过改变 PWM 信号的占空比和频率,可以使 LED 灯以不同的亮度发光,并且可以避免闪烁现象。 - 通信领域:在一些通信协议中,PWM 可用于信号的传输和编码。例如,通过改变 PWM 信号的占空比和频率来表示不同的数据信息。
- 电力电子设备控制:用于控制逆变器、整流器等电力电子设备的输出电压和电流,实现对电网的电能质量控制和节能降耗。
频率区别
时钟
频率50Hz:每秒可运算50次,MHz
为百万次,GHz
为十亿次
PWM
频率50Hz:每秒完成50个完整的PWM周期,一个PWM周期
时长:1/50=0.02s=20ms
2 定时器简介
(1) 基本概念
定时器是一种具有计数
和定时功能的仪器组件,在芯片中主要负责精确测量时间间隔、产生定时中断以及提供时钟信号等功能(简单理解就是个可以控制间隔的计数器
)
(2) 分类
- 高级定时器:如 TIM1、TIM8,通常具有更丰富的功能和更高的性能,适用于对时间精度和功能要求较高的应用,如电机控制、高精度脉冲输出等(可产生7路PWM)
- 通用定时器:包括 TIM2-TIM5、TIM9-TIM14 等,其功能相对较为平衡,可满足一般的定时、计数、
PWM
输出等需求。(可产生4路PWM) - 基本定时器:如 TIM6、TIM7,功能相对简单,主要用于一些基础的定时任务。
- 系统定时器:SysTick 是一个特殊的定时器,位于 Cortex-M0 内核中,常用于操作系统的时钟节拍、延时函数等。
(3) 主要特点
- 多种工作模式:包括定时器模式、计数器模式、
PWM 模式
、输入捕获模式、输出比较模式等,可根据不同的应用场景选择合适的工作模式。 - 高精度:通过合理的配置
计数器
、预分频器
和自动重载寄存器
等参数,可以实现高精度的时间测量和控制,满足各种对时间精度要求较高的应用需求。 - 可编程性:用户可以通过编写程序对定时器的各项参数进行灵活配置,如
计数方向
、计数周期
、输出极性
等,以实现不同的功能。 - 中断和 DMA 支持:定时器可以产生中断请求,当定时器计数到达设定值或发生特定事件时,会触发中断,以便及时处理相应的任务。同时,定时器也可以与 DMA 配合使用,实现数据的自动传输和处理,提高系统的运行效率
二 PWM实现
本章重点介绍频率计算法,与定时器配置,并深入介绍PWM
1 定时器频率设定(重要)
(1) 频率计算原理
-
定时器实际频率:
总线时钟频率
/分频系数
,就是降频,如 72Mhz/2=36MHz,这个分频系数就叫PSC
。即每秒最大可计数3600万次,这只不过代表了计数能力
,想要实现计时,还需要设置时间间隔。 -
时间间隔:就是在该计数能力下,
计数多少次
才算一个间隔。就像表针每秒转一次,微控制器定时器的时间间隔,可以设的远远比1s更精确。假设在7200Hz下,间隔设为每计数72次完成一个间隔,那么72就是定时器的计数上限
,即ARR
。达到该上限后,计数清0,进入下一个时间间隔(因为日常理解都是以s为单位,所以易混淆)。 -
定时器周期:还是以实际频率7200Hz,72次为计数上限为例,可知1s可计数7200次,那么计数72次需要时长为:72/7200*1=0.01s=10ms,这便是
定时器周期
,公式表示即:计数上限/(总时钟频率/分频次数)
以控制SG90
舵机为例,舵机本身的PWM频率为50Hz
,则周期为1/50=20ms
,需要将定时器对应修改,可灵活修改:
时钟频率 | 分频系数 | 实际定时器频率 | 计数上限 | 定时器周期 |
---|---|---|---|---|
7200 0000(即72MHz) | 72 | 100 0000 | 20000 | 2w/100w=20ms |
72MHz | 7200 | 10000 | 200 | 200/1w=20ms |
并且,计数上限越大,则可控精度
便越高
。显而易见,2w会比20控制更精确。顺便一提,SG90舵机本身的控制方式:
- 一个PWM周期为20ms
- 占空比为 0.5ms, 1ms,1.5ms,2ms, 2.5ms时
- 对应的角度:0° 45° 90° 135° 180°
- 也就是
20ms
内,前0.5m通高电平
其余时间低电平,舵机转动到0°
位置, 0.5ms~ 1ms之间对应0-45°,其余同理。(对没错,就是这么精准)
定时器周期计算化简为字母公式就是:定时器周期=(PSC+1)(ARR+1)/系统时钟频率,不过字母的不怎么好看,啧,一套又一套的。(+1是因为配置本身数值问题,无需纠结)
(2) 定时器配置步骤
- 开RCC时钟,配置定时器时钟源
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开启TIM2的时钟
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟
- 时基单元初始化
名称 | 介绍 |
---|---|
TIM_Prescaler(PSC)分频系数 | 设置定时器的预分频 值,调整TIM_Prescaler的值,可以改变定时器的计数速度。如果TIM_Prescaler的值较大,定时器的计数速度就会变慢;反之,越小越快。除法计算 |
TIM_CounterMode | 设置定时器的计数方向 ,向上计数模式、向下计数模式和中心对齐计数模式等。在向上计数模式下,计数器从0开始计数,每次计数递增,达到最大值归计数0并进入下一个时间间隔 |
TIM_Period(ARR)计数上限 | 一次时间间隔的 计数上限 |
TIM_ClockDivision | 设置定时器的时钟分频因子,库函数提供了几个固定参数,实现不同的计数精度和计数速率 |
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 20000; //ARR的值,计数上限
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
2 PWM 定时器比较模式设定
PWM需要使用定时器的输出比较模式
,需要定时器在特定的时间点输出一个信号,通过定时器的输出比较模式,可以精确地设定这个时间点,当计数器的值与预设的比较值匹配时,输出相应的信号,从而实现占空比设置。
(1) 输出比较模式配置
- TIM_OCMode:选择
PWM模式
- TIM_OCPolarity:定时器输出引脚
输出
高低电平
设定 - TIM_OutputState:是否开启比较
- TIM_Pulse:其实就是
初始化时
的占空比
配置好后,需要打开对应引脚的通道
即可(通用定时器可支持4路PWM输出,高级定时器支持7路PWM输出)
//用于设置定时器的输出比较模式
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化.给结构体所有成员都赋一个默认值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //当计数器的值与 捕获/比较寄存器的值相等时,定时器的输出引脚是输出高电平还是低电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较通道是否打开
TIM_OCInitStructure.TIM_Pulse = 0; //初始化占空比
//配置TIM2的输出比较通道
TIM_OC1Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC1Init通道
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //1、2分别对应PA0、PA1
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
(2) 占空比设定
通过TIM_SetCompareX函数,直接设置对应通道占空比。注意这里并非填入一个比例
值得,而是根据计数上限
ARR设定:
如:ARR=20000,一个定时周期为20ms,前0.5m要设置高电平。
则 compare的值为:0.5 / 20 * 20000=500
//设置占空比函数
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
由上述例子可得出,在SG90舵机的例子中,Compare=500对应舵机的0°,同理可计算出180°:2.5/20*20000=2500,故该频率和计数上限下:
500~2500
可表示舵机的0-180°,按比例对应皆可控制舵机角度。
3 深入剖析PWM
虽然数字引脚本身只有高和低两种状态,但PWM通过快速切换
这两种状态并调整
其持续时间
,可以模拟出不同的输出电平效果,从而实现调控输出的电平大小的效果。基与原理如下:
(1) 冲量相等效果相同原理
当冲量(即窄脉冲的面积)相等而形状不同的窄脉冲加在具有惯性
的环节上时,其效果基本相同。在PWM(脉冲宽度调制)控制技术中,这一原理被广泛应用。通过对一系列脉冲的宽度进行调制,来等效地获得所需要的波形(包括形状和幅值)。
(2)信号等效原理
在某些特定条件下,一个复杂的信号可以用另一个 简单的信号来等效或替代,而这种替代不会对系统或分析产生实质性的影响。
LED的有趣特性
在PWM控制LED亮度场景中,而由于人眼的视觉残留效应,人眼就会感觉不到高频
的LED闪烁
,看到的是一个稳定的常亮效果,类似于一种“惯性”效果。但当你通过修改定时器,把PWM实际频率调为60hz,占空比哪怕只设为1/ARR
,用手机摄像头录像就可以捕捉到,其实LED是在高速闪烁
,本身亮度没有衰弱。
总结:
<1> 通过改变高电平持续的时间,就可以控制负载
在一个周期内接收的平均电流
或能量
,从而改变其亮度或工作状态。
<2> 表现:高电平持续时间较长
,那么负载就会接收到更多的能量,表现得更亮或工作更强烈;反之,如果高电平持续时间较短,负载就会接收到较少的能量,表现得较暗或工作较弱。
三 代码实现
本章为几个简单实例,含基础PWM驱动,与ServoSG90舵机驱动。本章阅读及建议目录跳转
1 PWM基本驱动
(1)头文件
//PWM.h
#ifndef __PWM_H
#define __PWM_H
/*
Tim2 4 通道可控制4路PWM信号,使用2例
PWM输出引脚:A0 A1
*/
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);//A0
void PWM_SetCompare2(uint16_t Compare);//A1
#endif
(2)实现文件
//PWM.c
#include "stm32f10x.h" // Device header
#include "PWM.h"
/*
功能:PWM初始化
*/
void PWM_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0引脚初始化为复用推挽输出
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 20000; //即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
//用于设置定时器的输出比较模式
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化.给结构体所有成员都赋一个默认值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //当计数器的值与 捕获/比较寄存器的值相等时,定时器的输出引脚是输出高电平还是低电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较通道是否打开
TIM_OCInitStructure.TIM_Pulse = 0; //初始化占空比
//配置TIM2的输出比较通道
TIM_OC1Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC1Init,
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //1、2分别对应PA0、PA1
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/*
功能:TIM2通道1,占空比 (改变每个周期内高电平或低电平的持续时间)
参数:Compare要写入的CCR的值,范围:0~20000 ,即为ARR范围
返回值:无
注意:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
占空比: CCR/(ARR+1)
*/
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //设置CCR1的值,精确地设置高电平持续时间
}
/*
功能:TIM2通道2,占空比
-同上-
*/
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare);
}
(3)main函数测试文件
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
//简易测试
void servo(int arr){
int r=1.0*arr/180*2000+500;
PWM_SetCompare1(r);
}
void LED(int arr){
int r=1.0*arr/1024*20000;
PWM_SetCompare2(r);
}
/*
舵机A0
LED A1
*/
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
PWM_Init(); //PWM初始化
/**/
servo(140);
while (1)
{
/*
舵机测试 与 LED测试
*/
for(int i=0;i<=180;i++){
servo(i);
OLED_ShowNum(1,1,i,3);
Delay_ms(15);
}
for(int i=0;i<=1024;i++){
LED(i);
Delay_ms(5);
}
Delay_ms(500);
}
}
2 ServoSG90舵机驱动
简易实现舵机度数设定
,增加
,获取角度
功能,2路舵机。
(1)头文件
//ServoSG90.h
#ifndef __SERVOSG90_H
#define __SERVOSG90_H
/*
ServoSG90输出引脚:A0 A1
*/
extern int Servo1_angle; //舵机1角度
extern int Servo2_angle; //舵机2角度
void Servo_Init(void); //所有舵机初始化
void Servo1_Set(double Compare);//设置舵机1角度
void Servo1_Add(int angle); //增加舵机1角度
int Get_Servo1(void); //获取舵机1角度
void Servo2_Set(double Compare);//设置舵机2角度
void Servo2_Add(int angle); //增加舵机2角度
int Get_Servo2(void); //获取舵机2角度
#endif
(2)实现文件
//ServoSG90.c
#include "stm32f10x.h" // Device header
#include "ServoSG90.h"
int Servo1_angle=0;
int Servo2_angle=0;
/*
功能:Servo初始化
使用A0 A1引脚
*/
void Servo_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*配置时钟源*/
TIM_InternalClockConfig(TIM2);
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 20000; //即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
//用于设置定时器的输出比较模式
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化.给结构体所有成员都赋一个默认值
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //输出比较模式,选择PWM模式
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //当计数器的值与 捕获/比较寄存器的值相等时,定时器的输出引脚是输出高电平还是低电平
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较通道是否打开
TIM_OCInitStructure.TIM_Pulse = 0; //初始化占空比
//配置TIM2的输出比较通道
TIM_OC1Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC1Init通道
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //1、2分别对应PA0、PA1
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/*
功能:设置舵机1度数,
参数:0-180
*/
void Servo1_Set(double Compare)
{
if(Compare>=180)
Servo1_angle=180;
if(Compare<=0)
Servo1_angle=0;
Servo1_angle=Compare; //记录角度
double tem=Compare/180*2000+500;
TIM_SetCompare1(TIM2, tem); //设置CCR1的值,精确地设置高电平持续时间
}
/*
功能:增加舵机1度数,
参数:0-180,越界会自动归与边界
*/
void Servo1_Add(int angle){
Servo1_Set(Servo1_angle+angle);
}
/*
功能:获取舵机1度数,
参数:0-180,越界会自动归与边界
*/
int Get_Servo1(void){
return Servo1_angle;
}
/*
功能:设置舵机2度数,
参数:0-180
*/
void Servo2_Set(double Compare)
{
if(Compare>=180)
Servo2_angle=180;
if(Compare<=0)
Servo2_angle=0;
Servo2_angle=Compare; //记录角度
double tem=Compare/180*2000+500;
TIM_SetCompare2(TIM2, tem); //设置CCR1的值,精确地设置高电平持续时间
}
/*
功能:增加舵机2度数,
参数:0-180,越界会自动归与边界
*/
void Servo2_Add(int angle){
Servo2_Set(Servo2_angle+angle);
}
/*
功能:获取舵机2度数,
参数:0-180,越界会自动归与边界
*/
int Get_Servo2(void){
return Servo2_angle;
}
(3)main函数测试文件
#include "stm32f10x.h" // USE_STDPERIPH_DRIVER
#include "delay.h" //--no-multibyte-chars
#include "OLED.h"
#include "Serial.h"
#include "ServoSG90.h"
/*
1 串口连接:RX-A9 TX-A10
2 OLED连接:SCL-B8 SDA-B9
3 SercoSG90舵机:A0 A1
*/
int main(void)
{
Serial_Init();
OLED_Init();
OLED_ShowString(1,1,"Servo TEST");
Servo_Init();
int degree=3;
while (1)
{
/*舵机1 Set 遍历法
for(int i=0;i<=180;i++){
Servo1_Set(i);
OLED_ShowNum(2,1,Get_Servo1(),4);
Delay_ms(25);
}
*/
/*舵机1 Add遍历法 */
Servo1_Add(degree);
int t=Get_Servo1();
OLED_ShowString(2,1,"Servo1 angle:");
OLED_ShowNum(2,14,t,3);
Delay_ms(50);
if(t>=180) degree=-3; //角度增加-3
if(t<=0) degree=3; //角度增加3
/*舵机2 测试
Servo2_Add(degree);
int t=Get_Servo2();
OLED_ShowString(3,1,"Servo2 angle:");
OLED_ShowNum(3,14,t,3);
Delay_ms(50);
if(t>=180) degree=-3; //角度增加-3
if(t<=0) degree=3; //角度增加3
*/
}
}
3 AD控制舵机
此处为结合AD模数转换
与PWM
控制舵机,只需配置AD和PWM引脚:电位器控制舵机只需几行代码
AD_Init(); //A2
Servo_Init(); //A0 A1
while (1)
{
int degree=AD_GetValue(ADC_Channel_2);//采集模拟引脚值,比例转化为度数
degree=(degree+1)/4096.0*180;
Servo1_Set(degree); //PWM控制舵机
OLED_ShowString(2,1,"Servo angle:");
OLED_ShowNum(2,14,degree,3);
}