讲动人的故事,写懂人的代码
当嵌入式开发小小白(既是嵌入式开发小白,又是编程小白)读到下面的嵌入式开发核心概念BSP、HAL和PAC的描述时,常常会感到一头雾水。
板级支持包(Board Support Crate, BSP, 在非 Rust 环境中通常称为Board Support Package板级支持包,因此有此缩写)BSP 的职责是对整个开发板(如 micro:bit)进行统一抽象。它需要提供对微控制器以及板上的传感器、LED 等设备的抽象接口。对于定制开发板,通常没有现成的 BSP 可用。这种情况下,你需要使用芯片的 HAL,并自行开发传感器驱动程序或在 crates.io 上寻找。不过幸运的是,micro:bit 已有现成的名为microbit-v2的 BSP,所以我们可以直接在 HAL 之上使用它。
硬件抽象层(Hardware Abstraction Layer, HAL)HAL 建立在芯片的 PAC 之上,为不熟悉芯片特性的开发者提供易用的抽象接口。HAL 通常将每个外设抽象为独立的结构体,使开发者能够方便地进行数据传输等操作。如果开发micro:bit v2开发板的嵌入式程序,我们将使用 nRF52-hal。
接下来我们来探讨 Rust 嵌入式世界中的一个核心软件组件:
embedded-hal
。正如其名称所示,它与我们了解到的第二层抽象 HAL 层密切相关。embedded-hal
提供了一组特征(traits),用于描述所有 HAL 实现中特定外设的共同行为。比如,它定义了控制引脚电源开关的基本功能,这使我们能够控制开发板上的 LED 灯或其他设备。embedded-hal
让我们能够开发通用的硬件驱动程序(如温度传感器驱动),这些驱动程序可以在任何实现了embedded-hal
特征的芯片上运行。这种通用性是通过仅依赖embedded-hal
特征来实现的。这样编写的驱动程序被称为平台无关的。值得庆幸的是,crates.io
上的驱动程序大多都采用了这种平台无关的设计。外设访问包(Peripheral Access Crate, PAC)PAC 为芯片的外设提供相对安全的直接访问接口,让开发者能够精确控制每个寄存器位(当然也可能会配置错误)。通常只有在更高层抽象无法满足需求时,或开发更高层代码时,才需要直接使用 PAC。如果开发micro:bit v2开发板的嵌入式程序,我们将主要以隐式方式使用 nRF52 的 PAC。
现在,小吾就带大家分析一个简单的例子:用Rust代码点亮micro:bit v2开发板上的一个LED灯(如图1)。通过这个例子,我们将深入理解BSP、HAL、embedded-hal和PAC这些概念到底说的是什么。
图1 用Rust代码点亮micro:bit v2开发板上的一个LED灯
在深入讨论之前,让我们先看一下代码。
本文的完整代码可在github的wubin28账号的learn-rust-by-games代码库的ch01/lu1l目录下找到。如果想运行这些代码,你只需花费两三杯咖啡的价格购买一块micro:bit v2开发板,然后按照《小小白学Rust:从点亮LED到玩转编程》(极简版)第1章的步骤操作即可。下面列出与本文相关的两个源代码文件。
Rust源代码入口文件src/main.rs如代码清单1所示:
代码清单1 ch01/lu1l/src/main.rs
// 禁用不安全代码
#![deny(unsafe_code)]
// 声明这是一个独立程序,不使用标准入口点
#![no_main]
// 不使用标准库,这是嵌入式系统常见做法
#![no_std]
// 导入必要的嵌入式开发库
// 指定程序起点
use cortex_m_rt::entry;
// 操作硬件接口
use embedded_hal::digital::OutputPin;
// 控制开发板
use microbit::board::Board;
// 导入 panic 运行时错误处理程序
use panic_halt as _;
// 程序入口点
#[entry]
fn main() -> ! { // 返回 ! 表示这是一个不返回的函数
// 获取 microbit 板的控制权
let mut board = Board::take().unwrap();
// 点亮LED点阵的第4行第4列的LED,
// 即在设置第4列为低电平的同时,设置第4行为高电平
board.display_pins.col4.set_low().unwrap();
board.display_pins.row4.set_high().unwrap();
// 无限循环保持程序运行
loop {}
}
通用Rust项目包管理文件Cargo.toml如代码清单2所示:
代码清单2 ch01/lu1l/Cargo.toml
[package]
# 项目名称
name = "lu1l"
# 项目版本号
version = "0.1.0"
# Rust版本要求(2021版)
edition = "2021"
# 依赖包及其版本号
[dependencies]
# Cortex-M启动运行时支持
cortex-m-rt = "0.7.3"
# 提供panic处理机制,程序崩溃时停止运行
panic-halt = "0.2.0"
# BBC micro:bit v2开发板支持包
microbit-v2 = "0.15.0"
# 嵌入式硬件抽象层接口
embedded-hal = "1.0.0"
Cargo.toml是Rust项目的核心配置文件,它记录了项目的基本信息,如项目名称、版本号和所需的依赖包。
🧠为什么我们需要在Cargo.toml的[dependencies]
下列出这些依赖包?如果删除它们会发生什么?你能找出这些依赖包分别对应main.rs中的哪些代码吗?
💡我们聊聊为什么需要在Cargo.toml中列出这些依赖包,以及它们如何与main.rs中的代码相互配合。首先是cortex-m-rt
包,它就像一个启动器,负责初始化Cortex-M处理器并处理程序的启动流程。在代码中,我们通过use cortex_m_rt::entry
和#[entry]
标注来使用它。如果没有这个包,程序就无法启动,因为找不到入口点。
接着是panic-halt
包。它的作用是在程序发生严重错误时,让系统进入一个安全的停止状态。代码中的use panic_halt as _
就是在使用它。在嵌入式环境中,我们必须有这样一个错误处理机制,否则编译器会提醒我们缺少必要的安全保障。
microbit-v2
包则是我们的主角,它提供了与开发板交互的所有功能。当我们在代码中写use microbit::board::Board
时,就是在调用这个包提供的功能。如果没有它,我们就无法控制开发板上的任何组件。
最后是embedded-hal
包,它像是一个通用的翻译器,定义了控制硬件的标准接口。在代码中,我们用use embedded_hal::digital::OutputPin
来操作引脚。这个包让我们可以用统一的方式来控制不同的硬件设备。
如果删除这些依赖包中的任何一个,我们的代码就会像缺少零件的机器一样无法工作。这是因为:
🧠为什么仅使用Cargo.toml中的microbit-v2
依赖包还不够,还需要embedded-hal
依赖包来点亮LED灯?这两个依赖包是如何配合工作的?
💡我们聊一下为什么仅使用microbit-v2
依赖包还不够,还需要embedded-hal
依赖包来点亮LED灯。这个问题涉及到依赖包之间的协作机制。
首先,microbit-v2
作为一个板级支持包,就像一位管家,负责管理整个开发板。但管家需要一套标准的、平台无关的规则来操作各种设备,包括LED灯。这样的标准规则能让同样的操作方法适用于不同的硬件平台(比如本文所讨论的micro:bit v2 平台,或者 Arduino、STM32、ESP32 平台)。
这时,embedded-hal
就像一本通用的操作手册,它定义了标准接口(如OutputPin
trait)。它告诉管家:"要打开或关闭设备,你需要遵循这些规则。"这使得不同的硬件管理者都能按照统一的方式工作。
它们的配合是这样的:microbit-v2
这位专业的管家,通过embedded-hal
提供的标准方法来控制硬件。当我们想要点亮LED时,管家就会使用embedded-hal
定义的OutputPin
trait来完成这项工作。
要点亮LED,除了管家和标准操作手册外,还需要具体执行工作的团队:实现embedded-hal
的针对nRF52833平台的"项目经理"nrf52833-hal
,以及项目经理所依赖的"专业技工"nrf52833-pac
。
让我们先看看这个团队中每个成员的职责:
main()
函数microbit-v2
交互microbit-v2
(Board Support Crate,又称Board Support Package,BSP)板级支持包embedded-hal
和nrf52833-hal
执行具体操作embedded-hal
(Hardware Abstraction Layer,HAL)平台无关的硬件抽象层OutputPin
等通用接口nrf52833-hal
(Hardware Abstraction Layer,HAL)针对nRF52833平台的硬件抽象层embedded-hal
的标准规范nrf52833-pac
(Peripheral Access Crate,PAC)针对nRF52833平台的外设访问包这些组件之间的关系,可以用下面的UML类图来表示,如图2:
图2 BSP、embedded-hal、HAL和PAC之间的关系
让我们详细看看这张 UML 类图的架构设计。这是一个典型的嵌入式系统分层架构,从应用层到硬件访问层层层递进。
从顶层开始:
microbit-v2
,好比管家)作为一个适配层,将开发板的具体硬件(如 LED 矩阵的行列引脚)封装成易用的接口。它依赖 embedded-hal
接口来操作 GPIO,这种设计使板级支持包可以适配任何实现了 embedded-hal
接口的硬件平台。embedded-hal
,好比平台无关操作手册)定义了标准的硬件操作接口(如 OutputPin
)。这是实现跨平台兼容的关键,它声明了 set_high()
和 set_low()
等基本 GPIO 操作。nrf52833-hal
,好比nRF52833 芯片的项目经理)为 nRF52833 芯片实现了 embedded-hal
接口。它负责将抽象的 GPIO 操作转换为对应的寄存器操作,同时提供安全的访问机制。nrf52833-pac
,好比nRF52833 芯片的专业技工)提供最底层的寄存器访问功能,直接与硬件交互。它封装了硬件寄存器的读写操作,但通常不建议上层直接使用。这种分层设计的优势在于:
embedded-hal
接口实现硬件抽象,使应用代码可以轻松移植到其他平台图中的依赖关系清晰地展示了各层之间的交互方式:实线箭头表示"使用"关系,虚线空心箭头表示"实现"关系。这种设计既保持了良好的解耦性,又能充分利用硬件特性。
🧠什么是GPIO?
💡这是嵌入式系统中的基础硬件接口,全称是General Purpose Input/Output(通用输入/输出接口),主要用于实现数字信号的输入和输出。
它的主要特点和作用如下:
在我们的UML图中,你可以看到micro:bit v2开发板如何巧妙运用GPIO。每个LED都像一个小灯泡,通过行引脚和列引脚的配合来控制。通过适当调整这些GPIO引脚的电平,就能实现对特定LED的精确控制,让它在我们的指令下亮起或熄灭。
🧠如何快速验证BSP、embedded-hal、HAL和PAC之间的依赖关系?
💡这张UML图中的依赖关系,可以通过运行cargo tree
(显示当前项目的依赖关系树)命令来验证:
% cargo tree
lu1l v0.1.0 (/Users/<your-username>/learn-rust-by-games/ch01/lu1l)
# main使用cortex-m-rt
├── cortex-m-rt v0.7.5
(其他行略)
# main使用embedded-hal
├── embedded-hal v1.0.0
# main使用microbit-v2
├── microbit-v2 v0.15.1
│ └── microbit-common v0.15.1
# microbit-v2使用embedded-hal接口
│ ├── embedded-hal v1.0.0
# microbit-v2通过embedded-hal接口使用注入的nrf52833-hal
│ ├── nrf52833-hal v0.18.0
│ │ ├── nrf-hal-common v0.18.0
(其他行略)
# nrf52833-hal实现了embedded-hal接口
│ │ │ ├── embedded-hal v1.0.0
(其他行略)
# nrf52833-hal使用nrf52833-pac
│ │ └── nrf52833-pac v0.12.2 (*)
│ └── tiny-led-matrix v1.0.2
# main使用panic-halt
└── panic-halt v0.2.0
理解了上述概念,现在可以聊聊Rust代码是如何通过层层配合来点亮LED灯的精彩历程。这是一段从顶层到底层的探索之旅。
一切始于最顶层的main函数。这里,我们遇到第一位重要角色:Board
对象。通过一行简洁的代码:
let mut board = Board::take().unwrap();
我们创建了这个与硬件交互的核心接口,它就像一扇通向硬件世界的大门。
接着,我们来到microbit-v2
这一层。这里是为micro:bit v2精心打造的控制中心,包含了Board
总控制台、DisplayPins
显示控制面板,以及整齐排列的row0
到row4
和col0
到col4
引脚开关。在这里,我们可以执行如下操作:
board.display_pins.col4.set_low().unwrap();
board.display_pins.row4.set_high().unwrap();
再往下,我们来到embedded-hal
接口层。这是一个关键的抽象层,它定义了标准的硬件操作接口。通过定义OutputPin
trait,它规定了所有输出引脚都必须实现set_high()
和set_low()
等基本方法。这种统一的接口设计确保了代码的可移植性。
nrf52833-hal
层则负责实现这些接口。它通过gpio(通用输入输出控制)、Pin(引脚抽象)和Level(电平控制)等组件,将embedded-hal
定义的抽象接口转换为具体的硬件操作。当我们调用set_high()
或set_low()
时,正是这一层在处理具体的实现细节。
最后来到最底层的nrf52833-pac
。这里直接与硬件对话,配备着GPIO寄存器操作员、物理引脚控制器和底层寄存器访问工具。
整个点亮LED的过程就像一场精密的接力赛:从创建Board实例开始,经过display_pins
精确定位,通过OutputPin
接口发出指令,由hal层转换为具体操作,最终经pac层写入硬件寄存器。就这样,LED矩阵(4,4)位置的一个小灯被点亮了。这种层层递进的设计就像一台精密的机器,每个零件都扮演着独特而重要的角色,共同完成这个看似简单的点灯任务。
如果喜欢这篇文章,别忘了给文章点个“赞”,好鼓励小吾继续写哦~😃
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。