Rust + Embedded: A Powerful Development Combination

The origin of Rust 

The inspiration for the Rust programming language was born by accident. In 2006, when Graydon Hoare returned to his apartment in Vancouver, he found that the elevator had failed again due to a software crash. Living on the 21st floor, when he had no choice but to climb the stairs, he couldn't help thinking, "We who are engaged in computers, how can we not even make a functioning elevator!" After this experience, Hoare began to design a new programming language , he hoped that the language would produce shorter and faster code without introducing memory errors [ 1 ]. 

You can read more about Rust's development at [ 2 ] and [ 3 ] . 

As time goes by, 18 years have passed, and Rust has become the hottest emerging programming language in the world, attracting more and more attention every year. In the first quarter of 2020, about 600,000 developers used Rust for development, and by the first quarter of 2022, the number of users had grown to 2.2 million [ 4 ]. Well-known tech giants, such as Mozilla, Dropbox, Cloudflare, Discord, Facebook (Meta), Microsoft, etc., use the Rust language extensively in their code bases. 

  

According to the 2021 developer survey statistics, the Rust language has been ranked first in the "developers' favorite programming language" list for six consecutive years.  

Programming Languages ​​for Embedded Development 

Compared with web development and desktop development, embedded development has received relatively limited attention. The reasons behind it may involve the following aspects: 

  • Hardware limitations: The limited hardware resources (such as performance and memory) of embedded systems increase the difficulty of software development. 
  • Market constraints: Compared to web and desktop applications, the embedded market is small, and the economic returns for embedded programming developers are relatively low. 
  • Low-level expertise requirements: Embedded development requires developers to have expertise on specific hardware and low-level programming languages. 
  • Longer development cycles: Developing software for embedded systems requires testing and optimizing code to meet specific hardware requirements, which can take longer than developing software for web and desktop applications. 
  • Low-level programming language limitations: Low-level programming languages ​​such as assembly and C provide developers with fewer abstractions, and direct access to hardware resources and memory can introduce memory errors. 

The above aspects are enough to make us feel the uniqueness of embedded development, and also explain why embedded development is not as popular as web development for young programmers. When developing in common modern programming languages ​​such as Python, JavaScript, or C#, developers don't need to count every processor cycle or every kilobyte used in memory, which is convenient but hugely variable, causing development It is difficult for developers to adapt to embedded development for the first time, whether it is a beginner or an experienced web/desktop/mobile developer, it is extremely challenging. Therefore, it is necessary to design a modern programming language dedicated to embedded development. 


Why choose Rust? 

Rust is a relatively young, modern programming language that focuses on memory and thread safety to develop reliable and safe software. In addition, Rust's support for concurrency and parallelism enables efficient use of resources, which is especially important in embedded development. Rust is widely used and its ecosystem is gradually taking shape. More and more developers who pursue efficiency and safety are beginning to use it as an ideal development language. These are the main reasons to choose Rust for embedded development or projects where security and reliability are important. 

Advantages of Rust (compared to C and C++) 

  • Memory safety: Rust effectively eliminates memory-related bugs such as null pointer dereferences and buffer overflows by using the concepts of ownership and borrowing. That is, through the ownership and borrowing system, Rust can guarantee compile-time memory safety. Because of the difficulty in detecting and resolving memory-related errors in embedded development due to memory and resource constraints, the memory safety guarantees provided by Rust are especially important. 
  • Concurrency safety: Rust perfectly supports zero-cost abstraction, concurrent programming, and multithreading. Through the built-in async/await syntax and powerful type system, Rust can effectively eliminate common concurrency errors such as data races. This feature is not only applicable to embedded systems, developers in various fields can easily write safe and efficient concurrent code. 
  • High performance: Rust's performance is comparable to C and C++, and Rust also provides more reliable memory safety and concurrency safety. 
  • Code readability: Compared with C and C++, Rust's grammar design pays more attention to code readability and error rate reduction. It introduces features such as pattern matching, type deduction and functional programming structure, making code writing and maintenance easier. For convenience, it is especially suitable for more complex large-scale projects. 
  • Growing ecosystem: Rust's ecosystem includes a variety of libraries (crates), tools, and resources for use in various development areas, including embedded development. As the ecosystem continues to grow, developers can easily build projects in Rust and find the specific support and resources they need. 
  • Package manager and build system: Rust has a built-in Cargo package manager for automating the build, test, and release process, as well as creating new projects and managing dependencies. 

Disadvantages of Rust (compared to C and C++) 

