你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

单片机裸机程序框架是怎样炼成的?

[复制链接]
gaosmile 发布时间:2020-8-26 23:07
前言

前不久,我有位做测试的朋友转去做开发的工作,面试遇到了一个问题,他没明白,打电话问了我。题目大概就是:

在单片机裸机开发时,单片机要处理多个任务,此时你的程序框架是怎样的呢?

这其实是个经典面试问题,我以前面试也被问过。

答案一:轮询系统

代码结构如:

左右滑动查看全部代码>>>

int main(void)
: \% z  C, O# b) q* [4 b{
6 w) v" y' N. n6 ] init_something();8 K# p+ e2 e# W2 P9 z4 N+ q( I3 I

7 ?2 b+ R: R& {8 r while(1)
- o$ Z% F3 {7 F2 s. n9 p" ]! X {" ]# v8 n) G4 u4 v7 l7 D3 v. m8 F
  do_something1();
2 t0 V# H8 L: z( j% k        do_something2();$ Z5 ~7 ?. p! x: B5 X5 a, q. H
        do_something3();
0 @2 R- T' E% S; c$ l }+ ^- F8 U& f9 d% L" w& A, x. }
}
3 J0 B& E6 u' C# g

这种结构大概是我们初学单片机的时候的代码结构。在没有外部事件驱动时,可以较好使用。

只答出了这种情况,印象分估计会比较低,多半凉凉。

答案二:前后台系统

代码结构如(该代码来自 《RT-Thread内核实现与应用开发实践指南》 ):

左右滑动查看全部代码>>>

int flag1 = 0;
4 M8 _! Y' p2 e' M2 o6 Hint flag2 = 0;
- z9 ]. Q2 `, aint flag3 = 0;) Y* U* l1 d8 S+ {4 i1 |* V9 P5 N/ H
, |6 q7 k6 N  X0 V, n
int main(void)+ t/ x2 |# L, }$ t
{
4 N- U' O% T, d- { /* 硬件相关初始化 */
9 q+ l4 j: @& f3 u HardWareInit();$ j) x# m6 L! ]5 |# x$ d+ c% h. A
$ z6 k8 ?9 u  E/ w, E" M
/* 无限循环 */$ G; I. c3 Y5 e2 X# A) F$ r
for (;;) {
+ b) v2 _4 c0 H! {   if (flag1) {
4 N2 S8 o4 j8 Y& z2 X     /* 处理事情 1 */0 z. \# ]8 a+ y( b6 Y+ |# J
     DoSomething1();
5 Q+ Z+ @: ?. C; T. b7 G9 ^! z   }
0 G+ w( q" X/ S: [# ~3 D" {. I; N2 L/ p* R/ S  a1 |8 f
   if (flag2) {
3 a2 b) p* x& k- [% z# k5 ?     /* 处理事情 2 */4 g. f& j+ ]; }9 a6 R
     DoSomethingg2();" r% s3 }! G# X) `5 f
   }3 _! @4 U' D+ Q6 d9 P
