引言:FSP 生成的代码,为什么总被覆盖?
FSP 自动生成代码后,如何避免用户代码被覆盖?如果你使用过 Renesas FSP(Flexible Software Package,灵活配置软件包),大概率遇到过这样的场景:
- 为了快速验证功能,直接在 ra_gen 目录中加了几行代码;
- 为了修改 PWM 周期,手工编辑了 common_data.c;
- 为了调整 UART 回调,直接修改了生成实例的初始化结构;
- 当工程配置发生变化,需要再次执行 Generate Project Content 时;
- 工程还能编译,但之前写的代码不见了。
这不是 FSP 出错,而是工程边界没有划分清楚。
FSP 的本质是:根据图形化配置生成底层工程代码。 只要某个文件可以由 FSP 根据配置重新生成,就应该假设它可能被覆盖。把产品逻辑写入生成文件,重新生成后丢失代码是正常结果。
本文的核心目标只有一个:让 FSP 管配置和生成结果,让用户代码承载产品行为。工程应该可以随时重新生成,而不丢失产品功能。
一、先划清 FSP 的覆盖边界
不同 FSP 版本和工程模板生成的目录结构可能不同,例如 ra_gen、ra_cfg、fsp_cfg 等。通常情况下,由 FSP 根据配置自动生成或维护的文件,都不建议直接加入产品业务代码。

一句话判断:删掉该文件后,FSP 能按图形配置重新生成它,就不要把产品逻辑放在里面。
二、推荐工程目录与依赖方向
建议把工程划分为生成层和应用层。ra/、ra_gen/、ra_cfg/ 构成 FSP 生成层,服务于 Smart Configurator 配置、FSP 框架模块及自动生成的初始化代码和实例。src/app/ 为应用层,负责状态机、控制算法和产品流程;src/hal_entry.c 为应用程序入口,负责完成启动初始化并进入应用流程。
推荐的工程目录结构如下:

