事件驱动系统的软件跟踪:QS组件详解
1. QS关键部分定义
当QS检测到QF临界区宏
QF_INT_LOCK()
和
QF_INT_UNLOCK()
已定义时,QS会使用这些定义来处理自身的临界区。但如果在没有QF实时框架的情况下使用QS,则需要在
qs_port.h
头文件中定义特定于平台的中断锁定/解锁策略,如下所示:
#define QS_INT_KEY_TYPE
...
#define QS_INT_LOCK(key_)
...
#define QS_INT_UNLOCK(key_)
...
这些QS宏与QF宏
QF_INT_KEY_TYPE
、
QF_INT_LOCK()
和
QF_INT_UNLOCK()
分别类似。
2. QS记录的通用结构
QS将跟踪数据记录在离散的块中,称为QS跟踪记录。每个跟踪记录的通用结构如下:
QS_BEGIN_xxx(record_type)
/* trace record begin */
QS_yyy(data);
/* QS data element */
QS_zzz(data);
/* QS data element */
...
/* QS data element */
QS_END_xxx()
/* trace record end */
每个跟踪记录总是以
QS_BEGIN_xxx()
宏的一个变体开始,并以匹配的
QS_END_xxx()
宏结束。需要注意的是,
QS_BEGIN_xxx()
和
QS_END_xxx()
宏不以分号结尾。
QS提供了四对不同用途的开始/结束宏:
-
QS_BEGIN()/QS_END()
:用于创建特定于应用程序的QS记录,在记录开始时锁定中断,结束时解锁。
-
QS_BEGIN_NOLOCK()/QS_END_NOLOCK()
:用于特定于应用程序的记录,不进入QS临界区,应仅在已建立的临界区内使用。
-
QS_BEGIN_()/QS_END_()
和
QS_BEGIN_NOLOCK_()/QS_END_NOLOCK_()
:用于QP组件内部生成预定义的QS记录,根据是否需要进入临界区选择使用。
3. QS过滤器
QS提供了两级互补的过滤机制:全局开/关过滤器和局部过滤器。
3.1 全局开/关过滤器
全局开/关过滤器基于记录ID。
qs.h
头文件提供了所有预定义内部跟踪记录ID的枚举,枚举以
QS_USER
值结束,该值是可用于特定于应用程序的跟踪记录的第一个数值。
全局开/关过滤器通过
QS_glbFilter_[]
位掩码数组实现,每个位代表一个跟踪记录。当前,
QS_glbFilter_[]
数组包含32字节,共256位,可用于256种不同的跟踪记录。其中,超过四分之一的记录已被预定义的QP跟踪记录占用,其余四分之三可用于特定于应用程序的用途。
QS_BEGIN()
宏展示了全局开/关过滤器的实现:
#define QS_BEGIN(rec_, obj_) \
if (((QS_glbFilter_[(uint8_t)(rec_) >> 3U] \
& (1U << ((uint8_t)(rec_) & 7U))) != 0) ...\
全局开/关过滤器通过检查与给定跟踪记录参数 “rec_” 对应的位的状态来工作。对于任何常量值的参数 “rec_”,位掩码和位都是编译时常量。
QS提供了简单的接口来设置和清除
QS_glbFilter_[]
位掩码数组中的单个位:
-
QS_FILTER_ON(rec_)
:打开与记录 “rec_” 对应的位。
-
QS_FILTER_OFF(rec_)
:关闭与记录 “rec_” 对应的位。
-
QS_FILTER_ON(QS_ALL_RECORDS)
:打开所有记录。
-
QS_FILTER_OFF(QS_ALL_RECORDS)
:关闭所有记录。
在QS初始化后,全局开/关过滤器默认对所有记录类型设置为关闭状态,需要显式打开某些记录的过滤器以启用跟踪。全局禁用所有记录是实现软件跟踪触发器的有用方法,可以在感兴趣的事件发生后迅速停止跟踪,防止新的跟踪数据覆盖感兴趣的数据。
3.2 局部过滤器
局部过滤器允许仅为指定的对象生成跟踪记录。例如,可以设置一个局部过滤器来仅记录给定状态机对象的活动,或者设置另一个局部过滤器来仅记录给定内存池的活动。
以下是局部过滤器的总结:
| 局部过滤器 | 对象类型 | 示例 | 适用于的QS记录 |
| — | — | — | — |
|
QS_FILTER_SM_OBJ()
| 状态机 |
QS_FILTER_SM_OBJ(&l_qhsmTst);
|
QS_QEP_STATE_EMPTY
,
QS_QEP_STATE_ENTRY
,
QS_QEP_STATE_EXIT
,
QS_QEP_STATE_INIT
,
QS_QEP_INIT_TRAN
,
QS_QEP_INTERN_TRAN
,
QS_QEP_TRAN
,
QS_QEP_IGNORED
|
|
QS_FILTER_AO_OBJ()
| 活动对象 |
QS_FILTER_AO_OBJ(&l_philo[3]);
|
QS_QF_ACTIVE_ADD
,
QS_QF_ACTIVE_REMOVE
,
QS_QF_ACTIVE_SUBSCRIBE
,
QS_QF_ACTIVE_UNSUBSCRIBE
,
QS_QF_ACTIVE_POST_FIFO
,
QS_QF_ACTIVE_POST_LIFO
,
QS_QF_ACTIVE_GET
,
QS_QF_ACTIVE_GET_LAST
|
|
QS_FILTER_MP_OBJ()
| 内存池 |
QS_FILTER_MP_OBJ(l_regPoolSto);
|
QS_QF_MPOOL_INIT
,
QS_QF_MPOOL_GET
,
QS_QF_MPOOL_PUT
|
|
QS_FILTER_EQ_OBJ()
| 事件队列 |
QS_FILTER_EQ_OBJ(l_philQueueSto[3]);
|
QS_QF_EQUEUE_INIT
,
QS_QF_EQUEUE_POST_FIFO
,
QS_QF_EQUEUE_POST_LIFO
,
QS_QF_EQUEUE_GET
,
QS_QF_EQUEUE_GET_LAST
|
|
QS_FILTER_TE_OBJ()
| 时间事件 |
QS_FILTER_TE_OBJ(&l_philo[3].timeEvt);
|
QS_QF_TICK
,
QS_QF_TIMEEVT_ARM
,
QS_QF_TIMEEVT_AUTO_DISARM
,
QS_QF_TIMEEVT_DISARM_ATTEMPT
,
QS_QF_TIMEEVT_DISARM
,
QS_QF_TIMEEVT_REARM
,
QS_QF_TIMEEVT_POST
,
QS_QF_TIMEEVT_PUBLISH
|
|
QS_FILTER_AP_OBJ()
| 通用应用程序对象 |
QS_FILTER_AP_OBJ(&myAppObject);
| 从
QS_USER
开始的特定于应用程序的记录 |
要指定局部过滤器,可以调用相应的QS宏,例如
QS_FILTER_SM_OBJ(aStateMachinePointer)
,其中
aStateMachinePointer
是要跟踪的状态机对象的指针。通过将
NULL
指针传递给相应的QS宏,可以停用任何局部过滤器。在QS初始化后,所有局部过滤器默认设置为
NULL
,意味着局部过滤器对所有对象开放。
QS_BEGIN()
宏定义中的高亮代码展示了特定于应用程序对象的局部过滤器的实际实现:
#define QS_BEGIN(rec_, obj_) \
if (((QS_glbFilter_[(uint8_t)(rec_) >> 3U] \
& (1U << ((uint8_t)(rec_) & 7U))) != 0) \
&& ((QS_apObj_ == (void *)0) || (QS_apObj_ == (obj_)))) \
{ \
...
4. QS数据协议
QS用于将跟踪数据从目标传输到主机的数据传输协议是其最大的优势之一。该协议非常轻量级,但具有ISO定义的HDLC协议的许多元素。
QS协议的帧结构如下:
graph LR
A[帧序列号] --> B[记录ID]
B --> C[数据]
C --> D[校验和]
D --> E[标志 0x7E]
- 帧序列号:每个帧以帧序列号字节开始,目标QS组件为插入循环缓冲区的每个帧递增该序列号,序列号从255自然回滚到0,允许QSPY主机组件检测任何数据不连续性。
- 记录ID:紧随帧序列号之后的是记录ID字节,它是预定义的QS记录之一或特定于应用程序的记录。
- 数据:记录ID之后是零个或多个数据字节。
- 校验和:校验和是对帧序列号、记录ID和所有数据字节计算得出的。
- 标志:校验和之后是HDLC标志(0x7E),用于分隔帧。
4.1 透明度
为了避免在数据传输过程中混淆无意出现的标志字节和有意发送的标志,HDLC使用了透明度技术(也称为字节填充或转义)。当发送器在数据中遇到标志字节时,会在输出流中插入一个2字节的转义序列。第一个字节是转义字节(0x7D),第二个字节是原始字节与0x20进行异或运算的结果。同样,转义字节本身也需要进行转义处理。
发送器在进行任何字节填充之前计算校验和,接收器在计算校验和之前必须执行字节去填充的反向过程。
例如,假设有以下跟踪记录需要插入到跟踪缓冲区(透明字节以粗体显示):
Record ID = 0x7D, Record Data = 0x7D 0x08 0x01
假设当前帧序列号为0x7E,校验和将按以下字节计算:
Checksum == (uint8_t)(~(0x7E + 0x7D + 0x7D + 0x08 + 0x01)) == 0x7E
实际插入到QS跟踪缓冲区的帧如下:
0x7D 0x5E 0x7D 0x5D 0x7D 0x5D 0x08 0x01 0x7D 0x5E 0x7E
4.2 字节序
除了类似HDLC的帧结构,QS传输协议指定数据的字节序为小端字节序。所有多字节数据元素,如16位和32位整数、指针和浮点数,都以小端字节序(最低有效字节在前)插入到QS跟踪缓冲区中。QS数据插入宏以平台无关的方式将数据放置在跟踪缓冲区中,避免了潜在的数据对齐问题。
5. QS跟踪缓冲区
5.1 缓冲区设计优势
QS目标组件在将字节插入QS跟踪缓冲区时执行类似HDLC的帧处理,这意味着缓冲区中仅放置完整的帧,这是QS目标组件设计的关键,带来两个重要结果:
-
数据插入与移除解耦
:使用HDLC格式的数据允许将数据插入跟踪缓冲区与从跟踪缓冲区移除数据解耦。可以以任意块大小移除数据,无需考虑帧边界,可利用目标上的任何物理数据链路将跟踪数据从目标传输到主机。
-
“最新即最佳”跟踪策略
:QS传输协议为每个跟踪记录维护帧序列号和校验和,意味着新的跟踪数据可直接插入循环跟踪缓冲区,即使可能覆盖尚未发送或正在发送的旧数据,数据损坏也能被可靠检测。检测数据损坏的责任由QSPY主机组件承担。当主机组件检测到帧丢失时,可以采取以下措施:
- 应用更多过滤以减少生成的跟踪数据量。
- 增加跟踪缓冲区的大小。
- 提高数据传输吞吐量。
5.2 初始化QS跟踪缓冲区
QS_initBuf()
在开始生成跟踪数据之前,必须通过调用
QS_initBuf()
函数初始化QS跟踪缓冲区。通常在
QS_onStartup()
回调中调用此函数,示例如下:
#ifdef Q_SPY
/* define QS callbacks */
uint8_t QS_onStartup(void const *arg) {
static uint8_t qsBuf[2*1024];
/* buffer for Quantum Spy */
QS_initBuf(qsBuf, sizeof(qsBuf));
/* Initialize the QS data link */
return success;
/* return 1 for success and 0 for failure */
}
#endif
/* Q_SPY */
-
QS回调函数(如QS_onStartup())仅在启用QS跟踪时定义。 -
QS_onStartup()回调函数至少要初始化QS跟踪缓冲区。 - 需要静态分配QS跟踪缓冲区的存储空间,缓冲区大小取决于应用性质和到主机的数据链路。高频率事件跟踪和大量跟踪数据需要更大的缓冲区,而高带宽数据链路可减小缓冲区大小。
-
QS_initBuf()函数初始化内部QS变量以使用提供的跟踪缓冲区。
需要注意的是,QS可以使用任何大小的跟踪缓冲区,但较小的缓冲区在数据溢出时会丢失数据。不过,由于QS数据协议为每个跟踪记录维护序列号,当QSPY主机应用检测到序列号不连续时,会产生以下消息:
*** Incorrect record past seq=xxx
*** Dropped yy records
5.3 字节导向接口:
QS_getByte()
由于从跟踪缓冲区移除数据没有任何限制,可以在任意时间实例一次移除1字节。QS为此提供了
QS_getByte()
函数,其签名在
qs.h
中定义。示例如下展示了如何使用此函数:
void QF_onIdle(void) {
/* called with interrupts LOCKED */
QF_INT_UNLOCK(dummy);
/* always unlock interrupts */
#ifdef Q_SPY
if ((inportb(l_uart_base + 5) & (1 << 5)) != 0) {
/* THR Empty? */
uint8_t fifo = UART_16550_TXFIFO_DEPTH;
/* depth of the 16550 Tx FIFO */
uint16_t b;
QF_INT_LOCK(dummy);
while ((fifo != 0)
&& ((b = QS_getByte()) != QS_EOD)) {
/* get the next byte */
QF_INT_UNLOCK(dummy);
outportb(l_base + 0, (uint8_t)b);
/* insert byte into TX FIFO */
--fifo;
QF_INT_LOCK(dummy);
}
QF_INT_UNLOCK(dummy);
}
#endif
}
操作步骤如下:
1. 空闲处理是实现跟踪数据输出的理想时机,这里使用了合作式内核的
QF_onIdle()
空闲回调。
2.
QF_onIdle()
回调在中断锁定的情况下调用,因此需要先解锁中断。
3. 仅当定义了
Q_SPY
宏时才执行QS跟踪缓冲区输出。
4. 检查16550 UART的发送保持寄存器(THR)是否为空。
5. 16550 UART的发送FIFO通常可以接受多达
UART_16550_TXFIFO_DEPTH
字节(通常为16字节)。
6. 定义一个临时变量
b
来保存
QS_getByte()
的返回值,该变量为2字节宽。
7. 在调用
QS_getByte()
之前锁定中断。
8. 循环继续,直到发送FIFO有空间。
9. 调用
QS_getByte()
函数获取下一个要传输的跟踪字节,返回值
QS_EOD
表示数据结束。
10. 可以解锁中断。
11. 将跟踪字节写入发送保持寄存器。
12. 发送FIFO中可用空间减少1字节。
13. 锁定中断以再次调用
QS_getByte()
。
14. 在返回调用者之前解锁中断。
需要注意的是,
QS_getByte()
函数内部不锁定中断,并且不是可重入的。因此,软件设计应确保在中断锁定的情况下调用
QS_getByte()
。此外,应用程序应始终一致地使用字节导向接口
QS_getByte()
或块导向接口
QS_getBlock()
,但不能同时使用两者。
综上所述,QS组件在事件驱动系统的软件跟踪中具有重要作用,通过合理配置和使用QS的关键部分定义、过滤器、数据协议和跟踪缓冲区等功能,可以有效地实现对系统的跟踪和调试,提高系统的可靠性和性能。
超级会员免费看
102

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



