Comprehensive Rust Bare Metal 上午练习:用 I2C 罗盘与串口在 micro:bit v2 上构建无操作系统应用

发布时间:2026/9/10 13:46:45
Comprehensive Rust Bare Metal 上午练习:用 I2C 罗盘与串口在 micro:bit v2 上构建无操作系统应用 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-eabihfmicro: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 SUBSYSTEMhidraw, ATTRS{idVendor}0d28, MODE0660, GROUPlogindev, TAGuaccess |\ sudo tee /etc/udev/rules.d/50-microbit.rules sudo udevadm control --reload-rules规则中的idVendor 0d28正是 NXPmicro: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 主设备外设称为 TWIMLSM303AGR 驱动需要一个实现了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、数字输入等抽象 traitlsm303agr是 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指定目标芯片为 nRF52833xxAA 变体调试时启用 GDB 服务器并在复位后先挂起halt_afterwards true便于连接调试器后再继续执行。.cargo/config.toml见 compass/.cargo/config.toml与下午 RTC 练习中指向aarch64-unknown-none不同本练习将其中的构建目标固定为 Cortex-M4F[build] target thumbv7em-none-eabihfsrc/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/ttyACM0picocom --baud 115200 --imap lfcrlf /dev/ttyACM0macOS 上的设备名略有不同例如/dev/tty.usbmodem14502picocom --baud 115200 --imap lfcrlf /dev/tty.usbmodem14502参数说明--baud 115200必须与代码中Baudrate::BAUD115200一致--imap lfcrlf把固件输出的\n转换为\r\n使终端回显符合习惯。退出 picocom 使用快捷键CtrlA 然后 CtrlQ。启动时应首先看到 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),仅供参考