您现在的位置是:首页 >技术杂谈 >Rust + 嵌入式:强力开发组合网站首页技术杂谈

Rust + 嵌入式:强力开发组合

乐鑫科技 Espressif 2024-06-15 18:01:02
简介Rust + 嵌入式:强力开发组合

Rust 的由来 

Rust 编程语言的灵感诞生于一次意外。2006年,当 Graydon Hoare 回到位于温哥华的公寓时,发现电梯又因为软件崩溃出了故障。住在 21 楼的他无奈爬楼时,不禁心想,“我们搞计算机的,怎么连个能正常运行的电梯都做不出来!” 这次经历后,Hoare 开始着手设计一门新的编程语言,他希望这门语言可以在不引入内存错误的同时,产出更短、更快的代码 [1] 。 

您可以前往 [2] 和 [3],进一步了解 Rust 的发展。 

时光流转,已经过去了 18 年,Rust 现已成为全球炙手可热的新兴编程语言,每年都吸引着越来越多的关注。2020 年第一季度时,约有 60 万开发者使用 Rust 进行开发,到了 2022 年第一季度,使用人数已增长至 220 万 [4]。著名的科技巨头,如 Mozilla、Dropbox、Cloudflare、Discord、Facebook(Meta)、Microsoft 等,都在代码库中广泛运用 Rust 语言。 

  

根据 2021 年开发者调查统计,Rust 语言已连续六年位列“开发者最爱的编程语言”榜首。  

应用于嵌入式开发的编程语言 

比起 Web 开发和桌面开发,嵌入式开发受到的关注较为有限,背后的原因或许涉及以下几个方面: 

  • 硬件限制:嵌入式系统有限的硬件资源(如性能和内存)增大了软件开发的难度。 
  • 市场限制:相对于 Web 和桌面应用,嵌入式市场较小,嵌入式编程开发者所获得的经济回报相对较低。 
  • 底层专业知识要求:嵌入式开发要求开发者具备对具体硬件和底层编程语言的专业知识。 
  • 开发周期较长:相对于为 Web 和桌面应用开发软件,为嵌入式系统开发软件需要对代码进行测试和优化,以满足特定硬件要求,耗费时间更长。 
  • 底层编程语言限制:汇编语言和 C 语言等底层编程语言为开发者提供的抽象较少,而直接访问硬件资源和内存可能会引入内存错误。 

上述的几个方面足以让我们感受到嵌入式开发的独特之处,也解释了为什么对于年轻程序员来说,嵌入式开发并不像 Web 开发那么受欢迎。使用 Python、JavaScript 或 C# 等常见的现代编程语言进行开发时,开发者并不需要计算每个处理器周期或内存中使用的每个千字节,这样虽然便利但极大的变化,会导致开发者在初次接触嵌入式开发时很难适应,不论是初学者还是经验丰富的 Web/桌面/移动开发者,都极具挑战性。因此,有必要设计一门专用于嵌入式开发的现代编程语言。 


为什么选择 Rust? 

Rust 是一门发展时间较短的现代编程语言,它专注于内存和线程安全,旨在开发可靠且安全的软件。此外,Rust 对并发和并行的支持可以实现对资源的高效利用,这一点在嵌入式开发中尤为重要。Rust 的应用逐渐广泛,其生态系统也逐渐成型,越来越多追求高效与安全性的开发者都开始将其作为理想的开发语言。这些都是在嵌入式开发或是注重安全性和可靠性的项目中选择 Rust 的主要原因。 

Rust 的优势(与 C 和 C++ 相比) 

  • 内存安全: Rust 通过使用所有权和借用的概念,有效地消除了空指针解引用和缓冲区溢出等内存相关的错误。也就是说,通过所有权和借用系统,Rust 可以保证编译时的内存安全。因为在嵌入式开发中,由于内存和资源的限制,检测和解决内存相关错误存在困难,所以Rust 提供的内存安全保证尤为重要。 
  • 并发安全:Rust 完美支持零成本抽象、并发编程和多线程,通过内置 async/await 语法和强大的类型系统,有效地消除数据竞争等常见的并发错误。该特性不仅适用于嵌入式系统,各个领域的开发者都可以借此轻松写出安全高效的并发代码。 
  • 高性能:Rust 的性能与 C 和 C++ 旗鼓相当,Rust 还能提供更可靠的内存安全和并发安全。 
  • 代码可读性:相较于 C 和 C++, Rust 的语法设计更注重代码可读性和降低错误率,它引入了模式匹配、类型推导和函数式编程结构等特性,使代码的编写与维护更为便捷,尤其适用于较为复杂的大型项目。 
  • 日益壮大的生态系统:Rust 的生态系统包括各种库(crates)、工具和资源,可供包括嵌入式开发在内的各开发领域使用。随着生态系统的不断发展壮大,开发者可以轻松使用 Rust 搭建工程,并找到所需的特定支持和资源。 
  • 包管理器和构建系统:Rust 内置 Cargo 包管理器,用于自动化构建、测试和发布流程,同时也能创建新项目并管理依赖项。 

