71564|157

4996

帖子

19

TA的资源

裸片初长成(初级)

楼主
 

浅谈单片机应用程序架构 [复制链接]

 

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

   

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

 

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

 

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

 

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

 

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

[ 本帖最后由 zhaojun_xf 于 2011-11-22 08:16 编辑 ]
查看精华帖全部内容,请登录或者注册
此帖出自NXP MCU论坛

最新回复

前辈牛B,正在为程序发愁,突然觉得,整个程序应该有一个结构上的考量。   您的文章让我的小想法,得到了大发展。 我也希望,以后我也要努力写一些干货文章,分享给大家。   详情 回复 发表于 2020-5-9 10:51
点赞(1) 关注(42)
个人签名我的博客
 

回复
举报

4996

帖子

19

TA的资源

裸片初长成(初级)

推荐
 

2. 时间片轮询法

 

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

 

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

 

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

 

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

 

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

 

2. 定义一个数值:

 

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

  2.  

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

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

复制代码

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

  1. /**************************************************************************************
    * 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为例:

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

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

复制代码

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

 

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

 

循环判断标志位:

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

 

执行其他函数:

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

 

时间片轮询法的架构:

 

1.设计一个结构体:

 

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

复制代码

 

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

 

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

 

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

  2.     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. 任务处理

 

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

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

复制代码

 

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

 

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

 

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

 

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

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

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

  3. };

复制代码

 

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

 

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

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

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

 

2. 任务列表

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

复制代码

 

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

 

3. 编写任务函数

 

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

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

  3.  

  4. }

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


  6. }

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


  8. }

  9.  

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

复制代码

 

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

 

4. 主函数

 

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

        InitSys();                  // 初始化

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

复制代码

 

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

 

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

[ 本帖最后由 zhaojun_xf 于 2011-11-22 14:19 编辑 ]
此帖出自NXP MCU论坛

点评

这种方法的前提是执行的每个任务都是短小精悍的,要不然一个任务执行的时间过长,大于了其它任务设置的时间片值,那其它任务就无法保证按它预设的时间片来执行,唉,这也只是一个定时调用的利子,楼主把它抽像化了,  详情 回复 发表于 2014-7-20 19:44
 
个人签名我的博客
 
 

回复

4996

帖子

19

TA的资源

裸片初长成(初级)

推荐
 

1. 顺序执行法:

 

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

 

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

 

 

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

 

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

 

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

        uint8 keyValue;

  2.  

  3.     InitSys();                  // 初始化

  4.  

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

复制代码

[ 本帖最后由 zhaojun_xf 于 2011-11-22 14:37 编辑 ]
此帖出自NXP MCU论坛
 
个人签名我的博客
 
 

回复

4996

帖子

19

TA的资源

裸片初长成(初级)

4
 

3.操作系统

 

       操作系统的本身是一个比较复杂的东西,任务的管理,执行本事并不需要我们去了解。但是光是移植都是一件非常困难的是,虽然有人说过“你如果使用过系统,将不会在去使用前后台程序”。但是真正能使用操作系统的人并不多,不仅是因为系统的使用本身很复杂,而且还需要购买许可证(ucos也不例外,如果商用的话)。

 

       这里本人并不想过多的介绍操作系统本身,因为不是一两句话能过说明白的,下面列出UCOS下编写应该程序的模型。大家可以对比一下,这三种方式下的各自的优缺点。

 

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

        OSInit();                // 初始化uCOS-II

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

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

复制代码

 

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

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

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

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

复制代码

 

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

[ 本帖最后由 zhaojun_xf 于 2011-11-23 07:47 编辑 ]
此帖出自NXP MCU论坛

点评

楼主说的很给力,受教了。但操作系统的架构说的不多,想要更多的了解该何去何从,希望给指条明路。谢谢!!!  详情 回复 发表于 2018-4-18 09:07
 
个人签名我的博客
 
 

回复

48

帖子

0

TA的资源

一粒金砂(中级)

5
 
期待下文,期待每一种架构的详细说明,最好有例程,最好能让新手好懂。
此帖出自NXP MCU论坛
 
 
 

回复

60

帖子

0

TA的资源

一粒金砂(中级)

6
 
MARK!等待中……
此帖出自NXP MCU论坛
 
 
 

回复

7238

帖子

195

TA的资源

五彩晶圆(高级)

7
 
期待下文 楼主V5
此帖出自NXP MCU论坛
 
 
 

回复

391

帖子

1

TA的资源

一粒金砂(高级)

8
 
很棒的讲解     谢谢分享   学习了
此帖出自NXP MCU论坛
 
 
 

回复

115

帖子

0

TA的资源

一粒金砂(初级)

9
 
谢谢 谢谢 谢谢
此帖出自NXP MCU论坛
 
 
 

回复

2734

帖子

0

TA的资源

裸片初长成(初级)

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

回复

18

帖子

0

TA的资源

一粒金砂(中级)

11
 
good article
此帖出自NXP MCU论坛
 
 
 

回复

153

帖子

0

TA的资源

一粒金砂(高级)

12
 
精华!精华!
此帖出自NXP MCU论坛
 
个人签名希望在论坛中答疑解惑、得到启示,找到互惠的朋友。
 
 

回复

2

帖子

0

TA的资源

一粒金砂(中级)

13
 
任务执行时间必须把握好,否则导致不可预测的问题!这点还是没法和操作系统比的
此帖出自NXP MCU论坛
 
 
 

回复

4996

帖子

19

TA的资源

裸片初长成(初级)

14
 

回复 13楼 cchd20 的帖子

那是当然,但是操作系统的难度,和系统运行的额外开销以及许可证的购买,恐怕也必须考虑吧
此帖出自NXP MCU论坛
 
个人签名我的博客
 
 

回复

4996

帖子

19

TA的资源

裸片初长成(初级)

15
 
各有优缺点,关键是看全面的权衡后才能选择,并不是哪一种最好。。。。。
此帖出自NXP MCU论坛
 
个人签名我的博客
 
 

回复

185

帖子

0

TA的资源

一粒金砂(初级)

16
 
学习了
此帖出自NXP MCU论坛
 
个人签名加油学习,努力提高
 
 

回复

1180

帖子

0

TA的资源

五彩晶圆(初级)

17
 
讲的不错哦!不过操作系统那部分程序看不懂,有时间补补这块缺失的知识!
此帖出自NXP MCU论坛
 
 
 

回复

5

帖子

0

TA的资源

一粒金砂(中级)

18
 

谢谢

我是一名初学者,看了之后虽然有些不是很理解。但是还是谢谢。

此帖出自NXP MCU论坛
 
 
 

回复

85

帖子

0

TA的资源

一粒金砂(高级)

19
 
记号
此帖出自NXP MCU论坛
 
 
 

回复

7

帖子

0

TA的资源

一粒金砂(中级)

20
 
抽空好好试下!
此帖出自NXP MCU论坛
 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/10 下一条
立即报名 | 2025 瑞萨电子工业以太网技术日即将开启!
3月-4月 深圳、广州、北京、苏州、西安、上海 走进全国6城
2025瑞萨电子工业以太网技术巡回沙龙聚焦工业4.0核心需求,为工程师与企业决策者提供实时通信技术最佳解决方案。
预报从速,好礼等您拿~

查看 »

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

 
机器人开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

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