# dtest **Repository Path**: Lamdonn/dtest ## Basic Information - **Project Name**: dtest - **Description**: dtest (Dynamic Test) - 轻量级动态 C 语言调试框架,一个极度轻便、极具可移植性的纯 C 语言应用层调试与观测框架。它旨在为不方便连接仿真器,或需要进行高频动态观测与控制的嵌入式系统(及 PC 软件)提供远程交互手段,您可以在不修改或重新编译代码的情况下,在系统运行时动态读取/修改内存变量、配置周期信号上报、甚至直接传入地址调用系统内的任意函数(支持传参)。 - **Primary Language**: C/C++ - **License**: GPL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-06-16 - **Last Updated**: 2026-08-01 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # dtest (Dynamic Test) - 轻量级动态 C 语言调试框架 `dtest` 是一个极度轻便、极具可移植性的纯 C 语言应用层调试与观测框架。它旨在为不方便连接仿真器,或需要进行高频动态观测与控制的嵌入式系统(及 PC 软件)提供一套类似 UDS (Unified Diagnostic Services) 的远程交互手段。 通过 `dtest`,您可以在**不修改或重新编译代码**的情况下,在系统运行时动态读取/修改内存变量、配置周期信号上报、甚至直接传入地址调用系统内的任意函数(支持传参)。 ## 🌟 核心特性 - **极致轻量且无依赖**:纯 C99 实现,不依赖任何特定硬件平台或底层库,可部署于任何 32 位或 64 位系统。 - **UDS 风格的协议分发**:支持请求/正响应/负响应标准处理结构,统一错误码 (NRC) 反馈。 - **动态内存读写 (0x22 / 0x2E)**:基于地址抓取和修改任意内存空间数据。 - **信号周期监控 (0x2A)**:在运行时动态设定监控的内存地址和组包偏移,组件会自动周期发送变量状态,防止频繁轮询。 - **高阶动态函数调用 (0x32)**:只要提供函数在内存中的地址及其参数,组件即可在运行时强转映射执行,**支持高达16个任意类型(U8-U64/F32-F64)组合参数传递及结果返回**。 - **后台周期任务 (0x33)**:支持将任意函数及运行参数注册到后台,以指定的周期(毫秒)自动循环执行。 - **命令队列机制**:接收与执行分离,可在底层高频/硬件中断中安全接收命令,推迟到主循环统一处理。 - **内存控制权让渡**:组件内不使用 `malloc`,所有结构体内存池均由用户在外部静态分配后注入,彻底杜绝内存碎片与非法溢出。 - **多核平台数据路由**:针对多核的芯片平台,支持一主多从模式,上位机仅需与主核交互即可将数据路由到从核。 ## 🏗️ 框架架构与数据流向 ### 1. 框架 ![Framework](image/framework.png) ### 2. 数据流向 `dtest` 的核心设计理念是**“数据解耦与异步执行”**。 1. `dtest_receive()` 将底层硬件中断收到的串口数据极速压入无锁缓存队列并立即返回。 2. 主循环 `dtest_task()` 弹出指令、解析大端序数据并分配给对应的动作句柄(如强转指针进行读取)。 3. 使用挂载的 `tx_cb()` 同步/异步返回执行结果 (Positive/Negative Response) 给上位机。 ![DataFlow](image/DataFlow.png) ## 🚀 快速集成与移植指南 `dtest` 的移植极为简便。只需完成底层收发对接与内存池注入即可。 ### 1. 准备底层发送函数 实现一个能将字节流发往外部的数据发送接口: ```c dtest_tx_status_t my_uart_tx(const uint8_t *data, uint16_t len) { // 如果是同步阻塞发送 // uart_send_blocking(data, len); // return DTEST_TX_OK; // 如果是异步发送 (例如启动 DMA 控制器) if (uart_dma_send(data, len) == OK) { return DTEST_TX_PENDING; } return DTEST_TX_ERROR; } // 对于异步发送,可以配套提供一个状态检查接口 dtest_tx_status_t my_uart_check_tx(void) { if (uart_dma_is_busy()) { return DTEST_TX_PENDING; } return DTEST_TX_OK; } ``` ### 2. 初始化 dtest 在系统启动时,将发送回调注入到组件: ```c dtest_config_t config = {0}; config.tx_cb = my_uart_tx; // 如果需要使用信号、预置函数等高级功能,在此挂载内存池,例如: // config.signal_pool = user_signal_array; // config.max_signals = 16; dtest_init(&config); ``` ### 3. 接收数据投递 当底层(如串口接收中断、网络接收任务)收到数据时,直接喂给 `dtest` 的接收队列: ```c void on_uart_receive(const uint8_t *rx_data, uint16_t len) { dtest_receive(rx_data, len); } ``` ### 4. 放置周期任务 在您的主循环(While 1)或 RTOS 任务中,周期性调用 `dtest_task`: ```c while (1) { // 假设循环每 10ms 运行一次 dtest_task(10); delay_ms(10); } ``` ### 5. 本地终端体验 组件内置了一个交互式测试终端 `main.c`,您可以使用它在本地 PC 环境(Windows 或 Linux)模拟测试整个通信和控制过程。 **编译运行**: ```bash make ./built/bin/dtest ``` **运行效果**: 控制台会提示交互环境已启动,输入十六进制请求(空格分隔)后按回车,即可观测底层的行为。例如: ```text === Interactive dtest Terminal === Task running at 10ms interval. Press Ctrl+C to exit. Enter hex commands separated by space. Example (Read Memory g_temperature): 22 00 00 00 00 00 40 40 20 00 02 ``` 将 Example 中的指令复制粘贴并发送,您将看到终端回复相应的十六进制数据(模拟收发过程)。 ### 6. 本地GUI体验 利用 `main.c` 开启 `USE_VIM_COMM` 宏,您可以直接在 PC 上体验下位机与 GUI 的全真互联通信: 1. **编译下位机模拟器**: ```bash make ./built/bin/dtest ``` 此时模拟器会进入等待状态。 2. **启动可视化上位机**: ```bash python ./gui/gui.py ``` 上位机启动后会自动连接下位机管道,您可以在 GUI 中尽情尝试发包、观测信号波形以及调用内部函数! ### 7. 进阶宏配置与定制 在 `core/dtest.h` 的顶部,您可以根据实际需求对内存限额进行微调: ```c /* --- Core Configuration --- */ // [核心] 模块内部收发缓冲区的最大尺寸 (字节)。该值决定了一次性可抓取或覆写的最大内存容量,建议 >=128。 #define DTEST_FRAME_MAX_LEN 256 // [信号] 针对周期抓取组包发送 (0xAA) 的最大封包长度。该值不能超过 DTEST_FRAME_MAX_LEN。 #define DTEST_SIGNAL_FRAME_LEN 64 // [队列] 异步接收缓冲区的命令队列深度。若单次发来的命令极多且 `dtest_task` 调度极其缓慢,可适度增大以抗突发流量。 #define DTEST_CMD_QUEUE_SIZE 1 ``` *(如果您是移植到 64 位的 x86 或 ARM64 Linux 平台进行测试,`DTEST_PTR_SIZE` 宏会自动推导为 8 字节,与 32 位 MCU (4 字节) 完美兼容互通。)* ## 🖥️ 上位机 GUI 工具使用指南 ![ConnectionView](image/ConnectionView.png) `gui.py` 启动后,提供多个选项卡实现不同维度的调试功能。 * **侧边栏**:提供不同页面的选择。 * **顶部栏**:可以配置目标设备的架构32位或者64位,可以选择命令发到哪个内核(0~15,主核为0),可以查看与设备的连接状态,同时消息弹窗也会弹出在顶部栏左侧。 * **底部栏**:交互命令操作区,可以在这里输入原始命令进行请求,以及收到的响应也会显示到此。同时可以配置命令响应的超时时间,以及显示命令执行状态。一般情况下,在其他页面中直接点击按钮即可生成命令到此并发送,以及收到响应完成解析。 ### `🔌` Connection (连接配置) 在连接页面中,您可以选择现已支持的常用连接方式,以及配置连接的参数。 #### 1. VIM虚拟接口 ![ConnectionVim](image/ConnectionVim.png) 通过PC的管道功能实现,可以用于本地的通信测试。 * **VIM Role**: 作为上位机,选择 RBS(Tester) 即可 * **Mask**: 用于标识VIM,需和下位机配置一致 #### 2. Serial串行接口 ![ConnectionSerial](image/ConnectionSerial.png) 串行接口,可以用于 UART、RS232、RS485等串行接口,依赖于SLUP串行链路通用协议。 * **Port**: PC端口名,类似 COM1、COM2等 * **Baudrate**: 端口波特率 * **Timeout**: 超时时间,默认配置为0 * **SLUP Head**: SLUP协议配置帧头,HEX格式,用空格分开,最大支持4字节帧头,应根据使用场景选择不常用的数据作为帧头 * **SLUP Tail**: SLUP协议配置帧尾(可不配置),HEX格式,用空格分开,最大支持4字节帧尾,应根据使用场景选择不常用的数据作为帧尾 #### 3. Network网络接口 ![ConnectionNetwork](image/ConnectionNetwork.png) 网络接口,可以用于 ETH、WIFI等网络设备接口,除了本身网络,还需依赖于SLUP串行链路通用协议以防止粘包等原因导致数据解析出错以提高通信稳定性。 * **Protocol**: 协议类型,可以选择TCP或者UDP * **Timeout**: Socket 超时时间,默认配置为0.5s * **Local IP**: 本地端测试上位机IP地址 * **Local Port**: 本地端测试上位机端口,0则是任意端口 * **Target IP**: 下位机IP地址 * **Target Port**: 下位机端口 * **SLUP Head**: SLUP协议配置帧头,HEX格式,用空格分开,最大支持4字节帧头,应根据使用场景选择不常用的数据作为帧头 * **SLUP Tail**: SLUP协议配置帧尾(可不配置),HEX格式,用空格分开,最大支持4字节帧尾,应根据使用场景选择不常用的数据作为帧尾 #### 4. CAN总线接口 ![ConnectionCAN](image/ConnectionCAN.png) CAN接口,常用于车载ECU,依赖CanTp协议栈。 * **Bus Type**: CAN Bus类型,支持 socketcan、pcan、virtual,以及主流的 Vector CANoe * **Channel**: CAN 通道,根据不同 Bus 类型填入合适通道,如 CANoe VN1640 channel 1 ~ 4,则是填入 0 ~ 3 * **App Name**: App 名,针对CAN分析仪驱动提供给应用的配置,如 CANoe则可以使用默认APP名 **CANoe** * **Init HW**: 是否需要初始化CAN分析仪硬件设备,主要配置波特率、如CANoe:单独使用dtest上位机软件则需要勾选此选项来初始化CANoe盒子,如和CANoe仿真工程一并时候则可以不勾选次选项而由CANoe仿真工程来初始化CANoe盒子 * **TX ID**: CanTp协议栈中配置的 TX CANID,由上位机发给下位机 * **RX ID**: CanTp协议栈中配置的 RX CANID,由下位机发给上位机 * **Enable CAN-FD**: 是否使能CAN-FD * **Bitrate**: CAN通信比特率 * **Data Bitrate**: 数据段比特率,针对CAN-FD * **Enable BRS**: 使能BRS ### `💾` Memory (内存读写) ![MemoryView](image/MemoryView.png) 在内存读写功能中,可以基于物理地址的原始读写,适合调试底层寄存器。 #### 1. 读内存 **Address**: 读写内存的地址,十六进制 **Size**: 读写内存的大小,十进制 **Data**: 读取到的内存数据,十六进制byte形式,空格分隔 #### 2. 写内存 **Address**: 读写内存的地址,十六进制 **Size**: 读写内存的大小,十进制 **Data**: 写到内存的数据,十六进制byte形式,空格分隔 ### `📡` Signal (信号配置及读取) ![SignalView](image/SignalView.png) 在信号页面,您可以配置信号以及读取信号。相对比直接读取内存,信号相当于给内存添加上信号名。 信号配置,可以静态配置编译好在下位机(静态配置),也可以在上位机动态配置下发到下位机(动态配置)。但不管静态配置还是动态配置,在上位机均需要定义好信号表来描述及解析信号(针对静态配置,信号表可以省略信号地址的配置,已经静态配置在下位机指定地址,上位机只需解析即可),信号表定义参考 [1. 周期信号观测配置表 (signals.csv)](#1-周期信号观测配置表-signalscsv)。 #### 1. 信号配置 **Load Config** : 点击此按钮,可以加载定义好的信号表,gui启动时候也会默认加载 `gui\config\signals.csv` **Save Config** : 点击此按钮,可以将当前信号配置保存回磁盘。 **Send Config to Device** : 点击此按钮,上位机将会一键把信号表配置好的信号逐个动态配置到下位机。 #### 2. 信号控制 **Enable Cycle** : 使能周期信号,使能后下位机将会周期发送已经配置好的信号 **Disable Cycle** : 失能周期信号,失能后下位机将会停止周期发送信号 **Cycle Ms** : 下位机发送信号的周期,单位 ms **Enable Dyn Len** : 使能信号动态长度,下位机发送的周期信号帧会根据现配置好的信号,动态适配长度发送 **Disable Dyn Len** : 失能动态信号长度,失能后不管现配置有多少个信号,都将会按预设好的固定长度发送 **Clear All Signals** : 清除所有信号配置 #### 3. 信号读取 **Signal** : 下拉框选择已经加载的信号表信号,选择后将会自动填充 信号ID 和 信号类型 **ID** : 信号ID,配置好的信号位置索引即为信号ID,从0开始 **Type** : 信号类型,下拉选择信号的配置类型 **Read** : 点击此按钮,将会读取指定信号 **Value** : 读取到的信号值将会按照类型显示在此 #### 4. 信号添加 **Signal Address** : 信号地址,如果是变量可以通过map等符号文件来确定地址 **Signal Size** : 信号大小,字节 **Frame Offset** : 信号在信号报文中的偏移位置 **Add Signal** : 点击此按钮,将信号配置发送到下位机进行配置 ### ⚙️ Function (函数调用) ![FunctionView](image/FunctionView.png) 极具魔法的高级功能,适用于运行时强行干预系统状态,允许调用一般任意函数类型的函数。 #### 1. 函数原型 gui启动时候也会默认加载 `gui\config\function_proto.json` 函数配置,将常用的函数参数列表保存为 Prototype,支持一键保存到 `function_proto.json` 并复用。 **Select Function** : 下拉框选择已经加载配置好的函数 **Load Function** : 将选中的函数的配置加载到gui中,包括函数地址、函数返回值、函数参数等 **Function Name** : 函数名字 **Save Function** : 将当前函数配置保存为 `Function Name` 指定的函数原型,不存在则是新增 #### 2. 函数参数 支持添加高达 16 个参数,并可分别配置它们为 `IN` (输入)、`OUT` (输出指针,获取返回值) 类型及 `U8-F64` 类型。 **Arg xx** : 复选框选择,函数存在这个参数则勾上 **Type** : 支持选择常用的基本类型,`VOID`、`U8`、`I8`、`U16`、`I16`、`U32`、`I32`、`U64`、`I64`、`F32`、`F64`、`BYTES` **Dir** : 形参的数据方向,`IN`、`OUT`、`INOUT` **Value** : 输入的参数值,或者输出的值 #### 3. 函数执行 函数执行,可以执行下位机静态配置好的函数,按照函数索引来定位函数;也可以按照函数地址进行动态调用函数,则需要根据程序的符号文件来确定函数地址。函数执行结束后,会将函数返回值返回(配置了返回值为非void),并将参数为输出方向的参数值回显。 **Func ID** : 配置好的函数位置索引即为信号ID,从0开始 **Ret Type** : 函数返回值类型,支持选择常用的基本类型,`VOID`、`U8`、`I8`、`U16`、`I16`、`U32`、`I32`、`U64`、`I64`、`F32`、`F64`、`BYTES` **Ret Val** : 返回值回显 **Static Call** : 执行静态配置函数 **Func Addr** : 函数地址,十六进制 **Ret Type** : 函数返回值类型,支持选择常用的基本类型,`VOID`、`U8`、`I8`、`U16`、`I16`、`U32`、`I32`、`U64`、`I64`、`F32`、`F64`、`BYTES` **Ret Val** : 返回值回显 **Dynamic Call** : 执行动态配置函数 ### ⏱️ Task (周期任务) ![TaskView](image/TaskView.png) 与Function (函数调用)功能最大的区别在与,Task (周期任务)功能将写好的调用逻辑以固定周期(如 1000ms)挂载在下位机后台持续运行,无需上位机一直驻留。 在配置上,函数原型、函数参数配置与Function (函数调用)功能是一致的。 #### 1. 任务配置 **Func Addr** : 需要执行任务的函数地址,十六进制 **Cycle Ms** : 任务执行周期,单位 ms **Ret Type** : 函数返回值类型,执行中上位机没有获取返回值,支持选择常用的基本类型,`VOID`、`U8`、`I8`、`U16`、`I16`、`U32`、`I32`、`U64`、`I64`、`F32`、`F64`、`BYTES` **Add Task** : 点击此按钮,则将会配置执行该任务 **Func Addr** : 需要删除任务的函数地址,十六进制 **Delete Task** : 点击此按钮,则将会删除该任务 **Clear All Tasks** : 点击此按钮,则将会清空所有周期任务 ### 📊 Dashboard (信号仪表盘) ![DashboardView](image/DashboardView.png) 在仪表盘中,您可以**实时、直观**地观测下位机的任何内存变量,且自带超限告警,UI 表格会自动解析提取真实物理值,当信号越过 CSV 定义的安全阈值 (Min/Max) 时,背景会变为红色并累计错误。并会根据信号状态变成相应红黄绿色标识。 该功能需要搭配 `Signal (信号配置及读取)` 功能使用,配置信号以及启动周期监控。 #### 1. 日志保存 信号仪表盘支持自动记录信号日志,勾选上 `Auto-save Log` 复选框即可以自动保存信号日志,保存的日志将会以表格形式保存在`log` 目录,会以录制时间为文件名区分。 #### 2. 信号筛选 信号仪表盘支持信号筛选,可以按照特定需求筛选需要展示的信号(但不影响日志保存)。可以按照 `Core`筛选来自不同内核的信号,可以`Name`筛选指定展示的信号,可以按照`Group`来筛选特定的信号组,可以按照`Status`筛选特定状态的信号(如红黄绿)。 #### 3. 信号摘要 信号摘要,会简单展示当前信号总的状态,包括有没有信号出现了报警,以及哪些信号组出现了报警。 #### 4. 信号波形 点击 `Graph` 按钮即可以进入信号波形图界面,在 `Graph` 页面您可以看到信号实时变化的折线图 ![Graph](image/Graph.png) `Graph`页面除了包含了 `Dashboard` 中的一些基本功能(包括过滤器、基本数值显示),还包含了 `Graph` 设置功能。 **Source** : 在这里可以选择绘制信号折线图的数据来源,就包括是实时数据和历史数据(就是前面录制的Signal日志) **Y-Axis** : 可以选择全部信号共享一个Y轴(所有信号在同一个图表中完成绘制)和分割Y轴(每个信号共用时间X轴而单独Y轴独立显示) **Marker** : 针对信号更新点的标记点形式,可以选择圆形、星行、正方形以及没有标记点。 **Y-Scale** : 调整Y轴显示范围,可以选择自动适配(可以直观看到全部数据大小范围)和最大最小值范围(可以直观看出信号是否超过预期配置范围,哪个时间段超出了) **Resume Auto-Scale** : 恢复自动标尺,按照数据更新自动调整 ### 💻 Console () ![ConsoleView](image/ConsoleView.png) 终端视图主要展示 GUI 运行过程的日志信息。 #### 1. 日志筛选 终端视图支持日志筛选,可以按照特定需求筛选需要展示的日志类型,包括`Info`、`Warning`、`Error`、`TX`、`RX`类型 #### 2. 日志保存 终端视图支持自动记录信号日志,勾选上 `Auto-save Log` 复选框即可以自动保存信号日志,保存的日志将会以表格形式保存在`log` 目录,会以录制时间为文件名区分。同时也可以手动点击`Save Log`进行保存当前的日志。也可以点击 `Clear Console`清空当前终端的日志。 ### 📝 GUI 配置文件格式说明 上位机 GUI 的核心观测驱动依赖于配置文件,默认存放在 `config/` 目录下。 #### 1. 周期信号观测配置表 (`signals.csv`) 这是一个标准的 CSV 文件,您可以使用 Excel 或文本编辑器手动编写,用于指导 Dashboard 抓取、解析和判定目标变量。 | 列名 | 说明 | 示例 | | ----------------------------- | --------------------------------------------------------------------------- | ----------------- | | **SignalName** | 信号名称,作为面板中显示和过滤的唯一标识。 | `g_system_tick` | | **Group** | 信号所属的分组名,便于在界面中批量过滤和统计错误。 | `System` | | **Address** | 变量在下位机内存中的绝对物理地址(十六进制格式)。 | `0x20000004` | | **Size** | 变量在内存中实际占用的字节大小(十进制)。 | `4` | | **Offset** | 该变量在下位机上传的`0xAA` 报文 Payload 中的字节偏移量。 | `0` | | **Endian** | 数据的端序,填写`Little` (小端) 或 `Big` (大端)。 | `Little` | | **Type** | 数据基础类型 (`U8`/`I8`/`U16`/`U32`/`F32`/`F64`/`BYTES` 等)。 | `U32` | | **Factor** | 物理值换算系数(公式:`Phys = Raw * Factor + OffsetVal`)。 | `1.0` | | **OffsetVal** | 物理值换算偏移量。 | `0` | | **Min** / **Max** | 安全的上下限物理阈值。一旦超出此范围,仪表板即标红并累计错误。 | `0` / `255` | | **Unit** | 数据的物理单位文本(仅作显示用)。 | `ms` | **参考示例**: ```csv SignalName,Group,Address,Size,Offset,Endian,Type,Factor,OffsetVal,Min,Max,Unit g_system_tick,System,0x00000000,4,0,Little,U32,1.0,0,0,,ms g_temperature,Sensor,0x00000000,2,4,Little,U16,0.1,-40,-20,85,C ``` *(提示:`function_proto.json` 函数原型配置文件建议直接在 GUI 界面上通过填表保存生成,一般无需手动编写。)* ## 📖 协议规范 (UDS 风格) | Frame | Byte 0 | Byte 1 | Byte 2 | Byte 3 | ... | | ----------------- | ------- | ------------ | ------- | ------ | --- | | **Request** | Core ID | Service ID | Payload | ... | ... | | **Respond** | Core ID | Respond Type | ... | ... | ... | 帧结构由 `[Service ID] + [Payload...]` 组成。 正响应帧头为 `[SID + 0x40]`,负响应固定为 `[0x7F] [SID] [NRC]`。 > **注意:** 协议中的 `[Addr]` 字段长度根据目标系统的指针大小自适应 (`sizeof(void*)`)。在 32 位单片机中占 4 字节,在 64 位 PC 中占 8 字节。数据采用大端序 (Big-Endian) 传输。 ### 1. 支持的交互服务 (Service IDs) 概览 | SID (Hex) | 服务名称 | 功能描述 | | :-------: | :------------------------------------- | :----------------------------------------------------------------------------------------------- | | `0x22` | **Read Memory** (读内存) | 根据物理地址直接读取下位机指定长度的内存数据。 | | `0x2E` | **Write Memory** (写内存) | 根据物理地址直接覆写/篡改下位机指定长度的内存数据。 | | `0x2A` | **Config Signal** (配置周期信号) | 动态设定内存观测点,底层将在后台自动抓取快照并组装为`0xAA` 报文周期回传。 | | `0x2B` | **Read Signals** (即时读取信号) | 不等周期,立刻索要并回传当前配置好的所有(或单个)观测信号的最新内存状态快照。 | | `0x31` | **Call Static** (静态函数调用) | 通过短别名 ID,触发调用下位机预先通过`dtest_register_function` 注册好的特定调试函数。 | | `0x32` | **Call Dynamic** (动态高阶调用) | 提供目标函数的物理地址及其形参特征,在运行时强转类型并注入实参执行,支持提取执行结果与输出指针。 | | `0x33` | **Config PTask** (配置周期任务) | 挂载某个带参函数的调用请求至下位机后台,底层将按设定的毫秒周期自动循环唤醒执行。 | > **被动上报报文 (`0xAA`)**:当通过 `0x2A` 配置了周期信号观测且处于开启状态时,下位机会自动向外吐出以 `0xAA` 为首字节的数据帧,它不属于一问一答的请求/响应模型,而是单向的周期推流。 ### 2. 基本数据类型 (TypeID) 与方向指示 (Direction) 在动态调用 (`0x32`) 和后台任务 (`0x33`) 中,需要指明参数的类型和流向。协议将**流向控制位**(高 2 位)与**基础数据类型**(低 6 位)通过按位或 (`|`) 组合成一个 `TypeID` 字节。 | 基础数据类型 | 值 | 描述 |   | 数据流向 (Direction) | 值 | 描述 | | :---------------- | :---------------: | :------------------------ | :----: | :------------------- | :------: | :--------------------------------------------- | | `VOID` | `0x00` | 空类型 (无参数/无返回) | | `IN` (默认) | `0x00` | 输入参数 (值直接压栈传递) | | `U8` / `I8` | `0x01`/`0x02` | 8 位单字节整数 | | `OUT` | `0x40` | 输出参数 (传指针,不带初值) | | `U16` / `I16` | `0x03`/`0x04` | 16 位双字节整数 | | `INOUT` | `0x80` | 双向参数 (传指针,携带初值) | | `U32` / `I32` | `0x05`/`0x06` | 32 位四字节整数 | | - | - | *(如 `OUT U32` = 0x40 \| 0x05 = `0x45`)* | | `U64` / `I64` | `0x07`/`0x08` | 64 位八字节整数 | | - | - | - | | `F32` / `F64` | `0x09`/`0x0A` | 单/双精度浮点数 | | - | - | - | | `BYTES` | `0x0B` | 变长字节流 (`uint8_t*`) | | - | - | - | > **组包机制要点:** > > 1. **隐式指针转换**:标记为 `OUT` 和 `INOUT` 的参数,在 C底层调用时会自动**转为指针(地址)传递**。 > 2. **结果回收**:这些被标记为输出的参数,在目标函数执行完毕后,其最新结果会被系统提取,紧贴在函数的 `RetVal` 后,随正响应报文一同原路返回。 > 3. **变长字节流**:`BYTES` 类型参数的实参 Payload,必须以 2 字节的长度字段 `[LenH] [LenL]` 起头。 > 4. **跳过载荷**:纯 `OUT` 类型的参数由于不需初始值,其请求报文可直接省去该值载荷字段(注:`OUT BYTES` 仍需预留 2 字节指示分配尺寸)。 ### 3. 交互服务详细报文结构 #### 读内存 (Read Memory, SID: `0x22`) | 报文方向 | Byte 0 | Byte 1 ~`PTR_SIZE` | 追加载荷 (大端序) | | ---------------- | -------- | ------------------------------------ | -------------------------------------- | | **请求** | `0x22` | `Addr` (要读取的起始物理地址) | `Size` (2字节,请求读取的连续字节数) | | **正响应** | `0x62` | `Data...` (读取到的实际内存数据流) | - | #### 写内存 (Write Memory, SID: `0x2E`) | 报文方向 | Byte 0 | Byte 1 ~`PTR_SIZE` | 追加载荷 (大端序) | | ---------------- | -------- | ------------------------------- | --------------------------------------------------------- | | **请求** | `0x2E` | `Addr` (要写入的起始物理地址) | `Size` (2字节) + `Data...` (即 Size 长度的连续写入流) | | **正响应** | `0x6E` | - | *(无数据,仅代表执行成功)* | #### 配置周期信号 (Config Signal, SID: `0x2A`) 用于在底层建立周期性的数据抓取机制,目标变量被组装后会以 `0xAA` 为首字节自动上报。 | 操作动作 (Action) | 请求报文格式 (Byte 0 =`0x2A`) | 正响应 (`0x6A`) | | ---------------------------------- | ------------------------------------------------------------------ | ----------------- | | **新增监控节点** (`0x01`) | `[0x2A] [0x01] [Addr] [Size(2 bytes)] [Offset(2 bytes)]` | `[0x6A]` | | **清空所有节点** (`0x02`) | `[0x2A] [0x02]` | `[0x6A]` | | **启动/停止周期** (`0x03`) | `[0x2A] [0x03] [CycleMs(2 bytes)]` *(注:不带CycleMs则为停止)* | `[0x6A]` | | **开启动态长度** (`0x04`) | `[0x2A] [0x04] [Enable(1 byte)]` *(1:动态裁剪截断, 0:固定长)* | `[0x6A]` | #### 即时读取已配信号 (Read Configured Signals, SID: `0x2B`) | 报文方向 | Byte 0 | Byte 1 (可选) | 追加载荷 | | ---------------- | -------- | ----------------------------------------------------------------------- | -------- | | **请求** | `0x2B` | `SigID` (指定要读的池内索引。若省去此字节则直接读取全部) | - | | **正响应** | `0x6B` | `Payload...` (返回组包好的信号数据快照,结构与 `0xAA` 自动报文一致) | - | #### 静态预配函数调用 (Call Static, SID: `0x31`) 对于高频使用的固定业务代码,可在下位机预先通过 `dtest_register_function` 绑定短别名 ID。 | 报文方向 | Byte 0 | Byte 1 | 追加载荷 (根据下位机配置顺序拼接) | | ---------------- | -------- | ------------------------------------ | --------------------------------------------------- | | **请求** | `0x31` | `FuncID` (预先注册好的函数短别名) | `[Arg1_Val] [Arg2_Val]...` (依次填入实参数据流) | | **正响应** | `0x71` | `RetVal...` (函数本身的直接返回值) | `[OutArg_Val]...` (附带返回的所有 OUT 参数提取值) | #### 动态高阶函数调用 (Call Dynamic, SID: `0x32`) 极其危险但也极其强大的“上帝模式”:强转构造形参并执行任何内存地址位置的代码逻辑。 | 报文方向 | Byte 0 | Byte 1 ~`PTR_SIZE` | Byte (PTR+1) | Byte (PTR+2) | 追加循环载荷 (各参数的描述与实体) | | ---------------- | -------- | ------------------------------------ | --------------------------------------------------- | --------------------- | ---------------------------------------------------- | | **请求** | `0x32` | `FuncAddr` (物理绝对地址) | `RetTypeID` (返回值类型) | `ArgCnt` (参数总数) | `{ [ArgX_TypeID] [ArgX_Val] }...` (按顺序说明形参) | | **正响应** | `0x72` | `RetVal...` (函数本身的直接返回值) | `[OutArg_Val]...` (附带返回的所有 OUT 参数提取值) | - | - | #### 周期任务挂载 (Config Periodic Task, SID: `0x33`) 将特定函数的执行请求挂载在底层后台,由 `dtest_task` 按给定周期自动循环唤醒驱动。 | 操作动作 (Action) | 请求报文格式 (Byte 0 =`0x33`) | 正响应 (`0x73`) | | ---------------------------------- | ------------------------------------------------------------------------------------------- | ----------------- | | **新增/更新任务** (`0x01`) | `[0x33] [0x01] [FuncAddr] [CycleMs(2B)] [RetTypeID] [ArgCnt] { [Arg_Type] [Arg_Val] }...` | `[0x73]` | | **删除指定任务** (`0x02`) | `[0x33] [0x02] [FuncAddr]` | `[0x73]` | | **清空所有任务** (`0x03`) | `[0x33] [0x03]` | `[0x73]` | ### 4. 异常/负响应码 (NRC) 字典 当请求格式不符或目标不具备执行条件时,下位机会拒绝执行,并统一返回形如 `[0x7F] [请求的SID] [NRC]` 的报错。 | NRC | 名称 | 场景触发说明 | | -------- | ------------------------- | -------------------------------------------------------------------------------------------- | | `0x10` | General Reject | 一般性拒绝 (未知异常或发送被锁) | | `0x12` | SubFunction Not Supported | 不支持的子功能 (Action 动作码错误,或单片机内存池配置配额为 0 导致此功能被系统剥离禁用) | | `0x13` | Incorrect Message Length | 报文长度错误 (命令包被截断,或携带的实参数据量与传入的`TypeID` 所声明的合法尺寸不符) | | `0x31` | Out Of Range | 数值或索引越界 (读写长度逾越缓冲区上限,或添加信号/任务时超出了全局配置的阵列总数限制) | | `0x33` | Memory Overlap | (特有) 内存重叠 (仅在配置周期信号`0x2A` 时出现:指定的 `Offset + Size` 大于最大单帧上限) | ## 📊 资源占用与性能评估 `dtest` 专为资源受限的微控制器(MCU)设计,采用**零动态内存分配**(Zero `malloc`)策略。 ### 1. ROM (Flash) 占用 - **代码极简**:核心源文件 `dtest.c` 仅千余行,无任何庞大的第三方库依赖(仅需 ``, ``, ``)。 - **占用极低**:在 GCC `-O2` 优化级别下,编译后的完整功能二进制指令体积通常在 **2KB ~ 4KB** 之间(主要体积集中于高阶动态函数调用时的堆栈压入与大端序转换逻辑)。 ### 2. RAM (SRAM) 占用 RAM 的占用完全由用户掌控,所有结构体内存均在外部静态分配后注入,没有任何隐藏的内存开销。 - **基础上下文 (`dtest_ctx_t`)**:主要由 `tx_buffer` (默认256字节) 和 `cmd_queue` (默认256字节×深度) 组成,基础占用约 **550 字节**。 - **特性池开销 (按需开启)**: - **信号池** (`dtest_signal_t`):每个信号槽位约占用 `16` 字节。 - **静态函数池** (`dtest_function_t`):每个函数槽位约占用 `32` 字节。 - **周期任务池** (`dtest_periodic_t`):由于需要备份所有实参供后台执行,每个任务槽位约占用 `288` 字节。 ## 🔀 RTOS 与裸机部署指南 `dtest` 的架构天然支持“单生产者-单消费者”模式,非常适合在复杂的中断与实时操作系统(RTOS)环境下安全部署。 ### 1. 接收端 (ISR 安全) `dtest_receive()` 函数内部采用无锁环形队列(Lock-free Ring Buffer)设计,且没有任何耗时的循环或阻塞操作(仅做一次极速的 `memcpy` 压栈)。 - **部署建议**:强烈推荐直接将其放入底层的 **硬件接收中断 (如 UART RX ISR, CAN RX ISR)** 或高优先级的网络接收线程中。 - **并发警告**:若您的系统存在**多个**并发数据源同时向 `dtest_receive()` 投递数据,请在调用前使用互斥锁或关闭全局中断 (`DISABLE_INT()`) 以防止队列指针竞争。 ### 2. 执行端 (低优先级安全) `dtest_task(delta_ms)` 函数是框架的大脑,负责大段内存复制、协议解析甚至动态调用用户的业务函数。 - **部署建议**:由于其执行时间不确定(取决于外部调用的具体函数),绝对**不可以**放在任何中断上下文中运行。请将其放置于: - **裸机系统**:`main()` 函数的 `while(1)` 超级循环内。 - **RTOS 系统**:放置在优先级极低(如 IDLE 任务或低优先级的 Debug 任务)的后台线程中,给予合适的 `osDelay()` 并传入对应流逝的 `delta_ms`。 ### 3. 发送端 (支持异步 DMA) 注入的 `tx_cb` 发送回调可以自由选择“阻塞式发送”或“非阻塞异步发送”。 - 若底层硬件使用 DMA 异步搬运,`tx_cb` 可直接返回 `DTEST_TX_PENDING`。`dtest` 状态机会自动悬停锁定内部的发送缓冲区,直到您注入的 `tx_check_cb` 返回完成,彻底杜绝发送过程中的数据覆写踩踏。 ## 🛡️ 安全与生产环境警告 (Security Warning) ⚠️ **极度危险的上帝模式(God Mode)** `dtest` 赋予了外部通信接口**无限制地读写设备所有物理内存**、**调用所有内部函数**的最高权限。如果不加限制地被带入终端用户的生产环境中,将面临毁灭性的安全越权风险。 ### 推荐的量产防护措施: 1. **条件编译剥离**: 在 Release / 量产版本的 Firmware 编译链中,使用宏开关彻底剥离该组件: ```c #ifndef NDEBUG // 仅在 Debug 模式下启用 dtest_init(&config); dtest_receive(data, len); #endif ``` 2. **安全解锁鉴权 (UDS 0x27 风格)**: 若量产后仍需保留该后门用于售后故障分析,请在底层的串口接收回调中自行增加 **Seed & Key 挑战-响应** 握手层。只有在安全鉴权通过后,才允许将接收到的串口数据透传投递给 `dtest_receive()`。 3. **内存屏障限制**: 如果是基于 MPU/MMU 且具备权限隔离的复杂系统,请在外部通过 MPU 规则限制 `dtest_task` 所在线程的执行权限,防止它通过 `0x22/0x2E` 命令意外触碰或篡改核心保护区域(如 Flash Bootloader 扇区)。