拔出式开发之stm32定时器的基本写法
拔出式开发之stm32定时器的基本写法
实验目的
了解时钟的基本原理,利用定时器做到每隔一秒产生一次中断,在屏幕上显示计数+1,实现简单秒表功能。并且通过按钮接入外部时钟,用定时器的方式实现手动计数器。
原理
定时器其实就是计数器。STM32有三种:高级、通用和基本定时器,从前到后包含后者全部功能。其中高级定时器在APB2外设总线,性能更强。另两个都在APB1总线。
基本定时器
基本定时器只有定时中断和主模式触发DAC:

下半部分是时基单元,包含PSC Prescaler(PSC预分频器)、Auto-reload Register(自动重装寄存器)、CNT Counter(CNT计数器)。预分频器的输入信号其实就是内部时钟CK_INT,也就是主频72MHz。
预分频的值+1就是分频的分母,假设预分频器置2,72MHz三分频后的CK_CNT频率就是72/3=24MHz。预分频器有16位,最大分频为2^16=65536。计数器负责对分频器输出信号计数,也是16位,即最大计65536。也就是说,如果分频器和计数器都置最大值,在计数器从0开始直到被迫清空,总共可以计65536*65536/72M = 59.65s。当计数达到目标值后会触发更新中断,计时完成并清零计数器。这个目标值就存在自动重装寄存器(16位)里。更新中断通向NVIC,可以在NVIC的定时器通道中处理。
可以发现内部时钟信号CK_INT经过触发控制器后会到DAC,这样一来DAC的控制信号就不需要通过CPU中断触发,而是可以直接由时钟自动操作。比如输出正弦波时每隔1us要获得一次应该输出的电压,如果不走DAC就需要CPU每隔1us中断一次来处理它,但是走DAC就不用。
通用定时器

它的时基单元部分和基本定时器完全一样。不过从通用定时器开始计数器支持向下计数和中央对齐模式。下面是相比于基本定时器其他的主要变化:
外部时钟源
从一开始的时钟源上,通用定时器可以选择外部时钟源,没必要非要用主频时钟。TIMx_ETR引脚如果接了外部方波时钟,它的信号经过极性选择、边沿检测、预分频和输入滤波之后,作为ETRF进入触发控制器。图上可以看出外部时钟还可以由TRGI提供,这是触发输入。它也有很多输入可供选择,可以选刚才说的ETRF信号,也可以选下面的ITR信号,这是来自其它定时器的输出。其他定时器的TRGO输出可以接到这上面,实现级联输入,用来加长定时时间。如果选TI1F_ED,连的就是输入捕获的CH1引脚。另外,TI1FP1和TI2FP2分别连接的是下面CH1和CH2引脚的时钟。
输入捕获和输出比较电路
分别是图中下方左右部分,它们共用中间的捕获/比较寄存器。
高级定时器

相比于通用定时器,它在申请中断之前还要过一个重复次数计数器(Repetition counter),实现每隔n个周期产生一次更新中断。另外,添加了死区生成电路DTG,右侧的输出也能生成互补PWM波来驱动三相无刷电机。下方的刹车输入也是为了驱动电机添加的。
预分频器的时序

更新事件UEV跟着计数寄存器走,计数寄存器清零它才触发。另外,预分频控制寄存器更新后,频率不会马上改变,而是写入缓冲器,等到这个计数周期完成再变,这是为了避免计数到一半忽然改变频率导致同一周期内前后频率不同。
计数器更新时序
这里不放图了。在更新计数目标时,如果从大改到小,则计到新目标后就清零;另一种情况是已经计过了新目标,则等到计完这一轮旧目标再从新目标计,同时触发更新事件。这个过程同样通过缓冲器实现。
时钟树