+ f+ g0 Q! o, E  H' |  X
   if (flag3) {
  x  ?3 j* `$ x. V( _     /* 处理事情 3 */6 ^8 G& r* ?$ R2 M+ V; h
     DoSomethingg3();
* I% D5 R8 \# O4 V8 S6 R+ N& ?% b2 T   }
7 i( @5 U9 B* O8 r$ M0 c }- U- j  T+ D- a' l8 a( r
}
+ t7 Z( M" h* A& t; ~0 m1 M9 O+ j& |9 k" N
void ISR1(void)
- \" ~0 p& G) g/ B( L* l( U{0 K8 M# u7 B$ C" B
/* 置位标志位 */
- p. q, W$ @7 {" X5 ^- [! a! ~ flag1 = 1;
2 o! R5 n& ^5 T  y! D /* 如果事件处理时间很短,则在中断里面处理
6 z4 }8 r7 C0 b( n" m0 o4 v& S/ ?0 | 如果事件处理时间比较长,在回到后台处理 */8 ~/ C) X6 `- j5 L; P$ u# N
DoSomething1();
4 O& G2 Z# @8 P1 {& A}
2 v( K$ b7 x/ Y
$ e6 ^% U" o* O$ U( j2 ivoid ISR2(void)) m9 F' a; ~& \
{
. x% b! _9 b2 y /* 置位标志位 */
6 q5 s1 d0 N' E+ ?' t& T! Z flag2 = 2;
9 V8 S* a, Q! O4 h8 s9 |8 c  h7 \% ~8 B! x: S1 J
/* 如果事件处理时间很短,则在中断里面处理- G% `5 a0 y9 h& ~
如果事件处理时间比较长,在回到后台处理 */
$ A* Y, W+ w* M0 ^" a DoSomething2();7 c4 W6 V8 `9 L8 z
}2 i, u: y' Z3 H- l6 y
3 B- W- ^$ }0 P, O; P
void ISR3(void)% k1 o8 U0 b% V) ^, ~3 d
{4 `  b2 ?4 S9 L$ ^, N
/* 置位标志位 */. r% ~& ^, {. i% c. x' P0 w0 X2 f0 G
flag3 = 1;7 W/ z- }+ Y' D7 @4 i$ J
/* 如果事件处理时间很短,则在中断里面处理
1 g! W/ |7 ~, O# c, d2 ]1 | 如果事件处理时间比较长,在回到后台处理 */, }: F9 G/ ?5 |8 a3 Q8 w3 F4 [
DoSomething3();
" r. G; g# `( e6 }2 l}
4 u$ W% W+ \, }& M$ W

此处,中断称为前台,main中的while循环称为后台。相比于循环系统,这种方式相对可以提高外部事件的实时响应能力。

可以回答出这种情况,印象分大概一半以上,会再细问。

答案三:升级版前后台系统(软件定时器法)

以前,学C语言时,常常听到有人说:指针是C语言的灵魂,没学会指针就是没学会C语言。。

后来,学单片机时,又听到有人说:中断和定时器是单片机的灵魂,没掌握中断与定时器就没学会单片机。。

大佬们都那么说了,那就拿定时器来搞点事情。定时器浑身都是宝,本篇笔记我们来介绍使用定时器(系统滴答定时器或者其它定时器)来做的裸机框架。软件定时器法也有另一种说法:时间片轮询法。

可以回答出这种情况,这场面试多半稳了。

下面以STM32单片机为例看看这种方法的使用。

站在巨人的肩膀上

开源项目—— MultiTimer ,项目仓库地址:

http://github.com/0x1abin/MultiTimer

1、MultiTimer 简介

MultiTimer 是一个软件定时器扩展模块,可无限扩展你所需的定时器任务,取代传统的标志位判断方式, 更优雅更便捷地管理程序的时间触发时序。

