工程师实战:单片机裸机程序框架是怎样炼成的

发布者:SereneMelody最新更新时间:2021-06-30 来源: 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(LedTimer) return;
 LedTimer = 2 * TIMER1_SEC;
 //----------------------------------------------------------------
 // 温度采集任务主体
 GetTemperature();
}

void Task_SendToLcd(void)
{
 //----------------------------------------------------------------
 // 等待定时时间
 if(LedTimer) return;
 LedTimer = 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初始化
}


[1] [2]
关键字:单片机  裸机程序  框架 引用地址:工程师实战:单片机裸机程序框架是怎样炼成的

上一篇:单片机指令中如何区分是字节传送和位传送
下一篇:单片机I/O控制方式(UART中断和DMA中断的区别)

推荐阅读最新更新时间:2024-11-12 13:21

51单片机程序设计中的地址指针及其应用
CS-51单片机外部RAM的地址空间为64K,地址总线为16位,访问外接RAM可执行如下4条指令: MOVX A,@DPTR MOVX @DPTR,A MOVX A,@RI MOVX @RI,A 其中DPTR为16位地址寄存器,地址高8位存于DPH,地址低8位存于DPL;Ri(I=0,1)是8位寄存器,作为地址指针时仅存低8位地址。 MCS-51执行上述指令时分为两个阶段:首先,是从外接程序存储器中取出指令代码,并进行分析。然后,执行对外接RAM的数据读/写操作。在这两个阶段,P0口、P1口上的地址选通是有区别的。 执行“MOVX A,@DPTR”和“MOVX @DPTR,A”指令时,在读指令代码阶段,由程序计数器(PC)提供A0
[单片机]
单片机学习的四个阶段 
  第一阶段   是先浏览教科书里的硬件部分,大至了解单片机的硬件结构。如   ROM、RAM、地址、I/O口等,以及看一些厂家的MCU资料(Data Sheet),来加强MCU所提供各项资源的印象。   第二阶段   就是了解二进位数字、十六进位数和软件方面的内容。尽管有很多   高级语言可用于单片机的编程,但我觉得初学还是以汇编语言为好,更有利于和硬件结合,掌握硬件结构。知道汇编语言、机器语言、 指令、 程序等概念后,就从MOV指令开始,学习汇编语言和编程,在此如51的MCU汇编语言系统有   111条指令,简单又好理解它们怎 样和硬件联系,更有助于一般学习单片机的指令整合与运用.因此其方法可先了 解几条基本的MOV指令和它
[单片机]
单片机学习之一:单片机概述
在学习单片机之前,我们先把一些相关的简单概念给大家作一个入门性的介绍帮助同学们建立一个初步的概念。 一、什么是单片机 单片机(MCU-Micro Controller Unit),它是把组成微型计算机的各个功能部件:中央处理器(CPU)、随机存取存储器(RAM)、只读存储器(ROM或者EPROM)、I/O接口电路、定时器和计数器以及串行通讯接口等部件制作在一块集成的芯片中,构成一个完整的微型计算机。 CPU可以分为运算器,控制器和寄存器3个部分,其是单片机的核心。 存储器可以分为两类,ROM和RAM。RAM可以被CPU随机读写,断电后存储的内容消失;ROM中的数据只能被读取,一般用于存放固定的程序。
[单片机]
51单片机实验8:led点阵(1):点亮一个点
开发板led点阵模块电路图如下: 74HC595:74HC595是一个8位串行输入、并行输出的位移缓存器。芯片第11角为数据输入时钟线,上升沿有效。芯片第12脚为输出存储器锁存时钟线,上升沿有效。芯片第13脚为输出有效(低电平)。芯片第14脚为串行数据输入。 为表示出输入74HC595的8位二进制数,开发板加入了led模块(图一中绿色所示)。若要使led发光,则需将JP595接vcc。 OE为输出有效控制端,低电平有效,所以务必将JOE短接片短接到GND端。 _nop_();函数为延时一个机器周期,所对应头文件为intrins.h #include reg52.h #include intrins.h #
[单片机]
51<font color='red'>单片机</font>实验8:led点阵(1):点亮一个点
电动车锂电池组保护电路的单片机设计方案
  随着电动车普及,锂电池也成为众人关心的焦点。锂电池与镍镉、镍氢电池不太一样,因其能量密度高,对充放电要求很高。当过充、过放、过流及短路保护等情况发生时,锂电池内的压力与热量大量增加,容易产生爆炸,因此通常都会在电池包内加保护电路,用以提高锂电池的使用寿命。 针对目前电动车锂电池组所用的保护电路大多都由分立原件构成,存在控制精度不够高、技术指标低、不能有效保护锂电池组等特点,本文中提出一种基于单片机的电动车36V锂电池组(由10节3. 6 V锂电池串联而成)保护电路设计方案,利用高性能、低功耗的ATmega16L 单片机作为检测和控制核心,用由MC34063构成的DC /DC变换控制电路为整个保护电路提供稳压电源,辅以LM60 测
