单片机的非OS的事件驱动

单片机的非OS的事件驱动

Part 1  前言

  很多单片机项目恐怕都是没有操作系统的前后台结构,就是main函数里用while无限循环各种任务,中断处理紧急任务。这种结构最简单,上手很容易,可是当项目比较大时,这种结构就不那么适合了,编写代码前你必须非常小心的设计各个模块和全局变量,否则最终会使整个代码结构杂乱无序,不利于维护,而且往往会因为修改了某部分代码而莫名其妙的影响到其他功能,而使调试陷入困境。
  改变其中局面的最有效措施当然是引入嵌入式操作系统,但是大多数的操作系统都是付费的(特别是商业项目)。我们熟悉的uc-os/II如果你应用于非商业项目它是免费的,而应用于商业项目的话则要付费,而且价格不菲。
  我们也可以自己编写一套嵌入式OS,这当然最好了。可要编写一套完整的OS并非易事,而且当项目并不是非常复杂的话也不需要一个完整的os支持。我们只要用到OS最基本的任务调度和上下文切换就够了。正是基于这样的想法,最近的一个项目中我就尝试采用事件驱动的思想重新构建了代码架构,实际使用的效果还不错,在这里做个总结。
  本质上新架构仍然是前后台结构,只不过原来的函数直接调用改成通过指向函数的指针来调用。实际上这也是嵌入式OS任务调度的一个核心。C语言中可以定义指向函数的指针:
  void (*handle)(void);
  这里的handle就是一个指向函数的指针,我们只要将某函数的函数名赋给该指针,就能通过实现函数的调用了:

1 void func1(void)
2 {
3      // Code
4 }
5
6 handle = func1;
7 (*handle)(); // 实现func1的调用

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

 1 void main()
 2 {
 3     ...
 4     while(1)
 5     {
 6         ...
 7         keyScan();
 8         if(flagKeyPress)
 9         {
10             keyHandle(); // 检测到按键就设置flagKeyPress标志,进入处理函数
11         }
12     }
13 }

key.c

1 void keyHandle(void)
2 {
3     switch (_keyName) // 存放按键值的全局变量
4     {
5         ...
6         case KEY1: pwrOpen(); break;
7         case KEY2: pwrClose(); break;
8     }
9 }

power.c

1 void pwrOpen(void)
2 {
3     ...
4 }
5
6 void pwrClose(void)
7 {
8     ...
9 }

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

Part 2  事件驱动的实现

  需要一个事件队列
  u16 _event[MAX_EVENT_QUEUE];
  它是一个循环队列,保存事件编号,我们可以用一个16位数为各种事件编号,可以定义65535个事件足够使用了。
  一个处理函数队列

1 typedef struct
2 {
3     u16 event; // 事件编号
4     void (*handle)(void); // 处理函数
5 }handleType;
6
7 handleType _handle[MAX_HANDLE_QUEUE];

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

 1 void eventProc(void)
 2 {
 3     u16 event;
 4     u8 i;
 5
 6     if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
 7     {
 8         event = _eq[_eventHead];
 9         _event [_eventHead++] = 0; // 清除事件
10
11         if(_eventHead>= MAX_EVENT_QUEUE)
12         {
13             _eventHead= 0; // 循环队列
14         }
15         
16          // 依次比较,执行与事件编号相对应的函数
17         for(i=0; i<_handleTail; i++)
18         {
19             if(_handle[i].event == event)
20             {
21                 (*_handle[i].handle)();
22             }
23         }
24     }
25 }

  main函数可以精简成这样:

1 void main(void)
2 {
3     ...
4     while(1)
5     {
6         eventProc();
7     }
8 }

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

Part3  改进与变通

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

1 typedef struct
2 {
3     void (*handle)(u16 event); // 仅保存模块总的散转函数
4 }handleType;
5
6 handleType _handle[MAX_HANDLE_QUEUE];

  修改驱动函数:

 1 void eventProc(void)
 2 {
 3     u16 event;
 4     u8 i;
 5
 6     if((_eventHead!=_eventTail) || _eventFull) // 事件队列里有事件时才处理
 7     {
 8         ...
 9         
10         for(i=0; i<_handleTail; i++)
11         {
12             (*_handle[i].handle)(event); // 将事件编号传递给模块散转函数
13         }
14     }
15 }

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

1 void pwrEventHandle(u16 event)
2 {
3     switch (event)
4     {
5         ...
6         case EVENT_KEY1_PRESS: pwrOpen(); break;
7         ...
8     }
9 }

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

