ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32上QP框架实战:事件驱动与层次式状态机移植指南

STM32上QP框架实战:事件驱动与层次式状态机移植指南 1. 为什么要在STM32上折腾QP框架第一次接触QP框架是在做一个多路传感器采集的项目上当时用裸机的前后台架构写了一个状态机代码里全是switch-case嵌套一个按键短按、长按、双击的逻辑就写了三百多行后来加了个低功耗模式整个逻辑直接崩了改一处坏三处。那段时间我就在想有没有一种方式能让嵌入式代码像PC端那样用“事件”来驱动而不是靠一堆标志位和轮询硬撑。QP框架就是干这个的。它是一套轻量级的、专门为嵌入式设计的事件驱动框架核心思想是把每个独立的功能模块抽象成一个活动对象每个活动对象内部维护一个层次式状态机对象之间通过事件进行异步通信。听起来有点抽象我换个说法你可以把它理解成一个微型的“操作系统”但它不调度任务而是调度事件。每个活动对象就像一个独立的小程序平时处于某个状态收到事件后根据当前状态决定怎么响应响应完继续等下一个事件。这套东西在STM32上跑非常合适。STM32的RAM和Flash虽然比不上PC但QP框架的内核非常小裁剪之后ROM占用大概几KBRAM占用取决于你创建多少个活动对象和事件池的大小。我用STM32F103C8T664KB Flash20KB RAM跑过三个活动对象的小项目内核加应用总共占了不到12KB FlashRAM用了大概3KB剩下的资源完全够用。这篇文章适合谁看如果你已经会用STM32点灯、串口收发、定时器中断但每次写稍微复杂一点的逻辑就觉得代码乱成一团那这篇内容就是给你准备的。我会从零开始把QP框架的移植、活动对象的创建、状态机的编写、事件的发送和接收全部走一遍代码直接给全你拿去就能编译。注意QP框架有QP/C、QP/C、QP-nano几个版本STM32上常用的是QP/C本文所有内容基于QP/C 6.x版本用Keil MDK和STM32CubeMX生成的HAL库工程作为基础。2. QP框架的核心概念拆解2.1 活动对象到底是个什么东西活动对象是QP框架里最基本的执行单元。一个活动对象包含三样东西一个状态机、一个事件队列、一个优先级。状态机决定了它收到事件后怎么反应事件队列用来缓存还没处理的事件优先级决定了多个活动对象同时有事件时谁先处理。我刚开始学的时候总觉得活动对象和RTOS里的任务很像后来发现区别很大。RTOS的任务是一个死循环里面通常有阻塞式的延时或等待而活动对象没有自己的栈它就是一个状态机函数事件来了就调用一次处理完就返回。这意味着活动对象的RAM开销极小不需要为每个对象分配独立的栈空间。创建活动对象的代码大概长这样typedef struct { QActive super; /* 继承QP的活动对象基类 */ uint8_t sensorValue; /* 自己的成员变量 */ } SensorAO; static SensorAO l_sensorAO; /* 静态分配避免动态内存 */ QActive * const AO_Sensor l_sensorAO.super;这里QActive是QP提供的基类里面包含了事件队列、优先级等基础设施。你只需要在结构体里加上自己需要的成员变量就行。静态分配是我强烈推荐的方式嵌入式里能不用malloc就不用避免内存碎片。2.2 层次式状态机为什么比switch-case好用层次式状态机是QP框架的另一个核心。普通的有限状态机每个状态是平级的状态之间的转换全靠switch-case里的条件判断。层次式状态机允许状态之间有“父子”关系子状态可以继承父状态的行为。举个例子一个电机控制的状态机有“停止”、“运行”、“故障”三个顶层状态。“运行”下面又分“正转”、“反转”、“加速”、“减速”四个子状态。如果用普通状态机从“正转”切到“停止”需要写一遍停止逻辑从“反转”切到“停止”又要写一遍。用层次式状态机你只需要在“运行”这个父状态里写一次停止逻辑所有子状态都能继承。QP里写状态机的语法是这样的QState Motor_run(MotorAO * const me, QEvt const * const e) { switch (e-sig) { case Q_ENTRY_SIG: { /* 进入运行状态时的动作 */ return Q_HANDLED(); } case Q_EXIT_SIG: { /* 退出运行状态时的动作 */ return Q_HANDLED(); } case STOP_SIG: { /* 收到停止事件切换到停止状态 */ return Q_TRAN(Motor_stop); } } return Q_SUPER(QHsm_top); /* 父状态是顶层 */ }Q_ENTRY_SIG和Q_EXIT_SIG是QP内置的进入和退出信号不需要你自己定义。Q_TRAN表示状态转换Q_SUPER指定父状态。这种写法比一堆if-else清晰太多了而且状态之间的层次关系一目了然。2.3 事件驱动和轮询的本质区别轮询是“我每隔一段时间去看看有没有事情要做”事件驱动是“事情来了会通知我”。在裸机开发里轮询通常放在主循环里比如while(1) { if (flag) { ... } }。这种方式的问题是如果逻辑复杂了主循环会变得非常臃肿而且响应时间取决于循环周期。事件驱动的方式是中断服务程序或者定时器回调里只负责“发送事件”具体怎么处理由活动对象的状态机决定。这样中断里做的事情极少不会阻塞其他中断系统的实时性更好。QP里发送事件的代码/* 在定时器中断里发送一个周期事件 */ static QEvt const tickEvt QEVT_INITIALIZER(TICK_SIG); QACTIVE_POST(AO_Sensor, tickEvt, 0);QACTIVE_POST把事件投递到活动对象的事件队列里如果队列满了会返回失败。最后一个参数是发送者指针不需要的话传0就行。提示QP的事件可以是静态分配的全局const变量也可以是动态分配的从事件池里取。静态事件适合周期性的、内容不变的事件动态事件适合携带数据的、一次性的事件。3. 在STM32上移植QP框架的完整步骤3.1 准备工作工程模板和源码获取我用的工程模板是STM32CubeMX生成的芯片选STM32F103C8T6时钟配置成72MHz开启了USART1用于打印调试信息开启了TIM2用于产生周期事件。HAL库版本是1.8.xKeil MDK版本是5.36。QP/C的源码可以从官网下载下载下来是一个压缩包解压后主要关注这几个文件夹qpc/inc头文件目录qpc/src源文件目录qpc/ports移植层目录里面有各种编译器和平台的移植文件STM32用的是ARM Cortex-M内核所以移植层选qpc/ports/arm-cm下面的文件。具体来说需要这几个文件qk_port.cQK内核的移植文件QK是QP的一种内核支持抢占式调度qv_port.cQV内核的移植文件QV是另一种内核只支持合作式调度qassert.h断言相关的宏定义我一般用QV内核因为它的逻辑更简单不需要在中断里做复杂的上下文切换适合大多数STM32项目。QK内核适合对实时性要求极高的场景但移植起来稍微麻烦一点。3.2 把QP源码加入Keil工程在Keil里新建几个分组把QP的源文件加进去。我习惯这样分组QP/inc存放QP的头文件QP/src存放QP的核心源文件包括qep.c、qf.c、qfsm.c、qhc.c等QP/port存放移植层的文件比如qv_port.c然后在Keil的“Options for Target”里把qpc/inc和qpc/ports/arm-cm/qv/gnu或者iar、keil取决于你的编译器添加到Include Paths里。这里有个坑要注意QP的源码里有一些文件是互斥的比如qk.c和qv.c不能同时编译qep.c和qepn.c也不能同时编译。我一开始没注意把所有源文件都加进去了结果编译报了一堆重复定义的错误。后来仔细看了QP的文档才知道要根据选择的内核来添加文件。用QV内核的话需要添加的源文件是qep.c层次式状态机引擎qf.c框架核心事件队列、活动对象管理qv.cQV内核qfsm.c有限状态机如果不用层次式状态机可以不添加qhc.c层次式状态机的辅助函数移植层只需要添加qv_port.c。3.3 配置QP的编译选项QP有一个配置文件叫qf_port.h里面定义了很多编译时的选项比如事件队列的最大长度、活动对象的最大数量、是否支持动态事件分配等。这个文件需要你自己创建一个放在你的工程目录里然后在Keil的Include Paths里把这个目录也加进去。我的qf_port.h大概长这样#ifndef QF_PORT_H #define QF_PORT_H #define QF_MAX_ACTIVE 8 /* 最多8个活动对象 */ #define QF_EVENT_SIZ_SIZE 2 /* 事件池大小用2字节表示 */ #define QF_EQUEUE_CTR_SIZE 1 /* 事件队列计数器用1字节 */ #define QF_MPOOL_SIZ_SIZE 2 /* 内存池大小用2字节 */ #define QF_MPOOL_CTR_SIZE 2 /* 内存池计数器用2字节 */ #define QF_TIMEEVT_CTR_SIZE 2 /* 时间事件计数器用2字节 */ #define QF_ISR_NESTING 1 /* 允许中断嵌套 */ #endif这些宏定义决定了QP内部数据结构的位宽根据你的项目规模来调整。活动对象不多的话QF_MAX_ACTIVE设成8就够了设大了浪费RAM。3.4 编写板级支持包QP需要一个“板级支持包”来和硬件打交道主要做两件事一是提供系统时钟节拍二是提供临界区保护。系统时钟节拍我用的是SysTick在SysTick_Handler里调用QF_TICK()void SysTick_Handler(void) { HAL_IncTick(); QF_TICK(); /* 通知QP框架一个时钟节拍过去了 */ }临界区保护需要实现两个宏QF_INT_DISABLE()和QF_INT_ENABLE()。在Cortex-M上可以用__disable_irq()和__enable_irq()也可以用PRIMASK寄存器操作。我一般用CMSIS提供的内联函数#define QF_INT_DISABLE() __disable_irq() #define QF_INT_ENABLE() __enable_irq()这两个宏定义在qf_port.h里或者单独放在一个头文件里。注意QP在进入临界区时会保存当前的中断状态退出时恢复。如果你在中断里调用QP的API要确保中断优先级配置正确避免在临界区里被高优先级中断打断导致状态不一致。4. 创建第一个活动对象和状态机4.1 定义事件信号事件信号是活动对象之间通信的“语言”。每个事件都有一个信号值通常用枚举来定义enum { TICK_SIG Q_USER_SIG, /* 从Q_USER_SIG开始避免和QP内置信号冲突 */ BUTTON_PRESS_SIG, SENSOR_DATA_SIG, MAX_SIG };Q_USER_SIG是QP定义的第一个用户信号值所有自定义信号都从它开始往后排。MAX_SIG用来标记信号的最大值方便调试时检查信号是否合法。如果需要事件携带数据可以定义一个继承自QEvt的结构体typedef struct { QEvt super; /* 继承QEvt基类 */ uint16_t value; /* 传感器数据 */ uint8_t channel; /* 通道号 */ } SensorEvt;发送带数据的事件时需要从事件池里动态分配SensorEvt *e Q_NEW(SensorEvt, SENSOR_DATA_SIG); e-value 1234; e-channel 1; QACTIVE_POST(AO_Display, e-super, 0);Q_NEW宏从事件池里分配一块内存并自动设置信号值。事件池的大小在qf_port.h里通过QF_MPOOL_SIZ_SIZE等宏来配置。4.2 编写状态机函数状态机函数是活动对象的核心。我以一个简单的“按键控制LED”为例按键按下时LED翻转长按3秒进入低功耗模式。/* LED活动对象的结构体 */ typedef struct { QActive super; uint8_t ledState; uint16_t pressCount; } LedAO; static LedAO l_ledAO; /* 状态机的前向声明 */ static QState Led_initial(LedAO * const me, QEvt const * const e); static QState Led_on(LedAO * const me, QEvt const * const e); static QState Led_off(LedAO * const me, QEvt const * const e); static QState Led_lowPower(LedAO * const me, QEvt const * const e);Led_initial是初始伪状态活动对象启动时会先进入这个状态然后立即转换到真正的初始状态。这是QP的约定每个状态机都必须有一个初始伪状态。static QState Led_initial(LedAO * const me, QEvt const * const e) { return Q_TRAN(Led_off); /* 初始状态是LED灭 */ } static QState Led_off(LedAO * const me, QEvt const * const e) { switch (e-sig) { case Q_ENTRY_SIG: { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); me-ledState 0; return Q_HANDLED(); } case BUTTON_PRESS_SIG: { return Q_TRAN(Led_on); } } return Q_SUPER(QHsm_top); } static QState Led_on(LedAO * const me, QEvt const * const e) { switch (e-sig) { case Q_ENTRY_SIG: { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); me-ledState 1; me-pressCount 0; return Q_HANDLED(); } case TICK_SIG: { me-pressCount; if (me-pressCount 300) { /* 3秒假设10ms一个tick */ return Q_TRAN(Led_lowPower); } return Q_HANDLED(); } case BUTTON_PRESS_SIG: { return Q_TRAN(Led_off); } } return Q_SUPER(QHsm_top); } static QState Led_lowPower(LedAO * const me, QEvt const * const e) { switch (e-sig) { case Q_ENTRY_SIG: { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); /* 进入低功耗模式关闭外设时钟等 */ return Q_HANDLED(); } case BUTTON_PRESS_SIG: { return Q_TRAN(Led_off); } } return Q_SUPER(QHsm_top); }这段代码里Led_off和Led_on是平级状态Led_lowPower也是平级状态。TICK_SIG事件每10ms发送一次在Led_on状态里累加计数超过300次就切换到低功耗状态。4.3 初始化和启动活动对象活动对象写好了需要在main函数里初始化并启动int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_TIM2_Init(); /* 初始化QP框架 */ QF_init(); /* 初始化活动对象 */ LedAO_ctor(l_ledAO); QACTIVE_START(AO_Led, 1, 0, 0, 0); /* 启动定时器每10ms产生一个中断 */ HAL_TIM_Base_Start_IT(htim2); /* 进入QP的事件循环 */ return QF_run(); }QF_init()初始化框架内部的数据结构。LedAO_ctor()是活动对象的构造函数里面会调用QActive_ctor()来初始化基类并设置初始状态机。QACTIVE_START启动活动对象第一个参数是活动对象指针第二个参数是优先级数字越小优先级越高后面三个参数是事件队列的存储区和长度传0表示使用QP内部的默认队列。QF_run()是QP的事件循环它永远不会返回。在这个循环里QP会检查所有活动对象的事件队列按照优先级顺序分发事件。提示QACTIVE_START的优先级参数数字越小优先级越高。QP的调度器是严格按照优先级来分发的高优先级活动对象的事件队列不为空时低优先级活动对象的事件不会被处理。5. 事件发送与接收的实战细节5.1 从定时器中断发送周期事件定时器中断是嵌入式系统里最常用的事件源。在STM32的HAL库里定时器中断的回调函数是HAL_TIM_PeriodElapsedCallbackvoid HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { static QEvt const tickEvt QEVT_INITIALIZER(TICK_SIG); QACTIVE_POST(AO_Led, tickEvt, 0); } }这里用了一个静态的QEvt变量因为TICK_SIG事件不携带数据每次发送的内容都一样没必要动态分配。QACTIVE_POST的第三个参数是发送者指针传0表示不关心发送者。有个细节要注意QACTIVE_POST在中断里调用是安全的QP内部会做临界区保护。但如果事件队列满了QACTIVE_POST会返回false事件会被丢弃。对于周期事件丢一两个通常没关系但如果你的逻辑依赖每个tick就需要把队列长度设大一点或者在队列满时做特殊处理。5.2 从GPIO中断发送按键事件按键中断的处理和定时器类似但需要做消抖。我一般不在中断里做消抖而是发送一个事件在活动对象的状态机里用软件定时器做消抖void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { static QEvt const pressEvt QEVT_INITIALIZER(BUTTON_PRESS_SIG); QACTIVE_POST(AO_Led, pressEvt, 0); } }在状态机里收到BUTTON_PRESS_SIG后不立即响应而是启动一个一次性定时器20ms后再检查按键状态。QP提供了QTimeEvt来实现软件定时器static QTimeEvt l_debounceTimeEvt; /* 在构造函数里初始化 */ QTimeEvt_ctorX(l_debounceTimeEvt, AO_Led, DEBOUNCE_SIG, 0); /* 在状态机里启动定时器 */ case BUTTON_PRESS_SIG: { QTimeEvt_armX(l_debounceTimeEvt, 2, 0); /* 2个tick后触发不重复 */ return Q_HANDLED(); } case DEBOUNCE_SIG: { /* 检查按键是否仍然按下 */ if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { return Q_TRAN(Led_on); } return Q_HANDLED(); }QTimeEvt_armX的第二个参数是延时多少个tick第三个参数是重复周期传0表示只触发一次。DEBOUNCE_SIG是自定义的信号需要在信号枚举里定义。5.3 活动对象之间的通信活动对象之间不能直接调用对方的函数只能通过发送事件来通信。这是QP框架的核心原则目的是解耦。比如一个传感器采集活动对象采集到数据后发送给显示活动对象/* 在传感器活动对象的状态机里 */ case SENSOR_READ_SIG: { uint16_t value read_sensor(); SensorEvt *e Q_NEW(SensorEvt, SENSOR_DATA_SIG); e-value value; e-channel 1; QACTIVE_POST(AO_Display, e-super, me); return Q_HANDLED(); }Q_NEW从事件池里分配内存如果事件池空了会触发断言。事件池的大小在qf_port.h里配置我一般设成10到20个事件的大小具体取决于系统的并发程度。接收方在状态机里处理case SENSOR_DATA_SIG: { SensorEvt *e (SensorEvt *)e; char buf[32]; sprintf(buf, CH%d: %d\n, e-channel, e-value); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); return Q_HANDLED(); }注意这里把QEvt const *强制转换成了SensorEvt *因为事件池分配的内存是可写的。如果事件是静态分配的就不能这样转换只能读取。注意动态分配的事件在使用完毕后QP会自动回收吗不会。QP不会自动回收事件内存你需要手动调用QF_gc()来回收。或者如果你确定事件不再被使用可以在处理完后调用QF_gc(e)。我一般是在事件池快满的时候调用QF_gc()或者在空闲循环里定期调用。6. 常见问题与排查技巧实录6.1 编译报错“undefined symbol QF_init”这个问题通常是因为QP的源文件没有全部加入工程或者Include Paths没有配置正确。检查一下qpc/src目录下的qf.c、qep.c、qv.c是否都加入了工程以及qpc/inc目录是否在Include Paths里。还有一个可能的原因是你用的QP版本和移植层文件不匹配。比如QP 6.x的qf_port.h里需要定义QF_MAX_ACTIVE等宏而QP 5.x的宏定义方式不同。建议从官网下载最新版本的QP并仔细阅读ports目录下的说明文件。6.2 活动对象收不到事件如果活动对象启动后收不到事件先检查这几个地方第一QACTIVE_START的优先级参数是否合法。QP的优先级范围是1到QF_MAX_ACTIVE传0或者超过最大值都会导致断言失败。如果你在qf_port.h里把QF_MAX_ACTIVE设成了8优先级就只能填1到8。第二事件队列的长度是否够用。QACTIVE_START的第四个参数是队列存储区第五个参数是队列长度。如果传0QP会使用默认的队列长度默认值在qf_port.h里通过QF_EQUEUE_CTR_SIZE等宏来配置。如果队列太短事件会被丢弃。第三发送事件的代码是否真的执行到了。可以在发送事件的地方加一个GPIO翻转用示波器或者逻辑分析仪看一下。我遇到过一个问题定时器中断没有正确配置中断根本没触发导致事件一直没发送。6.3 状态机进入错误状态层次式状态机的转换逻辑比较复杂如果状态转换写错了可能会进入一个意料之外的状态。QP提供了一个调试工具叫QSpy可以通过串口输出状态机的运行轨迹。不过QSpy的配置稍微麻烦一点我一般用简单粗暴的方法在每个状态的Q_ENTRY_SIG里加一句串口打印。case Q_ENTRY_SIG: { printf(Enter Led_on\n); return Q_HANDLED(); }这样从串口输出就能看到状态机的运行轨迹非常直观。等调试完了再把打印去掉。还有一个常见问题是状态转换后没有返回Q_HANDLED()而是返回了Q_SUPER()。Q_TRAN宏本身会返回一个状态指针不需要再包一层return。正确的写法是return Q_TRAN(Led_on);而不是Q_TRAN(Led_on); return Q_HANDLED();。6.4 事件池耗尽导致断言失败如果系统运行一段时间后触发断言提示事件池为空说明动态事件分配得太多了或者回收不及时。解决方法有几个一是增大事件池的大小。在qf_port.h里事件池的大小由QF_MPOOL_SIZ_SIZE和QF_MPOOL_CTR_SIZE决定但实际的事件池数组是在qf_init()里初始化的你需要修改QP的源码或者提供一个自定义的事件池。二是及时回收事件。在事件处理完后调用QF_gc(e)把事件内存还给事件池。但要注意如果事件被多个活动对象引用不能提前回收。三是尽量使用静态事件。对于不携带数据的事件用静态分配的方式不占用事件池。只有确实需要携带数据的事件才用动态分配。6.5 中断优先级配置错误导致系统卡死Cortex-M的中断优先级配置很重要。QP在临界区里会关闭中断如果中断优先级配置不当可能会导致中断嵌套问题。我一般把SysTick的优先级设成最低把其他外设中断的优先级设成比SysTick高。这样SysTick不会打断其他中断的处理。在STM32CubeMX里NVIC的优先级分组默认是4位抢占优先级0位子优先级。我一般把SysTick的抢占优先级设成15最低把串口、定时器等外设的抢占优先级设成5到10之间。提示QP的QF_ISR_NESTING宏如果设成1表示允许中断嵌套。如果你的系统里中断嵌套比较复杂建议设成0禁止中断嵌套简化临界区的管理。7. 从裸机到QP的思维转变7.1 不要再用全局变量传递状态裸机开发里我们习惯用全局变量来传递状态比如volatile uint8_t flag。在QP里全局变量应该尽量避免因为活动对象之间的通信应该通过事件来完成。全局变量会破坏活动对象的封装性让代码重新变得耦合。我刚开始用QP的时候总是忍不住在活动对象里直接读写全局变量结果发现状态机的逻辑变得很混乱。后来强迫自己把所有跨模块的数据都封装成事件代码结构立刻清晰了很多。7.2 状态机不是万能的QP框架适合事件驱动、状态复杂的场景比如用户界面、通信协议、电机控制等。但如果你的逻辑很简单比如只是定时采集数据并通过串口发送用QP反而会增加复杂度。我一般是在项目有超过三个状态、或者状态之间有复杂的转换关系时才考虑用QP。另外QP的状态机是同步执行的一个活动对象在处理事件时其他活动对象不能抢占它除非用QK内核。这意味着如果一个活动对象处理事件的时间太长会影响其他活动对象的响应。所以状态机里的处理逻辑要尽量短小耗时的操作应该拆分成多个事件来处理。7.3 调试QP程序的实用技巧QP程序调试起来比裸机程序稍微麻烦一点因为事件是异步的状态转换是隐式的。我总结了几条实用的调试技巧第一给每个活动对象分配一个独立的串口或者用同一个串口加前缀方便区分输出。比如[LED] Enter Led_on、[SENSOR] Read value 1234。第二用GPIO翻转来标记关键事件。比如在状态机的Q_ENTRY_SIG里翻转一个GPIO用逻辑分析仪抓波形可以直观地看到状态切换的时序。第三定期打印事件队列的长度。如果队列长度经常接近最大值说明事件产生得太快或者处理得太慢需要调整优先级或者优化处理逻辑。第四用QP自带的断言机制。QP的断言比标准C的assert更强大它会打印出文件名、行号和表达式方便定位问题。确保在qf_port.h里定义了Q_ASSERT宏指向一个有效的断言处理函数。7.4 性能优化的几个方向QP框架的性能瓶颈通常在事件队列和状态机的查找上。如果系统的事件吞吐量很大可以考虑这几个优化方向一是减少动态事件的使用。动态事件需要从事件池分配内存涉及临界区保护开销比静态事件大。能用静态事件的地方就用静态事件。二是合并事件。如果多个事件总是同时发生可以合并成一个事件减少事件队列的操作次数。三是调整活动对象的优先级。把响应时间要求高的活动对象设成高优先级把不紧急的设成低优先级。QP的调度器是严格按优先级分发的高优先级活动对象的事件队列不为空时低优先级活动对象的事件不会被处理。四是使用QK内核代替QV内核。QK内核支持抢占式调度高优先级活动对象可以打断低优先级活动对象的处理响应时间更短。但QK内核的移植和调试更复杂需要仔细配置中断优先级。我在实际项目里用QV内核跑过10ms周期的控制循环活动对象有5个事件队列长度设成16运行很稳定。后来换到QK内核响应时间从毫秒级降到了微秒级但调试花了不少时间。所以我的建议是先用QV内核把功能跑通如果性能不够再考虑QK内核。最后再分享一个小技巧QP的源码里有一个qassert.h文件里面的断言宏可以自定义。我一般会把断言处理函数改成打印错误信息后进入死循环而不是直接复位。这样调试的时候可以看到错误信息方便定位问题。等产品发布的时候再改成复位保证系统的可靠性。
返回列表