Rust 的劣势(与 C 和 C++ 相比) 

虽然优势明显,但 Rust 并非一门完美的语言,与其他编程语言(包括但不限于 C 和 C++)相比,它也存在一些不足之处。 

  • 学习曲线陡峭:Rust 的学习曲线比起 C 等多数编程语言更为陡峭。开发者们需要花费更多的时间和精力来理解它的独特特性,如上文所提到的所有权、借用系统等。这对初次接触 Rust 的开发者来说,可能会是一种挑战。 
  • 编译时间长:和其他语言比起来,Rust 拥有高级类型系统和借用检查器,需要花费更长的编译时间,这一点在大型工程中尤为明显。 
  • 工具支持不足:尽管 Rust 的生态系统正在迅速扩展,但 C 和 C++ 等更加成熟的编程语言已经存在了几十年,积累了大量的代码库。相较之下,Rust 的工具支持还是略显不足,在特定项目中找到并使用合适工具的难度相较略高。 
  • 底层控制不足:相较于 C 和 C++,Rust 的安全特性可能会限制其底层控制能力。尽管 Rust 仍支持特定底层优化或是直接与硬件交互,但难度略高。 
  • 社区支持不足:与 C 和 C++ 等更为成熟的编程语言相比,Rust 仍然是一门相对较新的编程语言。它的社区规模较小,贡献代码的开发者和可供使用的资源、库和工具也相对较少。 

综上所述,与 C  和 C++ 等传统的嵌入式开发语言相比,Rust 在内存安全、并发支持、性能、代码可读性以及生态系统等方面都具有诸多优势。因此,在嵌入式开发中,尤其是在注重安全、可靠性和稳定性的项目中,使用 Rust 的开发者数量稳步上升。但相较于 C 和 C++,Rust 的不足之处往往与其作为一门相对较新的语言以及独特的特性有关。不过,Rust 的优势已足以使其成为某些项目的不二之选。 


如何运行 Rust 代码? 

根据不同的环境和应用需求,可以通过多种方式运行基于 Rust 的固件,此类固件通常支持在宿主环境或裸机环境下运行。接下来将详细介绍这两种环境。 

什么是宿主环境? 

与普通的 PC 环境类似,Rust 的宿主环境提供了一个可供构建 Rust 标准库 (std) 的操作系统。标准库(std)是包含在各个 Rust 安装程序中的模块和类型的集合,支持多种用于构建 Rust 程序的功能,包括数据结构、配网、互斥锁和其他同步原语、输入/输出等。 

在宿主环境 (简称为 std) 下,开发者可以使用基于 C 的 ESP-IDF 开发框架中的功能。ESP-IDF 提供的 newlib 环境支持开发者在该框架之上构建 Rust 标准库。还可以将 ESP-IDF 作为操作系统来构建 Rust 应用程序。因此除上述列出的所有标准库功能外,还可以使用 ESP-IDF API 中实现基于 C 的功能。 

以下为在 ESP-IDF (FreeRTOS) 上运行的 blinky 示例(更多示例存放在 esp-idf-hal 仓库中): 

// 导入示例所需外设 
use esp_idf_hal::delay::FreeRtos; 
use esp_idf_hal::gpio::*; 
use esp_idf_hal::peripherals::Peripherals; 
 
// Start of our main function i.e entry point of our example 
fn main() -> anyhow::Result<()> { 
    // 应用所需 ESP-IDF 补丁 
    esp_idf_sys::link_patches(); 
 
    // 初始化所有所需外设 
    let peripherals = Peripherals::take().unwrap(); 
 
    // 创建一个 led 对象,将其设置为 GPIO4 引脚的输出模式 
    let mut led = PinDriver::output(peripherals.pins.gpio4)?; 
 
    // 设置一个每 500 毫秒即切换 LED 开/关状态的无限循环 
    loop { 
        led.set_high()?; 
        // 进入睡眠模式,以防触发看门狗 
        FreeRtos::delay_ms(1000); 
 
        led.set_low()?; 
        FreeRtos::delay_ms(1000); 
    } 
} 

适用宿主环境的情况 

  • 实现多项功能:如需在嵌入式系统实现大量功能,比如支持网络协议、文件输入/输出或者引入复杂的数据结构,可以考虑使用宿主环境,该环境下的标准库提供丰富的功能,帮助您快速高效地构建复杂的应用程序。 
  • 要求可移植性:标准库提供了一套支持在不同平台和架构上使用的标准化 API,方便编写具有可移植性和重用性的代码。 
  • 进行快速开发:标准库提供了丰富的功能集,可以快速高效地构建应用程序,无需担心底层细节。 

