当单片机遇到状态机


前言

前些日子在微信上看到李肖遥的公众号,里面系统讲述了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);         // 时间基准为1msNVIC_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_offdefault: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状态。

当单片机遇到状态机 入门QP相关推荐

  1. 51单片机的新手入门前所有疑问整理

    51单片机的新手入门前所有疑问整理 ///插播一条:我自己在今年年初录制了一套还比较系统的入门单片机教程,想要的同学找我拿就行了免費的,私信我就可以哦~点我头像黑色字体加我地球呺也能领取哦.最近比较闲 ...

  2. 51单片机攻略—入门

    51单片机攻略-入门测试:点亮一个LED 一.基础知识 (一)单片机的电平特性 1.TTL电平 TTL电平信号规定,+5V等价于逻辑"1",0V等价于逻辑"0" ...

  3. 单片机零基础入门(8-4)实战:单片机动态数码管消影---附源代码

    单片机零基础入门(8-4)实战:单片机动态数码管消影 一.回顾 二.问题及原因 三.解决办法: 四.解决后的源代码: 五.补充知识:数码管驱动方式 1.单片机直接扫描: 2.专用驱动芯片: 一.回顾 ...

  4. 单片机零基础入门(9-1)实战:模块化编程(模块化两个案例含源码--以及无法显示头文件(.h)的解决方案)

    单片机零基础入门(9-1)实战:模块化编程-(以及无法显示头文件(.h)的解决方案) 本文作为单片机零基础入门(8-5)模块化编程的拓展和补充,比前面的单片机零基础入门(8-5)模块化编程更为详细. ...

  5. 1002. 电子编程入门到工程师--重拾信心-单片机一篇入门

    这一节写单片机一篇入门,拭目以待,必有惊喜!. 不入门的根本原因? 简单问题复杂化, 复杂问题简单化, 头脑迷糊没框架, 无头无尾似散沙. 劝退的单片机框架: 我们的单片机框架: 现在个人.工业化领域 ...

  6. 经验分享之二:单片机懵懂的入门

    单片机懵懂的入门 作者背景: 工作2年,辞职闲荡的菜鸟电子工程师. 微博:weibo.com/zhoupeng7d 邮箱:zhoupeng6d@qq.com 之所以要写一些自己关于单片机学习经验的分享 ...

  7. 51单片机c语言定时器程序,51单片机定时器设置入门与程序解析

    89C51单片机有两个计数器T0和T1,每个计数器都是由两个8位的RAM 单元组成的,即每个计数器都是16 位的计数器,最大的计数容量是216=65536,记住是从0-65535. 其次.提供给定时器 ...

  8. QP状态机学习①——QP状态机架构

    QP是一个实时嵌入式框架(RTEF),事件驱动的实时内核(RTOS内核)以及一套基于主机的互补工具相结合的,可实现事件驱动的体系结构,并且基于模型设计可以自动代码生成. QP-bundle,是QM建模 ...

  9. wxpython视频教程-单片机C语言入门_单片机教程_单片机编程语言 - C语言网

    本教程由 继承叔 编写,面向对单片机有基础认知的初学者! 1.单片机了解 单片机是一块集成电路的控制芯片,我们熟知的家电例如洗衣机的定时控制和滚筒运作,电饭煲的保温功能和鸣叫提醒,电冰箱,空调等等这些 ...

最新文章

  1. 上海大学建了一个“突发事件语料库”,包括地震、恐怖袭击等5大类
  2. 使用JVisualVM远程监控Tomcat
  3. 【Python】Python字符串格式化问题:%、format()与f-strings
  4. python编写arcgis脚本教程_ArcGIS使用Python脚本工具
  5. Xftp5如何设置默认的文件夹
  6. 第十八章——基于策略的管理(1)——评估数据库属性
  7. Linux操作系统中ps命令常见用法
  8. Python遗传算法工具箱的使用(二)求解最短路径问题
  9. c语言bim的题目,BIM考试|BIM技术概论考试技术习题练习10
  10. python如何求p值_在python中计算F分布p值?
  11. Linux——RAID磁盘阵列及配置
  12. 计算机开机后无法网络拨号怎样处理,拨号上网时电脑假死的原因和解决方法
  13. Non-OK-status: Status(error::Code::INVALID_ARGUMENT, “Unsupported data format“) status: Invalid argu
  14. opencv把图片转换成二进制_Python+OpenCV实现将图像转换为二进制格式
  15. 2021 最新 Cloudera 大数据平台 CDP 升级指南白皮书完整版开放下载!(中文版)...
  16. Python之统计作图函数讲解(三)
  17. 我与MVVM的恩怨情仇
  18. opencv3/C++ mixChannels()详解:4通道图像分割、HSV通道获取
  19. 电脑上不显示WLAN,无法连接WIFI!
  20. python正则表达式 - 30 正则表达式的基本用法

热门文章

  1. 10、JSP快速入门
  2. c语言中的strtok函数,C中的strtok函数出错
  3. postgresql 客户端导入csv文件命令格式
  4. DotNetBar 学习笔记
  5. 2022-2028全球军用车载电源行业调研及趋势分析报告
  6. 企业信息化规划时应考虑到哪些关键因素
  7. 牛散村:Java字节码编程javassist的详细介绍
  8. java8 对象转map时重复key Duplicate key 该如何解决?
  9. [SuperMap软件教程] 如何使用超图将线转面
  10. 【2023年第十一届泰迪杯数据挖掘挑战赛】A题:新冠疫情防控数据的分析 建模方案及python代码详解