文章目录
- 0. 写在前面
- 1. 环境与目标
- 2. 第一个坑:本机 FSP 根本不带外设驱动源码
- 现象
- 根因
- 决策
- 3. 寄存器级 SCI9 驱动(115200 8N1)
- 关键参数
- 模块时钟
- `sci9_uart.c`(节选,完整见文末目录)
- 4. 拉取真正的开源 MicroShell(而不是手写)
- 拉取方式
- 裸机裁剪 `ush_config.h`
- CMake 加 include 路径
- 5. MicroShell 与 SCI9 对接
- I/O 接口
- 初始化描述符
- 注册命令
- 6. 第二个坑:提示符乱码 `▒▒AF▒ ▒▒`
- 现象
- 根因(读库源码确认)
- 修复(一行)
- 7. 第三个坑:输入慢、丢字符(输入 help 只回显 h)
- 现象
- 根因
- 修复三件套
- 8. 编译与烧录
- 编译(关键:RASC_EXE_PATH)
- 烧录
- 9. 最终目录结构
- 10. 总结
- 在这里插入图片描述
- 参考
关键词:Renesas RA4M2、FSP、RASC、寄存器级 UART、MicroShell、CH340、嵌入式命令行
平台:RA-Eco-RA4M2-100PIN-V2.0 开发板 / R7FA4M2AD3CFP(Cortex-M33)
0. 写在前面
调试嵌入式项目时,最顺手的往往不是仿真器,而是一个能随时敲命令的串口 shell:看点状态、点个灯、读个寄存器,比反复改代码烧录快得多。
本篇文章记录我把一块RA-Eco-RA4M2-100PIN-V2.0开发板的CH340 USB 串口(接芯片的 SCI9,P109/P110 引脚)接上一个真正的开源 shell 组件 —— MicroShell(marcinbor85/microshell,MIT License)的完整过程。
过程中踩了三个真实的坑,逐一解决后,最终效果:
[RA4M2 /]$ help Commands: help - show this help led <n> <0|1> - set LED n(0/1/2) on/off echo <text> - print text back info - board / shell info reset - reset shell [RA4M2 /]$ led 0 1 led 0 on [RA4M2 /]$ info RA-Eco-RA4M2-100PIN-V2.0 MCU: R7FA4M2AD3CFP (Cortex-M33) Shell: MicroShell (marcinbor85/microshell, MIT) UART: CH340 -> SCI9 (P109 TX / P110 RX), 115200 8N1下面按"环境 → 第一个坑 → 拉库 → 对接 → 第二/三个坑 → 编译烧录"的顺序展开。
1. 环境与目标
| 项目 | 内容 |
|---|---|
| 开发板 | RA-Eco-RA4M2-100PIN-V2.0 |
| MCU | R7FA4M2AD3CFP(Cortex-M33,200 MHz PLL,ICLK 100 MHz) |
| FSP 版本 | 6.6.0 |
| 工具链 | Renesas arm-llvmclang 22.1.0 |
| 构建器 | Ninja(~/.renesas/platform/ninja-build/ninja.exe) |
| 代码生成 | RASC(sc_v2026-07_fsp_v6.6.0/eclipse/rasc.exe) |
| 串口 | CH340(USB 口 A / 主机 COM3),MCU 侧 SCI9,P109=TXD9、P110=RXD9 |
| Shell | 开源 MicroShell(marcinbor85/microshell,MIT) |
目标:把 CH340 串口接一个 shell,支持回显、退格、Tab 补全,能注册自己的命令(点灯、看板子信息等)。
为什么选 MicroShell:它是纯 C、零依赖、面向裸机的轻量 shell,提供 Tab 补全、命令树、VT100 样式,且 API 简洁(一个
read/ 一个write回调 +ush_init/ush_service),非常契合资源受限的 MCU。
2. 第一个坑:本机 FSP 根本不带外设驱动源码
现象
最初我按常规思路,在 RASC(FSP 配置器)里给 SCI9 挂一个uart_on_sci_uart模块,生成hal_data.h,然后调R_SCI_UART_Open / Write / Read。
结果编译直接挂:
ra_gen/hal_data.h:7:10: fatal error: 'r_sci_uart.h' file not found不止一个文件报错,main.c、hal_entry.c、hal_data.c全挂。
根因
排查后发现一个关键事实:本机安装的 FSP 只包含了工具(rasc.exe、各种生成器),并不包含ra/fsp/src下各外设的驱动源码(如r_sci_uart.c / r_uart_api.h)。
也就是说:
- RASC 能生成引用
r_sci_uart.h的hal_data.h; - 但链接阶段找不到驱动实现(源码根本不在机器上);
- VSCode 的 RA 扩展也没有把 FSP 源码打包进来。
决策
放弃 FSP 的uart_on_sci_uart栈,改为直接操作 SCI9 寄存器自己写一个最小串口驱动。好处:
- 零外部依赖,只用到 FSP 的
bsp(寄存器定义R_SCI9、R_BSP_MODULE_START宏)和r_ioport; - 代码量很小,便于移植到任何 Renesas RA 系列;
- 编译 100% 可控,不依赖缺失的驱动文件。
顺手把
configuration.xml里的uart_on_sci_uart.0模块删掉(保留 P109/P110 → SCI9 的引脚复用配置,RASC 会重新生成进ra_gen/pin_data.c)。
3. 寄存器级 SCI9 驱动(115200 8N1)
关键参数
- SCI9 基地址
0x40118900,结构体R_SCI0_Type对所有通道共用,通过R_SCI9访问(定义在R7FA4M2AD.h)。 - PCLKB = 50 MHz(XTAL 24M → PLL(/3×25)=200M → PCLKB=/4)。
- 异步模式波特率:
波特率 = PCLKB / (16 × (BRR + 1))(SMR.CKS=0、SEMR.ABCS/ABCSE/BGDM=0)。 50e6 / (16 × 27) = 115740 Hz→BRR = 26,误差+0.47%(< 1%,合规)。
模块时钟
SCI9 对应MSTPCRB 第 22 位(FSP 宏R_BSP_MODULE_START(FSP_IP_SCI, 9)内部会自动处理 PRCR 写保护,清掉该位开启时钟)。
sci9_uart.c(节选,完整见文末目录)
#include"hal_data.h"/* R_SCI9, R_BSP_MODULE_START, FSP_IP_SCI */#include<stdint.h>#defineSCI9_BRR26U/* PCLKB=50MHz 下的 115200 波特率 */voidsci9_init(void){R_BSP_MODULE_START(FSP_IP_SCI,9);/* 开 SCI9 模块时钟 */R_SCI9->SCR=0x00U;/* 先禁止收发 */R_SCI9->SMR=0x00U;/* 异步 8N1:CKS=0, CHR=0(8位), PE=0, STOP=0 */R_SCI9->SCMR=0xF2U;/* 异步模式固定值 */R_SCI9->SEMR=0x00U;/* 波特率除数因子 = 16 */R_SCI9->BRR=SCI9_BRR;/* 波特率 */__NOP();/* 等待至少 1 个总线周期 */R_SCI9->SCR=(1U<<5)|(1U<<4);/* 使能发送 TE、接收 RE(轮询,不开中断) */}voidsci9_putc(charc){while(0U==(R_SCI9->SSR&(1U<<7)));/* 等 TDRE */R_SCI9->TDR=(uint8_t)c;while(0U==(R_SCI9->SSR&(1U<<2)));/* 等 TEND,整帧发完 */}boolsci9_getc_poll(char*out){if(R_SCI9->SSR&(1U<<6)){/* SSR.RDRF 有数据 */*out=(char)R_SCI9->RDR;/* 读 RDR 自动清 RDRF */returntrue;}returnfalse;}这个驱动最初就是上面的"裸"版本。后面为了解决"输入丢字符",又加了软件接收环形缓冲和 SysTick 节拍,见第 7 节。
4. 拉取真正的开源 MicroShell(而不是手写)
这里要特别诚实地说一个插曲:第一版我给的是一份"MicroShell 风格"的手写等价实现,并误称"无外网所以内置"。那其实不是开源组件。后来用户指出后,我拉取了真实的 MicroShell 源码替换掉。下面记录的也是真实库的接法。
拉取方式
构建机直连github.com不通,但codeload.github.com可达,直接下载整包最可靠(比 WebFetch 拼凑准):
curl-sLhttps://codeload.github.com/marcinbor85/microshell/tar.gz/refs/heads/main\-omicroshell.tar.gztar-xzfmicroshell.tar.gz把src/inc/*.h、src/src/*.c、src/src/commands/*.c、LICENSE直接vendor进工程的src/microshell/(逐字节真实源码,未改写库本身)。
裸机裁剪ush_config.h
MicroShell 默认带cat/cd/echo/help/ls/pwd/xxd等内置命令,且断言用到fprintf/exit。裸机环境没有这些,于是新增src/microshell/config/ush_config.h裁剪:
/* 禁用全部内置命令,改用本项目自注册命令 */#defineUSH_CONFIG_ENABLE_COMMAND_CAT0#defineUSH_CONFIG_ENABLE_COMMAND_CD0#defineUSH_CONFIG_ENABLE_COMMAND_ECHO0#defineUSH_CONFIG_ENABLE_COMMAND_HELP0#defineUSH_CONFIG_ENABLE_COMMAND_LS0#defineUSH_CONFIG_ENABLE_COMMAND_PWD0#defineUSH_CONFIG_ENABLE_COMMAND_XXD0#defineUSH_CONFIG_ENABLE_FEATURE_COMMANDS1#defineUSH_CONFIG_ENABLE_FEATURE_AUTOCOMPLETE1/* Tab 补全 */#defineUSH_CONFIG_ENABLE_FEATURE_SHELL_STYLES1/* VT100 样式 *//* 裸机无 fprintf/exit,断言改为空操作 */#defineUSH_ASSERT(cond)((void)(cond))CMake 加 include 路径
GeneratedSrc.cmake(RASC 自动生成)会用GLOB_RECURSE src/*.c自动收录.c,所以库文件无需手动登记。但需要把src/microshell和src/microshell/inc加入 include 搜索路径。
注意:这段要在用户自己的CMakeLists.txt里加,不要写进GeneratedSrc.cmake(RASC 重生成会覆盖它):
# CMakeLists.txt 末尾 target_include_directories(${PROJECT_NAME}.elf PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/microshell ${CMAKE_CURRENT_SOURCE_DIR}/src/microshell/inc )5. MicroShell 与 SCI9 对接
MicroShell 的 I/O 模型非常干净:用户提供一个ush_io_interface,里面有两个回调——read(有字符返回 1 并填*ch,否则返回 0)和write(发送 1 字节)。这正好对应我们驱动的sci9_getc_poll/sci9_putc。
I/O 接口
staticintshell_io_read_char(structush_object*self,char*ch){(void)self;returnsci9_getc_poll(ch)?1:0;/* 非阻塞:有则 1,无则 0 */}staticintshell_io_write_char(structush_object*self,charch){(void)self;sci9_putc(ch);return1;}staticconststructush_io_interfaceg_shell_io={.read=shell_io_read_char,.write=shell_io_write_char,};初始化描述符
staticconststructush_descriptorg_shell_desc={.io=&g_shell_io,.input_buffer=g_in_buf,.input_buffer_size=sizeof(g_in_buf),.output_buffer=g_out_buf,.output_buffer_size=sizeof(g_out_buf),.path_max_length=128,.hostname="RA4M2",.exec=NULL,};注册命令
命令是一条ush_file_descriptor数组,通过ush_commands_add()挂到全局命名空间,与官方 example 的commands_register()等价:
staticconststructush_file_descriptorg_my_cmds[]={{.name="help",.description="show help",.exec=cmd_help_exec},{.name="led",.description="toggle LED",.exec=cmd_led_exec},{.name="echo",.description="echo text",.exec=cmd_echo_exec},{.name="info",.description="board info",.exec=cmd_info_exec},{.name="reset",.description="reset shell",.exec=cmd_reset_exec},};命令回调函数里用 FSP 的R_IOPORT_PinWrite点灯、ush_print回显即可。例如led:
staticvoidcmd_led_exec(structush_object*self,structush_file_descriptorconst*file,intargc,char*argv[]){(void)file;if(argc<3){ush_print(self,"usage: led <0|1|2> <0|1>");return;}intidx=argv[1][0]-'0';inton=(argv[2][0]=='1')?1:0;bsp_io_port_pin_tpin;if(idx==0)pin=BSP_IO_PORT_00_PIN_02;elseif(idx==1)pin=BSP_IO_PORT_04_PIN_04;elseif(idx==2)pin=BSP_IO_PORT_04_PIN_05;else{ush_print(self,"led index must be 0/1/2");return;}R_IOPORT_PinWrite(&g_ioport_ctrl,pin,on?BSP_IO_LEVEL_HIGH:BSP_IO_LEVEL_LOW);charmsg[24];(void)snprintf(msg,sizeof(msg),"led %d %s",idx,on?"on":"off");ush_print(self,msg);}6. 第二个坑:提示符乱码▒▒AF▒ ▒▒
现象
串口终端上提示符变成了:
[RA4M2 ▒▒AF▒ ▒▒]$ uShell 0.1.0 [RA4M2 ▒▒AF▒ ▒▒]$横幅uShell 0.1.0是库正常打印的欢迎信息(不是乱码),但提示符中间那串▒▒AF▒ ▒▒是乱码。
根因(读库源码确认)
MicroShell 的提示符格式是[hostname path]$,其中path来自self->current_node->path(ush_prompt.c)。
调用链:
ush_init()内部先把self->root = NULL,再调ush_reset()打印横幅;ush_commands_add()只把命令节点挂到self->commands链表(供按名匹配命令),不会建立 root 树;- 真正设置
self->root、self->current_node并给节点path赋值的是ush_node_mount()(官方 example 里fs_mount→root_mount就是干这个的)。
我只调了ush_commands_add,没挂根节点 →root/current_node始终是 NULL → 提示符读current_node->path解引用 NULL,读到未初始化内存 → 乱码。
修复(一行)
在ush_init+ush_commands_add之后,补一个挂载空根节点:
ush_init(&g_ush,&g_shell_desc);ush_commands_add(&g_ush,&g_my_cmd_node,g_my_cmds,sizeof(g_my_cmds)/sizeof(g_my_cmds[0]));/* 关键:挂载根节点,建立合法的 root/current_node,消除提示符乱码 */ush_node_mount(&g_ush,"/",&g_shell_root,NULL,0);修复后提示符变成干净的[RA4M2 /]$。
命令解析不受影响:
ush_file_find_by_name先遍历self->commands(就是ush_commands_add注册的链表)按名匹配;挂载的根节点仅用于提示符路径显示。
7. 第三个坑:输入慢、丢字符(输入 help 只回显 h)
现象
连续输入help,只回显了h,后面的elp不见了;整体感觉输入很"慢"。
根因
最初hal_entry.c的主循环里有3 段阻塞延时:
R_IOPORT_PinWrite(...);R_BSP_SoftwareDelay(500,BSP_DELAY_UNITS_MILLISECONDS);R_IOPORT_PinWrite(...);R_BSP_SoftwareDelay(500,BSP_DELAY_UNITS_MILLISECONDS);R_IOPORT_PinWrite(...);R_BSP_SoftwareDelay(500,BSP_DELAY_UNITS_MILLISECONDS);uart_shell_poll();共1.5 秒阻塞,期间uart_shell_poll()完全不被调用。而 SCI9 接收寄存器只有1 字节,115200 波特下每字符约 87µs——阻塞期间新字符写入会把旧字符冲掉(overrun),所以help只来得及回显第 1 个字符。
修复三件套
① 主循环改成非阻塞(LED 流水用真实毫秒计时,每轮都轮询串口):
staticuint32_tled_t0=0;staticintled_step=0;while(1){uint32_tnow=board_millis();if((now-led_t0)>=500U){led_t0=now;/* 三灯轮流亮 */R_IOPORT_PinWrite(&g_ioport_ctrl,BSP_IO_PORT_00_PIN_02,led_step==0?BSP_IO_LEVEL_HIGH:BSP_IO_LEVEL_LOW);R_IOPORT_PinWrite(&g_ioport_ctrl,BSP_IO_PORT_04_PIN_04,led_step==1?BSP_IO_LEVEL_HIGH:BSP_IO_LEVEL_LOW);R_IOPORT_PinWrite(&g_ioport_ctrl,BSP_IO_PORT_04_PIN_05,led_step==2?BSP_IO_LEVEL_HIGH:BSP_IO_LEVEL_LOW);led_step=(led_step+1)%3;}uart_shell_poll();/* 每轮都处理串口,零延迟 */}② 基于 SysTick 的 1ms 节拍(不触发中断,纯轮询,供计时):
#defineICLK_HZ100000000UL/* ICLK = 100 MHz */voidboard_tick_init(void){SysTick->LOAD=(ICLK_HZ/1000UL)-1UL;SysTick->VAL=0UL;SysTick->CTRL=SysTick_CTRL_CLKSOURCE_Msk|SysTick_CTRL_ENABLE_Msk;}uint32_tboard_millis(void){if(SysTick->CTRL&SysTick_CTRL_COUNTFLAG_Msk)g_ms++;returng_ms;}③ 软件接收环形缓冲(64 字节):每轮主循环先把硬件RDR里所有已就绪字节搬进软件缓冲,sci9_getc_poll改为从缓冲取字节。即使主循环偶发被占用,硬件 1 字节寄存器也不会被新字符冲掉。
#defineRX_RING_SZ64Ustaticuint8_tg_rx_ring[RX_RING_SZ];staticuint16_tg_rx_head=0,g_rx_tail=0;voidsci9_rx_drain(void){while(R_SCI9->SSR&(1U<<6)){/* RDRF 有数据 */uint8_tb=(uint8_t)R_SCI9->RDR;uint16_tnext=(g_rx_head+1U)%RX_RING_SZ;g_rx_ring[g_rx_head]=b;g_rx_head=next;}}boolsci9_getc_poll(char*out){if(g_rx_head!=g_rx_tail){*out=(char)g_rx_ring[g_rx_tail];g_rx_tail=(g_rx_tail+1U)%RX_RING_SZ;returntrue;}returnfalse;}主循环每轮先sci9_rx_drain()再ush_service():
voiduart_shell_poll(void){sci9_rx_drain();/* 硬件字节 -> 软件环 */while(ush_service(&g_ush)){;}/* 驱动 MicroShell 状态机 */}关于硬件 FIFO:RA4M2 的 SCI9 其实自带硬件接收 FIFO(
FCR.FM+FRDRL),但 FIFO 模式下发送必须改写到FTDRL且TDRE变成TDFE,当前无法在板子上实测,改错会拖累整个 shell。软件环形缓冲已彻底解决丢字符问题,硬件 FIFO 留作后续可选优化。
8. 编译与烧录
编译(关键:RASC_EXE_PATH)
Ninja 在重生成build.ninja时会触发 CMake 重配置,而Config.cmake要求RASC_EXE_PATH变量,否则报错RASC_EXE_PATH variable is not set!。
把它缓存进CMakeCache.txt即可(只需首次):
cmake-Bbuild/Debug-S.\-DRASC_EXE_PATH="C:/Renesas/RA/sc_v2026-07_fsp_v6.6.0/eclipse/rasc.exe"之后直接:
RASC_EXE_PATH="C:/Renesas/RA/sc_v2026-07_fsp_v6.6.0/eclipse/rasc.exe"\ninja-Cbuild/Debugcmake 不在 PATH 也没关系,
build.ninja已记录它上次用的路径,ninja 会自动重跑 cmake。
编译产出build/Debug/RA4M2_Hello.hex(约 41 KB)。MicroShell 自身代码有几条-Wsign-conversion警告(非 fatal),二十九个库源文件全部编译链接通过。
烧录
- 用Renesas Flash Programmer(RFP)烧录
RA4M2_Hello.hex(RFP 会自动识别型号,无需手动选); - 串口终端以115200 8N1打开 CH340(主机 COM3),上电即可看到
[RA4M2 /]$提示符。
9. 最终目录结构
RA4M2_Hello/ ├── CMakeLists.txt # 末尾新增 MicroShell 的 include 路径 ├── configuration.xml # 已移除 uart_on_sci_uart 模块,保留 P109/P110->SCI9 引脚 ├── src/ │ ├── hal_entry.c # 非阻塞主循环 + 每轮 uart_shell_poll() │ ├── sci9_uart.c/.h # 寄存器级 SCI9 驱动 + 接收环 + SysTick 节拍 │ ├── uart_shell.c/.h # MicroShell I/O 接口 + 命令 + 初始化 │ └── microshell/ # 开源 MicroShell(vendor,MIT) │ ├── config/ush_config.h # 裸机裁剪配置 │ ├── inc/ # 库头文件 │ ├── src/ # 库实现(ush.c / ush_node_mount.c / ...) │ └── LICENSE └── build/Debug/RA4M2_Hello.hex10. 总结
| 问题 | 根因 | 解法 |
|---|---|---|
编译报r_sci_uart.h not found | 本机 FSP 不含外设驱动源码 | 放弃 FSP UART 栈,写寄存器级 SCI9 驱动 |
提示符乱码▒▒AF▒ ▒▒ | 只ush_commands_add没挂根节点,current_node为 NULL | 补ush_node_mount(&ush, "/", ...) |
| 输入慢/丢字符 | 主循环 1.5s 阻塞延时,1 字节硬件寄存器 overrun | 非阻塞主循环 + SysTick 节拍 + 64 字节软件接收环 |
三个坑都和"FSP 环境不完整 + 裸机 shell 的特殊时序"有关,解决思路可以复用到任何 RA 系列 + 串口 shell 的场景。寄存器级 SCI9 驱动是通用代码,后续要加日志输出、AT 指令解析等都可直接复用sci9_uart.c。
参考
- MicroShell 仓库:https://github.com/marcinbor85/microshell(MIT License)
- Renesas RA Flexible Software Package (FSP) 文档
- RA4M2 硬件手册:SCI(异步串行接口)章、MSTP 模块停止控制章