1  // 该函数在系统初始化时调用
2 void pwrInit(void)
3 {
4     ...
5     addEventListener(pwrEventHandle);
6     ...
7 }

  addEventListener定义如下:

 1 void addEventListener(void (*pFunc)(u16 event))
 2 {
 3     if(!_handleFull)
 4     {
 5         _handle[_handleTail].handle = pFunc;
 6         _handleTail++;
 7
 8         if(_handleTail>= MAX_HANDLE_QUEUE)
 9         {
10             _handleFull= TRUE;
11         }
12     }
13 }

  每个模块都定义各自的散转处理,然后在初始化的时候将该函数存入处理事件队列中,即能实现事件处理又不会占用很多的内存空间。
  加入到事件队列需要封装成统一的函数dispatchEven,由各模块直接调用。例如,key模块就可以dispatchEvent(EVENT_KEY1_PRESS)来触发一个事件

 1 void dispatchEvent(u16 event)
 2 {
 3     u8 i;
 4     bool canDispatch;
 5
 6     canDispatch = TRUE;
 7
 8     if(!_eventFull)
 9     {
10         // 为了避免同一事件被多次加入到事件队列中
11         for(i=_eventHead; i!=_eventTail;)
12         {
13             if(_event[i] == event)
14             {
15                 canDispatch = FALSE;
16                 break;
17             }
18
19             i++;
20             if(i >= MAX_EVENT_QUEUE)
21             {
22                 i = 0;
23             }
24         }
25
26         if(canDispatch)
27        {
28             _event[_eventTail++] = event;
29
30             if(_eventTail>= MAX_EVENT_QUEUE)
31             {
32                 _eventTail= 0;
33             }
34             if(_eventTail== _eventHead)
35             {
36                 _eventFull = TRUE;
37             }
38         }
39     }
40 }

part 4  深一步:针对与时间相关的事件

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

1 typedef struct
2 {
3     u8 type;   // 事件类别,循环事件还是延时事件
4     u16 event; // 触发的事件编号
5     u16 timer;   // 延时或周期时间计数器
6     u16 timerBackup; // 用于周期事件的时间计数备份
7 }timerEventType;
8
9 timerEventType _timerEvent[MAX_TIMER_EVENT_Q];

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

 1 void SysTickHandler(void)
 2 {
 3     ...
 4     for(i=0; i<_timerEventTail; i++)
 5     {
 6         _timerEvent[i].timer--;
 7         if(_timerEvent[i].timer == 0)
 8         {
 9             dispatchEvent(_timerEvent[i].event);// 事件触发器
10
11             if(_timerEvent[i].type == CYCLE_EVENT)
12             {
13                 // 循环事件,重新计数
14                 _timerEvent[i].timer = _timerEvent[i].timerBackup;
15             }
16             else
17             {
18                 // 延时事件,触发后删除
19                 delTimerEvent(_timerEvent[i].event);
20             }
21         }
22     }
23 }

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

 1 void addTimerEvent(u8 type, u16 event, u16 timer)
 2 {
 3      _timerEvent[_timerEventTail].type = type;
 4      _timerEvent[_timerEventTail].event = event;
 5      _timerEvent[_timerEventTail].timer = timer; // 时间单位是系统Tick间隔时间
 6      _timerEvent[_timerEventTail].timerBackup = timer; // 延时事件并不使用
 7      _timerEventTail++;
 8 }
 9
10 void delTimerEvent(u16 event)
11 {
12     ...
13     for(i=0; i<_timerEventTail; i++)
14     {
15         if(_timerEvent[i].event == event)
16         {
17             for(j=i; j<_timerEventTail; j++)
18             {
19                 _timerEvent[j] = _timerEvent[j+1];
20             }
21
22             _timerEventFull= FALSE;
23             _timerEventTail--;
24         }
25     }
26 }

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

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

时间: 2024-10-11 17:02:18

单片机的非OS的事件驱动的相关文章

(一) 这就是所谓的Node.js------单线程,非阻塞,事件驱动

Node.js 第一天笔记(V1) 一:Node.js到底是从何而来 2008年的秋天,一个名叫做Ryan Dahl(罗伊?达尔)的年轻人在玩了几年服务器编程之后,越发感到服务器高并发性能的瓶颈是一个很难逾越的问题.无论是自己擅长的Ruby on Rails,还是传统的LAMP.以及C或者Lua.都各有各的缺陷.Ruby的虚拟机太烂,C虽然性能比较高,但是天生的语言本身缺憾致使开发Web的效率低下.Lua则是已有的同步I/O问题导致无法发挥性能优势. Ryan使用了这些语言开发Web服务器几年之

