软件追踪系统 QS 深度解析
1. 活跃对象初始化与中断处理
活跃对象初始化时,可包含封装在活跃对象内的项目的字典条目。以 Table 初始化为例,它会为 l_table 对象插入一个对象字典条目,并为状态处理程序 QHsm_top、Table_initial 和 Table_serving 插入三个函数字典条目。最后,会进行从 QHsm_top 到 Table_serving 的最顶层初始转换。
活跃对象初始化完成后,中断被启用。首个 Tick 中断在时间戳 0000070346 到达,可通过唯一的优先级编号确定中断类型,例如 Tick 中断的优先级为 0xFF(即 255)。Tick 中断共发生七次,通过比较 Tick 1 和 Tick 2 中断进入时的时间戳,可确定滴答速率,计算方式为 ((0000135566 – 0000070346)/7 = 65220 ≈ 0x10000)。在 DPP 应用中,时间戳由 8254 定时器/计数器的计数器 0 提供,该计数器由运行频率为 1.193182MHz 的振荡器驱动,同样的计数器 0 每 0x10000 个计数(即 18.2Hz 的 DOS 滴答)产生一次时间滴答中断。
在时间戳 0000461783 进入的 Tick 7 中断中,一个时间事件会向 l_philo[1] 活跃对象发布 TIMEOUT_SIG 事件,这会触发应用程序中的大量活动,实际上,在下一个 Tick 8 之前会产生超过 42 条跟踪记录。
以下是该过程的 mermaid 流程图:
graph TD;
A[活跃对象初始化] --> B[启用中断];
B --> C[首个 Tick 中断到达];
C --> D[多次 Tick 中断发生];
D --> E[Tick 7 中断触发活动];
2. QS 目标组件概述
Quantum Spy 追踪系统的目标驻留组件称为 QS,它由环形缓冲区、QS 过滤器以及添加到 QEP、QF、QK 和应用程序中的检测工具组成。
与原始的 printf() 语句相比,使用 QS 进行软件追踪的侵入性要小得多。因为所有数据格式化操作都从目标系统中移除,在主机端事后完成。此外,目标代码的时间关键路径中产生的数据记录开销仅为将数据存储到跟踪缓冲区,通常不包括将数据发送出目标设备的开销。在 QS 中,数据记录和发送到主机是分开的,目标系统通常可以在时间关键路径之外(例如在目标 CPU 的空闲循环中)执行传输操作。
移除目标端的数据格式化操作还带来了自然的数据压缩效果。与格式化输出相比,例如单字节的 ASCII 表示需要两个十六进制数字(和三个十进制数字),避免格式化可使数据密度至少提高两倍。在此基础上,QS 还使用了数据字典和压缩格式信息等技术,实际上与扩展的人类可读格式相比,压缩因子可达 4 到 5。
QS 提供了两级互补的过滤器,以实现非常有选择性的追踪功能:
-
全局过滤器
:基于跟踪记录类型进行过滤,例如状态进入或事件发布,该级别对整个系统中的所有状态机和事件发布全局生效。
-
组件特定过滤器
:可设置为仅跟踪特定的状态机对象。
大多数 QS 跟踪记录都带有时间戳,QS 提供了一个高效的 API 来获取特定平台的时间戳信息。只要目标系统中有合适的定时器 - 计数器资源,就可以为 QS 提供所需的精确时间戳信息,时间戳大小可配置为 1、2 或 4 字节。
QS 的数据传输协议是其一大优势,该协议非常轻量级,但具有国际标准化组织(ISO)定义的高级数据链路控制(HDLC)协议的许多元素,具备检测传输错误的功能,并允许在任何错误(如由于 RAM 缓冲区溢出导致的数据丢失)后立即重新同步。此外,QS 还包含一个轻量级的 API 用于实现向主机的数据传输,该 API 支持任何实现策略(轮询、中断、DMA 等)和任何物理传输层(如串口、SPI、USB、以太网、数据文件等)。
以下是 QS 目标组件结构的表格:
| 组成部分 | 说明 |
| ---- | ---- |
| 环形缓冲区 | 用于存储跟踪数据 |
| QS 过滤器 | 包括全局过滤器和组件特定过滤器,实现选择性追踪 |
| 检测工具 | 添加到 QEP、QF、QK 和应用程序中 |
3. QS 源代码组织
QS 软件追踪组件的平台无关目录和文件结构如下:
<qp>\qpc\
- QP/C root directory (<qp>\qpcpp for QP/C++)
|
+-include/
- QP platform-independent header files
| +-qs.h
- QS platform-independent active interface
| +-qs_dummy.h
- QS platform-independent inactive interface
|
+-qs/
- QS target component
| +-source/
- QS platform-independent source code (*.C files)
| | +-qs_pkg.h
- internal, packet-scope interface for QS implementation
| | +-qs.c
- internal ring buffer and formatted output functions
| | +-qs_.c
- definition of basic unformatted output functions
| | +-qs_blk.c
- definition of block-oriented interface QS_getBlock()
| | +-qs_byte.c
- definition of byte-oriented interface QS_getByte()
| | +-qs_f32.c
- definition of 32-bit floating point output QS_f32()
| | +-qs_f64.c
- definition of 64-bit floating point output QS_f64()
| | +-qs_mem.c
- definition of memory-block output
| | +-qs_str.c
- definition of zero-terminated string output
|
+-ports\
- Platform-specific QP ports
| +- . . .
+-examples\
- Platform-specific QP examples
| +- . . .
QS 源文件通常每个文件只包含一个函数或一个数据结构定义,这种设计旨在将 QS 部署为一个细粒度的库,可与应用程序进行静态链接。细粒度意味着 QS 库由多个小的、松散耦合的模块(目标文件)组成,而不是一个包含所有功能的单一模块。
4. QS 平台无关头文件
4.1 qs.h 和 qs_dummy.h 的作用
与大多数 C 或 C++ 软件追踪系统一样,QS 严重依赖 C 预处理器,以便在编译时启用或禁用追踪检测工具,而无需更改被检测的源代码。大多数 QS 工具以预处理器宏的形式提供,根据全局宏 Q_SPY 的定义,QS 工具要么提供实际的 QS 服务,要么被“虚拟”掉以防止在未定义全局宏 Q_SPY 时生成任何代码。这样,QS 检测工具可以一直保留在代码中,只有在编译代码时定义了宏 Q_SPY 才会激活。通常通过编译器选项(通常是 -D)从外部定义宏 Q_SPY。
4.2 qs.h 文件内容
以下是 qs.h 文件的部分内容:
#ifndef qs_h
#define qs_h
#ifndef Q_SPY
(1)
#error "Q_SPY must be defined to include qs.h"
#endif
(2)
enum QSpyRecords {
/* QEP records */
(3)
QS_QEP_STATE_ENTRY,
/**< a state was entered */
QS_QEP_STATE_EXIT,
/**< a state was exited */
...
/* QF records */
(4)
QS_QF_ACTIVE_ADD,
/**< an AO has been added to QF (started) */
QS_QF_ACTIVE_REMOVE,
/**< an AO has been removed from QF (stopped) */
QS_QF_ACTIVE_SUBSCRIBE,
/**< an AO subscribed to an event */
QS_QF_ACTIVE_UNSUBSCRIBE,
/**< an AO unsubscribed to an event */
QS_QF_ACTIVE_POST_FIFO,
/**< an event was posted (FIFO) directly to AO */
...
/* QK records */
(5)
QS_QK_MUTEX_LOCK,
/**< the QK mutex was locked */
QS_QK_MUTEX_UNLOCK,
/**< the QK mutex was unlocked */
QS_QK_SCHEDULE,
/**< the QK scheduled a new task to execute */
...
/* Miscellaneous QS records */
(6)
QS_SIG_DICTIONARY,
/**< signal dictionary entry */
QS_OBJ_DICTIONARY,
/**< object dictionary entry */
QS_FUN_DICTIONARY,
/**< function dictionary entry */
QS_ASSERT,
/** assertion failed */
...
/* User records */
(7)
QS_USER
/**< the first record available for user QS records */
};
...
/* Macros for adding QS instrumentation to the client code .................*/
(8)
#define QS_INIT(arg_)
QS_onStartup(arg_)
(9)
#define QS_EXIT()
QS_onCleanup()
(10)
#define QS_FILTER_ON(rec_)
QS_filterOn(rec_)
(11)
#define QS_FILTER_OFF(rec_)
QS_filterOff(rec_)
(12)
#define QS_FILTER_SM_OBJ(obj_) (QS_smObj_ = (obj_))
#define QS_FILTER_AO_OBJ(obj_) (QS_aoObj_ = (obj_))
#define QS_FILTER_MP_OBJ(obj_) (QS_mpObj_ = (obj_))
#define QS_FILTER_EQ_OBJ(obj_) (QS_eqObj_ = (obj_))
#define QS_FILTER_TE_OBJ(obj_) (QS_teObj_ = (obj_))
#define QS_FILTER_AP_OBJ(obj_) (QS_apObj_ = (obj_))
/* Macros to generate user QS records (formatted data output) ..............*/
(13)
#define QS_BEGIN(rec_, obj_)
...
(14)
#define QS_END()
...
(15)
#define QS_BEGIN_NOLOCK(rec_, obj_) ...
(16)
#define QS_END_NOLOCK()
...
...
(17)
#define QS_I8 (w_, d_)
QS_u8((uint8_t) (((w_) << 4)) | QS_I8_T,
(d_))
(18)
#define QS_U8 (w_, d_)
QS_u8((uint8_t) (((w_) << 4)) | QS_U8_T,
(d_))
#define QS_I16(w_, d_)
QS_u16((uint8_t)(((w_) << 4)) | QS_I16_T, (d_))
#define QS_U16(w_, d_)
QS_u16((uint8_t)(((w_) << 4)) | QS_U16_T, (d_))
#define QS_I32(w_, d_)
QS_u32((uint8_t)(((w_) << 4)) | QS_I32_T, (d_))
#define QS_U32(w_, d_)
QS_u32((uint8_t)(((w_) << 4)) | QS_U32_T, (d_))
(19)
#define QS_F32(w_, d_)
QS_f32((uint8_t)(((w_) << 4)) | QS_F32_T, (d_))
(20)
#define QS_F64(w_, d_)
QS_f64((uint8_t)(((w_) << 4)) | QS_F64_T, (d_))
(21)
#define QS_STR(str_)
QS_str(str_)
(22)
#define QS_STR_ROM(str_)
QS_str_ROM(str_)
(23)
#define QS_MEM(mem_, size_)
QS_mem((mem_), (size_))
#if (QS_OBJ_PTR_SIZE == 1)
(24)
#define QS_OBJ(obj_)
QS_u8(QS_OBJ_T, (uint8_t)(obj_))
#elif (QS_OBJ_PTR_SIZE == 2)
(25)
#define QS_OBJ(obj_)
QS_u16(QS_OBJ_T, (uint16_t)(obj_))
#elif (QS_OBJ_PTR_SIZE == 4)
(26)
#define QS_OBJ(obj_)
QS_u32(QS_OBJ_T, (uint32_t)(obj_))
#else
(27)
#define QS_OBJ(obj_)
QS_u32(QS_OBJ_T, (uint32_t)(obj_))
#endif
#if (QS_FUN_PTR_SIZE == 1)
(28)
#define QS_FUN(fun_)
QS_u8(QS_FUN_T, (uint8_t)(fun_))
#elif (QS_FUN_PTR_SIZE == 2)
...
#endif
#if (Q_SIGNAL_SIZE == 1)
(29)
#define QS_SIG(sig_, obj_) QS_u8 (QS_SIG_T, (sig_)); QS_OBJ_(obj_)
#elif (Q_SIGNAL_SIZE == 2)
...
#endif
/* Dictionary records ...................................................*/
(30)
#define QS_OBJ_DICTIONARY(obj_) ...
(31)
#define QS_FUN_DICTIONARY(fun_) ...
(32)
#define QS_SIG_DICTIONARY(sig_, obj_) ...
...
/* Macros used only internally in the QP code ..............................*/
(33)
#define QS_BEGIN_(rec_, obj_)
...
(34)
#define QS_END_()
...
(35)
#define QS_BEGIN_NOLOCK_(rec_, obj_) ...
(36)
#define QS_END_NOLOCK_()
...
/* QS functions for managing the QS trace buffer ...........................*/
(37)
void QS_initBuf(uint8_t sto[], uint32_t stoSize);
(38)
uint16_t QS_getByte(void);
/* byte-oriented interface */
(39)
uint8_t const *QS_getBlock(uint16_t *pNbytes); /* block-oriented interface */
/* QS callback functions, typically implemented in the BSP .................*/
(40)
uint8_t QS_onStartup(void const *arg);
(41)
void QS_onCleanup(void);
(42)
void QS_onFlush(void);
(43)
QSTimeCtr QS_onGetTime(void);
#endif
/* qs_h */
4.3 qs_dummy.h 文件内容
以下是 qs_dummy.h 文件的部分内容:
#ifndef qs_dummy_h
#define qs_dummy_h
#ifdef Q_SPY
(1)
#error "Q_SPY must NOT be defined to include qs_dummy.h"
#endif
(2)
#define QS_INIT(arg_)
((uint8_t)1)
(3)
#define QS_EXIT()
((void)0)
#define QS_DUMP()
((void)0)
#define QS_FILTER_ON(rec_)
((void)0)
#define QS_FILTER_OFF(rec_)
((void)0)
#define QS_FILTER_SM_OBJ(obj_)
((void)0)
...
(4)
#define QS_GET_BYTE(pByte_)
((uint16_t)0xFFFF)
(5)
#define QS_GET_BLOCK(pSize_)
((uint8_t *)0)
(6)
#define QS_BEGIN(rec_, obj_)
if (0) {
(7)
#define QS_END()
}
#define QS_BEGIN_NOLOCK(rec_, obj_)
QS_BEGIN(rec_, obj_)
#define QS_END_NOLOCK()
QS_END()
#define QS_I8(width_, data_)
((void)0)
#define QS_U8(width_, data_)
((void)0)
...
#define QS_SIG(sig_, obj_)
((void)0)
#define QS_OBJ(obj_)
((void)0)
#define QS_FUN(fun_)
((void)0)
#define QS_SIG_DICTIONARY(sig_, obj_) ((void)0)
#define QS_OBJ_DICTIONARY(obj_)
((void)0)
#define QS_FUN_DICTIONARY(fun_)
((void)0)
#define QS_FLUSH()
((void)0)
...
#endif
/* qs_dummy_h */
4.4 头文件相关说明
- qs.h :指定了所有 QS 工具的活动接口,若未定义 Q_SPY 宏而包含该文件,会报告编译时错误。枚举 QSpyRecords 定义了所有标准 QS 记录类型,每个 QP 组件会生成特定的 QS 记录类型,同时可通过添加用户定义的记录扩展 QS 记录列表,用户记录必须从 QS_USER 确定的数值开始。
- qs_dummy.h :指定了非活动的 QS 接口,若在定义了 Q_SPY 宏时包含该文件,会报告编译时错误。大多数 QS 虚拟宏被定义为 ((void)0),用于在未启用追踪时避免生成代码。
5. QS 临界区
QS 目标组件必须保护跟踪缓冲区的内部完整性,该缓冲区在并发运行的任务和中断之间共享。为保证对跟踪缓冲区的互斥访问,QS 使用与 QP 平台其余部分相同的机制,即在进入代码的临界区时锁定中断,在退出时解锁中断。
以下是在代码中使用 QS 临界区的步骤:
1. 在需要使用 QS 记录的代码段开始处,使用 QS_BEGIN() 宏锁定中断。
2. 在代码段中进行数据记录操作。
3. 在代码段结束时,使用 QS_END() 宏解锁中断。
应避免生成大的跟踪记录,因为这可能会延长中断延迟。
综上所述,QS 是一个功能强大的软件追踪系统,通过合理利用其各种特性和工具,可以高效地进行软件调试和性能分析。
6. QS 过滤器的使用与优势
6.1 过滤器的工作原理
QS 提供了两级互补的过滤器,为软件追踪带来了极大的灵活性和选择性。
全局过滤器
全局过滤器基于跟踪记录类型进行过滤。例如,当我们关注状态机的状态转换时,可以通过全局过滤器只追踪状态进入(如
QS_QEP_STATE_ENTRY
)和状态退出(如
QS_QEP_STATE_EXIT
)的记录。这种过滤方式对整个系统中的所有状态机和事件发布全局生效,能够帮助我们快速定位系统中状态机的整体行为。
组件特定过滤器
组件特定过滤器允许我们针对特定的组件进行追踪。比如,我们只对某个特定的状态机对象感兴趣,就可以设置组件特定过滤器,只追踪该对象的相关记录。这样可以避免被大量无关的追踪信息干扰,聚焦于我们关心的组件。
6.2 过滤器的操作步骤
以下是使用 QS 过滤器的具体操作步骤:
1.
全局过滤器的设置
- 若要开启某种类型的跟踪记录,使用
QS_FILTER_ON(rec_)
宏。例如,要开启状态进入记录的追踪,可以使用
QS_FILTER_ON(QS_QEP_STATE_ENTRY)
。
- 若要关闭某种类型的跟踪记录,使用
QS_FILTER_OFF(rec_)
宏。比如,关闭事件发布记录的追踪,可使用
QS_FILTER_OFF(QS_QF_ACTIVE_POST_FIFO)
。
2.
组件特定过滤器的设置
- 若要追踪特定的状态机对象,使用
QS_FILTER_SM_OBJ(obj_)
宏。例如,有一个状态机对象
smObj
,要追踪它的相关记录,可使用
QS_FILTER_SM_OBJ(smObj)
。
- 对于其他类型的对象,如活跃对象(AO)、消息池对象(MP)等,也有相应的过滤器宏,如
QS_FILTER_AO_OBJ(obj_)
、
QS_FILTER_MP_OBJ(obj_)
等,使用方法类似。
6.3 过滤器的优势
通过使用这两级互补的过滤器,我们可以实现非常有选择性的追踪功能。在复杂的系统中,可能会有大量的状态机和事件发布,使用过滤器可以帮助我们只获取我们需要的信息,减少追踪数据的量,提高调试和分析的效率。同时,我们可以根据不同的调试需求,灵活组合使用这两级过滤器,以满足各种场景的要求。
以下是一个简单的表格,总结了 QS 过滤器的相关信息:
| 过滤器类型 | 功能 | 操作宏 |
| ---- | ---- | ---- |
| 全局过滤器 | 基于跟踪记录类型过滤 |
QS_FILTER_ON(rec_)
、
QS_FILTER_OFF(rec_)
|
| 组件特定过滤器 | 针对特定组件过滤 |
QS_FILTER_SM_OBJ(obj_)
等 |
7. QS 数据传输协议与 API
7.1 数据传输协议
QS 的数据传输协议是其一大优势。它非常轻量级,但具备高级数据链路控制(HDLC)协议的许多元素。该协议能够检测传输错误,当出现数据丢失(如由于 RAM 缓冲区溢出导致的数据丢失)等错误时,允许立即重新同步。
7.2 数据传输 API
QS 包含一个轻量级的 API 用于实现向主机的数据传输。这个 API 支持多种实现策略和物理传输层:
-
实现策略
:支持轮询、中断、DMA 等多种方式。例如,在资源有限的系统中,我们可以使用轮询方式进行数据传输;而在对实时性要求较高的系统中,可以使用中断或 DMA 方式。
-
物理传输层
:支持串口、SPI、USB、以太网、数据文件等多种物理传输层。我们可以根据系统的实际情况选择合适的传输层。比如,在嵌入式系统中,串口是一种常用的传输方式;而在网络环境中,以太网则更为合适。
7.3 使用数据传输 API 的步骤
以下是使用 QS 数据传输 API 的一般步骤:
1.
初始化传输
:在系统启动时,调用
QS_onStartup(arg_)
函数进行初始化。该函数会调用
QS_initBuf(sto[], stoSize)
函数初始化 QS 缓冲区,并返回初始化状态。
2.
数据记录
:在需要记录数据的地方,使用各种 QS 宏(如
QS_BEGIN()
、
QS_I8()
等)将数据记录到缓冲区中。
3.
数据传输
:在合适的时机(如在目标 CPU 的空闲循环中),调用
QS_onFlush()
函数将缓冲区中的数据发送到主机。
4.
清理操作
:在系统关闭时,调用
QS_onCleanup()
函数进行清理操作。
以下是一个简单的 mermaid 流程图,展示了 QS 数据传输的流程:
graph TD;
A[系统启动] --> B[QS_onStartup() 初始化];
B --> C[数据记录到缓冲区];
C --> D[QS_onFlush() 发送数据];
D --> E{是否继续记录};
E -- 是 --> C;
E -- 否 --> F[系统关闭];
F --> G[QS_onCleanup() 清理];
8. QS 在实际应用中的注意事项
8.1 避免大的跟踪记录
如前文所述,应避免生成大的跟踪记录,因为这可能会延长中断延迟。在设计代码时,我们应该尽量将大的操作分解为多个小的记录,或者根据实际需求选择合适的记录粒度。
8.2 合理使用过滤器
过滤器是 QS 的重要特性之一,合理使用过滤器可以提高调试和分析的效率。在开始追踪之前,我们应该明确我们需要关注的信息,然后根据这些信息设置合适的过滤器。同时,在调试过程中,我们可以根据实际情况动态调整过滤器的设置。
8.3 选择合适的时间戳大小
QS 的时间戳大小可配置为 1、2 或 4 字节。我们应该根据系统的实际需求选择合适的时间戳大小。如果系统对时间精度要求不高,选择较小的时间戳大小可以节省存储空间;如果需要高精度的时间信息,则选择较大的时间戳大小。
8.4 处理传输错误
虽然 QS 的数据传输协议具备检测和重新同步的能力,但在实际应用中,我们仍然需要考虑传输错误的情况。可以在代码中添加适当的错误处理机制,如在
QS_onFlush()
函数中检测传输错误并进行相应的处理。
9. 总结
QS 作为一个功能强大的软件追踪系统,为事件驱动系统的调试和分析提供了有力的支持。通过其环形缓冲区、过滤器、数据传输协议和轻量级 API 等特性,我们可以高效地进行软件追踪,获取系统的详细运行信息。
在使用 QS 时,我们需要了解其各个组件的工作原理和使用方法,合理利用过滤器、时间戳等特性,避免生成大的跟踪记录,选择合适的传输方式和时间戳大小,并处理好传输错误等问题。通过合理使用 QS,我们可以更快速地定位系统中的问题,提高软件的质量和性能。
希望本文能够帮助读者更好地理解和使用 QS 软件追踪系统,在实际的软件开发和调试中发挥其优势。
超级会员免费看
247

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



