Startec

Startec

ESP32 Embedded Rust at the HAL: Analog Temperature Sensing using the ADC

Mai 25, às 16:21

·

14 min de leitura

·

0 leituras

This blog post is the sixth 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...
ESP32 Embedded Rust at the HAL: Analog Temperature Sensing using the ADC

This blog post is the sixth 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 to keep up to date with similar posts, here's the list of channels you can follow/subscribe to:

Introduction

In this post, I will be configuring and setting up an esp32c3-hal ADC to measure ambient temperature using a 10k NTC Thermistor. Temperature measurements will be continuously collected and sent to the terminal output. For terminal output, I will be leveraging the esp-println crate I started using in the last post. Additionally, I will not be using any interrupts and the example will be set up as a simplex system that transmits in one direction only (towards the terminal/PC).

📚 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 working principles of NTC Thermistors. This page is a good resource.

💾 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.

In addition to the above, you would need to install some sort of serial communication terminal on your host PC. Some recommendations include:

For Windows:

For Mac and Linux:

Some installation instructions for the different operating systems are available in the Discovery Book.

🛠 Hardware Setup

Materials

ESP32C3 Devkit

  • A 10k NTC Temperature Sensor.

⚡ Connections

  • Temperature sensor signal pin connected to pin gpio1. In Wokwi this is a direct connection. However, if you have the individual NTC component, you need to set it up in a voltage divider configuration with a 10K resistor (circuit in next section).

🔌 Circuit Analysis

The temperature sensor used is a negative temperature coefficient (NTC) sensor. This means the resistance of the sensor increases as the temperature increases. The following figure shows the schematic of the temperature sensor circuit:

NTC Circuit

It is shown that the NTC Thermistor is connected in a voltage divider configuration with a 10k resistor. As such, the voltage at the positive terminal of the op-amp V+V_{+} is equal to the voltage on the signal terminal and expressed as:

V+=Vcc∗R1R1+RNTCV_{\text{+}} = V_{cc}* \frac{R_{1}}{R_{1}} + R_{\text{NTC}}

Where R1=10kΩR_1 = 10k\Omega and the resistance value of (RNTC(R_{\text{NTC}} is the one that needs to be calculated to obtain the temperature. This means that later in the code, I would need to retrieve back the value of RNTCR_{\text{NTC}} from the V+V_{\text{+}} value that is being read by the ADC. With some algebraic manipulation, we can move all the known variables to the right-hand side of the equation to reach the following expression:

RNTC=(VccV+−1)∗R1R_{\text{NTC}} = \left( \frac{ V_{cc} }{ V_{\text{+}} } -1 \right) * R_{1}

After extracting the value of RNTCR_{\text{NTC}} , I would need to determine the temperature. Following the equations in the datasheet, I leverage the Steinhart-Hart NTC equation that is presented as follows:

β=ln(RNTCR0)(1T−1T0)\beta = \frac{ln(\frac{R_{\text{NTC}}}{R_0})}{(\frac{1}{T}-\frac{1}{T_0})}

where β\beta is a constant and equal to 3950 for our NTC as stated by Wokwi and TT is the temperature we are measuring. T0T_0 and R0R_0 refer to the ambient temperature (typically 25 Celcius) and nominal resistance at ambient temperature, respectively. The value of the resistance at 25 Celcius ( T0T_0 ) is equal to 10kΩ10k\Omega ( R0R_0 ). With more algebraic manipulation we solve for TT to get:

T=11β∗ln(RNTCR0)+1T0T = \frac{1}{\frac{1}{\beta} * ln(\frac{R_{\text{NTC}}}{R_0}) +\frac{1}{T_0}}

👨‍🎨 Software Design

Now that we know the equations from the prior section, an algorithm needs to be developed and is quite straightforward in this case. After configuring the device, the algorithmic steps are as follows:

  1. Kick off the ADC and obtain a reading/sample.

  2. Calculate the temperature in Celcius.

  3. Print the temperature value on the terminal.

  4. Go back to step 1.

👨‍💻Code Implementation

📥 Crate Imports

In this implementation, the following crates are required:

  • 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.

  • The libm crate to provide an implementation for a natural logarithm.

use esp32c3_hal::{
 clock::ClockControl, peripherals::Peripherals, prelude::*, systimer::SystemTimer,
 timer::TimerGroup, Delay, Rtc, IO,
};
use esp_backtrace as _;
use esp_println::println;
use libm::log;

Enter fullscreen mode Exit fullscreen mode

🎛 Initialization/Configuration Code

⌨️ GPIO Peripheral Configuration:

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. To avoid this issue, the following code needs to be included:

let 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);
let mut wdt0 = timer_group0.wdt;
let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
let mut wdt1 = timer_group1.wdt;

Enter fullscreen mode Exit fullscreen mode

3️⃣ Instantiate and Create Handle for IO: We need to configure the NTC pin as an analog input and obtain a handler for the pin so that we can control it. This will be done in the following step. Though before we can obtain any handles for the NTC 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);

