前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >⚡️ 一个LED灯的自述:我是如何被5层代码点亮的

⚡️ 一个LED灯的自述:我是如何被5层代码点亮的

原创
作者头像
程序员吾真本
发布2024-12-11 22:29:37
发布2024-12-11 22:29:37
4220
举报

讲动人的故事,写懂人的代码

当嵌入式开发小小白(既是嵌入式开发小白,又是编程小白)读到下面的嵌入式开发核心概念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灯

在深入讨论之前,让我们先看一下代码。

点亮LED灯的Rust代码

本文的完整代码可在github的wubin28账号的learn-rust-by-games代码库的ch01/lu1l目录下找到。如果想运行这些代码,你只需花费两三杯咖啡的价格购买一块micro:bit v2开发板,然后按照《小小白学Rust:从点亮LED到玩转编程》(极简版)第1章的步骤操作即可。下面列出与本文相关的两个源代码文件。

Rust源代码入口文件:src/main.rs

Rust源代码入口文件src/main.rs如代码清单1所示:

代码清单1 ch01/lu1l/src/main.rs

代码语言:rust
复制
// 禁用不安全代码
#![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

通用Rust项目包管理文件Cargo.toml如代码清单2所示:

代码清单2 ch01/lu1l/Cargo.toml

代码语言:ini
复制
[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来操作引脚。这个包让我们可以用统一的方式来控制不同的硬件设备。

如果删除这些依赖包中的任何一个,我们的代码就会像缺少零件的机器一样无法工作。这是因为:

  • 这些包各自承担着不可或缺的基础功能
  • 它们之间存在着密切的协作关系
  • 在嵌入式开发这个特殊的环境中,它们共同提供了必要的系统支持

点亮LED灯为何仅使用microbit-v2还不够?

🧠为什么仅使用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

BSP、embedded-hal、HAL和PAC之间到底是什么关系?

让我们先看看这个团队中每个成员的职责:

  • main()函数
    • 作为程序的主控制者,负责发出指令
    • 决定开发板的初始化时机和LED矩阵的控制方式
    • 直接与microbit-v2交互
  • microbit-v2(Board Support Crate,又称Board Support Package,BSP)板级支持包
    • 担任micro:bit v2开发板的专属管理者
    • 掌握LED矩阵、按钮等所有设备的位置信息
    • 通过embedded-halnrf52833-hal执行具体操作
  • embedded-hal(Hardware Abstraction Layer,HAL)平台无关的硬件抽象层
    • 定义设备操作的标准规范
    • 提供OutputPin等通用接口
    • 统一不同设备的控制方式
  • nrf52833-hal(Hardware Abstraction Layer,HAL)针对nRF52833平台的硬件抽象层
    • 作为nRF52833芯片的协调者,专注于功能实现,向PAC层发送指令
    • 遵循并实现embedded-hal的标准规范
    • 提供安全的操作接口
  • nrf52833-pac(Peripheral Access Crate,PAC)针对nRF52833平台的外设访问包
    • 作为nRF52833芯片的底层执行者,负责具体硬件操作
    • 管理设备的物理地址映射
    • 提供最基础的硬件控制功能

这些组件之间的关系,可以用下面的UML类图来表示,如图2:

图2 BSP、embedded-hal、HAL和PAC之间的关系

让我们详细看看这张 UML 类图的架构设计。这是一个典型的嵌入式系统分层架构,从应用层到硬件访问层层层递进。

从顶层开始:

  • 应用层(main,好比宅子的主人)是用户代码所在的位置,它通过调用 Board 接口来控制 LED 矩阵,而无需关心底层实现细节。
  • 板级支持包(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 接口实现硬件抽象,使应用代码可以轻松移植到其他平台
  • BSP 层将底层的 GPIO 操作转换为更符合应用需求的 LED 矩阵控制接口
  • 层层封装降低了直接操作硬件的风险,提供了更安全的开发环境
  • 每层职责明确,便于代码维护和单元测试

图中的依赖关系清晰地展示了各层之间的交互方式:实线箭头表示"使用"关系,虚线空心箭头表示"实现"关系。这种设计既保持了良好的解耦性,又能充分利用硬件特性。

🧠什么是GPIO?

💡这是嵌入式系统中的基础硬件接口,全称是General Purpose Input/Output(通用输入/输出接口),主要用于实现数字信号的输入和输出。

它的主要特点和作用如下:

  • 它具有出色的可编程性。每个GPIO引脚都像一个灵活的开关,可以根据需要设置为输入或输出模式。
    • 在输入模式下,它就像一个细心的侦察兵,可以读取各种外部信号,比如按钮按下状态或传感器状态变化。
    • 在输出模式下,它则成为一个指挥官,可以控制外部设备的行为,无论是点亮LED灯还是驱动马达。
  • 在输出模式下,GPIO能够输出两种数字电平,如同开关的两种状态:
    • 高电平相当于"开"的状态,通常是3.3V或5V的电压
    • 低电平相当于"关"的状态,通常是0V或接地

在我们的UML图中,你可以看到micro:bit v2开发板如何巧妙运用GPIO。每个LED都像一个小灯泡,通过行引脚和列引脚的配合来控制。通过适当调整这些GPIO引脚的电平,就能实现对特定LED的精确控制,让它在我们的指令下亮起或熄灭。

🧠如何快速验证BSP、embedded-hal、HAL和PAC之间的依赖关系?

💡这张UML图中的依赖关系,可以通过运行cargo tree(显示当前项目的依赖关系树)命令来验证:

代码语言:bash
复制
% 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

BSP、HAL、embedded-hal 和 PAC 是如何协作点亮 LED 的?

理解了上述概念,现在可以聊聊Rust代码是如何通过层层配合来点亮LED灯的精彩历程。这是一段从顶层到底层的探索之旅。

一切始于最顶层的main函数。这里,我们遇到第一位重要角色:Board对象。通过一行简洁的代码:

代码语言:rust
复制
let mut board = Board::take().unwrap();

我们创建了这个与硬件交互的核心接口,它就像一扇通向硬件世界的大门。

接着,我们来到microbit-v2这一层。这里是为micro:bit v2精心打造的控制中心,包含了Board总控制台、DisplayPins显示控制面板,以及整齐排列的row0row4col0col4引脚开关。在这里,我们可以执行如下操作:

代码语言:rust
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 点亮LED灯的Rust代码
    • Rust源代码入口文件:src/main.rs
    • 通用Rust项目包管理文件:Cargo.toml
  • 点亮LED灯为何仅使用microbit-v2还不够?
  • BSP、embedded-hal、HAL和PAC之间到底是什么关系?
  • BSP、HAL、embedded-hal 和 PAC 是如何协作点亮 LED 的?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档