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
The Wokwi 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:
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.
π *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 provideprintln!
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;
π 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();
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();
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>
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();
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;
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);
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);
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];
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.
Next, we can zoom in to see the timing. I zoomed in to the second channel and viewed the following:
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 {
}
}
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 π.
Top comments (0)