Skip to main content

Command Palette

Search for a command to run...

03 - Triển khai một dự án ESP32C3 dùng Rust (P1)

Updated
6 min read
03 - Triển khai một dự án ESP32C3 dùng Rust (P1)

Trong bài viết này, mình sẽ demo việc triển khai một dự án ESP32C3 bằng việc sử dụng các công cụ đã được cài đặt từ bài viết trước. Từ dự án này, chúng ta sẽ cùng phân tích những ưu điểm mà Rust trong quá trình phát triển firmware cho môt MCU.

Khởi tạo dự án với esp-generate

Nếu bạn đã quen với viêc dùng nút New Project trên các IDE chuyên biệt để phát triển, giờ chúng ta đã có công cụ esp-generate trên máy, đây sẽ là lúc sử dụng nó để khởi tạo dự án.

Mình sẽ để thông tin cấu hình dưới đây và giải thích từng chi tiết.

  • Enable unstable HAL features: Một số tính năng được đánh dấu là unstable và có thể được thay đổi trong các minor update, các tính năng unstable có thể kể đến như:

    • ESP_HAL_CONFIG_PLACE_SPI_MASTER_DRIVER_IN_RAM: Thiết đặt driver SPI master ở RAM để tăng hiệu suất.

    • ESP_HAL_CONFIG_PLACE_RMT_DRIVER_IN_RAM: Đặt driver RMT ở RAM để tăng hiệu suất.

    • ESP_HAL_CONFIG_STACK_GUARD_VALUE: Giá trị ghi vào biến stack guard (mặc định là 0xDEEDBAAD).

    • ESP_HAL_CONFIG_STACK_GUARD_MONITORING: Sử dụng data watchpoint để kiểm tra xem stack guard có bị ghi đè không.

    • ESP_HAL_CONFIG_WRITE_VEC_TABLE_MONITORING: Sử dụng data watchpoint để bảo vệ vector table khỏi bị ghi đè

    • ESP_HAL_CONFIG_STACK_GUARD_MONITORING_WITH_DEBUGGER_CONNECTED: Bật stack guard ngay cả khi đang kết nối debugger.

    • ESP_HAL_CONFIG_IMPL_CRITICAL_SECTION: Cung cấp implementation cho critical-section.

Các tính năng này có thể hữu ích khi được sử dụng trong dự án, vì vậy mình sẽ enable trước để có thể sử dụng nếu cần. Chú ý là nếu bạn muốn dùng feature nào thì sẽ cần set value ở [env] trong file .cargo/config.toml

  • Enable allocations via the esp-alloc crate: Cho phép phân bổ bộ nhớ Heap thông qua esp-alloc. Trong quá trình làm việc của hệ thống, có thể sẽ cần sử dụng đến vùng nhớ Heap (dù thường thì người phát triển sẽ tránh sử dụng Heap), ví dụ như:

    • Sử dụng kiểu dữ liệu động: String, Vec<T>, Box<T> / Rc<T> / Arc<T>.

    • Xử lý dữ liệu không biết trước kích thước.

Tính năng này cho phép cấp phát bộ nhớ trong Heap với crate esp-alloc. Một vài tính năng (Wi-Fi, BLE,…) sẽ chỉ có thể được sử dụng nếu tính năng này được enable.

  • Enable Wi-Fi via the esp-radio crate: Cho phép sử dụng Wi-Fi.

  • Enable BLE via the esp-radio crate (embassy-trouble): Cho phép sử dụng BLE (Bluetooth Low Energy). Nếu bạn để ý thì còn có lựa chọn với (bleps), tuy nhiên độ hoàn thiện của embassy-trouble là tốt hơn và thường được sử dụng hơn. Tính năng này chỉ có thể enable nếu bạn chọn cùng với Add embassy framework support, mình sẽ nói thêm về framework này ở phần sau

  • Use probe-rs to flash and monitor instead of espflash: Sử dụng probe-rs thay vì espflash.

  • Flashing, logging and debugging (probe-rs):

    • Use defmt to print messages: defmt là một logging framework hiệu suất cao, được thiết kế để giải quyết vấn đề tốn tài nguyên khi ghi log với println! hoặc crate log. Cách giải quyết của nó khá hay: Thay vì lưu chuỗi log vào Flash của MCU, nó sẽ lưu chuỗi đó vào một file metadata trên máy host với 1 ID riêng, khi cần gửi log thì MCU sẽ chỉ gửi ID và thông tin bổ sung. Ví dụ:
      Đối với Vị trí (x):{} với giá trị truyền vào là 10
      Logging bình thường: MCU lưu Vị trí (x): vào Flash, xử lý ghép giá trị 10 vào chuỗi, rồi gửi Vị trí (x):10 qua UART.

      Sử dụng defmt: Khi compile thì chuỗi Vị trí (x): được lưu với ID là 1 trong file metadata, MCU sẽ chỉ gửi raw data như [1, 25], probe-rs sẽ xử lý việc tìm và nối chuỗi rồi in ra trên máy host.

    • Use panic-rtt-target as the panic handler: panic-rtt-target sử dụng giao thức RTT (Real-Time Transfer) thay vì UART để thông báo lỗi khi chương trình bị panic.

