RA-Eco-RA4M2-100PIN-V2.0的USB 串口加上一个开源 Shell(MicroShell 移植实录)

文章目录

关键词: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)。

调用链:

  1. ush_init() 内部先把 self->root = NULL,再调 ush_reset() 打印横幅;
  2. ush_commands_add() 只 把命令节点挂到 self->commands 链表(供按名匹配命令),不会建立 root 树;
  3. 真正设置 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),二十九个库源文件全部编译链接通过。

烧录

  1. 用 Renesas Flash Programmer(RFP) 烧录 RA4M2_Hello.hex(RFP 会自动识别型号,无需手动选);
  2. 串口终端以 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 模块停止控制章
相关推荐
OpenCSG2 小时前
行业观察 | 在宜昌看具身智能:开源社区被摆到了台前
数据库·开源
m4Rk_2 小时前
【论文阅读】Agent 记忆机制(87):VizoMem——把文本历史转化为可检索的视觉记忆
论文阅读·人工智能·学习·开源·github
lpfasd1232 小时前
GitHub非AI开源方向调研报告
人工智能·开源·github
白山编程大哥3 小时前
适合初学者练习的 C/C++ 开源项目推荐
c语言·c++·开源
枫叶丹43 小时前
AI 进入日常工作后,任务应该怎样重新拆分
人工智能·chatgpt·开源·agent·codex
喵了几个咪4 小时前
RushWind Admin — 契约驱动:203 条路由零手写的工程化拆解
微服务·rust·开源·admin
斯内普吖4 小时前
(开源)水果蔬菜商城实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源
Rosanci5 小时前
从零到合入主线:我的 DeepSeek Harness 开源贡献实战手记
开发语言·前端·开源
miofly6 小时前
GitHub 今日推荐|archify:AI 代理自动生成可交互架构图的技能模块
开源·github