DEV Community

Cover image for ESP32 Embedded Rust at the HAL: Remote Control Peripheral
Omar Hiari
Omar Hiari

Posted on • Updated on

ESP32 Embedded Rust at the HAL: Remote Control Peripheral

This blog post is the tenth of a multi-part series of posts where I explore various peripherals in the ESP32C3 using embedded Rust at the HAL level. Please be aware that certain concepts in newer posts could depend on concepts in prior posts.

If you find this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

Subscribe Now to The Embedded Rustacean

Introduction

The Remote Control Transceiver (RMT) peripheral in the ESP32C3 microcontroller provides a simple and efficient way to transmit and receive remote control signals. It is commonly used in applications such as infrared (IR) remote control systems. The RMT can produce programmable pulse patterns, has built-in carrier modulation, and supports multiple channels for both transmit and receive.

While the RMT peripheral in the ESP32C3 microcontroller is primarily designed for remote control applications, it can also be used for other purposes that involve generating and receiving pulse train signals. This includes but is not limited to applications like; sensor interfacing, LED control, signal generation, and timing and synchronization.

In this post, I'm going to build an application that uses the RMT to generate two different waveforms on two different pins. I will also be leveraging the Wokwi logic analyzer to verify/monitor the output of the pins.

📚 Knowledge Pre-requisites

To understand the content of this post, you need the following:

  • Basic knowledge of coding in Rust.

  • Familiarity with the basic template for creating embedded applications in Rust.

💾 Software Setup

All the code presented in this post is available on the apollolabs ESP32C3 git repo. Note that if the code on the git repo is slightly different then it means that it was modified to enhance the code quality or accommodate any HAL/Rust updates.

Additionally, the full project (code and simulation) is available on Wokwi here.

For this post, you need to download PulseView from the Sigrok website. PulseView is an open-source logic analyzer software that is going to be used to view the output signals. If you are a first-time user of PulseView and the logic analyzer function on Wokwi, I recommend you read my earlier blog post with a step-by-step setup here.

🛠 Hardware Setup

Materials

ESP32C3 Devkit

The Wokwi Logic Analyzer

Logic Analyzer

🔌 Connections

Connections include the following:

  • Gpio6 to D0 on the Wokwi logic analyzer

  • Gpio5 to D1 on the Wokwi logic analyzer

👨‍🎨 Software Design

Before delving into what the software will do, understanding the working of the RMT would be useful to understand how it'll be configured. As such, the figure below extracted from the ESP reference manual shows a block diagram of of a single RMT transmitter channel in the ESP32-C3:

RMT Tx

In the upper path, known as the data path, an input is provided by the user (in the program) where the RMT transmitter, in turn, will generate the waveforms on the pin. The lower path, known as the control path, generates the carrier signal required for modulation purposes. For the purpose of this post, we are not doing any modulation so the lower path (control path) will be disabled.

The RMT hardware employs a pattern known as the RMT symbol to define data shown below. Each symbol comprises two pairs of values. The first value within a pair, spanning 15 bits, represents the duration of the signal in RMT ticks. The second value, encapsulated within a single bit, denotes the signal level, distinguishing between high and low. This arrangement ensures an efficient and concise representation of the signal characteristics, facilitating precise communication and interpretation within the RMT system.

RMT Symbol

📝 *Note*

The image above for the RMT symbol comes from the ESP reference, however, you need to be careful as it might be confusing. The numbers on top, do not reflect bit positions like normally expected in many other manuals.

In the code introduced in this post, the application will configure two RMT channels and send a different pulse code to each. We will use the logic analyzer to verify that the correct pulses were generated on each pin.

👨‍💻 Code Implementation

📥 Crate Imports

In this implementation the crates required are as follows:

  • The esp32c3_hal crate to import the ESP32C3 device hardware abstractions.

  • The esp_backtrace crate to define the panicking behavior.

  • The esp_println crate to provide println! implementation.

use esp32c3_hal::{
    clock::ClockControl,
    esp_riscv_rt::entry,
    peripherals::Peripherals,
    prelude::*,
    pulse_control::{ClockSource, ConfiguredChannel, OutputChannel, PulseCode, RepeatMode},
    timer::TimerGroup,
    PulseControl, Rtc, IO,
};
use esp_backtrace as _;
use esp_println::println;
Enter fullscreen mode Exit fullscreen mode

