单片机程序编写常使用的程序架构

0、前言

  对于做嵌入式开发,或者单片机开发的工程师来说,编写程序肯定是家常便饭,对于程序,大家肯定都不陌生。但是无论你是刚毕业的应届生,还是久经职场的工程师,在编写程序时,考虑程序架构是非常有必要的事情。但是往往实际开发中,真正考虑好的程序架构的恐怕并不多。随着程序开发的难度以及复杂度的增加,没有一个好的程序架构,往往会使开发者在编写程序的过程中产生逻辑混乱。
  当然,我也是一个普通的软件工程师,前人已经给我们总结出了优秀的程序架构,我们只要理解它们,并运用到自己的开发项目中,用着用着你就知道程序架构带来的好处了。目前,单片机程序的架构大致分为三种:

1、顺序执行法:这类写法是大多数人使用的方法,根据产品的工作逻辑,从上往下的执行顺序来编写应用程序即可,不需要考虑程序的具体架构。

2、时间片轮询法:这种方法是介于顺序执行法和操作系统之间的一种方法。本文也是主要对该方法的总结与讨论。

3、操作系统:这种方法是程序编写的最高境界,即单片机中嵌入实时操作系统,然后再操作系统的基础上进行编程。但是,上操作系统的话对单片机的硬件要求比较高,一般普通的或者8位的单片机还是不足以上操作系统的。

1、顺序执行法

  在应用程序比较简单,实时性,并行性要求不太高的情况下,使用顺序执行法进行编程是不错的方法,程序设计简单,思路也比较清晰。但是,当应用程序比较复杂的时候,如果没有一个完整的流程图,编写应用程序的工程师在写逻辑的时候会混乱。不利于在程序功能增加是进行代码维护和优化。特别是当多个任务需要用到定时器且定时时间不一样是,就需要增加标记变量和定时变量来加以区分,最终虽然能够实现功能,但是可能逻辑思维并不是很清晰,一直处于混乱状态。
  大多数人采用这种方法的原因是,一开始接触学习单片机的时候就一直以这种方式在写程序。而且在开发项目的过程中,从来没有去考虑过程序结构的问题,对于我们这边没有学过数据结构,程序架构的人单片机工程师来说,在程序设计上基本为零。

顺序结构如下面的程序模型:

/************************************************
*函数名称:main
*函数功能:主函数
*输入参数:void
*输出返回:void
*************************************************/
void main(void) 
{ 
    uint8 keyValue;
    InitSys();                  // 初始化
    while (1)
    {
        TaskDisplayClock();
        keyValue = TaskKeySan();
        switch (keyValue)
       {
            case x: TaskDispStatus(); break;
            ...
            default: break;
        }
    }
}

2、时间片轮询法

  时间片轮询法,很多时候都是与操作系统一起出现的,因为很多操作系统中就使用了这种方法。许多刚入门的工程师当然还没有机会学习和掌握多任务处理的操作系统开发模式。但是,在使用和学习多任务操作系统开发之前,我们依然需要一种开发模式,它能够在某种程度上和多任务与的操作系统类型,能够实现某种意义上的时间片轮询执行的模式。
  当然,这种程序设计模式很多工程师也许都已经在使用了,使用起来比我还要好,还要高明。但是,很多搞那个出来的学生或者还没有出来的学习就没有这个机会看到这种处理模式了。其实,时间片轮询法,就是对定时器的应用,一个定时器多出复用,定时器产生一个1ms,或者10ms,或者100ms的定时周期,给每个需要执行的任务设置好执行任务的周期,直白点就是多久执行一次任务1,多久执行一次任务2,然后以定时器的周期进行计时,一旦时间到了就执行相应的任务。
**注意:**定时器初始化时的定时中断时间不可以过短也不可以过长,时间太短,中断过于频繁效率低,中断太长,实时性就差。

/**
  ******************************************************************************
  * @文件名 : Task.h
  * @作  者 : JayYang
  * @版  本 : V1.0.0
  * @日  期 : 2021年04月22日
  * @摘  要 : 任务执行头文件
  ******************************************************************************/
#ifndef __TASK_H__
#define __TASK_H__
  
/*------------------------------- 包含的头文件 -------------------------------*/


/*---------------------------------- 宏定义 ----------------------------------*/
#define TASK_MAX    3       //定义会使用定时器定时的任务数方式一

#define NotRunning  0       //初始运行状态标志为未运行

//每个任务运行的间隔时间,目前定时器是10ms中断一次
#define TaskTimer1 10 
#define TaskTimer2 20
#define TaskTimer3 30


/*--------------------------------- 类型定义 ---------------------------------*/
/*
typedef enum{
    TASK_1,
    TASK_2,
    TASK_3,
    // 这里添加你的任务.....
    TASK_MAX       // 总的可供分配的定时任务数目,定义会使用定时器定时的任务数方式二
}Task_List_e;      //这里定义这个任务列表的目的其实就是参数TASK_MAX的值,其他值是没有具体的意义的,只是为了清晰的表面任务的关系而已
*/

typedef struct{
    unsigned char RunningStatus;    //运行状态标志位:0-不运行;1-运行
    unsigned char TimerCountDown;   //定时器计数变量,进行--计数
    unsigned char TimerInitialVal;  //定时器定时初值,也就是每个任务运行的间隔时间
    void (*TaskHandler)(void);      //任务处理程序函数指针
}TaskManage_t;                      //定义任务管理结构体,它包含了一个任务所需要的所有信息

typedef struct{
    void (*TaskMarks)(void);        //任务定时标志位处理函数,[定时器中断函数中调用]
    void (*TaskProcess)(void);      //任务处理函数,[主循环中调用]
}Task_t;                            //定义任务结构体

