esp-idf-hal

repository·master·Indexed 20 days ago

https://github.com/esp-rs/esp-idf-hal

A Hardware Abstraction Layer (HAL) for Espressif's ESP family of microcontrollers based on the ESP-IDF framework. It provides safe Rust wrappers for ESP-IDF drivers, including GPIO, SPI, I2C, TIMER, PWM, I2S, and UART, implementing embedded-hal traits for both blocking and async modes. Designed for environments with standard library support, it allows developers to write high-level Rust code for ESP32 microcontrollers.

Tokens
17.5K
Snippets
60
Records
75
Agent score
71%

What's inside esp-idf-hal

  1. Overview of esp-idf-hal

    master

    The esp-idf-hal crate provides safe Rust wrappers for the drivers in the ESP IDF SDK. It is designed for use in environments where the ESP-IDF (standard library support) is available.

    Key Features:

    • Implements embedded-hal traits (both V0.2 and V1.0) for both blocking and async modes.
    • Supports a wide range of ESP IDF drivers including GPIO, SPI, I2C, TIMER, PWM, I2S, and UART.
    • Provides both blocking and async modes for drivers (async support is currently in progress).
    • Re-exports esp-idf-sys as esp_idf_hal::sys for access to raw bindings.

    Important Distinctions:

    • For baremetal (no_std) projects: Use esp-hal instead. esp-hal is officially supported by Espressif and does not use ESP-IDF.
    • For high-level services: Check out esp-idf-svc.
  2. Build Prerequisites for esp-idf-hal

    master
    Before building projects with esp-idf-hal, you must follow the prerequisite setup instructions defined in the esp-idf-template crate. This typically involves setting up the Rust toolchain and the ESP-IDF environment.
  3. Flash and monitor examples using cargo-espflash

    master

    You can build and flash examples conveniently using cargo-espflash. To run an example (e.g., ledc_simple) on a specific MCU (e.g., esp32c3), use the following command pattern. You must specify the correct --target for your specific chip.

    $ MCU=esp32c3 cargo espflash flash --target riscv32imc-esp-espidf --example ledc_simple --monitor
  4. Implement a custom RMT encoder with `SimpleEncoder` and `EncoderCallback`

    master

    The SimpleEncoder provides a high-level interface for implementing custom RMT (Remote Control) encoders. Instead of managing low-level buffers manually, you implement the EncoderCallback trait, which provides a SymbolBuffer to write output symbols into.

    Workflow

    1. Define your data type: Choose the type of input data you want to encode (e.g., u8).
    2. Implement EncoderCallback: Implement the encode method. This method is called from an ISR context, so avoid blocking or calling non-ISR-safe APIs.
    3. Handle Buffer Limits: If the provided SymbolBuffer is too small to encode the next chunk of data, return Err(NotEnoughSpace). The encoder will call your callback again later with a larger buffer. You must track how much input data you have already processed.
    4. Complete Encoding: Once all input data is processed, return Ok(()). The encoder will set the done flag.
    5. Initialize: Use SimpleEncoder::with_config to create the encoder.

    Tracking Progress

    Since the encode method might be called multiple times for the same input data (if space runs out), you must track your progress. You can use SymbolBuffer::position() to see how many symbols have been written so far. If you know your encoding ratio (e.g., 1 input byte = 8 output symbols), you can calculate processed items via position / 8.

    ISR Safety Warning

    The encode function is called from an ISR context. Do not call std, libc, or standard FreeRTOS APIs. You may only use FreeRTOS APIs with the FromISR suffix.

    use esp_idf_hal::rmt::encoder::{SimpleEncoder, EncoderCallback, SimpleEncoderConfig, SymbolBuffer, NotEnoughSpace};
    use esp_idf_hal::rmt::Symbol;
    
    struct MyEncoder { 
        processed_count: usize 
    }
    
    impl EncoderCallback for MyEncoder {
        type Item = u8;
    
        fn encode(&mut self, input_data: &[Self::Item], buffer: &mut SymbolBuffer<'_>) -> Result<(), NotEnoughSpace> {
            let remaining_input = input_data.len() - self.processed_count;
            
            for i in 0..remaining_input {
                let val = input_data[self.processed_count + i];
                // Example: encode 1 byte into 2 symbols
                if buffer.remaining() < 2 {
                    return Err(NotEnoughSpace);
                }
                
                // Logic to convert val to symbols...
                // buffer.write_all(&[symbol1, symbol2]).unwrap();
                
                self.processed_count += 1;
            }
    
            Ok(())
        }
    }
    
    // Usage:
    // let config = SimpleEncoderConfig::default();
    // let encoder = SimpleEncoder::with_config(MyEncoder { processed_count: 0 }, &config).unwrap();
  5. Configure RMT Memory Access Mode

    master

    The MemoryAccess enum controls how the RMT channel accesses memory, allowing a choice between CPU-driven or DMA-driven transfers.

    Variants:

    • MemoryAccess::Direct { memory_block_symbols: usize }: Enables the DMA backend. This offloads workload from the CPU but may not be supported on all ESP chips (check the TRM for your specific chip).
    • MemoryAccess::Indirect { memory_block_symbols: usize }: Disables DMA. The CPU must read/write the data. The memory_block_symbols value should be at least 64.
    // Using DMA for high throughput
    let access = MemoryAccess::Direct { memory_block_symbols: 1024 };
    
    // Using CPU-driven access
    let access = MemoryAccess::Indirect { memory_block_symbols: 64 };
  6. Prepare signals with `FixedLengthSignal` and `VariableLengthSignal`

    master

    Signals are composed of pulses. There are two primary ways to prepare them:

    1. FixedLengthSignal<const N: usize>:

      • Lives on the stack.
      • Requires knowing the number of pulse pairs (N) ahead of time.
      • Use .set(index, &(pulse1, pulse2)) to populate it.
    2. VariableLengthSignal (requires alloc feature):

      • Uses the heap.
      • Allows incremental addition of pulses using .push(&[pulse1, pulse2, ...]).
      • Useful when the signal length is unknown at compile time.
    // FixedLengthSignal example
    let mut signal = FixedLengthSignal::<2>::new();
    let p1 = Pulse::new(PinState::High, PulseTicks::new(10)?);
    let p2 = Pulse::new(PinState::Low, PulseTicks::new(11)?);
    signal.set(0, &(p1, p2))?;
    
    // VariableLengthSignal example
    let mut signal = VariableLengthSignal::new();
    signal.push(&[Pulse::new(PinState::High, PulseTicks::new(10)?), Pulse::new(PinState::Low, PulseTicks::new(9)?)])?;
  7. Configure GPIO pins using PinDriver

    master

    The PinDriver is the primary way to interact with GPIO pins. It uses a type-state pattern where the MODE generic parameter determines which methods are available (e.g., Input, Output, InputOutput, Disabled, or RTC variants).

    To configure a pin, use the corresponding try_into_* methods or the direct constructor methods.

    Available Modes:

    • Disabled: Pin is disconnected.
    • Input: Pin acts as a digital input. Supports Pull configuration.
    • Output: Pin acts as a digital output. Supports DriveStrength configuration.
    • InputOutput: Pin acts as both input and output.
    • OutputOD: Pin acts as an open-drain output.
    • InputOutputOD: Pin acts as an open-drain input-output.
    • RTC variants: On supported chips, pins can be configured in RTC mode (e.g., RtcInput, RtcOutput) to maintain functionality during deep sleep.
    // Example: Configuring a pin as an output
    let mut output_pin = PinDriver::output(pin)?;
    output_pin.set_high()?;
    
    // Example: Configuring a pin as an input with a pull-up resistor
    let mut input_pin = PinDriver::input(pin, Pull::Up)?;
    if input_pin.is_high() {
        // ...
    }
  8. Pin Traits and Marker Traits

    master

    The GPIO API uses marker traits to define the capabilities of specific pins at compile time. This ensures that you cannot, for example, attempt to set an output level on a pin that is only capable of being an input.

    • Pin: The base trait for all pins. Provides pin() to get the PinId.
    • InputPin: Marker for pins that can be used as inputs.
    • OutputPin: Marker for pins that can be used as outputs.
    • ADCPin: Marker for pins capable of Analog-to-Digital conversion.
    • DACPin: Marker for pins capable of Digital-to-Analog conversion (available on esp32, esp32s2).
    • TouchPin: Marker for pins capable of capacitive touch sensing (available on esp32, esp32s2, esp32s3).
    • RTCPin: Marker for pins that can operate in RTC mode.
  9. Use the `impl_queue_set!` macro to monitor multiple queues

    master

    Since Rust lacks variadic generics, the impl_queue_set! macro is used to create a typed FreeRTOS queue set. This allows you to monitor multiple queues of potentially different types simultaneously.

    Usage Pattern:

    1. Define a struct and enum name for your set.
    2. Provide tuples in the format (TypeIdent, fieldIdent, VariantIdent) for each queue.
    3. The macro generates a struct that holds references to the queues and an enum that carries the received item.

    Constraints:

    • All member queues must be empty when new() is called.
    • A queue can only belong to one queue set at a time.
    • The combined capacity of all queues must be greater than zero.

    API Methods:

    • new(...): Creates the set and adds the queues. Returns Err if any queue is not empty or already in a set.
    • select_from_set(timeout): Blocks until any member queue has an item. On success, it automatically receives the item and returns it wrapped in the generated enum variant. Returns None on timeout. This method is ISR-aware.
    // Example of how the macro is invoked
    impl_queue_set!(MyQueueSet, MyQueueSetSelected, (u32, q0, Q0), (String, q1, Q1));
    
    // Usage:
    let set = MyQueueSet::new(&queue_u32, &queue_string)?; 
    if let Some(event) = set.select_from_set(timeout) { 
        match event { 
            MyQueueSetSelected::Q0(val) => { /* handle u32 */ },
            MyQueueSetSelected::Q1(val) => { /* handle String */ },
        }
    }
  10. Implement a custom RMT encoder using the `Encoder` trait

    master

    To create a custom RMT encoder in Rust, implement the Encoder trait. An RMT encoder is responsible for generating and writing RMT symbols into hardware memory or DMA buffers during an RMT TX transaction.

    Key Requirements and Constraints:

    • ISR Safety: The encode function is called from an ISR context. You must not call any blocking APIs within this function.
    • Performance: It is highly recommended to place the implementation of the encode function in IRAM to minimize interrupt latency and avoid cache misses.
    • State Management: The driver may call encode multiple times during a single transaction because the RMT memory block might not hold all artifacts at once (using a ping-pong approach). Your encoder must track its own state to resume encoding correctly.
    • Memory Management: If the encoder returns EncoderState::EncodingMemoryFull, the caller must yield from the current session as there is no more space for artifacts.

    Implementation Steps:

    1. Define a struct to hold your encoder's state.
    2. Implement Encoder for your struct, defining the Item type (the data you want to encode).
    3. Implement encode(&mut self, handle: &mut RmtChannelHandle, primary_data: &[Self::Item]) -> (usize, EncoderState).
    4. Implement reset(&mut self) -> Result<(), EspError>.
    5. Convert your encoder into a raw encoder compatible with the RMT driver using into_raw(encoder).

    Encoder States:

    • EncoderState::EncodingReset: The session is in a reset state.
    • EncoderState::EncodingComplete: The encoder has finished its work.
    • EncoderState::EncodingMemoryFull: The encoding artifact memory is full; the caller should return from the current session.
    • EncoderState::EncodingWithEof: (Available for ESP-IDF $\ge$ 5.5.0) The session has inserted the EOF marker.
    // Example conceptual implementation
    struct MyEncoder { /* state */ }
    
    impl Encoder for MyEncoder {
        type Item = u8;
    
        fn encode(
            &mut self, 
            _handle: &mut RmtChannelHandle, 
            primary_data: &[Self::Item]
        ) -> (usize, EncoderState) {
            // 1. Perform encoding logic
            // 2. Return number of bytes written and the next state
            (primary_data.len(), EncoderState::EncodingComplete)
        }
    
        fn reset(&mut self) -> Result<(), EspError> {
            // Reset internal state
            Ok(())
        }
    }
    
    // To use with the RMT driver:
    // let raw_encoder = into_raw(MyEncoder { ... });
  11. Hardware constraints for GPIO pins

    master

    Depending on your specific ESP32 chip variant, certain GPIO pins have shared functions or hardware limitations that you should be aware of:

    • SPI Flash/PSRAM usage: On many chips, pins Gpio26 through Gpio32 (and potentially Gpio33 through Gpio37 if using Octal RAM/Flash) are used by SPI0/SPI1 for external memory. Using these for general-purpose I/O is not recommended.
    • ADC/DAC/Touch capabilities: Not all pins support Analog-to-Digital Conversion (ADC), Digital-to-Analog Conversion (DAC), or Touch sensing. The specific capabilities are baked into the pin types at compile time.
    • RTC functionality: Some pins are connected to the Real-Time Clock (RTC) domain, allowing them to be used in low-power modes.