并发、非阻塞、事件驱动、上下文切换、多核在现实生活中的例子!

平常生活中就有很多例子可以去研究和理解并发和多线程. 比如: 做饭有几个任务(说简单点):煮饭.炒菜 同步就是,先去煮饭,等20分钟饭煮好了再去炒菜:我想现实生活中没有人去这样做,这样效率太低. 我没必要等到把饭煮熟好了再去炒菜,可以边煮饭边炒菜,并发进行:等菜炒完了,饭也煮好了:这样节省了时间,提高了效率. 这里面我们可以看到: --并发:煮饭和炒菜是并发执行,我就是CPU(一个人单核):说明单核也能实现并发: --IO阻塞:煮饭的电饭煲是一个相对耗时的处理动作,比如是一个网络的请求 --非阻

理解同步,异步,阻塞,非阻塞,多路复用,事件驱动IO

以下是IO的一个基本过程 先理解一下用户空间和内核空间,系统为了保护内核数据,会将寻址空间分为用户空间和内核空间,32位机器为例,高1G字节作为内核空间,低3G字节作为用户空间.当用户程序读取数据的时候,会经历两个过程:磁盘到内核空间(这块消耗性能,下面简称内核数据准备),内核空间拷贝到用户空间(下面简称用户空间拷贝). 基于这个前提,同步异步IO,阻塞非阻塞IO 这几个概念其实非常类似的,区分的关键点在于被调用者的返回方式. 当我们进行IO操作的时候,如果被调用者将任务全部执行完返回,称为同步

Nginx:异步非阻塞IO

在使用socket编程中,经常会看到阻塞.非阻塞.同步.异步,那么它们之间到底有什么关系跟区别呢? 本次将那Nginx的异步非阻塞的事件驱动模型来解释一下它们之间的关系. 阻塞IO 在linux中,默认所有socket都是阻塞的. 这意味着使用该socket调用诸如recv的函数时,在没有数据到达之前,该函数将不会返回,导致线程被阻塞,直到数据到达. 非阻塞IO 我们可以使用fcntl把socket设置为非阻塞的. 这意味着使用该socket调用诸如recv的函数时,该函数将立刻返回,可以根据返

在51上写一个“OS”原型

自己在51单片机上实现任务调度器的记录过程,下面的文本内容,完整的图文文档传送到了文库.传送门 闲来无聊,便有了想写操作系统的念头.之前也用过ucso.rtt.raw-os,虽然没怎么深入应用,但对操作系统也有些认识.好奇心的驱使,终于在国庆这段时间里实现了这个“OS”.于是,便有了本文,用来记录自己实现一个OS的过程.当然,这个OS,可不像上面说的几个rtos那样,这个OS只是实现了任务调度功能,还不能算真正意义的OS,甚至编码上看起来很丑陋.由于51单片机相对简单,尽管资源上比较有限,但还是

nodejs所用的概念(同步,异步,事件驱动,事件循环等)通俗解释

1.回调:异步编程基本方法之一,当需要执行异步程序时候 ,一般采用后续传递,后续函数写入参数,逐层嵌套,使程序按期望方式走完流程2.异步: 每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的.异步的. js实现异步的方法:回调函数.事件监听.发布/订阅.Promises对象 ,有兴趣可以去阮一峰http://www.ruanyifeng.com/blog/

同步异步阻碍非阻碍

作者:严肃 链接:https://www.zhihu.com/question/19732473/answer/20851256 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. "阻塞"与"非阻塞"与"同步"与"异步"不能简单的从字面理解,提供一个从分布式系统角度的回答. 1.同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchron

[转帖]OS/2 兴 衰 史

OS/2 兴 衰 史 https://zhidao.baidu.com/question/12076254.html 最近在看windows的版本 感觉自己接触电脑太晚 知道的也是很少 不明白 之前有那么多的OS版本以及其他. 在Windows 3.x 开 始 为 世 人 接 受 之 初, 个 人 电 脑 的 操 作 系 统 中 唯 一 可 以 与Windows操 作 系 统 分 庭 抗 礼 的 就 是OS/2. 这 套 原 先 是 微 软 与IBM 合 作 生 产 的 操 作 系 统, 由 于

Python著名的lib和开发框架(均为转载)

第一,https://github.com/vinta/awesome-python Awesome Python A curated list of awesome Python frameworks, libraries, software and resources. Inspired by awesome-php. Awesome Python Admin Panels Algorithms and Design Patterns Anti-spam Asset Management A