4、交叉触发调试:硬件交叉触发机制、软件触发配置、事件路由设置

多核调试里,最让人头疼的问题是什么?

我觉得是「不知道谁先动了谁的奶酪」。两个核跑着跑着,一个挂了,另一个还在跑。你停下来查,现场早就没了。这时候,交叉触发(Cross-Trigger)就是你的救命稻草。

说白了,交叉触发就是让一个核的事件,能通知到另一个核。让它们能「同步停、同步跑、同步看现场」。我做过一个双核的项目,一个核处理传感器数据,另一个核做控制算法。传感器核崩了,控制核还在傻傻地发指令,结果执行器直接飞车。从那以后,我再也不敢轻视交叉触发配置了。

4.1 硬件交叉触发机制

Cortex-M 多核架构里,交叉触发靠的是 CTI(Cross-Trigger Interface) 和 CTM(Cross-Trigger Matrix)。这两个东西,你把它想象成一个「事件交换机」就行。

核心概念:CTI 是每个核的「门口」,CTM 是连接所有门口的「总线」。一个核的调试事件(比如断点命中、Watchpoint 触发)通过 CTI 送到 CTM,CTM 再广播给其他核的 CTI,其他核收到后执行对应的动作(比如 halt、reset)。

我习惯把 CTI 的通道分为两种:

  • 输入通道(Input Trigger):接收来自 CTM 或其他外设的事件。
  • 输出通道(Output Trigger):把本核的事件发出去。

每个 CTI 通常有 4 个输入通道和 4 个输出通道。具体数量看芯片厂商的实现,但基本够用。

通道类型 方向 典型用途
输入通道 CTM → CTI 接收其他核的 halt 信号
输出通道 CTI → CTM 发送本核的断点命中事件
调试请求通道 双向 连接 DAP(调试访问端口)
外设触发通道 双向 连接 ETM/ITM 等跟踪单元

硬件连接上,CTI 和 CTM 是硬连线在芯片内部的。你不需要画 PCB 走线,但需要在寄存器层面配置它们的路由关系。

4.2 软件触发配置

硬件搭好了,软件怎么用?

嗯,这里要注意:交叉触发的配置,本质上就是写寄存器。我整理了一个典型的配置流程,你照着做基本不会错。

  1. 使能 CTI 时钟:CTI 模块通常挂在 APB 总线上,先要把时钟打开。
  2. 配置触发映射(Trigger Mapping):把本核的调试事件(如 EDBGRQ、DBGRESTART)映射到 CTI 的输出通道。
  3. 配置触发使能(Trigger Enable):打开输出通道的使能位,让事件能发出去。
  4. 配置门控(Gate):设置哪些输入通道可以接收事件。我建议一开始全开,排查问题时再逐个关闭。
  5. 配置事件路由(Event Routing):在 CTM 里设置哪个核的哪个通道,路由到哪个核的哪个通道。

下面是一个伪代码示例,配置 Core0 的断点触发 Core1 暂停:

// 步骤1:使能 CTI 时钟
CTI0->CTICONTROL = 0x1;  // 使能 CTI0

// 步骤2:映射 Core0 的调试事件到输出通道 0
// 通常 EDBGRQ(调试请求)映射到通道 0
CTI0->OUTPUT0 = 0x1;     // 使能 EDBGRQ 到输出通道 0 的映射

// 步骤3:使能输出通道 0
CTI0->OUTEN0 = 0x1;

// 步骤4:配置 Core1 的 CTI 接收输入
CTI1->INEN0 = 0x1;       // 使能输入通道 0

// 步骤5:在 CTM 中建立路由
// 假设 CTM 的寄存器为 CTM_CR
CTM_CR = (0 << 0) |      // Core0 输出通道 0 连接到
         (1 << 4);       // Core1 输入通道 0

我曾经踩过的坑:配置完 CTI 后,发现事件死活传不过去。查了半天,原来是 CTM 的路由寄存器没写对。CTM 的位域定义各家芯片不一样,有的用位偏移,有的用独立寄存器。一定要看芯片参考手册的 CTM 章节,别只看 CTI 的。

4.3 事件路由设置

事件路由,说白了就是「谁发给谁,发什么」。我把它分成三种模式:

  • 广播模式:一个核的事件,发给所有其他核。适合全局断点同步。
  • 单播模式:一个核的事件,只发给指定核。适合核间协同调试。
  • 多播模式:一个核的事件,发给部分核。适合集群调试。

实际项目中,我用的最多的是广播模式。比如 Core0 触发了一个硬件断点,我希望 Core1、Core2 全部停下来,一起看状态。这时候广播模式最省事。

但广播模式也有代价。如果 Core1 正在处理一个时间敏感的中断,被强行 halt 可能会导致外设超时。所以,我建议在调试初期用广播,后期定位问题时改用单播。

下面是一个事件路由的配置表示例,假设你有 4 个核:

源核 源通道 目标核 目标通道 触发事件 路由模式
Core0 Ch0 Core1, Core2, Core3 Ch0 断点命中 广播
Core1 Ch1 Core0 Ch1 Watchpoint 命中 单播
Core2 Ch2 Core0, Core1 Ch2 复位请求 多播

你想想看,如果没有这个路由表,你只能靠软件轮询来同步核的状态。那效率,低得让人抓狂。

4.4 实战中的避坑指南

交叉触发配置好了,不代表万事大吉。我遇到过几个典型问题,分享给你:

  • 事件风暴:如果配置了循环触发(Core0 触发 Core1,Core1 又触发 Core0),会导致无限 halt/restart 循环。我建议在 CTI 里使能「触发抑制(Trigger Suppression)」功能,防止事件回环。
  • 优先级反转:当多个核同时触发事件时,CTM 的仲裁逻辑可能导致低优先级核的事件被延迟。如果你的应用对时间敏感,建议把关键核的 CTI 通道优先级设高。
  • 复位状态丢失:系统复位后,CTI 和 CTM 的寄存器会恢复默认值。如果你在调试脚本里配置了交叉触发,记得在复位后重新配置一遍。

我的个人习惯:在调试脚本(比如 .gdbinit 或 IDE 的初始化脚本)里,把交叉触发的配置写成一个函数。每次连接目标板后自动调用。这样即使复位了,配置也能自动恢复,省得手动敲寄存器。

最后,给你一张交叉触发的整体流程图,帮你理清思路:

交叉触发调试流程 Core0 断点命中 Core1 Watchpoint 命中 Core2 复位请求 CTI0 CTI1 CTI2 CTM(交叉触发矩阵) 事件路由与广播 Core0 收到事件 → Halt Core1 收到事件 → Halt Core2 收到事件 → Halt 说明:事件从源核 → CTI → CTM → 目标核 CTI → 目标核,实现多核同步暂停

这张图把整个流程串起来了。你调试的时候,脑子里要有这张图。哪个环节出问题,就查哪个环节的寄存器。嗯,交叉触发这东西,配置一次后面就省心了。但第一次配置,一定要耐心,一步一步来。