单片机应用层软件设计_单片机 底层 应用层 - CSDN
  • “分层思想”并不是什么神秘的东西,事实上很多做项目的工程师本身自己也会在用。...但是如果不懂程序设计的思想的话,会给你做项目的过程中带来很多很多的困惑。 参考了市面上各种各样的嵌入式书籍,MCS-51,

    “分层思想”并不是什么神秘的东西,事实上很多做项目的工程师本身自己也会在用。看了不少帖子都发现没有提及这个东西,然而分层结构确是很有用的东西,参透后会有一种恍然大悟的感觉。如果说我不懂LCD怎么驱动,那好办,看一下datasheet,参考一下别人的程序,很快就可以做出来。但是如果不懂程序设计的思想的话,会给你做项目的过程中带来很多很多的困惑。

    参考了市面上各种各样的嵌入式书籍,MCS-51,AVR ,ARM 等都有看过,但是没有发现有哪本是介绍设计思想的,就算有也是凤毛麟角。写程序不难,但是程序怎么样才能写的好,写的快,那是需要点经验积累的。结构化模块化的程序设计的思想,是最基本的要求。然而怎么将这个抽象的概念运用到工程实践当中呢?那需要在做项目的过程中经历磨难,将一些东西总结出来,抽象升华为理论,对经验的积累和技术的传播都大有裨益。所以在下出来献丑一下,总结一些东西。就我个人的经验而谈,有两个设计思想是非常重要的。

    一个就是“时间片轮的设计思想”,这个对实际中解决多任务问题非常有用,通常可以用这个东西来判断一个人是单片机学习者,还是一个单片机工程师。这个必须掌握。由于网上介绍这个的帖子也不少,所以这里就不多说了。

    第二个就是我今天想说的主题“分层屏蔽的设计思想”。下面用扫描键盘程序例子作为引子,引出今天说的东西。

    问题的提出 :单片机学习板一般为了简单起见,将按键分配的很好,例如整个 4*4 的键盘矩阵分配到P1口上面,8条控制线,刚好。这样的话程序也非常好写。只需要简单的

    KEY_DAT = P1;

    端口的数据就读进来了。诚然,现实中没有这么好的事情。在实际的项目应用当中,单片机引脚的复用相当厉害,这跟那些所谓的单片机学习板就有很大的差别了。

    另外一个原因,一般设计来说,是“软件配合硬件”的设计流程,简单点说就是,先确定好硬件原理图,硬件布线,最后才是软件的开发,因为硬件修改起来比较麻烦,相对来说软件修改的时候比较好改。这个就是中国传统的阴阳平衡哲学原理。硬件设计和软件设计本来就是鱼和熊掌的关系,两者不可兼得。方便了硬件设计,很可能给写软件带来很大的麻烦。反过来说,方便了软件设计,硬件设计也会相当的麻烦。如果硬件设计和软件设计同时方便了,那只有两种可能,一是这个设计方案非常简单,二是设计师已经达到了一个非常高的境界。我们不考虑那么多情况,单纯从常用的实际应用的角度来看问题。 硬件为了布线的方便,很多时候会可能将IO口分配到不同的端口上面,例如上面说的4*4键盘,8根线分别分配到 P0 P1 P2 P3 上面去了。那么,开发板的那些扫描键盘程序可以去见鬼了。怎么扫按键?我想起了我刚开始学习的时候,分成3段非常相似的程序,一个一个按键的扫描的经历...... 或许有人不甘心,“那些东西我花了很长时间学习的,也用的好好的,怎么能说一句不用就不用?”虽然有点残忍,但是我还是想说“兄弟,接受现实吧,现实是残酷的......”

    不过,人区别于低等动物的差别,是人会创造,在碰到困难的时候会想办法解决,于是我们开始了沉思...... 后我们引入初中数学学的“映射”的概念来解决问题。基本思想就是,将不同端口的按键映射到相同端口上面。

    这样按键扫描程序就分成3个层次了。

    1)最底层的是硬件层,完成端口扫描,20ms延时消抖,将端口的数据映射到一个KEY_DAT寄存器上面,KEY_DAT作为对上层驱动层的一个接口。

    2)中间的一层是驱动层,驱动层只对 KEY_DAT 寄存器的数值进行操作。简单点说,我们无论底层的硬件是怎么接线的,在驱动层都不需要关心,只需要关心 KEY_DAT 这个寄存器的数值是什么就可以了。这样出来的间接效果就是“屏蔽了底层硬件的差异”,所以驱动层写的程序就可以通用了。驱动层的另外一个功能是为了上层提供消息接口。我们用了类似window程序的消息的概念。这里可以提供一些按键消息,例如:按下消息,松开消息,长按键消息,长按键的时候的步进消息,等等。

    3)应用层。这里就是根据项目的不同分别写按键功能程序,属于最上层的程序。它使用的是驱动层提供的消息接口。在应用层写程序的思想就是,我不管下层是怎么工作的,我只关心按键消息。有按键消息来的时候我就执行功能,没有消息来的时候,我就什么也不做。

    下面用一个简单的常用的例子,说明我们这个设计思想的用法。秒表调整时间的时候,要求按着某个按键不放,时间能连续的向上增加。这个东西很实用,实际的家电中用途很广泛。在看下面的东西之前,大家可以想一下,这东西难吗?相信大家都会很响亮的回答,“不难!!”,然而我再问:“这东西麻烦吗?”我相信很多人肯定会说“很麻烦!!” 这不禁让我想起开始学单片机的时候写这种按键的那程序,乱七八糟的结构。如果不相信的话,可以自己用51写一下哦,那样就更加能体会本文说的分层结构的优越性。

    项目要求:两个按键,分别分配在P10 和P20,分别是“加”“减”按键,要求长按键的时候实现连续加和连续减的功能。

    实战:假设:按键上拉,没有按键的时候高电平,有按键的时候低电平,另外,为了突出问题,这里没有将延时消抖的程序写上去,在实际项目中应该加上。C语言函数参数的传递多种多样,这里作为例子,用了最简单的全局变量来传递参数,当然你也可以用 unsigned char ReadPort(void) 返回一个读键结果,甚至还可以 void ReadPort(unsigned char *pt) 用一个指针变量传递地址而达到直接修改变量的目的。方法是多种多样的,这个决定于每个人的程序风格。

    1)开始写硬件层程序,完成映射

    #define KYE_MIN 0X01

    #define KEY_PLUS 0X01

    unsigned char KeyDat;

    void ReadPort(void)

    {

    if (P1 & KEY_PLUS == 0 ){

    KeyDat |= 0x01 ;

    }

    if (P2 & KEY_MIN == 0 ){

    KeyDat |= 0x02 ;

    }

    }

    C语言应该很容易看懂吧?如果 KEY_PLUS 按下,P10口读到低电平,则 P1 & KEY_PLUS 的结果为 0 ,满足if 的条件,进入KeyDat |= 0x01 是将 KeyDat 的bit0 置一,也就是说,将 KEY_PLUS 映射到 KeyDat 的 bit0

    KEY_MIN 是同样的道理映射到 KeyDat 的 bit1

    如果 KeyDat 的 bit0 为 1 ,则说明 KEY_PLUS 按下,反则亦然。

    不需要想的很神秘,映射就是这么一回事。如果还有其他按键的话,用同样办法,将他们全部映射到 KeyDat 上面。

    2)驱动层程序编写 【【硬件层给驱动层提供最基本的一些屏蔽掉底层的一些硬件的相关数据,这样可以把硬件层的数据结构(比如这里是KeyDat)提供给驱动层,驱动层负责调用硬件层,应用层负责调用驱动层】】

    如果将 KeyDat想象成 P1 口,那么这个跟学习板那标准的扫描程序不就是一样了吗?对的,这个就是底层映射的目的了。

    3)应用层程序编写

    根据消息,硬件层是必须分离出来,然而驱动层和应用层的要求就不那么严格了,事实上一些简单的项目没有必要将这两层分离开来,根据实际应用灵活应对就可以了。其实这样写程序是很方便移植的,根据板子的不同而适当的修改一下硬件层那个 ReadPort 函数就完成了,驱动层和应用层很多代码可以不经过修改直接用,很能提高开发效率的。当然这个按键程序会存在一定的问题,特别是遇到常闭按键和点触按键的混合使用的场合。这个留给大家自己去想了,反正问题总是能找到解决办法的,尽管方法有好有坏。

    结束语

    以按键为媒介,介绍了程序设计当中的“分层屏蔽”的思想的原理和应用,按键只是一个例子,其实分层的思想普遍存在着程序设计当中。细心留意一下的话发现其实window,linux,网络的tcp/ip 结构全部都是分层的。这东西不是绣花枕头,而是实际用在工程上面的,只是平时不多见帖子介绍,或者没有人特意这样来总结,又或者是有经验的工程师作为藏在心中的法宝吧,这个就不得而知。不过好东西应该共享,菜鸟应该共勉,一起来学飞吧。

    展开全文
  • 应用层到驱动层

    2016-12-04 21:51:29
    1、应用层—->VFS——>驱动层——–>硬件层; 2、应用层的程序要想跟底层的硬件打交道必须要有设备文件;在应用层通过open打开一个设备文件时,在VFS层会建立inode结构体和file结构体,前者是静态的描述设备的一些...

    1、应用层—->VFS——>驱动层——–>硬件层;
    2、应用层的程序要想跟底层的硬件打交道必须要有设备文件;在应用层通过open打开一个设备文件时,在VFS层会建立inode结构体和file结构体,前者是静态的描述设备的一些信息(如:设备号,节点指针,设备类型以及cdev结构体),后者则会描述设备类型的一些动态信息(如:文件方法集,读写位置,权限,私有属性等)。注意file结构体中有inode的地址。
    3、此时根据inode结构体中的设备号在内核的chrdevs数组中找到对应的cdev结构体;然后将找到的cdev结构体的地址赋值给inode中的i_cdev成员。同时将cdev中的file方法集的地址复制给file结构体中的f_op(这是关键的一步)。
    4、最后,VFS层返回一个与file结构体相对应的文件描述符fd给应用程序,应用程序就通过这个fd找到VFS中的file结构体,再进一步找到file结构体中的file方法集,从而找到字符设备的函数接口了。

    展开全文
  • 工作中经过摸索实验,总结出单片机大致应用程序的架构有三种: 1. 简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。 2. 时间片轮询法...

    工作中经过摸索实验,总结出单片机大致应用程序的架构有三种:

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


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

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


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

    一、顺序执行法这种方法,这应用程序比较简单,实时性,并行性要求不太高的情况下是不错的方法,程序设计简单,思路比较清晰。但是当应用程序比较复杂的时候,如果没有一个完整的流程图,恐怕别人很难看懂程序的运行状态,而且随着程序功能的增加,编写应用程序的工程师的大脑也开始混乱。即不利于升级维护,也不利于代码优化。本人写个几个比较复杂一点的应用程序,刚开始就是使用此法,最终虽然能够实现功能,但是自己的思维一直处于混乱状态。导致程序一直不能让自己满意。
     这种方法大多数人都会采用,而且我们接受的教育也基本都是使用此法。对于我们这些基本没有学习过数据结构,程序架构的单片机工程师来说,无疑很难在应用程序的设计上有一个很大的提高,也导致了不同工程师编写的应用程序很难相互利于和学习。
    本人建议,如果喜欢使用此法的网友,如果编写比较复杂的应用程序,一定要先理清头脑,设计好完整的流程图再编写程序,否则后果很严重。当然应该程序本身很简单,此法还是一个非常必须的选择。
    下面就写一个顺序执行的程序模型,方便和下面两种方法对比:
    代 码/**************************************************************************************
    * 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;
            }
        }
    }
    二、时间片轮询法时间片轮询法,在很多书籍中有提到,而且有很多时候都是与操作系统一起出现,也就是说很多时候是操作系统中使用了这一方法。不过我们这里要说的这个时间片轮询法并不是挂在操作系统下,而是在前后台程序中使用此法。也是本贴要详细说明和介绍的方法。 对于时间片轮询法,虽然有不少书籍都有介绍,但大多说得并不系统,只是提提概念而已。下面本人将详细介绍这种模式,并参考别人的代码建立的一个时间片轮询架构程序的方法,我想将给初学者有一定的借鉴性。
    在这里我们先介绍一下定时器的复用功能。 使用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;       // 延时20msTaskMark[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();             // 任务处理
        }
    }到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面? 不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。
    三、操作系统操作系统的本身是一个比较复杂的东西,任务的管理,执行本事并不需要我们去了解。但是光是移植都是一件非常困难的是,虽然有人说过“你如果使用过系统,将不会在去使用前后台程序”。但是真正能使用操作系统的人并不多,不仅是因为系统的使用本身很复杂,而且还需要购买许可证(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);
        }
    }不难看出,时间片轮询法优势还是比较大的,即由顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解。

    展开全文
  • 在工作中经过摸索实验,总结出单片机大致应用程序的架构有三种:1. 简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。2. 时间片轮询法,此...
        

    640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1


    在工作中经过摸索实验,总结出单片机大致应用程序的架构有三种:1. 简单的前后台顺序执行程序,这类写法是大多数人使用的方法,不需用思考程序的具体架构,直接通过执行顺序编写应用程序即可。
    2. 时间片轮询法,此方法是介于顺序执行与操作系统之间的一种方法。
    3. 操作系统,此法应该是应用程序编写的最高境界。
    下面就分别谈谈这三种方法的利弊和适应范围等。一、顺序执行法这种方法,这应用程序比较简单,实时性,并行性要求不太高的情况下是不错的方法,程序设计简单,思路比较清晰。但是当应用程序比较复杂的时候,如果没有一个完整的流程图,恐怕别人很难看懂程序的运行状态,而且随着程序功能的增加,编写应用程序的工程师的大脑也开始混乱。即不利于升级维护,也不利于代码优化。本人写个几个比较复杂一点的应用程序,刚开始就是使用此法,最终虽然能够实现功能,但是自己的思维一直处于混乱状态。导致程序一直不能让自己满意。
     这种方法大多数人都会采用,而且我们接受的教育也基本都是使用此法。对于我们这些基本没有学习过数据结构,程序架构的单片机工程师来说,无疑很难在应用程序的设计上有一个很大的提高,也导致了不同工程师编写的应用程序很难相互利于和学习。
    本人建议,如果喜欢使用此法的网友,如果编写比较复杂的应用程序,一定要先理清头脑,设计好完整的流程图再编写程序,否则后果很严重。当然应该程序本身很简单,此法还是一个非常必须的选择。
    下面就写一个顺序执行的程序模型,方便和下面两种方法对比:代 码/**************************************************************************************
    * 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;
            }
        }
    }
    二、时间片轮询法时间片轮询法,在很多书籍中有提到,而且有很多时候都是与操作系统一起出现,也就是说很多时候是操作系统中使用了这一方法。不过我们这里要说的这个时间片轮询法并不是挂在操作系统下,而是在前后台程序中使用此法。也是本贴要详细说明和介绍的方法。 对于时间片轮询法,虽然有不少书籍都有介绍,但大多说得并不系统,只是提提概念而已。下面本人将详细介绍这种模式,并参考别人的代码建立的一个时间片轮询架构程序的方法,我想将给初学者有一定的借鉴性。
    在这里我们先介绍一下定时器的复用功能。 使用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;       // 延时20msTaskMark[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();             // 任务处理
        }
    }到此我们的时间片轮询这个应用程序的架构就完成了,你只需要在我们提示的地方添加你自己的任务函数就可以了。是不是很简单啊,有没有点操作系统的感觉在里面? 不防试试把,看看任务之间是不是相互并不干扰?并行运行呢?当然重要的是,还需要,注意任务之间进行数据传递时,需要采用全局变量,除此之外还需要注意划分任务以及任务的执行时间,在编写任务时,尽量让任务尽快执行完成。。。。。。。。
    三、操作系统操作系统的本身是一个比较复杂的东西,任务的管理,执行本事并不需要我们去了解。但是光是移植都是一件非常困难的是,虽然有人说过“你如果使用过系统,将不会在去使用前后台程序”。但是真正能使用操作系统的人并不多,不仅是因为系统的使用本身很复杂,而且还需要购买许可证(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);
        }
    }不难看出,时间片轮询法优势还是比较大的,即由顺序执行法的优点,也有操作系统的优点。结构清晰,简单,非常容易理解。

    640?

    1.为什么原来每换一个CPU就必须改写程序,但现在不用了?

    2.Linux 将不再支持旧 CPU 架构,可节省 50 万行代码!

    3.单片机的外围功能电路,学完这个才能运行系统!

    4.在选不选择ROS这个问题上,资深嵌入式工程师如是说!

    5.十年开发经验总结的 Java 程序员架构学习路线

    6.2018年第4期《单片机与嵌入式系统应用》电子刊新鲜出炉!

    640?wx_fmt=gif

    免责声明:本文系网络转载,版权归原作者所有。如涉及作品版权问题,请与我们联系,我们将根据您提供的版权证明材料确认版权并支付稿酬或者删除内容。

    展开全文
  • C语言嵌入式系统编程修炼之一:背景篇不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发...
  • 详谈单片机应用系统硬件电路设计 一个单片机应用系统的硬件电路设计包含两部分内容:一是系统扩展,即单片机内部的功能单元,如ROM、RAM、I/O、定时器/计数器、中断系统等不能满足应用系统的要求时,必须在片外...
  • 单片机和嵌入式,其实没有什么标准的定义来区分他们,对于进行过单片机和嵌入式开发的开发者来说,都有他们自己的定义,接下来,就谈谈本人对这两个概念的理解和感悟。 首先明确概念,什么是单片机单片机是一种...
  • 单片机软件工程(一)--FIFO设计  中午调一个程序,一个比较简单的程序,就是几个按键,一个数码管,使得用按键控制数码管上的数字,前提是不阻塞CPU!!(本来整个程序设计是不准备利用延时的,...
  • 如果现在单片机及接口这块很熟悉,并且能用C和汇编语言来编程的话,从嵌入式系统的硬件走起来相对容易,硬件也是驱动的基础,一个优秀的驱动工程师是要能够看懂硬件的电路图和自行完成CPLD的逻辑设计的,同时...
  • 书名:《单片机应用技术选编(10)》(北京航空航天大学出版社.何立民) PDF格式扫描版,全书分为11章,共838页。2004年出版。 内容简介 《单片机应用技术选编》系列图书是汇集了多年间国内主要期刊杂志中有关单片机...
  • 在基于模型的设计中,生成的应用层程序要整合到底层驱动代码程序中,经编译下载到单片机运行,在整合时,就设计到一个问题:应用层程序的调用周期应设为多少 解决方法:整合时,先根据硬件设备设计一个初始值,然后...
  • 如果从应用层的应用开发出发,建议先熟悉API函数,推荐《UNIX环境高级编程》,现在应该是第3版了,如果从应用层的界面开发出发,则建议学习Qt,WinCE或Android等。 但真实的嵌入式开发,熟悉以上还是远远不够的,...
  • 一个单片机应用系统的硬件电路设计包含两部分内容:一是系统扩展,即单片机内部的功能单元,如ROM、RAM、I/O、定时器/计数器、中断系统等不能满足应用系统的要求时,必须在片外进行扩展,选择适当的芯片,设计相应的...
  • 摘要:借助系统模型,阐明GSM模块收发短信的基本概念以及串口控制SMS的...关键词:单片机 短信收发 软件设计 GSM(Global System for Mobile communication)系统是目前基于时分多址技术的移动通信体制中,比较成熟完
  • 下面是总结的一些设计中应注意的问题,和单片机硬件设计原则,希望大家能看完 探索者号智能自平衡车 http://www.makeru.com.cn/course/details/2961?s=69821 (1) 在元器件的布局方面,应该把相互有关的元件尽量放...
  • 经过几天的回忆与思考,分析了我的成长路径,选择一些我自认为是成长关键点的位置,总结一个关键字,一步一步来讨论关于嵌入式单片机软件的架构。 我先把总结出的关键字写出来吧:**流水式、中断前后台、任务式、...
  • 关注、星标公众号,不错过精彩内容整理:黄工素材来源:网络参考来源:https://blog.51cto.com/kenotu/1614390在正规的项目开发中,项目往往是并行开发的,也就...
  • 单片机硬件设计总结

    2018-09-14 14:56:15
    下面是总结的一些设计中应注意的问题,和单片机硬件设计原则,老生常谈了,不过还是写一下:   (1)在元器件的布局方面,应该把相互有关的元件尽量放得靠近一些  例如,时钟发生器、晶振、CPU的时钟输入端都易...
  • 软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三,从下至上分别为:数据访问、业务逻辑(又或称为领域)、表示。各的作用1:数据访问:主要是对非...
1 2 3 4 5 ... 20
收藏数 8,015
精华内容 3,206
关键字:

单片机应用层软件设计