简介:面向瑞萨RH850/F1K汽车级32位MCU开发者的定时器中断基础例程,集中演示定时器外设的初始化流程、中断服务程序挂载方式与中断向量表配置方法,覆盖从底层寄存器操作到GHS工程构建的关键环节,适合嵌入式工程师及单片机学习者快速上手。压缩包共41个文件,其中7个头文件、2个C源文件与4个GHS工程文件构成核心代码框架,配合链接脚本、调试连接配置和SourceInsight工程,完整呈现可编译、可调试的工程结构,同时附有hex、map、list等构建产物,便于直接查看生成结果。整体仅358KB,结构轻量,目录层级清晰,并配有ReadMe文档辅助阅读。已有363人学习该资源。通过该例程可掌握RH850/F1K的中断响应流程、定时器寄存器操作方式与优先级设置,并以此为模板扩展到其他外设中断应用,有效降低项目初期的环境搭建与调试成本。例程中的中断向量表配置清晰,便于对照硬件手册理解。
1. 一个定时器中断例程背后的RH850中断链路
汽车电子里,RH850/F1K 这类 MCU 大量用于 BCM、网关和域控制器,而 R7F701581 的定时器中断几乎是所有周期任务的地基。你可以在网上找到许多官方例程,但像 F1K_GHS_2_R7F701581_TimerInterrupt 这样把 GHS 工程、SourceInsight 工程、链接映射文件、启动代码全部打包在一起的并不常见。它看起来只是一个 LED 闪烁级别的定时器例程,实际却把 RH850 的中断向量表、ICU 控制器、TAU 定时器单元和 GHS 链接脚本的协作关系完整串了一遍。对刚切换过来做 RH850 的工程师来说,这套工程值得逐文件拆开读,因为踩坑点几乎都藏在 startup 和 .ld 里。
2. R7F701581的TAU定时器架构与时钟分频配置
2.1 TAU 单元结构与通道分工
RH850/F1K 内部的定时器阵列单元(Timer Array Unit)被拆成若干独立通道,每个通道都可以工作在间隔定时器模式、PWM 输出模式、输入脉冲间隔测量模式等。R7F701581 的 TAU 通常包含两个单元,每个单元四个通道,例程里的 r_tau.c 只用了其中一个通道,这正好适合作为入门分析对象。
每个通道对应一组控制寄存器,常用的有 TMR(模式寄存器)、TMCR(控制寄存器)、TMM(匹配寄存器)和 TDR(数据寄存器)。间隔定时器模式下,计数器沿着时钟源逐步增加,当计数值与 TMM 匹配时产生事件,硬件自动清零并继续下一轮计数,同时拉起中断请求信号。这个过程不占用 CPU 周期,因此非常适合做固定的时间基准。
2.2 时钟源与分频链路的选型思路
TAU 的输入时钟并非直接使用主频,而是经过系统时钟生成器(RH850 里的 SGCU 模块)分配后的一路低频或高频时钟。例程中默认选择的是 PCLK 分频后的时钟,因为 PCLK 与 CPU 主频之间通常是整数倍关系,便于用户计算精确的中断周期。
实际配置时,我一般会先确认 PCLK 的频率,再查看 TAU 单元的时钟选择寄存器(CKSR)决定是哪一路时钟源。接着通过 TPS 寄存器设置预分频值。比如 PCLK 为 80MHz,TPS 设为 0,则计数时钟就是 80MHz;若要降低能量消耗,可设置分频为 2 或 4,代价是计数器分辨率下降。
下表列出常见分频系数与最小中断间隔的对应关系,假设 PCLK=80MHz,计数器为 16 位(最大计数值 65535):
| 分频系数 | 计数时钟频率 | 单次计数时间 | 最大匹配值对应周期 |
|---|---|---|---|
| 1 | 80MHz | 12.5ns | 819.2us |
| 2 | 40MHz | 25ns | 1.638ms |
| 8 | 10MHz | 100ns | 6.553ms |
| 64 | 1.25MHz | 800ns | 52.42ms |
| 128 | 0.625MHz | 1.6us | 104.86ms |
需要注意的是,超出 16 位计数器上限的周期,需要改用周期更长的时钟源,或者在中断服务程序里累加软件计数。这里的选择会在第四章的具体代码中体现。
2.3 r_tau.c 中体现的时钟配置痕迹
在例程的 r_tau.c 里,初始化函数通常会先对 TAU 单元做全局复位,例如向 TT0 寄存器写入全部通道的停止请求,确保之前的状态不会干扰配置。随后设置通道 0 的 TMR 寄存器,选择间隔定时器模式,并配置计数时钟为 PCLK 的指定分频。这类代码片段在 GHS 的寄存器定义头文件 dr7f701581.dvf.h 和 device.h 中都有对应宏,方便直接读寄存器位。
因为该例程的名字是 TimerInterrupt,所以整个配置基本围绕“启动计数并等待中断”展开。区别于 PWM 输出,这里不需要配置 TOE(输出使能)和 TOR(输出引脚选择),中断路径更加干净,适合把注意力放在中断链路上。
3. GHS编译器下的中断向量表构建与ICU使能
3.1 启动代码中的向量表占位
GHS 编译器生成的 RH850 工程,中断向量表通常由启动文件 dr7f701581_startup.850 和链接脚本 dr7f701581.ld 共同决定。启动文件里会定义一段连续的地址空间,每个入口占用 4 字节或 16 字节,具体取决于使用的中断类型。R7F701581 的 CPU 支持两级中断架构,外设中断通过 ICU(Interrupt Controller Unit)统一汇入。
在 dr7f701581_startup.850 中,我见过这样的写法:
// 为 CPU 异常和外部中断预留向量槽 __section(".intvect") void (* const __intvect_table[])() = { __RESET, // 0x00000000 复位入口 __INTF_0, // 保留 __INTF_1, // 保留 // ... 后续为各外设中断入口 __ICU_TAU0_0, // TAU 通道 0 中断 __ICU_TAU0_1, // TAU 通道 1 中断 // ... };这里的__section(".intvect")告诉链接器把这个表放到指定地址段。而链接脚本 dr7f701581.ld 中,会有类似下面这样的定义:
MEMORY { ROMVECT : ORIGIN = 0x00000000, LENGTH = 0x400 ROMCODE : ORIGIN = 0x00100000, LENGTH = 0x180000 }通过__mem_rom_vstart和__mem_rom_vect等符号,向量表被放在代码段之前。如果修改了中断函数入口,同时忘记更新启动文件中的符号对应关系,就会出现中断触发后跳到错误地址的经典故障,排查起来非常困难。
3.2 使用 GHS 的#pragma interrupt注册服务函数
GHS 针对 RH850 提供了比较简洁的中断注册语法:在中断服务函数上方声明#pragma interrupt,告知编译器该函数需要保存和恢复额外寄存器,并且使用 iret 指令返回。典型写法如下:
#pragma interrupt R_TAU0_Interrupt void R_TAU0_Interrupt(void) { // 清中断标志,处理用户逻辑 ICU.ISR[0].BIT.IT = 1U; }编译器会自动把该函数地址填入对应中断向量槽。但要注意,#pragma interrupt中的名称必须与启动文件中的引用名称保持完全一致,否则链接阶段不会报错,实际却是普通函数调用,中断永远无法触发。我在排查其他工程时,遇到最多次的就是这类名称不匹配问题。
3.3 ICU 的使能与优先级设置
RH850 的外部中断由 ICU 模块管理,每个中断源对应一个中断请求编号,例如 TAU0 通道 0 中断会挂在 ICU 的某个通道上。要使能该中断,需要操作 ICU 的使能寄存器:
// 使能 TAU0 通道 0 中断请求 ICU.IER[TAU0_0_IRQ].BIT.IEN = 1U; // 设置优先级为 6 ICU.ISCR[TAU0_0_IRQ].BIT.PRI = 6U;优先级数值越小,中断级别越高。如果不设置优先级,默认可能被挂在同一个等级,导致多个中断同时请求时产生不确定行为。R7F701581 还支持中断请求响应清除模式,需要在服务函数里显式清除内部标志,避免中断反复进入。
4. 从main到r_tau.c:定时器中断初始化与参数调整
4.1 主函数中的初始化流程
例程的 main.c 并不复杂,但顺序很重要。首先要调用硬件初始化,再配置定时器,最后开启全局中断。以下是常见做法的简化代码:
#include "device.h" #include "r_tau.h" void main(void) { // 关闭所有中断,避免初始化过程被打断 __DI(); // 初始化系统引脚、时钟和看门狗 HardwareSetup(); // 初始化 TAU 通道 0 为间隔定时器 R_TAU_Init(); // 设置 TAU 通道 0 中断并使能 R_TAU_Start(); // 开启全局中断 __EI(); while (1) { // 主循环可执行低优先级任务 } }代码中__DI()和__EI()是 GHS 内置的关中断和开中断指令。在初始化外设时先关闭全局中断,可以防止外设中断在配置未完成时触发。要注意,R_TAU_Init 内部可能还会操作 ICU 的屏蔽寄存器,因此在主函数里__EI()之前,外设中断已经处于 pending 状态,一旦开全局中断立刻会进入服务函数,这也是调试时经常看到的诡异现象。
4.2 R_TAU_Init 的寄存器写入细节
r_tau.c 里的初始化函数,会针对 TAU 单元 0 的通道 0 做一组寄存器配置。我见过典型的实现是:
void R_TAU_Init(void) { // 停止通道 0 的计数 TT0.BIT.TT0 = 1U; // 清通道 0 中断标志 TCR0.BIT.TCF = 0U; // 选择时钟源:PCLK 8 分频 TMR0.BIT.CK1 = 0U; TMR0.BIT.CK0 = 1U; TMR0.BIT.CKS = 0U; // 设置为间隔定时器模式 TMR0.BIT.MD1 = 0U; TMR0.BIT.MD0 = 0U; // 写入匹配值,也就是计数的上限 TDR0 = 12500U; }这里逐位设置的字段含义是:CK1/CK0/CKS共同决定 TAU 通道的计数时钟来源以及分频系数,MD1/MD0选择操作模式,TDR0是 16 位匹配寄存器。如果 PCLK 是 80MHz,8 分频后得到 10MHz 计数时钟,那么 TDR0 设为 12500 时,中断周期就是 12500 / 10MHz = 1.25ms。
4.3 中断周期调整与计数值计算
中断周期由时钟源、分频系数和 TDR0 三者决定。直接修改 TDR0 是最直观的方式,但要注意它的范围是 0x0000 到 0xFFFF。若需要更长的周期,应当先调大分频系数,再调整 TDR0。下面给出一个实用的计算表格:
| 目标周期 | PCLK=80MHz | 分频系数 | 实际计数频率 | 计数器周期 | TDR0 设置值 |
|---|---|---|---|---|---|
| 1ms | 80MHz | 8 | 10MHz | 100ns | 10000 |
| 5ms | 80MHz | 64 | 1.25MHz | 800ns | 6250 |
| 10ms | 80MHz | 64 | 1.25MHz | 800ns | 12500 |
| 100ms | 80MHz | 128 | 0.625MHz | 1.6us | 62500 |
| 500ms | 80MHz | 128 | 0.625MHz | 1.6us | 31250 |
实际工程中我一般会先写一个脚本,用 Python 或者 Excel 把候选分频方案列出来,选出 TDR0 数值不溢出的组合。GHS 的调试器也可以直接读取 TDR0 寄存器,验证当前实际值是否符合预期,但更稳妥的方式是用逻辑分析仪或示波器测量 GPIO 翻转频率。
4.4 中断服务函数里的标志清除
中断触发后,TAU 通道会产生匹配事件,硬件会设置对应的事件状态位。如果服务函数里不手动清除该位,中断请求会一直保持有效。RH850/F1K 的 TAU 通道事件状态位于 TCR 寄存器的 TCF 位,清除方式通常是写 1 清 0:
__interrupt void R_TAU0_Interrupt(void) { // 清除 TAU0 通道 0 匹配标志 TCR0.BIT.TCF = 0U; // 用户自定义逻辑 PortPinToggle(); }但这里有两个细节容易搞错。一个是 TCF 位的清除必须在读取相关数据之后进行,否则可能丢失边沿事件;另一个是有些 RH850 型号的 TAU 中断清标志还必须在 ICU 的 ISR 寄存器里额外写一次,形成“双清”操作。每颗芯片的硬件手册对 TCF 的访问方式描述不同,R7F701581 的例程中如果同时看到对 ICU.ISR 的清零操作,那是在做第二层确认,不必感到奇怪。
5. 通过map文件验证向量表并用软定时器扩展中断应用
5.1 使用example.map确认中断向量地址
GHS 链接完成后生成的 example.map 文件是所有工程师最容易忽略的宝藏。打开它,可以找到中断向量表段的符号地址。例如搜索R_TAU0_Interrupt,能看到类似这样的记录:
R_TAU0_Interrupt 0x0001f3c2 0x0000003a地址落在链接脚本定义的向量表区间,说明#pragma interrupt生效并且符号被正确链接。如果该符号地址在 0x00100000 之后的代码段,大概率是寄存器声明写错了。另一个值得检查的值是栈顶地址,map 文件开头会显示__stack和__top_of_stack,确保栈位于 RAM 有效区域。
5.2 软定时器扩展:不额外占用硬件资源
当 R7F701581 的硬件 TAU 通道不够用时,最自然的做法是在定时器中断里维护多个软件定时器。先在中断服务函数里保存一个节拍计数:
volatile uint32_t tick_count = 0U; __interrupt void R_TAU0_Interrupt(void) { TCR0.BIT.TCF = 0U; tick_count++; }然后提供一个查询函数,判断某个任务是否到期:
uint8_t SoftTimer_Check(uint32_t start_tick, uint32_t interval) { return (uint32_t)(tick_count - start_tick) >= interval; }由于tick_count是无符号 32 位整数,计算差值时即使发生回绕也能获得正确结果。这里可以用 GHS 的调试器人为修改tick_count来模拟溢出场景,验证时间基准在 49 天回绕后是否依然准确。
5.3 中断响应时间测量的两个小技巧
第一个技巧,是在定时器中断服务函数的入口处翻转一个 GPIO 引脚,用示波器测量两次翻转之间的间隔,就能直接看到实际中断周期与理论计算之间的偏差。这比在调试器里看断点更加直观,能发现时钟树配置错误、中断嵌套抢占等问题。
第二个技巧,是在中断函数里临时读取 ICU 的当前优先级寄存器。如果发现进入中断时优先级不是预期值,说明初始化过程中某个寄存器被意外改写。我通常会在HardwareSetup前后各设置一次断点,对比中断使能寄存器的变化,快速定位是哪个外设驱动覆盖了 ICU 配置。
这套例程虽然只做了定时器中断最基本的动作,但当你把 map 文件中的向量地址和实际中断触发点对应起来后,RH850/F1K 的中断链路在它之上就能建立清晰的认识。后续再处理 P50 端口、CAN 唤醒或者多优先级嵌套时,都能复用这套验证方法。
本文还有配套的精品资源,点击获取