单片机的非OS的事件驱动
eetrendMcu 2023-01-29

很多单片机项目恐怕都是没有操作系统的前后台结构,就是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[i].event == event)
{
(*_handle[i].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[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;
}
}
}
}

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

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

首先要设定系统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代码不容易实现。 

声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 【7.24 深圳】2025国际AI+IoT生态发展大会/2025全球 MCU及嵌入式技术论坛


  • 相关技术文库
  • 单片机
  • 嵌入式
  • MCU
  • STM
  • 基于C51单片机实现汽车座椅自动控制系统的软硬件设计

    引言 随着人们生活水平的提高,对汽车座椅的舒适性要求也越来越高,要求对汽车座椅地调节能够更加简单、方便、快捷。目前,汽车座椅位置的调节多采用基于手动调节方式的机械和电动控制两种方式。汽车座椅位置的调节...

    07-02
  • MCS51单片机程序设计时堆栈的计算方法解析

    用C语言进行MCS51系列单片机程序设计是单片机开发和应用的必然趋势。Keil公司的C51编译器支持经典8051和8051派生产品的版本,通称为Cx51。应该说,Cx51是C语言在MCS51单片机上的扩展,既有C语言的共性,又有它自己...

    07-02
  • 51单片机定时器工作原理及用法

    TMOD : 控制定时器的工作方式。8个bit,高四位 bit 控制 T1,、低四位 bit 控制 T0。因为定时器有4种工作方式;TMOD = 0x00(工作方式0),TMOD = 0x01(工作方式0),TMOD = 0x02(工作方式2),TMOD = 0x03(工作方式3)。...

    07-02
  • 51单片机学习单片机之路总结

    学习单片机有一学期了,现在也由51转到STM32了。一直想对51的学习做一个总结。也希望对别人有一些启发。也给后学者提供一些建议。当然本文是我对自己学习过程的总结,若有不对的地方,还请高手指出。 我想,再看本...

    07-02
  • hot51增强型单片机开发板原理图

    功能要求: 一):绿灯25s倒计时,绿灯过度红灯有5s黄灯时间,红灯25s后直接跳绿灯。 二):按键按下模拟闯红灯输入,产生5s蜂鸣器鸣叫。 开发环境: 软件:Keil uVision4 硬件:HOT51增强型单片机开发板 程序代码:...

    07-01
  • 51单片机的延时子程序

    延时程序在单片机编程中使用非常广泛,但一些读者在学习中不知道延时程序怎么编程,不知道机器周期和指令周期的区别,不知道延时程序指令的用法, ,本文就此问题从延时程序的基本概念、机器周期和指令周期的区别和联系...

    07-01
  • 什么是Flash盘?Flash盘的结构是什么样的?

    Flash是大家常使用的存储之一,对于Flash,大家或多或少有所了解。上篇文章中,小编对Flash闪存的类型有所介绍。为继续增进大家对Flash的认识,本文将对Flash盘、Flash盘结构以及Flash读写操作予以介绍。如果你对本...

    07-01
  • 深谈嵌入式系统,嵌入式系统是如何组成的?

    嵌入式系统在生活中有诸多应用,大家对于嵌入式系统或多或少有所耳闻。在前两篇文章中,小编对嵌入式系统进行过详细介绍。为继续增进大家对嵌入式系统的认识,本文将对嵌入式系统的组成加以说明。如果你对嵌入式系...

    06-27
  • 嵌入式系统秘籍共享,最全嵌入式系统解析

    嵌入式系统的应用十分广泛,因此越来越多的人学习嵌入式系统。由此,在学习嵌入式系统之前,我们应当对嵌入式系统具备一些认识。所以在本文余下部分,小编将对嵌入式系统进行全面解析。如果你对嵌入式系统具有兴趣...

    06-27
  • 51单片机超声波测距程序详解

    51单片机超声波测距程序详解 超声波四通道测距:超声波测距实现分为三大块: 其一是12864带字库的液晶驱动程序: 代码如下: /////////////////12864驱动程序/////////////////////////// //1写数据 void WriteDat...

    06-25
  • 51系列单片机的引脚图

    51系列单片机的引脚图 端子介绍 l P0.0~P0.7 P0口8位双向口线(在引脚的39~32号端子)。 l P1.0~P1.7 P1口8位双向口线(在引脚的1~8号端子)。 l P2.0~P2.7 P2口8位双向口线(在引脚的21~28号端子)。 l P3.0~P3.7 P2口8...

    06-25
  • 51单片机串口通信需要加超时中断吗?

    接收数据时,超过一定时间就算出错. 这个超时的时间是单片机自己算出的吗?超时的时间是由编程序的人定的,他定多长就多长从一段程序开始 实现电脑向 单片机发送一些数据,单片机返回Iget +数据 #include #define u...

    06-25
下载排行榜
更多
评测报告
更多
广告