原创 单片机的非OS的事件驱动思考1

2011-3-30 16:50 3018 12 13 分类: 软件与OS

很多单片机项目恐怕都是没有操作系统的前后台结构,就是main函数里用while无限循环各种任务,中断处理紧急任务。这种结构最简单,上手很容易,可是当项目比较大时,这种结构就不那么适合了,编写代码前你必须非常小心的设计各个模块和全局变量,否则最终会使整个代码结构杂乱无序,不利于维护,而且往往会因为修改了某部分代码而莫名其妙的影响到其他功能,而使调试陷入困境。

改变其中局面的最有效措施当然是引入嵌入式操作系统,但是大多数的操作系统都是付费的(特别是商业项目)。我们熟悉的uc-os/II如果你应用于非商业项目它是免费的,而应用于商业项目的话则要付费,而且价格不菲。

我们也可以自己编写一套嵌入式OS,这当然最好了。可要编写一套完整的OS并非易事,而且当项目并不是非常复杂的话也不需要一个完整的os支持。我们只要用到OS最基本的任务调度和上下文切换就够了。正是基于这样的想法,最近的一个项目中我就尝试采用事件驱动的思想重新构建了代码架构,实际使用的效果还不错,在这里做个总结。

本质上新架构仍然是前后台结构,只不过原来的函数直接调用改成通过指向函数的指针来调用。实际上这也是嵌入式OS任务调度的一个核心。C语言中可以定义指向函数的指针:

    void (*handle)(void);

这里的handle就是一个指向函数的指针,我们只要将某函数的函数名赋给该指针,就能通过实现函数的调用了:

    void func1(void)
    {
        // Code
    }

    handle = func1;
    (*handle)(); // 实现func1的调用

有了这个函数调用新方法,我们就可以想办法将某个事件与某个函数关联,实现所谓的事件驱动。例如,按键1按下就是一个事件,func1响应按键1按下事件。但是,如果是单纯的调用方法替代又有什么意义呢?这又怎么会是事件驱动呢?关键就在于使用函数指针调用方法可以使模块和模块之间的耦合度将到最低。一个例子来说明这个问题,一个按键检测模块用于检测按键,一个电源模块处理按键1动作。

传统的前后台处理方法:
main.c

    void main()
    {
        ...
        while(1)
        {
            ...
            keyScan();
            if(flagKeyPress)
            {
                keyHandle(); // 检测到按键就设置flagKeyPress标志,进入处理函数
            }
        }
    }

key.c

    void keyHandle(void)
    {
        switch (_keyName) // 存放按键值的全局变量
        {
             ...
            case KEY1: pwrOpen(); break;
            case KEY2: pwrClose(); break;
        }
    }

power.c

    void pwrOpen(void)
    {
        ...
    }

    void pwrClose(void)
    {
        ...
    }

这样的结构的缺点在哪里呢?
1. key代码中直接涉及到power代码的函数,如果power代码里的函数变更,将引起key代码的变更
2. 一个按键值对应一个处理函数,如果要增加响应处理函数就要再次修改key代码
3. 当项目越来越大时,引入的全局变量会越来越多,占用过多的内存
很显然key模块与其他模块的耦合程度太高了,修改其他模块的代码都势必去修改key代码。理想的状态是key模块只负责检测按键,并触发一个按键事件,至于这个按键被哪个模块处理,它压根不需要知道,大大减少模块之间的耦合度,也减少出错的几率。这不正好是事件驱动的思想吗?

接下来,该如何实现呢?

事件驱动的实现。
需要一个事件队列

    u16 _event[MAX_EVENT_QUEUE];

它是一个循环队列,保存事件编号,我们可以用一个16位数为各种事件编号,可以定义65535个事件足够使用了。

一个处理函数队列

    typedef struct
    {
        u16 event; // 事件编号
        void (*handle)(void); // 处理函数
     }handleType;

    handleType _handle[MAX_HANDLE_QUEUE];

它实际是一个数组,每个元素保存事件编号和对应的处理函数指针。

一个驱动函数

    void eventProc(void)
    {
        u16 event;
        u8 i;

        if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
        {
            event = _eq[_eventHead];
            _event [_eventHead++] = 0; // 清除事件

            if(_eventHead>= MAX_EVENT_QUEUE)
            {
                _eventHead= 0; // 循环队列
            }
        
             // 依次比较,执行与事件编号相对应的函数
            for(i=0; i<_handleTail; i++)
            {
                if(_handle.event == event)
                {
                    (*_handle.handle)();
                }
            }
        }
    }

main函数可以精简成这样:

    void main(void)
    {
        ...
        while(1)
        {
            eventProc();
        }
    }

这样代码的缺陷是需要为处理函数队列分配一个非常大的内存空间,项目越复杂分配的空间越大,显然不可取。需要做个变通,减少内存空间的使用量。变通方法就是将各模块的散转处理保存在模块内部,而不再写入到处理函数队列中,具体实现如下:

事件队列不需要修改。
修改处理函数队列:

    typedef struct
    {
        void (*handle)(u16 event); // 仅保存模块总的散转函数
    }handleType;

    handleType _handle[MAX_HANDLE_QUEUE];

修改驱动函数:

    void eventProc(void)
    {
        u16 event;
        u8 i;

        if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
        {
             ...
        
            for(i=0; i<_handleTail; i++)
            {
                (*_handle.handle)(event); // 将事件编号传递给模块散转函数
            }
        }
    }

把散转处理交回给各模块,例如power模块的散转函数:

    void pwrEventHandle(u16 event)
    {
        switch (event)
        {
            ...
            case EVENT_KEY1_PRESS: pwrOpen(); break;
            ...
        }
    }