2、MultiTimer 的demo
左右滑动查看全部代码>>>
#include "multi_timer.h"
5 ?$ d( }6 u$ w4 x7 O% C2 r/ `/ H( `
, C( R( g" M' X+ H/ f2 j3 ~struct Timer timer1;! K* |5 _" x$ g/ o
struct Timer timer2;
& p  J6 K' Z0 m2 x0 z8 j4 p5 c) s# h9 a( F/ u1 d' |
void timer1_callback()
, Z* c+ `. i$ b8 u& t{
& U. X  n6 B& F: S" i. A6 \    printf("timer1 timeout!\r\n");
' i5 Z) Y& R' l- G4 S}/ w- f2 C' [% i1 a& b
% S4 Q3 Q; P. B. m9 u1 y) z
void timer2_callback()2 `5 R' U& d6 |) N+ v
{
. \# o8 a( Y+ S( o8 O- k* W/ P    printf("timer2 timeout!\r\n");
  B4 }; g+ f% d" y' q/ r) T}' ~5 g/ R$ }6 N3 f4 }
7 h( ]9 g4 j8 d3 p( I2 a* F
int main()
$ {* W+ y( }: Z: V5 Q5 F0 J{
) m" c9 `4 J9 N' [& r; |    timer_init(&timer1, timer1_callback, 1000, 1000); //1s loop
$ f3 _5 S: W$ ^/ t( h    timer_start(&timer1);, Z; k( ]: ]) N0 d- b1 M) n& X
    - `: s. j& G  B: L+ o
    timer_init(&timer2, timer2_callback, 50, 0); //50ms delay
% D. z; O5 v6 v- G; L2 E1 h/ H    timer_start(&timer2);
. V& w4 x" O0 @) n' Q    9 T4 r5 J. G0 x6 I
    while(1) {  U5 ^' f8 J# E
        
% u  e$ G% c- B, S6 h3 I* h" T+ b        timer_loop();( e! z2 K( L$ c' A
    }* R9 }- B# P! @8 d# D9 p
}
' G6 g+ w8 `" L( }) C/ x
9 T0 x' v5 d) r1 J* D, \4 Uvoid HAL_SYSTICK_Callback(void)3 Z( {$ ]8 `% ^9 s( O9 s
{
* i. v  w# \4 L2 w' V: n$ Z    timer_ticks(); //1ms ticks* m6 Z! B8 }9 F# F4 ^( q5 ~3 F- E
}
" m$ s3 `. ^: x2 \) _- c7 z$ j
3、MultiTimer 的移植、剖析

想要对MultiTimer 进行深入学习可阅读项目源码及如下这篇文章:

第6期 | MultiTimer,一款可无限扩展的软件定时器
% J9 H# l* i, x8 F. l3 J" P

自己动手,丰衣足食1、代码模板

准备一个定时器,可以是系统滴答定时器,也可以是TIM定时器,使用这个定时器拓展出多个软件定时器。

比如我们系统中有三个任务:LED翻转、温度采集、温度显示。此时我们可以使用一个硬件定时器拓展出3个软件定时器,定义如下宏定义:

左右滑动查看全部代码>>>

#define  MAX_TIMER            3            // 最大定时器个数
- C; S* s7 D" WEXT volatile unsigned long    g_Timer1[MAX_TIMER]; 2 e$ |$ }& O7 l( Y4 ~* v5 T# i/ j
#define  LedTimer             g_Timer1[0]  // LED翻转定时器
6 v, L; B, v/ U  `. W5 ?% p#define  GetTemperatureTimer  g_Timer1[1]  // 温度采集定时器
+ [. a9 O0 `( n2 N7 @  }#define  SendToLcdTimer       g_Timer1[2]  // 温度显示定时器
6 t6 Z; ^# s" L. L( K  m4 K2 |. T
% _" e7 {6 ^' y* N#define  TIMER1_SEC        (1)              // 秒
1 w" J% |9 H" t0 e  c0 o! V* v) Q6 H  p3 B#define  TIMER1_MIN        (TIMER1_SEC*60)  // 分
# m3 F$ f2 f3 _7 u# ]


. i/ }4 |; b9 C) e" J# K" \

在定时器初始化的时候也顺便给三个软件定时器进行初始化操作:

左右滑动查看全部代码>>>

/********************************************************************************************************# D2 Q4 D7 {6 t3 R0 Z6 g% C
** 函数: TIM1_Init, 通用定时器1初始化  l* ~/ W" l& r# y
**------------------------------------------------------------------------------------------------------+ f: t: v* J, F, e" i  y6 _
** 参数: arr:自动重装值 psc:时钟预分频数; v+ E' {  N  r: n1 q$ O
** 说明: 定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft
  r- T# T4 Y2 o- S. v& `** 返回: void 8 o8 T1 @2 h$ F& E
********************************************************************************************************/
! T1 o7 ]6 P9 \* Wvoid TIM1_Init(uint16_t arr, uint16_t psc): Y: N2 r1 g! J& ]2 m% L5 `+ Y
{
* T4 [. d& ~5 o8 t* W! s( Q    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;7 S2 R7 b+ u2 P/ ]3 \/ B
NVIC_InitTypeDef NVIC_InitStructure;" |+ n: X! P- R% T3 e1 y
: R% v. a, b9 R% v4 R
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
; T# h) x+ r: C3 ]/ |% W0 I $ I! I8 k+ A7 x  Z- C  e' {
/* 定时器TIM1初始化 */5 O8 n& {. q5 n% ?5 i) g8 Z
TIM_TimeBaseStructure.TIM_Period = arr; 0 s0 [# t& F- b
TIM_TimeBaseStructure.TIM_Prescaler =psc;
* S# Z5 E; ^' h- c# i/ ]4 d: _$ w TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; $ i9 K0 i% C! v! I2 Z5 y
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
  E& D$ ?7 n+ w- U/ I$ `4 ~8 n% o/ z TIM_TimeBaseStructure.TIM_RepetitionCounter=0;
% y- J8 \0 B) l; F8 R6 s TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
* x; h3 u, `0 X0 x4 d% }# L& O# }  TIM_ClearFlag(TIM1,TIM_FLAG_Update );
( A; n, `0 b7 h/ ]8 K
$ r. V4 U7 P) z1 T( U; l6 h1 S /* 中断使能 */3 |. }! v. W, z2 ?) E4 r/ [
TIM_ITConfig(TIM1,TIM_IT_Update, ENABLE );
5 t! V0 K- t! H. W" |% {
; S% m3 l. g" r /* 中断优先级NVIC设置 */
( r1 m9 B) a' R- O' V, x    NVIC_InitStructure.NVIC_IRQChannel =  TIM1_UP_IRQn;& i$ H- ?( |* t/ L6 l, a% q' ~/ {
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  
7 h- G, U# a8 P5 o7 K NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  0 I0 Z: l: x# w( E, M- ~
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  H' {6 y( T* V7 L NVIC_Init(&NVIC_InitStructure);  
  B  m& V5 E$ q4 x! a TIM_Cmd(TIM1, ENABLE);  
+ t2 }" r% @% i   
* w: y% M( v& T // 全局定时器初始化( p" Y" J! q7 e' H  K( r
for(int i = 0; i < MAX_TIMER; i++)
2 n- a4 b. F# a$ k6 N! Q {
2 _# @. N7 e6 [; \  g_Timer1 = 0;   9 v+ _6 V8 {2 ?1 s# I( Q
}
/ p4 ], ?0 Y" y* D5 G1 m}
  x8 K. g# ^- [3 K3 c5 B( d1 j

7 F; x: L  O) B( a  q; s

在定时器中断中对这些软件定时器进行定时值做递减操作:

左右滑动查看全部代码>>>

/********************************************************************************************************' U5 D! k* r2 i2 ]9 a
** 函数: TIM1_IRQHandler,  定时器1中断服务程序" i9 Q* K) }7 B8 n
**------------------------------------------------------------------------------------------------------; X1 d6 x' D0 L2 q) |* v
** 参数: 无8 f  Z, Z3 a% g( {
** 返回: 无 / x8 O2 C: h- r# R9 s0 V4 y
********************************************************************************************************/
0 z& \; x: O1 q6 T. a+ F+ l7 j0 V9 avoid TIM1_UP_IRQHandler(void)   //TIM1中断
2 v1 \4 U7 R0 ]' L3 B8 L- L{
2 i, C: G) c7 {/ c1 t uint8 i;! U; E) g* F# ~

0 v' D, N7 s) S5 a1 s if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)  // 检查TIM1更新中断发生与否
" c6 A2 ?1 _* k2 k' c {
+ O- ~$ s* J9 S( U  //-------------------------------------------------------------------------------, p, r1 g8 e6 ?- B
  // 各种定时间器计时4 B/ x. ^" w/ R. h: u$ k: G
  for (i = 0; i < MAX_TIMER; i++)     // 定时时间递减     
# X7 Z) {$ K+ V3 ^   if( g_Timer1 ) g_Timer1-- ;0 h, P0 G1 E' w5 V2 k2 U4 v
  TIM_ClearITPendingBit(TIM1, TIM_IT_Update);     //清除TIMx更新中断标志
! S# u3 {& N9 j1 ^/ M$ v! I }: Y, V" P$ K2 N  a2 i! j, n2 J
} $ Z; `* }/ P8 `4 y8 I' n+ ?* z


5 R# b5 Q1 o0 d

我们在各个定时任务中给这些软件定时器赋予定时值,这些定时值递减到0则该任务会被触发执行,比如:

左右滑动查看全部代码>>>

void Task_Led(void)
4 E( U7 X; f' T- a8 h# U, u{
# N* c. m8 S: O //----------------------------------------------------------------; u3 q5 q* h( w1 M" d5 m
// 等待定时时间9 O( c$ m  |% V: \
if(LedTimer) return;! Y7 {# _( p3 P% \& m* r
LedTimer = 1 * TIMER1_SEC;  a( {& \3 H2 f% N
//----------------------------------------------------------------
1 p0 J& p1 I( e+ ?4 x5 K8 }& X // LED任务主体# \# S. [8 h' I/ r  F
LedToggle();
4 L* Q' P& a2 M  X}0 s( \* x7 [6 @! C

7 ]/ A# a4 [7 j* _5 Z; o! pvoid Task_GetTemperature(void), |+ h4 B! R4 a1 ]  H& u
{
# r8 |' b: @; B1 o //----------------------------------------------------------------
' l; X- {! x( |* h6 m: V+ [ // 等待定时时间
7 i# @* P! c+ O* i7 [5 I- P if(LedTimer) return;) q' _2 W7 B7 B- y. K( S& `: ?
LedTimer = 2 * TIMER1_SEC;& Q, p- m6 H/ v" i4 n
//----------------------------------------------------------------
+ g5 g% t# n9 D2 | // 温度采集任务主体# e2 P) a; X/ _2 J2 l
GetTemperature();
! s8 A, M+ C. E" o( U}* W7 Z5 _& d0 Y' O+ l- ~
4 D7 u# ]+ k; a% ~5 a
void Task_SendToLcd(void)0 i" Z( P6 r& b* N" v$ F
{
; q, e; m# I  |5 H8 Y //----------------------------------------------------------------
. Y; r3 y" d9 J( H6 Q5 c( F // 等待定时时间
+ u4 Q, ?' x& I" X# @ if(LedTimer) return;
2 R' E; p4 `' G6 ~ LedTimer = 2 * TIMER1_SEC;
; T6 }. {0 W; ^& D# M //----------------------------------------------------------------  {/ Y- }- ^7 w6 h
// 温度显示任务主体
+ [: v" f$ b+ ^( i6 D LcdDisplay();
& F9 w! [* A& F}7 g0 U3 F) }! f  g) F


/ v/ c; K+ I' R! `3 s0 Z1 N  f# {

如此一来,每过1、2、4秒则分别触发LED翻转任务、温度采集任务、温度显示任务。

这里配置的最小定时单位为1秒,当然根据实际需要进行配置(定时器初始化),定时器初始化可以放在系统统一初始化函数里:

左右滑动查看全部代码>>>

/********************************************************************************************************6 _; b8 u9 n& \& R3 U9 O* R2 U' q
** 函数: SysInit, 系统上电初始化
7 e" t1 q' P( D. u% v! A**------------------------------------------------------------------------------------------------------  G& ~2 |1 M& @. c( Q6 T" Z
** 参数: " G& Y+ O6 d# ]
** 说明: 0 k+ i( R! I/ B+ \- i
** 返回: % U+ J$ e3 s. C( [; V. G: W. m5 H
********************************************************************************************************/
, w/ c+ H0 _" ~- c8 i: E) L- svoid SysInit(void)
( a! U  F; ?5 F" }7 s9 }{9 r. t7 g9 y+ `3 r! {5 P& k1 f# H9 s
CpuInit();                  // 配置系统信息函数
: o9 t! t, @3 Q3 t$ T4 ~% W SysTickInit();              // 系统滴答定时器初始化函数
' U# w' r% @) C% E3 [$ f' n UsartInit(115200);          // 串口初始化函数,波特率1152005 g! g/ T& @; R! p
TIM1_Init(2000-1, 36000-1); // 定时周期1s1 Y7 N4 [, f5 ]" Q1 r8 W
LedInit();                  // Led初始化$ T/ T! s) T, B1 [: T- `( }
TemperatureInit();          // 温度传感器初始化- \" K* M- P5 Z1 r7 D
LcdInit();                  // LCD初始化8 a" X, c' W& R3 N4 e# l  T
}
9 n, W( k4 h) _. t9 X$ o# c/ a5 c* E


8 X# i* w8 d) q# O; G- m7 Q  M

此时我们的main函数就可以设计为:

int main(void)' Y  L$ D$ r+ C- e# K5 ~+ p; X
{
8 Z2 L0 B* i  R2 |% ^9 }2 K( P7 s# { //-----------------------------------------------------------------------------------------------
$ c8 y) Y) `2 x' F: \! x // 上电初始化函数3 Y# t( z+ [& Q. M! c; G
SysInit(); , x4 t$ [7 Z2 }5 f; M

; u" u6 E4 l% o1 a3 ?) W //-----------------------------------------------------------------------------------------------
& ^7 a* G9 p7 I // 主程序. z) }5 e0 _7 i
while (1)
( o- o4 A* _8 B0 T( u {
6 S$ Y2 `9 ?* v& M! a' H( d  //----------------------------------------------------------------------------------------------- $ |4 y7 H$ J! |8 H  [& r
  // 定时任务, e& o; o8 \5 T2 p' n7 e  w5 z
  Task_Led();
! J7 d- a0 t  [  Task_GetTemperature();
; R) p/ S; |7 D  w  Task_SendToLcd();) e9 V+ e6 B) [- e1 H; ^0 L
}- {  o' p% Y- T
}( H- a% Y+ A7 T- N

主函数主要是进行系统上电的一些初始化操作,接着是调用各定时任务函数。

本demo使用定时器1来扩展出3个软件定时器,如果TIM资源不够用,可以换用系统滴答定时器来做。如:

微信图片_20200826230432.png

其中,时间基数可以根据实际需要进行调整。

2、实践(代入法)

套用以上模板,分享我的一个实例:

微信图片_20200826230436.png
微信图片_20200826230440.png
微信图片_20200826230443.png
微信图片_20200826230447.png
微信图片_20200826230450.png
微信图片_20200826230454.png
微信图片_20200826230457.png
微信图片_20200826230501.png
微信图片_20200826230503.png

( y# m" o% \1 G# v

需要思考及注意的问题是给每个任务的定时值设置多大合适?这也是一些朋友有疑问的,这只能是自己对自己的任务做考虑,具体情况具体分析,给经验值、调试调整。

就如同常常有人问定义多大的数组合适?在使用RTOS时每个线程的线程栈大小设置多大合适、优先级设置为多少合适?这些都是需要我们自己进行思考的。

有模板/轮子套用是好事,但有些问题不能单单依靠模板,否则有可能把自己给套进去。

以上是以STM32为例的,其它单片机也是可以用这样子的思想的,包括51单片机。

面对文首提到的面试问题,若是可以提到使用软件定时器来处理,进一步能清楚地表达出来,再进一步能写出一些伪代码,那这场面试多半是稳了。

不仅仅是为了面试,本文的方法是很经典的,小编曾经接触的产品项目中就有用到,很实用,值得学习掌握。方法掌握多了,实际应用的时候想用屠龙刀还是倚天剑根据实际情况选择使用即可。

以上就是本次的分享,如有错误,欢迎指出,谢谢。

4 j. s, m! P, ~6 ~$ B+ a. [& v- @; `( g
赞 收藏 评论1 发布时间:2020-8-26 23:07

举报

1个回答
jeffhe1 回答时间:2020-8-27 09:02:01
假如你已經使用 RTOS (答案2) , 那一切不就更有效率 ,  也比自己用 第3方法 timer管理 , 更好一 些
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版