zoukankan      html  css  js  c++  java
  • 浅谈单片机应用程序架构----本质是定时调用

    浅谈单片机应用程序架构----本质是定时调用

    发表于2014/7/30 14:52:15  311人阅读

    分类: 软件架构/分层 单片机/STM32


    2011-11-22 15:39:52|  分类: ARM|举报|字号 订阅

     
     
              对于单片机程序来说,大家都不陌生,但是真正使用架构,考虑架构的恐怕并不多,随着程序开发的不断增多,本人觉得架构是非常必要的。前不就发帖与大家一起讨论了一下《谈谈怎样架构你的单片机程序》,发现真正使用架构的并不都,而且这类书籍基本没有。

            本人经过摸索实验,并总结,大致应用程序的架构有三种:

    1. 简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。

    2. 时间片轮询法,此方法是介于顺序执行与操作系统之间的一种方法。

    3. 操作系统,此法应该是应用程序编写的最高境界。

    下面就分别谈谈这三种方法的利弊和适应范围等。。。。。。。。。。。。。

    1. 顺序执行法:

           这种方法,这应用程序比较简单,实时性,并行性要求不太高的情况下是不错的方法,程序设计简单,思路比较清晰。但是当应用程序比较复杂的时候,如果没有一个完整的流程图,恐怕别人很难看懂程序的运行状态,而且随着程序功能的增加,编写应用程序的工程师的大脑也开始混乱。即不利于升级维护,也不利于代码优化。本人写个几个比较复杂一点的应用程序,刚开始就是使用此法,最终虽然能够实现功能,但是自己的思维一直处于混乱状态。导致程序一直不能让自己满意。

           这种方法大多数人都会采用,而且我们接受的教育也基本都是使用此法。对于我们这些基本没有学习过数据结构,程序架构的单片机工程师来说,无疑很难在应用程序的设计上有一个很大的提高,也导致了不同工程师编写的应用程序很难相互利于和学习。

           本人建议,如果喜欢使用此法的网友,如果编写比较复杂的应用程序,一定要先理清头脑,设计好完整的流程图再编写程序,否则后果很严重。当然应该程序本身很简单,此法还是一个非常必须的选择。

    下面就写一个顺序执行的程序模型,方面和下面两种方法对比:

    代码:

    /**************************************************************************************
    * FunctionName   : main()
    * Description    : 主函数
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    int main(void) 

        uint8 keyValue;

        InitSys();                  // 初始化

        while (1)
        {
            TaskDisplayClock();
            keyValue = TaskKeySan();
            switch (keyValue)
           {
                case x: TaskDispStatus(); break;
                ...
                default: break;
            }
        }
    }

    2. 时间片轮询法

           时间片轮询法,在很多书籍中有提到,而且有很多时候都是与操作系统一起出现,也就是说很多时候是操作系统中使用了这一方法。不过我们这里要说的这个时间片轮询法并不是挂在操作系统下,而是在前后台程序中使用此法。也是本贴要详细说明和介绍的方法。

           对于时间片轮询法,虽然有不少书籍都有介绍,但大多说得并不系统,只是提提概念而已。下面本人将详细介绍本人模式,并参考别人的代码建立的一个时间片轮询架构程序的方法,我想将给初学者有一定的借鉴性。

           记得在前不久本人发帖《1个定时器多处复用的问题》,由于时间的问题,并没有详细说明怎样实现1个定时器多处复用。在这里我们先介绍一下定时器的复用功能。。。

    使用1个定时器,可以是任意的定时器,这里不做特殊说明,下面假设有3个任务,那么我们应该做如下工作:

    1. 初始化定时器,这里假设定时器的定时中断为1ms(当然你可以改成10ms,这个和操作系统一样,中断过于频繁效率就低,中断太长,实时性差)。

    2. 定义一个数值:

    复制内容到剪贴板
    代码:

    #define TASK_NUM   (3)                  //  这里定义的任务数为3,表示有三个任务会使用此定时器定时。

    uint16 TaskCount[TASK_NUM] ;           //  这里为三个任务定义三个变量来存放定时值

    uint8  TaskMark[TASK_NUM];             //  同样对应三个标志位,为0表示时间没到,为1表示定时时间到。

    3. 在定时器中断服务函数中添加:

    复制内容到剪贴板
    代码:
    /**************************************************************************************
    * FunctionName : TimerInterrupt()
    * Description : 定时中断服务函数
    * EntryParameter : None
    * ReturnValue : None
    **************************************************************************************/
    void TimerInterrupt(void)
    {
        uint8 i;

        for (i=0; i<TASKS_NUM; i++) 
        {
            if (TaskCount[i]) 
            {
                  TaskCount[i]--; 
                  if (TaskCount[i] == 0) 
                  {
                        TaskMark[i] = 0x01; 
                  }
            }
       }
    }

    代码解释:定时中断服务函数,在中断中逐个判断,如果定时值为0了,表示没有使用此定时器或此定时器已经完成定时,不着处理。否则定时器减一,知道为零时,相应标志位值1,表示此任务的定时值到了。

    4. 在我们的应用程序中,在需要的应用定时的地方添加如下代码,下面就以任务1为例:

    复制内容到剪贴板
    代码:

    TaskCount[0] = 20;       // 延时20ms

    TaskMark[0]  = 0x00;     // 启动此任务的定时器

    到此我们只需要在任务中判断TaskMark[0] 是否为0x01即可。其他任务添加相同,至此一个定时器的复用问题就实现了。用需要的朋友可以试试,效果不错哦。。。。。。。。。。。

    通过上面对1个定时器的复用我们可以看出,在等待一个定时的到来的同时我们可以循环判断标志位,同时也可以去执行其他函数。

    循环判断标志位:

    那么我们可以想想,如果循环判断标志位,是不是就和上面介绍的顺序执行程序是一样的呢?一个大循环,只是这个延时比普通的for循环精确一些,可以实现精确延时。

    执行其他函数:

    那么如果我们在一个函数延时的时候去执行其他函数,充分利用CPU时间,是不是和操作系统有些类似了呢?但是操作系统的任务管理和切换是非常复杂的。下面我们就将利用此方法架构一直新的应用程序。

    时间片轮询法的架构:

    1.设计一个结构体:

    复制内容到剪贴板
    代码:

    // 任务结构
    typedef struct _TASK_COMPONENTS
    {
        uint8 Run;                 // 程序运行标记:0-不运行,1运行
        uint8 Timer;              // 计时器
        uint8 ItvTime;              // 任务运行间隔时间
        void (*TaskHook)(void);    // 要运行的任务函数
    } TASK_COMPONENTS;       // 任务定义

    这个结构体的设计非常重要,一个用4个参数,注释说的非常详细,这里不在描述。

    2. 任务运行标志出来,此函数就相当于中断服务函数,需要在定时器的中断服务函数中调用此函数,这里独立出来,并于移植和理解。

    复制内容到剪贴板
    代码:

    /**************************************************************************************
    * FunctionName   : TaskRemarks()
    * Description    : 任务标志处理
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void TaskRemarks(void)
    {
        uint8 i;

        for (i=0; i<TASKS_MAX; i++)          // 逐个任务时间处理
        {
             if (TaskComps[i].Timer)          // 时间不为0
            {
                TaskComps[i].Timer--;         // 减去一个节拍
                if (TaskComps[i].Timer == 0)       // 时间减完了
                {
                     TaskComps[i].Timer = TaskComps[i].ItvTime;       // 恢复计时器值,从新下一次
                     TaskComps[i].Run = 1;           // 任务可以运行
                }
            }
       }
    }

    大家认真对比一下次函数,和上面定时复用的函数是不是一样的呢?

    3. 任务处理

    复制内容到剪贴板
    代码:

    /**************************************************************************************
    * FunctionName   : TaskProcess()
    * Description    : 任务处理
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void TaskProcess(void)
    {
        uint8 i;

        for (i=0; i<TASKS_MAX; i++)           // 逐个任务时间处理
        {
             if (TaskComps[i].Run)           // 时间不为0
            {
                 TaskComps[i].TaskHook();         // 运行任务   

    【【注意:这种方法的前提是执行的每个任务都是短小精悍的,要不然一个任务执行的时间过长,大于了其它任务设置的时间片值,那其它任务就无法保证按它预设的时间片来执行,唉,这也只是一个定时调用的利子,楼主把它抽像化了】】

                 TaskComps[i].Run = 0;          // 标志清0
            }
        }   
    }

    此函数就是判断什么时候该执行那一个任务了,实现任务的管理操作,应用者只需要在main()函数中调用此函数就可以了,并不需要去分别调用和处理任务函数。

    到此,一个时间片轮询应用程序的架构就建好了,大家看看是不是非常简单呢?此架构只需要两个函数,一个结构体,为了应用方面下面将再建立一个枚举型变量。

    下面我就就说说怎样应用吧,假设我们有三个任务:时钟显示,按键扫描,和工作状态显示。

    1. 定义一个上面定义的那种结构体变量

    复制内容到剪贴板
    代码:

    /**************************************************************************************
    * Variable definition                            
    **************************************************************************************/
    static TASK_COMPONENTS TaskComps[] = 
    {
        {0, 60, 60, TaskDisplayClock},            // 显示时钟
        {0, 20, 20, TaskKeySan},               // 按键扫描
        {0, 30, 30, TaskDispStatus},            // 显示工作状态

         // 这里添加你的任务。。。。

    };

    在定义变量时,我们已经初始化了值,这些值的初始化,非常重要,跟具体的执行时间优先级等都有关系,这个需要自己掌握。

    ①大概意思是,我们有三个任务,没1s执行以下时钟显示,因为我们的时钟最小单位是1s,所以在秒变化后才显示一次就够了。

    ②由于按键在按下时会参数抖动,而我们知道一般按键的抖动大概是20ms,那么我们在顺序执行的函数中一般是延伸20ms,而这里我们每20ms扫描一次,是非常不错的出来,即达到了消抖的目的,也不会漏掉按键输入。

    ③为了能够显示按键后的其他提示和工作界面,我们这里设计每30ms显示一次,如果你觉得反应慢了,你可以让这些值小一点。后面的名称是对应的函数名,你必须在应用程序中编写这函数名称和这三个一样的任务。

    2. 任务列表

    复制内容到剪贴板
    代码:

    // 任务清单
    typedef enum _TASK_LIST
    {
        TAST_DISP_CLOCK,            // 显示时钟
        TAST_KEY_SAN,             // 按键扫描
        TASK_DISP_WS,             // 工作状态显示
         // 这里添加你的任务。。。。
         TASKS_MAX                                           // 总的可供分配的定时任务数目
    } TASK_LIST;

    好好看看,我们这里定义这个任务清单的目的其实就是参数TASKS_MAX的值,其他值是没有具体的意义的,只是为了清晰的表面任务的关系而已。

    3. 编写任务函数

    复制内容到剪贴板
    代码:

    /**************************************************************************************
    * FunctionName   : TaskDisplayClock()
    * Description    : 显示任务

    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void TaskDisplayClock(void)
    {

    }

    /**************************************************************************************
    * FunctionName   : TaskKeySan()
    * Description    : 扫描任务
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void TaskKeySan(void)
    {


    }

    /**************************************************************************************
    * FunctionName   : TaskDispStatus()
    * Description    : 工作状态显示
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    void TaskDispStatus(void)
    {


    }

    // 这里添加其他任务。。。。。。。。。

    现在你就可以根据自己的需要编写任务了。

    4. 主函数

    复制内容到剪贴板
    代码:

    /**************************************************************************************
    * FunctionName   : main()
    * Description    : 主函数
    * EntryParameter : None
    * ReturnValue    : None
    **************************************************************************************/
    int main(void) 

        InitSys();                  // 初始化

        while (1)
        {
            TaskProcess();             // 任务处理
        }
    }

    到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面?

           不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。。



    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);
        }
    }

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

     http://bbs.eeworld.com.cn/thread-311494-1-1.html

    http://bbs.21ic.com/icview-691804-1-1.html

  • 相关阅读:
    hdu 4027 Can you answer these queries? 线段树
    ZOJ1610 Count the Colors 线段树
    poj 2528 Mayor's posters 离散化 线段树
    hdu 1599 find the mincost route floyd求最小环
    POJ 2686 Traveling by Stagecoach 状压DP
    POJ 1990 MooFest 树状数组
    POJ 2955 Brackets 区间DP
    lightoj 1422 Halloween Costumes 区间DP
    模板 有源汇上下界最小流 loj117
    模板 有源汇上下界最大流 loj116
  • 原文地址:https://www.cnblogs.com/wolf-man/p/6697680.html
Copyright © 2011-2022 走看看