TI PRU中断控制器INTC:三级映射与优先级仲裁实战解析

1. PRU中断控制器:实时系统的神经中枢

如果你在搞基于TI Sitara系列处理器的嵌入式实时项目,比如用AM335x做电机控制,或者用AM437x做高速数据采集,那你肯定绕不开PRU(Programmable Real-Time Unit)。这俩200MHz的小核,没有缓存,没有流水线乱序执行,指令执行时间确定,天生就是为硬实时任务而生的。但光有PRU核还不够,要让它们和外部世界(各种传感器、通信接口、PWM模块)高效、及时地“对话”,关键就在于那个默默工作的幕后英雄——PRU中断控制器,也就是INTC。

你可以把INTC想象成整个PRU子系统的“前台”和“调度中心”。外部世界发生了什么事(比如一个定时器到点了、一个GPIO电平变化了、或者一串SPI数据收完了),这些事件会以“系统事件”的形式敲门。INTC的工作就是接待这些访客,问清楚谁更重要(优先级仲裁),然后决定叫醒哪个PRU核心(或者ARM、DSP)来处理,并且确保不会因为同时来了一堆客人就把系统搞崩溃(中断嵌套与管理)。它的设计直接决定了你的实时任务响应速度是否够快、够稳。

官方文档给出了它的核心能力框架:它能管理最多64个系统事件,通过10个内部通道进行归类,最终映射到10个主机中断输出。这听起来有点抽象,但理解了这个“事件->通道->主机中断”的三级映射模型,你就掌握了INTC的钥匙。接下来,我们就掰开揉碎,看看这个模型怎么工作,以及在实际项目中如何配置它,才能压榨出PRU的全部实时性能。

2. INTC核心架构与映射机制深度拆解

INTC的整个工作流程,本质上是一个高度可配置的“筛选-分组-上报”管道。理解这个管道里每一环的设计意图和约束,是正确使用它的前提。

2.1 三级映射模型:事件、通道与主机中断

官方描述点出了几个关键原则,每一个背后都有其硬件和逻辑考量:

  1. 任意系统事件可映射至任意通道:64个系统事件(System Event 0-63)中的任何一个,都可以被分配到10个内部通道(Channel 0-9)中的某一个。这给了我们极大的灵活性。比如,你可以把所有紧急的、高优先级的事件(如过流保护信号)都映射到通道0,而把一些非紧急的、用于状态查询的事件(如周期性的ADC采样完成)映射到通道9。

  2. 多事件可映射至单通道:这是实现“中断分组”的基础。例如,你可以将ECAP0、ECAP1、ECAP2这三个捕获模块的中断事件(假设是系统事件1, 2, 4)都映射到同一个通道,比如通道2。这样,无论哪个ECAP模块触发了事件,都会激活通道2。在软件中断服务程序(ISR)里,你只需要检查通道2对应的状态寄存器,就能知道具体是哪个ECAP产生的中断,然后分别处理。这减少了需要管理的中断向量数量。

  3. 单一事件不可映射至多通道:这是一个重要的硬件限制。一个系统事件不能同时属于两个或更多通道。如果强行配置,会导致未定义的行为,很可能造成中断丢失或误触发。这很好理解,就像一个快递只能有一个派送路径,不能同时走两条路,否则就乱套了。

  4. 通道到主机中断的映射及默认建议:10个通道可以映射到10个主机中断(Host Interrupt 0-9)。主机中断是最终输出给处理单元的信号,例如Host Interrupt 0和1直接连接到了PRU0和PRU1的R31寄存器特定比特位,而Host Interrupt 2-9则输出给ARM或DSP的中断控制器,生成系统级中断事件。文档建议将通道x映射到主机中断x,这是一种直连且易于理解的配置,在大多数简单场景下推荐使用。但规则同样禁止一个通道映射到多个主机中断。

  5. 优先级仲裁规则:这是INTC的精髓。优先级分为两级:

    • 通道间优先级:对于映射到同一个主机中断的多个通道,通道编号越小,优先级越高。例如,如果通道1和通道3都映射到了主机中断5,那么当两者同时有效时,通道1的中断会优先被处理。
    • 通道内优先级:对于映射到同一个通道的多个系统事件,系统事件编号越小,优先级越高。接上面的例子,如果系统事件2和系统事件4都映射在通道1内,那么事件2的优先级高于事件4。

