目录
前言
PWM( Pulse Width Modulation**):**一种通过调节脉冲信号的占空比(高电平持续时间与整个周期的比值)来控制输出功率或模拟信号的技术。它广泛应用于电机控制、LED亮度调节、音频信号生成以及电源管理等领域。通过改变占空比,PWM可以在保持频率不变的情况下精确地控制设备的能量输入或输出,同时具有高效、易于实现和低功耗的优点。

PWM参数
频率 = 1 / TS
占空比 = TON / TS
分辨率 = 占空比变化步距

技术实现
原理图
无
接线图

代码实现
main.c
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h" //延时函数
#include "OLED.h"
#include "PWM.h"
uint8_t duty; //控制PWM占空比
int main(void)
{
/*
OLED初始化
*/
OLED_Init();
PWM_Init();
while(1)
{
/*
输出PWM波形(此处使用一个变量即可,多一个变量将会多占用存储空间)
*/
for(duty=0;duty<=100;duty++)
{
PWM_SetComparel(duty);
Delay_ms(10);
}
for(duty=0;duty<=100;duty++)
{
PWM_SetComparel(100-duty);
Delay_ms(10);
}
}
}
PWM.h
cpp
#ifndef __PWM_H__
#define __PWM_H__
#include "stm32f10x.h" // Device header
void PWM_Init(void);
void PWM_SetComparel(uint16_t Compare);
#endif
PWM.c
cpp
#include "PWM.h"
/**
* @brief PWM初始化函数
* @param None
* @retval None
* @note 输出频率为1kHz,占空比为50%,分辨率为1%的PWM波形
**/
void PWM_Init(void)
{
/*
开启时钟
*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启TIM2时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA时钟
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//
// /*
// 引脚重映射
// */
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); //将TIM2的CH1从PA0重映射到PA15
// /*
// 解除引脚复用
// */
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //PA15引脚作为JTD1功能使用,此处解除AFIO的JTAG复用
/*
配置端口
*/
GPIO_InitTypeDef GPIO_InitStruct; //定义结构体
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //选择PA0 //重映射到GPIO_Pin_15
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出,将引脚的控制权交给片上外设
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
/*
选择TIM时钟
*/
TIM_InternalClockConfig(TIM2);
/*
配置时基单元
*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //定义一个TIM_TimeBaseInitTypeDef类型的结构体用于初始化时基单元
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分频模式为不分
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //计数方式设置为向上计数模式
/*
输出的PWM波形的频率为1kHz,分辨率为1%,占空比为50%
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
*/
TIM_TimeBaseInitStruct.TIM_Period = 100-1; //ARR,自动加载重装寄存器,要写入自动重装值
TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1; //PSC,预分频器的分频值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; //重复计数,只有高级定时器才会使用,通用定时器用不到
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); //初始化时基单元
/*
初始化输出比较单元
*/
TIM_OCInitTypeDef TIM_OCInitStruct; //定义一个TIM_OCInitTypeDef类型的结构体用以输出比较单元初始化
TIM_OCStructInit(&TIM_OCInitStruct); //给每个结构体成员赋初始值,防止因未使用到的结构体成员未初始化而导致程序出现错误
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //选择PWM模式1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //OC1有效电平为高电平
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //输出比较使能
/*
输出PWM的波形占空比
PWM占空比: Duty = CCR / (ARR + 1)
*/
TIM_OCInitStruct.TIM_Pulse = 0; //CCR,捕获/比较器,本工程中有封装的函数用于改变占空比,此处先设置为0
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
TIM_Cmd(TIM2,ENABLE); //启动TIM2
}
/**
* @brief 指定CCR寄存器的值改变PWM Duty
* @param None
* @retval None
* @note None
**/
void PWM_SetComparel(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
Delay.h
cpp
#ifndef __DELAY_H
#define __DELAY_H
void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);
#endif
Delay.c
cpp
#include "stm32f10x.h"
/**
* @brief 微秒级延时
* @param xus 延时时长,范围:0~233015
* @retval 无
*/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 * xus; //设置定时器重装值
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000)); //等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
/**
* @brief 毫秒级延时
* @param xms 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_ms(uint32_t xms)
{
while(xms--)
{
Delay_us(1000);
}
}
/**
* @brief 秒级延时
* @param xs 延时时长,范围:0~4294967295
* @retval 无
*/
void Delay_s(uint32_t xs)
{
while(xs--)
{
Delay_ms(1000);
}
}
内容要点
PWM基本结构

