单片机的非OS的事件驱动

Part 1  前言
  很多单片机项目恐怕都是没有操作系统的前后台结构,就是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模块只负责检测按键,并触发一个按键事件,至于这个按键被哪个模块处理,它压根不需要知道,大大减少模块之间的耦合度,也减少出错的几率。这不正好是事件驱动的思想吗?
  接下来,该如何实现呢?

Part 2  事件驱动的实现
  需要一个事件队列:
  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[i].event == event)
            {
                (*_handle[i].handle)();
            }
        }
    }
}

main函数可以精简成这样:

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

这样代码的缺陷是需要为处理函数队列分配一个非常大的内存空间,项目越复杂分配的空间越大,显然不可取。需要做个变通,减少内存空间的使用量。

Part3  改进与变通
  这样代码的缺陷是需要为处理函数队列分配一个非常大的内存空间,项目越复杂分配的空间越大,显然不可取。需要做个变通,减少内存空间的使用量。

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[i].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[i] == 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;
            }
        }
    }
}

part 4  深一步:针对与时间相关的事件
  对于与时间相关的事件(循环事件和延时处理事件)需要做进一步处理。
  首先要设定系统Tick,可以用一个定时器来生成,例如配置一个定时器,每10ms中断一次。
  注:tick一般指os的kernel计时单位,用于处理定时、延时事件之类。一般使用硬件定时器中断处理tick事件
  定义一个时间事件队列:

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[i].timer--;
        if(_timerEvent[i].timer == 0)
        {
            dispatchEvent(_timerEvent[i].event);// 事件触发器

            if(_timerEvent[i].type == CYCLE_EVENT)
            {
                // 循环事件,重新计数
                _timerEvent[i].timer = _timerEvent[i].timerBackup;
            }
            else
            {
                // 延时事件,触发后删除
                delTimerEvent(_timerEvent[i].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[i].event == event)
        {
            for(j=i; j<_timerEventTail; j++)
            {
                _timerEvent[j] = _timerEvent[j+1];
            }

            _timerEventFull= FALSE;
            _timerEventTail--;
        }
    }
}

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

——【感谢】资料来源于https://wenku.baidu.com/view/5465391d10a6f524ccbf8591.html

  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单片机是一种基于微处理器的集成电路,具有资源有限的特点。它通常包含有限的运算能力、存储空间以及输入输出接口。对于单片机来说,资源的有限性意味着需要高效地管理这些资源,以保证系统的正常运行。 为了充分利用单片机的资源并提高系统的稳定性,在单片机系统中引入操作系统(OS)是一种通常的做法。操作系统是一种控制和管理计算机硬件和软件资源的软件系统。 针对单片机的资源有限的特点,单片机操作系统(RTOS)是一种轻量级的、实时响应的操作系统。相比于传统的桌面操作系统,RTOS在占用更少的内存和处理器资源的同时,提供了更高的实时性能和可靠性。 RTOS提供了以下几个方面的功能: 1.任务管理:RTOS将系统分解成多个任务,每个任务执行不同的操作。它能够合理地分配任务的优先级和时间片,及时处理任务的切换和调度,从而提高资源的利用率和系统的响应速度。 2.内存管理:RTOS通过动态分配和释放内存,确保系统能够高效地利用存储空间。它还可以提供内存保护和垃圾回收功能,以防止内存泄漏和碎片化,确保系统的稳定性。 3.设备驱动:RTOS提供了对各种外设的驱动支持,可以管理和控制与单片机相连的外部设备。它能够通过标准接口和协议,实现与外设的可靠通信和数据交换。 4.中断处理:RTOS能够管理中断并在需要时及时响应,确保及时处理外部的中断事件,提高系统的实时性和可靠性。 5.通信与同步:RTOS提供了各种通信和同步机制,如消息队列、信号量、互斥量等,用于实现不同任务之间的通信和同步,防止数据冲突和竞争条件的发生。 总而言之,单片机资源有限,但通过引入RTOS,可以有效地管理和利用这些资源,提高系统的性能和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值