这个两级优先级机制,使得我们可以设计出非常精细的中断处理策略。你可以用通道来划分中断的“大类”(如安全相关、通信相关、控制相关),再用事件编号来区分“小类”(如UART0接收完成、UART0发送完成)。

2.2 系统事件详解:中断的源头

系统事件是中断的源头,分为两部分:

  • 事件0-31:由PRUSS子系统外的外设产生。具体哪个外设对应哪个事件号,需要查芯片的数据手册。例如,在AM3358上,系统事件17可能是UART2的中断,而事件20可能是USB0的中断。这里有一个非常重要的配置点:PRUSSEVTSEL信号。它位于系统配置寄存器CFGCHIP3[3]。这个信号像一个全局开关,能改变事件0-31的源头。 例如,当PRUSSEVTSEL=0时,系统事件3可能来自Timer64P0;而当PRUSSEVTSEL=1时,同一个系统事件3可能就变成了来自Timer64P2。这个功能在芯片引脚复用紧张或者需要动态切换中断源时非常有用。务必在初始化时根据你的硬件连接,确认并设置好这个位。

  • 事件32-63:由PRU核心自身通过写其R31寄存器产生。这是PRU之间,或者PRU向ARM/DSP发送软件中断(或称为“事件”)的机制。例如,PRU0完成一项计算后,可以通过写R31的特定比特来触发一个系统事件(比如事件32),这个事件可以被配置为去中断PRU1或者ARM。这是一种高效的核间通信方式。

2.3 主机中断的输出目的地

主机中断的去向决定了最终由谁来响应:

  • Host Interrupt 0 & 1:直接连接到PRU0和PRU1内部。具体是PRU的R31寄存器的第30和31位。这意味着PRU可以通过轮询这两个比特位来快速检查是否有中断发生,无需通过复杂的中断控制器。通常用于极低延迟的PRU间信号传递。
  • Host Interrupt 2-9:输出到芯片的系统级中断控制器,最终映射为ARM可处理的系统中断事件(如PRUSS_EVTOUT0PRUSS_EVTOUT7)。ARM需要像处理其他外设中断一样,为其配置中断服务例程。

注意:ARM/DSP侧的中断号映射是固定的,但不同芯片型号可能不同。例如在AM335x的Linux内核中,PRUSS_EVTOUT0通常对应ARM的irq号。你需要查阅内核的DTS(设备树)文件或相关文档来确认,并在驱动中申请正确的IRQ。

3. INTC配置流程与寄存器操作实战

理解了原理,我们来看如何动手配置。INTC的配置是一系列有序的寄存器写操作,顺序错了可能会导致中断无法正常触发或无法清除。

3.1 配置步骤详解