Enter fullscreen mode Exit fullscreen mode

4️⃣ Configure and Create Handle for Analog Pin: Similar to how pins were configured before with gpio, there is instead an into_analog() method that configures the pin as an analog pin. An ntc_pin handle is created to gpio1 to and analog pin as follows:

let ntc = io.pins.gpio1.into_analog();

Enter fullscreen mode Exit fullscreen mode

ADC Peripheral Configuration:

1️⃣ Obtain a handle for ADC configuration: To configure an analog pin in the esp32c3-hal, first, an ADC configuration instance needs to be created. The same configuration instance is then later used to both enable the analog pin and create an ADC instance. As such, an adc_config handle is created using the AdcConfig type new method as follows:

// Create handle for ADC configuration parameters
let mut adc_config = AdcConfig::new();

Enter fullscreen mode Exit fullscreen mode

2️⃣ Obtain a handle and enable the analog pin: In order to enable the analog ntc_pin pin, the AdcConfig type has an enable_pin method that takes two arguments. The first argument is the analog gpio pin, and the second is an Attenuation enum specifying the desired level of attenuation:

let mut adc_pin =
 adc_config.enable_pin(
 ntc,
 Attenuation::Attenuation0dB
 );

Enter fullscreen mode Exit fullscreen mode

3️⃣ Obtain a handle and Configure an ADC instance: Before creating an ADC instance, similar to some other peripherals, the peripheral needs to be promoted to HAL-level structs. This is done using the split method on the APB_SARADC peripheral type (if not familiar, read this past post of mine explaining split and constrain methods) as follows:

// Promote ADC peripheral to HAL-level Struct
let analog = peripherals.APB_SARADC.split();

Enter fullscreen mode Exit fullscreen mode

Now that the peripheral is split, we have access to the individual ADC to pass to an ADC instance. As a result, to create an ADC instance there is an adc method as part of the ADC type in the esp32c3-hal. The adc method takes three arguments, a peripheral clock controller instance (accessed via the system handle), an ADC instance (accessed via the analog handle), and an ADC configuration instance (the adc_config handle):

let mut adc = ADC::adc(
 &mut system.peripheral_clock_control,
 analog.adc1,
 adc_config,
)
.unwrap();

Enter fullscreen mode Exit fullscreen mode

This is it for configuration! Let's now jump into the application code.

📱Application Code

Following the design described earlier, before entering my loop, I first need to set up a couple of constants that I will be using in my calculations. This includes keying in the constant values for β\beta and R0R_0 as follows:

const B: f64 = 3950.0; // B value of the thermistor
const R0: f64 = 10000.0; // Nominal NTC Value

Enter fullscreen mode Exit fullscreen mode

After entering the program loop, as the software design stated earlier, first thing I need to do is kick off the ADC to obtain a sample/reading. This is done through the read method that takes a mutable reference to the adc_pin instance and returns a Result:

 let sample: u16 = adc.read(&mut adc_pin).unwrap();

Enter fullscreen mode Exit fullscreen mode

Next, I convert the sample value to a temperature by implementing the earlier derived equations as follows:

let temperature = 1. / (log(1. / (4096. / sample as f64 - 1.)) / B + 1.0 / 298.15) - 273.15;

Enter fullscreen mode Exit fullscreen mode

A few things to note here; first I don't convert the collected sample to value to a voltage as in the first calculation the voltage calculation is a ratio. This means I keep the sample in LSBs and use the equivalent LSB value for VccV_{cc} . To plug in VccV_{cc} I simply calculate the maximum possible LSB value (upper reference) that can be generated by the ADC. This is why I needed to know the resolution, which was 12 because Vcc=212LSBsV_{cc} = 2^{12} LSBs . Second, recall from the read method that sample is a u16, so I had to use as f64 to cast it as an f64 for the calculation. Third, log is the natural logarithm obtained from the libm library that I imported earlier. Fourth, and last, the temperature is calculated in Kelvins, the 273.15 is what converts it to Celcius.

Finally, now that the temperature is available, I send it over to the console using the println! macro as follows:

println!("Temperature {:02} Celcius\r", temperature);

Enter fullscreen mode Exit fullscreen mode

This is it!

