注意:cmake中的编译选项-march=rv32imac_zicsr这里和GD32EmbeddedBuilder中的-march=rv32imac不同,区别是前者启用了RV32 Z扩展指令。而GD32EmbeddedBuilder中的gcc编译器版本太老导致链接时会出现不支持Z扩展指令的问题,解决方案是下载最新版的riscv gcc编译器即可( 下载地址 )
Tips:相关工具链请自行百度安装哦
一、串口字符串打印
1.基本思路
GD32VW553官方固件库中包含了外设库、启动代码,初始化代码等等。我们若想使用rust则需要将这块移植过来,如果是重写这部分的话工作量又过大。所以这里其实是rust+c的混合开发。(为什么不直接用c:rust提供了零成本抽象且内置了多种功能函数,相较于c语言博主觉得在项目开发时用rust爽的不是一点点。另一方面是博主有一个rust写的os项目,准备移植到这块板子上😀)
先来看看启动汇编(Firmware/RISCV/env_Eclipse/start.S)
首先的一长串是中断向量表,它们定义了所有中断的处理函数,他们被标识为weak表示后定义的函数可以覆盖它们。直到_start这里才是代码真正开始执行的地方,
从链接脚本中也能知道开始执行的函数:
OUTPUT_ARCH( "riscv" )
ENTRY( _start )
MEMORY
{
/* Run in FLASH */
flash (rxai!w) : ORIGIN = 0x08000000, LENGTH = 4096k
ram (wxa!ri) : ORIGIN = 0x20000000, LENGTH = 288k
/* Run in RAM */
/* flash (rxai!w) : ORIGIN = 0x20000000, LENGTH = 32k
ram (wxa!ri) : ORIGIN = 0x20008000, LENGTH = 256K
*/
}
SECTIONS
{
.....
}
这里的Memory定义了芯片上的内存布局,SECTIONS则是定义编译输出的elf文件的布局,这里不深究链接脚本的内容,直接使用官方的即可(这里有部分段是为gcc准备的,如果是纯rust开发或许可以去掉他们)。
回到启动汇编:
_start:
/* Disable Global Interrupt */
csrc CSR_MSTATUS, MSTATUS_MIE
/* Initialize GP and Stack Pointer SP */
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, _sp
/* Set the the NMI base to share with mtvec by setting CSR_MMISC_CTL */
li t0, MMISC_CTL_NMI_CAUSE_FFF
csrs CSR_MMISC_CTL, t0
/* Intial the mtvt*/
la t0, vector_base
csrw CSR_MTVT, t0
/* Intial the mtvt2 and enable it*/
la t0, irq_entry
csrw CSR_MTVT2, t0
csrs CSR_MTVT2, 0x1
/* Intial the CSR MTVEC for the Trap and NMI base addr*/
la t0, exc_entry
csrw CSR_MTVEC, t0
/* Set the interrupt processing mode to ECLIC mode */
li t0, 0x3f
csrc CSR_MTVEC, t0
csrs CSR_MTVEC, 0x3
/* ===== Startup Stage 2 ===== */
#ifdef __riscv_flen
/* Enable FPU */
li t0, MSTATUS_FS
csrs mstatus, t0
csrw fcsr, x0
#endif
/* Enable mcycle and minstret counter */
csrci CSR_MCOUNTINHIBIT, 0x5
/* ===== Startup Stage 3 ===== */
/* Load data section */
la a0, _data_lma
la a1, _data
la a2, _edata
bgeu a1, a2, 2f
1:
lw t0, (a0)
sw t0, (a1)
addi a0, a0, 4
addi a1, a1, 4
bltu a1, a2, 1b
2:
/* Clear bss section */
la a0, __bss_start
la a1, _end
bgeu a0, a1, 2f
1:
sw zero, (a0)
addi a0, a0, 4
bltu a0, a1, 1b
2:
/*
* Call vendor defined SystemInit to
* initialize the micro-controller system
*/
call SystemInit
/* Call global constructors */
# la a0, __libc_fini_array
# call atexit
/* Call C/C++ constructor start up code */
# call __libc_init_array
/* do pre-init steps before main */
call _premain_init
/* ===== Call Main Function ===== */
/* argc = argv = 0 */
li a0, 0
li a1, 0
call main
/* do post-main steps after main */
call _postmain_fini
1:
j 1b
这里首先关闭中断确保初始化代码正确运行,然后就是设置中断向量表、为后续的c/rust代码设置堆栈,清空bss段等早期操作。
在清空bss后请注意:
/*
* Call vendor defined SystemInit to
* initialize the micro-controller system
*/
call SystemInit
这里首先跳转到SystemInit函数,通过定义可知这里是在初始化系统时钟等等
然后是:
/* Call global constructors */
# la a0, __libc_fini_array
# call atexit
/* Call C/C++ constructor start up code */
# call __libc_init_array
这里相当于执行了atexit(__libc_fini_array
)和__libc_init_array()这两个函数是为了使c/cpp代码正确运行而调用的,他们定义在c/cpp运行时库中,具体可以百度一下。这里我注释了它们是因为我们的代码不会涉及相关操作,所以也就没有必要执行,注释了还能减小生成文件的大小。
再往下走:
/* do pre-init steps before main */
call _premain_init
这里调用_premain_init函数,根据定义它进行了开启了ICache、初始化异常处理、设置中断分组、关闭定时器等操作。
/* ===== Call Main Function ===== */
/* argc = argv = 0 */
li a0, 0
li a1, 0
call main
/* do post-main steps after main */
call _postmain_fini
1:
j 1b
在这里我们看见了熟悉的main函数,没错这里就是c语言中那个熟悉的main函数,_postmain_fini函数是在main结束之后调用的,但main一般最后都是个死循环不会执行到这里。但如果main函数异常退出,那我们可以利用_postmain_fini函数来捕获main函数中的错误以便调试。再往后就是一个自己跳转到自己俗称无限循环的操作,也就是说cpu无论如何最后都是进入无限循环。
那么我们主要关注main,这个名字可以被更改,我们这里还是使用main,从这里进去就是执行我们编写的代码了。
2.正式操作
我们吧Firmware搬到我们的项目中,我将Firmware文件夹重命名为clib,并在该目录下编写一段cmake用来编译它们
CMakeLists.txt:设置要编译的c文件和包含目录,编译选项是从GD32EmbeddedBuilder中获取的。这里的-specs=nano.specs是启用精简标准库的意思,我们这里可以去掉
cmake_minimum_required(VERSION 3.10)
project(GD32_CLib C)
enable_language(ASM)
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/build/)
set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/lib)
add_compile_options(
-march=rv32imac_zicsr
-mabi=ilp32
-mcmodel=medlow
-msmall-data-limit=8
-mdiv
-O0
-fmessage-length=0
-fsigned-char
-ffunction-sections
-fdata-sections
-g3
-std=gnu11
-fno-builtin
# -specs=nano.specs
)
set(SOURCES
RISCV/env_Eclipse/entry.S
RISCV/env_Eclipse/start.S
RISCV/env_Eclipse/handlers.c
RISCV/env_Eclipse/init.c
gd32vw55x_standard_peripheral/system_gd32vw55x.c
gd32vw55x_standard_peripheral/Source/gd32vw55x_rcu.c
gd32vw55x_standard_peripheral/Source/gd32vw55x_eclic.c
gd32vw55x_standard_peripheral/Source/gd32vw55x_usart.c
gd32vw55x_standard_peripheral/Source/gd32vw55x_gpio.c
)
set(INCLUDE
RISCV/drivers
RISCV/env_Eclipse
gd32vw55x_standard_peripheral/Include
gd32vw55x_standard_peripheral
)
add_library(GD32_CLib ${SOURCES})
target_include_directories(GD32_CLib PRIVATE ${INCLUDE})
toolchain.cmake: 设置编译器及架构
# toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR riscv)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_C_COMPILER riscv-none-elf-gcc)
#![no_std]
#![no_main]
use core::panic::PanicInfo;
unsafe extern "C" {
fn uart_init();
fn put_char(ch: u8);
// fn gd32vw55x_firmware_version_get() -> u32;
}
fn uinit() {
unsafe {
uart_init();
};
}
fn uart_char(ch: u8) {
unsafe { put_char(ch) };
}
#[unsafe(no_mangle)]
extern "C" fn main() {
uinit();
let s = "Hello, RUST!".bytes();
for b in s {
uart_char(b);
}
loop {}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
按照要求编写裸机rust代码,不难看出这里是在初始化串口并发送了一个字符串“Hello, RUST!”
uart_init和put_char都是外部引入的c语言函数,博主在Firmware/RISCV/env_Eclipse/init.c中定义它们,本来是在一个单独的c文件中的,但是链接时找不到符号,索性就放在了这里。
.cargo/config.toml:它可以传递编译选项和设置编译目标,这样不用每次进行手动指定
如果没有安装这个target需要通过
rustup target add riscv32imac-unknown-none-elf
来安装
[build]
target = "riscv32imac-unknown-none-elf"
然后进行编译
c:在cmake文件目录下新建一个build文件夹进入然后执行
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE="D:/TOOLS/projects/rustProjects/gd32vw553/clib/toolchain.cmake" ..
ninja
在clib/build/lib下可以看到编译输出的库文件libGD32_CLib.a
rust:
cargo build
在target/riscv32imac-unknown-none-elf/debug下可以看到生成的库文件libgd32vw553.a
如果没有报错的话我们就来到了倒数第二步:链接
使用gd32vw55x.lds作为链接脚本,并执行命令:
riscv-none-embed-ld -T .\gd32vw55x.lds .\clib\build\lib\libGD32_CLib.a .\target\riscv32imac-unknown-none-elf\debug\libgd32vw553.a -o .\target\riscv32imac-unknown-none-elf\debug\output.elf
即可在target\riscv32imac-unknown-none-elf\debug\下看到output.elf文件
但是烧录的是纯二进制文件,所以我们还需要进行最后一步:提取二进制
riscv-none-embed-objcopy -O binary .\target\riscv32imac-unknown-none-elf\debug\output.elf .\target\riscv32imac-unknown-none-elf\debug\output.bin
将他烧录进板子即可看见串口输出字符串"Hello, RUST!"
二、systick定时器中断实验
好了你已经学会了使用Rust语言进行GD32VW553开发了,现在手写一个rtos吧。😀😀
哈哈以上只是验证了在这块开发板上进行Rust开发的可行性,现在我们来对以上代码进行一些封装整理并实现一个简单的systick定时器中断处理。
毕竟rtos也是根据systick定时1ms来调度的是吧,也算和rtos有一点点关系😀
build.cmd:
cd clib/build
cmake -G Ninja -DCMAKE_TOOLCHAIN_FILE="../toolchain.cmake" ..
ninja
cd ../..
cargo build
riscv-none-elf-ld -T .\gd32vw55x.lds .\clib\build\lib\libGD32_CLib.a .\target\riscv32imac-unknown-none-elf\debug\libgd32vw553.a -o .\target\riscv32imac-unknown-none-elf\debug\output.elf
riscv-none-elf-objcopy -O binary .\target\riscv32imac-unknown-none-elf\debug\output.elf .\target\riscv32imac-unknown-none-elf\debug\output.bin
在这里我们吧编译命令放在一个脚本里面这样只需要在项目目录下执行.\build.cmd即可完成编译,避免每次输入命令的繁琐步骤
clib.rs:
unsafe extern "C" {
pub fn systick_config();
pub fn uart_init();
pub fn put_char(ch: u8);
pub fn gd32vw55x_firmware_version_get() -> u32;
}
pub fn systick_enable() {
unsafe {
systick_config();
}
}
我们把C语言函数统一放在这个文件里面方便管理
console.rs:
use core::fmt::{self, Write};
pub fn uart_init() {
unsafe { super::clib::uart_init() };
}
fn console_putstr(s: &str) {
for c in s.bytes() {
unsafe { super::clib::put_char(c) };
}
}
struct Stdout;
impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
console_putstr(s);
Ok(())
}
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {
$crate::console::_print(format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\r\n"));
($fmt:expr) => ($crate::print!(concat!($fmt, "\r\n")));
($fmt:expr, $($arg:tt)*) => ($crate::print!(
concat!($fmt, "\r\n"), $($arg)*));
}
这样我们可以使用print和println进行格式化输出
lib.rs:
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
mod clib;
mod console;
use core::panic::PanicInfo;
#[unsafe(no_mangle)]
extern "C" fn main() {
console::uart_init();
clib::systick_enable();
println!("hello rust!");
// panic!("TEST");
loop {
unsafe {
// 低功耗循环
core::arch::asm!("wfi")
}
}
}
static mut COUNT: u32 = 0;
#[unsafe(no_mangle)]
fn eclic_mtip_handler() {
unsafe {
COUNT += 1;
if COUNT % 1000 == 0 {
println!("{}", COUNT);
if COUNT == 10000 {
panic!("Time out!")
}
}
}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
println!("{:?}", _info);
loop {
unsafe {
// 低功耗循环
core::arch::asm!("wfi")
}
}
}
这里我们使用全局变量COUNT来统计systick中断触发数,也就是毫秒数
在systick定时器中断处理函数eclic_mtip_handler中每过一秒打印一次COUNT值,10秒时触发panic
由于rust编译默认会优化函数名,所以我们这里需要在定义前加上#[unsafe(no_mangle)]来防止函数名被修改导致中断处理函数不生效
执行编译脚本,烧录后效果:
部分过程可以看看b站视频
纯折腾,目前看起来就像是c语言写好的东西给rust调用,rust完整移植的话体验会更好。博主的os移植完成之后直接使用os接口也可以避免使用c语言,前提是驱动全部写好哈哈😉

