文章目录
前言
提示:本文主要用作在学习江科大自化协STM32入门教程后做的归纳总结笔记,旨在学习记录,如有侵权请联系作者
本案例利用输出占空比可调的PWM波形来驱动LED灯,实现了一个LED呼吸灯的效果。
一、应用案例演示
TIM输出比较之PWM驱动LED呼吸灯演示
二、电路接线图
三、应用案例代码
PWD.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
PWD.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_15;
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 = 100 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; //CCR
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
主程序main.c文件:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
uint8_t i;
int main(void)
{
OLED_Init();
PWM_Init();
while (1)
{
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(100 - i);
Delay_ms(10);
}
}
}
四、应用案例分析
我们先来看一下PWM的基本结构图,如下所示,我们只需要把下面这些模块打通就可以输出PWM了。
4.1 基本思路
具体的步骤如下:
- 第一步,RCC开启时钟。把我们要用的TIM外设和GPIO外设的时钟都打开。
- 第二步,配置时基单元。包括这前面的时钟源选择。
- 第三步,配置输出比较单元。包括CCR的值、输出比较模式、极性选择、输出使能这些参数。
- 第四步,配置GPIO。把PWM对应的GPIO口初始化为复用推挽输出的配置。
- 第五步,运行控制。启动计数器,这样就能输出PWM了。
以上就是基本思路,老规矩,我们先来看一下都有哪些相关的库函数。
4.2 相关库函数介绍
先来看一下这四个函数
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
这四个函数就是用来配置输出比较模块的,也就是对应PWM的基本结构图中的输出比较单元的位置。输出比较单元有4个,那对应的就是这四个函数。参数TIMx,选择定时器。参数TIM_OCInitStruct,就是输出比较那些参数。
接下来再往下看,该函数是用来给输出比较结构体赋一个默认值的。
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
那到这里,输出比较的配置其实就已经可以完成了,接下来就是一些小功能和运行时需要更改参数的函数了。
我们来看一下下面这四个函数,这四个函数是用来配置强制输出模式的,如果你在运行中想要暂停输出波形并且强制输出高或者低电平就可以使用这四个函数。用得不多,了解一下即可。
void TIM_ForcedOC1Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC2Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC3Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
void TIM_ForcedOC4Config(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
再看一下下面这四个函数,这四个函数是用来配置CCR寄存器的预装功能的。这个预装功能就是影子寄存器,就是你写入的值不会立即生效,而是在更新事件才会生效,这样可以避免一些小问题。用得不多,了解一下即可。
void TIM_OC1PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC3PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
void TIM_OC4PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
再看一下下面这四个函数,这四个函数是用来配置快速使能的,用得也不多,了解即可。
void TIM_OC1FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC2FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC3FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
void TIM_OC4FastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
再看一下下面这四个函数,这四个函数是用于外部事件时清除REF信号,用得也不多,了解即可。
void TIM_ClearOC1Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC2Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC3Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
void TIM_ClearOC4Ref(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
再来看一下下面这些函数,这些函数就是用来单独设置输出比较的极性的,这里带个N的就是高级定时器里互补通道的配置,0C4没有互补通道,所以就没有OC4N的函数。那这里有函数可以设置极性,在结构体初始化的那个函数里也可以设置极性。这两个地方设置极性的作用是一样的。
void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC1NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC2PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC2NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC3PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_OC3NPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
再看一下下面这两个函数,这两个函数是用来单独修改输出使能参数的。
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);
void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
再下面,这个是用来单独更改输出比较模式的函数。
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
再往下看,这四个函数是用来单独更改CCR寄存器值的,这四个函数比较重要。我们在运行的时候更改占空比就需要用到这四个函数。
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
补充一个函数,该函数仅高级定时器使用,在使用高级定时器输出PWM时需要调用这个函数,使能主输出,否则PWM将不能正常输出。
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
ok,那到这里,有关输出比较的函数就介绍完了,现在开始写代码。
4.3 初始化PWM模块
4.3.1 RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
注意:TIM2是APB1总线的外设,要使用APB1的开启时钟函数
4.3.2 配置时基单元
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 = 100 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_InternalClockConfig(TIM2);//选择时基单元的时钟源,选择内部时钟。若不调用这个函数,系统上电后默认也是内部时钟。
- TIM_ClockDivision:指定时钟分频。在这里选择TIM_CKD_DIV1,1分频,也就是不分频。
- TIM_CounterMode:计数模式。在这里选择TIM_CounterMode_Up向上计数模式。
- TIM_Period:ARR自动重装器的值。
- TIM_Prescaler:PSC预分频器的值。
- TIM_RepetitionCounter:重复计数器的值。这个是高级定时器才有的,我们不需要用,直接给0即可。
计算公式如下:
PWM频率:Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)
换算公式:1 MHz = 1,000 KHz = 1,000,000 Hz
假设我要输出一个频率为1KHz,占空比为50%,分辨率为1%的PWM波形,时钟源选择内部时钟,也就是说CK_PSC=72MHz。
代入公式计算可得:
Freq =1000 = 72000000 / 720 / 100
那么可以推算出PSC为719,ARR为99
同样的道理,Duty = 50% = CCR / 100,推算出CCR为50。
同样也可以推算出周期 T = 1 / 1000 = 0.001秒,也就是1毫秒。(频率是周期的倒数 f = 1 / T)
4.3.3 配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; //CCR
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
各个参数解析如下:
- TIM_OCMode:设置输出比较的模式,这里选择PWM模式1。
- TIM_OCPolarity:设置输出比较的极性,这里选择高极性,也就是极性不翻转,REF波形直接输出。
- TIM_OutputState: 设置输出使能,设置使能。
- TIM_Pulse:设置CCR,这里先暂时设置为0。
设置完成后,在TIM2的OC1通道上就可以输出PWM波形了,但最终这个波形肯定要借用一下GPIO口才能输出对吧?那这个TIM2的OC1通道是借用了哪个GPIO通道呢?
外设引脚和GPIO引脚的复用关系和重映射介绍:
我们可以查看一下引脚定义表,我们来看一下默认复用功能这一列,这一列就是片上外设的端口和GPIO的连接关系。
在这里可以看到,有TIM2_CH1_ETR,它是在这个PA0这一行的,这就说明TIM2的ETR引脚和通道1的引脚都是借用了PA0这个引脚的位置的。换句话说就是TIM2的引脚复用在了PA0引脚上。所以说如果我们要使用TIM2的OC1也就是CH1通道输出PWM,那它就只能在PA0引脚上输出,而不能任意选择引脚输出。
同样,如果选择TIM2的CH2通道,那就只能在PA1端口上输出。同样的道理,TIM2的CH3就对应PA2,以此类推。这些关系是定死的,不能随意更改。但是如果想要更改的话,还是有办法的,那就是重定义,也叫做重映射。
比如如果你既想用USART2_TX引脚,又要用TIM2的CH3通道,他俩冲突了,没办法同时使用,那我们就可以在这个重映射的列表里找一下,比如我们在引脚号为21的这里找到了TIM2_CH3,那TIM2的CH3就可以从原来的引脚,换到这里的引脚也就是PB10,这样就避免了两个外设引脚的冲突。
如果这个重映射的列表里找不到,那外设复用的GPIO就不能挪位置,这就是重映射的功能。配置重映射是用AFIO来完成的。
那在了解完外设引脚和GPIO引脚的关系之后,通过查看引脚定义表可知,TIM2的CH1通道复用在了PA0上。或者使用重映射,可以看到TIM2_CH1_ETR可以重映射到PA15上。
4.3.4 配置GPIO
通过上面的分析可以知道TIM2的CH1通道复用在了PA0上,所以需要初始化一个GPIOA,引脚为pin 0。
要注意的一点就是,这里GPIO的模式要选择GPIO_Mode_AF_PP复用推挽输出模式,为什么要选择这个模式呢?
对于普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的,那如果想让定时器来控制引脚,那就需要使用复用开漏/推挽输出的模式。在这里输出数据寄存器将被断开,输出控制权将转移给片上外设。那通过刚才看到的引脚定义表我们就知道了这里的片上外设引脚连接的就是TIM2的CH1通道,所以,只有把GPIO设置为复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能通过引脚输出。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
这样,输出PWM的GPIO就配置好了。
4.3.5 运行控制
TIM_Cmd(TIM2, ENABLE);
最后一步,启动定时器。这样,PWM的波形就能通过PA0输出了。
4.4 PWM输出模块
我们想让LED呈现呼吸灯的效果,那就不断更改CCR的值就行了。
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}
4.5 主程序
我们只需要在while(1)主循环里不断调用PWM_SetCompare1函数更改CCR的值(占空比从0到100,再从100到0),这样就能完成LED呼吸灯的效果了。
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
uint8_t i;
int main(void)
{
OLED_Init();
PWM_Init();
while (1)
{
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for (i = 0; i <= 100; i++)
{
PWM_SetCompare1(100 - i);
Delay_ms(10);
}
}
}