DEV Community

Cover image for Embassy on ESP: Timers
Omar Hiari
Omar Hiari

Posted on

Embassy on ESP: Timers

This blog post is the fifth of a multi-part series of posts where I will explore various peripherals of the ESP32 using the embedded Rust embassy framework.

Introduction

Hardware timers while relatively simple circuits are really effective in several applications in embedded. Timer peripherals are effective in timing both software and hardware events. Timers also have features that allow the generation of hardware waveforms (Ex. PWM). In this post, I'll use embassy to measure the width of the pulse for two different signals. The resulting pulse width value will be printed on the console.

The square waves used in this post will be generated using the Wokwi custom external block. In general, this type of measurement would be useful as it emulates the behavior of applications that include tachometers or anemometers. Tachometers and anemometers generate square wave signals that are proportional to the speed of rotation. With some calculations, and depending on the application, the provided code can be expanded to provide frequency and/or rpm values.

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

Subscribe Now to The Embedded Rustacean

πŸ“š Knowledge Pre-requisites

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

  • Basic knowledge of coding 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.

πŸ›  Hardware Setup

Materials

  • ESP32-C3-DevKitM

    ESP32C3

  • Square Wave Generator/Pulse Generator: This can take many forms in real hardware though since I'm using Wokwi, there is the custom chip feature that allows me to generate square waves.

    SqWave

πŸ”Œ Connections

πŸ“ Note

All connection details are also shown in the Wokwi example.

Connections include the following:

  • Gpio0 wired to the top pin of the breakout custom chip.

  • Gpio1 wired to the bottom pin of the breakout custom chip.

πŸ‘¨β€πŸŽ¨ Software Design

The square wave signal is going to be fed into the ESP32 as an input. For the purpose of this post, the code will measure the widths of the pulses in each square wave. The custom chip is designed such that one wave generates pulses that are 10ms wide and another that are 25ms wide. To measure the pulse width, the algorithm in this post needs to determine the time elapsed between every positive edge and the negative edge that follows.

Following the configuration of the pins and the device, for each pin the follow steps are taken:

  1. await a positive edge transition.

  2. Capture timer instant.

  3. await negative edge transition.

  4. Calculate then print the duration of the pulse.

  5. Go back to step 1

Let's now jump into implementing this logic.

🚨 *Important Note*

The custom chip block in Wokwi has its own internal code that generates the square waves/pulses. There is a tab in the project where one can look at the source code. However, how to create and code a custom block is not part of this post. For the interested in creating custom chips in Wokwi I recommend checking out the chips api documentation.

πŸ‘¨β€πŸ’» Code Implementation

πŸ“₯ Crate Imports

In this implementation, the following crates are required:

  • The embassy_executor crate to import the embassy executor.

  • The esp32c3-hal crate to import the necessary ESP32C3 abstractions.

  • The esp_backtrace crate needed to define panic behavior.

  • The embassy_time crate to obtain timer abstractions.

  • The embedded_hal_async crate to obtain digital pin Wait abstractions.

  • The esp_backtrace crate needed to define panic behavior.

use embassy_executor::Spawner;
use embassy_time::Instant;
use embedded_hal_async::digital::Wait;
use esp32c3_hal::gpio::{AnyPin, Input, PullUp};
use esp32c3_hal::{clock::ClockControl, embassy, peripherals::Peripherals, prelude::*, IO};
use esp_backtrace as _;
Enter fullscreen mode Exit fullscreen mode

πŸ’“ The Pulse Timer Task

There are two pulse timer tasks, one for each pin. Both will replicate the same behavior except for two different pins. The pulse timer task is expected to await for pin edges to time the pulse width. These are the required steps:

1️⃣ Create a Pulse Timer Task: Tasks are marked by the #[embassy_executor::task] macro followed by a async function implementation. The task created is referred to as pulse1_timer task defined as follows:

#[embassy_executor::task]
async fn pulse1_timer(mut pin: AnyPin<Input<PullUp>>) {
Enter fullscreen mode Exit fullscreen mode

There is a similar task that will have the exact same code called pulse2_timer .

πŸ” Task Loop

1️⃣ Wait for rising edge on pin: The first thing we need to do is await the incoming pulse to become high. For that, there exists a wait_for_high method implementation for the Wait trait in the embedded-hal-async. wait_for_high is an async function that resolves into a Future if its waiting on a condition. Otherwise, we get a Result . We call wait_for_high on pin as follows:

pin.wait_for_high().await.unwrap();
Enter fullscreen mode Exit fullscreen mode

2️⃣ Capture the time instant: now that the signal turned high, a timer needs to be started to time the duration of the pulse. Using the new method from the Instant abstraction in the embassy-time crate allows the capture of the current time instant. We can bind the captured instant to an inst variable as follows:

let inst = Instant::now();
Enter fullscreen mode Exit fullscreen mode

3️⃣ Wait for falling edge on pin: Now all we need to do is await the incoming signal to become low marking the end of the pulse. Now instead we use a wait_for_low method implementation for the Wait trait in the embedded-hal-async. We call wait_for_low on pin as follows:

pin.wait_for_low().await.unwrap();
Enter fullscreen mode Exit fullscreen mode

4️⃣ Calculate and print duration: To calculate the Duration, Instant has a checked_duration_since method that takes two Instants and calculates a Duration. As such, we have to calculate the difference between the current Instant (after falling edge) and inst that was captured after a rising edge. Also using the as_millis Duration method, we can extract the time in milliseconds and print as follows:

// Calculate Duration
let pwidth = Instant::checked_duration_since(&Instant::now(), inst).unwrap();
// Print Duration
esp_println::println!("Sq Wave 1 Pulse Width is {}ms", pwidth.as_millis());
Enter fullscreen mode Exit fullscreen mode

πŸ“± The Main Task

The start of the main task is marked by the following code:

#[embassy_executor::main]
async fn main(spawner: Spawner)
Enter fullscreen mode Exit fullscreen mode

The following steps will mark the tasks performed in the main task.

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

let peripherals = Peripherals::take();
let system = peripherals.SYSTEM.split();
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
Enter fullscreen mode Exit fullscreen mode

2️⃣ Initialize Embassy Timers for the ESP32C3: In embassy, there exists an init function that takes two parameters. The first is system clocks and the second is an instance of a timer. Under the hood, what this function does is initialize the embassy timers. As such, we can initialize the embassy timers as follows:

embassy::init(
    let timer_group0 = esp32c3_hal::timer::TimerGroup::new(peripherals.TIMG0, &clocks);
    embassy::init(&clocks, timer_group0.timer0);
Enter fullscreen mode Exit fullscreen mode

3️⃣ Instantiate and Create Handle for IO: We need to configure the signal input pins as inputs and obtain handlers for them. Before we can obtain any handles we need to create an IO struct instance. The IO struct instance provides a HAL-designed struct that gives us access to all gpio pins thus enabling us to create handles for individual pins. This is similar to the concept of a split method used in other HALs (more detail here). We do this by calling the new() instance method on the IO struct as follows:

let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
Enter fullscreen mode Exit fullscreen mode

Note how the new method requires passing the GPIO and IO_MUX peripherals.

4️⃣ Obtain a handle and configure the signal input pins: The signal inputs are connected to pins 0 and 1 (gpio0 and gpio1) as stated earlier. Additionally, since the pin is driven by an external signal that has known states it can be configured as floating. A floating input can be configured for the pin using the into_floating_input() method as follows:

let del_but = io.pins.gpio2.into_floating_input().degrade();
Enter fullscreen mode Exit fullscreen mode

Note that we are using the degrade method which "degrades" the pin type into a generic AnyPin type that is required to pass to the button_press task.

5️⃣ Spawn Pulse Timer Tasks: now that configuration is done, we can kick off both the pulse1_timer and pulse2_timer tasks. This is done using the spawn method as follows:

spawner.spawn(pulse1_timer(pulse1)).ok();
spawner.spawn(pulse2_timer(pulse2)).ok();
Enter fullscreen mode Exit fullscreen mode

πŸ“± 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]
#![feature(type_alias_impl_trait)]

use embassy_executor::Spawner;
use embassy_time::{Instant, Duration};
use embedded_hal_async::digital::Wait;
use esp32c3_hal::gpio::{AnyPin, Input, Floating};
use esp32c3_hal::{clock::ClockControl, embassy, peripherals::Peripherals, prelude::*, IO};
use esp_backtrace as _;

#[embassy_executor::task]
async fn pulse1_timer(mut pin: AnyPin<Input<Floating>>) {
    loop {
        // Wait for rising edge
        pin.wait_for_high().await.unwrap();
        // Capture time instant at rising edge
        let inst = Instant::now();
        // Wait for falling edge
        pin.wait_for_low().await.unwrap();
        // Calculate Duration
        let pwidth = Instant::checked_duration_since(&Instant::now(), inst).unwrap();
        // Print Duration
        esp_println::println!("Sq Wave 1 Pulse Width is {}ms", pwidth.as_millis());
        // Uncomment below line to reduce console print frequency
        // Timer::after(Duration::from_millis(1000)).await;
    }
}

#[embassy_executor::task]
async fn pulse2_timer(mut pin: AnyPin<Input<Floating>>) {
    loop {
        // Wait for rising edge
        pin.wait_for_high().await.unwrap();
        // Capture time instant at rising edge
        let inst = Instant::now();
        // Wait for falling edge
        pin.wait_for_low().await.unwrap();
        // Calculate Duration
        let pwidth = Instant::checked_duration_since(&Instant::now(), inst).unwrap();
        // Print Duration
        esp_println::println!("Sq Wave 1 Pulse Width is {}ms", pwidth.as_millis());
        // Uncomment below line to reduce console print frequency
        // Timer::after(Duration::from_millis(1000)).await;
    }
}

#[main]
async fn main(spawner: Spawner) {
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM.split();
    let clocks = ClockControl::boot_defaults(system.clock_control).freeze();

    // Initialize Embassy with needed timers
    let timer_group0 = esp32c3_hal::timer::TimerGroup::new(peripherals.TIMG0, &clocks);
    embassy::init(&clocks, timer_group0.timer0);

    // This line is for Wokwi only so that the console output is formatted correctly
    esp_println::print!("\x1b[20h");

    // Inititalize and configure pins
    // Acquire Handle to IO
    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    // Configure pins as pull up input and degrade
    let pulse1 = io.pins.gpio0.into_floating_input().degrade();
    let pulse2 = io.pins.gpio1.into_floating_input().degrade();

    // Spawn pulse measurement tasks
    spawner.spawn(pulse1_timer(pulse1)).ok();
    spawner.spawn(pulse2_timer(pulse2)).ok();
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, a timer application measuring square wave pulse durations was created. The application leverages the Timer peripherals for the ESP32C3 microcontroller. The code was created leveraging the embassy framework. Have any questions? Share your thoughts in the comments below πŸ‘‡.

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

Subscribe Now to The Embedded Rustacean

Top comments (0)