Toggle navigation
首页
问答
文章
积分商城
专家
专区
更多专区...
文档中心
返回主站
搜索
提问
会员
中心
登录
注册
RT-Thread
rust
先楫HPM_RISCV
【24嵌入式设计大赛】居家PM2.5环境展示和监控 -- 探索 Rust + RT-Thread 混合编程开发
发布于 2024-09-18 23:25:18 浏览:666
订阅该版
[tocm] RT-Thread作为一个广泛应用的实时操作系统(RTOS),凭借其模块化设计和强大的生态系统,成为众多嵌入式项目的首选。传统上RT-Thread主要基于C语言进行开发,近年来,Rust语言因其内存安全性、并发性能以及现代化的开发体验,逐渐在嵌入式领域崭露头角。本文将探讨如何将Rust与C语言在RT-Thread框架中进行混合使用,充分发挥两者的优势,实现更高效、安全的嵌入式系统开发。 ## 前人的工作
提供了接近可用的 RT-Thread Rust 封装, 该项目4年前更新, 目前维护状态未知, 有若干 TODO 未完成.
提供了一个 RT-thread 上的 Rust 支持层, 完成度较高, 通过过程宏的方式, 注册 Rust RT-Thread 应用. Papalymo 的
提供了简单的 print "Hello World" 式例子. aozima 的
和
提供了在 RT-Thread 下, C 和 Rust 函数相互调用的例子. ## 前置准备 硬件 - HPM5300EVK (HPM5361 chip) - PMS7003 PM2.5 采集模块 - SSD1306 OLED 显示屏 杜邦线若干, 考虑到 HPM5300EVK GPIO 丝印使用不便, 做了一个 pin to pin 扩展板. 软件技术栈 - RT-Thread (master branch) - Rust (nightly) - hpm-hal:
- 若干 Rust 嵌入式库 ![Screenshot 2024-09-19 at 4.30.22 PM.png](https://oss-club.rt-thread.org/uploads/20240919/61388c9c5e321d39fe4c5049497df1a3.png.webp) 对于 Rust 语言, 和 "Why Rust" 一类的话题, 本文不做讨论, 请自行搜索. 这里主要谈一下目前 RT-Thread 的组织方式和 Rust 兼容时的问题挑战. RT-Thread 的组织方式是基于 C 语言的, 在 RT-Thread 上使用 Rust 需要解决的主要问题是如何在 C 和 Rust 之间进行有效的交互. 最基础的是让编译器通过编译, 成功输出二进制 ELF 文件. Rust 底层 使用 clang/llvm 编译, 所以不可避免会遇到 GCC 和 LLVM 的兼容问题. 最好的方案是通过 clang 编译 RT-Thread, 但大概率需要改写 RT-Thread 代码, 以通过 clang 编译. 其次是使用较高版本的 GCC 编译, 争取兼容性. 这里我使用 macOS 环境, 俗话说的好, 想不开的人才用 macOS 开发嵌入式. 相信 macOS 能跑通, 其他平台一定没问题. 使用 Homebrew 安装 GCC RISC-V 工具链, 目前版本是 13.2.0: ```bash brew tap riscv-software-src/riscv brew install riscv-gnu-toolchain ``` Rust 的安装直接使用 rustup, 并安装 riscv32 target. ```bash rustup target add riscv32imafc-unknown-none-elf ``` ### 修改项目增加 Rust 支持 修改 `rtconfig.py` 脚本, 替换 GCC 工具链路径. 其中比较核心的是 `ARCH_ABI = ' -march=rv32imafc_zifencei -mabi=ilp32f '` 这一行修改, 需要匹配对应的 Rust 目标. RT-Thread 使用 SCons 编译工具, 好处是简单和高度定制化, 对接其他第三方编译链时需要新增对应的编译脚本. 按照
提供的方式增加子项目. 在空 HPM5300EVK 项目下创建一个 rsapp Rust 项目. 增加 `SConscript` 文件, 内容如下: ```python Import("RTT_ROOT") Import("rtconfig") from building import * # change it if you want to use a different chip llvm_target = "riscv32imafc-unknown-none-elf" cargo = Builder( action=[ "cargo build --release --manifest-path ${SOURCE.abspath} --target ${LLVM_TARGET} --target-dir ${TARGET.dir.abspath}", Copy( "${TARGET.abspath}", "${TARGET.dir.abspath}/${LLVM_TARGET}/release/${TARGET.file}", ), ], suffix=".a", src_suffix=".toml", prefix="lib", chdir=1, ) Env.Append(BUILDERS={"Cargo": cargo}) Env.AppendUnique(LLVM_TARGET=llvm_target) cwd = GetCurrentDir() src = Glob("*.c") CPPPATH = [ cwd, ] # 'rsapp' is ".a" file name # rsapp is the name of the crate rttrust = Env.Cargo("rsapp", "Cargo.toml") Env.AlwaysBuild(rttrust) group = DefineGroup( "Rust", src, depend=[""], LIBS=[rttrust], CPPPATH=CPPPATH, # LINKFLAGS = ' -z muldefs' ) Return("group") ``` 其中核心的内容是 Rust 编译命令, 并配置输出的 `.a` LIBS. ### 配置 Rust 中使用 RT-Thread 的 C 库 为了在 Rust 中使用 RT-Thread 的 C 库,我们需要使用 bindgen 工具来生成 Rust 绑定. bindgen 是一个自动生成 Rust FFI 绑定的工具,可以帮助我们将 C 库接口转换为 Rust 代码。使用 bindgen 可以大大简化 Rust 与 C 代码之间的交互过程。 它可以加入到编译过程中, 也可以提前生成好直接使用 .rs 文件. 这里简化处理, 直接生成到项目里的 `src/ffi.rs` 文件. ```bash bindgen --use-core --merge-extern-blocks include/rtthread.h --no-prepend-enum-name --no-layout-tests -- -I ./bsp/hpmicro/hpm5300evk -I ./include -I ./components/legacy -I ./components/drivers/include -I ./components/finsh ``` ### 增加 Rust 相关逻辑的入口 然后增加 Rust 相关逻辑的入口到 `src/lib.rs` 文件. ```rust #[export_name = "rust_main"] pub unsafe extern "C" fn main() -> c_int { //.... } ``` 在 C applications `main` 下, 修改逻辑, 调用 `rust_main` 函数. ```c extern int rust_main(); int main(void) { rust_main(); return 0; } ``` 此时编译, 会输出 `rtthread.elf` 文件, 自动链接 C 和 Rust 代码. (能到这一步, 已经非常接近成功了, 这里往往会遇到各种链接错误, 需要细心检查编译标志和脚本) ## 编写 Rust 代码 现在可以开始编写Rust代码来实现你的 PM2.5 采集和显示功能了. 打算创建两个线程, 一个负责从 PM2.5 传感器中持续获取数据, 另一个负责显示到 OLED 屏幕上. 两者通过消息队列通信, 单向发送. 这里简单思考下 Why 的问题: Rust 嵌入式最重要的是其生态, 包括 `embedded-hal` 系列, 和基于此之上的众多设备驱动. 而 RT-Thread 作为一个嵌入式 RTOS, 提供了强大的任务管理, 线程同步, 通讯功能. 结合两者, 这里我们选择放弃 RT-Thread 的设备外设驱动部分, 使用 Rust 的 HPMicro 系列芯片支持:
替代. 而基础的线程和任务管理, 我们使用 RT-Thread 组织. 对接 `embedded-hal` 生态, 最主要的是实现各种 Trait, 考虑到 HAL 部分直接套用 Rust hpm-hal, 不需要迁移即可使用, 那么剩下的主要是 `Delay` 实现, 和 `print!` 系列的 Debug 支持: ```rust pub struct Delay; impl embedded_hal::delay::DelayNs for Delay { fn delay_ns(&mut self, _ns: u32) { todo!("impl ns-level delay using MCHTMR") } fn delay_ms(&mut self, ms: u32) { unsafe { ffi::rt_thread_mdelay(ms as _); } } fn delay_us(&mut self, us: u32) { unsafe { ffi::rt_hw_us_delay(us); } } } ``` 虽然直接在 Rust 代码中使用 `ffi::rt_thread_mdelay` 是可行的, 但 Rust 的解决思路是希望芯片支持库有一个 `DelayNs` 的实现, 包括一些传感器驱动的 crate 都有可能使用这个 trait 类型来实现延迟函数. 对于 `print!` 系列, 套用相关宏也是很简单的, 实现从 `printf(..)` 到 `println!()` 的过渡. ### RTT 核心 API 封装 -- MessageQueue 为例 RT-Thread 的核心功能都通过 C API 来提供, 为了使之成为统一风格的 Rust 式 API, 需要对其做封装. 使用Rust的Result和Option类型来进行适当的错误处理,确保代码能够优雅地处理各种可能的错误情况。 利用Rust的所有权系统和生命周期检查,确保代码是内存安全的,特别是在与C代码交互的边界。 例如消息队列 MessageQueue API, `rt_mq_` 系列, 我们可以这样封装: ```rust pub struct MessageQueue
{ raw: ffi::rt_mq_t, _phantom: core::marker::PhantomData
, } unsafe impl
Send for MessageQueue
{} unsafe impl
Sync for MessageQueue
{} impl
MessageQueue
{ pub fn new(name: &str, cap: usize) -> Option
{ // crated omitted } pub fn send(&self, msg: T) -> bool { // ... } pub fn blocking_send(&self, msg: T, timeout: u32) -> bool { // ... } pub fn send_urgent(&self, msg: T) -> bool { // ... } pub fn recv(&self) -> Option
{ let mut msg = core::mem::MaybeUninit::
::uninit(); let ret = unsafe { ffi::rt_mq_recv( self.raw, msg.as_mut_ptr() as *mut _, core::mem::size_of::
() as ffi::rt_size_t, ffi::RT_WAITING_FOREVER as _, ) }; if ret > 0 { Some(unsafe { msg.assume_init() }) } else { None } } } impl
Drop for MessageQueue
{ fn drop(&mut self) { unsafe { ffi::rt_mq_delete(self.raw); } } } ``` 通过增加 T 泛型参数的方式, 方便地实现一个 "可以发送任意 Copy 类型的消息队列". 其他的海量 RT-Thread API 都可以按照这样的逻辑封装. ### 使用 MessageQueue 简单展示下 MessageQueue 类型的使用, 这里我们用它来完成传感器线程向 OLED 显示线程的数据发送功能: ```rust #[derive(Debug, Clone, Copy)] pub struct SensorValues { pub pm2_5: u16, pub pm10: u16, } static mut MQ: Option
> = None; ``` 在入口函数初始化 MQ: ```rust #[export_name = "rust_main"] pub unsafe extern "C" fn main() -> c_int { // ... MQ = MessageQueue::new("sensor_values", 10); // ... } ``` ### 编写第一个 thread -- 获取 PM2.5 传感器数据 RT-Thread 的任务单位是线程 thread, 这里以 PM2.5 传感器为例: PMS7003 传感器是 UART 接口, 每次发送 32 字节数据, 二进制格式, 按照特定方式解析. ```rust extern "C" fn pms7003_sensor_thread(_: *mut c_void) { let periph = unsafe { peripherals::UART2::steal() }; let rx_pin = unsafe { peripherals::PB09::steal() }; let mut uart_config = hal::uart::Config::default(); uart_config.baudrate = 9600; let mut rx = hal::uart::UartRx::new_blocking(periph, rx_pin, uart_config).unwrap(); loop { let mut buf = [0u8; 32]; if let Ok(_) = rx.blocking_read(&mut buf) { let m = pms7003::Measurement::new(&buf); if let Some(m) = m { let values = SensorValues { pm2_5: m.pm2_5(), pm10: m.pm10(), }; unsafe { if let Some(mq) = &MQ { mq.send(values); } } } } } } ``` 这里使用了 `unsafe { peripherals::UART2::steal() }` 的语法来获得 Rust 对应的设备类型, 只用于展示目的, 也是最粗糙的方法. 如果进行适当的封装,完全可以借鉴 RTIC
的资源共享方式来进行管理。 ### OLED 显示采集数据 OLED 的展示我们使用 Rust 嵌入式中强大的 embedded-graphics 库来完成. 和 ssd1306 使用库完成 OLED 的显示操作. ```rust unsafe extern "C" fn display_thread(_: *mut c_void) { let p = hal::uninited(); // 另一种获取 HAL 外设类型结构的方法 // 初始化 I2C let mut i2c_config = hal::i2c::Config::default(); i2c_config.mode = hal::i2c::I2cMode::FastPlus; let i2c = hal::i2c::I2c::new_blocking(p.I2C1, p.PB07, p.PB06, i2c_config); // 初始化 OLED let di = I2CDisplayInterface::new(i2c); let mut display = Ssd1306::new(di, DisplaySize128x64, DisplayRotation::Rotate0).into_buffered_graphics_mode(); display.init().unwrap(); display.clear(BinaryColor::Off).unwrap(); // display waiting message Text::with_baseline( "Waiting for sensor data...", Point::new(0, 20), text_style, Baseline::Top, ); display.flush().unwrap(); loop { let sensor_values = unsafe { MQ.as_ref().and_then(|mq| mq.recv()) }; // println!("recv!"); if let Some(values) = sensor_values { display.clear(BinaryColor::Off).unwrap(); Text::with_baseline( "# RT-Thread Rust Demo!", Point::zero(), text_style, Baseline::Top, ) .draw(&mut display) .unwrap(); Text::with_baseline( &format!("PM2.5: {} ug/m3", values.pm2_5), Point::new(0, 20), text_style, Baseline::Top, ) .draw(&mut display) .unwrap(); Text::with_baseline( &format!("PM10: {} ug/m3", values.pm10), Point::new(0, 40), text_style, Baseline::Top, ) .draw(&mut display) .unwrap(); display.flush().unwrap(); } } } ``` 可以看到, 在主循环中,我们不断从消息队列中接收新的传感器数据。当收到新数据时,就更新显示内容。 这个设计展示了如何在 RT-Thread 中使用 Rust 的强大功能: - 类型安全:通过使用强类型的 SensorValues 结构体来传递数据。 - 错误处理:使用 Result 和 Option 类型来处理可能的错误情况。 - 硬件抽象:利用 embedded-hal 和特定芯片的 HAL 来简化硬件操作。 - 图形库:使用 embedded-graphics 来简化 OLED 显示器的操作。 通过这种方式,我们既利用了 RT-Thread 的实时操作系统功能,又充分发挥了 Rust 在嵌入式开发中的优势,实现了一个既安全又高效的 PM2.5 监测系统。 ### 注册并启动线程 有了线程定义, 还需要在入口函数里启动这些线程. 这里就采用直接调用 ffi 函数的方式解决. 封装得当的话, 完全可以用过程宏的方法实现. ```rust #[export_name = "rust_main"] pub unsafe extern "C" fn main() -> c_int { // ... let t2 = ffi::rt_thread_create( b"display\0".as_ptr() as *const _, Some(display_thread), ptr::null_mut(), 1025 * 4, // more stack for graphics related stuff 25, 100, ); ffi::rt_thread_startup(t2); let t3 = ffi::rt_thread_create( b"pms7003\0".as_ptr() as *const _, Some(pms7003_sensor_thread), ptr::null_mut(), 1024 * 4, 25, 20, ); ffi::rt_thread_startup(t3); // .... } 这里的 Rust 代码就是最传统的 unsafe FFI 写法, 往往需要涉及到指针的类型转换, C str 的 `\0` 结尾处理等等. 与此同时, Rust 对定义 RT-Thead shell 命令也有很好的支持: ```rust #[used] #[link_section = "FSymTab"] static FSYM_HELLO: ffi::finsh_syscall = ffi::finsh_syscall { name: b"hello\0".as_ptr() as _, desc: b"hello world command, written in Rust\0".as_ptr() as _, opt: ptr::null_mut(), func: Some(hello_world), }; unsafe impl Sync for ffi::finsh_syscall {} unsafe extern "C" fn hello_world() -> i32 { println!("This is a command in Rust!"); 0 } ``` 实际上 RT-Thread 内部大量使用了链接 section 的魔法实现了自动初始化, 自动调用函数, 命令注册等等功能. 这些黑魔法一样可以在 Rust 中通过类型安全的宏或者过程宏来实现. 这里展示的是最粗糙的原始方法. ### 编译烧录执行 这里使用 hpmicro-rs team 维护的 probe-rs
工具来烧录: ```bash probe-rs run --chip HPM5361 --protocol jtag rtthread.elf ``` probe-rs 工具同时直接支持 JLink RTT 的 debug 输出. 当然本例中没有使用. ## 最终效果 代码位于
## 可能的未来展望
和
两个项目已经 实现了绝大部分内容的封装, 本文绕开了这两个库, 从原理和底层介绍了最原始的 FFI 调用用法. RT-Thread 与 Rust 的结合为嵌入式开发带来了新的可能性。未来,可能期待以下几个方面的发展: **更完善的交叉编程和代码共享, 方便不同背景的嵌入式开发者在同一项目协作** 开发完整的 RT-Thread Rust API 封装库,使 Rust 开发者能更自然地使用 RT-Thread 功能. **借鉴 RTIC 的进程间数据共享定义方式, 抛弃传统的 static 全局变量** RTIC 的处理共享资源方式是我遇到的 Rust 嵌入式解决方案里很优雅的. 例如: ```rust #[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])] mod app { use cortex_m_semihosting::{debug, hprintln}; #[shared] struct Shared {} #[local] struct Local { local_to_foo: i64, local_to_bar: i64, local_to_idle: i64, } #[init] fn init(_: init::Context) -> (Shared, Local) { // .... } #[task(local = [local_to_foo], priority = 1)] async fn foo(cx: foo::Context) { // ... } } ``` **工具链优化** 目前对于初学者, 来处理 Rust + C 的混合编译不是一件容易的事情. 如果能实现 RT-Thread 核心部分使用 Clang/LLVM 编译, 那么兼容性问题将得到解决. **性能与安全性提升** 利用 Rust 的零成本抽象,在保证安全的同时优化关键路径性能. 探索 Rust Async 在 RT-Thread 下的实现可能.
5
条评论
默认排序
按发布时间排序
登录
注册新账号
关于作者
王依依
这家伙很懒,什么也没写!
文章
1
回答
0
被采纳
0
关注TA
发私信
相关文章
1
RT-THREAD在STM32H747平台上移植lwip
2
正点原子miniSTM32开发板读写sdcard
3
反馈rtt串口驱动对低功耗串口lpuart1不兼容的问题
4
Keil MDK 移植 RT-Thread Nano
5
RT1061/1052 带 RTT + LWIP和LPSPI,有什么坑要注意吗?
6
RT thread HID 如何收发数据
7
求一份基于RTT系统封装好的STM32F1系列的FLASH操作程序
8
RT-Thread修改项目名称之后不能下载
9
rt-studio编译c++
10
有木有移植rt-thread(nano)到riscv 32位MCU上
推荐文章
1
RT-Thread应用项目汇总
2
玩转RT-Thread系列教程
3
国产MCU移植系列教程汇总,欢迎查看!
4
机器人操作系统 (ROS2) 和 RT-Thread 通信
5
五分钟玩转RT-Thread新社区
6
【技术三千问】之《玩转ART-Pi》,看这篇就够了!干货汇总
7
关于STM32H7开发板上使用SDIO接口驱动SD卡挂载文件系统的问题总结
8
STM32的“GPU”——DMA2D实例详解
9
RT-Thread隐藏的宝藏之completion
10
【ART-PI】RT-Thread 开启RTC 与 Alarm组件
热门标签
RT-Thread Studio
串口
Env
LWIP
SPI
AT
Bootloader
Hardfault
CAN总线
FinSH
ART-Pi
USB
DMA
文件系统
RT-Thread
SCons
RT-Thread Nano
线程
MQTT
STM32
RTC
FAL
rt-smart
ESP8266
I2C_IIC
WIZnet_W5500
UART
ota在线升级
PWM
cubemx
freemodbus
flash
packages_软件包
BSP
潘多拉开发板_Pandora
定时器
ADC
GD32
flashDB
socket
中断
Debug
编译报错
msh
SFUD
keil_MDK
rt_mq_消息队列_msg_queue
at_device
ulog
C++_cpp
本月问答贡献
踩姑娘的小蘑菇
7
个答案
3
次被采纳
a1012112796
13
个答案
2
次被采纳
张世争
9
个答案
2
次被采纳
rv666
5
个答案
2
次被采纳
用户名由3_15位
11
个答案
1
次被采纳
本月文章贡献
程序员阿伟
7
篇文章
2
次点赞
hhart
3
篇文章
4
次点赞
大龄码农
1
篇文章
3
次点赞
ThinkCode
1
篇文章
1
次点赞
Betrayer
1
篇文章
1
次点赞
回到
顶部
发布
问题
投诉
建议
回到
底部