Comprehensive Rust Bare Metal 上午练习:用 I2C 罗盘与串口在 micro:bit v2 上构建无操作系统应用
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
本指南围绕 Comprehensive Rust 课程中 Bare Metal(无操作系统 Rust)模块的上午练习展开:在 BBC micro:bit v2 开发板上,通过内部 I2C 总线读取 LSM303AGR 惯性测量单元(IMU)的磁力计数据,将罗盘方向实时输出到串口,并可选地在 5×5 LED 点阵上可视化。阅读并动手完成本练习后,你将掌握#![no_std]嵌入式程序的完整开发链路——从目标架构安装、外设初始化、embedded-hal 驱动抽象到cargo-embed烧录与picocom串口调试。仓库中的参考实现位于 src/exercises/bare-metal/compass/src/main.rs,是理解整套流程的最佳范本。
练习在课程中的位置
Bare Metal 部分在 src/SUMMARY.md 中作为课程独立章节存在,其课程总览文档 src/bare-metal.md 明确说明:这是一门面向已经掌握 Rust 基础(例如完成了 Comprehensive Rust 课程主体内容)、并且最好有其他语言裸机编程经验的学员设计的单日课程。上午的日程覆盖两个主题:
- 什么是
no_stdRust; - 为微控制器编写固件(microcontroller firmware)。
课程选取 BBC micro:bit v2 作为微控制器示例硬件。它基于 Nordic nRF52833 单片机,板载 LED、按键、通过 I2C 连接的加速度计与磁力计(即本练习的主角 LSM303AGR),并带有板上 SWD 调试器。morning.md所指向的罗盘练习正是这套硬件的直接应用:读取 I2C 罗盘方向并记录到串口,其详细任务说明见 src/exercises/bare-metal/compass.md,完整参考解答见 src/exercises/bare-metal/solutions-morning.md。
一、开发环境准备:从工具链到 USB 权限
正式开始前需要按 src/bare-metal.md 中的指引安装整套交叉编译与调试工具链。在 gLinux 或 Debian 系系统上执行:
sudo apt install gdb-multiarch libudev-dev picocom pkg-config qemu-system-arm build-essential rustup update rustup target add aarch64-unknown-none thumbv7em-none-eabihf rustup component add llvm-tools-preview cargo install cargo-binutils curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh各工具的作用如下:
thumbv7em-none-eabihf:micro:bit v2 所用的 Cortex-M4F 目标三元组,编译出来的固件不依赖操作系统;aarch64-unknown-none:为课程下午的应用处理器(AArch64/QEMU virt)练习预留;llvm-tools-preview与cargo-binutils:提供objcopy等二进制工具,用于把 ELF 固件转换为可直接烧录的镜像;probe-rs:通过 SWD 调试器与目标板通信,提供cargo-embed烧录命令;picocom:轻量串口终端,用于查看板子通过 USB 串口输出的日志。
在 Linux 上还需要一条 udev 规则,让plugdev组的用户可以访问 micro:bit 的烧录/调试接口:
echo 'SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d28", MODE="0660", GROUP="logindev", TAG+="uaccess"' |\ sudo tee /etc/udev/rules.d/50-microbit.rules sudo udevadm control --reload-rules规则中的idVendor == "0d28"正是 NXP(micro:bit 所用芯片厂商)的 USB Vendor ID。配置完成后,执行lsusb应能看到设备名为 "NXP ARM mbed" 的条目。若使用 Chromebook 的 Linux 环境,还需在系统设置的 Crostini 页面中把 USB 设备共享给 Linux(即chrome://os-settings/crostini/sharedUsbDevices)。
macOS 用户则使用 Homebrew 安装等价工具:
xcode-select --install brew install gdb picocom qemu rustup update rustup target add aarch64-unknown-none thumbv7em-none-eabihf rustup component add llvm-tools-preview cargo install cargo-binutils curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh二、练习目标与硬件背景
compass.md 给出的任务表述非常简洁:从一个 I2C 罗盘读取方向,并记录到串口;如果时间充裕,再尝试把读数以某种方式显示在 LED 上,或者利用按键做交互。文档随附的 Hint 给出了完成此任务的关键技术线索:
- 关注
lsm303agr与microbit-v2两个 crate 的文档,以及 micro:bit 硬件资料; - LSM303AGR 惯性测量单元(IMU)连接在内部 I2C 总线上;
- TWI 是 I2C 的另一种叫法,因此 I2C 主设备外设称为 TWIM;
- LSM303AGR 驱动需要一个实现了
embedded_hal::i2c::I2ctrait 的实例,而microbit::hal::Twim正好实现了它; microbit::Board结构体携带各种引脚和外设字段,开箱即用。
硬件链路梳理
结合 Hint 可以还原出完整的数据通路:nRF52833 芯片内部的 TWI/I2C 主控制器(TWIM0)连接板载 LSM303AGR 传感器;lsm303agrcrate 通过embedded-hal1.0 的 I2C 抽象对该传感器进行初始化与读取;microbit-v2crate 是 nRF52833 的板级支持库(Board Support Crate),把底层 PAC/HAL 封装为更友好的Board结构与命名。
关于"板级支持库"的概念,课程在 src/bare-metal/microcontrollers/board-support.md 中有专门讲解:板级支持 crate 在 HAL 之上再包一层,为特定开发板提供方便的名称、初始化逻辑,甚至板载外设驱动——microbit-v2就附带了一个简单的 LED 点阵驱动,这正好被参考实现用在了"把罗盘读数显示到 LED"的扩展部分。
三、练习工程骨架解读
练习模板是一个完整的 Cargo 工程,位于仓库的 src/exercises/bare-metal/compass/ 目录。文档明确说明以下三个文件不需要修改,它们把工程绑定到了具体硬件:
Cargo.toml(见 compass/Cargo.toml):
[workspace] [package] name = "compass" version = "0.1.0" edition = "2024" publish = false [dependencies] cortex-m-rt = "0.7.5" embedded-hal = "1.0.0" lsm303agr = "1.1.0" microbit-v2 = "0.16.0" panic-halt = "1.0.0"依赖清单与练习提示一一对应:cortex-m-rt提供 Cortex-M 的启动代码与#[entry]入口宏;embedded-hal1.0 提供 I2C、数字输入等抽象 trait;lsm303agr是 LSM303AGR 传感器驱动;microbit-v2是板级支持库;panic-halt则定义了no_std环境下的 panic 处理(挂起并进入死循环)。
Embed.toml(见 compass/Embed.toml):
[default.general] chip = "nrf52833_xxAA" [debug.gdb] enabled = true [debug.reset] halt_afterwards = true这是cargo-embed的配置文件:chip指定目标芯片为 nRF52833(xxAA 变体);调试时启用 GDB 服务器,并在复位后先挂起(halt_afterwards = true),便于连接调试器后再继续执行。
.cargo/config.toml(见 compass/.cargo/config.toml):与下午 RTC 练习中指向aarch64-unknown-none不同,本练习将其中的构建目标固定为 Cortex-M4F:
[build] target = "thumbv7em-none-eabihf"src/main.rs 骨架(练习起点):文档给出的模板只包含main与loop两个标注了// TODO的空位,其中引入了microbit::hal::{Delay, uarte::{Baudrate, Parity, Uarte}}与Board:
#![no_main] #![no_std] extern crate panic_halt as _; use core::fmt::Write; use cortex_m_rt::entry; use microbit::{hal::{Delay, uarte::{Baudrate, Parity, Uarte}}, Board}; #[entry] fn main() -> ! { // TODO loop { // TODO } }这段骨架已经展示了no_std固件的两个铁律:#![no_std]声明不链接标准库;#![no_main]关闭运行时环境要求的main符号,改用cortex_m_rt的#[entry]属性标记入口,且入口函数返回!(永不返回)。Uarte是 nRF52833 的 UART 外设封装,Baudrate/Parity用于配置串口参数。
四、参考实现逐段拆解
仓库提供了完整的参考解答(src/exercises/bare-metal/compass/src/main.rs),我们按逻辑顺序逐段分析,这也是读者应自行实现的代码路径。
4.1 导入与常量
use embedded_hal::digital::InputPin; use lsm303agr::{ AccelMode, AccelOutputDataRate, Lsm303agr, MagMode, MagOutputDataRate, }; use microbit::Board; use microbit::display::blocking::Display; use microbit::hal::twim::Twim; use microbit::hal::uarte::{Baudrate, Parity, Uarte}; use microbit::hal::{Delay, Timer}; use microbit::pac::twim0::frequency::FREQUENCY_A; const COMPASS_SCALE: i32 = 30000; const ACCELEROMETER_SCALE: i32 = 700;两个常量是 LED 可视化时的量程参数:磁力计读数单位是纳特斯拉(nT),典型地磁场强度在数万 nT 量级,因此COMPASS_SCALE = 30000;加速度计读数单位是毫重力(mg),取ACCELEROMETER_SCALE = 700作为 ±0.7 g 的显示量程。FREQUENCY_A是 TWIM 时钟频率的枚举类型(如 100 kHz)。
4.2 串口与外设初始化
#[entry] fn main() -> ! { let mut board = Board::take().unwrap(); // Configure serial port. let mut serial = Uarte::new( board.UARTE0, board.uart.into(), Parity::EXCLUDED, Baudrate::BAUD115200, ); // Use the system timer as a delay provider. let mut delay = Delay::new(board.SYST); writeln!(serial, "Setting up IMU...").unwrap(); let i2c = Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100); let mut imu = Lsm303agr::new_with_i2c(i2c); imu.init().unwrap(); imu.set_mag_mode_and_odr( &mut delay, MagMode::HighResolution, MagOutputDataRate::Hz50, ) .unwrap(); imu.set_accel_mode_and_odr( &mut delay, AccelMode::Normal, AccelOutputDataRate::Hz50, ) .unwrap(); let mut imu = imu.into_mag_continuous().ok().unwrap(); // Set up display and timer. let mut timer = Timer::new(board.TIMER0); let mut display = Display::new(board.display_pins); let mut mode = Mode::Compass; let mut button_pressed = false;这段初始化代码包含了多个嵌入式开发的核心模式:
- 外设所有权模型:
Board::take()一次性拿走板上所有外设(且只能调用一次),随后通过board.UARTE0、board.TWIM0、board.SYST等字段把具体外设分发给驱动。board.uart.into()把引脚引脚转换为 UART 引脚配置——这正是 board-support.md 所讲的"板级库提供更友好命名与初始化"的体现; - 串口配置:
Uarte::new(board.UARTE0, ..., Parity::EXCLUDED, Baudrate::BAUD115200)将 UARTE0 外设配置为无校验、115200 波特率,与后面picocom的--baud 115200参数严格对应; - I2C 总线:
Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100)用 100 kHz 时钟初始化内部 I2C 总线(TWI 主设备),这里的board.i2c_internal正是连接到 LSM303AGR 的内部总线引脚; - 传感器驱动状态机:
lsm303agrcrate 采用类型状态设计:先Lsm303agr::new_with_i2c(i2c)构造原始实例,init()完成芯片初始化,set_mag_mode_and_odr/set_accel_mode_and_odr分别配置磁力计(高分辨率模式、50 Hz 输出)与加速度计(普通模式、50 Hz 输出),最后into_mag_continuous()把实例转型为"连续磁力计读数模式",未就绪时返回Err,因此用ok().unwrap()处理——每个步骤都在编译期推进驱动状态机,避免非法调用序列; - 延时与显示:
Delay::new(board.SYST)用系统滴答定时器提供毫秒级延时(传感器模式切换需要等待);Timer::new(board.TIMER0)与Display::new(board.display_pins)驱动 5×5 LED 点阵。
4.3 主循环:采集、记录、可视化
writeln!(serial, "Ready.").unwrap(); loop { // Read compass data and log it to the serial port. while !(imu.mag_status().unwrap().xyz_new_data() && imu.accel_status().unwrap().xyz_new_data()) {} let compass_reading = imu.magnetic_field().unwrap(); let accelerometer_reading = imu.acceleration().unwrap(); writeln!( serial, "{},{},{}\t{},{},{}", compass_reading.x_nt(), compass_reading.y_nt(), compass_reading.z_nt(), accelerometer_reading.x_mg(), accelerometer_reading.y_mg(), accelerometer_reading.z_mg(), ) .unwrap(); let mut image = [[0; 5]; 5]; let (x, y) = match mode { Mode::Compass => ( scale(-compass_reading.x_nt(), -COMPASS_SCALE, COMPASS_SCALE, 0, 4) as usize, scale(compass_reading.y_nt(), -COMPASS_SCALE, COMPASS_SCALE, 0, 4) as usize, ), Mode::Accelerometer => ( scale( accelerometer_reading.x_mg(), -ACCELEROMETER_SCALE, ACCELEROMETER_SCALE, 0, 4, ) as usize, scale( -accelerometer_reading.y_mg(), -ACCELEROMETER_SCALE, ACCELEROMETER_SCALE, 0, 4, ) as usize, ), }; image[y][x] = 255; display.show(&mut timer, image, 100);主循环是"传感器查询—串口输出—LED 可视化"三步走:
- 同步等待新数据:轮询
mag_status()与accel_status()的xyz_new_data(),直到磁力计和加速度计都产出新样本,避免读到陈旧数据(50 Hz 输出率下每次轮询等待约 20 ms); - 格式化输出:磁力计以
x_nt()/y_nt()/z_nt()输出纳特斯拉三轴分量,加速度计以x_mg()/y_mg()/z_mg()输出毫重力三轴分量,用制表符分隔、用core::fmt::Write的writeln!写入 UART——这也是开头use core::fmt::Write;的作用(no_std下没有println!,需要手动实现fmt::Write的串口驱动配合writeln!使用); - LED 点阵投影:把三维读数投影到 5×5 坐标,
scale()函数负责把传感器量程线性映射到0..=4的 LED 网格并clamp到边界,点亮对应像素。罗盘模式取(-x, y)方向投影,加速度计模式取(x, -y)投影(取负是为了让"前倾/左倾"的屏幕方向符合直觉)。
4.4 按键切换模式与模式枚举
// If button A is pressed, switch to the next mode and briefly blink all LEDs // on. if board.buttons.button_a.is_low().unwrap() { if !button_pressed { mode = mode.next(); display.show(&mut timer, [[255; 5]; 5], 200); } button_pressed = true; } else { button_pressed = false; } } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] enum Mode { Compass, Accelerometer, } impl Mode { fn next(self) -> Self { match self { Self::Compass => Self::Accelerometer, Self::Accelerometer => Self::Compass, } } } fn scale(value: i32, min_in: i32, max_in: i32, min_out: i32, max_out: i32) -> i32 { let range_in = max_in - min_in; let range_out = max_out - min_out; let scaled = min_out + range_out * (value - min_in) / range_in; scaled.clamp(min_out, max_out) }按键处理展示了裸机编程中常见的边沿检测手法:button_a.is_low()是物理电平(按下为低),而button_pressed布尔变量记录上一次采样状态;只有电平从"未按下"变为"按下"(is_low为真且button_pressed为假)时才算一次有效按键,避免按住时反复切换。按键后 LED 全亮 200 ms 作为模式切换的视觉反馈。Mode枚举在罗盘/加速度计两种显示模式间切换,scale函数则是一段通用的线性映射代码((value - min_in) / range_in * range_out + min_out,最后clamp防止越界)。
五、查看串口输出
编译烧录完成后,Linux 下用picocom连接 USB 串口(micro:bit 的 DAPLink 提供 CDC 串口,通常为/dev/ttyACM0):
picocom --baud 115200 --imap lfcrlf /dev/ttyACM0macOS 上的设备名略有不同(例如/dev/tty.usbmodem14502):
picocom --baud 115200 --imap lfcrlf /dev/tty.usbmodem14502参数说明:--baud 115200必须与代码中Baudrate::BAUD115200一致;--imap lfcrlf把固件输出的\n转换为\r\n,使终端回显符合习惯。退出 picocom 使用快捷键Ctrl+A 然后 Ctrl+Q。启动时应首先看到 "Setting up IMU...",初始化完成后再看到 "Ready.",随后是每秒 50 行的六轴数据流。
六、扩展思路与课程衔接
原练习把 LED 显示和按键交互列为"有时间再做"的加分项,参考实现已完整覆盖;在此之上你还可以尝试:
- 校准:板载磁力计受 PCB 走线与周围金属影响存在偏移,可先采集 360° 旋转数据求零偏后再映射方向角;
- 方向角计算:利用
atan2(y, x)计算航向角并以度数输出,比直接输出三轴分量更贴近"罗盘"语义; - 降低输出频率:50 Hz 的六列数据在串口终端刷屏很快,可考虑按固定节拍(如每 200 ms)采样输出。
若想继续深入,课程下午的 应用处理器练习 会切换到 QEMU aarch64 virt 平台,为 PL031 实时时钟编写 MMIO 驱动——上午练就的"外设初始化 + 轮询 + 串口输出"方法论在那里依然成立,只是 I2C 换成了直接寄存器访问,烧录换成了make qemu。
总结
本练习用一块微控制器、一颗传感器和一条 USB 串口,串联起no_stdRust 固件开发的所有关键环节:目标三元组与工具链、板级支持库的外设所有权模型、embedded-hal抽象下的 I2C 驱动接入、类型状态驱动的传感器初始化、轮询式数据采集,以及 LED 与按键组成的极简人机交互。对照仓库中的参考实现逐行阅读、再亲手重写一遍main与主循环,你就能把这套模式迁移到任意thumbv7em系列芯片项目上。相关资源索引:练习说明、参考解答、参考实现源码、课程工具链指南。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考