stm32 - 中断/定时器
stm32 - 中断/定时器
概念
时钟树
https://www.bilibili.com/video/BV1th411z7sn?p=13&vd_source=7155082256127a432d5ed516a6423e20
执行main函数之前,程序中主函数还会执行一个systeminit函数,这个函数就是用来配置时钟树的
SYSCLK是系统时钟72MHZ
震荡源:内部震荡源RC振荡器8MHZ,外部震荡源:4~16MHZ(石英晶振),…
图中与门就是RCC_xxx函数打开时钟
定时器类型
- 高级定时器:TIM1,TIM8 -> APB2总线,
- 通用定时器:TIM2,TIM3,TIM4,TIM5 -> APB1总线,
- 基本定时器:TIM6,TIM7 -> APB1总线,
针对STM32C8T6:TIM1,TIM2,TIM3,TIM4
基准时钟(系统时钟)
stm32主频72MHZ
RCC_TIMxCLK(主频)-> 内部时钟CK_INT(基本计数时钟) ->控制器 -> CK_PSC ->时基单元
因此,通向时基单元的计数基准频率是72MHZ
预分频器 - 时基单元
对输入的基准频率提前进行一个分频的操作
对72MHZ的计数时钟进行预分频
预分频器=0,不分频:输出频率=输入频率=72MHZ
预分频器=1,2分频:输出频率=输入频率/2=36MHZ
预分频器=11 ,12分频:输出频率=输入频率/12=6MHZ
预分频器是16位,最大可以写65535个数,最大是65535+1=65536分频;输出频率=输入频率/65536=1.0986328125KHZ
CNT计数器 - 时基单元
对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器值+1
CNT计数器是16位的,最多计0~65535的数值,再+1就从0开始计数
实际定时中断,应该是计数器达到目标值时,产生中断
自动重装寄存器 - 时基单元
存储计数目标的寄存器
自动重装寄存器是16位的, 是写入的固定值,当计数器的计数值达到自动重装寄存器的值的时候,表明定时时间到,产生中断信号,并清零计数器开始下一次从0开始计数
计数器值=自动重装寄存器的值(也叫更新中断),产生中断,产生中断后通往NVIC,再配置号NVIC定时器的通道, 执行中断服务
基本定时器结构
通用定时器
计数器模式
向上计数模式:基本定时器只有这一个功能
向下计数模式:通用计时器
中央对齐模式:通用计时器
- 向下计数模式
从自定义的自动重装值开始,向下自减,减到0后重新回到自动重装值开始计数
- 中央对齐模式
0-> 自增 -> 自动重装值 -> 自减 -> 0
内外时钟源选择
对于基本定时器,只能选择内部时钟进行定时,即系统频72MHZ
对于通用定时器,即能选择内部时钟72MHZ,也能选择外部时钟
外部时钟
TIMx_ETR引脚上的外部时钟(查看引脚定义图)
在引脚上接一个外部的方波时钟,然后配置内部的电路
定时中断基本结构
时序
预分频器时序
计数器时序
例子
通用定时器 - 内部定时中断
main.c
#include "stm32f10x.h"
#include "timer.h"
extern uint16_t timer2_num;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"helloworld");
Timer_Init();
while (1)
{
OLED_ShowNum(2,1,timer2_num,5);
}
}
timer.h
#ifndef __TIMER_H__
#define __TIMER_H__
#include "stm32f10x.h"
void Timer_Init();
#endif
timer.c
#include "timer.h"
uint16_t timer2_num;
void Timer_Init()
{
// RCC时钟 系统基准时钟,和外设工作时钟
// 通用定时器TIM2是挂载在APB1上的
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); // 时钟树种的与门结构
// 时基单元时钟源
// 选择内部时钟
TIM_InternalClockConfig(TIM2);
// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
/* 这里定时为1s=1/1HZ; 分频后72MHZ/((7200-1)+1)=10000HZ=10KHZ; 1/10000s记一次数, */
TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1; // PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_Period=10000-1; // ARR自动重装器的值 0~65535
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 计数模式,向上计数
/*
- TIM_ClockDivision 与时基单元关系不大,时钟分频 不等于 预分频器
- 外部时钟输入,输入引脚后接一个滤波器,滤波器可以过滤信号的抖动干扰
- 在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的频率,就代表输入信号稳定,就把这个采样值输出出去
- 如果n个采样点采样值不尽相同,说明信号有抖动,就保持上一次的输出,或者直接输出其他默认的电平
- f和N为滤波器的参数,频率f越低???,采样点数越多,滤波效果越好,但是会导致信号延迟越大
- 这个频率f可以由内部时钟而来,也可以是内部时钟分频而来,即TIM_ClockDivision确定
- TIM_CKD_DIV1不分频,x2为2分频,x4为4分频
*/
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; // 重复计数器的值,与高级定时器相关
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*
预分频器有影子寄存器(缓冲寄存器),写入的值只有在触发更新事件时,才会真正起作用
在定时计数溢出之后,伴随产生更新事件,更新事件触发后,会将写入预装载的值加载到对应的影子寄存器中
自动重装寄存器->对应的影子寄存器;预分频器->对应的影子寄存器
针对ARR的ARPE位用于控制ARR自动重载寄存器是否具有缓冲作用,
如果具有缓冲作用,只有当等到更新事件触发后,才会写入影子寄存器并生效
如果不具有缓冲作用,那么立即写入影子寄存器并生效
*/
/*从源码可知,为了防止初始化完成立即进入中断,使得中断标志位置1,这里手动清理标志位,避免进入中断*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
// 配置中断输出控制,使能中断,开启更新中断(而不是更新事件)
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); // 中断到NVIC的通路
// 配置NVIC,打开定时器中断的通道,分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn; // 中断输出通道
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2; // 抢占优先级(在抢占中断中)
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1; // 响应优先级 (在响应中断中)
NVIC_Init(&NVIC_InitStructure);
// 启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void TIM2_IRQHandler() // 更新中断服务
{
if (TIM_GetITStatus(TIM2,TIM_IT_Update)==SET) // 判断中断标志位
{
// 清楚中断标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
timer2_num++;
}
}
通用定时器 - 外部时钟定时中断
main.c
#include "stm32f10x.h"
#include "timer.h"
extern uint16_t timer2_num;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"helloworld");
Timer_Init();
while (1)
{
OLED_ShowNum(2,1,timer2_num,5);
OLED_ShowNum(3,1,Timer_GetCounter(),5);
}
}
timer.h
#ifndef __TIMER_H__
#define __TIMER_H__
#include "stm32f10x.h"
void Timer_Init();
uint16_t Timer_GetCounter();
#endif
timer.c
#include "timer.h"
uint16_t timer2_num;
void Timer_Init()
{
// RCC时钟 系统基准时钟,和外设工作时钟
// 通用定时器TIM2是挂载在APB1上的
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
// 时基单元时钟源
// TIM_InternalClockConfig(TIM2); // 选择内部时钟
// 不需要外部触发预分频器
// 外部时钟触发的极性: 反向:低电平或下降沿有效;不反向:高电平或上升沿有效
// 外部时钟触发滤波器:0x00~0xFF ,这个值决定外部计数的滤波器的频率f和采样点n,见手册有对应的关系; 这里暂时不用滤波器
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00); // 外部时钟模式2
// 外设时钟,需要使用GPIO
// 初始化APB2外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// 配置GPIO - ODR/CLR
GPIO_InitTypeDef GPIO_InitStructure; // 配置结构体
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; // GPIO_Mode_IPU 上拉输入,因为是外部时钟方波输入,所以是输入模式
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_0; // GPIOA的PA0号引脚
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; // 50MHZ
GPIO_Init(GPIOA,&GPIO_InitStructure); // A0推挽输出
// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Prescaler=1-1; // PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_Period=10-1; // ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 计数模式,向上计数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 关系不大,时钟分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; // 重复计数器的值,与高级定时器相关
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
/*
预分频器有影子寄存器(缓冲寄存器),写入的值只有在触发更新事件时,才会真正起作用
在定时计数溢出之后,伴随产生更新事件,更新事件触发后,会将写入预装载的值加载到对应的影子寄存器中
自动重装寄存器->对应的影子寄存器;预分频器->对应的影子寄存器
针对ARR的ARPE位用于控制ARR自动重载寄存器是否具有缓冲作用,
如果具有缓冲作用,只有当等到更新事件触发后,才会写入影子寄存器并生效
如果不具有缓冲作用,那么立即写入影子寄存器并生效
*/
/*从源码可知,为了防止初始化完成立即进入中断,使得中断标志位置1,这里手动清理标志位,避免进入中断*/
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
// 配置中断输出控制,使能中断
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE); // 中断到NVIC的通路
// 配置NVIC,打开定时器中断的通道,分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn; // 中断输出通道
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2; // 抢占优先级(在抢占中断中)
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1; // 响应优先级 (在响应中断中)
NVIC_Init(&NVIC_InitStructure);
// 启动定时器
TIM_Cmd(TIM2,ENABLE);
}
uint16_t Timer_GetCounter()
{
return TIM_GetCounter(TIM2); // 中断后自动归零
}
void TIM2_IRQHandler()
{
if (TIM_GetITStatus(TIM2,TIM_IT_Update)==SET) // 判断中断标志位
{
// 清楚中断标志位
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
timer2_num++;
}
}
定时器 - 输出捕获
输出比较可以通过比较CNT和CCR寄存器值的关系,来对输出电平进行置1、0或翻转的操作用于输出一定频率和占空比的PWM波形
每个高级定时器和通用定时器都拥有4个输出比较通道
CCR是给定的值
PWM
https://www.bilibili.com/video/BV1Mb411e7re?p=33&vd_source=7155082256127a432d5ed516a6423e20
注意:输出PWM不需要中断申请
脉冲宽度调制
使用PWM波形实现模拟信号的输出
频率越快,等效模拟的信号越平稳,但是性能开销越大
占空比:T_on/T_s:高电平/整个周期的时间比例
占空比决定了PWM等效出的模拟电压的大小
占空比越大,等效的模拟电压越趋近与高电平,反之亦然
分辨率:占空比变化步距
输出比较通道 - 通用定时器
oc1ref是一个参考信号电平
注意:输出PWM不需要中断申请
PWM频率:等于计数器的更新频率
例子 - 呼吸灯
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "LED.h"
#include "key.h"
#include "Buzzer.h"
#include "PhotoSensor.h"
#include "OLED.h"
#include "infrCountSensor.h"
#include "encoder.h"
#include "timer.h"
#include "PWM.h"
static uint16_t pwmCompare1Num=0;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"helloworld");
PWM_Init();
while (1)
{
for (uint16_t i=0;i<=100;i++)
{
PWM_SetCompare1(i);
Delay_ms(10);
}
for (uint16_t i=0;i<=100;i++)
{
PWM_SetCompare1(100-i);
Delay_ms(10);
}
}
}
PWM.h
#ifndef __PWM_H__
#define __PWM_H__
#include "stm32f10x.h" // Device header
void PWM_Init();
void PWM_SetCompare1(uint16_t compare);
#endif
PWM.c
#include "PWM.h"
void PWM_Init()
{
// RCC时钟 系统基准时钟,和外设工作时钟
// 通用定时器TIM2是挂载在APB1上的
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
// 时基单元时钟源
TIM_InternalClockConfig(TIM2); // 选择内部时钟
// 外设时钟,需要使用GPIO
// 初始化APB2外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// 配置GPIO - ODR/CLR
// 注意针脚GPIO复用和重映射的问题,见手册针脚定义
GPIO_InitTypeDef GPIO_InitStructure; // 配置结构体
/*
对于普通的开漏和推挽输出,引脚的控制权是来自输出数据寄存器的
如果想让定时器控制引脚,就需要使用复用开漏/推挽输出的模式,使得输出输出寄存器断开,输出控制权转移到片上外设 这里是TIM2_CH1通道
*/
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; // GPIO_Mode_AF_PP 复用推挽输出
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_0; // GPIOA的PA0号引脚
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; // 50MHZ
GPIO_Init(GPIOA,&GPIO_InitStructure); // A0推挽输出
// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Prescaler=100-1; // PSC预分频器的值 // 72MHZ/((100-1)+1)=720KHZ
TIM_TimeBaseInitStructure.TIM_Period=720-1; // ARR自动重装器的值 // 720*(1/720KHZ)=1/1KHZ=1ms
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 计数模式,向上计数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 关系不大,时钟分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; // 重复计数器的值,与高级定时器相关
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
// 无需配置中断相关内容
// 无需 配置中断输出控制,使能中断
// 无需 配置NVIC,打开定时器中断的通道,分配优先级
// 配置输出比较单元
// TIM_OCxInit output compare 输出比较模块
// 配置好之后,就可以在TIM_OC1通道上输出PWM波形,然后借用GPIO口进行输出 ,TIM2_CH1_ETR引脚和PA0引脚复用,见引脚定义手册
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct); // 默认初始值
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1; // 设置输出比较的模式,一共8个模式
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable; // 设置输出使能,输出控制器
TIM_OCInitStruct.TIM_Pulse=0; // 设置CCR 0~0xFFFF,这里频率1kHZ,占空比50%,1%的分辨率
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High; // 设置输出比较的极性,这里REF有效电平为高电平(这里应该不是对应REF的极性选择器)
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
// 启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(uint16_t compare)
{
TIM_SetCompare1(TIM2,compare);
}
例子 - 呼吸灯-端口重映射
PWM.c
PA0重映射到PA15
#include "PWM.h"
void PWM_Init()
{
// RCC时钟 系统基准时钟,和外设工作时钟
// 通用定时器TIM2是挂载在APB1上的
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
// 时基单元时钟源
TIM_InternalClockConfig(TIM2); // 选择内部时钟
// 外设时钟,需要使用GPIO
// 初始化APB2外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
// 配置AFIO 实现引脚重映射
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2,ENABLE); // 见手册,部分重映射
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); // 恢复PA15的正常GPIO端口功能,因为
// 配置GPIO - ODR/CLR
// 注意针脚GPIO复用和重映射的问题,见手册针脚定义
GPIO_InitTypeDef GPIO_InitStructure; // 配置结构体
/*
对于普通的开漏和推挽输出,引脚的控制权是来自输出数据寄存器的
如果想让定时器控制引脚,就需要使用复用开漏/推挽输出的模式,使得输出输出寄存器断开,输出控制权转移到片上外设 这里是TIM2_CH1通道
*/
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP; // GPIO_Mode_AF_PP 复用推挽输出
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_15; // GPIOA的PA15号引脚,重映射之后需要使用PA15的端口
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; // 50MHZ
GPIO_Init(GPIOA,&GPIO_InitStructure); // A0推挽输出
// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_Prescaler=720-1; // PSC预分频器的值 // 72MHZ/((720-1)+1)=100KHZ
TIM_TimeBaseInitStructure.TIM_Period=100-1; // ARR自动重装器的值 // 100*(1/100KHZ)=1/1KHZ=1ms
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 计数模式,向上计数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; // 关系不大,时钟分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0; // 重复计数器的值,与高级定时器相关
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
// 无需配置中断相关内容
// 无需 配置中断输出控制,使能中断
// 无需 配置NVIC,打开定时器中断的通道,分配优先级
// 配置输出比较单元
// TIM_OCxInit output compare 输出比较模块
// 配置好之后,就可以在TIM_OC1通道上输出PWM波形,然后借用GPIO口进行输出 ,TIM2_CH1_ETR引脚和PA0引脚复用,见引脚定义手册
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct); // 默认初始值
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1; // 设置输出比较的模式,一共8个模式
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable; // 设置输出使能,输出控制器
TIM_OCInitStruct.TIM_Pulse=0; // 设置CCR 0~0xFFFF,这里频率1kHZ,占空比50%,1%的分辨率
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High; // 设置输出比较的极性,这里REF有效电平为高电平(这里应该不是对应REF的极性选择器)
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
// 启动定时器
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(uint16_t compare)
{
TIM_SetCompare1(TIM2,compare);
}