🎛 Peripheral Configuration Code

1️⃣ Obtain a handle for the device peripherals: In embedded Rust, as part of the singleton design pattern, we first have to take the PAC-level device peripherals. This is done using the take() method. Here I create a device peripheral handler named dp as follows:

let peripherals = Peripherals::take();
Enter fullscreen mode Exit fullscreen mode

2️⃣ Disable the Watchdogs: The ESP32C3 has watchdogs enabled by default and they need to be disabled. If they are not disabled then the device would keep on resetting. I'm not going to go into much detail, however, watchdogs require the application software to periodically "kick" them to avoid resets. This is out of the scope of this example, though to avoid this issue, the following code needs to be included:

let mut system = peripherals.SYSTEM.split();
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

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();
Enter fullscreen mode Exit fullscreen mode

3️⃣ Configure and Obtain Handle for the RMT: To create an instance of the RMT, we need to start with creating an instance of the PulseControl peripheral (that's the way the RMT is named in the hal) in the esp32c3-hal. In PulseControl there is a new() instance method that has the following signature:

pub fn new(
    instance: impl Peripheral<P = RMT> + 'd,
    peripheral_clock_control: &mut PeripheralClockControl,
    clk_source: ClockSource,
    div_abs: u8,
    div_frac_a: u8,
    div_frac_b: u8
) -> Result<PulseControl<'d>, SetupError>
Enter fullscreen mode Exit fullscreen mode

The first three parameters are more or less expected where the RMT would require an instance of the peripheral to be used, a peripheral_clock_control struct, and a clk_source struct. However, for the last three parameters, although the HAL documentation does not provide much detail, it can be found in the reference manual (page 828). Here we aren't going to need any clock division, so all the last three values will remain zero. As such, I create an instance for the pulse handle as follows:

    // Configure RMT peripheral
    let pulse = PulseControl::new(
        peripherals.RMT,
        &mut system.peripheral_clock_control,
        ClockSource::APB,
        0,
        0,
        0,
    )
    .unwrap();
Enter fullscreen mode Exit fullscreen mode

Be aware that these settings, mainly to do with clocks, will apply to all the RMT channels within the peripheral. Nevertheless, we will be able to configure individual channel settings in the next step after we divide the channels.

4️⃣ Obtain handle and set up the channels: In the ESP32C3 there are two RMT transmitter channels that are encoded as members of the PluseControl struct; channel0 and channel1 . We can create a handle for each separately as follows:

    // Get reference to channel
    let mut rmt_channel0 = pulse.channel0;
    let mut rmt_channel1 = pulse.channel1;
Enter fullscreen mode Exit fullscreen mode

At this point, however, the individual channels are not fully configured yet. There are several methods that would enable us to do so. You can find the applicable methods in the Channel0 and Channel1 struct documentation in the esp32c3-hal. Here we use the set_idle_output_level to configure the default output level when the channel is idle, set_carrier_modulation to deactivate the carrier modulation, set_channel_divider to set the channel clock divider value, and set_idle_output to enable the output while the channel is idle:

    // Set up channel
    rmt_channel0
        .set_idle_output_level(false)
        .set_carrier_modulation(false)
        .set_channel_divider(1)
        .set_idle_output(true);

    rmt_channel1
        .set_idle_output_level(false)
        .set_carrier_modulation(false)
        .set_channel_divider(1)
        .set_idle_output(true);
Enter fullscreen mode Exit fullscreen mode

5️⃣ Assign Pins and Obtain Handles for the Configured Channels:

All of the methods in the previous step return a Channel0 or Channel1 type. Neither of which is possible to send a sequence to. Instead, we need a ConfiguredChannel0 or ConfiguredChannel1 type. Only the assign_pin method returns a configured channel. As such, we assign the pins and create handles for the configured channels as follows:

    let mut rmt_channel0 = rmt_channel0.assign_pin(io.pins.gpio6);
    let mut rmt_channel1 = rmt_channel1.assign_pin(io.pins.gpio5);
Enter fullscreen mode Exit fullscreen mode

quite frankly I found this pattern a bit different from what I expected compared to other peripherals.

Now that the channels are configured, all we have to do is create the application.

📱 Application Code

In the application, first thing that needs to be done is create the patterns that we want to generate on the pins. In order to do that, we'd have to use the PulseCode type. PulseCode is an object that represents the state of one RMT symbol explained earlier. Here I will create two arrays each with three pulse codes representing a different pattern. The two different patterns will be sent to two different channels:

let mut seq = [PulseCode {
    level1: true,
    length1: 10u32.nanos(),
    level2: false,
    length2: 90u32.nanos(),
}; 3];

let mut seq1 = [PulseCode {
    level1: true,
    length1: 50u32.nanos(),
    level2: false,
    length2: 50u32.nanos(),
}; 3];
Enter fullscreen mode Exit fullscreen mode

In the seq array three pulse codes will be sent. For each PulseCode, the first level of the symbol is set to high (true) for 10ns and the second part of the symbol to low (false) for 90ns. The seq1 array does the same though with different timings of 50ns for both on and off times.

🏃 Result

Now we can monitor the outputs with the Wokwi logic analyzer. Looking at the zoomed-out view, with the signals of both channels, you can see that the first channel is generating the three pulse codes followed by the second channel. Exactly like desired in the code.

LA Output

Next, we can zoom in to see the timing. I zoomed in to the second channel and viewed the following:

LA Zoom in

Notice the on and off times are equal like we intended them to be. However, when measuring the timing it is 620ns rather than the 50ns that we expect. This is an issue that I am still investigating and will update this post as soon as it's clear what is occurring.

📱 Full Application Code

Here is the full code for the implementation described in this post. You can additionally find the full project and others available on the apollolabs ESP32C3 git repo. Also the Wokwi project can be accessed here.

#![no_std]
#![no_main]

use esp32c3_hal::{
    clock::ClockControl,
    esp_riscv_rt::entry,
    peripherals::Peripherals,
    prelude::*,
    pulse_control::{ClockSource, ConfiguredChannel, OutputChannel, PulseCode, RepeatMode},
    timer::TimerGroup,
    PulseControl, Rtc, IO,
};
use esp_backtrace as _;
use esp_println::println;

#[entry]
fn main() -> ! {
    // Take Peripherals, Initialize Clocks, and Create a Handle for Each
    let peripherals = Peripherals::take();
    let mut system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Instantiate and Create Handles for the RTC and TIMG watchdog timers
    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;

    // Disable the RTC and TIMG watchdog timers
    rtc.swd.disable();
    rtc.rwdt.disable();
    wdt0.disable();
    wdt1.disable();

    // Instantiate and Create Handle for IO
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);

    // Configure RMT peripheral
    let pulse = PulseControl::new(
        peripherals.RMT,
        &mut system.peripheral_clock_control,
        ClockSource::APB,
        0,
        0,
        0,
    )
    .unwrap();

    // Get reference to channel
    let mut rmt_channel0 = pulse.channel0;
    let mut rmt_channel1 = pulse.channel1;

    // Set up channel
    rmt_channel0
        .set_idle_output_level(false)
        .set_carrier_modulation(false)
        .set_channel_divider(1)
        .set_idle_output(true);

    rmt_channel1
        .set_idle_output_level(false)
        .set_carrier_modulation(false)
        .set_channel_divider(1)
        .set_idle_output(true);

    // Assign GPIO pin where pulses should be sent to
    let mut rmt_channel0 = rmt_channel0.assign_pin(io.pins.gpio6);
    let mut rmt_channel1 = rmt_channel1.assign_pin(io.pins.gpio5);

    // Create pulse sequence
    let mut seq = [PulseCode {
        level1: true,
        length1: 10u32.nanos(),
        level2: false,
        length2: 90u32.nanos(),
    }; 3];

        let mut seq1 = [PulseCode {
        level1: true,
        length1: 50u32.nanos(),
        level2: false,
        length2: 50u32.nanos(),
    }; 3];

            rmt_channel0
            .send_pulse_sequence(RepeatMode::SingleShot, &seq)
            .unwrap();

            rmt_channel1
            .send_pulse_sequence(RepeatMode::SingleShot, &seq1)
            .unwrap();

    // Application Loop
    loop {
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, an application was created leveraging the remote control peripheral (RMT) for the ESP32C3 to generate different pulse waveforms. The RMT code was created at the HAL level using the Rust esp32c3-hal. Have any questions/comments? Share your thoughts in the comments below 👇.

If you found this post useful, and if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

Subscribe Now to The Embedded Rustacean

Top comments (0)