当单片机遇到状态机——入门QP
共 6723字,需浏览 14分钟
·
2021-11-09 15:32
来源:技术让梦想更伟大
作者:ming_mei
前言
前些日子在微信上看到李肖遥的公众号,里面系统讲述了QP框架,我很有感触。我用QP框架很多年了,一开始是使用QM和QPC++,到后来抛弃了QM,直接使用QPC裸写程序,到后来自己写状态机框架。
可以这么说,QP框架引导了我的技术成长。我共享的博文,虽然都以QP为起点进行展开,但很多东西,都是QP官网的资料所没有的。我希望接受大家的意见、建议和批评,相信对我来说,会有更大的提升。
这一系列的博文,称为《当单片机遇上状态机》系列,暂时先规划以下几篇:
入门QP
让大家开始使用QP,消除对QP的畏难心理,建立起初步的信心。这一步非常重要。
从switch-case到框架的进化
大家很难理解,自己用switch-case实现状态机,用的好好的,干嘛要用状态机框架。这篇博文,就是为了说明,switch-case状态机,是如何一步一步进化到一个状态机框架的。我们所写的这个状态机框架,和QP之间,到底有着什么关系,有着多少差距。
QP的高阶使用和QM的使用
QM作为一个辅助工具?它的作用是什么?它是怎么生成代码的?它和QP之间是什么关系?在这一篇里,将会做详细介绍。
QP的哲学
精通QP,理解其哲学思想非常重要。它的哲学思想是什么样的?是如何体现的?
其他
后续的规划,我希望根据大家的反馈意见而定。我用状态机框架多年,难免做不到换位思考,不能照顾到初学者的感受。希望大家踊跃反馈意见。无论是赞扬还是批评,我都虚心接受。
入门QP
我们学习一个语言,或者一项技术,第一件要做的事情,就是实现一个类似于Hello world的最小程序。在单片机上,当然就是LED灯的闪烁。不说废话了,先上代码。
代码结构
代码结构,可以在Keil工程中看到,是一个QP的运行最小系统。QP版本使用的是最新的V6.9.3版本。
为了便于大家的学习,我抛弃了官方例程。官方例程有些繁琐,里面还有大量的doxygen格式的注释,对初学者不友好。与官方例程相比,能删掉的部分,全部都删掉了,只留下代码和必要中文注释,目的就是为了最大限度降低大家学习QP的入门门槛,也算是中国特色吧。这四个源码,代码未来我们程序架构的不同层次,以后所有的例程,就是以这个代码结构为基础,进行扩充。
还有一个需要说明的,第一个例程,我并没有使用QM建模工具进行LED状态机的建模和代码生成。QM工具,本质上基于模型的开发方法,是形式化开发方法之一。在软件开发中,这种方法一直饱受争议。这个世界现存的大部分软件框架,是不存在所谓代码生成工具的。目前我对QM等建模工具持保守态度,软件开发还是要回归代码本身,能利用工具,但不要依赖工具。QM工具,我认为是QP框架在营销和商业上的需求推动的。因此,在未来的教程中,我将QM的使用,放在次要位置,主要还直接编程为主,我认为这样才会给大家带来真正的提升。
这四个源码分别是:
main.c 包含了硬件的初始化、QP框架的初始化、各状态机模块(暂定称呼,严谨应叫AO模块)的构建,框架的启动等一系列流程。
bsp.c 硬件初始化,此处仅包含SysTick的初始化和SysTick中断函数。
ao_led.c LED状态机的源码。
hook.c QP框架的回调函数的实现,此处都为空函数,暂时不予实现。
evt_def.h 事件的定义。QP框架的事件定义,使用枚举实现。个人觉得,事件的定义,如果用字符串实现,更加有利于模块的解耦和对分布式的支持(这个问题可参考后续的博客《将软总线进行到底》)。QP使用枚举来定义事件,个人认为是为了降低RAM和CPU的开销。
其他
QP源码 QP接口代码 QP框架对硬件平台或者RTOS的接口源码。 MCU相关代码,包含Startup文件、CMSIS相关、固件库相关代码
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间共享全局变量的。
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开发,是运用了一些特殊技巧的。
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状态。
https://blog.csdn.net/ming_mei 请勿二次转载,否则将举报,谢谢
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ 关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看。