历史上的今天
返回首页

历史上的今天

今天是:2025年08月12日(星期二)

正在发生

2021年08月12日 | STM32 | 分享一个简单易用的单片机裸机程序模板

2021-08-12 来源:eefocus

前言

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


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


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


答案一:轮询系统

代码结构如:


// 微信公众号:嵌入式大杂烩

int main(void)

{

 init_something();

 

 while(1)

 {

  do_something1();

        do_something2();

        do_something3();

 }

}


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


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


答案二:前后台系统

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


// 微信公众号:嵌入式大杂烩

int flag1 = 0;

int flag2 = 0;

int flag3 = 0;


int main(void)

{

 /* 硬件相关初始化 */

 HardWareInit();


 /* 无限循环 */

 for (;;) {

   if (flag1) {

     /* 处理事情 1 */

     DoSomething1();

   }


   if (flag2) {

     /* 处理事情 2 */

     DoSomethingg2();

   }


   if (flag3) {

     /* 处理事情 3 */

     DoSomethingg3();

   }

 }

}


void ISR1(void)

{

 /* 置位标志位 */

 flag1 = 1;

 /* 如果事件处理时间很短,则在中断里面处理

 如果事件处理时间比较长,在回到后台处理 */

 DoSomething1();

}


void ISR2(void)

{

 /* 置位标志位 */

 flag2 = 2;


 /* 如果事件处理时间很短,则在中断里面处理

 如果事件处理时间比较长,在回到后台处理 */

 DoSomething2();

}


void ISR3(void)

{

 /* 置位标志位 */

 flag3 = 1;

 /* 如果事件处理时间很短,则在中断里面处理

 如果事件处理时间比较长,在回到后台处理 */

 DoSomething3();

}


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


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


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

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


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


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


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


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


站在巨人的肩膀上

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


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


1、MultiTimer 简介

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


2、MultiTimer 的demo

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


#include "multi_timer.h"


struct Timer timer1;

struct Timer timer2;


void timer1_callback()

{

    printf("timer1 timeout!rn");

}


void timer2_callback()

{

    printf("timer2 timeout!rn");

}


int main()

{

    timer_init(&timer1, timer1_callback, 1000, 1000); //1s loop

    timer_start(&timer1);

    

    timer_init(&timer2, timer2_callback, 50, 0); //50ms delay

    timer_start(&timer2);

    

    while(1) {

        

        timer_loop();

    }

}


void HAL_SYSTICK_Callback(void)

{

    timer_ticks(); //1ms ticks

}


3、MultiTimer 的移植、剖析

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


第6期 | MultiTimer,一款可无限扩展的软件定时器


自己动手,丰衣足食

1、代码模板

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


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


#define  MAX_TIMER            3            // 最大定时器个数

EXT volatile unsigned long    g_Timer1[MAX_TIMER]; 

#define  LedTimer             g_Timer1[0]  // LED翻转定时器

#define  GetTemperatureTimer  g_Timer1[1]  // 温度采集定时器

#define  SendToLcdTimer       g_Timer1[2]  // 温度显示定时器


#define  TIMER1_SEC        (1)              // 秒

#define  TIMER1_MIN        (TIMER1_SEC*60)  // 分


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


/********************************************************************************************************

** 函数: TIM1_Init, 通用定时器1初始化

**------------------------------------------------------------------------------------------------------

** 参数: arr:自动重装值 psc:时钟预分频数

** 说明: 定时器溢出时间计算方法:Tout=((arr+1)*(psc+1))/Ft

** 返回: void 

********************************************************************************************************/

void TIM1_Init(uint16_t arr, uint16_t psc)

