QP-nano 实时框架深入解析
1. 定时器事件处理
在处理定时器事件时,为了节省栈空间,临时变量
a
(指向活动对象的指针)被声明为静态变量。活动对象指针从 ROM 数组
QF_active[]
中加载,该数组将活动对象的优先级映射到活动对象指针。
当滴答计数器不为零时,给定活动对象的时间事件正在运行。滴答计数器会递减并与零进行比较,当计数器达到零时,时间事件会自动解除。
QF_tick()
函数通过
QActive_postISR()
函数将
Q_TIMEOUT_SIG
事件发送给拥有该计数器的活动对象,因此
QF_tick()
只能在中断服务程序(ISR)上下文中调用。这个循环会对所有优先级大于零的活动对象继续执行。
2. 事件队列
在 QP - nano 中,每个活动对象都有自己的事件队列。队列由位于
QActive
结构内部的一个事件(所有活动对象都从该结构派生)和活动对象外部分配的事件环形缓冲区组成。
常量数组
QF_active[]
存储活动对象的“控制块”,即
QActiveCB
结构的实例。每个
QF_active[]
元素包含指向活动对象的指针
act
、指向环形缓冲区队列的指针
queue
以及环形缓冲区最后一个元素的索引
end
,这些元素在编译时为每个活动对象进行初始化。
QActive
结构存储在运行时会发生变化的事件队列元素。队列存储由外部用户分配的环形缓冲区队列和状态机内部存储的当前事件
evt
组成。所有发送到状态机的事件都必须经过“当前事件”
evt
数据成员,这在图中用虚线表示。这个位于环形缓冲区之外的额外位置通过频繁绕过缓冲来优化队列操作,因为在大多数情况下,队列会在空和非空状态之间交替,且队列中一次只有一个事件。
在极端情况下,如果足够的队列深度仅为一个事件,整个环形缓冲区可以完全消除。此时,不分配环形缓冲区,使用
NULL
作为队列指针,使用零作为有效队列长度来初始化
QF_active[]
数组。
环形缓冲区的索引
head
和
tail
以及
QF_active[]
中的
end
都是相对于队列指针的。这些索引管理着一个环形缓冲区队列,客户端必须预先分配一个连续的
QEvent
类型的事件数组。事件总是从缓冲区的
tail
索引处提取,新事件插入到
head
索引处,这对应于先进先出(FIFO)排队。提取事件时,
tail
索引总是递减;插入事件时,
head
索引也总是递减。
end
索引限制了
head
和
tail
索引的范围,当它们达到零时,必须“环绕”到
end
,这会导致
head
和
tail
索引围绕环形缓冲区逆时针移动。
下面是一个简单的 mermaid 流程图,展示事件队列的基本操作:
graph LR
A[新事件到达] --> B{队列是否为空}
B -- 是 --> C[直接复制到当前事件]
B -- 否 --> D[插入到环形缓冲区头部]
D --> E{头部索引是否为 0}
E -- 是 --> F[头部索引环绕到 end]
E -- 否 --> G[头部索引递减]
C --> H[更新 QF_readySet_]
F --> H
G --> H
H --> I[结束]
3. 就绪集(QF_readySet_)
QP - nano 包含一个协作式的“普通”内核和一个名为 QK - nano 的抢占式实时内核。为了在这两个内核中进行高效调度,QP - nano 使用一个单字节的
QF_readySet_
来维护应用程序中所有活动对象事件队列的全局状态。
QF_readySet_
是一个位掩码,代表系统中所有非空事件队列的“就绪集”。
QF_readySet_
字节中的每一位对应一个活动对象。例如,当且仅当优先级为
n + 1
的活动对象的事件队列为非空时,
QF_readySet_
中的第
n
位为 1(位传统上从 0 开始编号,而 QP - nano 中的优先级从 1 开始编号)。
当向优先级为
p
的空队列发送事件时,
QF_readySet_
位掩码中的第
p - 1
位会被设置为 1;相反,当从优先级为
q
的队列中移除最后一个事件时,
QF_readySet_
位掩码中的第
q - 1
位会被清除。显然,对全局
QF_readySet_
位掩码的所有操作都必须在临界区中进行。
4. 从任务级别发送事件(QActive_post())
QActive_post()
函数用于从一个活动对象向另一个活动对象发送事件,不能用于从 ISR 发送事件,因为它使用任务级别的中断锁定策略。
下面是
QActive_post()
函数的代码:
#if (Q_PARAM_SIZE != 0)
(1)
void QActive_post(QActive *me, QSignal sig, QParam par) {
#else
(2)
void QActive_post(QActive *me, QSignal sig) {
#endif
(3)
QF_INT_LOCK();
if (me->nUsed == (uint8_t)0) {
/* is the queue empty? */
++me->nUsed;
/* update number of events */
(4)
Q_SIG(me) = sig;
/* deliver the event directly */
#if (Q_PARAM_SIZE != 0)
(5)
Q_PAR(me) = par;
#endif
(6)
QF_readySet_ |= Q_ROM_BYTE(l_pow2Lkup[me->prio]);
/* set the bit */
#ifdef QK_PREEMPTIVE
(7)
QK_schedule_();
/* check for synchronous preemption */
#endif
}
else {
(8)
QF_pCB_ = &QF_active[me->prio];
/* the queue must be able to accept the event (cannot overflow) */
(9)
Q_ASSERT(me->nUsed <= Q_ROM_BYTE(QF_pCB_->end));
++me->nUsed;
/* update number of events */
/* insert event into the ring buffer (FIFO) */
(10)
((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].sig = sig;
#if (Q_PARAM_SIZE != 0)
(11)
((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].par = par;
#endif
(12)
if (me->head == (uint8_t)0) {
(13)
me->head = Q_ROM_BYTE(QF_pCB_->end);
/* wrap the head */
}
(14)
--me->head;
}
(15)
QF_INT_UNLOCK();
}
QActive_post()
函数的签名取决于是否配置了带参数的事件。任务级别的事件发送操作总是在任务级别的临界区中进行。当事件队列为空时,新事件会直接复制到状态机内部的当前事件中。对应活动对象优先级的位会在
QF_readySet_
位掩码中被设置,常量查找表
l_pow2Lkup[]
按如下方式初始化:
l_pow2Lkup[p] == (1 << (p - 1))
,其中
p
为 1 到 8 的所有优先级。
当配置了抢占式 QK - nano 内核时,会调用抢占式调度器来处理潜在的同步抢占(当一个活动对象向更高优先级的任务发送事件时会发生同步抢占)。全局变量
QF_pCB_
保存指向 ROM 中活动对象控制块
&QF_active[me->prio]
的指针,该变量仅在 QP - nano 函数内部局部使用,用于避免栈上加载临时变量。
断言确保队列能够接受这个事件,新事件会复制到环形缓冲区的
head
索引处。
head
索引会检查是否需要环绕,若需要则移动到缓冲区的末尾,并且
head
索引总是递减。最后,解锁中断以离开临界区。
5. 从 ISR 级别发送事件(QActive_postISR())
QActive_postISR()
函数用于从 ISR 向活动对象发送事件,不能用于从活动对象发送事件,因为它使用特定于 ISR 的临界区机制。
下面是
QActive_postISR()
函数的代码:
#if (Q_PARAM_SIZE != 0)
void QActive_postISR(QActive *me, QSignal sig, QParam par)
#else
void QActive_postISR(QActive *me, QSignal sig)
#endif
{
(1)
#ifdef QF_ISR_NEST
(2)
#ifdef QF_ISR_KEY_TYPE
(3)
QF_ISR_KEY_TYPE key;
(4)
QF_ISR_LOCK(key);
#else
(5)
QF_INT_LOCK();
#endif
#endif
if (me->nUsed == (uint8_t)0) {
++me->nUsed;
/* update number of events */
Q_SIG(me) = sig;
/* deliver the event directly */
#if (Q_PARAM_SIZE != 0)
Q_PAR(me) = par;
#endif
QF_readySet_ |= Q_ROM_BYTE(l_pow2Lkup[me->prio]);
/* set the bit */
}
else {
QF_pCB_ = &QF_active[me->prio];
/* the queue must be able to accept the event (cannot overflow) */
Q_ASSERT(me->nUsed <= Q_ROM_BYTE(QF_pCB_->end));
++me->nUsed;
/* update number of events */
/* insert event into the ring buffer (FIFO) */
((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].sig = sig;
#if (Q_PARAM_SIZE != 0)
((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].par = par;
#endif
if (me->head == (uint8_t)0) {
me->head = Q_ROM_BYTE(QF_pCB_->end);
/* wrap the head */
}
--me->head;
}
#ifdef QF_ISR_NEST
#ifdef QF_ISR_KEY_TYPE
QF_ISR_UNLOCK(key);
#else
QF_INT_UNLOCK();
#endif
#endif
}
只有在允许中断嵌套时才会锁定中断。如果定义了
QF_ISR_KEY_TYPE
,QP - nano 使用“保存和恢复中断状态”的高级策略,需要一个临时变量
key
来保存中断状态并锁定中断;如果未定义
QF_ISR_KEY_TYPE
,则使用“无条件中断解锁”的简单策略,与任务级别相同。
需要注意的是,ISR 级别的事件发送操作
QActive_postISR()
不会调用 QK - nano 调度器,因为任务永远不能同步抢占 ISR。
6. 协作式“普通”内核
默认情况下,QP - nano 使用简单的协作式“普通”调度器。下面是实现整个“普通”内核的
QF_run()
函数代码:
#ifndef QK_PREEMPTIVE
void QF_run(void) {
(2)
static uint8_t const Q_ROM Q_ROM_VAR log2Lkup[] = {
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4
};
(3)
static uint8_t const Q_ROM Q_ROM_VAR invPow2Lkup[] = {
0xFF, 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F
};
(4)
static QActive *a;
/* declared static to save stack space */
(5)
static uint8_t p;
/* declared static to save stack space */
/* trigger initial transitions in all registered active objects... */
(6)
for (p = (uint8_t)1; p <= (uint8_t)QF_MAX_ACTIVE; ++p) {
(7)
a = (QActive *)Q_ROM_PTR(QF_active[p].act);
(8)
Q_ASSERT(a != (QActive *)0);
/* QF_active[p] must be initialized */
(9)
a->prio = p;
/* set the priority of the active object */
#ifndef QF_FSM_ACTIVE
(10)
QHsm_init((QHsm *)a);
/* take the initial transition in HSM */
#else
(11)
QFsm_init((QFsm *)a);
/* take the initial transition in FSM */
#endif
}
(12)
QF_onStartup();
/* invoke startup callback */
(13)
for (;;) {
/* the event loop of the vanilla kernel */
(14)
QF_INT_LOCK();
(15)
if (QF_readySet_ != (uint8_t)0) {
#if (QF_MAX_ACTIVE > 4)
(16)
if ((QF_readySet_ & 0xF0) != 0U) {
/* upper nibble used? */
(17)
p = (uint8_t)(Q_ROM_BYTE(log2Lkup[QF_readySet_ >> 4]) + 4);
}
else
/* upper nibble of QF_readySet_ is zero */
#endif
{
(18)
p = Q_ROM_BYTE(log2Lkup[QF_readySet_]);
}
(19)
QF_INT_UNLOCK();
(20)
a = (QActive *)Q_ROM_PTR(QF_active[p].act);
#ifndef QF_FSM_ACTIVE
(21)
QHsm_dispatch((QHsm *)a);
/* dispatch to HSM */
#else
(22)
QFsm_dispatch((QFsm *)a);
/* dispatch to FSM */
#endif
(23)
QF_INT_LOCK();
(24)
if ((--a->nUsed) == (uint8_t)0) {
/* queue becoming empty? */
(25)
QF_readySet_ &= Q_ROM_BYTE(invPow2Lkup[p]);/* clear the bit */
}
else {
(26)
QF_pCB_ = &QF_active[a->prio];
(27)
Q_SIG(a)=((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[a->tail].sig;
#if (Q_PARAM_SIZE != 0)
(28)
Q_PAR(a)=((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[a->tail].par;
#endif
(29)
if (a->tail == (uint8_t)0) {
/* wrap around? */
(30)
a->tail = Q_ROM_BYTE(QF_pCB_->end);
/* wrap the tail */
}
(31)
--a->tail;
}
(32)
QF_INT_UNLOCK();
}
(33)
else {
(34)
QF_onIdle();
/* see NOTE01 */
}
}
}
#endif
协作式普通内核仅在未配置抢占式 QK - nano 内核时进行编译。常量数组
log2Lkup[]
是二进制对数(以 2 为底)查找表,用于快速确定位掩码中最显著的 1 位;常量数组
invPow2Lkup[]
是按位取反的 2 的幂查找表,用于屏蔽
QF_readSet_
位掩码中的位。
临时变量
a
和
p
被定义为静态变量以节省栈空间。
QF_run()
函数中的静态变量
p
与
QF_tick()
中的
p
不同,因为这两个函数会并发执行。
通过一个
for
循环,从最低优先级的活动对象开始触发所有活动对象的初始转换。通常,活动对象应该按优先级顺序初始化,因为最低优先级的活动对象往往有最长的事件队列,这在活动对象从初始转换中相互发送事件时可能很重要。
活动对象指针从 ROM 中的活动对象控制块获取,断言确保
QF_active[]
数组已正确初始化,并初始化活动对象的内部优先级。根据是否配置了有限状态机(FSM)或层次状态机(HSM),触发相应的初始转换。
QF_onStartup()
回调函数用于配置和启动中断,该函数在应用程序级别(在 BSP 中)实现。
“普通”内核的事件循环开始,首先锁定中断以访问
QF_readySet_
就绪集。如果就绪集不为空,内核需要处理一些事件。为了快速找到具有非空事件队列的最高优先级活动对象,使用二进制对数查找表。由于 QP - nano 中的
log2Lkup[]
查找表只能处理 0 到 15 的值,当活动对象数量大于查找表范围时,会先测试
QF_readySet_
位掩码的高半字节。
如果高半字节不为零,将高半字节右移 4 位并应用
log2Lkup[]
查找表,然后加上 4 得到活动对象的优先级;否则,直接对低半字节应用
log2Lkup[]
查找表。解锁中断后,获取活动对象指针,并根据是否配置了 FSM 或 HSM 进行事件分发。
再次锁定中断以更新活动对象事件队列的状态,队列中的事件数量递减并与零比较。如果队列变为空,使用
invPow2Lkup[]
查找表清除
QF_readySet_
位掩码中对应的位;否则,将下一个事件从环形缓冲区的
tail
索引处复制到状态机内部的当前事件中,检查
tail
索引是否需要环绕并递减。最后解锁中断。
当所有活动对象的事件队列都为空时,进入空闲条件,调用
QF_onIdle()
回调函数,该函数通常在应用程序级别(在 BSP 中)实现,用于让应用程序有机会将 MCU 置于低功耗睡眠模式。
7. 中断处理和空闲处理
在协作式“普通”内核下,中断处理与前后台架构一样简单,通常可以使用 C 编译器生成的 ISR。对于 QP - nano,需要在中断嵌套策略上保持一致。如果在
qpn_port.h
头文件中定义了宏
QF_ISR_NEST
来配置嵌套中断,在调用 QP - nano 函数
QActive_postISR()
或
QF_tick()
之前需要一致地解锁中断。
空闲回调
QF_onIdle()
在 QP - nano 中的工作方式与完整版本的 QP 完全相同,可以参考相关示例为各种 CPU 定义此回调函数。
8. 抢占式实时内核(QK - nano)
QP - nano 包含一个名为 QK - nano 的抢占式实时内核,其工作方式与完整版本 QP 中的 QK 抢占式内核非常相似。在决定使用像 QK - nano 这样的抢占式内核之前,建议先了解相关知识。
需要注意的是,抢占式 QK - nano 内核比非抢占式普通内核对目标 CPU 和编译器的要求更高。一般来说,如果处理器和编译器满足以下要求,就可以使用 QK - nano:处理器支持能够容纳栈变量(不仅仅是返回地址)的硬件栈。
综上所述,QP - nano 提供了丰富的功能和灵活的配置选项,无论是协作式内核还是抢占式内核,都能满足不同场景的需求。在实际应用中,需要根据具体的硬件平台和应用需求来选择合适的内核和配置方式。
QP-nano 实时框架深入解析
9. 内核选择与应用场景分析
在实际应用中,选择合适的内核对于系统的性能和稳定性至关重要。下面通过一个表格来对比协作式“普通”内核和抢占式 QK - nano 内核的特点和适用场景:
| 内核类型 | 特点 | 适用场景 |
| — | — | — |
| 协作式“普通”内核 | 简单、实现成本低;任务按顺序执行,不会出现任务抢占;对硬件和编译器要求较低 | 对实时性要求不高、任务之间协作性强、硬件资源有限的场景,如一些简单的传感器数据采集系统 |
| 抢占式 QK - nano 内核 | 支持任务抢占,能更好地处理实时事件;可以提高系统的响应速度;对硬件和编译器要求较高 | 对实时性要求高、有紧急任务需要及时处理的场景,如工业自动化控制系统、航空航天设备等 |
10. 代码优化建议
为了提高 QP - nano 系统的性能和效率,可以从以下几个方面对代码进行优化:
-
栈空间优化
:如前面所述,将一些临时变量声明为静态变量可以节省栈空间。例如,在
QF_run()
函数中,
a
和
p
被定义为静态变量。在编写代码时,对于一些频繁使用且生命周期较长的临时变量,可以考虑使用静态变量。
-
查找表优化
:合理使用查找表可以提高代码的执行效率。例如,
log2Lkup[]
和
invPow2Lkup[]
查找表,通过预先计算好的值,避免了复杂的计算过程,加快了查找速度。在实际应用中,如果有类似的计算需求,可以考虑使用查找表来优化。
-
中断处理优化
:在中断处理中,尽量减少中断服务程序(ISR)的执行时间,避免在 ISR 中进行复杂的操作。可以将一些耗时的操作放到任务中处理,以保证系统的实时性。例如,在 ISR 中只进行简单的事件记录,然后在任务中进行具体的处理。
11. 常见问题及解决方法
在使用 QP - nano 过程中,可能会遇到一些常见问题,下面是一些问题及对应的解决方法:
| 问题 | 现象 | 解决方法 |
| — | — | — |
| 事件队列溢出 | 事件无法正常添加到队列中,系统出现异常 | 检查事件队列的长度,根据实际需求调整队列大小;优化事件处理逻辑,减少事件的产生 |
| 任务调度异常 | 任务无法按预期执行,出现任务阻塞或抢占异常 | 检查内核配置是否正确,确保选择了合适的内核类型;检查任务优先级设置,避免优先级冲突 |
| 中断处理异常 | 中断服务程序执行异常,系统响应不及时 | 检查中断嵌套策略是否正确,确保在调用相关函数时中断状态的一致性;优化中断服务程序的代码,减少执行时间 |
12. 总结与展望
QP - nano 是一个功能强大、灵活可配置的实时框架,它提供了协作式“普通”内核和抢占式 QK - nano 内核,满足了不同应用场景的需求。通过合理使用事件队列、就绪集等机制,QP - nano 可以高效地处理事件和任务调度。
在未来的开发中,随着硬件技术的不断发展,QP - nano 可以进一步优化以适应更复杂的应用场景。例如,可以考虑支持更多的硬件平台,提高系统的兼容性;优化内核算法,提高系统的实时性和性能。同时,对于开发者来说,需要深入理解 QP - nano 的原理和机制,根据具体需求进行合理的配置和优化,以充分发挥 QP - nano 的优势。
下面是一个 mermaid 流程图,展示了 QP - nano 系统的整体工作流程:
graph LR
A[系统启动] --> B[初始化活动对象]
B --> C[调用 QF_onStartup()]
C --> D{是否配置 QK - nano 内核}
D -- 是 --> E[使用 QK - nano 内核进行任务调度]
D -- 否 --> F[使用协作式“普通”内核进行任务调度]
E --> G[事件处理循环]
F --> G
G --> H{是否有新事件}
H -- 是 --> I[处理新事件]
H -- 否 --> J[调用 QF_onIdle()]
I --> G
J --> G
通过以上的分析和介绍,相信读者对 QP - nano 实时框架有了更深入的了解。在实际应用中,可以根据具体需求选择合适的内核和配置方式,同时注意代码优化和问题解决,以构建高效、稳定的实时系统。
超级会员免费看
611

被折叠的 条评论
为什么被折叠?



