完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
前言
前些日子在微信上看到李肖遥的公众号,里面系统讲述了QP框架,我很有感触。我用QP框架很多年了,一开始是使用QM和QPC++,到后来抛弃了QM,直接使用QPC裸写程序,到后来自己写状态机框架。可以这么说,QP框架引导了我的技术成长。我共享的博文,虽然都以QP为起点进行展开,但很多东西,都是QP官网的资料所没有的。我希望接受大家的意见、建议和批评,相信对我来说,会有更大的提升。 这一系列的博文,称为《当单片机遇上状态机》系列,暂时先规划以下几篇。
我们学习一个语言,或者一项技术,第一件要做的事情,就是实现一个类似于Hello world的最小程序。在单片机上,当然就是LED灯的闪烁。不说废话了,先上代码。 代码结构 源码我已经开源在gitee(qpstudy)。代码结构,可以在Keil工程中看到,是一个QP的运行最小系统。QP版本使用的是最新的V6.9.3版本。 为了便于大家的学习,我抛弃了官方例程。官方例程有些繁琐,里面还有大量的doxygen格式的注释,对初学者不友好。与官方例程相比,能删掉的部分,全部都删掉了,只留下代码和必要中文注释,目的就是为了最大限度降低大家学习QP的入门门槛,也算是中国特色吧。这四个源码,代码未来我们程序架构的不同层次,以后所有的例程,就是以这个代码结构为基础,进行扩充。 还有一个需要说明的,第一个例程,我并没有使用QM建模工具进行LED状态机的建模和代码生成。QM工具,本质上基于模型的开发方法,是形式化开发方法之一。在软件开发中,这种方法一直饱受争议。这个世界现存的大部分软件框架,是不存在所谓代码生成工具的。目前我对QM等建模工具持保守态度,软件开发还是要回归代码本身,能利用工具,但不要依赖工具。QM工具,我认为是QP框架在营销和商业上的需求推动的。因此,在未来的教程中,我将QM的使用,放在次要位置,主要还直接编程为主,我认为这样才会给大家带来真正的提升。 这四个源码分别是:
QP的启动流程 以下代码就是QP框架的启动过程。 #include "qpc.h" // qpc框架头文件 #include "evt_def.h" // 事件定义头文件 #include "bsp.h" // 硬件初始化 #include "ao_led.h" // LED状态机 Q_DEFINE_THIS_MODULE("Main") // 定义当前的模块名称,此名称在QS和断言中会使用。 ao_led_t led; // 状态机LED对象 int main(void) { static QSubscrList sub_sto[MAX_PUB_SIG]; // 定义订阅缓冲区 static QF_MPOOL_EL(m_evt_t) sml_pool_sto[128]; // 定义事件池 QF_init(); // 状态机框架初始化 QF_psInit(sub_sto, Q_DIM(sub_sto)); // 发布-订阅缓冲区的初始化 QF_poolInit(sml_pool_sto, // 事件池的初始化 sizeof(sml_pool_sto), sizeof(sml_pool_sto[0])); ao_led_ctor(&led); // 状态机的构建 return QF_run(); // 框架启动 } QP的回调函数 通常的调用,都是上层函数调用底层函数。如果使用了某个函数,需要上层实现,这样就产生了底层对上层函数的调用,称为回调函数(Call back),也叫钩子函数(Hook)。 一般而言,回调函数,主要用于顶层功能在底层模块里的插入,或者实现底层模块的定制功能。QP框架定义四个回调函数,需要QP的使用者来实现。 void QF_onStartup(void) { bsp_init(); // 硬件初始化 } void QF_onCleanup(void) {} void QV_onIdle(void) {} void Q_onAssert(char_t const * const module, int_t const loc) { (void)module; (void)loc; while (1); } QF_onStartup是用于QP框架启动时,所调用的回调函数。一般可以执行一些初始化工作,比如硬件初始化,内存初始化。这也就是为什么在main函数中没有看到硬件初始化的原因。 QF_onCleanup与RTOS相关,暂时用不到。 QV_onIdle是QP框架空闲时,也就是没有任何事件产生时,所执行的函数。 Q_onAssert是QP的断言的实现。断言,是程序一种检查机制,当程序的执行发生异常时,用于检查不可能发生情况。比如下面的函数,当函数func_add的两个参数,都不可能大于或者等于100时,就可以对使用断言进行检查,以防御可能出现的参数输入错误。这种编程方式,也叫做防御式编程。防御式编程的思想就是,若崩溃,就崩溃的更猛烈些,以便在编程的早期,就发现程序错误,并强迫开发者解决掉。具体可以参考后续的博文《谈防御式编程》。 int func_add(int x, int y) { Q_ASSERT(x < 100); Q_ASSERT(y < 100); return (x + y); } 系统嘀嗒 在当前的历程中,使用一个QP中自带的协作式内核QV。在使用了QV内核的前提下,SysTick只有一个作用,那就是为时间事件提供时间基准。 #include "bsp.h" #include "stm32f10x.h" #include "qpc.h" void bsp_init(void) { SysTick_Config(SystemCoreClock / 1000); // 时间基准为1ms NVIC_SetPriority(SysTick_IRQn, 0); // 设置中断优先级 } void SysTick_Handler(void) { QF_TICK_X(0U, &l_SysTick_Handler); // 时间基准 } 如果大家需要换一个芯片跑这个例程,那么仅仅需要更换Keil RTE中的Deivce和这里的代码即可。只有这里的代码是硬件相关的。以后大家写程序,也是一样,要执行硬件相关最小原则,也就是说,要把硬件相关的代码压缩到最低。后续也会有博文专门讲这个话题(《将设备抽象进行到底 驱动篇》)。 LED状态机 LED状态机是核心功能,学会了这个,就入门了QP。在QP中,AO(Active Object)是核心,QP的所有功能都是围绕AO展开的,就好比在RTOS中任务是核心一样。AO之间,纯粹靠事件进行通信,原则上是不允许AO间共享全局变量的(详细请参考后续《当单片机遇上并发 Actor篇》)。 LED状态机的类定义 下面是头文件的定义。头文件中,主要定义了LED状态机类,并声明了类方法。这里所说的类,是在逻辑上的类。在C语言中,没有类的概念,只能使用结构体替代类的实现。 #include "qpc.h" #define AO_LED_QUEUE_LENGTH 32 // LED类的定义 typedef struct ao_led_tag{ QActive super; // 对QActive类的继承 QEvt const *evt_queue[AO_LED_QUEUE_LENGTH]; // 事件队列 QTimeEvt timeEvt; // 延时事件 bool status; // LED状态 } ao_led_t; // LED的类方法 构造函数 void ao_led_ctor(ao_led_t * const me); LED状态机是完全按照C语言面向对象的方法实现的。在C语言中,由于在语言层面并没有对面向对象进行支持,因此面向对象的C开发,是运用了一些特殊技巧的。这些技巧,我们会在后续(《将面向对象进行到底 C语言篇》)进行详细介绍。目前,为了增强大家入门的信心,我只说与QP入门相关的东西。 QActive类,简单说就是状态机类。在定义一个状态机对象时,需要从QActive类进行继承。 LED状态机类的实现 LED状态机类的实现,共分为两个部分,一是类方法的实现,二是类状态的实现。 这里只有一个类方法,那就是LED类的构造函数。构造函数,是C++中的概念,C语言中并没有这个概念,这里与类相似,仍然是构造功能的模拟。从代码可以看出,构造函数有几个内容,一个必须的步骤,就是活动对象的构造和启动。构造函数中的另一个内容,就是初始化一个时间事件的对象,因为每500ms要发送一个Evt_Time_500ms事件。 // 活动对象(AO,Active Object)LED的构建 void ao_led_ctor(ao_led_t * const me) { // LED对象的变量初始化 me->status = false; // 活动对象的构建 QActive_ctor(&me->super, Q_STATE_CAST(&state_init)); // 时间对象的构建 QTimeEvt_ctorX(&me->timeEvt, &me->super, Evt_Time_500ms, 0U); // 活动对象的启动 QACTIVE_START( &me->super, 1, // 优先级 me->evt_queue, // 事件队列 AO_LED_QUEUE_LENGTH, // 事件队列深度 (void *)0, // 任务栈,RTOS相关,可忽略 0U, // 任务栈深度,RTOS相关,可忽略 (QEvt *)0); } LED状态类有三个状态,初始状态,ON状态和OFF状态。 初始状态 所有的初始状态都是一样的,就是先订阅状态机运行所需要的事件。然后直接跳转到某个特定的状态。实际上,事件的订阅,不一定要在初始状态里执行。在状态机运行时,随时都能订阅事件,或者解除对事件的订阅。 这个事件的订阅机制,就是在软件设计模式中,大名鼎鼎的发布-订阅模式(可参考后续的博文《当单片机遇上设计模式 发布-订阅模式》)。发布-订阅模式的最大好处,就是模块间的彻底解耦。这里插入一个程序设计原则,好的程序,一定是解耦良好的程序。所谓耦合,就是模块A变了,模块B也得跟着变,否则,B模块会运行不正常,模块之间有依赖;所谓解耦,就是去除模块之间的依赖,模块A变了,模块B无须改变。 // 初始状态 static QState state_init(ao_led_t * const me, void const * const par) { // 事件Evt_Time_500ms的订阅 QActive_subscribe(&me->super, Evt_Time_500ms); return Q_TRAN(&state_on); } ON状态 参数的传输 从代码中,可以看到,当产生事件时,框架会自动调用state_on函数,led对象,是通过参数me传进来的,这个me指针,相当于C++里的this指针,而所产生的事件,是通过参数e传输进来的。 事件的处理 大家注意到代码里有三个事件Q_ENTRY_SIG、Q_EXIT_SIG和Evt_Time_500ms。其中前两个是系统事件,也就是QP框架默认支持的事件。Q_ENTRY_SIG是状态进入事件,当进入一个状态时,QP框架会默认执行这个事件。Q_EXIT_SIG是状态退出事件,当退出一个状态时,QP框架也会默认执行这个事件。Evt_Time_500ms是用户事件,也就是我们自己定义的事件。Q_ENTRY_SIG和Q_EXIT_SIG并不强制定义,而我们要根据自己的需要,看在进入或者退出一个状态时,是否有动作执行,来决定是否对这两个系统事件进行实现。QP还有一个系统事件,Q_INIT_SIG,这个和层次化状态机相关,以后再讨论。 事件后的返回值 大家注意到每个状态机在不同的case分支下,都有不同的返回值,比如Q_HANDLED(),Q_TRAN(&state_off)或者Q_SUPER(&QHsm_top)。 之所以有这些返回值的不同,是为了在处理完毕一个事件后,告诉框架,下一步要干什么。Q_SUPER(&QHsm_top)告诉框架此事件被忽略,什么也不处理;Q_HANDLED()告诉框架,此事件已经处理;而Q_TRAN(&state_off)告诉框架,需要跳转到state_off状态,框架这时会执行当前状态的退出事件和下一个状态的进入事件。 QP框架的技术约束 无论是事件处理的机制,还是返回值的格式,都是QP框架的技术约束。任何一个软件框架,在带来编程便利的同时,也会带来性能上的开销和技术的约束。我们要使用一个框架,也就要遵守它制定的技术约束,否则框架就没有办法有效的运行。 // LED的on状态 static QState state_on(ao_led_t * const me, QEvt const * const e) { switch (e->sig) { case Q_ENTRY_SIG: // 状态的进入事件 me->status = true; // 打开LED灯 QTimeEvt_armX(&me->timeEvt, 500, 0U); // 500ms后发送时间事件 return Q_HANDLED(); // 通知框架,事件已处理 case Q_EXIT_SIG: // 状态的退出事件 QTimeEvt_disarm(&me->timeEvt); return Q_HANDLED(); case Evt_Time_500ms: return Q_TRAN(&state_off); // 通知框架,状态转移至state_off default: return Q_SUPER(&QHsm_top); // 其他事件,在此时不处理 } } // LED的Off状态 static QState state_off(ao_led_t * const me, QEvt const * const e) { switch (e->sig) { case Q_ENTRY_SIG: me->status = false; // 关闭LED灯 QTimeEvt_armX(&me->timeEvt, 500, 0U); return Q_HANDLED(); case Q_EXIT_SIG: QTimeEvt_disarm(&me->timeEvt); return Q_HANDLED(); case Evt_Time_500ms: return Q_TRAN(&state_on); default: return Q_SUPER(&QHsm_top); // 其他事件,在此时不处理 } } OFF状态 与ON状态一样,不再赘述。有人可以会提出疑问,在收到Evt_Time_500ms事件的时候,让LED的状态翻转,不必跳转到OFF状态,不就节约了一个状态吗?的确,这样写的确更简练,但我们的目的是为了展示状态机的使用,因此可以增加了一个OFF状态。 |
|
|
|
只有小组成员才能发言,加入小组>>
3280 浏览 9 评论
2958 浏览 16 评论
3460 浏览 1 评论
9004 浏览 16 评论
4052 浏览 18 评论
1115浏览 3评论
573浏览 2评论
const uint16_t Tab[10]={0}; const uint16_t *p; p = Tab;//报错是怎么回事?
571浏览 2评论
用NUC131单片机UART3作为打印口,但printf没有输出东西是什么原因?
2303浏览 2评论
NUC980DK61YC启动随机性出现Err-DDR是为什么?
1859浏览 2评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-25 14:05 , Processed in 1.183998 second(s), Total 79, Slave 59 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号