📱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::{
 adc::{AdcConfig, Attenuation, ADC},
 clock::ClockControl,
 peripherals::Peripherals,
 prelude::*,
 timer::TimerGroup,
 Rtc, IO,
};
use esp_backtrace as _;
use esp_println::println;
use libm::log;
#[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);
 let mut wdt0 = timer_group0.wdt;
 let timer_group1 = TimerGroup::new(peripherals.TIMG1, &clocks);
 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);
 // Create ADC Instance
 // Create handle for ADC configuration parameters
 let mut adc_config = AdcConfig::new();
 // Configure ADC pin
 let mut adc_pin =
 adc_config.enable_pin(io.pins.gpio1.into_analog(), Attenuation::Attenuation0dB);
 // Promote ADC peripheral to HAL-level Struct
 let analog = peripherals.APB_SARADC.split();
 // Create handle for ADC, configuring clock, and passing configuration handle
 let mut adc = ADC::adc(
 &mut system.peripheral_clock_control,
 analog.adc1,
 adc_config,
 )
 .unwrap();
 const B: f64 = 3950.0; // B value of the thermistor
 const R0: f64 = 10000.0; // Nominal NTC Value
 // Algorithm
 // 1) Get adc reading
 // 2) Convert to temperature
 // 3) Send over Serial
 // 4) Go Back to step 1
 // Application
 loop {
 // Get ADC reading
 let sample: u16 = adc.read(&mut adc_pin).unwrap();
 // For blocking read
 // let sample: u16 = nb::block!(adc.read(&mut adc_pin)).unwrap();
 //Convert to temperature
 let temperature = 1. / (log(1. / (4096. / sample as f64 - 1.)) / B + 1.0 / 298.15) - 273.15;
 // Print the temperature output
 println!("Temperature {:02} Celcius\r", temperature);
 }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, an analog temperature measurement application was created leveraging the ADC peripheral for the ESP32C3. The resulting measurement is also sent over to terminal output. All code was based on polling (without interrupts). Additionally, all code was created at the HAL level using the esp32c3-hal. Have any questions? Share your thoughts in the comments below 👇.

If you found this post useful, and to keep up to date with similar posts, here's the list of channels you can follow/subscribe to:


Continue lendo

Showmetech

Motorola Razr Plus é o novo dobrável rival do Galaxy Z Flip
Após duas tentativas da Motorola em emplacar — novamente — telefones dobráveis, eis que temos aqui a terceira, e aparentemente bem-vinda, tentativa. Estamos falando do Motorola Razr Plus, um smartphone...

Hoje, às 15:20

DEV

Mentoring for the LGBTQ+ Community
Once unpublished, all posts by chetanan will become hidden and only accessible to themselves. If chetanan is not suspended, they can still re-publish their posts from their dashboard. Note: Once...

Hoje, às 15:13

TabNews

IA: mais um arrependido / Déficit de TI / Apple: acusação grave · NewsletterOficial
Mais um pioneiro da IA se arrepende de seu trabalho: Yoshua Bengio teria priorizado segurança em vez de utilidade se soubesse o ritmo em que a tecnologia evoluiria – ele junta-se a Geoffr...

Hoje, às 14:37

Hacker News

The Analog Thing: Analog Computing for the Future
THE ANALOG THING (THAT) THE ANALOG THING (THAT) is a high-quality, low-cost, open-source, and not-for-profit cutting-edge analog computer. THAT allows modeling dynamic systems with great speed,...

Hoje, às 14:25

TabNews

[DISCUSÃO/OPINIÕES] – Outsourcing! O que, para quem, por que sim, por que não! · dougg
Quero tentar trazer nesta minha primeira publicação, uma mistura de um breve esclarecimento sobre o que são empresas de outsourcing, como elas funcionam e ganham dinheiro, mas também, ven...

Hoje, às 13:58

TabNews

Duvida: JavaScript - Desenvolver uma aplicação que vai ler um arquivo *.json · RafaelMesquita
Bom dia a todos Estou estudando javascript e me deparei com uma dificuldade e preciso de ajuda *Objetivo do estudo: *desenvolver uma aplicação que vai ler um arquivo *.json Conteudo do in...

Hoje, às 13:43

Showmetech

Automatize suas negociações com um robô de criptomoedas
Índice Como o robô de criptomoedas Bitsgap funciona?Qual a vantagem de utilizar um robô de criptomoedas?Bitsgap é confiável? O mercado de trading tem se tornado cada vez mais popular e as possibilidades de...

Hoje, às 13:13

Hacker News

Sketch of a Post-ORM
I’ve been writing a lot of database access code as of late. It’s frustrating that in 2023, my choices are still to either write all of the boilerplate by hand, or hand all database access over to some...

Hoje, às 13:11

Showmetech

14 chuveiros elétricos para o banho dos seus sonhos
Índice Chuveiro ou Ducha?Tipos de chuveiro elétrico9 fatores importantes para considerar na hora de comprar chuveiros elétricosMelhores chuveiros elétricosDuo Shower LorenzettiFit HydraAcqua Storm Ultra...

Hoje, às 11:00

DEV

Learn about the difference between var, let, and const keywords in JavaScript and when to use them.
var, let, and const: What's the Difference in JavaScript? JavaScript is a dynamic and flexible language that allows you to declare variables in different ways. You can use var, let, or const keywords to...

Hoje, às 10:21