题解:基于 micro:bit v2 的 Compass 罗盘项目全解析)
Comprehensive Rust 裸机实战Morning题解基于 micro:bit v2 的 Compass 罗盘项目全解析【免费下载链接】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本文是 Google《Comprehensive Rust》课程 Bare Metal 模块上午练习Morning Exercise——Compass 罗盘项目的官方题解深度剖析。文章以仓库中的 solutions-morning.md 为核心骨架完整展开嵌入式 Rust 项目的工程配置、硬件初始化、I2C/TWI 外设驱动、串口日志与 LED 点阵交互实现并结合compass项目真实源码#![no_std]、#![no_main]裸机程序逐段讲解。读完本文你将掌握在 nRF52833micro:bit v2上从零编写并运行一个读取 I2C 惯性测量单元 → 串口打印 → LED 点阵可视化 → 按键切换模式的完整嵌入式 Rust 程序并理解如何用picocom在宿主机上查看串口输出。一、练习与题解概览Bare Metal 模块的上午练习要求读者从 I2C 罗盘读取方向数据并将其记录到串口有余力时还可以把方向显示在 LED 点阵上或利用按键进行交互。练习描述见 morning.md完整练习说明含模板文件清单与提示见 compass.md。官方题解文档 solutions-morning.md 本身非常精炼——它通过 mdBook 的{{#include compass/src/main.rs:solution}}机制直接内嵌了完整可运行的题解源码。真正的干货全部位于 compass/src/main.rs约 168 行源码通过// ANCHOR: solution、top、main、loop等锚点标注供文档按区域抽取。因此要读懂题解必须把文档与源码结合起来看。课程约定作为课程资料题解代码块在 mdBook 中以rust,compile_fail标注这是因为裸机程序无法在文档构建环境中编译运行而非代码本身有错误。二、工程配置一个最小的 Cortex-M 裸机项目compass项目位于 src/exercises/bare-metal/compass/包含四个关键文件。练习模板中这几个文件不应修改它们是理解裸机工程的最小配置集。2.1 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各依赖的职责见 compass/Cargo.tomlmicrobit-v20.16.0micro:bit v2 的板级支持库Board Support CrateBSP提供Board结构体、hal模块内含Twim、Uarte、Delay、Timer等以及display::blocking::Display5×5 LED 点阵驱动。lsm303agr1.1.0LSM303AGR 惯性测量单元IMU驱动同时支持加速度计与磁力计罗盘要求传入一个实现embedded_hal::i2c::I2ctrait 的总线对象。embedded-hal1.0.0Rust 嵌入式生态的硬件抽象层标准接口lsm303agr依赖的 I2C trait 即来自这里。cortex-m-rt0.7.5Cortex-M 运行时提供#[entry]入口宏、向量表与启动代码。panic-halt1.0.0panic 处理器——发生 panic 时直接挂起halt这是最小裸机程序的常见选择。2.2 链接脚本与目标架构cargo/config.toml 指定了编译目标与链接脚本[build] target thumbv7em-none-eabihf # Cortex-M4F [target.cfg(all(target_arch arm, target_os none))] rustflags [-C, link-arg-Tlink.x]目标三元组thumbv7em-none-eabihf对应Cortex-M4F带硬件浮点单元这正是 micro:bit v2 上 nRF52833 的内核。-Tlink.x是cortex-m-rt提供的链接脚本负责把启动代码、向量表与程序段放到正确位置。2.3 Embed.toml调试器与芯片配置Embed.toml 是probe-rs/cargo-embed的配置[default.general] chip nrf52833_xxAA [debug.gdb] enabled true [debug.reset] halt_afterwards truechip nrf52833_xxAA目标芯片为 Nordic nRF52833micro:bit v2 主控。debug.gdb.enabled true允许启动 GDB 调试会话。debug.reset.halt_afterwards true复位后先暂停内核便于连接调试器。三、题解源码逐段解析完整题解位于 compass/src/main.rs。下面按文档锚点区域top/main/loop拆解。3.1 顶层声明ANCHOR: top裸机程序的三件套#![no_main] #![no_std] extern crate panic_halt as _; use core::fmt::Write; use cortex_m_rt::entry;#![no_std]不使用标准库因为目标平台没有操作系统依赖core库提供的fmt::Writetrait 来实现格式化串口输出。#![no_main]不使用常规main入口入口由cortex_m_rt::entry宏接管。extern crate panic_halt as _挂载 panic 处理器发生 panic 时 CPU 挂起。后续引入的关键模块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;注意microbit::pac::twim0::frequency::FREQUENCY_A来自 nRF52833 的 PAC外设访问库用于指定 TWI 时钟频率枚举。题解还定义了两个用于 LED 显示映射的量程常量const COMPASS_SCALE: i32 30000; // 磁力计量程 ±30000 nT纳特斯拉 const ACCELEROMETER_SCALE: i32 700; // 加速度计量程 ±700 mg毫重力3.2 入口函数ANCHOR: main外设初始化#[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);关键点一串口UARTE0。Uarte::new接收四个参数UART 外设实例、引脚board.uart.into()转换为 UARTE 引脚配置、奇偶校验、波特率。题解使用无奇偶校验Parity::EXCLUDED与115200 波特这必须与宿主机端picocom的--baud 115200参数保持一致否则会输出乱码。关键点二延时源SYST。Delay::new(board.SYST)使用 SysTick 系统定时器提供阻塞延时后续初始化 IMU 时set_mag_mode_and_odr/set_accel_mode_and_odr需要延时参数。紧接着是 IMU 的初始化这部分位于main锚点之后、loop锚点之前的连续代码中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;关键点三I2C/TWI 总线。练习提示明确指出TWI 是 I2C 的另一种叫法因此 I2C 主机外设叫 TWIM。代码通过Twim::new(board.TWIM0, board.i2c_internal.into(), FREQUENCY_A::K100)将 nRF52833 的TWIM0配置为 I2C 主机总线频率100 kHzFREQUENCY_A::K100连接到板载内部 I2C 总线board.i2c_internal即 LSM303AGR 所在的内部总线。关键点四LSM303AGR 驱动初始化链。lsm303agr驱动的使用分四步Lsm303agr::new_with_i2c(i2c)传入实现embedded_hal::i2c::I2ctrait 的Twim实例microbit::hal::Twim已实现该 traitimu.init()对芯片做初始化序列读芯片 ID、配置电源模式等set_mag_mode_and_odr(mut delay, MagMode::HighResolution, MagOutputDataRate::Hz50)磁力计设为高分辨率模式、50 Hz 输出数据率set_accel_mode_and_odr(mut delay, AccelMode::Normal, AccelOutputDataRate::Hz50)加速度计设为正常模式、50 Hzimu.into_mag_continuous()切换到磁力计连续测量模式便于主循环按状态寄存器轮询新数据。关键点五显示与按键状态。Timer::new(board.TIMER0)提供Display::show所需的计时Display::new(board.display_pins)初始化 5×5 LED 点阵。mode记录当前显示模式button_pressed用于按键沿检测去抖/防重复触发。3.3 主循环ANCHOR: loop采集、打印、可视化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();数据采集主循环先忙等busy-wait直到磁力计与加速度计的新数据状态位同时置位xyz_new_data()然后读取magnetic_field()与acceleration()。这一轮询模式避免了读到陈旧/半更新数据。串口输出格式每行输出 6 个数值用制表符分成两组mx,my,mzTABax,ay,az前三项为磁力计读数单位纳特斯拉nTx_nt()/y_nt()/z_nt()后三项为加速度计读数单位毫重力mgx_mg()/y_mg()/z_mg()。这种紧凑的 CSV 风格格式既便于人眼阅读也便于后续用脚本/电子表格做数据采集分析。LED 点阵可视化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);scale是一个通用线性映射函数带clamp钳制把传感器值映射到 0..4 的 LED 点阵坐标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) }Compass 模式下x scale(-mx)、y scale(my)符号取反用于修正传感器坐标系与 LED 点阵的朝向关系使指向在视觉上符合直觉Accelerometer 模式下x scale(ax)、y scale(-ay)同样做坐标变换目标像素亮度设为 255最亮其余为 0随后display.show(mut timer, image, 100)以100 ms的刷新间隔点亮点阵形成实时光标。按键切换模式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; }button_a.is_low()micro:bit 按键按下时引脚为低电平InputPintrait 方法button_pressed标志实现单次沿触发——按住不放只切换一次松开后才允许下次触发切换瞬间点亮全部 5×5 点阵 200 ms 作为视觉反馈。模式枚举#[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, } } }next()在 Compass罗盘方向与 Accelerometer倾角/加速度两种显示模式间循环切换演示了用无堆分配枚举表达设备状态的嵌入式惯用法。四、在真机上运行与查看串口输出练习说明 compass.md 给出了两个平台的串口查看命令。题解使用 115200 波特率因此宿主机端必须匹配Linuxpicocom --baud 115200 --imap lfcrlf /dev/ttyACM0macOS设备名可能略有差异picocom --baud 115200 --imap lfcrlf /dev/tty.usbmodem14502参数说明--baud 115200与题解中Baudrate::BAUD115200严格对应--imap lfcrlf输入映射把收到的\n转换为\r\n保证换行在终端中正确显示裸机串口通常只输出\n/dev/ttyACM0或/dev/tty.usbmodem14502micro:bit 通过 USB 枚举出的虚拟串口设备节点。退出 picocomCtrlA后按CtrlQ。启动程序后串口应依次输出Setting up IMU... Ready. -12345,23456,7890TAB120,-345,980 ...第一行提示 IMU 初始化第二行Ready.表明主循环就绪随后每 20 ms50 Hz 数据率左右输出一组 6 个数值。若方向变化磁力计数值随之改变若倾斜板子加速度计数值改变同时 LED 点阵上的亮点位置相应移动。五、题解设计的教学要点与可扩展方向作为课程上午练习的参考答案这段题解在设计上刻意覆盖了 Bare Metal 模块的核心概念概念题解中的体现#![no_std]/#![no_main]无标准库、无常规入口的裸机形态入口与向量表cortex_m_rt::entry宏 -Tlink.x链接脚本panic 处理panic_halt挂起策略外设抽象microbit-v2BSP 提供Board、Twim、Uarte等硬件抽象层embedded_hal::i2c::I2ctrait驱动与总线解耦传感器驱动lsm303agr的模式/采样率配置与数据就绪轮询阻塞式外设操作串口写入、I2C 传输、LED 刷新状态机表达Mode枚举 next()循环切换输入去抖/沿检测button_pressed标志练习描述中给出两个可选的加分项——把方向显示在 LED 上题解已实现以及以某种方式使用按键题解实现了模式切换。若想继续延伸可以尝试将串口输出的 CSV 喂给 Python/gnuplot 绘制实时航向引入embedded_hal的nb非阻塞接口替代忙等或把坐标映射改为按航向角atan2点亮点阵边缘更接近真实罗盘效果。六、延伸下午练习Afternoon的对照上午题解配套的下午练习是RTC 驱动见 rtc.md 与 solutions-afternoon.md它把战场从 Cortex-M 换到QEMU aarch64 虚拟机为 PL031 实时时钟基址0x9010000编写驱动用chrono格式化时间、用匹配寄存器忙等 3 秒扩展题则通过arm-gic配置 GIC 中断并用wfi()睡眠等待。两套练习形成对照MorningCompass真实硬件micro:bit v2 / nRF52833关注 I2C、串口、LED 等经典外设AfternoonRTC模拟环境QEMU virt关注 MMIO 寄存器操作、中断控制器GICv3与电源管理system_off。二者共享同一套裸机方法论#![no_std] 入口宏 内存映射寄存器 驱动分层这正是《Comprehensive Rust》Bare Metal 模块想传递给读者的核心能力。七、小结通过 solutions-morning.md 这篇题解及其背后完整的 compass/src/main.rs 源码我们可以看到一个真实、克制且可运行的嵌入式 Rust 参考实现工程配置上以Cargo.toml.cargo/config.tomlEmbed.toml三个文件支撑起最小裸机工具链代码上以no_std起步依次打通串口UARTE、延时SYST/TIMER0、I2CTWIM0、IMU 驱动lsm303agr与 5×5 LED 点阵最终在主循环中完成采集—上报—可视化—交互的闭环。无论是自学嵌入式 Rust还是作为课程讲师讲解裸机编程这份题解都是一份可以直接参照的完整范本。【免费下载链接】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),仅供参考