Despite the obvious advantages, Rust is not a perfect language, and it also has some shortcomings compared with other programming languages ​​(including but not limited to C and C++). 

  • Steep learning curve: Rust has a steeper learning curve than most programming languages ​​like C. Developers need to spend more time and effort to understand its unique features, such as the ownership and borrowing system mentioned above. This can be a challenge for developers new to Rust. 
  • Long compile time: Rust has an advanced type system and borrow checker, and takes longer to compile than other languages, especially on large projects. 
  • Insufficient tool support: While Rust's ecosystem is expanding rapidly, more mature programming languages ​​such as C and C++ have existed for decades, accumulating large code bases. In contrast, Rust's tool support is still slightly insufficient, and the difficulty of finding and using the right tool for a specific project is slightly higher. 
  • Insufficient low-level control: Compared to C and C++, Rust's safety features may limit its low-level control capabilities. While Rust still supports certain low-level optimizations or interacts directly with hardware, it's a little more difficult. 
  • Insufficient community support: Rust is still a relatively new programming language compared to more mature programming languages ​​such as C and C++. It has a small community, relatively few developers contributing code, and relatively few resources, libraries, and tools available. 

To sum up, compared with traditional embedded development languages ​​such as C and C++, Rust has many advantages in terms of memory safety, concurrency support, performance, code readability, and ecosystem. As a result, there has been a steady rise in the number of developers using Rust in embedded development, especially in projects that focus on safety, reliability, and stability. But Rust's shortcomings often have to do with being a relatively new language and having unique features compared to C and C++. Still, Rust has enough strengths to make it an obvious choice for some projects. 


How to run Rust code? 

Depending on the environment and application requirements, there are several ways to run Rust-based firmware, and such firmware usually supports running in a host environment or a bare-metal environment. Both environments are described in detail next. 

What is a hosting environment? 

Like a normal PC environment, Rust's hosting environment provides an operating system on which to build the Rust standard library (std). The standard library (std) is a collection of modules and types included in each Rust installation that supports a variety of features for building Rust programs, including data structures, distribution, mutexes and other synchronization primitives, input/output wait. 

In the host environment (referred to as std), developers can use the functions in the C-based ESP-IDF development framework. The newlib environment provided by ESP-IDF supports developers to build the Rust standard library on top of the framework. It is also possible to build Rust applications using ESP-IDF as an operating system. So in addition to all the standard library functions listed above, it is also possible to use C-based functions implemented in the ESP-IDF API. 

Here is a blinky example running on ESP-IDF (FreeRTOS) (more examples are in the esp-idf-hal repository): 

// 导入示例所需外设 
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); 
    } 

Applicable host environment 

  • Realize multiple functions: If you need to realize a large number of functions in the embedded system, such as supporting network protocols, file input/output or introducing complex data structures, you can consider using the host environment. The standard library in this environment provides rich functions to help you Build complex applications quickly and efficiently. 
  • Requires portability: The standard library provides a set of standardized APIs that support use on different platforms and architectures, making it easy to write portable and reusable code. 
  • Enable rapid development: The standard library provides a rich feature set to build applications quickly and efficiently without worrying about low-level details. 

What is a bare metal environment?  

A bare-metal environment is one in which there is no operating system available. When a Rust program is compiled with the no_std attribute, the program does not have access to certain features mentioned in the std section above. While features such as using distribution networks or introducing complex data structures are still supported, the implementation will be more complex. no_std programs rely on core language features available in all Rust environments, including data types, control structures, and low-level memory management. This environment is very useful in embedded programming, especially for scenarios where memory resources are limited and low-level control over the hardware is required. 

Here's an example of blinky running on a bare metal environment (without an OS) (more examples are in the esp-hal repository): 

#![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); 
    } 

Cases for Bare Metal Environments 

  • Reduced memory footprint: If your embedded system has limited resources and requires a smaller memory footprint, you might consider using a bare metal environment, as std can significantly increase the final binary size and compile time. 
  • Implement direct hardware control: If you need to implement direct hardware control in an embedded system, such as implementing low-level device drivers or accessing specific hardware functions, you can consider using a bare metal environment, because the abstraction layer of std makes it difficult to directly interact with hardware. 
  • Applications involving real-time constraints or time-sensitive: If an embedded system requires real-time performance or low-latency response times, a bare-metal environment may be considered, as std may cause unexpected delays and overhead. 
  • Customization requirements: Bare metal environments support more custom configurations, while also enabling fine-grained control over application behavior, making them ideal for specific or non-standard environments. 

Summary: Should I switch from C to Rust? 

If you plan to develop a new project or task that requires memory safety or concurrency, you may consider switching from C to Rust. But for projects that are already developed and working in C, there is no need to rewrite and test the entire codebase for Rust. It is not difficult to use Rust code to call C functions, so for this kind of project, you can consider retaining the existing C code and use Rust to write newly added functions and modules. Alternatively, try writing ESP-IDF components in Rust . All in all, whether to switch from C to Rust needs to be weighed according to specific needs. 


reference link 

  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

 

Guess you like

Origin blog.csdn.net/espressif/article/details/130621495