简介:面向汽车电子嵌入式开发者的RH850/F1L例程参考包,围绕瑞萨CS+集成开发环境整理了一套可直接编译调试的工程样例,适合已具备一定MCU基础、希望快速上手车身控制与车载网络开发的技术人员。压缩包共29个文件、约110KB,以ud与obj等工程编译中间文件、c与h源码头文件、asm启动及向量表、clnk链接脚本、map映射文件和mtpj工程配置为主,完整保留了CAN通信、时钟、主程序等模块的构建痕迹与目录组织。该资源已有5750人学习,说明其在RH850入门与工程复现中具备较高参考价值。读者可据此了解CS+工程的文件构成、编译链接选项与CAN驱动分层方式,对照iodefine寄存器定义和r_can、r_clock等源码梳理外设初始化流程,并借助调试配置与映射文件排查启动、中断向量及内存布局问题,从而缩短从环境搭建到功能验证的摸索周期,为后续车身控制与车载总线项目开发积累可复用的工程模板。
1. 从一份 RH850/F1L 例程包说起:先点灯还是先测时钟
接手一块别人画完、程序只跑过一半的车身控制板,最磨人的不是控制逻辑,而是上电之后连时钟都没起来。我拿到这颗 RH850/F1L 时,手上只有一个例程压缩包和两页引脚说明,里面每个外设都能单独编译,却没有一条从头到尾的上电顺序。把这份 RH850/F1L 例程按最小闭环重新排了一遍,才做到当天点灯、第二天串口出字、第三天 CAN 收到第一帧。
这份例程开发参考的价值不在于代码写得多漂亮,而在于它把启动、时钟、端口、定时器、串口、CAN、中断这些模块拆成了可以单独验证的小单元,每个单元都能独立编译、独立下载、独立观测。适合两类人:刚接手 F1L 平台、需要在短时间内把板子跑起来的工程师;以及手里已经有产品代码、但想把初始化顺序重新梳理一遍的老手。下面按我实际的调试顺序展开,先讲工程骨架,再逐个拆外设,最后落在几个真会让人翻车的地方。
2. 工程骨架与时钟:把压缩包改成能单步调试的工程
2.1 目录、编译器与链接脚本:三件先对齐的事
例程包解压之后不要急着编译,先看目录。常见的组织是 startup、driver、sample、linker、doc 五层,sample 里每个外设一个 main,driver 里放寄存器封装。我一般会先把 sample 拆掉,只保留一个空的 main 和一个 board 目录,把外设驱动留在 driver 里按需加,这样编译产物从一开始就是自己的工程,不用在别人的 main 里改来改去。
编译器这边有三个选项必须先对齐:内核类型、字节序、浮点 ABI。内核填错,链接能过,跑起来第一条跳转就飞;字节序和浮点 ABI 跟库文件不一致,表现是浮点运算结果莫名其妙。优化等级也要注意,例程里的软件延时函数在 -O0 下是准的,开到 -O2 可能被整段优化掉,我见过用 for 循环做 10ms 延时、结果上电直接把继电器抖成蜂鸣的现场。
# 先只编译不链接,确认每个源文件在当前选项下都能过 ccrh -c -cpu=g3k -O2 -Iinc src/clock.c src/gpio.c driver/tauj.c # 链接时显式指定链接脚本,入口符号和向量表段都在里面 ccrh -cpu=g3k -T linker/f1l_512k.ld -o build/demo.abs objs/*.obj上面两条命令里的-cpu参数必须和芯片内核匹配,选项拼写以编译器手册为准;-T指向的链接脚本决定了 ROM 起始、RAM 大小和向量表落点。逻辑很简单:编译阶段只验证语法和头文件路径,链接阶段才校验内存布局。参数上重点看三个地方,Flash 容量、RAM 容量、栈顶地址,任何一个和实际型号对不上,都会出现“下载成功但不运行”。
链接脚本里要盯住四个段:向量表段固定在 ROM 起始,.text 紧随其后,.data 的加载地址在 Flash、运行地址在 RAM,.bss 只占 RAM 不占 Flash。栈大小别照抄例程,中断嵌套深一点、加一个 printf 就可能把 1KB 的栈打穿,我通常先给 2KB,跑稳之后再往下压。
| 段 | 存放内容 | 常见错法 | 后果 |
|---|---|---|---|
| .vectors | 复位与中断向量 | 起始地址不是 ROM 基址 | 复位后取不到入口 |
| .text | 代码与常量 | 超出 Flash 容量 | 链接报错或运行错乱 |
| .data | 有初值的全局变量 | 只设运行地址不设加载地址 | 初值全是随机数 |
| .bss | 无初值的全局变量 | 忘记清零 | 全局状态从脏值开始 |
2.2 启动代码:复位后到 main 之间的四个动作
复位之后到 main 之前,芯片只做了最少的事:取栈指针、取复位入口。剩下全得自己写。顺序错了,后面的初始化再对也没用。我习惯把这段拆成四个动作:关全局中断、设栈指针、搬 .data、清 .bss,然后尽早关看门狗,最后才进 main。
/* crt0.c —— 复位到 main 之间的最小运行时 */ extern unsigned long __data_load, __data_start, __data_end; extern unsigned long __bss_start, __bss_end; extern int main(void); void _crt0(void) { unsigned long *src = &__data_load; unsigned long *dst = &__data_start; /* 1) 有初值的全局变量从 Flash 搬到 RAM,长度由链接脚本符号给出 */ while (dst < &__data_end) { *dst++ = *src++; } /* 2) 无初值的全局变量清零,否则 main 里的状态机从脏值开始 */ for (dst = &__bss_start; dst < &__bss_end; ) { *dst++ = 0UL; } /* 3) 看门狗必须在这里关,放到 clock_init 里已经晚了 */ wdt_disable_early(); (void)main(); /* 4) main 不允许返回,返回就复位,方便定位 */ for (;;) { ; } }这段代码的逻辑说明:三个符号不是变量,是链接脚本里定义的地址标签,__data_load是 Flash 中的加载地址,__data_start和__data_end是 RAM 中的起止地址。搬运循环用指针走,不要用 memcpy,因为 memcpy 本身可能依赖已经初始化好的数据。参数说明:栈顶地址不在这里设,放在汇编启动文件里用链接脚本符号直接赋给栈指针寄存器,比在 C 里改更稳。
看门狗那行是血泪经验。很多例程把看门狗关闭放在时钟初始化之后,理由是“等时钟稳定再操作外设”,但看门狗的超时时钟往往是独立时钟源,上电就开始计数,时钟初始化稍微久一点就复位,现象是调试器一连上就断、程序永远停在复位入口。
2.3 时钟初始化:PLL 的启动顺序比参数更关键
时钟这部分,参数抄错还能改,顺序写反就是玄学。正确顺序是:解锁写保护、停 PLL、设分频、设倍频、启动 PLL、等锁定、切主时钟源、确认切换完成、重新上锁。中间任何一步提前,都可能出现瞬时超频,芯片不一定立刻死,但会在某个外设上表现得不稳定。
/* clock_init.c —— 8MHz 外部晶振,PLL x10,ICLK 80MHz / PCLK 40MHz */ #define XTAL_HZ 8000000UL #define PLL_MUL 10U #define ICLK_DIV 1U #define PCLK_DIV 2U #define PLL_LOCK_TIMEOUT 100000UL int clock_init(void) { volatile unsigned long timeout; CLK_PROTECT_UNLOCK(); /* 1) 这类寄存器默认写保护 */ CLK->PLLCTL &= ~PLL_ENABLE; /* 2) 先停 PLL,再改分频 */ CLK->ICLK_DIV = ICLK_DIV - 1U; /* 3) 分频字段多为“值减一” */ CLK->PCLK_DIV = PCLK_DIV - 1U; CLK->PLLCTL = (CLK->PLLCTL & ~PLL_MUL_MASK) | ((PLL_MUL - 1U) << PLL_MUL_SHIFT); CLK->PLLCTL |= PLL_ENABLE; /* 4) 启动 PLL */ for (timeout = 0; timeout < PLL_LOCK_TIMEOUT; timeout++) { if (CLK->PLLSTAT & PLL_LOCKED) { break; /* 5) 等锁定,必须带超时 */ } } if (timeout >= PLL_LOCK_TIMEOUT) { CLK_PROTECT_LOCK(); return -1; /* 晶振不起振,交给上层处理 */ } CLK->MAIN_SEL = MAIN_SEL_PLL; /* 6) 切主时钟源 */ while ((CLK->MAIN_STAT & MAIN_STAT_MASK) != MAIN_STAT_PLL) { ; /* 7) 等状态位真正切换过去 */ } CLK_PROTECT_LOCK(); return 0; }逻辑说明:第 2 步先停 PLL 是为了避免改分频的瞬间,PLL 仍以旧倍频输出,出现一段超标时钟。第 5 步的超时不能省,外部晶振虚焊、负载电容不匹配时,锁定位永远不置起,没有超时就是死循环,调试器看起来像“程序卡在时钟初始化”,实际是硬件没起振。第 7 步读状态位而不是相信写入值,因为切换是异步的。
参数说明:PLL_MUL和两个分频系数决定最终频率,改之前先算一遍上限,ICLK 不能超过型号允许的最高主频,PCLK 通常不超过 ICLK 的一半。PLL_LOCK_TIMEOUT按循环指令周期估算,8MHz 晶振一般几十微秒就锁定,给到毫秒级足够。结构体CLK的基址和位域定义要按具体型号手册补,例程里的封装只能当模板,不能当权威。
2.4 最小可观测闭环:点灯加一行串口
时钟起来之后不要急着上 CAN,先做两件事:点一个灯、从串口打一行字,把当前主频一起打出来。这一步的意义是把“程序能不能跑”变成“跑得对不对”,后面所有外设出问题,都可以回到这里判断是时钟偏了还是外设配错了。
/* board_smoke_test.c —— 上电自检,先给一个肉眼可见的信号 */ void board_smoke_test(void) { gpio_init(); uart_init(115200U); led_on(LED_RUN); uart_puts("boot ok, iclk="); uart_put_u32(clock_get_iclk_hz()); uart_puts(" pclk="); uart_put_u32(clock_get_pclk_hz()); uart_puts("\r\n"); }逻辑说明:gpio_init和uart_init都依赖时钟已经配好,所以调用顺序放在clock_init成功返回之后。串口打印的频率值不是给人看的装饰,是给你核对分频有没有生效的凭据,写进去的 80MHz 和实际跑出来的可能不是一回事,后面串口乱码、定时器周期不对,第一嫌疑就是这里。参数说明:LED_RUN选一个不参与复用的普通 IO,115200只是初值,等串口验证通过再改成产品需要的波特率。
3. 外设例程拆解:GPIO、TAUJ、RLIN3 与 CAN 的落地顺序
3.1 端口三层配置:PMC、PFC、PIBC 的先后
RH850 的端口不是一个寄存器说了算,至少要过三层:功能选择、方向控制、输入缓冲。顺序上我固定为“先关输入缓冲、再选功能、最后定方向”,原因是切换过程中引脚可能处于悬空或两个驱动源对着拉的状态,输入缓冲开着就会灌电流,长时间跑会发热。例程里往往只写方向和电平,功能选择那一层被省略,接到复用脚上就不工作。
/* gpio.c —— 一个输出脚的三层配置,顺序不要调 */ void gpio_init(void) { /* 1) 先关输入缓冲,切换过程中不读这个脚 */ PORT->PIBC0 &= ~(1U << LED_RUN_PIN); /* 2) 功能选择:0=通用 IO,1=外设复用 */ PORT->PFC0 &= ~(1U << LED_RUN_PIN); /* 3) 方向:1=输出 */ PORT->PBDC0 |= (1U << LED_RUN_PIN); /* 4) 先写电平再开输出,避免上电瞬间闪一下 */ PORT->P0 |= (1U << LED_RUN_PIN); }逻辑说明:第 1 步和第 2 步的先后在多数场景下可以互换,但在引脚已经接了外部上拉的板子上,先关输入缓冲能避免切换瞬间的电流尖峰。第 4 步的顺序是给执行机构用的,驱动继电器或 MOS 管时,先写电平再开输出能避免一个几毫秒的误动作。参数说明:PIBC、PFC、PBDC的位定义要对着手册的端口寄存器表核,不同封装的引脚编号和端口号不是一一对应,例程里按端口号写,实际板子要按引脚名反查。
| 寄存器 | 作用 | 写错的表现 |
|---|---|---|
| PFC | 通用 IO 还是外设复用 | 复用脚当 IO 用,读回值不变 |
| PBDC | 输入还是输出 | 输出脚配置成输入,驱动不了负载 |
| PIBC | 输入缓冲开关 | 悬空脚耗流、模拟量串扰 |
| Pn | 输出电平 | 上电瞬间抖动 |
3.2 TAUJ:主从通道配对做 PWM
TAUJ 是通道阵列结构,做 PWM 时通常一个通道当主通道定周期,另一个当从通道定占空比。这里最容易翻车的是计数时钟和周期寄存器的位宽:PCLK 40MHz、目标 1kHz,周期值是 40000,16 位计数器放得下;但目标改成 100Hz,周期值 400000,直接溢出,表现是输出频率莫名其妙跳到另一个值。所以预分频不是可选项,是必须按目标频率算的。
/* tauj_pwm.c —— 1kHz PWM,占空比按千分比传入 */ int tauj_pwm_init(unsigned int duty_permille) { unsigned long pclk = clock_get_pclk_hz(); unsigned long period = pclk / 1000UL; /* 1kHz */ if (period > 0xFFFFUL) { return -1; /* 16 位放不下,先改预分频 */ } TAUJ0->TPS0 = 0U; /* 预分频 1,计数时钟=PCLK */ TAUJ0->CDR0 = (unsigned short)period; /* 主通道:周期 */ TAUJ0->CDR1 = (unsigned short)((period * duty_permille) / 1000UL); TAUJ0->CTL0 = TAUJ_CTL_MASTER | TAUJ_CTL_START; TAUJ0->CTL1 = TAUJ_CTL_SLAVE | TAUJ_CTL_START; TAUJ0->TOE0 = TAUJ_TOE_CH0 | TAUJ_TOE_CH1; /* 打开引脚输出 */ return 0; }逻辑说明:先算周期再判断位宽,超了就返回错误让上层改预分频,不要静默截断,截断之后现象很难查。主从通道的启动顺序是先主后从,反过来的话从通道可能错过第一个比较事件,输出会有一个周期的异常占空比。参数说明:duty_permille用千分比而不是百分比,是为了在小占空比时还有分辨率;TPS0的预分频档位按手册查表,不同档位对应的计数时钟不一样,算周期时要用分频后的频率。
采样和更新时机也要注意。有些型号支持缓冲寄存器,写入新占空比后在周期边界生效;不支持缓冲的,运行中直接改比较值可能出现一个窄脉冲。我的做法是能停就停、不能停就等到中断里改,别在主循环里随手写。
3.3 RLIN3:波特率误差要自己算
串口看着最简单,实际是外设里最容易“看起来通了但不可靠”的一个。RLIN3 配异步 UART,关键不是填个波特率宏,而是自己把分频值和误差算出来。PCLK 40MHz、115200 波特率、16 倍过采样,分频值是 21.7,取整 21,实际波特率 119047,误差约 3.3%,已经偏大;分频值取 22,实际 113636,误差 -1.4%,反而更稳。这种账不算,换一批板子就会出现有的能通有的不能通。
/* rlin3_uart.c —— 返回实际误差(ppm),超过阈值直接拒绝 */ int uart_init(unsigned int baud) { unsigned long pclk = clock_get_pclk_hz(); unsigned long div = pclk / (16UL * baud); /* 16 倍过采样 */ unsigned long real, err_ppm; if (div == 0UL || div > 255UL) { return -1000000; /* 分频超范围 */ } real = pclk / (16UL * div); err_ppm = (long)((real - baud) * 1000000L / baud); if (err_ppm > 20000L || err_ppm < -20000L) { return (int)err_ppm; /* 超过 2% 不接受 */ } RLIN3->BRG = (unsigned char)(div - 1U); RLIN3->CTL = RLIN3_CTL_UART | RLIN3_CTL_8N1; RLIN3->EN = 1U; return (int)err_ppm; }逻辑说明:先算分频再算实际波特率,最后用 ppm 表示误差,比百分比直观。2% 这个阈值是保守值,UART 本身能容忍到 3% 左右,但要留出晶振温漂和对端误差的余量。参数说明:div的位宽按手册核,示例按 8 位处理;RLIN3_CTL_8N1是封装出来的位组合,实际要拆成数据位、停止位、校验位三个字段分别写。接收侧建议一开始就上中断加环形缓冲,轮询接收在 115200 下丢字节是常态,不是偶发。
3.4 CAN:位时序、验收滤波和发送邮箱
CAN 初始化要过三关:位时序、验收滤波、邮箱使能。位时序决定能不能通,验收滤波决定收不收得到,邮箱使能决定发不发得出去。三关里最容易漏的是验收滤波,例程里经常全 0 或者全 1,全 0 表示只收 ID 为 0 的帧,现象是“能发不能收”,很多人第一反应是硬件收发器坏了,其实是滤波器把帧全挡了。
/* can_init.c —— 500kbps,PCLK 40MHz,采样点约 81% */ int can_init(void) { unsigned int tq_total = 16U; /* 一个位时间 16 个 tq */ unsigned int brp = clock_get_pclk_hz() / (500000UL * tq_total); if (brp == 0U || brp > 1024U) { return -1; } CAN->CTRL = 0U; /* 进初始化模式才能改时序 */ CAN->BRP = brp - 1U; CAN->TSEG1 = 12U; /* 同步段 + 传播段 + 相位段1 */ CAN->TSEG2 = 3U; /* 相位段2 */ CAN->SJW = 1U; can_set_accept_filter(0x100U, 0x700U); /* 只收 0x1xx 的标准帧 */ CAN->CTRL = CAN_CTRL_EXIT_INIT; while ((CAN->STAT & CAN_STAT_NORMAL) == 0U) { ; /* 等进入正常模式,超时要处理 */ } return 0; }逻辑说明:位时间由同步段、TSEG1、TSEG2 组成,三者之和乘以 BRP 就是一个位的实际时间。采样点算的是(1 + TSEG1) / 总 tq,这里 (1+12)/16 约 81%,工程上 75% 到 87.5% 都常见,总线长、节点多时往 80% 以上调。参数说明:brp在写寄存器时通常要减一;验收滤波用 ID 加掩码的方式表达,掩码位为 1 表示该位必须匹配。发送邮箱初始化之后要先置使能位再写数据,顺序反了第一帧会丢。
4. 中断与低功耗:两处最容易断链的初始化
4.1 INTC:向量号、优先级与 EI 级中断的登记
中断这块,新手最容易把向量号和中断源编号混为一谈。向量号是向量表里的下标,中断源编号是外设手册里给的那个数字,两者不一定相等,例程里通常用宏包了一层,改外设时如果直接改数字就会对不上。登记顺序我固定为:先写向量表基址、再填处理函数、再配优先级、最后开使能,任何一步提前开中断,都可能跑进一个空向量。
/* intc.c —— 登记一个中断源,顺序固定 */ void intc_register(unsigned int vec_no, isr_t handler, unsigned int prio) { /* 1) 向量表基址,必须在任何中断使能之前写 */ INTC->INTBP = (unsigned long)vector_table; /* 2) 向量表里填处理函数地址 */ vector_table[vec_no] = handler; /* 3) 优先级:0 表示屏蔽,1~15 有效,数值越大优先级越高 */ INTC->PRI[vec_no] = (unsigned char)(prio & 0x0FU); /* 4) 先开外设侧使能,最后开 CPU 全局中断 */ INTC->ENABLE |= (1UL << vec_no); ei(); }逻辑说明:第 1 步的向量表基址只写一次就够,放在每次登记里是为了防止有人中途改过。第 3 步的方向必须确认,RH850 这类内核的优先级是数值越大越高,和某些架构相反,照抄别的平台代码会把关键中断配成最低优先级。参数说明:prio传 0 不是“最低优先级”,是屏蔽,调试时中断进不去先看这里是不是 0。ei()放在函数末尾只适合初始化阶段,运行中动态登记要把这一步拆出去。
4.2 中断上下文:栈预算和环形缓冲
中断处理函数里做什么,决定了系统稳不稳。我的底线是:不在中断里调用 printf、不在中断里等标志位、不在中断里做超过 200 个周期的运算。原因很实在,printf 内部有锁和缓冲,重入一次就把栈和堆一起打穿;等标志位则可能把中断响应时间拉到毫秒级,别的中断全被拖住。
/* uart_irq.c —— 接收中断只做搬运,解析放到主循环 */ static volatile unsigned char rx_buf[256]; static volatile unsigned int rx_head, rx_tail; void uart_rx_isr(void) { unsigned char data = (unsigned char)RLIN3->RXD; unsigned int next = (rx_head + 1U) & 0xFFU; if (next != rx_tail) { /* 留一个空位区分满和空 */ rx_buf[rx_head] = data; rx_head = next; } else { rx_overflow++; /* 溢出计数,主循环里报错 */ } RLIN3->STAT = RLIN3_STAT_RX_CLR; }逻辑说明:环形缓冲用头尾指针,留一个空位是为了不用额外变量区分“满”和“空”。溢出时只累加计数不阻塞,保证中断能及时返回。参数说明:缓冲长度取 2 的幂,取模用位与代替除法;rx_head和rx_tail必须加 volatile,否则优化之后主循环读到的永远是旧值。主循环侧按帧解析,解析失败就丢,不要把半帧留在缓冲里等下一帧拼。
4.3 STOP 与 HALT:唤醒后的恢复顺序
低功耗模式本身不难进,难在醒来之后。STOP 模式下外设时钟停了,醒来第一件事不是继续跑主循环,而是把时钟和外设按依赖顺序恢复。顺序反了,串口会丢第一个字节,CAN 会错过第一帧,现象是“偶尔丢数据”,很难复现。
/* lowpower.c —— 进 STOP 之前保存,醒来按依赖恢复 */ void enter_stop_mode(void) { uart_flush(); /* 1) 等最后一个字节发完 */ clock_save_state(); /* 2) 记录当前分频和时钟源 */ port_hold_config(); /* 3) 锁存端口状态 */ irq_clear_all_pending(); /* 4) 清挂起标志,否则立刻被唤醒 */ enable_wakeup_source(WAKEUP_CAN_RX | WAKEUP_EXT_INT0); enter_stop(); /* 5) 执行后时钟停止 */ clock_restore_state(); /* 6) 先恢复时钟 */ uart_reinit(); /* 7) 再恢复依赖时钟的外设 */ can_reinit(); }逻辑说明:第 4 步最容易被忽略,进低功耗前如果还有挂起的中断标志,芯片会在进入后立刻被唤醒,看起来像“没睡下去”。第 6、7 步的顺序不能反,串口和 CAN 的初始化都要读时钟频率算分频,时钟没恢复就初始化,算出来的波特率是错的。参数说明:唤醒源按板子实际接线选,CAN 唤醒要求收发器支持唤醒功能,硬件上没接的话软件配了也没用。
注意:调试器连接状态下进 STOP,很多片上调试器会直接断连,看起来像程序跑飞。判断方法是看电流,或者用 GPIO 在进入前后翻转,不要只信调试器的状态。
5. 避坑排查:五个把例程跑成砖的现场
现象一:编译通过、下载成功,但程序反复复位,调试器停在入口。原因多半是看门狗在时钟初始化完成之前就超时。例程把关闭看门狗放在clock_init里,而看门狗用的是独立时钟,上电即计数。解决是把关闭动作提前到启动代码里,紧跟 .bss 清零之后,等系统稳定再决定是否重新使能。排查时可以先全片擦除再下载,排除旧程序里开了看门狗又被新程序覆盖一半的情况。
现象二:串口输出乱码,换几个波特率都一样。先别怀疑串口模块,去查实际 PCLK。分频写了但主时钟源没切过去、或者切换后没等状态位,都会让外设跑在一个和配置表不符的频率上。解决方法是在初始化末尾把clock_get_pclk_hz()的返回值从另一个脚输出或者用调试器读回来,和预期值对一遍。对不上就回到时钟初始化,重点看 PLL 锁定和切换确认两步。
现象三:CAN 能发不能收,示波器上看总线有波形。十有八九是验收滤波配错。全 0 的滤波器只放行 ID 为 0 的帧,全 1 的掩码等于全放行但有些实现里会被当成无效配置。解决是先配成完全放行,确认能收到,再逐步收紧掩码。另外确认终端电阻,单节点短线上不加电阻也能通,一接上第二个节点就开始丢帧,这种现场很常见。
现象四:定时器中断死活进不去,标志位却置起来了。三个检查点:向量号和中断源编号是否对应、优先级是不是 0、CPU 全局中断是否打开。前两个占九成。向量表基址写错的表现
本文还有配套的精品资源,点击获取