什么是裸机环境?  

裸机环境是指没有可供使用的操作系统环境。当编译的 Rust 程序拥有 no_std 属性时,该程序无权访问上述 std 章节中提到的某些特定功能。尽管仍支持使用配网或引入复杂数据结构等功能,但实现方式将会更加复杂。 no_std 程序依赖于 Rust 所有环境中可用的核心语言特性,包括数据类型、控制结构和底层内存管理。此环境在嵌入式编程中非常实用,特别适用于内存资源有限、需要对硬件进行低级别控制的场景。 

以下为在裸机环境上(不借助操作系统)运行的 blinky 示例(更多示例存放在 esp-hal 仓库中): 

#![no_std] 
#![no_main] 
 
// 导入示例所需外设  
use esp32c3_hal::{ 
    clock::ClockControl, 
    gpio::IO, 
    peripherals::Peripherals, 
    prelude::*, 
    timer::TimerGroup, 
    Delay, 
    Rtc, 
}; 
use esp_backtrace as _; 
 
// 设置程序执行的起始点 
// 因为这是一个 `no_std` 程序,不存在主函数 
#[entry] 
fn main() -> ! { 
    // 初始化所有所需外设 
    let peripherals = Peripherals::take(); 
    let mut system = peripherals.SYSTEM.split(); 
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze(); 
 
    // 禁用看门狗定时器。对于 ESP32-C3 来说,包括 Super WDT、 
    // RTC WDT 和 TIMG WDT 
    let mut rtc = Rtc::new(peripherals.RTC_CNTL); 
    let timer_group0 = TimerGroup::new( 
        peripherals.TIMG0, 
        &clocks, 
        &mut system.peripheral_clock_control, 
    ); 
    let mut wdt0 = timer_group0.wdt; 
    let timer_group1 = TimerGroup::new( 
        peripherals.TIMG1, 
        &clocks, 
        &mut system.peripheral_clock_control, 
    ); 
    let mut wdt1 = timer_group1.wdt; 
 
    rtc.swd.disable(); 
    rtc.rwdt.disable(); 
    wdt0.disable(); 
    wdt1.disable(); 
 
    // 将 GPIO4 设置为输出,并将其初始状态设置为高电平 

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX); 
    // 创建一个 led 对象,将其设置为 GPIO4 引脚的输出模式 
    let mut led = io.pins.gpio5.into_push_pull_output(); 
 
    // 启动 LED 
    led.set_high().unwrap(); 
 
    // 初始化延迟外设,并在循环中 
    // 使用它来切换 LED 的状态 

    let mut delay = Delay::new(&clocks); 
 
    // 设置一个每 500 毫秒即切换 LED 开/关状态的无限循环 
    loop { 
        led.toggle().unwrap(); 
        delay.delay_ms(500u32); 
    } 
} 

适用裸机环境的情况 

  • 减少内存占用:如果嵌入式系统资源有限,需要占用较小的内存,可以考虑使用裸机环境,因为 std 会显著增加最终二进制文件的大小和编译时间。 
  • 实现直接硬件控制:如果需要在嵌入式系统中实现直接硬件控制,例如实现底层设备驱动程序或访问特定硬件功能,可以考虑使用裸机环境,因为 std 的抽象层会提高直接与硬件进行交互的难度。 
  • 涉及实时约束或对时间敏感的应用程序:如果嵌入式系统要求实时性能或低延迟响应时间,可以考虑使用裸机环境,因为 std 可能导致意外延迟和开销。 
  • 自定义需求:裸机环境支持更多自定义配置,同时也能实现对应用程序行为的精细控制,非常适用于特定或非标准环境。 

小结:要不要从 C 转到 Rust 呢? 

如果您打算新开发一个对内存安全或是并发性有要求的工程或任务,可以考虑从 C 转到 Rust。但对于已经用 C 开发完毕且正常运行的工程,就没有为 Rust 而重新编写并测试整个代码库的必要了。使用 Rust 代码调用 C 函数的难度并不大,因此对于这种工程,可以考虑保留已有的 C 代码,并使用 Rust 编写新添加的功能以及模块。此外,也可以尝试使用 Rust 编写 ESP-IDF 组件。总而言之,是否要从 C 转向 Rust 需根据具体的需求进行权衡。 


参考链接 

  1. How Rust went from a side project to the world’s most-loved programming language | MIT Technology Review
  2. Announcing Rust 1.0 | Rust Blog (rust-lang.org)
  3. 4 years of Rust | Rust Blog (rust-lang.org)
  4. The state of the Rust market in 2023 (yalantis.com)
  5. Stack Overflow Developer Survey 2021
  6. https://docs.rust-embedded.org/book/intro/no-std.html#hosted-environments

 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。