电子产业一站式赋能平台

PCB联盟网

搜索
查看: 37|回复: 0
收起左侧

嵌入式软件开发,手把手教你如何实现一个软件定时器。

[复制链接]

569

主题

569

帖子

4259

积分

四级会员

Rank: 4

积分
4259
发表于 昨天 19:26 | 显示全部楼层 |阅读模式
我是老温,一名热爱学习的嵌入式工程师
& q, p. B5 Q- u7 H* H9 W关注我,一起变得更加优秀!1.什么是软件定时器软件定时器是用程序模拟出来的定时器,可以由一个硬件定时器模拟出成千上万个软件定时器,这样程序在需要使用较多定时器的时候就不会受限于硬件资源的不足,这是软件定时器的一个优点,即数量不受限制。, ]3 |* H3 o5 ?5 I) V7 n
但由于软件定时器是通过程序实现的,其运行和维护都需要耗费一定的CPU资源,同时精度也相对硬件定时器要差一些。
) S# P  ^/ Y! H: R2.软件定时器的实现原理在Linux,uC/OS,FreeRTOS等操作系统中,都带有软件定时器,原理大同小异。典型的实现方法是:通过一个硬件定时器产生固定的时钟节拍,每次硬件定时器中断到,就对一个全局的时间标记加一,每个软件定时器都保存着到期时间。8 T8 |( X6 U) A/ O
程序需要定期扫描所有运行中的软件定时器,将各个到期时间与全局时钟标记做比较,以判断对应软件定时器是否到期,到期则执行相应的回调函数,并关闭该定时器。( R3 ^7 P0 v+ q7 q' ?- [
以上是单次定时器的实现,若要实现周期定时器,即到期后接着重新定时,只需要在执行完回调函数后,获取当前时间标记的值,加上延时时间作为下一次到期时间,继续运行软件定时器即可。5 A0 {: B0 K+ p: C" ?. j
3.基于STM32的软件定时器3.1 时钟节拍软件定时器需要一个硬件时钟源作为基准,这个时钟源有一个固定的节拍(可以理解为秒针的每次滴答),用一个32位的全局变量tickCnt来记录这个节拍的变化:2 w. K/ p$ M2 E. ?  ]) R- L
static volatile uint32_t tickCnt = 0;    //软件定时器时钟节拍& a+ S2 _3 x3 T
每来一个节拍就对tickCnt加一(记录滴答了多少下):
) z0 J# K8 }) i7 \/* 需在定时器中断内执行 */4 o# T% u! L8 U$ d2 v; _4 p- s- x4 p
void tickCnt_Update(void)
# F; o" ^2 B4 F# t9 F5 E; W{1 [. u! S7 H- u& k6 j5 E5 K
tickCnt++;, f* g7 W. t+ G' r) z
}* Q7 Q1 C- x. n1 I; d! V# i
一旦开始运行,tickCnt将不停地加一,而每个软件定时器都记录着一个到期时间,只要tickCnt大于该到期时间,就代表定时器到期了。/ w) h* X" i3 [$ v
3.2 数据结构软件定时器的数据结构决定了其执行的性能和功能,一般可分为两种:数组结构和链表结构。什么意思呢?这是(多个)软件定时器在内存中的存储方式,可以用数组来存,也可以用链表来存。. ]1 T, [# g3 h
两者的优劣之分就是两种数据结构的特性之分:数组方式的定时器查找较快,但数量固定,无法动态变化,数组大了容易浪费内存,数组小了又可能不够用,适用于定时事件明确且固定的系统;2 T3 E$ b6 g2 x1 P
链表方式的定时器数量可动态增减,易造成内存碎片(如果没有内存管理),查找的时间开销相对数组大,适用于通用性强的系统,Linux,uC/OS,FreeRTOS等操作系统用的都是链表式的软件定时器。
$ |" D; x7 y+ k本文使用数组结构:0 V' x5 t: d6 R; E; g0 z$ O* }" ?
static softTimer timer[TIMER_NUM];        //软件定时器数组
  x8 }# ]4 K- I数组和链表是软件定时器整体的数据结构,当具体到单个定时器时,就涉及软件定时器结构体的定义,软件定时器所具有的功能与其结构体定义密切相关,以下是本文中软件定时器的结构体定义:
