Omar Hiari
Posted on June 9, 2023
This blog post is the eighth 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
Serial communication interfaces can be really hard to debug when running into an issue. Running blind to what is happening at the electrical level just leads to more guesswork. Though with the proper tools, a lot more insight can make things much easier. As a result, when it comes to serial debugging, logic analyzers are worth their weight in gold.
This post is going to take a slightly different approach from prior ones. Although the post is about SPI it would be a Wokwi logical analyzer tutorial. To elaborate, I will be using SPI in a loop-back configuration to explore the logic analyzer feature in Wokwi.
📚 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.
Familiarity with the SPI interface.
💾 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.
🛠 Hardware Setup
Materials
🔌 Connections
Connections include the following:
Gpio7 (SCLK) to D0 on the Wokwi logic analyzer
Gpio6 (MISO) to D1 on the Wokwi logic analyzer
Gpio5 (MOSI) to D2 on the Wokwi logic analyzer
Gpio4 (CS) to D3 on the Wokwi logic analyzer
(MISO) Gpio6 to (MOSI) Gpio5 (loopback configuration)
👨🎨 Software Design
In the code introduced in this post, there isn't much of an application. Its mostly configuration since the hardware is configured in loopback mode (mosi connected to miso). The SPI peripheral will keep transferring the same message over and checking if it was received correctly.
Let's now jump into implementing this algorithm.
👨💻 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,
gpio::IO,
peripherals::Peripherals,
prelude::*,
spi::{Spi, SpiMode},
timer::TimerGroup,
Delay,
Rtc,
};
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️⃣ Instantiate and Create Handle for IO: We need to configure the LED pin as a push-pull output and obtain a handler for the pin so that we can control it. We also need to obtain a handle for the button input pin. Before we can obtain any handles for the LED and the button 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);
Note how the new
method requires passing the GPIO
and IO_MUX
peripherals.
4️⃣ Obtain handles for the SPI pins: I will be creating these handles for convenience since the SPI configuration will take care of configuring the pins. This would help me remember what pins were assigned to which function:
let sclk = io.pins.gpio7;
let miso = io.pins.gpio6;
let mosi = io.pins.gpio5;
let cs = io.pins.gpio4;
5️⃣ Obtain a handle and configure the SPI peripheral: to create a SPI instance, the Spi
type in the esp32c3-hal
has a new
method that takes 9 parameters, the first parameter is the SPI peripheral that we want to instantiate, the next 4 are the SPI pins that we created handles for, then the 5th parameter is the desired SPI frequency. The 6th parameter is the SPI mode which takes an SpiMode
enum
. The final two parameters are the clock handles.
let mut spi = Spi::new(
peripherals.SPI2,
sclk,
mosi,
miso,
cs,
25u32.kHz(),
SpiMode::Mode0,
&mut system.peripheral_clock_control,
&clocks,
);
6️⃣ Obtain a handle for the delay: I'll be using the hal Delay
type to create a delay handle as follows:
let mut delay = Delay::new(&clocks);
📱 Application Code
🔁 Application Loop
I'm going to try two things here. First is sending three bytes in three consecutive transactions. I will be printing the received byte to the console to confirm that the same transmitted byte is received:
loop {
// Individual arrays multiple transfers
let mut data = [0xde];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
let mut data = [0xca];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
let mut data = [0xad];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
delay.delay_ms(100u32);
}
You can see that I'm using the transfer
method which takes a single &[u8]
parameter. The transfer
method transmits the data in a data
array and stores the received data in the same data
array. This is a link to the Wokwi simulation. Once you run the simulation on Wokwi samples will be captured. After you stop the simulation a wokwi-logic.vcd
file will automatically be downloaded. To open it, you need to fire up Pulseview and click on the open icon and select "Import Value Change Dump data" as shown below:
Once opened, you'll see the below options before the signals are loaded. You'll only need to increase the downsampling factor to reduce memory usage. A factor of 50 would be sufficient for most serial communication applications:
When the signals appear in the view, you can choose a decoder to make signal analysis easier. You can do that by clicking on the green and yellow icon in the upper right corner in which the menu shown below appears. Search for SPI and double click.
Once doing that, an additional SPI signal line will appear in the window. The signals will remain red until you assign the signals. To do that you need to click on the SPI tag/label on the left, a menu will appear for you to identify the signals.
For our case the signals are as shown below:
Notice how the signals appear as small lines or pulses. This is because we need to zoom in further. You can zoom in either by double clicking on the signal in the window, or using the plus icon on top. After zooming, you should see something like this:
This is exactly what we expect. Notice that 0xde
is transmitted (and consequently received), followed by 0xce
, and finally 0xde
.
🔁 Second Experiment
Using the transfer
SPI interface its actually possible to pack the three bytes shown earlier in one array and perform a single transaction. This is shown in the code below:
loop {
// One array single transfer
let mut data = [0xde, 0xca, 0xad];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
}
Following the earlier steps, the corresponding logic analyzer signal is as follows:
I wanted to highlight this case since the decoder in this case seems to act weirdly. Although the SPI signal is correct, only the first byte seems to be decoded correctly. Additionally, not all bits seem to be decoded either. Point here is that, you probably shouldn't rely completely on the decoders assuming they are generating correct answers.
📱 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,
gpio::IO,
peripherals::Peripherals,
prelude::*,
spi::{Spi, SpiMode},
timer::TimerGroup,
Delay,
Rtc,
};
use esp_backtrace as _;
use esp_println::println;
#[entry]
fn main() -> ! {
let peripherals = Peripherals::take();
let mut system = peripherals.SYSTEM.split();
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
// Disable the 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;
rtc.swd.disable();
rtc.rwdt.disable();
wdt0.disable();
wdt1.disable();
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let sclk = io.pins.gpio7;
let miso = io.pins.gpio6;
let mosi = io.pins.gpio5;
let cs = io.pins.gpio4;
let mut spi = Spi::new(
peripherals.SPI2,
sclk,
mosi,
miso,
cs,
25u32.kHz(),
SpiMode::Mode0,
&mut system.peripheral_clock_control,
&clocks,
);
let mut delay = Delay::new(&clocks);
loop {
// One array single transfer
//let mut data = [0xde, 0xca, 0xad];
// spi.transfer(&mut data).unwrap();
// println!("{:x?}", data);
// Individual arrays multiple transfers
let mut data = [0xde];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
let mut data = [0xca];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
let mut data = [0xad];
spi.transfer(&mut data).unwrap();
println!("{:x?}", data);
delay.delay_ms(100u32);
}
}
Conclusion
In this post, a simple SPI loopback application was created leveraging the SPI peripheral for the ESP32C3 and the Wokwi logic analyzer. The SPI code was created at the HAL level using the Rust esp32c3-hal. Additionally, a walkthrough of setting up the Wokwi logic analyzer and decoding signals was also provided. 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:
Posted on June 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.