/*--------------------------------- 变量定义 ---------------------------------*/
extern Task_t Task;     

/*--------------------------------- 函数声明 ---------------------------------*/

#endif


/**
  ******************************************************************************
  * @文件名 : Task.c
  * @作  者 : JayYang
  * @版  本 : V1.0.0
  * @日  期 : 2021年04月22日
  * @摘  要 : 任务执行源文件
  ******************************************************************************/
  
/*------------------------------- 包含的头文件 -------------------------------*/
#include "bsp.h"

/*---------------------------------- 宏定义 ----------------------------------*/

/*--------------------------------- 变量定义 ---------------------------------*/

/*--------------------------------- 函数声明 ---------------------------------*/
static void TaskMarks(void);        //任务定时标志位处理函数
static void TaskProcess(void); //任务处理函数

static void Task1(void);            //任务1
static void Task2(void);            //任务2
static void Task3(void);            //任务3

TaskManage_t TaskManage[] = {
    {NotRunning,TaskTimer1,TaskTimer1,Task1},
    {NotRunning,TaskTimer2,TaskTimer2,Task2},
    {NotRunning,TaskTimer3,TaskTimer3,Task3}
};

Task_t Task = {TaskMarks,TaskProcess};


/*--------------------------------- 函数定义 ---------------------------------*/
/************************************************
*函数名称:TaskMarks
*函数功能:任务定时标志位处理函数
*          这个函数是用来更新每个任务的计数器以及它们的运行标志位的,
*          它是写在单片机的定时器中断服务函数中的
*输入参数:void
*输出返回:void
*************************************************/
static void TaskMarks(void)
{
    unsigned char i;

    //逐个任务进行时间计算
    for (i = 0; i < TASK_MAX; i++)
    {
        //如果计数器不为0,即要执行任务的时间还没到
        if (TaskManage[i].TimerCountDown)
        {
            TaskManage[i].TimerCountDown--; //任务执行时间--
            //如果时间到了
            if (TaskManage[i].TimerCountDown==0)
            {
                TaskManage[i].TimerCountDown = TaskManage[i].TimerInitialVal;   //恢复计时器值,重新下一次计时
                TaskManage[i].RunningStatus = 1;    //标识任务可以运行
            }
        }
    }
}

/************************************************
*函数名称:TaskProcess
*函数功能:任务处理函数
*          这个函数用来判断任务是否到了执行时间,它会逐个检查任务列表中的任务
*          如果到了就执行,同时清空执行标志位,没到就不执行
*输入参数:void
*输出返回:void
*************************************************/
static void TaskProcess(void)
{
    unsigned char i;

    for (i = 0; i < TASK_MAX; i++)
    {
        if (TaskManage[i].RunningStatus)        //如果任务需要运行
        {
            TaskManage[i].TaskHandler();        //运行任务
            TaskManage[i].RunningStatus = 0;    //运行标志清0
        }
    }
}

/************************************************
*函数名称:Task1
*函数功能:任务1函数
*输入参数:void
*输出返回:void
*************************************************/
static void Task1(void)
{
    
}

/************************************************
*函数名称:Task2
*函数功能:任务2函数
*输入参数:void
*输出返回:void
*************************************************/
static void Task2(void)
{
    
}

/************************************************
*函数名称:Task3
*函数功能:任务3函数
*输入参数:void
*输出返回:void
*************************************************/
static void Task3(void)
{
    
}

3、操作系统

  操作系统是一个比较复杂的东西,任务管理,执行本身并不需要我们去了解。但是光是移植就比较麻烦,虽然有人说过 “你如果使用过系统,将不会再去使用前后台程序”。但是真正能使用操作系统的人并不多,不仅是因为系统的使用本身很复杂,而且还需要购买许可证(ucos也不例外,如果商用的话)。
  这里本人并不想过多的介绍操作系统本身,因为不是一两句话能过说明白的,下面列出UCOS下编写应该程序的模型。大家可以对比一下,这三种方式下的各自的优缺点。

/**************************************************************************************
* FunctionName   : main()
* Description    : 主函数
* EntryParameter : None
* ReturnValue    : None
**************************************************************************************/
int main(void) 
{ 
    OSInit();                // 初始化uCOS-II

    OSTaskCreate((void (*) (void *)) TaskStart,        // 任务指针
                (void   *) 0,            // 参数
                (OS_STK *) &TaskStartStk[TASK_START_STK_SIZE - 1], // 堆栈指针
                (INT8U   ) TASK_START_PRIO);        // 任务优先级

    OSStart();                                       // 启动多任务环境
                                        
    return (0); 
}

/**************************************************************************************
* FunctionName   : TaskStart()          
* Description    : 任务创建,只创建任务,不完成其他工作
* EntryParameter : None
* ReturnValue    : None
**************************************************************************************/
void TaskStart(void* p_arg)
{
    OS_CPU_SysTickInit();                                       // Initialize the SysTick.

#if (OS_TASK_STAT_EN > 0)
    OSStatInit();                                               // 这东西可以测量CPU使用量 
#endif

 OSTaskCreate((void (*) (void *)) TaskLed,     // 任务1
                (void   *) 0,               // 不带参数
                (OS_STK *) &TaskLedStk[TASK_LED_STK_SIZE - 1],  // 堆栈指针
                (INT8U   ) TASK_LED_PRIO);         // 优先级

 // Here the task of creating your
                
    while (1)
    {
        OSTimeDlyHMSM(0, 0, 0, 100);
    }
}

不难看出,时间片轮询法优势还是比较大的,即由顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