{

    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;

 NVIC_InitTypeDef NVIC_InitStructure;

 

 RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); 

 

 /* 定时器TIM1初始化 */

 TIM_TimeBaseStructure.TIM_Period = arr; 

 TIM_TimeBaseStructure.TIM_Prescaler =psc; 

 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; 

 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  

 TIM_TimeBaseStructure.TIM_RepetitionCounter=0;

 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); 

  TIM_ClearFlag(TIM1,TIM_FLAG_Update );

 

 /* 中断使能 */

 TIM_ITConfig(TIM1,TIM_IT_Update, ENABLE ); 

 

 /* 中断优先级NVIC设置 */

    NVIC_InitStructure.NVIC_IRQChannel =  TIM1_UP_IRQn;

 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  

 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  

 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

 NVIC_Init(&NVIC_InitStructure);  

 TIM_Cmd(TIM1, ENABLE);  

    

 // 全局定时器初始化

 for(int i = 0; i < MAX_TIMER; i++)

 {

  g_Timer1[i] = 0;   

 }

}


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


/********************************************************************************************************

** 函数: TIM1_IRQHandler,  定时器1中断服务程序

**------------------------------------------------------------------------------------------------------

** 参数: 无

** 返回: 无 

********************************************************************************************************/

void TIM1_UP_IRQHandler(void)   //TIM1中断

{

 uint8 i;

 

 if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)  // 检查TIM1更新中断发生与否

 {

  //-------------------------------------------------------------------------------

  // 各种定时间器计时

  for (i = 0; i < MAX_TIMER; i++)     // 定时时间递减     

   if( g_Timer1[i] ) g_Timer1[i]-- ;

  TIM_ClearITPendingBit(TIM1, TIM_IT_Update);     //清除TIMx更新中断标志 

 }


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


// 微信公众号:嵌入式大杂烩

void Task_Led(void)

{

 //----------------------------------------------------------------

 // 等待定时时间

 if(LedTimer) return;

 LedTimer = 1 * TIMER1_SEC;

 //----------------------------------------------------------------

 // LED任务主体

 LedToggle();

}


void Task_GetTemperature(void)

{

 //----------------------------------------------------------------

 // 等待定时时间

 if(GetTemperatureTimer) return;

 GetTemperatureTimer = 2 * TIMER1_SEC;

 //----------------------------------------------------------------

 // 温度采集任务主体

 GetTemperature();

}


void Task_SendToLcd(void)

{

 //----------------------------------------------------------------

 // 等待定时时间

 if(SendToLcdTimer) return;

 SendToLcdTimer = 2 * TIMER1_SEC;

 //----------------------------------------------------------------

 // 温度显示任务主体

 LcdDisplay();

}


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


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


/********************************************************************************************************

** 函数: SysInit, 系统上电初始化

**------------------------------------------------------------------------------------------------------

** 参数: 

** 说明: 

** 返回: 

********************************************************************************************************/

void SysInit(void)

{

 CpuInit();                  // 配置系统信息函数

 SysTickInit();              // 系统滴答定时器初始化函数

 UsartInit(115200);          // 串口初始化函数,波特率115200

 TIM1_Init(2000-1, 36000-1); // 定时周期1s

 LedInit();                  // Led初始化

 TemperatureInit();          // 温度传感器初始化

 LcdInit();                  // LCD初始化

}


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


int main(void)

{

 //----------------------------------------------------------------------------------------------- 

 // 上电初始化函数

 SysInit(); 

 

 //----------------------------------------------------------------------------------------------- 

 // 主程序

 while (1)

 {

  //----------------------------------------------------------------------------------------------- 

  // 定时任务

  Task_Led();

  Task_GetTemperature(); 

  Task_SendToLcd();

 }

}


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


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

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

2、实践(代入法)

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

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


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


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

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


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


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

推荐阅读

史海拾趣

Display Elektronik GmbH公司的发展小趣事

Display Elektronik GmbH始终将产品质量放在首位。公司建立了严格的质量控制体系,从原材料采购到生产流程,再到成品检测,每一个环节都严格把关。这种对品质的坚持让Display Elektronik GmbH的产品在市场上赢得了良好的口碑,也为其赢得了众多知名客户的信赖。

BSI公司的发展小趣事

近年来,随着环保意识的提高,电子行业对绿色发展的需求也日益迫切。BSI积极响应这一需求,推动了电子行业绿色标准的制定和实施。通过制定严格的环保标准,BSI引导电子产品制造企业采用更加环保的生产工艺和材料,减少了对环境的污染。同时,BSI还为企业提供了碳排放核查等服务,帮助企业了解自身的碳排放情况,制定了减排措施,推动了电子行业的绿色发展。

