简介:面向汽车电子嵌入式开发者的 RH850/F1L 例程参考包,围绕瑞萨 CS+ 开发环境下的工程搭建、CAN 总线通信、时钟与中断配置、GPIO/定时器/ADC 等外设编程展开,适合已具备一定单片机基础、希望快速熟悉 RH850/F1L 软件结构与调试流程的工程师和学生对照学习。压缩包共 29 个文件,约 110KB,包含 C 源文件与汇编启动代码、头文件、目标文件,以及 CS+ 工程/调试配置、链接脚本、映射文件和可执行产物,能够较完整地呈现从源码组织到编译链接的工程脉络。已有 5750 人学习,说明该例程在同类参考资料中具有较高关注度。读者可从中梳理 CAN 示例的模块划分、寄存器与中断处理方式、启动及链接配置关系,并借鉴工程目录组织和构建选项设置,结合 CS+ 的断点与变量观察能力定位外设初始化问题,为后续移植与二次开发提供可复用的参考。
1. RH850/F1L 例程不是拿来烧的:先看懂三层结构
很多人拿到 RH850/F1L 例程包,第一反应是打开工程、点编译、下载,看到 LED 闪了就以为摸透了。换一块板子、换一个引脚,程序直接趴窝,回头查半天才发现问题不在驱动,而在启动文件和链接脚本。RH850/F1L 这类车规级 32 位 MCU 的例程开发参考,真正值钱的地方是把“复位后到 main 之前发生了什么”摊开给你看。它适合刚接手 RH850/F1L 项目的嵌入式工程师、从别的 MCU 平台迁过来的熟手,以及需要拿例程改量产代码的人。下面按工程结构、外设闭环、参数组、现场避坑、裁剪交付五步拆,每一步都落到能照着改的细节上。
2. 拆开例程包:从工程目录到启动文件的关键路径
例程包刚解压时,目录看着乱,其实职责分得很清楚。先别急着编译,花十分钟把目录和文件对应关系画出来,后面改任何外设都知道该动哪一层。RH850/F1L 的例程通常不是单一工程,而是一组按外设切分的示例,共用一个底层目录,这种结构对移植友好,但也容易让人误改公共文件。
2.1 目录结构与文件职责
常见例程包的目录大致如下表。不同资源包命名会有差异,但分层逻辑基本一致。
| 目录/文件 | 典型内容 | 改动风险 |
|---|---|---|
| startup | 复位向量、栈初始化、.data/.bss 处理 | 高,改错直接跑飞 |
| linker | 链接脚本,定义 Flash/RAM 段与堆栈 | 高,段越界会静默踩内存 |
| common | 寄存器定义头文件、类型定义、编译宏 | 中,改宏会影响全局 |
| drivers | 各外设初始化与操作函数 | 中,按需裁剪 |
| sample | 按外设拆分的示例入口 | 低,主要改业务 |
| doc | 引脚分配、时钟说明、版本记录 | 低,先读再动手 |
需要重点盯的是 startup 和 linker 这两个目录。很多例程把中断向量表放在启动文件里,用汇编或 C 数组定义,表项顺序必须和芯片手册的中断源编号严格对应。你如果只改驱动、不动这里,通常没事;一旦要新增中断或调整向量表基址,就必须回来核对。common 里的寄存器头文件一般是厂商按手册整理的,结构体成员顺序和地址偏移一一对应,别手痒去“优化”它。
我一般会先做一件事:把 sample 里最小的那个工程单独拷出来,保留 startup、linker、common,删掉无关驱动,编译一次确认能过。这一步是给后面所有实验建一个干净底座,避免在几十个示例里来回切换时把公共文件改脏。
2.2 启动代码与链接脚本
复位之后,内核从固定地址取向量,启动代码要完成三件事:设置栈指针、把初始化数据从 Flash 搬到 RAM、清零未初始化段,然后跳 main。下面是一段典型启动流程的示意写法,实际汇编指令以你手头例程为准。
; 复位入口示意:栈顶地址来自链接脚本符号 Reset_Handler: mov sp, #__stack_top ; 设置栈指针,符号由链接脚本导出 ldr r0, =__data_load_start ; .data 在 Flash 中的加载地址 ldr r1, =__data_start ; .data 在 RAM 中的运行地址 ldr r2, =__data_end copy_data: cmp r1, r2 beq clear_bss ldr r3, [r0], #4 str r3, [r1], #4 b copy_data clear_bss: ldr r1, =__bss_start ldr r2, =__bss_end mov r3, #0 zero_bss: cmp r1, r2 beq jump_main str r3, [r1], #4 b zero_bss jump_main: bl main ; 进入 C 入口这段代码的关键不在指令本身,而在那几个链接脚本符号:__stack_top、__data_load_start、__data_start、__bss_start。它们必须和链接脚本里的段定义对得上,名字差一个字母,链接阶段可能不报错,运行起来就是随机死机。链接脚本里通常用 MEMORY 声明几块区域:代码 Flash、数据 Flash、本地 RAM、保留 RAM,再用 SECTIONS 把输入段映射进去。栈和堆一般放在 RAM 尾部,大小要按最坏中断嵌套和递归深度估,别照抄例程的默认值。
提示:改链接脚本后,先看编译输出的 map 文件,确认 .data、.bss、栈顶地址没有重叠,再下载。map 文件比调试器更早暴露问题。
还有一个容易忽略的点:向量表对齐。RH850/F1L 的向量表有对齐要求,例程里通常用段属性或链接脚本的 ALIGN 保证。你如果自己新建工程,把向量表随便放,可能出现“中断进不去但也不报错”的玄学现象。稳妥做法是沿用例程的段名和对齐写法,只改内容不改布局。
2.3 时钟与复位配置
时钟是例程里第二个黑匣子。RH850/F1L 一般有内部高速振荡、主振荡、副振荡和 PLL,复位后默认跑内部振荡,例程会在启动后切到 PLL。切换顺序很讲究:先确认目标时钟源稳定,再改分频和选择位,最后等切换完成标志。顺序反了,轻则外设波特率全错,重则程序跑飞。
/* 时钟初始化示意:先起振荡,再配 PLL,最后切换 */ static int clock_init(void) { volatile uint32_t timeout = 100000U; /* 1. 使能主振荡,等待稳定标志 */ CLK_OSC_CTRL |= OSC_ENABLE; while ((CLK_OSC_STATUS & OSC_STABLE) == 0U) { if (--timeout == 0U) { return -1; /* 起振失败,保持内部振荡运行 */ } } /* 2. 配置 PLL 倍频与分频,具体值查手册时钟树 */ CLK_PLL_CTRL = PLL_MUL(8U) | PLL_DIV(2U); CLK_PLL_CTRL |= PLL_ENABLE; timeout = 100000U; while ((CLK_PLL_STATUS & PLL_LOCKED) == 0U) { if (--timeout == 0U) { return -2; /* PLL 未锁定,不要继续切时钟 */ } } /* 3. 切换系统时钟源到 PLL,并确认选择生效 */ CLK_SYS_CTRL = SYS_SEL_PLL; while ((CLK_SYS_STATUS & SYS_SEL_MASK) != SYS_SEL_PLL) { /* 等待硬件完成切换 */ } return 0; }参数上要盯三处:PLL 倍频和分频决定系统频率,分频链决定外设时钟,稳定等待超时决定异常时能不能退出。超时值不要抄一个魔数就完事,按振荡器起振时间留足余量,一般给到毫秒级。返回值也要处理,起振失败时继续用内部振荡跑,至少能让调试口活着,别一失败就死循环。
复位源同样值得在例程里加一段打印或记录。上电复位、看门狗复位、软件复位对应的标志位不同,量产后现场偶发重启,第一件事就是读复位标志,能省掉大量猜测。
3. 把第一个外设点起来:GPIO 与中断的最小闭环
时钟跑通之后,下一步不是急着上 CAN 或 ADC,而是用 GPIO 加一个定时中断搭最小闭环。这个闭环能同时验证四件事:端口配置对不对、中断向量挂没挂上、栈够不够用、调试手段通不通。例程里 GPIO 示例往往只做电平翻转,实际项目里还要处理复用功能、上下拉和读改写冲突,得自己补。
3.1 端口寄存器操作模板
RH850/F1L 的端口通常按组管理,每组有模式寄存器、数据寄存器、功能控制寄存器。配置一个输出脚,要先把复用功能关掉,再设方向,最后写数据。下面是一个通用模板,寄存器名按你手头头文件替换。
/* 端口初始化示意:某组某引脚配置为普通输出 */ static void port_init_output(volatile uint32_t *pmr, volatile uint32_t *pbd, volatile uint32_t *p, uint8_t pin) { /* 1. 关闭该引脚的复用功能,先当普通 IO 用 */ *pmr &= ~(1UL << pin); /* 2. 配置方向为输出,具体寄存器名以手册为准 */ *pbd |= (1UL << pin); /* 3. 初始电平拉低,避免上电瞬间误触发外部电路 */ *p &= ~(1UL << pin); } /* 翻转输出:用异或,避免读改写时被中断打断 */ static inline void port_toggle(volatile uint32_t *p, uint8_t pin) { *p ^= (1UL << pin); }这里有两个细节。第一,方向寄存器和数据寄存器可能是分开的,也可能是位带访问,例程里用哪种就沿用哪种,别混着写。第二,翻转用异或而不是“读出来取反再写回”,后者在中断里操作同一端口时可能丢状态。如果编译器支持位带或原子置位复位寄存器,优先用硬件原子操作。
端口复用是另一个高频翻车点。同一个引脚可能同时挂着定时器输出、串口接收和普通 IO,例程只配了其中一种,你加功能时忘了关另一路,就会出现“输出电平正常但波形不对”。配置前先查引脚分配表,把该引脚涉及的功能寄存器全部过一遍,确认只有一路使能。
3.2 中断向量与优先级
中断配置分三步:在向量表里挂服务函数、在中断控制器里使能对应源、设置优先级并开全局中断。例程里通常用宏把服务函数名和向量号绑在一起,改的时候两边都要动。
/* 中断服务函数示意:清标志要在业务之后、返回之前 */ void timer_isr(void) { /* 1. 清中断标志,防止退出后立即重入 */ TIMER_INT_FLAG = 0U; /* 2. 业务尽量短,长逻辑丢到主循环处理 */ g_tick++; } static void timer_int_init(void) { /* 挂接服务函数到指定向量,名称和向量号以例程为准 */ INTC_VEC[TIMER_VEC_NO] = (uint32_t)timer_isr; /* 设置优先级,数值含义查中断控制器章节 */ INTC_PRIO[TIMER_VEC_NO] = 1U; /* 使能该中断源,最后开全局中断 */ INTC_ENABLE |= (1UL << TIMER_VEC_NO); __enable_irq(); }优先级不要全部设成同一个值。RH850/F1L 支持中断嵌套,同优先级不能互相打断,高优先级可以打断低优先级。串口接收这类对延迟敏感的源给高一点,低速采样给低一点。还有一条血泪经验:清标志的位置放在业务代码之前还是之后,不同外设要求不一样,手册写“写 1 清除”的,别用读改写方式去清,否则可能把同时置位的其他标志一起清掉。
3.3 调试观察手段
板子不跑的时候,别急着加打印。先确认三件事:栈指针落在链接脚本划定的范围内、向量表基址寄存器指向正确、系统时钟频率和预期一致。这三项用调试器看内存映射寄存器最快,比串口打印可靠,因为串口本身也依赖时钟。
我常用的一个笨办法:拿一个空闲 GPIO,在中断入口拉高、出口拉低,用示波器看脉宽和周期。脉宽反映中断服务耗时,周期反映触发频率,一眼就能判断是配置错了还是业务太重。没有示波器就用调试器的周期计数器,在同一个点读两次计数,差值换算成时间。
注意:断点会改变实时性。在中断里下断点,看到的时序和实际运行差别很大,测耗时用 IO 翻转或硬件计数器,别用单步。
4. 定时器与 ADC 联动:例程里最容易跑偏的参数组
单点 GPIO 和中断跑通,只能说明骨架没问题。真正体现 RH850/F1L 例程参考价值的是定时器触发 ADC 这类联动场景:一个外设的输出当另一个外设的输入,参数之间互相约束,错一个就整条链路失效。这一章把分频、重载、采样窗口和结果校验拆开讲。
4.1 定时器分频与重载
定时器的基本账是:计数时钟 = 外设时钟 / 预分频,溢出周期 = (重载值 + 1) / 计数时钟。例程里通常直接给一个重载值,你不核算就不知道它对应多少微秒。假设外设时钟 40 MHz、预分频 8、想要 1 ms 中断,重载值算下来是 5000 减 1。算完还要看计数器位宽够不够,16 位计数器装不下就得加大预分频。
/* 定时器配置示意:按目标周期反推重载值 */ #define TIMER_CLK_HZ 40000000UL #define TIMER_PRESCALER 8UL #define TIMER_TARGET_US 1000UL static void timer_config(void) { uint32_t cnt_clk = TIMER_CLK_HZ / TIMER_PRESCALER; uint32_t reload = (cnt_clk / 1000000UL) * TIMER_TARGET_US; /* 重载值减一,具体减不减看计数器是从 0 还是从 1 开始 */ TIMER_PRESCALE = TIMER_PRESCALER - 1UL; TIMER_RELOAD = reload - 1UL; /* 清计数、开中断、启动 */ TIMER_COUNT = 0UL; TIMER_INT_ENABLE = 1U; TIMER_START = 1U; }参数改动的连锁反应要心里有数:改预分频会影响所有共用该时钟的定时器通道,改重载值只影响本通道。例程里如果多个通道共用一个预分频器,你只改其中一个通道的分频,另一个通道的周期会跟着变,这种问题在联调时很难第一眼发现。稳妥做法是把分频配置集中到一个函数里,注释写清每个通道的目标周期。
4.2 ADC 触发与采样窗口
ADC 由定时器触发时,要配三样:触发源选择、转换通道、采样时间。采样时间太短,高阻抗信号源来不及把采样电容充满,读出来的值偏低且跳;采样时间太长,高速采样时吞吐不够。例程默认值通常是按低阻抗源给的,你接分压电阻或传感器,就得重新算。
/* ADC 配置示意:定时器触发 + 单通道连续转换 */ static void adc_config(void) { /* 1. 选择触发源为定时器比较事件,关闭软件触发 */ ADC_TRIG_SEL = ADC_TRIG_TIMER; /* 2. 使能目标通道,关闭不用的通道降低功耗 */ ADC_CH_ENABLE = (1UL << ADC_CH_0); /* 3. 采样时间按源阻抗调,先给保守值再往下压 */ ADC_SAMPLE_TIME = ADC_SAMPLE_16CLK; /* 4. 使能转换,等待第一次完成 */ ADC_START = 1U; while ((ADC_STATUS & ADC_DONE) == 0U) { /* 等待首轮转换完成 */ } }采样窗口和源阻抗的粗算关系是:采样电容、源电阻和采样开关电阻构成 RC,采样时间至少给到五倍时间常数。手头没有精确参数时,先把采样时间设大一档,看读数稳定后再逐档减小,找到开始跳变的临界点,往上留一倍余量。这个办法不严谨,但现场够用。
4.3 结果校验
ADC 读数不能直接信。参考电压是否准确、地线有没有压降、通道间有没有串扰,都会让原始值偏移。例程里往往直接打印原始码值,实际项目至少加两层校验:上下限判断和滑动平均。
/* 简单滑动平均 + 上下限校验,窗口不宜过大 */ #define ADC_WIN_SIZE 8U static uint16_t adc_filter(uint16_t raw) { static uint16_t win[ADC_WIN_SIZE]; static uint8_t idx; static uint32_t sum; uint16_t out; /* 去掉明显越界的野值,避免污染窗口 */ if (raw < ADC_VALID_MIN || raw > ADC_VALID_MAX) { return win[(idx + ADC_WIN_SIZE - 1U) % ADC_WIN_SIZE]; } sum -= win[idx]; win[idx] = raw; sum += raw; idx = (idx + 1U) % ADC_WIN_SIZE; out = (uint16_t)(sum / ADC_WIN_SIZE); return out; }窗口长度和采样率要匹配。采样快、窗口长,响应会钝;采样慢、窗口短,滤波没效果。我一般让窗口覆盖两到三个工频周期,具体按信号带宽定。上下限别拍脑袋写,用实际传感器在边界条件下的读数统计出来,再留 10% 余量。
5. 避坑与排查:六条例程移植现场记录
例程在官方板上跑通不代表在你的板上能跑。移植过程里踩过的坑,按“现象、原因、解决”记下来,下次遇到能少花半天。
5.1 启动与时钟类问题
第一条:下载后不运行,调试器也连不上。现象是复位后电流正常但无任何输出。原因多半是链接脚本里的栈顶地址指向了不存在的 RAM 区域,或者启动代码的向量表基址和实际烧录地址不一致。解决方法是打开 map 文件核对栈顶和向量表地址,再对照芯片手册的存储映射,确认没有落到保留区。
第二条:程序偶尔跑飞,重启后有时正常。现象是无规律死机,复位标志读出来是看门狗。原因是时钟切换时没等稳定标志就继续往下跑,或者看门狗初始化前就被使能。解决方法是把时钟稳定等待改成带超时的循环,失败时退回内部振荡;看门狗在启动早期先喂一次或暂时关闭,等系统时钟确定后再按需开启。
第三条:串口能发不能收,或收到乱码。现象是波特率看着对,实际波形偏差大。原因是外设时钟分频算错,或者引脚复用没切到串口功能。解决方法是先用示波器量一个字节的时间宽度反推实际波特率,再回查时钟树和分频寄存器;复用寄存器要确认发送、接收两路都使能。
5.2 外设与中断类问题
第四条:中断进一次就不再进。现象是第一次触发正常,之后挂死。原因是中断标志清除方式不对,读改写把同时置位的其他标志一起清掉,或者服务函数里没清对寄存器。解决方法是按手册要求用写 1 清除,别用“读出来与掉再写回”;清标志的位置放在业务之前,防止业务执行期间新事件被误清。
第五条:ADC 读数随通道切换跳变。现象是单通道稳定,多通道轮询时相邻通道互相影响。原因是采样时间不够,前一通道的电荷没放完。解决方法是加长采样时间,或在通道切换后丢弃第一次转换结果;高阻抗源前面加跟随器。
第六条:低功耗唤醒后外设不工作。现象是休眠电流正常,唤醒后串口和定时器失效。原因是唤醒后时钟源回到了默认内部振荡,外设分频没恢复。解决方法是在唤醒处理里重新走一遍时钟初始化,或者进入低功耗前记录当前配置,唤醒后逐项恢复并校验状态标志。
提示:每修完一个坑,把现象和根因补到工程目录的说明文件里。半年后换人接手,这份记录比任何文档都管用。
6. 从例程到量产代码:裁剪、验证与交付检查
例程包里的代码是按“演示清楚”写的,不是按“量产可靠”写的。直接拿来用,最典型的问题是冗余驱动全量编译、断言和打印没关、初始化顺序依赖隐式假设。裁剪的第一步不是删文件,而是画一张调用关系图:从 main 出发,标出每个外设初始化函数依赖哪些公共函数,哪些是示例专用、哪些是底层必需。画完再动手,能避免删了某个“看起来没用”的函数,结果链接报一堆未定义符号。
第二步是把配置集中。时钟、引脚复用、中断优先级这三类参数散落在各个驱动里,量产前要收拢到一个配置文件,用宏或常量表管理。下面这张检查表是我交付前必过的项,按顺序走一遍,能挡掉大部分低级问题。
| 检查项 | 通过标准 | 常见疏漏 |
|---|---|---|
| 链接脚本 | map 文件无段重叠,栈顶在 RAM 内 | 照抄例程,RAM 大小不匹配 |
| 启动代码 | 复位源可读,异常有兜底 | 异常向量指向死循环且无记录 |
| 时钟 | 起振失败有回退,频率可测 | 只配 PLL,不校验切换标志 |
| 中断 | 每个源都有清标志路径 | 嵌套优先级未按实时性分配 |
| 外设 | 未用通道关闭,引脚无浮空 | 复用寄存器残留使能 |
| 看门狗 | 喂狗点覆盖所有长路径 | 中断里喂狗掩盖主循环卡死 |
| 低功耗 | 唤醒后外设状态可恢复 | 只测电流,不测功能 |
第三步是验证。功能验证之外,至少做一轮异常注入:拔掉信号源看 ADC 是否越界保护、人为制造看门狗超时看复位后能否自恢复、把供电缓慢拉低看复位是否干净。这些场景例程不会覆盖,但量产后现场一定会遇到。
最后说个习惯。我早期移植 RH850/F1L 例程时,改完直接下载,出了问题再从现象往回倒推,经常一查就是一天。后来强制自己每次改动链接脚本或启动文件后,先编译看 map、再下载跑最小闭环、最后才加业务代码,三步缺一不可。那以后同类问题基本能在半小时内定位。希望帮到你。
本文还有配套的精品资源,点击获取