RT-Thread移植到CH32V307VCT6:Ubuntu下RISC-V BSP开发全流程
把RT-Thread移植到CH32V307VCT6这颗国产RISC-V单片机上再顺手做一个能在Ubuntu下编译的BSP包这活儿听起来不大实际上涉及工具链、启动文件、时钟树、串口驱动、链接脚本和构建系统一堆事。我这次折腾完整套流程把中间踩过的坑和最终能跑通的方案整理出来给正打算在Linux环境下搞RISC-V开发的人一个可以直接参考的路线。这篇文章覆盖的内容包括为什么选这套方案、Ubuntu环境怎么搭、BSP目录结构和核心文件怎么设计、start.S和link.lds怎么改、时钟和串口怎么配、scons构建脚本怎么写以及我在移植过程中遇到的那些让人抓狂的奇怪问题。适合手里有CH32V307VCT6开发板、想在Linux下跑RT-Thread或者准备给新芯片手搓BSP的嵌入式开发者参考。1. 项目定位为什么要把RT-Thread搬上CH32V307VCT61.1 CH32V307VCT6这颗芯片到底有什么可折腾的CH32V307VCT6是国产MCU厂商推出的RISC-V架构微控制器100引脚LQFP封装最高主频144MHzFlash容量512KBSRAM 64KB。芯片上集成的东西相当丰富10M以太网MAC、USB2.0高速OTG、多路USART/SPI/I2C、12位ADC/DAC、DVP摄像头接口等。单从外设资源看它已经达到中高端ARM Cortex-M4级别MCU的水平但内核是RISC-V所以整个开发链路和传统的STM32不太一样。最关键的是这颗芯片虽然功能强但官方出厂自带的Demo工程和IDE环境大多绑定在Windows下。如果你主力开发机是Ubuntu想在命令行下用开源工具链写代码、用scons构建、用OpenOCD下载调试就得自己动手搭一套完全开源的开发流程。这就是这个项目的出发点让这颗RISC-V芯片在Linux生态里也能够丝滑开发。1.2 选RT-Thread而不是其他RTOS的理由很多人一提到国产MCU就跑FreeRTOS但我这次选RT-Thread有几点比较实在的考虑。RT-Thread孵化了完整的设备驱动框架串口、SPI、I2C、PIN、ETH都有标准设备接口写应用层代码的时候可以调用统一API不用被底层寄存器绑死。比如后面我要在BSP里挂一个LED点灯直接操作的是rt_pin_write这样的设备层接口而不是直接翻寄存器手册配置GPIO。RT-Thread有FinSH组件编译进内核后可以直接通过串口敲命令查看线程状态、内存使用、调用list_thread、list_device。在调试BSP阶段这个功能救命很多时候代码跑没跑起来、有没有进入线程调度一条help命令就能看出来。还有一点RT-Thread官方已经在bsp目录下维护了多个WCH系列芯片的板级支持包虽然不一定每个型号都覆盖到位但同厂同内核的参考代码很齐全。移植CH32V307VCT6的时候可以站在官方已支持的兄弟芯片肩膀上少走很多弯路。1.3 移植工作的边界和交付物这个项目最终要交出来的不是一坨不可维护的草稿代码而是一个标准RT-Thread BSP包具体包含bsp/ch32v307vct6目录里面有Kconfig、SConstruct、SConscript、board.c/h、link.lds、start.S这些标准文件在Ubuntu下能够一键scons编译出可直接烧录的elf和bin烧录后在串口能看到RT-Thread启动Logo和FinSH命令行LED点灯、新建线程、运行Shell这些基本功能验证通过。在动手之前先把整体逻辑理一遍对后面的每一步都能做到心里有数。2. Ubuntu下的准备阶段工具链、源码与烧录环境2.1 编译工具链的安装与版本选择CH32V307VCT6是RISC-V架构指令集是RV32IMAC加上自定义扩展指令支持硬件乘除法、原子操作、压缩指令还带了FPU浮点单元。编译工具链有两种选择一种是平头哥提供的GCC工具链另一种是RISC-V官方开源工具链。实测下来用riscv-none-embed-gcc这个版本最省心它专门针对嵌入式MCU场景裁剪过自带newlib精简C库链接脚本和启动文件适配都很成熟。Ubuntu下安装很简单可以通过软件源直接装也可以下载官方工具链压缩包解压使用。建议下载预编译的压缩包版本然后把工具链bin目录写进PATH环境变量这样scons调用CROSS_COMPILE的时候能直接找到riscv-none-embed-gcc。# 安装依赖工具 sudo apt update sudo apt install -y git build-essential python3 python3-pip scons sudo apt install -y gcc-riscv-none-embed # 软件源里如果有的话如果软件源里没找到这个包就去工具链官网下载Linux版本的tar.bz2压缩包解压到/opt目录然后配PATH。需要注意的是工具链版本尽量新一些旧版本对CH32V307的某些原子指令和浮点指令支持会有问题。环境验证只需要一条命令riscv-none-embed-gcc --version能正常打印出版本号说明工具链可用。2.2 拉取RT-Thread源码和参考BSPRT-Thread主仓库存放在Gitee和GitHub都有镜像我习惯用git clone的方式拉取完整主仓库。这样bsp目录下所有芯片的参考代码都能拿到后面找同系列参考BSP很方便。git clone https://github.com/RT-Thread/rt-thread.git主仓库拉下来之后到bsp目录里找WCH相关的板级支持包。重点关注有没有已支持的CH32V系列比如CH32V203、CH32V303这些。官方支持包的目录结构、启动文件、链接脚本写法是整个移植过程最重要的参考模板。如果某个兄弟型号已经能跑起来把它整个目录复制一份再改芯片相关的寄存器配置和外设时钟效率比从零手写高一个量级。2.3 烧录和调试环境搭建CH32V307支持SWD调试接口官方调试工具是WCH-Link同样也是基于RISC-V调试协议实现的。在Ubuntu下可以通过OpenOCD配合WCH-Link进行烧录和调试前提是OpenOCD版本要高于0.11并且编译时打开了WCH-Link驱动支持。如果没有WCH-Link串口ISP下载也是一个备选方案。芯片自带Bootloader通过串口把程序下载进Flash。Linux下用命令行方式控制串口下载需要用官方提供的下载工具或者直接用Python的pyserial写个小脚本发送hex文件。但串口下载速度慢调试也不方便我建议有条件还是整一个WCH-Link或者第三方SWD调试器。烧录相关配置放到后面实操环节再展开这里只需要确认硬件连接和工具链基本可用。2.4 从零搭建BSP目录骨架拿到RT-Thread主仓库后在bsp目录下新建ch32v307vct6文件夹。最稳妥的方式是找一款官方已经支持的同系列芯片BSP比如CH32V303系列整个目录复制过来然后在此基础上改。复制过来的目录里应该有board存放板级初始化代码、链接脚本、启动文件applications存放应用入口代码main.cdrivers存放串口、GPIO等设备驱动SConstruct顶层构建脚本scons入口Kconfig菜单配置项描述文件README.mdBSP说明文档接下来要做的就是把启动文件换成CH32V307VCT6对应的版本把链接脚本的内存布局改成512KB Flash和64KB RAM把board.c里的系统时钟初始化改成144MHz配置把串口驱动改成对应UART外设和引脚。整体工作量集中在board和drivers这两个目录里。3. BSP移植最核心的四个模块3.1 启动文件start.S从复位向量到C环境的钥匙start.S是芯片上电之后跑的第一段代码是连接固件与硬件的桥梁。CH32V307的复位流程大致是芯片上电后CPU从0x00000000地址取出第一条指令执行复位向量代码然后初始化栈指针、跳转到时钟初始化最后调用C语言的main函数进入RT-Thread调度。这一步涉及的关键点有几个。栈指针_sp的初始化必须放在最前面因为后续任何一次函数调用、任何一条压栈指令都会用到栈。实际上复位向量里有一条指令专门加载栈顶地址li sp, _stack_top。这个符号在链接脚本里用RAM起始地址加堆大小算出来。中断向量表的地址要写入mtvec寄存器。RISC-V内核的中断入口地址通过csrw mtvec, a0指令设置如果这一步没配好后面SysTick触发的时候CPU根本跳不过去中断服务函数系统直接卡死。.section .text .global _start _start: li sp, _stack_top la a0, _vector_table csrw mtvec, a0 la a0, SystemInit jalr a0 la a0, entry jalr a0 1: j 1b编完start.S之后第一时间检查反汇编文件确认向量表的第一条指令确实是跳转指令栈顶地址确实落在了RAM范围内。这一步做对了后面所有问题排查起来都能少一层迷雾。3.2 链接脚本link.lds内存布局不能拍脑袋链接脚本决定代码段、数据段、BSS段、堆栈分别放在Flash和RAM的什么位置。CH32V307VCT6的资源相对宽裕Flash 512KBRAM 64KB但也不能随便写要遵循几个原则。Flash的起始地址是0x00000000整个代码段放在这里。RAM的起始地址是0x20000000数据段、BSS段、堆和栈放在这里。向量表虽然也是代码但必须保证它从Flash起始地址开始因为CPU复位后固定从0x00000000取指。栈顶地址的设计也有讲究。栈是向下生长的初始化栈顶指针时应该指向RAM的最高地址也就是0x20000000 64KB。如果栈顶设置太低程序嵌套调用一深栈溢出就会悄悄踩掉BSS段的数据出现的症状非常诡异。MEMORY { FLASH (rx) : ORIGIN 0x00000000, LENGTH 512K RAM (rwx) : ORIGIN 0x20000000, LENGTH 64K } _estack ORIGIN(RAM) LENGTH(RAM);这里有个容易踩的坑如果RAM长度写错比如按32KB写程序编译时可能没问题但运行稍微复杂一点的应用就栈溢出、变量被覆盖、线程莫名其妙消失。定位这种问题非常费时间所以一开始就把内存尺寸核准后面省很多事。3.3 时钟初始化144MHz不是白给的CH32V307VCT6最高支持144MHz主频但上电复位后默认跑的内部8MHz高速时钟。要让CPU跑在144MHz需要在board.c里做PLL配置把内部高速时钟倍频上去。时钟树的配置逻辑大概是这样的内部高速振荡器HSI默认频率是8MHz通过锁相环PLL倍增到目标频率配置系统时钟源为PLL输出等待PLL锁定再切换系统时钟PLL计算有个基本公式系统时钟 输入时钟 × 倍频系数 / 分频系数。比如8MHz输入倍频系数18倍得到144MHz需要把分频系数设为1。void SystemClock_Config(void) { /* 配置PLL为144MHz */ RCC-CFGR (RCC-CFGR ~((uint32_t)RCC_PLL_MULL_MASK)) | RCC_PLL_MULL18; while ((RCC-CTLR RCC_CTLR_PLLRDY) 0); /* 等PLL锁定 */ RCC-CFGR (RCC-CFGR ~((uint32_t)RCC_SW_MASK)) | RCC_SW_PLL; while ((RCC-CFGR RCC_SW_MASK) ! RCC_SW_PLL); /* 等切换完成 */ }写这段配置的时候不能只抄寄存器数值要把数据手册里PLL锁定的等待时间考虑进去。如果PLL还没稳定就切系统时钟CPU可能跑在不可预期的频率上串口输出乱码、定时器时间不对这些奇怪问题都会出现。我在实际移植中遇到过一次串口波特率不对的情况排查半天最后发现是时钟频率配置出了问题PLL等待时间不够导致系统时钟偏低。所以时钟初始化之后建议先加一个翻转GPIO的测试点用示波器或者逻辑分析仪确认实际频率拉满再继续往下走。3.4 串口驱动串口通了整个系统就活了一半在嵌入式系统移植阶段串口是开发者唯一的眼睛和嘴巴。RT-Thread的串口驱动在BSP里担任的是控制台输出职责所有printk、日志、FinSH命令交互都依赖它。我移植的串口驱动基于UART1引脚映射关系要查芯片数据手册确认。串口驱动的核心逻辑分成两块。一块是初始化配置UART的波特率、数据位、停止位、引脚复用另一块是数据收发RT-Thread的rt_console_write接口最终会调用底层串口发送函数把要打印的字符一个字节一个字节从发送寄存器推出去。[ baud_rate_set \frac{clock}{16 \times (divider 1)} ]波特率配置本质上就是算分频系数。如果系统时钟是144MHz想要115200波特率分频值大概是78。算完之后还要考虑小数分频配置很多MCU的波特率寄存器都有小数位只算整数部分会导致实际波特率偏差串口打印乱码。static void uart1_init(void) { /* 开启UART1和GPIO时钟 */ RCC-APB2PCENR | RCC_APB2PERIPH_UART1; RCC-AHBPCENR | RCC_AHBPeriph_GPIOA; /* 引脚复用TX为PA9RX为PA10 */ GPIOA-CFGHR ~(0xF (4 * (10 - 8)) | 0xF (4 * (9 - 8))); /* 配置波特率 */ UART1-BRR calc_baud_div(144000000UL, 115200UL); /* 配置数据帧格式、使能发送和接收 */ UART1-CTLR1 (0x1 12) | (0x1 3) | (0x1 2); UART1-CTLR2 0; UART1-CTLR3 0; }关于串口引脚复用CH32V307内部有专门的映射配置寄存器这一步特别容易漏。GPIO的复用功能不是默认打开如果没配置对串口发送引脚根本不会输出信号程序跑得好好的但外部调试助手收不到任何数据。这种问题用万用表量TX引脚电压就能判断正常待机时TX引脚应该是高电平。4. 构建脚本和配置系统的适配4.1 scons构建框架是怎么工作的RT-Thread的构建系统基于scons本质上是一套Python脚本驱动的构建环境。顶层SConstruct负责找RT-Thread内核源码路径往编译环境里注入全局参数。每个BSP目录下的SConscript描述这个BSP要编译哪些源文件、包含哪些头文件目录。对于刚从ARM平台转过来搞RISC-V的人需要重点关注SConstruct里的交叉编译前缀。RT-Thread需要通过CROSS_COMPILE环境变量知道工具链名称。# 在SConstruct或者rtconfig.py中设置 CROSS_COMPILE riscv-none-embed-设置好这个变量后scons就会自动拼出riscv-none-embed-gcc、riscv-none-embed-objcopy这样的完整命令。如果工具链路径不在PATH里还需要把工具链bin目录也加进环境变量。4.2 rtconfig.h裁剪和定制RT-Thread功能rtconfig.h是RT-Thread内核编译时的总开关几乎所有功能模块都由宏控制。这个文件可以由menuconfig自动生成也可以手动编辑。在BSP移植阶段我习惯先手动配置一套最小可用配置跑通之后再慢慢打开需要的外设和组件。以下几个宏是必须确认的RT_THREAD_PRIORITY_MAX最大优先级数设32够了RT_TICK_PER_SECOND系统时钟节拍设1000也就是每个tick 1msRT_USING_MAILBOX、RT_USING_MESSAGEQUEUEIPC机制一般建议开启RT_USING_CONSOLE和RT_CONSOLE_DEVICE_NAME控制台设备名指向串口驱动注册的设备RT_USING_COMPONENTS_INIT自动初始化机制这个必须开BSP的板级初始化依赖它还有一个容易忽略的宏是RT_USING_HEAP堆内存的使能开关。线程栈、信号量、消息队列等内核对象都从堆里分配如果堆没开运行到动态创建对象的地方直接返回空指针。4.3 menuconfig和Kconfig的配合Kconfig是描述配置选项的文本文件menuconfig则是图形化配置工具。RT-Thread把Kconfig和scons集成得很好在BSP目录下运行scons --menuconfig会弹出类似Linux内核配置界面的交互菜单。Kconfig里要描述的内容包括这个BSP支持哪些片上外设驱动默认使能哪些组件内存优化等级之类的选项。驱动注册的开关一般是这样menu Onboard Peripheral Drivers config BSP_USING_UART1 bool Enable UART1 default y endmenu配置项被打开后scons依赖检查机制会自动把对应的驱动源文件编译进去。这样用户拿到BSP之后只需要在menuconfig里打勾或者去掉勾就能灵活裁剪功能不需要手改构建脚本这也是RT-Thread BSP比较规范的部分。5. 实际编译、烧录与运行验证过程5.1 首次编译命令和常见编译告警进入bsp/ch32v307vct6目录后执行scons编译cd rt-thread/bsp/ch32v307vct6 scons -j8第一次编译通常会等一段时间因为要生成链接脚本、编译内核源码、编译BSP驱动。如果环境变量和依赖都正确最后会得到rtthread.elf和rtthread.bin两个文件。如果编译报错大多数情况集中在两类。一类是找不到头文件多半是SConscript里include路径没写全或者头文件路径有大小写不一致。另一类是链接错误提示某个符号没定义这种情况先看符号名如果是系统调用或者libc函数就要确认工具链的newlib库是否链接进来了。5.2 烧录命令直接用OpenOCD打FlashCH32V307的烧录方式我实测下来用WCH-Link配合OpenOCD最方便。OpenOCD配置文件需要指定调试接口和芯片目标openocd -f wch-link.cfg -f target/ch32v307.cfg -c program rtthread.elf verify reset exit如果OpenOCD版本比较老不支持ch32v307的目标配置文件可以先用厂商提供的旧版OpenOCD或者把目标芯片配置改成同系列的RISC-V配置并调整寄存器参数。烧录完成之后如果一切正常串口调试助手立刻能看到RT-Thread的启动Logo。5.3 验证最小系统LED、线程和FinSH烧录完成不代表移植成功还要做几个基本功能验证。第一项是LED点灯。把开发板上一个GPIO配置成推挽输出在main函数里创建一个LED线程每隔500ms翻转一次电平。观察LED是否正常闪烁闪烁频率是否稳定。如果LED亮灭不稳定大概率是SysTick时钟节拍配置有问题。第二项是FinSH Shell。在PC端打开串口工具波特率设成115200连接开发板如果能敲入list_thread命令看到当前线程列表说明RT-Thread内核调度器已经正常工作串口驱动、控制台设备、线程创建流程都通了。第三项是创建一个简单的应用线程在循环里周期打印运行计数验证线程间调度和堆内存分配是否正常。int main(void) { rt_thread_t thread rt_thread_create(led_thread, led_thread_entry, RT_NULL, 512, 25, 10); if (thread ! RT_NULL) { rt_thread_startup(thread); } return 0; }如果这三项都通过BSP的最小系统就已经跑稳了后续再做以太网、USB、文件系统这些高级功能就有底子了。6. 移植过程中遇到的五个典型问题6.1 程序烧进去之后完全没有输出这种情况很常见。第一步先确认供电和复位引脚正常第二步量串口TX引脚电压正常待机时应该是高电平。如果TX引脚是低电平说明串口引脚复用没配好或者GPIO时钟没打开。还有一种可能是程序在start.S阶段就跑了但串口驱动初始化之前就卡死连系统时钟都没配成功。这个时候只能在start.S里加一个GPIO翻转的调试点逐步缩小问题范围看看代码跑到哪里停住了。我在移植初期遇到过系统时钟切换等待条件不满足导致死循环就是这个调试方法定位出来的。6.2 串口有输出但是乱码乱码基本可以锁定是时钟频率或者波特率配置不一致。先用示波器测量TX引脚的波形测出实际波特率如果和预期值偏差过大回溯检查PLL配置是否真的达到了144MHz。还要注意CH32V307的外设时钟树UART1挂载在APB2总线上APB2分频是否设置成1会影响最终的波特率精度。6.3 编译报错找不到riscv-none-embed-gcc这属于环境变量没配置好。打开终端临时设置一下PATHexport PATH$PATH:/opt/riscv-none-embed-gcc/bin如果想永久生效就把这行写入~/.bashrc然后重新打开终端。还有一种可能是scons没有拿到CROSS_COMPILE参数直接在编译命令前加参数scons -j8 CROSS_COMPILEriscv-none-embed-6.4 SysTick中断不触发线程调度卡死SysTick不受RT-Thread控制它是芯片提供的定时器外设RT-Thread只在board.c里把它作为RT_TICK的时钟源初始化。如果线程好像完全没有在切换先检查link.lds里向量表地址和start.S的mtvec设置是否一致。RISC-V的中断处理有时候需要显式设置中断代理寄存器把定时器中断从机器模式委托给S模式或者直接在机器模式里开全局中断。不同的工具链和RT-Thread配置组合要求不一样这块要仔细看参考BSP里怎么写的。6.5 线程栈分配过小导致莫名其妙死机RT-Thread的线程栈大小以字节为单位太小的话运行到函数嵌套调用比较深、或者使用了printf这类消耗栈空间的函数就会溢出。栈溢出不会立刻报错可能导致相邻内存被踩、标识位被改写、线程调度乱套。我自己最少给每个线程分配512字节栈带浮点运算的分配到1KB以上。同时开启RT-Thread的栈检查功能运行一段时间后查看线程栈使用率按实际需求调整。移植过程里我养成了一个习惯每改完一个模块就重新编译烧录跑一遍最小功能验证确认没引入新问题再继续下一步。这种逐步推进的方式虽然看起来慢但在交叉编译、没有仿真器直接看波形的情况下反而是排查问题最快的方式。如果你打算在自己板子上移植CH32V307或者其他RISC-V芯片的BSP建议也按这个节奏来千万别一口气改完全部代码再验证否则真出了问题都不知道该从哪里查起。整套流程跑通之后继续添加以太网适配、持久化存储、调试诊断等功能点就有了稳定的地基后续做起来会顺手很多。