Davies Molding公司的发展小趣事

Davies Molding公司在追求经济效益的同时,始终关注社会责任和可持续发展。公司积极参与环保公益活动,推动绿色生产,降低对环境的影响。此外,Davies Molding公司还注重员工福利和社会公益事业,为员工提供良好的工作环境和发展空间,为社会做出积极贡献。这些举措不仅提升了公司的社会形象,也为公司的长期发展奠定了坚实基础。

Esterline Technologies Corporation公司的发展小趣事

Davies Molding公司在发展过程中,始终注重市场布局与拓展。公司不仅在美国中西部建立了稳固的市场地位,还积极开拓国际市场,将产品出口到全球多个国家和地区。通过与全球客户的合作,Davies Molding公司不仅提升了品牌影响力,还获得了更多的发展机遇。

ARCOTRONICS公司的发展小趣事

在电子行业的早期,ARCOTRONICS公司凭借其卓越的研发团队,成功开发出一种新型的高效能电子元件。这一技术突破不仅大幅提升了电子设备的性能,还降低了生产成本,使公司在市场上迅速获得了竞争优势。这一技术突破为ARCOTRONICS公司奠定了坚实的基石,为其后续发展打下了坚实的基础。

Dynex公司的发展小趣事

随着技术实力的不断提升,ARCOTRONICS公司开始将目光投向国际市场。公司制定了一系列国际化战略,积极参与国际电子展会,与全球各地的合作伙伴建立紧密的合作关系。通过不断拓展市场,ARCOTRONICS公司的产品逐渐在国际上获得了广泛的认可,公司也因此成为了全球电子行业的重要参与者。

问答坊 | AI 解惑

行业精英策论PCB设计关键技术

基于Ansoft电磁技术的新一代PCB仿真设计     针对PCB设计人员关注的问题予以讨论,剖析PCB电磁问题的实质及处理方法,介绍Ansoft仿真解决方案的技术特点,与同类型产品的定位关系及比较,并为您展示Ansoft电磁技术及工具在国内 ...…

查看全部问答>

兄弟们,2010年6月23日,中国足球机器人首获世界杯冠军了!

博创足球机器人助中国首获RoboCup2010中型组世界冠军                                ...…

查看全部问答>

[===============]WINCE arm开发板 上电后 全屏 自动运行 指定程序

第一次 接触  wince 开发板 首先遇到的问题是  要让用vs2003写的程序 在 设备 一上电 就 全屏 自动运行 这个问题 就是不想让用户 看到 wince 系统界面上的程序 请各位大侠 指教 …

查看全部问答>

无聊,大家一起休闲下~~

http://www.hougong.info/?fromuid=92961…

查看全部问答>

播放器在wince5.0中,无法准确的定位播放位置

先详细解释一下标题的意思。    我的播放器运行在wince5.0中,我编写了各种各样的播放器,有使用WMP的,有使用TCPMP的,有使用用DSHOW的,都会有这么个情况:    我要跳转到视频的某个位置,比如说10s,不能准确的设置到10s,而 ...…

查看全部问答>

买STN32开发板的遭遇

                                 打电话到北京英蓓特办事处问有无STN32开发板,有,兴冲冲找了一个多小时,才在边远地区找到,拿出开发板,但无ulin ...…

查看全部问答>

EXTI的中断问题

                                 stm32例子中的EXTI中断程序的开始都有一个功能是查哪个管脚发生的,以EXTI15_10_IRQHandler为例,在程序开始的时候 ...…

查看全部问答>

TI 集成型 USB 充电端口的电源开关

    TI最新的电源管理器件TPS2540 支持全球各种 USB 充电模式,包括即将推出的 Battery Charging 1.2 草案规范、中国及欧洲法规要求以及领先电子 OEM 厂商的专利充电模式等。该 IC 是各种主机应用 USB 端口的理想选择,非常适用于 AC 墙 ...…

查看全部问答>

可以先体验一下micro Python (进http://micropython.org/live/)

进 http://micropython.org/live/ 体验micro python编程蛮好玩的 …

查看全部问答>