依赖方向应尽量单向:src/app/ 调用 FSP 生成的实例和接口,FSP 生成层不要反向包含产品业务模块。这样即使重新生成导致实例配置变化,影响也主要限制在 ra_gen/ 等生成层文件中。
三、用驱动封装替代直接修改生成文件
假设 FSP 生成 UART 实例 g_uart0。生成层负责定义实例,用户层负责定义产品需要的接口。不要在 r_sci_uart.c、common_data.c 或 hal_data.c 中插入收发状态机。
用户代码可以通过两种方式调用 UART:一种是通过 FSP 生成的实例接口,例如 g_uart0.p_api->write();另一种是在明确使用 SCI UART 驱动时,直接调用 R_SCI_UART_Write()。
在正式项目中,更推荐在用户层再封装 app_uart_write(),让业务代码依赖产品接口,而不是到处依赖具体实例名、控制块或底层驱动函数。示例代码如下:
#include "hal_data.h"
#include "app_uart.h"
fsp_err_t app_uart_open(void)
{
return g_uart0.p_api->open(g_uart0.p_ctrl, g_uart0.p_cfg);
}
fsp_err_t app_uart_write(uint8_t const * p_data, uint32_t length)
{
return g_uart0.p_api->write(g_uart0.p_ctrl, p_data, length);
}
或者:
#include "hal_data.h"
#include "app_uart.h"
fsp_err_t app_uart_open(void)
{
return R_SCI_UART_Open(&g_uart0_ctrl, &g_uart0_cfg);
}
fsp_err_t app_uart_write(uint8_t const * p_data, uint32_t length)
{
return R_SCI_UART_Write(&g_uart0_ctrl, p_data, length);
}
应用层调用 app_uart_write(),而不是到处访问 g_uart0.p_api 或 R_SCI_UART_Write()。这样做有四个好处:重新生成不会覆盖 app_uart.c;封装层可以统一增加超时和错误处理等;测试时可替换实现;切换通道或实例名称时修改范围可控。
同样模式适用于 ADC、GPT、IIC、SPI、CAN、USB、Flash 和 DMA。FSP 驱动是硬件能力提供者,用户封装才是产品接口的稳定边界。
四、hal_entry.c 只做入口编排
hal_entry.c 通常是用户代码入口,在 No RTOS 配置下,hal_entry() 可用于组织应用启动流程,适合放初始化顺序和主循环调度,但不适合作为所有功能的容器。状态机、通讯协议、控制算法、故障策略、数据处理、参数管理等应拆分到独立模块。
推荐结构如下:
void hal_entry(void)
{
app_board_init();
app_uart_init();
app_fault_init();
app_main_init();
while (1)
{
app_main_process();
}
}
在 RTOS 工程中,应用入口和初始化方式取决于工程模板及启动架构。工程可能生成 hal_entry.c,也可能使用其他入口文件或初始化函数。hal_entry.c(如果存在)通常不应作为传统的主循环容器;用户应根据实际启动流程,将初始化代码放在相应的用户入口中,将线程业务逻辑放在线程入口函数中。RTOS 的任务调度和线程间通信应通过线程、队列、事件标志组、信号量、互斥锁和定时器等机制实现。
五、回调函数是连接点,不是业务处理器
在 RA FSP 中,UART、SPI、IIC、ADC、GPT 等模块通常通过 callback 机制通知应用层,用户应避免在 callback 中执行复杂业务逻辑。
volatile bool g_uart_tx_done = false;
void uart0_callback(uart_callback_args_t * p_args)
{
if (UART_EVENT_TX_COMPLETE == p_args->event)
{
g_uart_tx_done = true;
}
}
回调中建议只执行设置标志位、写入环形缓冲区、复制少量固定长度数据、发送 RTOS 信号量或记录轻量诊断计数。回调中不建议执行 Flash 写入、复杂浮点计算、阻塞等待、长循环、协议全解析等全流程,因为这些操作会影响中断实时性和系统堆栈。
六、Pins、Clocks 和 Stacks 的变更流程
配置变更和产品业务修改混在一起,会让生成后的差异难以审查。每次修改 FSP 配置时,建议使用固定流程:
- 提交或暂存当前可工作代码,确保可以回退。
- 在 FSP 配置界面修改 Pins、Clocks、BSP 或 Stacks。
- 执行 Generate Project Content。
- 使用版本控制工具(如 Git)检查生成结果的差异。
- 编译工程,确认无 API、回调和资源冲突。
- 在目标板执行最小验证,例如串口收发、ADC 采样、PWM 波形或通信握手。
- 将配置文件、生成结果和必要的用户层适配修改作为同一逻辑变更提交。
尤其要注意中断优先级、DMA 通道、时钟频率和引脚复用。这些变化可能不导致编译失败,却会改变运行时行为。电机控制、工业通信和低功耗项目不应省略目标板验证。
七、必须修改生成代码时的处理方式
不建议直接修改生成代码,并不代表生成代码不可查看或不可调试。在定位问题、分析驱动行为或处理特殊 workaround 时,可以阅读甚至临时修改,但正式项目中应通过配置、用户封装或补丁机制管理修改。首先确认 FSP 的模块属性、BSP 设置、回调和扩展配置(FSP 为某些模块提供的高级、可选或依赖特定硬件功能的配置项)是否已有对应选项。若确实遇到 FSP 驱动缺陷、芯片勘误问题,优先在用户层封装规避。无法避免时,应将修改维护为独立补丁文件,明确对应 FSP 版本、修改原因、影响范围和重新生成后的应用方式。
临时修改生成代码但没有补丁、说明和重放步骤,本质上等于把风险留给下一次生成。
八、不要依赖用户代码块注释
FSP 没有一个类似其他一些生成器那样“官方统一、到处都能用”的 USER CODE BEGIN/END 机制,RA 工程推荐通过用户文件 + app 分层封装来避免代码被覆盖,而不是依赖保留块。
九、用版本控制把覆盖风险变成可审查差异
Git 不能阻止覆盖,但能让覆盖变成可见、可审查、可恢复的变化。每次生成前后至少执行:
git status
git diff
git diff -- ra_gen ra_cfg fsp_cfg
建议将 configuration.xml、FSP 配置、生成文件、用户代码、补丁文件、FSP 版本信息和构建脚本纳入可追溯管理。
FSP 升级应作为一次受控迁移处理,不要混在功能开发提交里。升级时应记录旧版和目标版本,重新生成并审查差异,处理 API 变化,验证启动、时钟、引脚、通信、ADC、定时器、DMA 和低功耗路径;对关键产品保留波形、通信日志和性能数据。
十、常见错误场景与正确替代方案

十一、团队提交前检查清单
- □ 用户业务逻辑没有写入 ra_gen、ra_cfg、fsp_cfg、common_data 或 hal_data。
- □ Pins、Clocks、Stacks、IRQ、DMA 和外设参数全部通过 FSP 配置修改。
- □ 回调函数位于用户文件,且回调内没有阻塞或长耗时工作。
- □ 新增外设已在用户层中提供稳定封装接口。
- □ hal_entry.c 只负责入口编排,没有堆叠协议解析、控制算法或长状态机。
- □ 生成前后已检查 git status 和 git diff。
- □ 编译通过,并完成与本次变更相关的目标板最小测试。
- □ 若修改过生成代码,已新增补丁、问题说明和重新应用步骤。
- □ FSP、IDE、编译器和关键工具版本已记录。
- □ FSP 升级与功能开发分开提交,升级结果有迁移说明和验证记录。
结束语:让重新生成成为日常动作
自动生成代码不是风险本身;真正的风险是配置、生成结果和产品逻辑混在一起。工程有清晰边界后,FSP 重新生成不再是不可控操作,而是可重复、可审查、可回退的工程动作。
最后记住一句话:配置改在 FSP,行为改在用户代码。 嵌入式工程管理的门道很多,在云栈社区有大量开发者分享过类似的工程化实践与踩坑记录,遇到问题时不妨去找找思路。