文章目录
- 使用定时器做定时中断的基本流程
- TIM_TimeBaseInit
- TIM_ClockDivision
- TIM_RepetitionCounter
- TIM_Prescaler 和 TIM_Period
- 中断函数
- 实验问题:数字从1开始增加?
- 最后一行代码作用
- 对解决方案的疑问
- 事件、Flag和中断
- 验证遇到了问题。。。
- 结论
摘要:本文记录了 STM32 定时器定时中断的配置流程,重点剖析了
TIM_TimeBaseInit中几个容易被忽略的参数(TIM_ClockDivision、TIM_RepetitionCounter、TIM_Prescaler与TIM_Period)的作用。随后围绕“屏幕数字从 1 开始增加”的实验现象展开深入排查,澄清了事件、Flag 与中断三者的区别,并解释了TIMx->EGR = TIM_PSCReloadMode_Immediate这行代码引发问题的原因及正确清标志位的操作。
使用定时器做定时中断的基本流程
- 使能RCC: RCC_APB1PeriphClockCmd
- 配置定时器时钟源: TIM_InternalClockConfig
- 配置定时器: TIM_TimeBaseInit
- 配置中断输出控制,允许更新中断输出到NVIC:TIM_ITConfig
- 配置NVIC,在NVIC中打开定时器中断通道,并且分配优先级
- 时基单元的运行控制:使能计数器
- 写定时器中断函数
配置的时候跟着老师的表述和PPT的框图流程来就行。这里主要记录一下配置过程中发现的一些之前没有注意过的配置和它们的作用。
TIM_TimeBaseInit
TIM_ClockDivision
配置定时器的时候,需要给TIM_TimeBaseInit提供一个结构体。结构体中包含这个元素TIM_ClockDivision。这个参数在定时器框图中看不到,而实际上,它影响的是框图中的Input Filter模块(下面红框)。
而TIM_ClockDivision的值实际影响的是CKD这个域。在RM中对这个域有解释。
所以,TIM_ClockDivision决定的是滤波器采样频率和CK_INT的关系,而滤波器都是在外部时钟或者输入捕获等路线上。虽然从框图中看起来CK_INT和滤波器通路没有关系,但实际上应该是框图中没有表现出来。
一个例子:假设使用外部时钟TIMx_ETR,提供的外部时钟为8MHz,并且设置它的prescalar为1;设置TIM_ClockDivision为2,那么这个filter就是以72/2=36MHz来采样这个8MHz的输入,输出ETRF。
另外有一个限制:
之前以为和Nyquist采样定理有关,但是后来查了一下,其实并不是。Nyquitst采样定理是建立模拟信号和数字信号之间的桥梁,解决的是模拟信号能否被无失真地重建;而这里采样地就是数字量,对采样频率也有要求,目标是正确捕获一个边沿事件。
滤波器的采样和外部时钟生成脉冲显然是异步的。对两个异步的设备A(采样)和B(产生数字信号),A要捕获B的脉冲边沿,就要求B相对于A足够慢,慢到A在它的每个采样周期都能采到足够的点。
这里的1/4限制是数字电路同步的“建立和保持”的要求:
- 至少要让滤波器有2个cycle捕获到外部时钟是高,才能认为外部时钟处于高电平
- 至少要让滤波器有2个cycle捕获到外部时钟是低,才能认为外部时钟处于低电平
这个限制下,ETRP的最高频率就也和TIM_ClockDivision有关了: - 如果这里的TIM_ClockDivision为1,那么ETRP的最高频率可以是18MHz
- 如果这里的TIM_ClockDivision为2,那么ETRP的最高频率可以是9MHz
有的时候,由于系统的原因,外部时钟的频率并不能和我们期望的一样,比如它可能是32MHz。这个时候就违反了这个规定。当然ST也给出了解决方案,可以用Prescalar先给ETR的输入降频到16MHz,甚至8MHz,作为ETRP,输入filter。
而总体而言,这个TIM_ClockDivision参数只有当我们用外部定时器、输入捕获等情况时才会有影响;如果我们用内部时钟作为定时器时钟源,这个参数没有影响。
TIM_RepetitionCounter
这是高级定时器上的一个配置。看了一下RM里面的描述
- 如果没有这个repitition counter (RCR),是计数到了ARR就产生事件
- 如果设置了这个RCR,每次计数到了ARR,repetition counter也会减1,然后repetition counter到了0,才会报一个event
感觉就是ARR的又一次套壳。。。
做了一些查询,然后结合RM中的描述,“This can be useful when generating PWM signals”,所以看起来ARR和RCR还是有点区别的,并且特别在PWM的应用中很有用: - ARR主要管PWM的周期
- RCR管多少个PWM周期后产生一个真正的寄存器更新/事件
TIM_Prescaler 和 TIM_Period
要让定时器以某个频率报中断,就是用这两个参数,用这个公式:
C K _ C N T _ O V = f ( C K _ C N T ) A R R + 1 = C K _ P S C ( 1 + P S C ) ( A R R + 1 ) CK\_CNT\_OV = \frac{f(CK\_CNT)}{ARR + 1} = \frac{CK\_PSC}{(1 + PSC)(ARR + 1)}CK_CNT_OV=ARR+1f(CK_CNT)=(1+PSC)(ARR+1)CK_PSC
如果我们希望定时器1s报一次中断/事件,则凑一个1即可,CK_PSC是72000000,可以凑一个组合:prescalar设置为7200,自动加载设置成1000,但实际参数应该设置为:TIM_Prescaler为(7200 -1),设置TIM_Period 为(10000 - 1)。还是有几个注意点:
- 设置的组合不唯一。除了上面的组合,也可以设置Prescalar 为3600, 自动重载为2000,只要范围在0~65535 (uint16的取值范围)
- 确定了prescalar和自动重载值之后,真正赋给struct里面参数的值要减去1。这样可以从公式角度理解:公式里面PSC和ARR都是加1运算的,那么ARR和PSC需要减1带入才能得到我们想要的等式。
中断函数
进入中断函数先检查中断源
(必须!)在退出中断函数之前清除中断标志位!
实验问题:数字从1开始增加?
课程中老师发现了一个现象:照理屏幕数值是从0开始增加的,但是它上来就是1。
他立刻猜测到是不是一上来就进了一次中断导致运行了一次Num++。他说这是由于定时器初始化函数TIM_TimeBaseInit的最后一行“TIMx->EGR = TIM_PSCReloadMode_Immediate; ”导致的结果。解决方法是在初始化之后调用TIM_ClearFlag。
对此,本人还是有点疑问
- “TIMx->EGR = TIM_PSCReloadMode_Immediate” 是什么意思?
- 疑问:老师怎么直到是用clearFlag而不是clearIT?另外,看起来这起来的一下是进入了中断(只有ISR里面Num才会加1),那这个应该是属于产生了中断,而不是事件啊,所以看起来应该用clearIT?
- 验证:如果真的是这一行产生的问题,那么如果我注释掉这一行,现象是不是会变成从0开始?
最后一行代码作用
这一行代码是把EGR寄存器的bit1设置成1,也就是初始化counter的值并且(通过软件来)触发一个更新事件。
之所以要做这一步,
- 除了初始化counter的值是个正常操作
- 另一个原因涉及之前提到过电路使用“影子寄存器”的机制:我们往PSC和ARR寄存器里写入值不一定是立刻生效的,因为电路读取的是它们的影子寄存器的值;而只有在更新事件发生之后,我们写入的值才会给到影子寄存器,从而在电路中开始生效。这一步操作就是用软件触发这么一个更新事件,在初始化完成之后让我们的配置立刻生效。
对解决方案的疑问
对于老师提供的解决方案:为什么是调用TIM_ClearFlag而不是TIM_ClearITPendingBit?因为看现象是跑到了TIM2的ISR,应该是调用ClearIT才对?
其实如果看它们的实现代码,会发现这两个functions几乎一模一样!说是“几乎”是因为它们的入参范围不一样,但是它们写的是同一个寄存器!
在当前代码这个场景下,看底层对寄存器的操作,这两个function是一样的。但是为了可读性,还是要用TIM_ClearFlag,因为从这两个这两个function的命名、定义以及入参来看,这两个function的语义是不一样的。
事件、Flag和中断
这几个名词一直有点模糊。之前几乎把“事件的产生”和Flag的产生认为是一个东西,但其实不是。结合定时器,区分一下
- 事件:一个功能层面的定义,它指的是某个功能模块产生一个脉冲信号,它可以触发其他外设或者CPU动作。它可能置标志位,也可能不置。
- Flag:是事件在寄存器层面的可见形式。产生Flag肯定表示有事件,但是有事件不一定体现在Flag上。
- 中断:可以理解为“事件+通知CPU”。结合这个定时器的代码,就是UIF(Update Interrupt Flag)和UIE(Update Interrupt Enable)同时为1时,就是一个中断。当然,最终要调用到对应ISR,必须配置好NVIC,使能相关IRQ通道。
对比TIM_GetFlagStatus和TIM_GetITStatus也可以发现
- 前者只是检查了SR寄存器的相应位
- 后者除了这个之外,还检查了IT是否被使能了(DIER寄存器的UIE位,由TIM_ITConfig置1)
验证遇到了问题。。。
事实证明,注释掉这一行还是会从1开始。下面对比了一下由这行和没这行的区别:
- 如果有这一行,在执行初始化代码之后分别用TIM_GetFLAGStatus和TIM_GetITStatus读取状态,FLAG是1,而IT是0(这个其实正常,因为IT还没有被使能。。)。
- 如果没有这一行,上面两个读出来都是0。
也就是说,确实是这一行造成了FLAG被置1。
但是,删掉这一行之后,OLED还是会从1开始。尝试debug:开启debug模式,并在如下内容处打断点:
- 进入ISR时
- OLED_WriteNum的结束(不是OLED_WriteNum这个函数调用的那一行!断点停在某一行其实这行还没有被执行!)
可以发现,程序一上来会进入TIM2的ISR,让Num++。然后才执行OLED的显示操作,导致第一次显示的还是1。
猜测可能是由于一开始电路使用的是shadow ARR和shadow PSR默认值造成的(而不是我们设置的值),这个默认值导致报中断的间隔非常小,所以上来就进入中断。这种情况下,即使我们在TIM_TimeBaseInit之后执行clearIT或者clearFlag,也都是没有用的。因为这个中断是配置完IT并且TIM2使能之后产生的。
注:但是我们读不到这两个shadow的值,对于这个猜测不能很好的验证。
另外不能说老师说的问题的原因不对,而是注释掉这一行之后,会有其他问题出现导致Num还是会在最开始被+1。
“TIMx->EGR = TIM_PSCReloadMode_Immediate”的原因也是为了让shadow值立刻变成我们设置的值,但是也带来一些“副作用”(初始化之后TIM_GetFLAGStatus就是1)
如下流程图说明了执行和不执行TIM_PSCReloadMode_Immediate导致“错误”现象的原因以及执行clearFlag之后为什么可以解决问题:
对于初始化中执行EGR=TIM_PSCReloadMode_Immediate而TIM使能之后立刻调用ISR的情况,这看起来是“已经有了Flag,使能了TIM2 IT(UIE=1)之后,Flag‘变成了’中断”?看上去像是这样,但更准确的说法应该是:UIE置1之后,事件被接到了中断通路上;并且由于NVIC已经配置,它看到了这个事件,于是进入TIM2的ISR。
所以,也结合上面“区分事件、Flag和中断”的部分:事件不会“变成中断”,事件一直都在,只是UIE和NVIC配置之后,NVIC看到了它。
结论
- 老师说出现这个现象的原因是由于“TIMx->EGR = TIM_PSCReloadMode_Immediate” 这行代码。其实应该说是对的。因为确实是这一行代码导致了Flag被置1。
- 我之前认为:如果是这一行造成的问题,那么注释掉它,那这个现象应该消失。这个推断看起来是不对的。因为没有了这一行会产生其他问题,造成类似的现象。
- 总之,这里有一个正确的操作:初始化一个外设之后,执行清Flag或者清中断的操作,防止一开始出现不期望看到的状态。
- 库函数封装很好,但是确实藏了很多我们不知道的东西啊。。。