前言
本系列,将stm32常用片上外设进行整理,包括大致原理和代码,主要是熟悉如何根据手册去编写代码。便于以后需要做实验时,能够快速编写基本的驱动,然后编写应用代码。
本系列基于“标准库”整理,开发板用的是正点原子精英版V1.5,单片机是STM32F103ZET6。开发工具KEIL。主要参考资料有《STM32F103xCDE中文参考手册》《STM32F103xCDE中文数据手册》、《CM3权威指南》、《STM32F103xCDE闪存编程手册》、《精英版原理图V1.5》。
》《STM32F1xx Cortex-M3编程手册-英文版》
STM32F1系列8个定时器,TIM6和TIM7为基本定时器,TIM2\3\4\5为通用定时器,TIM1和TIM8为高级定时器。从基本到高级,核心还是基本定时器,通用定时器和高级定时器都是在基本定时器上加了不少功能。在定时器里面最核心的肯定是计数器的值,只是这个值怎么变,得由控制寄存器来决定。以及这个值拿来干什么,就可以分为通用定时器和高级定时器。
一、基本定时器
1.基本定时器框图
在《STM32F103xCDE中文参考手册》手册的15.2TIM6和TIM7的主要特性,这一节介绍了基本定时器的框图。这张框图所包含的也是通用定时器和高级定时器均有的东西。如下图所、示:
结合下面这些说明:可以知道,时基单元是最重要的部分,其实就是时钟频率和计数值。因为定时时间= 时钟频率*计数个数。这个公式在基本、通用、高级中均是核心。时间到了,定时器会产生中断。
2.基本定时器代码配置
TIM6在APB1上,APB1的时钟是36M,经过定时器的2倍频器后,变为72M,此频率有点太快,TIM有预分频器,配置为10K,计数个数5000,所以定时时间= 5000/10K = 500ms。
//TIM6 时钟频率配置为10k,计数5000个,产生中断就是500ms
void tim6_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //时钟使能
//定时器TIM1初始化
TIM_TimeBaseStructure.TIM_Period = 5000 - 1; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值,减1因为计数是从0开始的。
TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1; //设置用来作为TIMx时钟频率除数的预分频值,分频系数是从0开始的,定时器频率10K,计1个数0.1ms,计5000个数,500ms
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ITConfig(TIM6,TIM_IT_Update,ENABLE ); //使能指定的TIM6中断,允许更新中断
//中断优先级NVIC设置
NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn; //TIM6中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
TIM_Cmd(TIM6, ENABLE); //使能TIMx
}
//定时器1中断服务程序
void TIM6_IRQHandler(void) //TIM1中断
{
if (TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //检查TIM6更新中断发生与否
{
TIM_ClearITPendingBit(TIM6, TIM_IT_Update ); //清除TIMx更新中断标志
LED1=!LED1;
}
}
二、通用定时器
通用定时器比基本定时器多了输入捕获和输出比较功能。其中输入捕获和输出比较不能同时使用,这两个功能都是基于计数功能衍生出来的。
1.通用定时器框图
在《STM32F103xCDE中文参考手册》的14.2节有通用定时器框图,如下图所示,其实①和基本定时器一样,②是输入捕获部分,③是输出比较部分。再看框图左边和右边都有TIMx_CH1,表示这两个其实是一个引脚,所以输入捕获和输出比较不能同时使用。
2.通用定时器捕获功能
对单片机的引脚来说都是TTL信号,所以只有高电平和低电平,那么定时器的输入捕获功能就是采集引脚上电平的上升沿或下降沿,当采集到上升沿或下降沿时记录下当前的CNT的值,并存如CCR寄存器中。
一个定时器有4个通道,其中1和2是一组,3和4是一组,例如要采集按键按下的时间,那么我们就可以使用定时器的输入捕获功能,按键未按下之前是高电平,按下之后变成低电平(下降沿),释放按键又变成高电平(上升沿)。CNT配置为向上计数,我们可以使用ch1来捕获下降沿,使用CH2来捕获上升沿,CH1连接到按键上,CH2悬空,当CH1捕获到下降沿时记录下当前CNT的值,存放到CCR1寄存器中,当CH2捕获到上升沿后,记录下当前CNT的值,存放到CCR2寄存器中。采集完成后产生捕获中断,在中断服务函数中,我们使用CCR2-CCR1即得到计数个数,再乘以定时器计时周期就得到按键按下的时间了。
3.通用定时器捕获实验
接下来我们使用标准库来实现采集按键按下时间的功能。配置步骤如下:
①开启GPIO时钟,开启TIM2时钟
②配置GPIO为上拉输入,TIM配置为10k,计数5000
③TIM配置CH1采集上升沿,CH2采集下降沿,开启中断
#include "stm32f10x.h"
void TIM2_CH1_CH2_Cap_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStruct;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//TIM2时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//APIOA时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE,ENABLE);//APIOE时钟使能
//PE4接在KEY0上
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //复用推挽
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOE, &GPIO_InitStructure);
//PA1接在KEY0上
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //复用推挽
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_ARRPreloadConfig(TIM2,ENABLE);//使能预加载,必然就会有手动产生一个更新事件使得数据生效。TIM_GenerateEvent(TIM2,TIM_EventSource_Update);
TIM_TimeBaseStructure.TIM_Prescaler = 7200-1; //定时器分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseStructure.TIM_Period = 5000-1; //自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_GenerateEvent(TIM2,TIM_EventSource_Update);//手动产生一个事件使得预加载值生效
TIM_SelectHallSensor(TIM2,DISABLE);//下面要使用CH1,此处关闭霍尔传感器
TIM_SetClockDivision(TIM2,TIM_CKD_DIV1);//不分频
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
//CH1捕获下降沿
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;//选择输入端 IC1映射到TI1上
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;//配置输入分频,不分频
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;// 配置输入滤波器 不滤波
TIM_ICInitStruct.TIM_ICFilter = 0;
TIM_ICInit(TIM2 ,&TIM_ICInitStruct);
//CH2捕获上升沿
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;//选择输入端 IC1映射到TI1上
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;//配置输入分频,不分频
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;// 配置输入滤波器 不滤波
TIM_ICInitStruct.TIM_ICFilter = 0;
TIM_ICInit(TIM2 ,&TIM_ICInitStruct);
TIM_CCxCmd(TIM2, TIM_Channel_1,ENABLE);
TIM_CCxCmd(TIM2, TIM_Channel_2,ENABLE);
TIM_ITConfig(TIM2,TIM_IT_CC2,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
TIM_Cmd(TIM2,ENABLE ); //使能定时器2
}
void TIM2_IRQHandler(void)
{
uint16_t cnt;
uint16_t ccr1,ccr2;
float ms;
if(TIM_GetITStatus(TIM2,TIM_IT_CC2) == SET)
{
TIM_ClearITPendingBit(TIM2,TIM_IT_CC2);
ccr2 = TIM_GetCapture2(TIM2);
ccr1 = TIM_GetCapture1(TIM2);
cnt = TIM_GetCapture2(TIM2) - TIM_GetCapture1(TIM2);
ms = cnt*0.1;//单位毫秒
printf("ms=%f,cnt=%d,ccr1=%d,ccr2=%d\r\n",ms,cnt,ccr1,ccr2);
}
}
int main(void)
{
uint8_t i=0;
NVIC_InitTypeDef NVIC_InitStruct;
SysTick_Config(SystemCoreClock/1000); //
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
delay_init(); //延时函数初始化
uart_init(115200); //串口初始化为115200
LED_Init(); //LED端口初始化
TIM2_CH1_CH2_Cap_Init();
while(1)
{
}
}
4.通用定时器输出比较功能
输出比较功能就是,CNT向上计数时,我们往CCR1里面写入一个值,当CNT<=CCR时CH1输出高电平,当CNT>CCR1时输出低电平。从而CH1上就有方波信号了。此时方波的周期=定时器计时周期,占空比=CCR1/CNT的比值。如果不断修改CCT1的值,那么CH1上就会出现不同占空比的PWM波形了,但是周期是一样了。
5.通用定时器输出比较实验
使用TIM2实现呼吸灯的实验,TIM2的CH2输出PWM信号,由于不好展示实物,此处就用KEIL自带示波器采集出来。只要引脚上能输出不同占空比的PWM信号,那么LED灯就能像呼吸一样亮灭。再AFIO知道TIM2是映射到PA1的,所以使用KEIL自带示波器抓取PA1波形。
代码配置如下:
void PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitstructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//根据AFIO章节定时器映射,知道TIM2的Ch2是映射到PA1上
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA ,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM2); //选择时基单元驱动时钟,这里选择内部时钟,也可以不写,因为定时器上电后默认调用内部时钟
//TIM2频率为100k,计数100,向上计数,从0计到100,周期=100/100k=1/1K =1ms
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;//一分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInitStructure.TIM_Period= 100-1 ; //自动重装器
TIM_TimeBaseInitStructure.TIM_Prescaler=720-1; //PSC预分频器的值,分频系数
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; //重复计数器的值,在高级计数器里面才有
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);//时基单元初始化完成
//输出比较初始化
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_OC2Init(TIM2,&TIM_OCInitstructure);//启动对应CH,可以自己查看GPIO复用功能
TIM_Cmd(TIM2,ENABLE);//开启定时器
}
int main(void)
{
uint8_t i;
SysTick_Config(SystemCoreClock/1000);
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
delay_init(); //延时函数初始化
PWM_Init();
while(1)
{
for(i=0;i<=100;i++)
{
PWM_SetCompare2(i);
delay_ms(10);
}
for(i=0;i<=100;i++)
{
PWM_SetCompare2(100-i);
delay_ms(10);
}
}
}
//占空比修改
void PWM_SetCompare2(uint16_t Compare)
{
TIM_SetCompare2(TIM2,Compare);
}
使用keil模拟示波器,采集PA1信号图如下:
模拟器配置如下:
①使用模拟调试器,而不用物理调试器
②点击调试,并打开逻辑分析仪,放置到右边好看一点。
③设置要抓取的引脚
④将采集模式改为高低电平。
⑤点击全速运行,再停止,此逻辑分析仪就会有PWM波形了
三、高级定时器
高级定时器,又在通用定时器上增加了,带死区控制和紧急刹车,主要应用于电机控制。
关于高级定时器控制电机,目前我还没有使用过,输入捕获和输出比较和通用定时器一样。
只是配置TIM1和TIM8的输出比较功能时,会多一行代码。TIM_CtrlPWMOutputs(TIM1, ENABLE);
其余一样配置如下:
void Timer1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitstructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA ,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM1);
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;//一分频
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInitStructure.TIM_Period= 100-1 ; //自动重装器
TIM_TimeBaseInitStructure.TIM_Prescaler=720-1; //PSC预分频器的值,分频系数
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; //重复计数器的值,在高级计数器里面才有
TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStructure);//时基单元初始化完成
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(TIM1,&TIM_OCInitstructure);
//TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable);
TIM_CtrlPWMOutputs(TIM1, ENABLE);//TIM1和TIM8需要加上
TIM_Cmd(TIM1,ENABLE);//开启定时器
}