官方给出了标准的配置流程,我们结合常见实践进行细化:

  1. 设置系统事件极性/类型(通常可跳过):通过SIPR1/2(极性)和SITR1/2(类型)寄存器设置。但文档明确指出,所有系统事件都是高电平有效(Active High)的脉冲(Pulse)。对于绝大多数应用,这两个寄存器保持默认值(全0)即可,无需配置。除非你使用的仿真器或特殊外设有特殊要求。

  2. 映射系统事件到通道:这是配置的核心步骤之一。通过CMR1CMR16共16个寄存器来完成。每个CMR寄存器管理4个系统事件(因为64/4=16)。每个事件用寄存器中的8个比特位来指定其通道号(0-9)。

    • 操作:假设我们要将系统事件SYS_EVT_17(UART2中断)映射到通道2。首先,计算SYS_EVT_17属于哪个CMR寄存器:事件号17除以4等于4余1,所以它在CMR5寄存器中(因为从CMR1开始计数)。然后,在CMR5寄存器中,找到管理事件17的8个比特位(余数1表示它是该寄存器管理的第2个事件,即比特位8-15)。最后,向这8个比特位写入通道号2。
    • 代码示例(伪代码)
      // 假设 INTC_BASE 是 INTC 寄存器的基地址 volatile uint32_t *cmr5 = (uint32_t *)(INTC_BASE + CMR5_OFFSET); uint32_t original_val = *cmr5; // 清除事件17原有的通道映射(比特位8-15),然后设置为通道2 original_val &= ~(0xFF << 8); // 清除8-15位 original_val |= (2 << 8); // 设置通道号为2 *cmr5 = original_val;
  3. 映射通道到主机中断:通过HMR1HMR3共3个寄存器完成。每个HMR寄存器管理4个通道。配置方法与CMR类似。遵循“通道x映射到主机中断x”的建议可以简化设计。

    • 操作:将通道2映射到主机中断2。通道2属于HMR1寄存器(通道0-3)。在HMR1中,找到通道2对应的8个比特位(通道2是第3个,即比特位16-23),写入主机中断号2。
  4. 清除所有系统中断状态:在使能任何中断之前,必须清除可能存在的残留中断状态位。这是避免一使能就误触发中断的关键步骤。通过向SECR1SECR2寄存器写入1来清除对应事件的状态位。更高效的方法是使用SICR(系统中断状态索引清除寄存器),直接写入要清除的事件号。

    • 操作:清除所有64个事件的状态。
      // 方法一:写 SECR1/SECR2 (假设是32位系统,需要处理64位) *(volatile uint32_t *)(INTC_BASE + SECR1_OFFSET) = 0xFFFFFFFF; *(volatile uint32_t *)(INTC_BASE + SECR2_OFFSET) = 0xFFFFFFFF; // 方法二:循环写 SICR for (int evt = 0; evt < 64; evt++) { *(volatile uint32_t *)(INTC_BASE + SICR_OFFSET) = evt; }
  5. 使能主机中断:通过HIEISR(主机中断使能索引设置寄存器)使能需要的主机中断。只需向该寄存器写入主机中断的编号即可。

    • 操作:使能主机中断2。
      *(volatile uint32_t *)(INTC_BASE + HIEISR_OFFSET) = 2;
  6. 使能系统中断:最后,使能具体的系统事件。通过EISR(系统中断使能索引设置寄存器)或ESR1/2/3寄存器。推荐使用EISR,写入事件号。

    • 操作:使能系统事件17。
      *(volatile uint32_t *)(INTC_BASE + EISR_OFFSET) = 17;
  7. 全局使能:设置GER(全局使能寄存器)的ENABLE位为1,打开INTC的总开关。

    • 操作
      *(volatile uint32_t *)(INTC_BASE + GER_OFFSET) = 1;

3.2 中断服务程序(ISR)内的关键操作

当中断触发,CPU跳转到ISR后,必须正确操作INTC以完成中断处理并准备好接收下一次中断。

  1. 识别中断源:读取HIPIR(主机中断优先级索引寄存器)或GPIR(全局优先级索引寄存器)。HIPIR用于查询特定主机中断上当前最高优先级的事件号,GPIR则返回所有主机中断中最高优先级的事件号。在简单映射(一通道一主机中断)下,用HIPIR更直接。

    • 操作:在主机中断2的ISR里,读取HIPIR2的值,它返回的就是当前在主机中断2上激活的最高优先级系统事件号。
  2. 处理中断任务:执行你的实际中断处理代码,例如从UART读取数据、设置一个GPIO等。

  3. 清除中断状态(至关重要):处理完成后,必须清除该中断在INTC中的状态位。否则,该中断会一直处于“待处理”状态,导致无法再次触发,或者影响其他中断的优先级判断。清除方法同样是写SECRSICR寄存器。

    • 操作:假设从HIPIR2读到事件号是17。
      *(volatile uint32_t *)(INTC_BASE + SICR_OFFSET) = 17;
    • 严重警告:在PRU应用下,在停止(Halt)PRU核心之前,必须确保所有已触发的系统中断状态都被清除。如果带着未清除的中断状态停止PRU,可能会导致PRU无法正常下电或产生不可预知的行为。
  4. 重新使能中断(如果需要):如果使用了中断嵌套并在ISR开始时禁用了某些中断,此时需要重新使能它们。