开启外设时钟
cpp
/*
开启时钟
*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启TIM2时钟
本实验使用TIM2输出调制PWM波来驱动LED呼吸灯,TIM2属于APB1外设,调用RCC_APB1PeriphClockCmd()函数来开启APB1外设时钟。
cpp
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启GPIOA时钟
根据STM32引脚的定义,PA0的默认复用功能为TIM2的输出比较通道之一TIM2_CH1,GPIOA隶属于APB2外设,调用RCC_APB2PeriphClockCmd()函数开启APB2外设时钟。
配置GPIO端口
cpp
/*
配置端口
*/
GPIO_InitTypeDef GPIO_InitStruct; //定义结构体
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //选择PA0 //重映射到GPIO_Pin_15
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出,将引脚的控制权交给片上外设
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
GPIO初始化中,初始化结构体GPIO_InitStruct结构体成员,将GPIO引脚设置为PA0 Pin,将GPIO模式设置为复用推挽输出,因为GPIO的主功能为PA0 IO口,这里使用其默认复用功能,故应将GPIO模式设置为复用推挽,使用普通的推挽输出无法输出PWM波形。

配置时基单元
cpp
/*
选择TIM时钟
*/
TIM_InternalClockConfig(TIM2);
选择TIM时钟为内部时钟
cpp
/*
配置时基单元
*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //定义一个TIM_TimeBaseInitTypeDef类型的结构体用于初始化时基单元
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分频模式为不分
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //计数方式设置为向上计数模式
/*
输出的PWM波形的频率为1kHz,分辨率为1%
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
*/
TIM_TimeBaseInitStruct.TIM_Period = 100-1; //ARR,自动加载重装寄存器,要写入自动重装值
TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1; //PSC,预分频器的分频值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; //重复计数,只有高级定时器才会使用,通用定时器用不到
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); //初始化时基单元
实验中驱动LED呼吸灯 ,输出的PWM波形为占空比(Duty)从0->100,然后Duty从100->0.按照输出PWM频率为1kHz,分辨率为1%计算出ARR(自动重装寄存器)和PSC(预分频器)的值。
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
初始化输出比较单元
cpp
/*
初始化输出比较单元
*/
TIM_OCInitTypeDef TIM_OCInitStruct; //定义一个TIM_OCInitTypeDef类型的结构体用以输出比较单元初始化
TIM_OCStructInit(&TIM_OCInitStruct); //给每个结构体成员赋初始值,防止因未使用到的结构体成员未初始化而导致程序出现错误
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; //选择PWM模式1
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; //OC1有效电平为高电平
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; //输出比较使能
/*
输出PWM的波形占空比
PWM占空比: Duty = CCR / (ARR + 1)
*/
TIM_OCInitStruct.TIM_Pulse = 0; //CCR,捕获/比较器,本工程中有封装的函数用于改变占空比,此处先设置为0
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
初始化输出比较单元时,在初始化TIM输出比较结构体之前调用TIM_OCStructInit()函数给结构体的每个成员初始化赋默认值,防止在实验中未使用到输出比较单元中的某些功能且未初始化这些功能对应的结构体成员时,发生意外错误。
将输出比较单元的输出模式设置为PWM模式1。

设置输出极性为高电平有效,即PWM模式1下计数器的值小于CCR时为高电平,反之为低电平。
此处是通过设置捕获比较使能寄存器TIMx_CCER位1CC1P设置输入/捕获1输出极性

此处将CCR寄存器的值设置为0,实验中使用封装的函数 void PWM_SetComparel(uint16_t Compare)来调用TIM_SetCompare1()函数来改变CCR寄存器的值实现占空比的改变。
cpp
/**
* @brief 指定CCR寄存器的值改变PWM Duty
* @param None
* @retval None
* @note None
**/
void PWM_SetComparel(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
输出PWM波形
cpp
uint8_t duty; //控制PWM占空比
定义一个uint8_t类型的变量duty来控制PWM占空比。
cpp
while(1)
{
/*
输出PWM波形(此处使用一个变量即可,多一个变量将会多占用存储空间)
*/
for(duty=0;duty<=100;duty++)
{
PWM_SetComparel(duty);
Delay_ms(10);
}
for(duty=0;duty<=100;duty++)
{
PWM_SetComparel(100-duty);
Delay_ms(10);
}
}
通过两个for循环实现PWM占空比从0->100,然后从100->0的变化达到呼吸灯效果,每次改变占空比后使用延时函数延时10ms,防止变化太快实现效果不明显。不同占空比的高电平驱动LED实现LED呼吸灯效果。
此处使用一个变量即可,占空比从100->0使用100-duty即可,for循环的条件不用改变。多定义一个变量会占用额外的空间
输出比较通道重映射
cpp
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//
// /*
// 引脚重映射
// */
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); //将TIM2的CH1从PA0重映射到PA15
// /*
// 解除引脚复用
// */
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //PA15引脚作为JTD1功能使用,此处解除AFIO的JTAG复用
在开启TIM2和GPIOA时钟后,开启AFIO时钟,调用GPIO_PinRemapConfig()将TIM2_CH1从PA0引脚重映射到PA15引脚。由于PA15默认为JTAG调试功能,调用GPIO_PinRemapConfig()函数解除引脚复用,PA15引脚作为JTD1功能使用,解除AFIO的JTAG复用。(如果不使用重映射功能,则忽略这部分)