Sau khi khởi tạo xong, chúng ta đã có một folder project trông như này:

Vậy là giờ chúng ta có thể bắt đầu viết code được rồi đấy, tuy nhiên mình sẽ nói qua một chút về thứ chúng ta sẽ dùng nhé.

Embassy Framework

Embassy là một framework hiện đại để phát triển dự án nhúng bằng Rust, với ý tưởng cốt lõi là Rust + async ❤️ embedded. Nó mang Async/Await (thứ mà thường gặp khi làm Backend Website với NodeJS🫨) xuống phần cứng bare-metal.

Async trên Bare-metal là cái thứ gì?

Mình sẽ để đoạn code mà chúng ta vừa khởi tạo lúc nãy ở đây để nói về Async nhé

// src/bin/main.rs
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
    // Khởi tạo các ngoại vi, cấp phát bộ nhớ, khai báo phần cứng blabla
    // ...
    // TODO: Spawn some tasks
    let _ = spawner;

    loop {
        info!("Hello world!");
        Timer::after(Duration::from_secs(1)).await;
    }

}

Thay vì đánh vật để xử lý các ngắt phức tạp, hoặc quản lý luồng nặng nề của RTOS, Embassy cho phép chúng ta viết code trông có vẻ “tuần tự” nhưng thực ra lại là chạy “đa nhiệm” hiệu quả.

  • Code blocking: delay(1000); sẽ khiến CPU thực sự phải chờ trong 1 giây mà không làm gì cả, gây lãng phí tài nguyên.

  • Code async: Timer::after(Duration::from_secs(1)).await; cho phép CPU chuyển sang task khác trong vòng 1 giây, sau khi đủ 1 giây thì sẽ quay trở lại.

Thông thường để đa nhiệm trên MCU, có 2 phương án được đưa ra:

  • Super-loop: Liên tục polling khiến cho code rất rối và khó quản lý thời gian.

  • RTOS: Dùng các luồng (thread) để quản lý, tốn tài nguyên (do mỗi thread cần stack riêng), dễ gặp Race Condition hoặc Deadlock.

Embassy xử lý bằng cách chọn mô hình Cooperative Multitasking dựa trên async/await của Rust.

  • Zero-cost: Không cấp phát bộ nhớ động bắt buộc.

  • Hiệu suất cao: CPU chỉ chạy khi thực sự có việc, còn không thì sẽ ngủ.

  • Không scheduler: embassy-executor sẽ chịu trách nhiệm xếp lịch chạy các task, vì là cooperative nên nhẹ và đơn giản hơn scheduler của RTOS nhiều

Những thành phần chính của framework

Embassy không chỉ là một thư viện, nó là một bộ công cụ đầy đủ:

  • embassy-executor: Như mình đã nhắc ở trên, đây chính là thứ điều phối cho hệ thống.

  • embassy-time: Quản lý thời gian, Clock, Delay, Timeout.

  • embassy-sync: Các công cụ để các Task giao tiếp với nhau an toàn: Channel, Mutex, Signal.

  • embassy-net: Network Stack (TCP/UDP, DHCP) hỗ trợ async.

  • HAL (Hardware Abstraction Layer): Các driver phần cứng hỗ trợ async.

    • STM32: embassy-stm32

    • nRF: embassy-nrf

    • RP2040: embassy-rp

    • ESP32: Sử dụng esp-hal (được tích hợp chặt chẽ với Embassy).

Flash chương trình vào ESP32C3

Okay, giờ chúng ta đã có một Hello world! project, giờ thì thử flash xuống board chứ nhỉ.

Khi chạy lệnh cargo run thì thực chất chúng ta chạy probe-rs run --chip=esp32c3 --preverify --always-print-stacktrace --no-location --catch-hardfault, bạn có thể kiểm tra nội dung file ./cargo/config.toml để xem thử nha.

[target.riscv32imc-unknown-none-elf]
runner = "probe-rs run --chip=esp32c3 --preverify --always-print-stacktrace --no-location --catch-hardfault"

Bài viết trước: 02 - Thiết lập môi trường phát triển dự án nhúng sử dụng Rust