一、STM32通用定时器原理 & f* z9 C! ^6 F& x* t2 ?
- x- n( I$ x, Q: j6 Q1 p( t4 Q STM32 系列的CPU,有多达8个定时器,其中TIM1和TIM8是能够产生三对PWM互补输出的高级定时器,常用于三相电机的驱动,它们的时钟由APB2的输出产生。其它6个为普通定时器,时钟由APB1的输出产生。4 z7 |9 X [: m" s
5 i- d3 A) ]2 X+ f
下图是STM32参考手册上时钟分配图中,有关定时器时钟部分的截图:
d) V; g7 U5 L/ t0 @1 i/ l" t$ s) m6 I7 n. E
% s0 {$ t9 w8 g- u% D
! M1 o. i1 i8 F0 s从图中可以看出,定时器的时钟不是直接来自APB1或APB2,而是来自于输入为APB1或APB2的一个倍频器,图中的蓝色部分。2 M! l, i7 u( h- E+ ?# q
$ T* D% Y/ i5 R) G) \
下面以通用定时器2的时钟说明这个倍频器的作用:当APB1的预分频系数为1时,这个倍频器不起作用,定时器的时钟频率等于APB1的频率;当APB1的预分频系数为其它数值(即预分频系数为2、4、8或16)时,这个倍频器起作用,定时器的时钟频率等于APB1的频率两倍。
H- {/ L& [6 {9 e, g2 T
; v* y* w5 p! @% d4 Z可能有同学还是有点不理解,OK,我们举一个例子说明。假定AHB=36MHz,因为APB1允许的最大频率为36MHz,所以APB1的预分频系数可以取任意数值;
- O9 `0 d; I: b8 \3 c0 I( t( u: L5 ^7 f/ v, N4 H
当预分频系数=1时,APB1=36MHz,TIM2~7的时钟频率=36MHz(倍频器不起作用);; H5 ^3 V8 J- Q. }$ U* O; Y( ~
, h% d6 @- y1 {' N3 _* c6 D当预分频系数=2时,APB1=18MHz,在倍频器的作用下,TIM2~7的时钟频率=36MHz。4 q$ W$ V+ f9 i h9 g4 G2 U+ p
, ]+ T! X: S( r p8 v& O& l4 X
有人会问,既然需要TIM2~7的时钟频率=36MHz,为什么不直接取APB1的预分频系数=1?答案是:APB1不但要为TIM2~7提供时钟,而且还要为其它外设提供时钟;设置这个倍频器可以在保证其它外设使用较低时钟频率时。
z# F" v* D$ k, b# a+ I0 P) H+ a) j
Stm32外设用户手册,如图:
$ W! c& g. k0 h: p$ x5 J+ b+ @
7 t3 f: j* l( \3 Q' T4 h2 N9 J' Y+ E& M, N
再举个例子:当AHB=72MHz时,APB1的预分频系数必须大于2,因为APB1的最大频率只能为36MHz。如果APB1的预分频系数=2,则因为这个倍频器,TIM2~7仍然能够得到72MHz的时钟频率。能够使用更高的时钟频率,无疑提高了定时器的分辨率,这也正是设计这个倍频器的初衷。* f: H2 X3 B# h3 J3 i4 q
4 V6 U. J5 ]2 L- m. k$ p* Y. A
; N+ j* L$ d# z$ D5 v4 z: `" Z. X1 v% ?
二、STM32通用定时器编程! L# p7 Y2 P% o' Y4 z" u1 Z
?" F0 z; _* u6 K ^ 定时器编程,就是中断的编程。因为使用定时器必定要使用到中断。
2 M. N. j6 K6 M9 J2 c1 @8 q/ S# ~
6 Z8 x5 R7 E0 K7 i" T! |/ p
! B6 ^/ \2 E( [& |2 R; v9 S9 @
/ n$ u* O. ], p8 ~1 \" g6 X步骤一:RCC_Configuration();//设置系统时钟,包括时钟RCC的配置,倍频到72MHZ。0 ~' C& [2 i4 O P0 X9 x- O) ~' u
h( x9 A, a9 Q+ q, \! x. `: S
' s# k! ^1 V( Y S& |7 [/ h; z
# w# K" L8 a7 r步骤二: GPIO的配置,使用函数为GPIO_cfg();,该函数的实现如下:* J/ [4 T$ P0 R c$ d% [
$ G5 S' H- d( ^) I
- void GPIO_cfg()* T3 O- j6 t( ?. I
0 u/ y5 D# Z5 D6 Q, F- {. y* W- {! _' T6 g
! b! a2 P8 E4 g0 A1 v( ?) Z% @( O- GPIO_InitTypeDef GPIO_InitStructure;
7 m) t; k: V4 ^; D+ X- D - & |; V8 A9 ^6 |3 J+ W
- RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 |RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD , ENABLE);
: c7 l% r2 F3 k: p1 V7 m - % k$ @8 C7 ]# k
, h% c& v, B8 F! U7 g
% `; ~8 A2 v' k2 R- GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; //选择引脚61 j4 C/ x( L- P: c& N0 R8 ^
\7 e: u; o0 @& R- GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //输出频率最大50MHz
: @. T* N8 }7 X9 i: n
M$ k1 g0 z, A. J$ j- GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //带上拉电阻输出
h7 y' y- {: L
7 ?" X+ q. y2 A9 E' }/ S- GPIO_Init(GPIOC,&GPIO_InitStructure);
6 Q9 g, V E) y0 }/ z1 A! T - . J- U, e4 r! _' L) a/ j) P
- }
复制代码 & r, P1 `* ?: X* i4 m) c/ |. b9 K* A
实际上定时器的讲解,不需要配置GPIO的引脚,只是我们在定时器实验中,9 L! H' X, f" O
3 a9 J* w! {: Z/ @5 u
使用每隔一秒点亮一次LED灯来做实验,所以需要配置对应GPIO的引脚。
. h. |& a' [0 T3 A6 P: |& s7 g0 ^! \0 S
7 ] y% E' X. ?& B" ?- J, o- I
9 `6 a% l% ^) r- M k
步骤三:嵌套中断控制器的配置,我们照样使用函数NVIC_Config();只是初始化的过程略有不同。这里我们也把函数实现列出来:4 P4 h6 J4 Q x
# W5 p5 O+ l! W% h1 j, `+ Q0 v. r
从以上函数实现来看,实际上只是改动了结构体成员NVIC_IRQChannel的值,现在需要的通道是TIM2的通道,因此初始化值为TIM2_IRQChannel。从这里也可以看出,这个函数实际上可以看做一个模型,可以拿去别的程序中改动后直接使用。
; X& q! Q* S6 o9 Y( K6 z4 s9 W3 A! U: E* R! Q% @8 p
; l. ?6 O' ^/ |8 O5 d( d8 U! S- N/ m! b. I, w L6 g( e2 ~, r
- void NVIC_cfg()
( _, s1 I# q" L
2 h! ^% ?/ N7 `3 X/ x- {" q3 S4 C$ A- O% D( H# z
- - E5 q# n: p2 V$ p
- NVIC_InitTypeDef NVIC_InitStructure;
7 w( D. B J- y1 X - ) v9 N ^! n, V& _* H
- //选择中断分组1; h: d+ b+ p$ ?$ E
- . r# P7 g0 V" k
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
4 P8 X2 x6 u. ^- H
7 q1 c8 m3 F J* P' y
- g2 j% J' q% o2 b1 K3 ^5 [, u- 9 |4 z' o6 m3 @% X0 c
- //选择TIM2的中断通道
% {) b0 ^ P& ?; G
2 `/ x3 X9 {# d3 i+ K- NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQChannel; 0 G5 l# x/ @& O* ?" m! A
- 5 G3 t' {' K1 C7 u) g* b
- //抢占式中断优先级设置为0) s$ l5 g6 u6 R
4 B+ e( l7 \+ f" F- NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
7 N% j- s6 [: \( {1 c8 V
, ^/ Z- F$ L( B7 Z h% T- //响应式中断优先级设置为0
0 D( f7 s$ {" K! |: v
7 [, @+ y- }4 {1 E) e- NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
! j- ^9 ^# m; U5 Q2 F7 `
7 r o/ n4 Z6 f, |- //使能中断
- s) r J5 {# o, s4 Y6 i
$ o5 `1 S$ }7 R! i2 i- NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
% e9 H3 @* t& h% M - 9 E% n3 k8 H, D
- NVIC_Init(&NVIC_InitStructure);2 z6 V. G% t2 W$ ^2 G! K
- 1 [, t. h+ \4 z* J4 N" _
- }
复制代码
2 N, w! [, R. T1 I O5 _7 x0 b0 B) Y/ g. i2 x( }; T, x
步骤四:定时器的初始化配置,使用Timer_Config();。OK,关键部分出来了。
# t4 W5 p2 A- _/ l% @- \. }# }/ i* l3 d3 J& R4 }: K
我们来看下实现过程:
/ y, Z Q% a1 ^/ ]8 Y
4 m! _& z7 v. _" o6 _* K5 @- TIMER_cfg(); //定时器的配置
& A& {* l9 _: Y2 R) `& o) u - ( S+ _! }7 C! a% G6 q4 R$ ~ r- p
- //开启定时器2- i# e3 ]+ |5 u& F
' X o9 d/ v; ^2 L5 [# D# W3 w' z- TIM_Cmd(TIM2,ENABLE);
1 c1 R! i3 M9 R: X) o3 A8 N' t, r) |8 G - % X. K8 z3 d4 O6 e
- voidTimer_Config(void)9 K. e. p9 q+ T" R
- {
. w) @ S* h3 a$ @, \) |
8 J7 ~: v. j! {# e$ R- J% K- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); 8 ^! J- g6 l' m; l7 i, ^9 u
8 U2 j) I3 i" Y# z: F5 J! J; Q5 A) [, `- TIM_DeInit(TIM2);
, l0 {" n* x0 M- S4 C- n
( H7 m1 A" j {$ l5 h) H- TIM_TimeBaseStructure.TIM_Period=2000-1; //自动重装载寄存器的值
! f' D: r1 Y! L7 e6 P5 ?- k - # {1 R, ^2 w4 N/ a4 X* z. E. H& ^- Q
- TIM_TimeBaseStructure.TIM_Prescaler=(36000-1); //时钟预分频数
* z$ X0 x z5 A. e - 9 C# K- N6 |3 q% b1 Y- a
- TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //采样分频
6 K2 f1 F+ D1 A) F# S3 W% l - 5 s& e" j/ B! D! W9 V
- TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
) c- T: m5 P4 h6 b% J - TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);# H$ [& G- E, m9 w
- 4 ^3 S! `) ]* y3 r4 }8 C2 g
- TIM_ClearFlag(TIM2,TIM_FLAG_Update); //清除溢出中断标志$ m9 t1 Q& d: z; l' w
- ! d! {# S, `$ h# ] j0 f4 w C
- TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);+ E: V1 f W/ {& M v* G
/ C. v) e0 p6 A. ~; Q- TIM_Cmd(TIM2,ENABLE); /开启时钟/ v# h& j1 l; E, L p2 L& x6 v
1 H2 d f: B$ i$ v+ H% e3 Z- }
复制代码 " u/ ], F) Y& o# v# v
我们每个语句都来解释一下。首先我们想使用定时器,就必须使能定时器的时钟,这就是函数 RCC_APB1PeriphClockCmd();,通过它开启 RCC_APB1Periph_TIM2。! K9 z; x. _ U
( x1 t. A) c! JTIM_DeInit(TIM2);该函数主要用于复位TIM2定时器,使之进入初始状态。9 y/ M- _: |5 J
& h! N7 N$ T% D; F% F" Y( i
然后我们对自动重装载寄存器赋值,TIM_Period的大小实际上表示的是需要经过TIM_Period次计数后才会发生一次更新或中断。接下来需要设置时钟预分频数TIM_Prescaler,这里有一个公式,我们举例来说明:例如时钟频率=72MHZ/(时钟预分频+1)。说明当前设置的这个TIM_Prescaler,直接决定定时器的时钟频率。通俗点说,就是一秒钟能计数多少次。比如算出来的时钟频率是2000,也就是
5 x; x% S$ _1 Q: g
4 S( M( z) n/ C% t2 `一秒钟会计数2000次,而此时如果TIM_Period设置为4000,即4000次计数后就会中断一次。由于时钟频率是一秒钟计数2000次,因此只要2秒钟,就会中断一次。" G+ I2 U# a7 f% l1 X( u
6 H3 N* N; I# b再往后的代码,还有一个需要注意的,TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;就是我们一般采用向上计数模式,即每次计数就会加1,直到寄存器溢出发生中断为止。最后别忘了,需要使能定时器!!
; M; u0 t5 j# M* c- v' s. h# x$ f! x# r3 D+ M& W) q9 X
发生中断时间=(TIM_Prescaler+1)* (TIM_Period+1)/FLK
0 y1 i; |% G4 l% |
4 f4 }' z6 @+ M, e4 t3 B q. ] 用上述公式可算出:发生中断时间 (2000-1+1)*(36000-1+1)/72000000=1 秒
- y5 f1 b8 I+ I- ^' {6 x9 [7 y& Z& H. y, Q% T( T
步骤五:编写中断服务程序。同样需要注意的,一进入中断服务程序,第一步要做的,就是清除掉中断标志位。由于我们使用的是向上溢出模式,因此使用 T: Y' h7 _2 U4 e8 r" {6 S
4 ]0 Q) y% x( T* A9 e1 R$ v) A
的函数应该是:TIM_ClearITPendingBit(TIM2,TIM_FLAG_Update);。+ X$ n0 a% u1 X. G. U5 u
o: @7 X4 l" Q, W: ISTM32开发板实现的中断服务程序如下:
9 j. ]# {, H( H0 A$ c6 M% i8 J# n+ d1 k) f) M2 H( Q
每隔一秒,发生中断时,进入此中断函数执行程序,让LED闪一下,此中断程序所在文件stm32f10x_it.c
; Q5 P B* ]* Z6 S4 {3 m7 H1 |% x# d$ @$ r, y9 S
- /*******************************************************************************
& O8 y6 v3 t* d% w/ l1 S
& U( n- x( R8 z% S2 q1 C- * Function Name : TIM2_IRQHandler
% }; F4 f6 C4 n: @/ b. F- p \, r
, \, [- P4 f$ Y% j! l$ H0 K- * Description : This function handles TIM2 global interrupt request.4 c0 r5 ?% r8 T( m
1 z* B- A& X" l" y. s' U9 [6 D A- * Input : None
) r# L, m; Y1 c+ m* G
( {8 \, w! Q2 Q- * Output : None) {, N& ~- @$ y5 l; |
- , j) g, q) c& p' A8 Y
- * Return : None
9 e5 u3 C# c6 H+ }' m! j, C& ?* K
- C; h/ A" I9 u: y- ******************************************************************************/. H1 J c; t" t# b
- / E6 l' U0 E3 E% U
3 Z' S6 {0 ?+ H2 G
: k9 l' }( ~ V8 Q4 o4 \$ E1 n- void TIM2_IRQHandler(void)
) u: I1 E6 Z9 t" `; Y. x) _ - 2 K6 c1 }; x/ K3 C) S" G
- {) _% M' a* C! ~
5 Z6 |4 u) Z. }. E/ c7 g0 `4 u- u8 ReadValue;
H: F V& w$ x
3 y$ }- k6 r9 d/ ^# \8 f
- \' x% F, O. a# z4 d! o0 C+ I a- - C9 t' ^" U# w: R9 a
- if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) //检测是否发生溢出更新事件% C+ @( _4 J, n/ P; s) h$ k
- , N7 A4 C R9 i' |3 M4 l
- {5 h% M, d+ K7 \6 E& \$ m
- 1 _8 C, L% X+ L! V# h
- <span style="color:#ff0000;">TIM_ClearITPendingBit(TIM2 , TIM_FLAG_Update);//清除TIM2的中断待处理位
8 N$ e* z9 w2 {/ ] - - Y2 t2 J6 e6 B% t8 ]6 ]3 |+ ?
- </span> //UART2_TX485_Puts("123450");
' G- Q. A5 u0 g0 J" ` - " Y7 x! @1 A% W: j$ f6 l& _+ c
! w n1 }3 O9 B- [- 8 D7 g, |& @( H! ^5 S! L
- /*调试定时器测试*/* o: g, e7 S" e' P- n" ` H4 Y1 R
- 1 [8 {9 y6 ~1 k. y. m
- ReadValue = GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_6);
. X7 p F: M+ P* c% q M7 ~
+ b7 w5 Z% _: a4 S- if(ReadValue == 0) //若该端口当前为低电平,
2 _+ ?7 f/ \+ H: B! C) i - 3 d2 f. D9 g+ b% B& @5 l8 S: {
- {
, a' `7 S6 k4 {- J, k/ |* J7 m+ ` - 8 `* a& u9 i, Y' q6 z' t
- GPIO_SetBits(GPIOC,GPIO_Pin_6);//将其改为高电平输出 ;$ Y* B# C2 M% m, g- d$ Z: s% b& O
- # t' F4 V1 B1 ]& P
- }
- M5 K) H# ~& }; }1 | - 4 n. u y2 X8 l, B/ X7 C
- 1 k9 x; N9 \$ f' Y1 D V
: S L% d" i% \( K' T( I- else //若该端口当前为高电平,6 v, m2 ], g3 Y' C
& L" E; u* @3 r( T1 G- {
: H5 v# n4 o# v' D: {- C" _ ]9 S2 \
3 B q |0 G' x- O( ?4 U- GPIO_ResetBits(GPIOC,GPIO_Pin_6); //将其改为低电平输出;7 D' u4 Y/ c4 m+ ~
- $ ]5 d' r! X1 A: q; q% M% W
- }& c4 d) }( f0 u _
! Z) a! G9 E6 o& h
9 p: ^9 |# \) g- # Y6 ^% G$ a4 T: `6 H9 f9 e6 _3 ?- N! }
- }
2 y) n1 a6 q* \ - , P+ _7 a; F+ M9 b: |, r) N& B
- }
复制代码 2 V, f$ Y! g1 V# v/ t% @0 l
4 l' z' j1 Y+ \
普通定时器工作原理图' O0 O$ b5 ]' f0 O
3 `+ t3 h- H+ l; y6 v4 `
9 t5 s7 E# `4 S6 |) [' q* _
2 G+ Q$ u, t) U) p- C% ~5 m& T4 q
( a$ I% M+ |/ R' V8 x" | |