在power模块的初始化函数中,将该散转函数加入到处理函数队列中:

     // 该函数在系统初始化时调用
    void pwrInit(void)
    {
        ...
        addEventListener(pwrEventHandle);
        ...
    }

addEventListener定义如下:

    void addEventListener(void (*pFunc)(u16 event))
    {
        if(!_handleFull)
        {
            _handle[_handleTail].handle = pFunc;
            _handleTail++;

            if(_handleTail>= MAX_HANDLE_QUEUE)
            {
                _handleFull= TRUE;
            }
        }
    }

每个模块都定义各自的散转处理,然后在初始化的时候将该函数存入处理事件队列中,即能实现事件处理又不会占用很多的内存空间。

加入到事件队列需要封装成统一的函数dispatchEven,由各模块直接调用。例如,key模块就可以dispatchEvent(EVENT_KEY1_PRESS)来触发一个事件

    void dispatchEvent(u16 event)
    {
        u8 i;
        bool canDispatch;

        canDispatch = TRUE;

        if(!_eventFull)
        {
            // 为了避免同一事件被多次加入到事件队列中
            for(i=_eventHead; i!=_eventTail;)
            {
                if(_event == event)
                {
                    canDispatch = FALSE;
                    break;
                }

                i++;
                if(i >= MAX_EVENT_QUEUE)
                {
                    i = 0;
                }
            }

            if(canDispatch)
            {
                _event[_eventTail++] = event;

                if(_eventTail>= MAX_EVENT_QUEUE)
                {
                    _eventTail= 0;
                }

                if(_eventTail== _eventHead)
                {
                    _eventFull = TRUE;
                }
            }
        }
    }

对于与时间相关的事件(循环事件和延时处理事件)需要做进一步处理。

首先要设定系统Tick,可以用一个定时器来生成,例如配置一个定时器,每10ms中断一次。

定义一个时间事件队列

    typedef struct
    {
        u8 type;   // 事件类别,循环事件还是延时事件
        u16 event; // 触发的事件编号
        u16 timer;   // 延时或周期时间计数器
        u16 timerBackup; // 用于周期事件的时间计数备份
    }timerEventType;

    timerEventType _timerEvent[MAX_TIMER_EVENT_Q];

在定时器Tick中断中将时间事件转换成系统事件:

    void SysTickHandler(void)
    {
        ...
        for(i=0; i<_timerEventTail; i++)
        {
            _timerEvent.timer--;
            if(_timerEvent.timer == 0)
            {
                dispatchEvent(_timerEvent.event);

                if(_timerEvent.type == CYCLE_EVENT)
                {
                     // 循环事件,重新计数
                    _timerEvent.timer = _timerEvent.timerBackup;
                }
                else
                {
                    // 延时事件,触发后删除
                    delTimerEvent(_timerEvent.event);
                }
            }
        }
    }

将增加和删除时间事件封装成函数,便以调用:

    void addTimerEvent(u8 type, u16 event, u16 timer)
    {
        _timerEvent[_timerEventTail].type = type;
         _timerEvent[_timerEventTail].event = event;
         _timerEvent[_timerEventTail].timer = timer; // 时间单位是系统Tick间隔时间
         _timerEvent[_timerEventTail].timerBackup = timer; // 延时事件并不使用
         _timerEventTail++;
    }

    void delTimerEvent(u16 event)
    {
        ...
        for(i=0; i<_timerEventTail; i++)
        {
            if(_timerEvent.event == event)
            {
                for(j=i; j<_timerEventTail; j++)
                {
                    _timerEvent[j] = _timerEvent[j+1];
                }

                _timerEventFull= FALSE;
                _timerEventTail--;
            }
        }
    }

对于延时处理,用事件驱动的方法并不理想,因为这可能需要将一段完整的代码拆成两个函数,破坏了代码的完整性。解决的方法需要采用OS的上下文切换,这就涉及到程序堆栈问题,用纯C代码不容易实现。



PARTNER CONTENT

文章评论1条评论)

登录后参与讨论

用户377235 2011-12-2 14:10

顶,不错
相关推荐阅读
用户1314788 2012-02-03 15:20
评论:@jjldc(九九)的电子博客 博客中提到的“转一篇比较详细介绍FatFs文件系统移植的文章”
11...
用户1314788 2011-05-04 10:11
STM32 IAP
引导加载程序是存储在内部引导ROM存储器(系统内存),其主要任务是通过下载应用程序到内部FLASH通过USART1的通信接口. 从系统内存启动bootloader然后通过USART1接口外设下载应...
用户1314788 2011-04-11 09:31
基于AVR单片机队列的UART通信模块
对于堆栈来说,插入、删除操作是固定在一端进行的,这一端称为“栈顶”,另一端称为“栈底”。 堆栈指针(Stack  Pointer)用于指示栈顶位置(地址),在有些单片机中,堆栈指针可以通过程序去设置。...
用户1314788 2011-04-08 11:03
assert_param STM32的固件库 使用须知
在STM32的固件库和提供的例程中,到处都可以见到assert_param()的使用。如果打开任何一个例程中的stm32f10x_conf.h文件,就可以看到实际上assert_param是一个宏定义...
用户1314788 2011-03-24 14:41
C
要从逻辑上删除一段C代码,更好的办法是用#if指令。 #if  0   statements#endif int *a;*a = 12;  //我们声明了这个指针变量,但从未对它进行过初始化,所以我们...
EE直播间
更多
我要评论
1
12
关闭 站长推荐上一条 /3 下一条