3 B+ c6 N- N; u# I3 jtypedef struct softTimer {
( D& _% g' k2 Fuint8_t state;           //状态
0 ^. M; S. ~. W! n* f: ^uint8_t mode;            //模式
- g& U. u( f4 j# Q& h4 d5 `uint32_t match;          //到期时间$ k5 \+ k+ m' O" f# {
uint32_t period;         //定时周期* t) S" a2 E- K9 g! H/ c
callback *cb;            //回调函数指针
6 k% N" H1 U' Z4 vvoid *argv;              //参数指针, o% `& D# }. k% z  D- g
uint16_t argc;           //参数个数
: m' b9 G  q- E' E- N' P7 d}softTimer;3 P3 G( L! e  v+ n  T* T
定时器的状态共有三种,默认是停止,启动后为运行,到期后为超时。
$ z% K3 j! M2 x( u/ w3 Jtypedef enum tmrState {: W. E7 {$ U  O
SOFT_TIMER_STOPPED = 0,  //停止
6 h, g& J. g! F. { SOFT_TIMER_RUNNING,      //运行
# L4 \+ u7 X$ T8 Z SOFT_TIMER_TIMEOUT       //超时
4 r" s+ w1 e! b: k3 o}tmrState;
% I5 [1 g8 s5 G7 i( k/ u模式有两种:到期后就停止的是单次模式,到期后重新定时的是周期模式。  M0 d0 \3 p1 g) F3 U0 Z
typedef enum tmrMode {& X6 V/ Y- G, \; w/ b
MODE_ONE_SHOT = 0,       //单次模式6 S7 A: N* V/ q" ~- z/ v
MODE_PERIODIC,           //周期模式
. z4 e" A9 u5 h( X  f! p}tmrMode;
/ A8 p& P0 _/ b3 G不管哪种模式,定时器到期后,都将执行回调函数,以下是该函数的定义,参数指针argv为void指针类型,便于传入不同类型的参数。
  t1 ~- y! N  A/ ?4 ^# [typedef void callback(void *argv, uint16_t argc);, S5 {; |9 x+ G5 L. e3 s
上述结构体中的模式state和回调函数指针cb是可选的功能,如果系统不需要周期执行的定时器,或者不需要到期后自动执行某个函数,可删除此二者定义。  }* M4 F& f7 ?# e$ f$ ]! ]
3.3 定时器操作3.3.1 初始化首先是软件定时器的初始化,对每个定时器结构体的成员赋初值,虽说static变量的初值为0,但个人觉得还是有必要保持初始化变量的习惯,避免出现一些奇奇怪怪的BUG。
) R5 e6 G  ?+ K( s( ivoid softTimer_Init(void)
3 A& b# }! Y; h- O{0 K4 c+ j" a  y/ s$ F
uint16_t i;
) y9 n7 u9 A* ~* I! S4 \! `$ s for(i=0; i
0 W1 L1 A! f9 X9 t1 S( V( A2 q0 x  timer.state = SOFT_TIMER_STOPPED;7 o+ N3 B# H7 @
  timer.mode = MODE_ONE_SHOT;2 f# K$ h, Q1 D) T
  timer.match = 0;0 V/ b: e3 @6 Z# }5 ?
  timer.period = 0;
) _0 O! U- n, n6 ^# p" Q4 K  timer.cb = NULL;- G( `3 v/ X3 G# g
  timer.argv = NULL;
) \9 d3 U/ H. I% C1 e, h% j  timer.argc = 0;0 W+ E; J6 [% i; ], e
}
: l  n: J% B  I. O# [* P9 H& c}
8 ?& d) R9 d6 j/ c9 x2 A! G1 m3.3.2 启动启动一个软件定时器不仅要改变其状态为运行状态,同时还要告诉定时器什么时候到期(当前tickCnt值加上延时时间即为到期时间),单次定时还是周期定时,到期后执行哪个函数,函数的参数是什么,交代好这些就可以开跑了。
9 D3 L* F. E+ E- b+ `void softTimer_Start(uint16_t id, tmrMode mode, uint32_t delay, callback *cb, void *argv, uint16_t argc)
* o- F5 {- }' T! Z6 H: D, V! k{( i0 Z- v/ ~9 g" M
assert_param(id 7 ^- m2 Y) R; A) F) w4 V
assert_param(mode == MODE_ONE_SHOT || mode == MODE_PERIODIC);% q, ~) o* J, w* l! [: e
$ L# N9 ]. g" q% z
timer[id].match = tickCnt_Get() + delay;
: Q' C9 \& c4 ^$ t6 y timer[id].period = delay;
6 d$ P. f9 O- W3 H$ m$ o timer[id].state = SOFT_TIMER_RUNNING;$ a/ z+ J- s0 A- v. M1 }' U; l
timer[id].mode = mode;
( W; k2 o3 [! m) q7 c: X: J timer[id].cb = cb;2 X& x: e1 h, B: K: a* R
timer[id].argv = argv;& ]0 R9 C% E1 E2 y
timer[id].argc = argc;
% ^1 c! s* I1 @. j}
3 U) @. {: ]" k6 I上面函数中的assert_param()用于参数检查,类似于库函数assert()。+ v4 |+ M3 a, E" h' R
3.3.3 更新本文中软件定时器有三种状态:停止,运行和超时,不同的状态做不同的事情。停止状态最简单,啥事都不做;运行状态需要不停地检查有没有到期,到期就执行回调函数并进入超时状态;1 s1 F7 d" |$ i; Y$ p3 x) r
超时状态判断定时器的模式,如果是周期模式就更新到期时间,继续运行,如果是单次模式就停止定时器。" `: S) f  p& s; r( ^+ U
这些操作都由一个更新函数来实现:
( Q6 j# m$ P/ F; k! svoid softTimer_Update(void)
4 ~& D+ j2 I7 C2 }+ O1 }{: J% S# `: k  m6 R. X4 F
uint16_t i;
2 m+ k. L& [2 c% w3 gfor(i=0; i6 X$ A* A2 C8 F$ e+ W# Z
   switch (timer.state) {3 `, |7 s1 H! M+ ~
       case SOFT_TIMER_STOPPED:
+ _2 ^; D0 ]% G     break;
% k3 U1 \) @  K, v; Z3 h. Q    case SOFT_TIMER_RUNNING:% N! g; {; L  j; t! M# k
     if(timer.match
- h2 A) }* C, @$ i3 y8 Z2 a; |      timer.state = SOFT_TIMER_TIMEOUT;
# v1 ~! r0 x6 J4 g8 V      timer.cb(timer.argv, timer.argc);       //执行回调函数& N: J9 Y4 i8 H
     }* B# i" r( ^( v- n% Q$ g9 n
     break;9 ~5 x& q& Z# _7 C" @; v* C
   : L# o1 y: `! b4 s
    case SOFT_TIMER_TIMEOUT:# S1 w  _6 u: {; T: F  z% Z
     if(timer.mode == MODE_ONE_SHOT) {
) E/ U5 W$ _7 R; ?" o7 d  x         timer.state = SOFT_TIMER_STOPPED;
0 ~# f) N$ l2 g4 D; I0 R( ]     } else {
( s( a( q+ r7 C) V- h      timer.match = tickCnt_Get() + timer.period;. z5 t' w6 m# S; ^/ n8 q( t+ L
         timer.state = SOFT_TIMER_RUNNING;7 u9 `  d* Y; T, s9 u# J
     }- H& Z+ s5 _' n8 W" `
     break;
2 ?5 b' }+ n% e# E, q. N. j    default:
+ m/ N: K% x; B$ i* f     printf("timer[%d] state error!\r1 Y: {+ _  [1 D0 M
", i);
( \% x  ?4 v5 J- S     break;
9 `% c" c( o, o   }$ Z- D% ?: t9 W# x6 x$ Q3 `
  }6 D- a0 G( }5 _! @! S4 K
}
9 \$ X/ D5 Q# R& w8 P3 U3 H3.3.4 停止如果定时器跑到一半,想把它停掉,就需要一个停止函数,操作很简单,改变目标定时器的状态为停止即可:( g' i! d0 u* |; \( L
void softTimer_Stop(uint16_t id)6 i& w0 y6 ]0 ~, S& F7 d- c
{
; p7 j2 e2 {/ w! |6 k. L assert_param(id 3 S* |. y" C- W
timer[id].state = SOFT_TIMER_STOPPED;
3 p3 G  l+ j9 y" b}
) B) }- D6 n  [# Z; ^3.3.5 读状态又如果想知道一个定时器是在跑着呢还是已经停下来?也很简单,返回它的状态:
: h0 s" M* C  m! S7 J" luint8_t softTimer_GetState(uint16_t id)( `* C: r! G4 J5 Z
{+ n6 P  N4 f6 P* \- a# Z! O, H
return timer[id].state;
6 S5 U$ _" P! K# _. e& M# h}2 z2 y( }1 N" Z6 q# ~2 u: O" Y$ b
或许这看起来很怪,为什么要返回,而不是直接读?别忘了在前面3.2节中定义的定时器数组是个静态全局变量,该变量只能被当前源文件访问,当外部文件需要访问它的时候只能通过函数返回,这是一种简单的封装,保持程序的模块化。# C- }1 R4 u, H7 N
3.4 测试最后,当然是来验证一下我们的软件定时器有没达到预想的功能。1 Z  z" u/ v1 v. M9 n3 f8 g- Y1 u8 c
定义三个定时器:
% V2 _0 ?8 \6 n* A3 ]9 O定时器TMR_STRING_PRINT只执行一次,1s后在串口1打印一串字符;# \% C8 A9 d, o! W- Z8 y! K- z
定时器TMR_TWINKLING为周期定时器,周期为0.5s,每次到期都将取反LED0的状态,实现LED0的闪烁;
8 V7 b) ~9 e2 k; z定时器TMR_DELAY_ON执行一次,3s后点亮LED1,跟第一个定时器不同的是,此定时器的回调函数是个空函数nop(),点亮LED1的操作通过主循环中判断定时器的状态来实现,这种方式在某些场合可能会用到。  \/ L' w* s+ E
static uint8_t data[] = {1,2,3,4,5,6,7,8,9,0};
$ P9 ~  C. m2 i8 }4 ]3 Z7 Tint main(void)
; X7 o! s" O( y& P  ?* x9 h1 h{
) {* B/ u2 I4 Y% L4 \9 o USART1_Init(115200);
' W' Q" @# j2 b# ~+ k TIM4_Init(TIME_BASE_MS);# |& m; L. B) i% J1 F1 u3 e
TIM4_NVIC_Config();
+ H; @( l" U) i4 i LED_Init();# }# Y! O9 R0 P: t
printf("I just grabbed a spoon.\r
/ q- R( \: R( c1 A, X! A0 F, g");
6 I( O$ _  T! S) } softTimer_Start(TMR_STRING_PRINT, MODE_ONE_SHOT, 1000, stringPrint, data, 5);
9 h' F  e/ g4 F9 l0 c2 A; D6 J; f  [ softTimer_Start(TMR_TWINKLING, MODE_PERIODIC, 500, LED0_Twinkling, NULL, 0);& D2 p5 p4 |/ [
softTimer_Start(TMR_DELAY_ON, MODE_ONE_SHOT, 3000, nop, NULL, 0);
! @8 L. V  l0 B( Y" D/ hwhile(1) {1 |3 m6 L0 \4 ^! C* i3 Z
  softTimer_Update();  Z7 H. B+ D! m# _/ N
if(softTimer_GetState(TMR_DELAY_ON) == SOFT_TIMER_TIMEOUT) {
& U( R, u& O6 z# M1 y; t' L   LED1_On();
/ a+ h2 \  @3 k, d2 Y  t$ U  }  k: l/ q$ E, s/ D6 r  q" w( L% H7 ?' \
}- }8 H  I6 e1 x" |! M; b+ P
}5 Z8 z% {. X' o3 a
文章来源于网络,版权归原作者所有,如有侵权,请联系删除。
8 p' i9 P/ W7 C2 s
% n2 V1 t, l9 t8 i5 ]

wev0qhl5m1264033541113.jpg

wev0qhl5m1264033541113.jpg

+ H& J8 s. w& J- L' B/ a0 b  \( z4 Q! A$ ?5 ~6 N
-END-
" {( Y3 \/ w! A# E/ S; w+ |7 M' g往期推荐:点击图片即可跳转阅读9 j1 H2 n- h8 x! E# G

jmocphi1hkk64033541213.jpg

jmocphi1hkk64033541213.jpg

& }0 \( B2 x, z! p" G" \# S被灵魂拷问!嵌入式为啥要关注AI?这到底能产生什么样的价值?
2 s, t; S8 ^9 G

dwg04quksih64033541313.jpg

dwg04quksih64033541313.jpg
% {4 y6 `: e$ J8 g3 P& N
嵌入式软件工程师,技术栈发展的三个阶段,天花板到底有多高?4 P: a; @2 k$ x4 v

ck0bjwy05nw64033541413.jpg

ck0bjwy05nw64033541413.jpg
  \" K# Y8 w9 q. R9 h# A
小师妹深夜求助,记录一次不太成功的硬件拆解经历!2 E. f! E/ s! u; h
我是老温,一名热爱学习的嵌入式工程师* A! U: D/ Y  O! V# F
关注我,一起变得更加优秀!
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


联系客服 关注微信 下载APP 返回顶部 返回列表