文章目录
-
- [0. 写在前面](#0. 写在前面)
- [1. 环境与目标](#1. 环境与目标)
- [2. 第一个坑:本机 FSP 根本不带外设驱动源码](#2. 第一个坑:本机 FSP 根本不带外设驱动源码)
- [3. 寄存器级 SCI9 驱动(115200 8N1)](#3. 寄存器级 SCI9 驱动(115200 8N1))
- [4. 拉取真正的开源 MicroShell(而不是手写)](#4. 拉取真正的开源 MicroShell(而不是手写))
-
- 拉取方式
- [裸机裁剪 `ush_config.h`](#裸机裁剪
ush_config.h) - [CMake 加 include 路径](#CMake 加 include 路径)
- [5. MicroShell 与 SCI9 对接](#5. MicroShell 与 SCI9 对接)
- [6. 第二个坑:提示符乱码 `▒▒AF▒ ▒▒`](#6. 第二个坑:提示符乱码
▒▒AF▒ ▒▒) - [7. 第三个坑:输入慢、丢字符(输入 help 只回显 h)](#7. 第三个坑:输入慢、丢字符(输入 help 只回显 h))
- [8. 编译与烧录](#8. 编译与烧录)
- [9. 最终目录结构](#9. 最终目录结构)
- [10. 总结](#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-llvm clang 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(节选,完整见文末目录)
c
#include "hal_data.h" /* R_SCI9, R_BSP_MODULE_START, FSP_IP_SCI */
#include <stdint.h>
#define SCI9_BRR 26U /* PCLKB=50MHz 下的 115200 波特率 */
void sci9_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(轮询,不开中断) */
}
void sci9_putc(char c)
{
while (0U == (R_SCI9->SSR & (1U<<7))) ; /* 等 TDRE */
R_SCI9->TDR = (uint8_t)c;
while (0U == (R_SCI9->SSR & (1U<<2))) ; /* 等 TEND,整帧发完 */
}
bool sci9_getc_poll(char *out)
{
if (R_SCI9->SSR & (1U<<6)) { /* SSR.RDRF 有数据 */
*out = (char)R_SCI9->RDR; /* 读 RDR 自动清 RDRF */
return true;
}
return false;
}
这个驱动最初就是上面的"裸"版本。后面为了解决"输入丢字符",又加了软件接收环形缓冲和 SysTick 节拍,见第 7 节。
4. 拉取真正的开源 MicroShell(而不是手写)
这里要特别诚实地说一个插曲:第一版我给的是一份"MicroShell 风格"的手写等价实现 ,并误称"无外网所以内置"。那其实不是开源组件 。后来用户指出后,我拉取了真实的 MicroShell 源码替换掉。下面记录的也是真实库的接法。
拉取方式
构建机直连 github.com 不通,但 codeload.github.com 可达,直接下载整包最可靠(比 WebFetch 拼凑准):
bash
curl -sL https://codeload.github.com/marcinbor85/microshell/tar.gz/refs/heads/main \
-o microshell.tar.gz
tar -xzf microshell.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 裁剪:
c
/* 禁用全部内置命令,改用本项目自注册命令 */
#define USH_CONFIG_ENABLE_COMMAND_CAT 0
#define USH_CONFIG_ENABLE_COMMAND_CD 0
#define USH_CONFIG_ENABLE_COMMAND_ECHO 0
#define USH_CONFIG_ENABLE_COMMAND_HELP 0
#define USH_CONFIG_ENABLE_COMMAND_LS 0
#define USH_CONFIG_ENABLE_COMMAND_PWD 0
#define USH_CONFIG_ENABLE_COMMAND_XXD 0
#define USH_CONFIG_ENABLE_FEATURE_COMMANDS 1
#define USH_CONFIG_ENABLE_FEATURE_AUTOCOMPLETE 1 /* Tab 补全 */
#define USH_CONFIG_ENABLE_FEATURE_SHELL_STYLES 1 /* VT100 样式 */
/* 裸机无 fprintf/exit,断言改为空操作 */
#define USH_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 重生成会覆盖它):
cmake
# 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 接口
c
static int shell_io_read_char(struct ush_object *self, char *ch)
{
(void)self;
return sci9_getc_poll(ch) ? 1 : 0; /* 非阻塞:有则 1,无则 0 */
}
static int shell_io_write_char(struct ush_object *self, char ch)
{
(void)self;
sci9_putc(ch);
return 1;
}
static const struct ush_io_interface g_shell_io = {
.read = shell_io_read_char,
.write = shell_io_write_char,
};
初始化描述符
c
static const struct ush_descriptor g_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() 等价:
c
static const struct ush_file_descriptor g_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:
c
static void cmd_led_exec(struct ush_object *self,
struct ush_file_descriptor const *file,
int argc, char *argv[])
{
(void)file;
if (argc < 3) { ush_print(self, "usage: led <0|1|2> <0|1>"); return; }
int idx = argv[1][0] - '0';
int on = (argv[2][0] == '1') ? 1 : 0;
bsp_io_port_pin_t pin;
if (idx == 0) pin = BSP_IO_PORT_00_PIN_02;
else if (idx == 1) pin = BSP_IO_PORT_04_PIN_04;
else if (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);
char msg[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 之后,补一个挂载空根节点:
c
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 段阻塞延时:
c
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 流水用真实毫秒计时,每轮都轮询串口):
c
static uint32_t led_t0 = 0;
static int led_step = 0;
while (1)
{
uint32_t now = 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 节拍(不触发中断,纯轮询,供计时):
c
#define ICLK_HZ 100000000UL /* ICLK = 100 MHz */
void board_tick_init(void)
{
SysTick->LOAD = (ICLK_HZ / 1000UL) - 1UL;
SysTick->VAL = 0UL;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
}
uint32_t board_millis(void)
{
if (SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk)
g_ms++;
return g_ms;
}
③ 软件接收环形缓冲(64 字节) :每轮主循环先把硬件 RDR 里所有已就绪字节搬进软件缓冲,sci9_getc_poll 改为从缓冲取字节。即使主循环偶发被占用,硬件 1 字节寄存器也不会被新字符冲掉。
c
#define RX_RING_SZ 64U
static uint8_t g_rx_ring[RX_RING_SZ];
static uint16_t g_rx_head = 0, g_rx_tail = 0;
void sci9_rx_drain(void)
{
while (R_SCI9->SSR & (1U<<6)) { /* RDRF 有数据 */
uint8_t b = (uint8_t)R_SCI9->RDR;
uint16_t next = (g_rx_head + 1U) % RX_RING_SZ;
g_rx_ring[g_rx_head] = b;
g_rx_head = next;
}
}
bool sci9_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;
return true;
}
return false;
}
主循环每轮先 sci9_rx_drain() 再 ush_service():
c
void uart_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 即可(只需首次):
bash
cmake -B build/Debug -S . \
-DRASC_EXE_PATH="C:/Renesas/RA/sc_v2026-07_fsp_v6.6.0/eclipse/rasc.exe"
之后直接:
bash
RASC_EXE_PATH="C:/Renesas/RA/sc_v2026-07_fsp_v6.6.0/eclipse/rasc.exe" \
ninja -C build/Debug
cmake 不在 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.hex
10. 总结
| 问题 | 根因 | 解法 |
|---|---|---|
编译报 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 模块停止控制章
