RH850低功耗唤醒实战:Deep Sleep与INTP12中断链路解析

发布时间:2026/9/17 17:58:58
RH850低功耗唤醒实战:Deep Sleep与INTP12中断链路解析
简介这是一份围绕RH850系列微控制器深度睡眠Deep Sleep唤醒功能的完整工程示例核心演示通过INTP12中断引脚将芯片从低功耗模式唤醒。资源面向刚接触嵌入式开发的初学者也适合需要快速上手RH850低功耗设计的工程师代码已在实际硬件上验证。压缩包共176个文件以81个C源文件和92个头文件为主配合2个汇编文件与1个mtpj工程配置文件boot.asm和cstart.asm负责启动初始化C文件则实现中断向量、端口配置、定时器及通信驱动等模块整体仅397KB轻量易读。项目覆盖RH850寄存器操作、Deep Sleep模式进入与退出、中断服务程序编写、汇编与C混合编程等关键知识点目录结构清晰便于循序渐进阅读。目前已有1125人学习浏览对希望掌握汽车电子低功耗和中断响应机制的开发者而言是一份可直接编译研究并验证的实战参考。1. RH850 的 Deep Sleep 与 INTP12一条边沿如何唤醒整颗芯片RH850 的 Deep Sleep对应瑞萨文档里的 Deep Stop 模式并不是简单地把 CPU 挂起。软件在进入前需要关掉 PLL、停掉主振荡器把大多数外设时钟切断这时候芯片只保留内部低速时钟和少量唤醒源在工作。INTP12 是外部引脚中断输入它属于深度停止期间仍能检测边沿的唤醒源外部信号跳变后中断控制器把请求透传给电源管理单元芯片走完固定的恢复时序CPU 重新开始执行代码。这个项目把这条链路完整呈现了出来——boot.asm 与 cstart.asm 负责启动和 C 运行环境RIIC、TAU、i2c_Sim_drive.c 负责外围通信INTP12 负责把芯片从 Deep Sleep 里叫醒而且代码已经跑在真实硬件上。适合刚接手 RH850 的工程师也适合要做低功耗方案、手里缺一份现成参考的人。2. 启动代码与中断向量boot.asm、cstart.asm 在唤醒链路里的分工2.1 boot.asm复位后的栈指针、看门狗与异常基地址RH850 上电后先从 boot.asm 的复位入口开始执行。这段汇编做四件事设置栈指针、关闭看门狗、把 EBASE异常基地址寄存器指到中断向量表、把时钟切换到合适的初始源。栈指针在 RH850 里是系统寄存器 SR0不能靠一条mov直接改 sp而是先放到通用寄存器再通过ldsr写回。; boot.asm 复位后关键流程节选 mov #__stack_top, r10 ; __stack_top 是链接脚本里的栈顶符号 ldsr r10, sr0 ; 写系统寄存器 SR0完成设栈 ; 关闭看门狗避免初始化还没跑完就被复位 ; 看门狗控制寄存器地址随型号不同CS 模板里有具体定义 mov #0x0, r11 st.b r11, 0xFFFFF800[r0] ; 示意写 WDTA 控制字 ; 设置异常基地址让内核知道中断向量表在哪 mov #__intvector_table, r12 ldsr r12, ebase ; 跳转到 C 运行环境初始化 jarl _cstart, lpldsr r10, sr0是这一段的关键把暂存在 r10 的栈顶地址写入系统寄存器 SR0之后硬件会自动维护栈指针。看门狗必须在上电后尽早关闭否则 CS 默认的看门狗窗口很短长一点的初始化代码会自动触发复位实际项目中看门狗可以在进入主循环后再用定时器任务喂但 Deep Sleep 期间看门狗如果还在工作芯片会反复复位这点在第五章再展开。EBASE 的设置决定中断服务程序入口的位置INTP12 的中断向量就是 EBASE 加固定偏移偏移量查对应芯片的中断向量表即可。2.2 cstart.asm从汇编转到 C 的收尾工作cstart.asm 是 C 运行时的入口通常由编译器或 CS 工程模板提供。它补齐 C 语言运行的前提把 .data 段从 ROM 拷贝到 RAM、把 .bss 段清零、配置堆栈和堆然后调用 main。在低功耗项目里这段代码与唤醒后的稳定性没有直接关系但有一个细节要注意如果启动阶段没有把 .data 段拷对main 里的全局变量初始值就是错的而 Deep Sleep 唤醒后依赖这些全局变量判断状态问题会被放大。; cstart.asm 中 .bss 清零环节节选 mov #__bss_start, r6 mov #__bss_end, r7 .bss_loop: cmp r6, r7 bge .bss_done st.w r0, 0[r6] ; r0 恒为 0按 4 字节写 add 4, r6 br .bss_loop .bss_done:__bss_start和__bss_end是链接脚本给出的符号CS 里通常由编译器自动生成。把逐字节清零改成逐 4 字节清零可以减少循环次数对启动时间有一点帮助对新手来说也是一个容易理解的优化思路。cstart.asm 的最后一条分支会跳到 main自此汇编代码让位给 C 语言。2.3 r_cg_intvector.cINTP12 的中断服务函数写在哪里r_cg_intvector.c 是 CS Code Generator 生成的中断向量定义文件里面列出了工程用到的全部中断入口。INTP12 的中断服务函数在这个文件里被声明并挂到对应的向量段用户只需要在函数体里补自己的处理逻辑函数名则由生成器固定比如r_intc_intp12_handler。/* r_cg_intvector.c 中 INTP12 中断入口CC-RH 写法 */ __interrupt static void r_intc_intp12_handler(void) { /* 用户代码区 * 1. 清 INTP12 中断标志 * 2. 置唤醒标志把耗时操作留给主循环 */ g_wakeup_flag 1; }CC-RH 编译器用__interrupt修饰函数链接器会把这个函数的地址自动填进中断向量表。写自己的处理逻辑时不要在这里做长时间操作比如重新配置 PLL 或初始化 I2C 外设因为低功耗场景里中断服务程序越短越不容易与主循环产生竞态。通常做法是只置一个volatile标志主循环检测到标志后统一做恢复。3. RIIC、TAU 与 I2C 模拟四个外设代码模块的协作3.1 r_riic_std.c 与 r_riic1_std.c硬件 I2C 的收发框架r_riic_std.c 是对 RH850 片内 RIIC 外设的驱动封装。RIIC 是硬件 I2C 控制器常见寄存器包括 ICCR1控制、ICSR状态、ICDR数据。CS 的 Code Generator 会生成主机发送、主机接收、从机处理等接口函数。工程里出现 r_riic_std.c 和 r_riic1_std.c 两个文件说明芯片上有两个 RIIC 通道被同时使用这在带多传感器或屏幕的板子上很常见。/* r_riic_std.c 中 RIIC0 主机发送一帧数据的骨架 */ uint8_t R_RIIC0_Master_Send(uint8_t addr, uint8_t *tx_buf, uint16_t len) { uint16_t i; RIIC0.ICCR1.BIT.IICE 1U; /* 使能 RIIC0 模块 */ RIIC0.ICSR.BIT.NACKF 0U; /* 清零 NACK 标志 */ /* 发送 7bit 地址最低位是写标志 0 */ RIIC0.ICDR (uint8_t)(addr 1U | 0x00U); for (i 0U; i len; i) { while (0U RIIC0.ICSR.BIT.TDRE) { ; } /* 等待发送数据寄存器空 */ RIIC0.ICDR tx_buf[i]; /* 写入下一个字节 */ } while (0U RIIC0.ICSR.BIT.TDRE) { ; } RIIC0.ICCR1.BIT.STOP 1U; /* 生成停止条件 */ return 1U; }addr是 7 位从机地址调用方不需要自己移位驱动里统一左移一位并拼上读写标志tx_buf和len是数据指针与长度。代码里所有while轮询都依赖 TDRE 位它表示发送数据寄存器已空可以继续写下一个字节。这个实现没有超时退出实际调试时如果从机不拉低时钟或总线被占用程序会卡死在 while 循环里所以我一般会在外面套一层超时判断或者用 TAU 定时器做看门狗式保护。3.2 r_tau.c定时器提供时间基准与超时保护r_tau.c 是 TAU 定时器阵列的驱动文件。TAU 在低功耗项目里承担两个角色一是提供软件时钟基准也就是固定间隔的中断来维护系统 tick但 Deep Sleep 期间 TAU 的时钟也会被停掉所以 tick 计数只能表示唤醒后的时间二是在主循环里作为超时保护避免外设通信卡死。/* r_tau.c 中把通道 0 配置为 1ms 间隔定时器的回调 */ static volatile uint32_t g_tick_ms; __interrupt static void R_TAU0_Channel0_Isr(void) { g_tick_ms; /* 每 1ms 中断一次累加 */ } uint32_t get_tick_ms(void) { uint32_t tick; __DI(); /* 短临界区防止读取被中断改写 */ tick g_tick_ms; __EI(); return tick; }get_tick_ms的临界区保护很关键如果主循环在读取g_tick_ms的中间被中断打断高 16 位和低 16 位可能来自不同时刻得到错误的时间戳。Deep Sleep 唤醒后g_tick_ms不会自动清零如果需要计算本次睡眠时长要在进入睡眠前记下基准值唤醒后用差值计算。TAU 的通道在唤醒后要确认是否已由硬件自动停止不同 RH850 型号的行为不完全一致稳妥做法是每次唤醒后重新启动定时器。3.3 i2c_Sim_drive.c为什么项目里留一份软件 I2C只要没有特别的原因我一般不在带硬件 I2C 的 MCU 上再写软件 I2C但这个工程同时保留了 RIIC 驱动和 i2c_Sim_drive.c。常见的原因是RIIC 在总线繁忙或异常断电时可能进入不可恢复状态需要等总线空闲条件或软件复位而软件 I2C 用 GPIO 自己拉时序只要引脚状态可控就没有这个限制在调试传感器或测试新器件时更灵活。/* i2c_Sim_drive.c 软件 I2C 起始条件与发送一字节 */ void i2c_sim_start(void) { I2C_SDA_SET_HIGH(); I2C_SCL_SET_HIGH(); i2c_sim_delay(); I2C_SDA_SET_LOW(); /* SCL 为高时 SDA 拉低这是起始条件 */ i2c_sim_delay(); I2C_SCL_SET_LOW(); /* 拉低 SCL准备传数据位 */ } uint8_t i2c_sim_send_byte(uint8_t data) { uint8_t i; for (i 0U; i 8U; i) { if (data 0x80U) { I2C_SDA_SET_HIGH(); /* 高位在前 */ } else { I2C_SDA_SET_LOW(); } data 1U; I2C_SCL_SET_HIGH(); /* 让从机在 SCL 高电平期采样 */ i2c_sim_delay(); I2C_SCL_SET_LOW(); i2c_sim_delay(); } I2C_SDA_SET_HIGH(); /* 释放 SDA等待 ACK */ I2C_SCL_SET_HIGH(); (void)I2C_SDA_READ(); /* 第 9 个时钟读取从机应答位此处忽略结果 */ I2C_SCL_SET_LOW(); i2c_sim_delay(); return 1U; }软件 I2C 的速度完全由i2c_sim_delay决定把这个延时调小可以跑到几百 kHz再往上就受 GPIO 翻转速率和中断响应限制。它的 ACK 判断在生产代码里不能像上面这样忽略至少要加一层重试逻辑——如果从机上电时序慢第一次访问时还没就绪连续重试几次往往能成功。3.4 r_sent.c 与 Config_PORT.c另一条传感器通路工程里出现 r_sent.c说明还有一路传感器走的是 SENT 协议。SENTSingle Edge Nibble Transmission是汽车领域常用的单线点对点协议用脉宽编码传数据RH850 的部分型号集成硬件 SENT 模块驱动文件主要做通道启停、触发放置和错误标志查询。SENT 是单线协议与 I2C 不同Deep Sleep 期间 SENT 引脚的保持电平要按传感器手册决定不能一概而论。Config_PORT.c 则是所有引脚复用、方向、上下拉的集中配置入口。CS 的 Code Generator 里把 INTP12 对应的引脚在 Port 配置页勾选为外部中断输入生成器就会把初始化代码写进 Config_PORT.c。排查“为什么唤醒后 SCL 一直为低”这类问题时先翻这个文件确认引脚复用是否正确比在硬件上反复量波形更快。3.5 comm_hal.c把硬件差异收进一层接口comm_hal.c 是专门做抽象的一层把 RIIC 和软件 I2C 统一成同样的接口上层业务代码只需要面对“读写某地址寄存器”这一个动作。这样设计的好处是Deep Sleep 唤醒后发现硬件 RIIC 异常可以整体切换到软件 I2C 通道而不需要改业务代码。/* comm_hal.c 中一组总线操作结构体 */ typedef struct { uint8_t (*init)(void); /* 初始化总线 */ uint8_t (*read)(uint8_t addr, uint8_t reg, /* 读从机寄存器 */ uint8_t *buf, uint16_t len); uint8_t (*write)(uint8_t addr, uint8_t reg, uint8_t *buf, uint16_t len); } comm_bus_t; /* 硬件 RIIC0 总线和软件模拟 I2C 总线各挂一份实现 */ static const comm_bus_t g_bus_riic0 { riic0_init, riic0_read, riic0_write }; static const comm_bus_t g_bus_sim { i2c_sim_init, i2c_sim_read, i2c_sim_write };comm_bus_t里的三个函数指针把初始化和收发动作抽象成统一调用方式g_bus_riic0与g_bus_sim分别绑定了两套实现。函数指针相比直接调用有少量开销但在 RH850 这类高性能内核上可以忽略换来的是移植和调试的便利。对这个项目来说最实际的价值在于INTP12 唤醒后的恢复函数只需要调用g_bus_riic0.init()如果总线状态也要恢复就可以在 HAL 层统一完成不需要在上层代码里分别处理。4. CS 工程配置与 INTP12 唤醒项目的移植步骤4.1 .mtpj 工程文件里需要核对的关键项RH850_WAKEUP.mtpj 是 CS 的工程文件里面保存了芯片型号、编译器选项、section 分配和调试器设置。用文本编辑器打开会看到 XML 结构字段随 CS 版本略有差异但关键信息就几类!-- RH850_WAKEUP.mtpj 关键配置示意 -- Project Device NameR7F701587 SeriesRH850/ Compiler NameCC-RH Optimization2 /Compiler LinkSection Section Name.text Address0x00000000/ Section Name.stack Address0xFEDC0000/ /LinkSection /ProjectDevice 决定使用哪一套芯片头文件和启动文件Compiler 决定 CC-RH 的优化级别和宏定义LinkSection 决定代码段和栈的位置。中断向量表的地址由 EBASE 和链接脚本共同决定不要随意修改.text起始地址除非你同时更新了 boot.asm 里的向量表符号。在 CS 图形界面里这几个选项分别在 Device、Compiler、Linker 三个属性页下。4.2 main 循环里如何进入 Deep Sleep进入 Deep Sleep 前要把外设状态准备好特别是通讯总线。常见流程如下void enter_deep_sleep(void) { __DI(); /* 关总中断 */ MaskOtherInterrupts(); /* 只保留 INTP12 的唤醒使能 */ SwitchClockToMoco(); /* 切到内部振荡器 */ StopPll(); /* 关闭 PLL这是大部分耗电来源 */ /* 把 I2C 引脚置成安全电平避免总线浮空漏电 */ KeepBusIdle(); /* 进入停止模式直到 INTP12 边沿到来被唤醒 */ EnterDeepStop(); __EI(); /* 唤醒后继续执行到这里 */ }关总中断放在第一步是为了防止准备过程中被其他中断打断导致进入睡眠的步骤不完整。EnterDeepStop()执行完后芯片停留在停止状态INTP12 触发唤醒时CPU 会先重新起振、恢复基本的执行条件然后回到__EI()之后。唤醒后的时钟默认不一定是 PLL所以__EI()之后通常还要重新配置时钟。4.3 INTP12 唤醒后的恢复顺序唤醒后的恢复顺序比进入顺序更影响稳定性。我一般按下面的次序处理步骤操作原因1清 INTP12 中断标志防止返回主循环后再次触发中断2重新配置时钟PLL 切换外设需要主时钟才能工作3重新初始化 RIIC / TAU深睡期间外设时钟被切断状态不复存在4恢复端口方向与输出值避免 GPIO 输出悬挂导致漏电5打开总中断恢复正常任务调度注意这张表是通用顺序。具体到某型号建议在硬件上实测一次唤醒过程确认哪些寄存器在深睡期间仍然保留再决定能否跳过某一步。清中断标志的先后顺序在不同 RH850 型号上有差异实测时如果唤醒后反复进入 ISR先检查这一步。恢复外设时要注意RIIC 的初始化函数不能直接复用上电时的初始化函数——上电时总线尚未接从机而唤醒时从机可能已经在工作初始化完成后要额外发送一个总线空闲状态。/* 唤醒后的恢复示例 */ void wakeup_recover(void) { /* 1. 恢复时钟到工作状态 */ RestoreSystemClock(); /* 2. 重新初始化外设 */ g_bus_riic0.init(); /* 通过 comm_hal 层统一调用 */ R_TAU0_Channel0_Start(); /* 3. 打开中断 */ __EI(); }g_bus_riic0.init()统一走 comm_hal 抽象层这样如果唤醒后发现 RIIC 不能正常工作可以把g_bus_riic0临时替换成g_bus_sim只改一行绑定代码。要注意 RIIC 初始化和 TAU 启动的先后关系如果传感器挂在总线上RIIC 初始化后需要等待一段总线空闲时间这个延时可以用刚重新启动的 TAU 计数来实现。5. 验证唤醒时序用 TAU 打时间戳以及三个容易踩的坑5.1 用 TAU 测量 INTP12 唤醒时间验证 INTP12 是否真的把芯片从 Deep Sleep 中唤醒最直接的方法不是看仿真器而是 GPIO 翻转加逻辑分析仪因为调试器连接状态下芯片的时钟和断点行为与实际运行不同。做法是进入 Deep Sleep 前把一个普通 GPIO 拉低在 INTP12 中断服务程序里把同一引脚拉高唤醒恢复完成后拉低。逻辑分析仪上高电平宽度就是“唤醒请求到 CPU 执行 ISR”的时间。/* 唤醒事件打点 */ __interrupt static void r_intc_intp12_handler(void) { GPIO_WAKE_MARK 1; /* 拉高测试引脚逻辑分析仪记录这里 */ g_wakeup_flag 1; GPIO_WAKE_MARK 0; /* 拉低标记 ISR 结束 */ }同时用 TAU 在 ISR 里读取计数器值可以换算出从唤醒请求到 ISR 的时钟周期数。注意 Deep Sleep 唤醒后 TAU 要重新启动所以打点用的计数器必须在恢复流程里先于 ISR 触发配置好否则 ISR 里读到的只是随机值。5.2 三个容易踩的坑第一个坑是看门狗。Deep Sleep 期间如果看门狗还在跑芯片会在唤醒后不久被复位现象是“唤醒后立刻重启”。部分 RH850 型号的看门狗在停止模式下仍然计数需要在进入 Deep Sleep 前关闭或进入喂狗暂停模式。这个配置务必在测试前确认。第二个坑是 INTP12 的滤波与沿选择。外部信号上升沿触发唤醒但信号源在上下电时出现抖动一次真实唤醒会伴随多次误触发。可以在引脚初始化时打开数字滤波窗口并选择双边沿触发唤醒后在 ISR 里做去抖计数连续几次有效才置位标志。第三个坑是软件 I2C 引脚的输出状态。Deep Sleep 前如果 SCL、SDA 处于未定义状态从机在总线上可能进入错误状态唤醒后第一笔通信就失败。进入睡眠前把总线释放成高阻或明确置高唤醒后重新初始化这两步之间不要穿插其他外设操作。最后给一个验证技巧用一根杜邦线把 INTP12 接到按键或信号发生器做手动触发按键两端并联 100nF 电容消除机械抖动。逻辑分析仪同时抓 INTP12 引脚、唤醒打点 GPIO 和 SCL 波形可以清楚看到“进入休眠–触发唤醒–ISR 执行–总线恢复”的完整链路。如果你有示波器把唤醒后的第一个 I2C 读操作和 INTP12 打点放在同一屏幕上还能直接读出总线恢复延时是否符合从机的上电时序要求。本文还有配套的精品资源点击获取