(一)定时器的输出比较功能
定时器除了之前说过的定时中断功能,还有输出比较功能,这个功能可以让定时器在定时的时候与我们设定的一个值进行比较,通过大小关系来输出高低电平,通过输出比较功能,我们可以生成PWM波形,可以通过数字输出来模拟出模拟信号
首先我们之前已经了解了定时器的基本配置和定时器中断的流程图,定时器的输出比较和中断一样是基于定时器的,因此我们要做的就有一下几步:(1)初始化时钟;(2)选择时钟源;(3)初始化时基单元;(4)初始化输出比较单元
这里和中断不同的只有初始化中断变为了初始化输出比较单元,可以从流程图直观看出
对于一个定时器,其输出比较通道有四条,四条比较通道的时钟是一个时钟,但是可以设置不同的比较值(CCR),这四条通道通过不同的IO口输出,可以看下输出比较通道与IO口的复用关系
我们只要把这些端口的输出模式设置为复用输出模式就可以在这些端口上输出我们的定时器输出比较波形了,下面是一些会用到的函数
//时基单元及之前
void TIM_DeInit(TIM_TypeDef* TIMx);
//把定时器的设置恢复为默认值
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//设置定时器的时基单元,操作三用来初始化时基单元里的变量
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//给定时器时基单元初始化结构体赋初始值,这里直接配置结构体,没有用到
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
//使能指定的时钟,等会用这个函数使能时钟2
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
//配置和使能时钟的中断,用来初始化中断输出控制,完成第四步
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
//配置内部时钟模式,在第一步选择内部时钟源时用这个函数配置时钟,完成第二步
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//设置时钟模式为外部时钟模式1,用于第一步选择外部时钟源时配置,用于初始化第二步
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//和之前两个函数一样,这个是选择时钟模式2,这里没有用到
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
//这个是配置预分频的,由于在计基单元初始化的结构体中已经配置了预分频,不需要单独配置,但是在代码执行中如果想修改预分频可以用这个函数修改
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
//这个函数是用来设置计时器的计数模式的,可以选择向上计数,向下计数和中央计数,计数模式在时基单元初始化的时候同样通过结构体配置了,这个是用在代码执行中单独配置的
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
//用于获取计数器的计数值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
//获取计数器的预分频
//中断部分没有用到
//---------------------------------------------------------------------------------------
//输出比较
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);
//这四个函数时一样的,分别初始化输出比较通道1、2、3、4,会用到
void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
//用于给输出比较通道初始化结构体赋默认值,这个会用到
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);
//用于设置输出极性,其中OCX后带有N的为高级定时器的函数,这里在初始化中已经设置了
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输出波形的占空比,以控制灯光亮度、舵机角度或电机速度
新的函数并不多,很多都是之前中断配置中已经见过的函数,我们的配置方法也和定时器中断配置中的一样
(二)输出比较的应用——LED呼吸灯
我们通过输出一个PWM波形,通过不断改变占空比来实现呼吸灯的效果,我们这里选择的是输出比较通道1,因此我们需要把LED灯插在PA0口上
(1)时钟初始化
和定时中断一样,先打开时钟
void breathing_light_rcc_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
(2)GPIO初始化
初始化IO口,注意,我们这里选择的输出模式是复用推挽输出(GPIO_Mode_AF_PP)
void breathing_light_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Pin = GPIO_Pin_0;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
}
(3)时钟源选择
选择时钟源
void breathing_light_time_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
(4)时基单元初始化
初始化时基单元
void breathing_light_time_base_init()
{
TIM_TimeBaseInitTypeDef time_base_init;
time_base_init.TIM_ClockDivision = TIM_CKD_DIV1;
time_base_init.TIM_CounterMode = TIM_CounterMode_Up;
time_base_init.TIM_Period = 100-1;
time_base_init.TIM_Prescaler = 7200-1; //T=10ms, f=100Hz
time_base_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &time_base_init);
}
这里设置PWM的周期为10ms,也就是频率为100Hz,周期公式再看一下
(5)输出比较单元初始化
设置好了自动重装值和预分频,我们就可以设置比较器了,由于我们的自动重装值是100,那我们的比较值应该在0-100之间,但是我们初始化的时候配置什么值都无所谓,因为我们在主程序中是通过修改这个值来控制LED的亮度的,接下来我们配置输出比较单元
我们先来看一下输出比较的初始化结构体
这个初始化的结构体有很多变量,有一些变量标明了只有TIM1和TIM8(高级定时器)才需要配置的,我们使用的通用定时器并不需要配置这些变量,因此我们使用给这个结构体赋默认值的方式来给结构体一个默认值,然后我们再配置我们想要配置的变量;
我们配置的第一个为比较模式,我们在定义中@ref后的内容按住Ctrl和F搜索一下
这里有六种模式,我们选择PWM模式就可以了,PWM1和PWM2主要是输出极性的区别
第二个参数是使能参数,这里选择使能
第三个参数是比较值,通过与比较值的大小关系来实现高低电平的输出
第四个参数是选择极性是否翻转
void breathing_light_compare_init()
{
TIM_OCInitTypeDef oc_init;
TIM_OCStructInit(&oc_init);
oc_init.TIM_OCMode = TIM_OCMode_PWM1;
oc_init.TIM_OutputState = TIM_OutputState_Enable;
oc_init.TIM_Pulse = 50;
oc_init.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &oc_init);
}
我们之前说过,LED呼吸灯试验是让LED插在PA0口的,我们的比较通道1是和PA0口复用,因此我们在这里调用的是TIM_OC1Init函数
(6)打开时钟
我们所有的部分就已经配置好了,接着我们打开时钟
void breathing_light_open_clock()
{
TIM_Cmd(TIM2, ENABLE);
}
(7)封装
将所有模块的初始化函数封装成一个函数供外部调用
void breathing_light_init()
{
breathing_light_rcc_init();
breathing_light_gpio_init();
breathing_light_time_channel_init();
breathing_light_time_base_init();
breathing_light_compare_init();
breathing_light_open_clock();
}
到这里,我们只实现了设置一个特定亮度的LED灯,要想控制LED灯函数,我们还需要一个函数可以修改输出比较的比较值,这个也很简单,只需要调用之前说过的TIM_SetCompare1就可以了,注意这里调用的是修改通道1的比较值,不要调动其他通道的比较值了
void breathing_light_set_ccr(unsigned char light)
{
TIM_SetCompare1(TIM2, light);
}
头文件声明一下初始化函数和比较值的修改函数
#ifndef __BREATHING_H__
#define __BREATHING_H__
void breathing_light_init(void);
void breathing_light_set_ccr(unsigned char light);
#endif
(8)主函数调用实现呼吸灯
主函数中,我们只要使用for循环来不断调整比较值就可以实现LED灯亮度的不断变化了
#include "stm32f10x.h" // Device header
#include "breathing_light.h"
#include "Delay.h"
int main()
{
unsigned char i;
breathing_light_init();
while(1)
{
for (i = 0; i <= 100; i++)
{
breathing_light_set_ccr(i);
Delay_ms(20);
}
for(i = 100; i > 0; i--)
{
breathing_light_set_ccr(i);
Delay_ms(20);
}
}
return 0;
}
(三)输出比较的应用——舵机角度控制
(1)舵机控制
舵机是通过PWM的占空比来控制其角度的机器,我们需要输出一个周期为20ms的PWM波,其中方波的占空比为2.5%时角度为0度,占空比为12.5%是角度为180度,我们想通过按键来控制其角度,当我们按下按键的时候,舵机角度增加45度,当180度按下时,其能返回0度的位置
舵机有三个口,一个接5v电源(stm32的只有3.3v,不能直接接在单片机上),一个接地,一个接控制端口,这里我们换一个输出比较通道,我们选择输出比较通道2,对应之前的表我们应该把控制端接在PA1引脚上
(2)程序实现
(1)时基单元之前的初始化
这部分的初始化和之前的基本一样,注意这里我们选择的是输出比较通道2,因此初始化的应该是PA1口
void servo_rcc_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
void servo_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Pin = GPIO_Pin_1;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
}
void servo_time_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
(2)时基单元初始化
这里和之前最大的不同就是我们要明确输出的PWM波的周期为20ms,那么依据我们之前的周期计算公式(),这里选择给period为2000,prescaler为720,这样后面进行五等分控制时比较值为整数且容易计算
void servo_time_base_init()
{
TIM_TimeBaseInitTypeDef time_base_init;
time_base_init.TIM_ClockDivision = TIM_CKD_DIV1;
time_base_init.TIM_CounterMode = TIM_CounterMode_Up;
time_base_init.TIM_Period = 2000-1; //T=20ms
time_base_init.TIM_Prescaler = 720-1;
time_base_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &time_base_init);
}
(3)输出比较单元的初始化
注意我们这里使用的是通道2,要调用对应的初始化函数
void servo_oc_init()
{
TIM_OCInitTypeDef oc_init;
TIM_OCStructInit(&oc_init);
oc_init.TIM_OCMode = TIM_OCMode_PWM1;
oc_init.TIM_OutputState = TIM_OutputState_Enable;
oc_init.TIM_Pulse = 0;
oc_init.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC2Init(TIM2, &oc_init);
}
这里的输出比较值给什么无所谓,我们按下按钮后会回到0度
(4)按键初始化
这里我们把按键放在PA7口上,初始化一下按键
void servo_key_init()
{
GPIO_InitTypeDef button_init;
button_init.GPIO_Mode = GPIO_Mode_IPU;
button_init.GPIO_Pin = GPIO_Pin_7;
button_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &button_init);
}
(5)控制角度
要控制角度,我们先要计算占空比对角度的影响,由于角度的控制是线性的,占空比为2.5%时角度为0度,占空比为12.5%是角度为180度,可以知道我们的占空比从2.5%开始到12.5%,占空比提高2.5%角度就增加45度,由于之前我们的自动重装值设置为2000,那么其2.5%就是50,也就意味着我们的比较值每提高50角度就增加45度,如果超出180度,我们就让舵机回到0度
unsigned int servo_angle()
{
if (servo_get_key_button())
{
angle += 50;
if (angle > 250)
{
angle = 50;
}
TIM_SetCompare2(TIM2, angle);
}
if (angle == 0)
{
return 0;
}
return (angle/50)*45-45;
}
最后返回的是舵机的角度,在复位的时候我们的舵机是没有控制的,我们按下第一次按键,舵机才会复位到0度,再按就会依次增加45度
(6)封装
把TIM2时钟打开
void servo_time_open()
{
TIM_Cmd(TIM2, ENABLE);
}
封装外部调用初始化
void servo_init()
{
servo_rcc_init();
servo_gpio_init();
servo_time_channel_init();
servo_time_base_init();
servo_oc_init();
servo_time_open();
servo_key_init();
}
把初始化和角度控制函数在头文件声明即可
#ifndef __SERVO_H__
#define __SERVO_H__
void servo_init(void);
unsigned int servo_angle(void);
#endif
整体的舵机控制代码如下
#include "stm32f10x.h" // Device header
#include "Delay.h"
unsigned int angle=0;
void servo_rcc_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
void servo_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Pin = GPIO_Pin_1;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
}
void servo_time_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
void servo_time_base_init()
{
TIM_TimeBaseInitTypeDef time_base_init;
time_base_init.TIM_ClockDivision = TIM_CKD_DIV1;
time_base_init.TIM_CounterMode = TIM_CounterMode_Up;
time_base_init.TIM_Period = 2000-1; //T=20ms
time_base_init.TIM_Prescaler = 720-1;
time_base_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &time_base_init);
}
void servo_oc_init()
{
TIM_OCInitTypeDef oc_init;
TIM_OCStructInit(&oc_init);
oc_init.TIM_OCMode = TIM_OCMode_PWM1;
oc_init.TIM_OutputState = TIM_OutputState_Enable;
oc_init.TIM_Pulse = 0;
oc_init.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC2Init(TIM2, &oc_init);
}
void servo_time_open()
{
TIM_Cmd(TIM2, ENABLE);
}
void servo_key_init()
{
GPIO_InitTypeDef button_init;
button_init.GPIO_Mode = GPIO_Mode_IPU;
button_init.GPIO_Pin = GPIO_Pin_7;
button_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &button_init);
}
void servo_init()
{
servo_rcc_init();
servo_gpio_init();
servo_time_channel_init();
servo_time_base_init();
servo_oc_init();
servo_time_open();
servo_key_init();
}
unsigned char servo_get_key_button()
{
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == 0);
Delay_ms(20);
return 1;
}
return 0;
}
unsigned int servo_angle()
{
if (servo_get_key_button())
{
angle += 50;
if (angle > 250)
{
angle = 50;
}
TIM_SetCompare2(TIM2, angle);
}
if (angle == 0)
{
return 0;
}
return (angle/50)*45-45;
}
(7)主函数调用
#include "stm32f10x.h" // Device header
#include "servo.h"
#include "OLED.h"
int main()
{
unsigned int angle;
OLED_Init();
servo_init();
OLED_ShowString(1, 1, "angle:");
while(1)
{
angle = servo_angle();
OLED_ShowNum(1, 7, angle, 3);
}
return 0;
}
(四)输出比较的应用——直流电机控速
(1)直流电机和控制芯片TB6612
直流电机控速要用到一个直流电机和一个控制芯片TB6612,如果你在正面没有看到这个芯片的引脚标明,或许你可以翻到背面看一下
VM要接到5v的电源,VCC要接到单片机的3.3v逻辑高电平,GND接到单片机的地,STBY是控制该芯片是否工作的,如果想控制是否工作就接入单片机IO口,想要一直工作就接单片机正极,接着是电机的控制,这个芯片可以控制两个直流电机,我们要把电机的两端接到A01和A02中,把输出PWM的端口接到PWMA中,再找两个IO端口来接AIN1和AIN2
我们控制IO端口来控制AIN1和AIN2就可以让其工作,如图
我们只要给IN1和IN2不同的电平就可以让其实现转动,如果在意的话可以看下正转和反转的配置
(2)直流电机模块
简单来说就是输出PWM波形(5K到20K赫兹比较好),控制两个IN口给不同电平
注意,这里我们选择了输出比较通道3,因此我们的PWM波形的输出端口是PA2,我把两个IN口插在了PA6和PA7口,控制速度的两个按键插在了PA0(提速)和PA4(减速)口
#include "stm32f10x.h" // Device header
#include "Delay.h"
int value = 0;
void dc_rcc_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
void dc_gpio_init()
{
GPIO_InitTypeDef gpio_init;
gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_init.GPIO_Pin = GPIO_Pin_2;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_init.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
gpio_init.GPIO_Mode = GPIO_Mode_IPU;
gpio_init.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_4;
gpio_init.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_init);
}
void dc_time_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
void dc_time_base_init()
{
TIM_TimeBaseInitTypeDef time_base_init;
time_base_init.TIM_ClockDivision = TIM_CKD_DIV1;
time_base_init.TIM_CounterMode = TIM_CounterMode_Up;
time_base_init.TIM_Period = 100-1; // 10khz
time_base_init.TIM_Prescaler = 72-1;
time_base_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &time_base_init);
}
void dc_oc_init()
{
TIM_OCInitTypeDef oc_init;
TIM_OCStructInit(&oc_init);
oc_init.TIM_OCMode = TIM_OCMode_PWM1;
oc_init.TIM_OCPolarity = TIM_OCPolarity_High;
oc_init.TIM_OutputState = TIM_OutputState_Enable;
oc_init.TIM_Pulse = 0;
TIM_OC3Init(TIM2, &oc_init);
}
void dc_time_open()
{
TIM_Cmd(TIM2, ENABLE);
}
void dc_motor_init()
{
dc_rcc_init();
dc_gpio_init();
dc_time_channel_init();
dc_time_base_init();
dc_oc_init();
dc_time_open();
GPIO_SetBits(GPIOA, GPIO_Pin_6);
GPIO_ResetBits(GPIOA, GPIO_Pin_7);
}
int dc_button_control()
{
if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0);
Delay_ms(20);
value += 20;
if (value > 100)
{
value = 100;
}
TIM_SetCompare3(TIM2, value);
}
else if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == 0)
{
Delay_ms(20);
while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == 0);
Delay_ms(20);
value -= 20;
if (value < 0)
{
value = 0;
}
TIM_SetCompare3(TIM2, value);
}
return value;
}
头文件引用一下外部调用函数
#ifndef __MOTOR_H__
#define __MOTOR_H__
void dc_motor_init(void);
int dc_button_control(void);
#endif
(3)主函数
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "dc_motor.h"
int main()
{
int speed = 0;
OLED_Init();
dc_motor_init();
OLED_ShowString(1, 1, "speed:");
while(1)
{
speed = dc_button_control();
OLED_ShowNum(1, 7, speed, 3);
}
return 0;
}
这样就实现了直流电机的加速和减速了
(五)总结
通过实现led呼吸灯,舵机角度控制和直流电机控速,我们了解了定时器除了定时中断外的另一功能——输出比较,我们现在已经可以通过输出比较来输出任意的周期和占空比的PWM波,并用于驱动一些外设