新建工程——添加.c,.h文件
根据上一次的博客,我们只需把下面这些模块打通,就可以输出PWM了。
第一步:RCC开启时钟,把我们需要的TIM外设和GPIO外设的时钟打开
第二步:配置时基单元,包括前面时钟源选择
第三步:配置输出比较单元,里面包括CCR的值,输出比较模式,极性选择,输出使能这些参数。在库函数也是用结构体统一来配置的,
第四步:配置GPIO,把PWM对应的GPIO口,初始化为复用推挽输出的配置。
这个PWM与GPIO的对应关系是怎样的呢?可以参考一下引脚定义表,
第五步:运行控制,启动计数器,这样就能输出PWM了。
接下来我们看一下与之相对应的函数。
打开
这些就是TIM外设对应的库函数,我们之前介绍了一部分,接下来我们继续看看。
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);
这四个函数是用来配置输出比较模块的,OC就是(output compare) 输出比较,配置结构图的
这一部分。一个函数配置一个输出比较单元。参数TIMx,选择定时器,第二个是结构体,就是输出比较的那些参数了,
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);
这个是用来配置强制输出模式的,如果你在运行中想要暂停输出波形并且强制输出高或低电平。可以用这个函数,不过一般用的不多,
因为 强制输出高或低电平和设置100%,0%占空比是一样的,
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);
这四个函数是用来配置CCR寄存器的预装功能的,这个预装功能,就是影子寄存器,就是你写入的值不会立即生效,而是在更新事件才会生效。这样可以避免一些小问题。一般不用,了解一下即可。
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);
这四个函数是用来配置快速使能的,这个功能在手册里单脉冲模式,那一节有一小段介绍。用的不多,不需要掌握
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);
这个功能在手册里,外部事件时清除REF信号,那一小节有介绍,这个也不需要掌握,了解即可。
上面的函数就是小功能配置,用的不多,感兴趣自己可以去手册上看看,
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);
这些函数就是用来单独设置输出比较的极性的,带个N的就是高级定时器里的互补通道的配置,OC4没有互补通道,所以没有OC4N的函数,那这里可以设置极性,在结构体初始化的那个函数也可以设置极性,这两个设置极性的作用是一样的,结构体初始化只不过是用结构体一起初始化的,而在这里是一个单独的函数进行修改的,一般来说,结构体里的参数,都会有一个单独的函数可以进行修改的。
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);
选择输出比较模式。这个是用来单独更改输出比较模式的函数
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);
这四个是用来单独更改CCR寄存器值的函数。这四个函数比较重要,我们在运行的时候,更改占空比,就需要用到这四个函数
到这里,输出比较的函数就介绍完了
总结一下:红色标记很重要,需要掌握。
补充:
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
这个函数仅高级定时器使用,在使用高级定时器输出PWM时,需要调用这个函数,使能主输出,否则PWM将不能正常工作。
我们先把定时器初始化的一部分代码拿过来
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//时基单元的代码写过,我们找之前定时中断的代码复制一下
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //打开时钟
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=10-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=1-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
//初始化输出比较单元
TIM_Cmd(TIM2,ENABLE);//启动定时器
}
然后进行初始化输出比较单元,你要初始化哪个通道,就调用哪个函数,不同的通道对应的GPIO口也是不一样的,所以这里按照你GPIO口的需求来,这里我使用的是PA0口,所以对应的就是第一个输出比较通道。所以我使用下面这个函数。
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
建立结构体,,可以看到结构体有很多变量,其中大部分都是高级定时器才用到的。比如带N的参数,OCIDlestate,所以一般我们只把我们需要的参数列出来即可
第一个需要的参数是OCMode,设置输出比较模式。
TIM_OCPolarity,设置输出比较的极性
OutputState,设置输出使能
Pulse,用来设置CCR的
void TIM_ICStructInit(TIM_OCInitTypeDef* TIM_ICInitStruct);
就是用来给结构体赋初始值的。
这个给结构体赋初始值的方法也非常简单,就是一个一个手动地给个值
这就是结构体赋值的注意事项,也是 StructInit的用途,就是如果你不想把所有成员都列一遍复制,就要StructInit赋一个初始值,再更改你想更改的值就OK了。
这些就是我们上一篇博客介绍的那几张输出比较模式。
Timing : 冻结模式
Active : 相等时置有效电平
Inactive :相等时置无效电平
Toggle :相等时电平翻转
PWM1:PWM1模式
PWM2 :PWM2模式
这是强制输出的两种模式。不过这两个参数不让我们初始化的时候使用。
我们主要用的是 PWM1:PWM1模式
关于Polarity的选择,这个可以根据你的电路需求来选择
后面依次找到每个变量的定义,找到我们要的参数
计算公式如下:比如产生频率1KHz,占空比为50%的PWM波形,分辨率为1%。代入公式,
ARR=100-1,PSC=720-1,CCR=50.
//初始化输出比较单元
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=;//CCR 等下再算
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
//通道已经初始化完成,在TIM2的OC1通道上就可以输出PWM波形了,这个波形借用GPIO口输出
通道已经初始化完成,在TIM2的OC1通道上就可以输出PWM波形了,这个波形借用GPIO口输出
通道与GPIO口的对应关系:
在这个TIM2的OC1通道是借了哪个GPIO口呢?
这些其他外设也是同理,比如我们要是有SPI的MISO引脚,那就是PA6,如果要I2C2的SCL引脚,那就是PB10,这个关系是定死的,不能任意更改。不过STM32还是给了一次更改的机会的,这就是重定义,或者叫重映射,,比如你既要用USART2的TX引脚,又要用TIM2的CH3通道,它俩冲突了,没办法同时使用,那我们就可以在重映射的列表里找找,比如TIM2_CH3,那如TIM2_CH3就可以从原来的引脚,换到这里的引脚,这样就避免了两个外设引脚的冲突,如果这个重映射的列表里找不到,那外设复用的GPIO就不能挪位置。这就是重映射的功能,配置重映射是用AFIO来完成的。
这些就是外设引脚和GPIO引脚的复用关系和重映射的介绍。我们在使用外设的引脚时,需要多参考这个引脚定义表,找到默认复用功能和重定义功能,
要接GPIO口,我们把GPIO口配置一下,选择的是复用推挽输出,只有把GPIO设置为复用推挽输出,引脚的控制权才能交给片上外设,PWM波形才能通过引脚输出。可以参考一下8种输入模式。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
PWM.c
此时产生一个频率为1KHz的,占空比为50%的方波,
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//时基单元的代码写过,我们找之前定时中断的代码复制一下
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //打开时钟、
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
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=50;//CCR 等下再算
TIM_OC1Init(TIM2,&TIM_OCInitStructure);
//通道已经初始化完成,在TIM2的OC1通道上就可以输出PWM波形了,这个波形借用GPIO口输出
//初始化GPIO
TIM_Cmd(TIM2,ENABLE);//启动定时器
}
为了实现呼吸灯,我们要一直改变占空比,也就是要一直改变CCR的值,
我们调用void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
这个函数是专门用来单独更改通道1的CCR值的。
注意,我们给了ARR=100,所以占空比直接由CRR决定,但是占空比是有两个共同决定的。
呼吸灯程序:
PWM.c
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//时基单元的代码写过,我们找之前定时中断的代码复制一下
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //打开时钟、
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
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);
//通道已经初始化完成,在TIM2的OC1通道上就可以输出PWM波形了,这个波形借用GPIO口输出
//初始化GPIO
TIM_Cmd(TIM2,ENABLE);//启动定时器
}
void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
PWM.h
#ifndef __OLED_H
#define __OLED_H
void PWM_Init(void);
void PWM_SetCompare1(uint16_t Compare);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "PWM.h"
uint8_t i;
int main()
{
OLED_Init();
PWM_Init();
while(1)
{
for(i=0;i<=100;i++)//LED逐渐变亮
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for(i=0;i<=100;i++)//LED逐渐变暗
{
PWM_SetCompare1(100-i);
Delay_ms(10);
}
}
}
最后,我们看看引脚重映射是怎么玩的,在PWM.c里面,加入下列代码
要使用AFIO,就要开启AFIO时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
调用上图的引脚重映射的函数,可以看到这个函数的方式非常多。
每种对应的重映射关系是啥呢?查看一下手册,里面有四种重映射的方式,
如果要把PA0改到PA15,就可以选择部分重映射1,或者完全重映射,用下面语句就可以把PA0换到PA15了,不过还是有一个问题。
GPIO_PinRemapConfig(GPIO_PartialRemap2_TIM2,ENABLE);
看到引脚定义表,因为引脚定义表会默认PA15上电后复用为调试端口JTDI,所以想让他作为普通GPIO或者复用定时器的通道,那先要调试端口的复用,
同样也是GPIO_PinRemapConfig这个函数,找到下面三个定义的参数,就是解除调试端口复用的
SWJ就是SWD和JTAG这两种调试方式。NOJTRST,就是解除JTRST的复用,
在引脚定义里看一下,NOJTRST就是PB4,如果使用这个参数,那么这个PB4就变为正常的GPIO口了,
SWJ_JTAGDisable 就是解除JTAG调试端口的复用,在引脚定义表里,就是PA15,PB3,PB4这三个端口变回GPIO,
SWJ_Disable这个就是把SWD和JTAG的调试端口全部解除,在引脚定义里。就是PA15,PA14,PA13,PB3,PB4这5个引脚全部变为普通的GPIO,没有调试功能了。但是这个参数千万不要随便调用,一旦你调用这个参数并且下载程序之后,那么你的调试端口就没有了,之后使用STLINK就下载不进去程序了。这时就只能使用串口下载,下载一个新的,没有解除调试端口的程序,这样才能把调试端口弄回来,所以使用这个参数要小心。
这些参数和GPIO能不能使用的情况,手册也写了,对应下图:
好,如果我们想使用PA15,PB3,PB4就是解除JTAG的复用,保留SWD的复用。
最后代码为
放入PWM.初始化里面
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable ,ENABLE);
如下图所示。 也要记得把引脚改为15号引脚