首先先来介绍一下PWM主要会用到的一些函数:

这四个函数就是用来配置输出比较的OC(output compare),四个输出比较单元对应四个输出比较函数

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);

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);

用来配置强制输出模式,如果运行中想要暂停波形并且强制输出高或低电平,可以使用这个函数,但一般用得不多,因为强制输出高电平和设置占空比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寄存器值的函数,比较重要,运行时更改占空比需要用到

接下来我们就可以根据下图的指示,来进行初始化了。

首先是时基单元的初始化,在上一次实验中已经展示了怎么初始化,这里就不再复述了,直接上代码:

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 = 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,与ARR和PSC共同决定输出PWM的周期和占空比
TIM_OC1Init(TIM2, &TIM_OCInitStructure);

因为我们先要做一个LED呼吸灯的实现,LED灯接在PA0端口,所以还需要对GPIO口进行初始化,一般建议把GPIO的初始化放在最前面:

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);

这里我们选择PA0口的理由如下:

图说明,TIM2的ETR引脚和通道1的引脚,都是借用了PA0这个引脚的位置,换句话说就是TIM2的引脚复用在了PA0引脚上,所以说如果我们要使用TIM2的OC1也就是CH1通道输出PWM,它就只能在PA0的引脚上输出,而不能任意选择引脚输出。同样如果使用了TIM2的CH2,那就只能够在PA1端口输出,再往下同理。

另外,我们对GPIO口的初始化模式为复用开漏/推挽输出,对于普通的开漏/推挽输出,引脚的控制权是来自于输出数据寄存器的,如果想让定时器来控制引脚,就需要使用复用开漏/推挽输出。

我们需要实现呼吸灯就需要不断改变LED的亮度,需要让PWM的占空比不断变化,我们可以通过更改CCR寄存器来实现

void PWM_SetCompare1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare);
}

 

(黄色:数据重装寄存器ARR,蓝色:计数器CNT,红色:捕获比较寄存器CCR,绿色:输出比较OC)

Freq = CK_PSC / (PSC + 1) / (ARR + 1) PWM频率

Duty = CCR / (ARR + 1) PWM占空比

Reso = 1 / (ARR + 1) PWM分辨率

 

 

#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波形。

不同通道的计数器是一样的,但是CCR都是有各自的,所以占空比可以各自设定,由于计数器更新,所有的PWM同时跳变,所以相位是同步的。

舵机要求的频率是20ms,那频率就是1/20ms = 50Hz

通过数学计算出CCR与角度之间的关系是:Angle / 180 * 2000 + 500,我们可以再创建.c文件来存放驱动舵机的函数

#include “stm32f10x.h”                  // Device header
#include “PWM.h”
void Servo_Init(void)
{
 PWM_Init();
}
void Servo_SetAngle(float Angle)
{
 PWM_SetCompare2(Angle / 180 * 2000 + 500);
}
主函数:
#include “stm32f10x.h”                  // Device header
#include “Delay.h”
#include “OLED.h”
#include “Servo.h”
#include “Key.h”
uint8_t KeyNum;
float Angle;
int main(void)
{
 OLED_Init();
 Servo_Init();
 Key_Init();
 OLED_ShowString(1, 1, “Angle:”);
 while (1)
 {
  KeyNum = Key_GetNum();
  if (KeyNum == 1)
  {
   Angle += 30;
  if (Angle >= 180)
  {
    Angle = 0;
   }
  }
   Servo_SetAngle(Angle);
   OLED_ShowNum(1, 7, Angle, 3);
 }
}

接下来是PWM驱动直流电机,这个的接线图比较复杂,电机功率比较大,GPIO口无法直接驱动,需要一个电机驱动电路来进行驱动:

直流电机的驱动的思想还是比较简单的,只要左边置高,右边置低,就会顺时针,反之逆时针,所以具体的操作还是GPIO口初始化,然后配置电平:
#include “stm32f10x.h”                  // Device header
#include “PWM.h”
void Motor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
PWM_Init();
}
void Motor_SetSpeed(int8_t Speed)
{
if (Speed >= 0)
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompare3(Speed);
}
else
{
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
PWM_SetCompare3(-Speed);
}
}
主函数:
#include “stm32f10x.h”                  // Device header
#include “Delay.h”
#include “OLED.h”
#include “Motor.h”
#include “Key.h”
uint8_t KeyNum;
int8_t Speed;
int main(void)
{
OLED_Init();
Motor_Init();
Key_Init();
Motor_SetSpeed(0);
OLED_ShowString(1, 1, “Speed:”);
while (1)
{
KeyNum = Key_GetNum();
if (KeyNum == 1)
{
Speed += 20;
if (Speed >= 100)
{
Speed = -100;
}
}
Motor_SetSpeed(Speed);
OLED_ShowSignedNum(1, 7, Speed, 3);
}
}

这两天刚好有点小感冒,又遇上了满课的一整天,今天刚好要学的还是最多的一课,视频总共有1小时那么多,搞了一整个晚上,两三个小时,但是今天的解析还是写的比较简单,感觉还有很多东西没有说到,但现在时间已经不早了,刚好还有点感冒,还是先早点休息吧。话说前两天趁着自己生日,奢侈了一把,买了一张sakanaction的专辑,但是从日本那边发的,感觉要好久才会收到,又花了三十块买了个光驱,不知道听起来会不会不一样,但这都是小事,最主要的还是能收藏一张专辑以后翻出来看到会很有感觉。以后也想多买点喜欢的乐队的专辑,工作后如果能买个好一点的CD机就更好不过了。现在看上一个叫巫单曲人生的CD机感觉不错,很好看,但要六七百,感觉真不是现在能负担得起的。我还想要攒钱买PS5呢。如果排个优先顺序的话,可能还是比较想要PS5,总之要好好省钱了。