4. 高级功能:中断嵌套与优先级管理实战

INTC提供了硬件支持的中断嵌套,这对于构建复杂的实时系统非常有用,可以确保高优先级任务能及时抢占低优先级任务。

4.1 嵌套的三种模式

  1. 基于通道优先级的全局嵌套:通过设置GNLR(全局嵌套级别寄存器)。当处理一个通道N的中断时,你可以将嵌套级别设置为N。这样,通道优先级小于等于N的所有中断都会被暂时禁止(嵌套掉),只有优先级更高的通道(编号小于N)的中断才能打断当前ISR。当前ISR退出前,需要恢复之前的嵌套级别。

    • 适用场景:系统中断关系简单,所有主机中断共享同一套优先级策略。
  2. 基于通道优先级的单主机中断嵌套:通过设置HINLR1HINLR2(主机中断嵌套级别寄存器)。每个主机中断有自己的嵌套级别控制。这样,一个主机中断上的高优先级事件可以打断其低优先级事件,但不会影响其他主机中断的中断流。

    • 适用场景:不同的主机中断服务于不同的、相对独立的子系统,需要独立的嵌套控制。
  3. 软件手动嵌套:最灵活,也最复杂。在ISR入口,软件手动禁用所有主机中断(通过GER),然后根据需求修改EISR/EICR来动态启用或禁用某些系统事件,最后再重新使能主机中断。退出ISR前反向操作。

    • 适用场景:嵌套逻辑非常复杂,无法用简单的通道优先级来概括。例如,某个中断处理过程中,需要允许另一个特定通道的中断,而不是简单地允许所有更高优先级通道。

4.2 优先级管理策略设计心得

在实际项目中,设计INTC的映射和优先级策略是一门艺术。以下是一些经验:

  • 为最紧急的事件保留最低编号的通道和事件:将生命安全相关的信号(如急停、过流)映射到通道0,并使用尽可能低的事件号。同时,确保它映射到的主机中断在ARM/DSP侧也被设置为最高优先级。
  • 合理分组:将功能相近或处理逻辑相似的中断映射到同一通道。例如,所有通信外设(UART, SPI, I2C)的中断可以放在通道3-5。这样在ISR中,通过检查HIPIR和状态寄存器,可以一次性处理多个相关事件,减少中断响应次数。
  • 避免通道拥堵:虽然一个通道可以映射多个事件,但不要把所有事件都塞进一两个通道。这会导致该通道的HIPIR查询逻辑变复杂,增加ISR的延迟。适当分散,平衡通道负载。
  • 利用PRU直接中断:对于PRU之间要求纳秒级响应的信号,考虑使用Host Interrupt 0/1,它们直接连接到PRU的R31,PRU可以通过轮询R31.b30b31来近乎即时地响应,完全 bypass INTC的仲裁延迟。
  • 调试技巧:在初始化完成后、使能全局中断前,可以通过读取SECR1/2来检查是否有意外的事件状态被置位(可能是硬件毛刺)。同样,在ISR中,如果发现HIPIR读出的值异常(如0xFFFFFFFF或大于63),很可能是因为没有正确清除之前的中断状态,或者发生了未映射的事件。

5. 常见问题排查与调试实录

即使理解了所有原理,实际调试INTC时还是会遇到各种坑。下面是我在项目中踩过的一些雷和解决方法。

5.1 中断完全不触发