[单片机]
电动车锂电池组保护电路的<font color='red'>单片机</font>设计方案
PIC单片机读/写AT24C系列存储器原理
  AT24C系列在增强型PIC实验板上编程的硬件原理图如下图所示,U7为实验板上24C02芯片,SDA与单片机的RB5口相连,SCL与单片机RB4相连,七段数码管D5、D7、D8组成了显示单元,字形码的数据通过RC口送入,各数码管的显示片选信号分别不同的RA口进行控制。   在MPLab IDE软件中新建工程,加入源程序代码,同时进行芯片型号的选择和配置位的设置,我们实验所用的芯片型号为PIC16F877A。   编写的程序代码如下,其中程序流程图如下图所示。   软件代码   编好程序后将编译好的HEX码通过ICD2仿真烧写器烧入单片机芯片,上电运行,主程序中在O×01地址写入了“O×55”,在O×0
[单片机]
PIC<font color='red'>单片机</font>读/写AT24C系列存储器原理
基于51单片机的电子记分牌C程序编程
#include reg52.h sbit r1=P2^0; sbit r2=P2^1; sbit h1=P3^0; sbit h2=P3^1; void delay(unsigned char x); char scank(); void display(char); unsigned char dispcode ={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0XD8,0x80,0x90}; void main() { char num=0,keynum; while(1) { keynum=scank(); num=num+keynum; if(num 0) n
[单片机]
基于51<font color='red'>单片机</font>的电子记分牌C<font color='red'>程序</font>编程
GD32单片机STM32远程下载手机程序升级固件下载局域网网页升级工具
GD32、STM32单片机,是我们最常见的一种MCU。通常我们在使用STM32单片机都会遇到程序在线升级下载的问题。 GD32/STM32单片机的在线下载通常需要以下几种方式完成: 1、使用ST/GD提供的串口下载工具,本地完成固件的升级下载。 2、自行完成系统BootLoader的编写,将系统程序分为BootLoader和APP两个部分,BootLoader完成固件升级。 3、使用STM32/GD固件服务器,完成固件的升级,固件服务器https://simplewifi.taobao.com/ 几种方式各有优缺点: 使用ST提供的方法进行固件升级,方法简单,不需要额外的开发。但是,只能本地完成STM32单片机的升级。
[单片机]
GD32<font color='red'>单片机</font>STM32远程下载手机<font color='red'>程序</font>升级固件下载局域网网页升级工具
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
  • 学习ARM开发(16)
    ARM有很多东西要学习,那么中断,就肯定是需要学习的东西。自从CPU引入中断以来,才真正地进入多任务系统工作,并且大大提高了工作效率。采 ...
  • 学习ARM开发(17)
    因为嵌入式系统里全部要使用中断的,那么我的S3C44B0怎么样中断流程呢?那我就需要了解整个流程了。要深入了解,最好的方法,就是去写程序 ...
  • 学习ARM开发(18)
    上一次已经了解ARM的中断处理过程,并且可以设置中断函数,那么它这样就可以工作了吗?答案是否定的。因为S3C44B0还有好几个寄存器是控制中 ...
  • 嵌入式系统调试仿真工具
    嵌入式硬件系统设计出来后就要进行调试,不管是硬件调试还是软件调试或者程序固化,都需要用到调试仿真工具。 随着处理器新品种、新 ...
  • 最近困扰在心中的一个小疑问终于解惑了~~
    最近在驱动方面一直在概念上不能很好的理解 有时候结合别人写的一点usb的例子能有点感觉,但是因为arm体系里面没有像单片机那样直接讲解引脚 ...
  • 学习ARM开发(1)
  • 学习ARM开发(2)
  • 学习ARM开发(4)
  • 学习ARM开发(6)
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved