1. 项目概述
如果你在搞基于TI Sitara系列处理器的硬实时应用,比如用PRU(Programmable Real-Time Unit)去抓取高速编码器脉冲、生成精密的PWM波形,或者处理McASP音频流,那你肯定绕不开一个核心硬件:PRU中断控制器,也就是INTC。这玩意儿是连接外部世界纷繁复杂的事件与PRU那确定性执行流水线的“交通枢纽”。手册里那些寄存器位域和映射表格看着头疼,但理解透了,你才能让PRU在微秒级甚至纳秒级的时间内,对特定外部事件做出精准响应,这才是发挥PRU硬实时威力的关键。很多新手觉得配置中断就是照着手册填几个寄存器地址,结果要么是中断死活不来,要么是一来就乱套,系统行为完全不可预测。问题往往就出在没有吃透INTC那套“事件 -> 通道 -> 主机中断”的映射与优先级逻辑。今天,我就结合手册和实际踩过的坑,把这套机制掰开揉碎了讲清楚,让你不仅能配通,更能配得明白、配得稳健。
2. PRU INTC 核心架构与设计哲学
2.1 为什么需要专用的中断控制器?
在通用ARM核上跑Linux,中断管理通常由GIC(通用中断控制器)这类复杂IP负责,它要面对的是多任务、虚拟内存、复杂优先级抢占的宏观世界。但PRU的世界是微观的、确定性的。它的核心任务是以最小的延迟、最可预测的方式,响应来自特定外设(如ePWM、ECAP、GPIO)的硬件事件。如果让PRU像ARM那样去处理一个庞大而复杂的中断向量表,或者让软件去轮询查询中断源,实时性就无从谈起。
PRU INTC的设计哲学非常清晰:硬件化、扁平化、可预测。
- 硬件化:从事件捕获、优先级仲裁到触发PRU中断输入,全程由硬件逻辑完成,软件开销几乎为零。
- 扁平化:它不搞太复杂的树状或网状中断拓扑,而是采用“系统事件 -> 通道 -> 主机中断”的两级映射,结构直观。
- 可预测:优先级规则是固定的、硬件实现的。对于映射到同一主机中断的多个通道,编号小的通道优先级永远更高;对于同一通道内的多个事件,编号小的事件优先级更高。这种确定性对实时系统至关重要。
2.2 INTC 核心功能模块全景
手册里那张中断处理流程图(Figure 12-23)是理解INTC的钥匙,我们可以把它拆解成几个清晰的阶段:
- 输入处理与标准化:来自不同外设的中断信号,其电气特性(高电平有效还是低电平有效)和类型(电平触发还是边沿触发)可能五花八门。INTC的第一道关卡就是“处理块”,它负责将所有中断同步到INTC的内部时钟域,并将它们统一转换为高电平有效的脉冲信号。这意味着,无论你的外设中断是低有效还是高有效,是电平还是边沿,进入INTC核心逻辑前,都被标准化了。这一点在配置时常常被忽略,但它是后续一切正确工作的基础。
- 使能门控:标准化后的事件并不会直接冲向后级。每个系统事件(0-63)都有一个独立的“使能”开关。只有被使能的事件,其状态变化才能继续向下传播。这个开关由软件通过
EISR(Event Indexed Set Register) 或ESR(Event Set Register) 来控制。这是你决定“关心哪些事件”的第一道软件屏障。 - 状态锁存:事件发生后,即使是一个脉冲,其状态也需要被记录下来,等待PRU或主机来处理。INTC有两套状态寄存器:
SRSR(System Event Raw Status Register):原始状态寄存器。只要事件发生(符合极性/类型),无论是否使能,这里对应的位就会被置1。它反映了物理世界最原始的中断信号。SECR(System Event Enabled Status/Clear Register):使能状态寄存器。只有既发生又被使能的事件,才会在这里置位。这是我们软件通常需要查询和清除的状态位。
- 通道映射与聚合:这是INTC最巧妙的设计之一。64个系统事件可以被映射到仅有的10个通道(Channel 0-9)上。你可以把通道理解为“优先级组”。例如,你可以把所有关键的、需要快速响应的电机过流事件(来自不同比较器)映射到高优先级的Channel 0;把一些非实时的状态报告事件映射到低优先级的Channel 9。多个事件可以映射到同一个通道,它们在该通道的输出是“或”的关系——任何一个事件发生,该通道就会有效。
- 主机中断映射:10个通道的输出,需要最终送达“主机”。这里的主机可以是两个PRU核心(Host Int 0, 1),也可以是ARM/DSP(Host Int 2-9)。每个通道可以独立地映射到任意一个主机中断上。手册建议采用直通映射(Channel x 映射到 Host Int x),这简化了逻辑,但并非强制。多个通道可以映射到同一个主机中断,这时就需要优先级仲裁了。
- 硬件优先级仲裁:当多个事件同时发生或相继pending时,谁先被处理?INTC的硬件仲裁器严格遵循两层规则:
- 通道间优先级:对于同一个主机中断,编号小的通道优先级最高。如果Channel 0和Channel 5都映射到了Host Int 2,且同时有效,那么Channel 0对应的事件会优先被上报。
- 通道内优先级:对于映射到同一个通道的多个事件,编号小的事件优先级最高。 仲裁结果体现在
HIPIR(Host Interrupt Priority Index Register) 和GPIR(Global Priority Index Register) 寄存器中,软件可以读取这些寄存器来快速获知当前最高优先级的待处理事件编号。
- 中断嵌套控制:这是一个高级功能,允许高优先级中断打断正在处理的低优先级中断。INTC支持基于通道优先级的全局或单主机嵌套。通过设置
GNLR或HINLR寄存器,你可以设定一个“嵌套等级”。所有优先级等于或低于此等级的通道产生的中断,在当前中断服务程序(ISR)执行期间会被临时屏蔽,只有更高优先级的中断才能抢占。这避免了低优先级中断的“饥饿”问题,并保护了关键中断的响应时间。
3. 关键配置流程与寄存器详解
理解了架构,我们来看怎么把它配起来。手册12.7.5.10节给出了配置步骤,但有些细节需要展开。
3.1 配置前必须理清的思路
在写第一行配置代码前,你必须想清楚以下几件事:
- 事件源:我的中断来自哪个外设?对应哪个系统事件编号(0-31是外部事件,32-63是PRU内部事件)?例如,ePWM0的周期中断可能对应系统事件24。
- 目的地:这个中断最终要通知谁?PRU0?PRU1?还是ARM核?这决定了目标主机中断(Host Interrupt 0-9)。
- 优先级规划:不同中断的紧急程度如何?是否需要分组?例如,安全相关的急停信号(最高优先级)单独一个通道映射到PRU0;多个传感器采样完成信号(中等优先级)可以聚合到另一个通道映射到PRU0;调试信息上报(最低优先级)映射到ARM。
- 嵌套策略:是否需要高优先级中断抢占低优先级?如果需要,是全局嵌套还是针对单个主机中断嵌套?
3.2 分步配置实战与寄存器操作
假设我们的场景是:使用PRU0处理一个高速GPIO输入事件(系统事件27,假设映射到GPIO Bank0中断)和一个ePWM0周期中断(系统事件24)。我们希望GPIO中断优先级更高,并能抢占ePWM中断的处理。
步骤1:确定并配置系统事件极性/类型理论上,所有输入INTC的事件都被统一视为高有效脉冲。SIPR(System Interrupt Polarity Register) 和SITR(System Interrupt Type Register) 通常在上电初始化后就被设置为默认值(全1,即高有效脉冲)。但在有些平台或特定外设配置下,你需要确认外设产生的中断信号是否符合这个预期。这一步常常被忽略,但如果外设产生的是低电平有效信号,而INTC期待高有效,中断将永远无法被识别。通常,在PRU开发中,我们默认外设驱动(如Linux内核中的pruss_intc驱动)或板级初始化代码已经做好了这部分工作。但如果你是在裸机或深度定制环境中,必须检查并配置这两个寄存器。
步骤2:映射系统事件到INTC通道这是核心配置之一。我们决定:
- 高优先级的GPIO事件(27)映射到Channel 0。
- 低优先级的ePWM事件(24)映射到Channel 1。
通道映射寄存器是CMR(Channel Map Register),共有16个(CMR1-CMR16),每个寄存器管理4个系统事件。每个事件用2个bit(可表示0-3)来指定通道,但INTC有10个通道(0-9),所以实际上手册中每个事件对应的是寄存器中的一个字段(通常多于2bit,具体看手册位域定义,例如可能是5bit,可表示0-31,但只使用0-9)。我们需要找到管理事件24和27的CMR寄存器。
关键点:
CMR寄存器的索引和位域需要仔细查阅具体芯片的TRM。例如,事件24可能由CMR7的某个位域控制。操作时,我们通常使用索引寄存器CMR_IDX和CMR寄存器来间接写入,或者直接对CMR寄存器的绝对地址进行位操作。为了清晰,以下以伪代码和概念操作为主:// 假设通过查表或计算得到以下信息: // 事件24 由 CMR7 的 [11:8] 位控制,应写入通道值 1 (0b0001) // 事件27 由 CMR7 的 [23:20] 位控制,应写入通道值 0 (0b0000) // 方法1:直接操作内存映射寄存器(假设已映射好寄存器地址) volatile uint32_t *cmr7 = (uint32_t*)(INTC_BASE + CMR7_OFFSET); uint32_t temp = *cmr7; temp &= ~(0xF << 8); // 清零事件24对应的位域 temp |= (1 << 8); // 设置事件24映射到通道1 temp &= ~(0xF << 20); // 清零事件27对应的位域 temp |= (0 << 20); // 设置事件27映射到通道0 *cmr7 = temp;特别注意:多个事件可以映射到同一通道,但一个事件绝对不能映射到多个通道,否则行为未定义。
步骤3:映射通道到主机中断我们计划两个中断都由PRU0处理。PRU0对应 Host Interrupt 0 和 1(通过R31寄存器的bit30和31接收)。按照手册建议的直通映射,我们:
- 将Channel 0映射到Host Interrupt 0。
- 将Channel 1映射到Host Interrupt 1。
主机中断映射寄存器是HMR(Host Map Register),共3个(HMR1-HMR3),每个管理4个通道。配置方式与CMR类似。
// 假设 Channel 0 由 HMR1 的 [3:0] 控制, Channel 1 由 HMR1 的 [7:4] 控制 volatile uint32_t *hmr1 = (uint32_t*)(INTC_BASE + HMR1_OFFSET); uint32_t temp = *hmr1; temp &= ~0xFF; // 清零低8位 temp |= (0 << 0) | (1 << 4); // Channel 0 -> Host Int 0, Channel 1 -> Host Int 1 *hmr1 = temp;步骤4:清除可能存在的残留中断状态在使能任何中断之前,必须清除所有系统事件的状态位,避免一使能就误触发。通过向SECR寄存器对应位写1,或向SICR(System Interrupt Indexed Clear Register) 写入事件编号来清除。
// 方法1:通过索引清除寄存器 SICR volatile uint32_t *sicr = (uint32_t*)(INTC_BASE + SICR_OFFSET); *sicr = 24; // 清除事件24 *sicr = 27; // 清除事件27 // 注意:有些实现中,向SICR写入可能需要在两次写入间加入延迟或检查操作完成位。 // 方法2:直接置位 SECR 对应位(需知道SECR1/SECR2哪个bit对应哪个事件) // 假设事件24对应 SECR1 的 bit24, 事件27对应 SECR1 的 bit27 volatile uint32_t *secr1 = (uint32_t*)(INTC_BASE + SECR1_OFFSET); *secr1 = (1 << 24) | (1 << 27);步骤5:使能主机中断我们需要使能 Host Interrupt 0 和 1,以便INTC能将中断信号送达PRU0。通过HIEISR(Host Interrupt Enable Indexed Set Register) 写入主机中断编号。
volatile uint32_t *hieisr = (uint32_t*)(INTC_BASE + HIEISR_OFFSET); *hieisr = 0; // 使能 Host Int 0 *hieisr = 1; // 使能 Host Int 1步骤6:配置中断嵌套(可选)为了实现GPIO中断(Channel 0)抢占ePWM中断(Channel 1),我们需要启用嵌套。由于只涉及PRU0(Host Int 0 和 1),我们可以选择配置针对 Host Interrupt 0 的嵌套(因为高优先级中断在Host Int 0上)。但更常见的做法是设置全局嵌套等级GNLR。 我们希望当Channel 1(优先级较低)的中断正在处理时,Channel 0(优先级较高)的中断还能进来。那么,在ePWM的ISR开始时,我们应该设置嵌套等级为1(即屏蔽Channel 1及更低优先级的通道)。这样,只有Channel 0(优先级0)能产生新的中断。
// 在ePWM的ISR开始时(假设在PRU汇编或C环境中能访问INTC寄存器) volatile uint32_t *gnlr = (uint32_t*)(INTC_BASE + GNLR_OFFSET); *gnlr = 1; // 设置全局嵌套等级为1,屏蔽 Channel 1-9 // ... 执行ePWM ISR ... // 在ePWM的ISR结束前,恢复嵌套等级 *gnlr = 0; // 清除全局嵌套,允许所有中断注意:嵌套操作需要非常小心,必须在ISR入口和出口成对操作,并且要考虑中断重入等问题。对于简单应用,初期可以不使用嵌套。
步骤7:使能系统事件最后,打开我们关心的两个事件的开关。通过EISR(Event Indexed Set Register) 写入事件编号。
volatile uint32_t *eisr = (uint32_t*)(INTC_BASE + EISR_OFFSET); *eisr = 24; // 使能系统事件24 (ePWM0) *eisr = 27; // 使能系统事件27 (GPIO Bank0)步骤8:全局使能INTC这是最后一步,也是总开关。设置GER(Global Enable Register) 的使能位。
volatile uint32_t *ger = (uint32_t*)(INTC_BASE + GER_OFFSET); *ger |= 0x1; // 全局使能重要顺序:务必遵循“先映射,后使能”的原则。特别是全局使能GER应该在所有具体配置完成后最后开启,避免配置过程中产生不可控的中断。
4. PRU侧的编程与响应机制
INTC配置好了,事件如何到达PRU并触发动作呢?这涉及到PRU核心的机制。
4.1 主机中断与PRU R31寄存器的连接
对于PRU0和PRU1:
- Host Interrupt 0连接到PRUx R31寄存器的 bit 30。
- Host Interrupt 1连接到PRUx R31寄存器的 bit 31。
当INTC断言一个主机中断时,对应的R31位会被硬件置位。PRU程序可以通过两种方式感知:
- 轮询方式:PRU代码在一个循环中不断读取R31寄存器,检查bit30或31是否被置1。这种方式简单,但会占用PRU的算力,且响应延迟取决于循环速度。
- 中断方式:这才是发挥PRU实时性的关键。PRU支持一条特殊的指令
HALT。当PRU执行HALT后,它会进入低功耗休眠状态,直到R31的bit30或31被置位,才会唤醒并继续执行下一条指令。这实现了事件驱动的零开销等待。
4.2 一个典型的中断驱动PRU程序流程
; 伪代码示例 - PRU0 等待 Host Int 0 (GPIO事件) 和 Host Int 1 (ePWM事件) ; 初始化代码... ; 配置PRU0的CONTROL寄存器,确保使能等 MAIN_LOOP: ; 等待任意中断到来。R31的bit30或31被置位会唤醒HALT HALT ; 唤醒后,首先判断是哪个中断 ; 读取R31,检查bit30和31 LDI r0, 0x40000000 ; bit30 mask for Host Int 0 LDI r1, 0x80000000 ; bit31 mask for Host Int 1 AND r2, r31, r0 ; 检查Host Int 0 QBNE HANDLE_GPIO, r2, 0 AND r2, r31, r1 ; 检查Host Int 1 QBNE HANDLE_EPWM, r2, 0 ; 如果不是预期中断,跳回继续等待(或错误处理) JMP MAIN_LOOP HANDLE_GPIO: ; 1. 清除PRU侧的中断标志(可选,但建议) ; 向R31的bit30写1可以清除该中断输入?不,对于Host Int输入,通常不是通过写R31清除。 ; 正确做法是清除INTC中的系统事件状态位(SECR)。 ; 这通常需要通过访问INTC寄存器空间来完成。PRU可以访问整个内存空间。 LDI32 r3, INTC_BASE_ADDR + SECR1_OFFSET LDI r4, (1 << 27) ; 事件27的位 SBCO &r4, r3, 0, 4 ; 写SECR1清除事件27状态 ; 2. 处理GPIO中断服务程序 ; ... 你的业务逻辑 ... ; 3. 处理完成后,返回主循环 JMP MAIN_LOOP HANDLE_EPWM: ; 1. 清除INTC中的ePWM事件状态 LDI32 r3, INTC_BASE_ADDR + SECR1_OFFSET LDI r4, (1 << 24) ; 事件24的位 SBCO &r4, r3, 0, 4 ; 2. 处理ePWM中断服务程序 ; ... 你的业务逻辑 ... ; 3. 返回主循环 JMP MAIN_LOOP关键点:在PRU的ISR中,必须清除INTC中对应的系统事件状态位(SECR)。如果不清除,该中断状态会一直保持pending,导致PRU退出HALT后立即再次进入中断,形成死循环。同时,清除操作也通知INTC该中断已被处理,可以接受下一次触发。
4.3 从PRU主动触发系统事件
系统事件32-63是由PRU自身通过写R31寄存器产生的。这用于PRU向另一个PRU核或ARM/DSP主机发送中断信号。例如,PRU0处理完数据后,可以通过触发一个系统事件来通知ARM核来取数据。
; PRU0 触发系统事件 32 (映射到 Host Int 2, 通往ARM) LDI r0, 32 ; 系统事件编号 LSL r0, r0, 16 ; 事件编号放在R31的[21:16]位 OR r0, r0, 0x00000001 ; 同时设置bit0=1,表示触发事件 MOV r31, r0 ; 写入R31,触发事件在ARM侧,需要配置INTC将系统事件32映射到某个通道,并最终映射到ARM的中断控制器(如GIC)对应的中断号上,并编写相应的Linux内核中断服务程序。
5. 常见问题、调试技巧与避坑指南
配置PRU INTC时,90%的问题都出在映射关系、状态清除和使能顺序上。下面是一些实战中总结的坑和排查方法。
5.1 中断不触发
这是最常见的问题。请按以下清单排查:
- 事件源确认:外设本身的中断产生了没有?用示波器或逻辑分析仪抓一下中断信号线,或者通过读取外设的中断状态寄存器来确认。PRU INTC只是个中转站,源头没信号,后面全白搭。
- INTC输入处理检查:确认
SIPR和SITR寄存器配置是否符合外设中断信号的特性(高有效脉冲)。如果不确定,可以尝试读取SRSR(原始状态寄存器)。如果事件发生了,SRSR对应位应该会被置1,无论是否使能。如果SRSR没置位,问题出在INTC的输入前端(信号路径、极性/类型配置)。 - 事件使能确认:检查
ESR或通过EISR操作,确认你关心的事件使能位已经置1。可以读取ESR寄存器来验证。 - 通道映射确认:读取
CMR寄存器,确认你的事件确实映射到了你期望的通道。一个常见的错误是算错了CMR寄存器的位域,导致映射到了错误的通道甚至未使用的通道。 - 主机中断映射确认:读取
HMR寄存器,确认通道映射到了正确的主机中断。特别是PRU的中断(Host Int 0/1)和ARM的中断(Host Int 2-9)别搞混。 - 主机中断使能确认:检查
HIEISR操作是否成功,或者读取主机中断使能寄存器确认。 - 全局使能确认:
GER寄存器的使能位开了吗? - PRU侧配置:如果目标是PRU,PRU核心的
CONTROL寄存器使能了吗?PRU程序是否执行到了HALT指令?PRU的WAKEUP寄存器是否允许R31的中断输入唤醒?WAKEUP寄存器需要设置为能响应R31的相应位(例如,对于Host Int 0,需要使能R31的bit30对应的唤醒位)。 - 状态未清除导致的阻塞:这是最隐蔽的坑之一。如果一个高优先级的中断状态一直没有被清除(
SECR),它可能会一直占据着优先级仲裁,导致低优先级中断永远无法被上报。即使你认为这个中断没发生,也要在初始化时清除所有可能用到的系统事件状态。
5.2 中断触发一次后不再触发
- 状态清除问题:99%的原因是ISR中没有正确清除
SECR状态位。在PRU或主机的中断服务程序中,必须在退出前向SECR对应位写1,或向SICR写入事件编号。只清除PRU R31的标志是不够的,因为那是INTC的输出结果,不是根源。 - 外设中断标志未清除:INTC的状态清除了,但产生中断的外设模块(如ePWM、ECAP)自己的中断标志位你清除了吗?如果没清,外设可能不会产生下一次中断脉冲。需要在你的ISR中同时清除外设的中断标志。
- 中断嵌套与屏蔽:如果你启用了嵌套(
GNLR或HINLR),并且在ISR中设置了嵌套等级,但在退出前没有恢复,会导致该中断本身或更低优先级的中断被永久屏蔽。确保嵌套设置是成对、可逆的。
5.3 中断响应延迟过大
- PRU执行路径:检查PRU的ISR代码是否过于冗长。PRU虽然快,但如果你在ISR里做了复杂的计算或内存访问,还是会增加延迟。对于极速响应,ISR应只做最必要的操作(如读取数据、清除标志),将非实时任务推迟到主循环或通知给ARM处理。
- 内存访问延迟:PRU访问片外DDR或通过OCP总线访问复杂外设的寄存器,延迟远高于访问其本地RAM。尽量避免在ISR中进行此类访问。
- 总线竞争:如果PRU和ARM或其他主机同时激烈访问共享资源(如INTC寄存器所在的总线),可能会产生仲裁延迟。虽然不常见,但在极端高负载下需考虑。
5.4 调试技巧
- 寄存器打印:在Linux用户空间,可以通过
devmem2或编写小程序直接读取INTC的关键寄存器(SRSR,SECR,HIPIR,GPIR),实时查看中断状态、pending事件和优先级索引。这是最直接的诊断手段。 - PRU调试器:使用CCS或相关调试工具,可以在PRU代码中设置断点,单步跟踪,观察
R31的值和程序流,精确判断中断是否到达以及ISR执行情况。 - 系统事件追踪:有些高级仿真器或芯片可能支持对系统事件线的监控,可以图形化看到事件的发生、传递和清除过程。
- 从简单开始:先用一个确定能产生的中断(比如用一个GPIO引脚接高电平,模拟一个持续中断)进行测试,确保INTC到PRU的整个通路是通的。然后再接入真实的外设中断。
配置PRU INTC就像设计一个精密的铁路调度系统,每个事件是火车,通道是轨道,主机是车站。理解每一段轨道的走向、每一个信号灯的意义,才能让关键任务准点到达。它不像在Linux下写个中断处理函数那么简单直接,需要你同时考虑硬件映射、寄存器配置和两侧(PRU和ARM)的软件协同。但一旦掌握,你就获得了在确定性时域内驾驭硬件事件的强大能力,这正是工业控制、电机驱动、高速通信等硬实时应用的基石。