这是最常见的问题。请按照以下清单逐项检查:

  1. 时钟与电源域:首先确认PRUSS子系统(包括INTC)的时钟和电源已经使能。在Linux下,需要确保设备树(DTS)中pruss节点的状态是okay,并且相关时钟配置正确。在裸机程序中,需要配置相应的PSC(Power Sleep Controller)和时钟模块寄存器。
  2. PRUSSEVTSEL配置:确认CFGCHIP3[3]位的设置与你硬件连接的外设事件源匹配。这是最容易忽略的一点。
  3. 映射关系检查:三重检查CMRxHMRx寄存器的配置值。确保你期望的系统事件确实映射到了某个通道,并且该通道映射到了一个已使能的主机中断。
  4. 使能顺序:严格按照“清除状态 -> 使能主机中断 -> 使能系统中断 -> 全局使能”的顺序。过早使能系统中断可能导致立即触发。
  5. ARM/DSP侧配置:如果使用的是Host Interrupt 2-9,确保在ARM Linux内核驱动中正确申请了对应的IRQ,并注册了中断处理函数。使用cat /proc/interrupts命令可以查看中断是否被正确注册和触发。
  6. 外设本身的中断使能:别忘了使能产生系统事件的那个外设本身的中断。例如,配置好了INTC来处理UART中断,但UART模块自身的IER(中断使能寄存器)没有打开接收中断,那也是白搭。

5.2 中断触发一次后不再触发

这个问题几乎99%是由于中断状态未清除导致的。

  • 症状:中断成功触发一次,ISR也执行了,但之后无论外设如何触发事件,中断再也不来了。
  • 排查
    • 在ISR中,在执行完业务逻辑后,必须调用清除状态的操作(写SICRSECR)。
    • 检查清除操作是否针对了正确的事件号。最好使用从HIPIR读出的值作为SICR的写入值。
    • 确保清除操作在ISR返回之前完成。
    • 对于脉冲型中断,确保外设产生的脉冲宽度足够被INTC捕获。太窄的脉冲可能会丢失。

5.3 中断响应延迟过大或不确定

这涉及到实时性的核心。

  • 检查INTC仲裁延迟:INTC的硬件优先级仲裁需要几个时钟周期。如果对延迟极其敏感,考虑使用PRU直接中断(Host Int 0/1)或重新规划优先级,让最关键的事件走更短的路径。
  • 关闭中断嵌套:如果未使用嵌套,确保GNLRHINLR没有被意外设置。一个被嵌套掉的中断虽然已触发,但会等到当前ISR完成才响应。
  • ARM Linux内核延迟:如果主机是ARM且运行Linux,中断延迟会受到内核配置(CONFIG_PREEMPT)、其他中断的屏蔽情况、以及CPU负载的影响。对于硬实时要求,考虑使用PREEMPT_RT实时内核补丁,或者将关键中断处理任务放到PRU中完成,ARM只负责非实时部分。
  • PRU代码优化:PRU的ISR应该尽可能短小精悍。避免在ISR内进行复杂的计算或循环。如果需要处理大量数据,可以只在ISR中设置标志位,唤醒主循环任务来处理。

5.4 多个中断相互影响或丢失

  • 通道内优先级混淆:如果同一个通道内有多个事件,并且它们的ISR处理时间都很长,低优先级事件可能会被“饿死”。因为INTC在同一个通道内,只会将最高优先级的事件号放入HIPIR。如果高优先级事件频繁触发,低优先级事件的状态位即使被置起,也无法被上报,直到高优先级事件被清除。解决方案是缩短ISR执行时间,或者将不同优先级的事件分配到不同通道。
  • 状态清除冲突:确保不同的ISR不会错误地清除其他事件的状态位。操作SICR/SECR时,务必使用本中断源的事件号。
  • 中断风暴:如果某个外设故障,连续产生中断,可能会霸占CPU。可以在ISR中增加简单的频率监控,或者在硬件/软件上增加去抖、限流机制。

调试INTC时,最有力的工具是读取其状态寄存器。在出现问题的地方,打印或通过调试器查看SECR1/2(已使能中断状态)、HIPIRxGPIR的值,可以清晰地看到哪个事件触发了、映射到了哪里、优先级如何,绝大多数问题都能通过这些状态信息定位。记住,耐心和系统地对照数据手册检查配置,是搞定复杂中断系统的唯一捷径。