时钟树在SYSTEM_INIT中加载。中心的SYSCLK就是主频系统时钟,左边是时钟产生,右边是分配。在产生电路中有4个震荡源:8MHz高速RC振荡器(HSI RC)、4-16MHz高速晶振(HSE OSC)、32.768kHz低速晶振(LSE OSC)、40kHz低速RC振荡器(LSI RC)。
高速晶振提供系统时钟供给APB和AHB总线,相比内部RC更加稳定精确。在具体配置时,首先选择8MHz为系统时钟,再启动外部时钟,进入PLLXTPRE倍频9倍升到72MHz(其实可以更高,有超频空间),等输出稳定后再把系统时钟改为它。另外,CSS会不断检测外部时钟状态,出问题时会自动切回内部时钟。
系统时钟信号进入AHB,它不分频,到APB1时二分频,也就是接在APB1上的外设都是36MHz。但是对于定时器,当APB1的分频不为1时会x2,即到定时器2-7的时钟还是72。到APB2时不分频,这就是为什么APB2性能更高。
图中可以看出时钟输出通过与门,这就是为什么c代码中需要用RCC_APB2_PeriphClockCMD
来开时钟,这句其实就是在与门边置1。
实验过程
硬件部分
把屏幕和开发板直连即可,这里延续上一个实验的接线:

注意
按键的接线不能随便接了,必须接在有外部时钟复用的引脚上,我在后续实验中接PA0。
软件部分
秒表
首先编写初始化定时器的逻辑。根据刚才的原理图,大致过程是:
RCC开时钟 -> 选择时钟源 -> 配置时基单元 -> 配置中断输出 -> 配置NVIC
使用TIM2定时器,它在APB1总线上,开启APB1的时钟。并且为TIM2分配内部时钟源:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
通过TIM_TimeBaseInit
配置时基单元,它的第二个参数需要结构体,里边写刚才说的时基单元组件的配置。它总共五个参数:
TIM_ClockDivision
,这是滤波器采样频率。这个值越低,采样频率越高,达到“稳定输出”的门槛就越高,信号效果就越好,延迟也越大。一般配置成1分频(72MHz);TIM_CounterMode
,计数模式,用向上计数;TIM_Period
,自动重装寄存器;TIM_Prescaler
:PSC预分频器;TIM_RepetitionCounter
:重复计数器,高级计数器才有,用不上给0。
在计算PSC和ARR值时,用如下公式:
这1的偏差其实就是分频系数和实际分频的关系(系数为0,一分频;系数1,二分频……),也就是说如果计1秒间隔,PSC和ARR分别取10000-1和7200-1即可。注意这两个值不要超过16位65535。最后还要使能TIM2的更新中断。代码如下:
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
的作用是清除更新中断标志位。在初始化时,系统会写值到预分频器,但是这个值被存在缓冲器中,所以系统会自动生成一个更新事件来让预分频器立刻生效。等于说TIM2的中断回调函数会立刻被执行一次,计数以后就从1开始计了。
最后一行就已经打通了定时器和NVIC。下面把它绑定到NVIC的通道中,这部分写法参考上次实验。注意,中断通道要选择TIM2专属的TIM2_IRQn:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
之后填充TIM2的中断函数:
extern uint16_t num;
void TIM2_IRQHandler(void) {
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) {
num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
在主程序中只需显示这个全局的num
,由定时器产生的中断负责更新:
uint16_t num;
int main(void) {
OLED_Init();
Timer_Init();
while(1) {
OLED_ShowNum(1, 1, num, 5);
}
}
通过TIM_GetCounter(TIM2)
还可以读到计数器的值。按刚才的ARR和PSC设置,这个值应该是在1秒内从0-9999
不断往复变化。
手动计数器
首先配置PA0,使用下拉输入:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 重要!
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
带供电的开关作为时钟源必须配成下拉,否则怎么按信号都不会变。
由于需要配置外部时钟源,在初始化时需要修改。TIM_ETRClockMode2Config
的参数分别是:时钟、外部触发预分频器(关闭)、外部触发极性(不反向,上升沿触发)、外部触发滤波器(0x00-0x0F),就是上边的稳定判断采样频率。由于手按几乎没啥稳定性,就不滤波,取0:
// TIM_InternalClockConfig(TIM2);
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
取消预分频并把自动重装改成8,计到8自动归0:
// TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
// TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_Period = 1 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 8 - 1;
实验结果
试验成功,每按一次按键,计数器+1,到8时归0并在num上+1。