ESP32 Standard Library Embedded Rust: Analog Temperature Sensing using the ADC
Tags: rust, tutorial, beginners, esp32
Published: 2023-08-18 06:11:43

> ***This blog post is the sixth of a multi-part series of posts where I explore various peripherals in the ESP32C3 using standard library embedded Rust and the esp-idf-hal. 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:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%}  

## Introduction

NTC thermistors, or Negative Temperature Coefficient thermistors, are temperature-sensitive resistors made from materials that exhibit a change in resistance with temperature variations. As the temperature increases, the resistance of an NTC thermistor decreases (thus the negative naming), and conversely, when the temperature decreases, the resistance increases. This unique property makes NTC thermistors invaluable in temperature measurement and control applications. They are widely used as temperature sensors in electronic circuits, household appliances, automotive systems, industrial processes, and medical devices. NTC thermistors enable accurate temperature monitoring, providing feedback for maintaining stable operating conditions, triggering alarms, and regulating various processes with high precision.

In this post, I will be configuring and setting up an esp32c3-hal ADC using the `esp-idf-hal` to measure ambient temperature using a 10k (NTC) Thermistor. The ADC collected value will be converted to temperature and sent to the terminal output.

### 📚 Knowledge Pre-requisites

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

* Basic knowledge of coding in Rust.
    
* Familiarity with the working principles of NTC Thermistors. [This](https://www.electronics-tutorials.ws/io/thermistors.html) page is a good resource.
    

### **💾** Software Setup

All the code presented in this post is available on the [**apollolabs ESP32C3**](https://github.com/apollolabsdev/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**](https://wokwi.com/projects/372530583695095809).

### **🛠** Hardware Setup

#### Materials

* [**ESP32-C3-DevKitM**](https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html)
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1681796942083/da81fb7b-1f90-4593-a848-11c53a87821d.jpeg?auto=compress,format&format=webp&auto=compress,format&format=webp&auto=compress,format&format=webp align="center")
    
* [A 10k NTC Temperature Sensor](https://s.click.aliexpress.com/e/_Dmuik9v).
    
* A 10 KOhm Resistor.
    
* If you want to wire real hardware, instead of getting the resistor and sensor separately, you can get assemblies that contain the needed setup like [this one](https://www.aliexpress.us/item/2251832596117980.html?spm=a2g0o.productlist.main.25.326f6cb7iji0Bv&algo_pvid=caecce9e-0a98-491a-a0b5-f5c15b66cd47&algo_exp_id=caecce9e-0a98-491a-a0b5-f5c15b66cd47-12&pdp_npi=4%40dis%21USD%210.75%210.75%21%21%210.75%21%21%40211b88f016923354037702773e1e02%2163076953666%21sea%21US%214083593659%21A&curPageLogUid=WGeMUOyNwKP7).
    

#### ⚡ Connections

* Temperature sensor signal pin connected to pin gpio4. In Wokwi this is a direct connection. However, if you have the individual NTC component (not integrated on a board), you need to set it up in a voltage divider configuration with a 10K Ohm 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qu2nv3nmn56gvdaveq4w.png)

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 {% katex inline %} V_{\+} {% endkatex %} is equal to the voltage on the signal terminal and expressed as:


{% katex %}V_{\text{+}} = V_{cc}* \frac{R_{1}}{R_{1}} + R_{\text{NTC}}{% endkatex %}

Where {% katex inline %}R_1 = 10k\Omega{% endkatex %} and the resistance value of {% katex inline %}(R_{\text{NTC}}{% endkatex %} 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 {% katex inline %}R_{\text{NTC}}{% endkatex %} from the {% katex inline %}V_{\text{+}}{% endkatex %} 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:

{% katex %}R_{\text{NTC}} = \left( \frac{ V_{cc} }{ V_{\text{+}} } -1 \right) * R_{1}{% endkatex %}

After extracting the value of {% katex inline %}R_{\text{NTC}}{% endkatex %}, 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:

{% katex %}\beta = \frac{ln(\frac{R\_{\text{NTC}}}{R_0})}{(\frac{1}{T}-\frac{1}{T_0})}{% endkatex %}

where {% katex inline %} \beta {% endkatex %} is a constant and equal to 3950 for our NTC as stated by [Wokwi](https://docs.wokwi.com/parts/wokwi-ntc-temperature-sensor) and {% katex inline %} T {% endkatex %} is the temperature we are measuring. {% katex inline %} T_0 {% endkatex %} and {% katex inline %} R_0 {% endkatex %} refer to the ambient temperature (typically 25 Celcius) and nominal resistance at ambient temperature, respectively. The value of the resistance at 25 Celcius ( {% katex inline %} T_0 {% endkatex %} ) is equal to {% katex inline %}10k\Omega{% endkatex %} ( {% katex inline %} R_0 {% endkatex %} ). With more algebraic manipulation we solve for {% katex inline %} T {% endkatex %} to get:

{% katex %}T = \frac{1}{\frac{1}{\beta} * ln(\frac{R\_{\text{NTC}}}{R_0}) +\frac{1}{T_0}}{% endkatex %}

Take note that the {% katex inline %} \beta {% endkatex %} value is typically obtained by manufacturers based on Kelvin temperatures. As such, when doing the calculations we'd have to convert back to Celcius.

## **👨‍🎨** 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 esp_idf_hal crate to import the needed device hardware abstractions.
    
* The `libm` crate to provide an implementation for a natural logarithm.
    

```rust
use esp_idf_hal::adc::config::Config;
use esp_idf_hal::adc::*;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::gpio::Gpio4;
use libm::log;
```

### **🎛** 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 device peripherals. This is done using the `take()` method. Here I create a device peripheral handler named `peripherals` as follows:

```rust
let peripherals = Peripherals::take().unwrap();
```

**2️⃣ Configure and Obtain a handle for the ADC driver**: The ADC is configured in two steps in the `esp-idf-hal` first by setting up the `AdcDriver` that configures the ADC, and second setting up the channel that will configure the channel/pin settings. From the [ESP32C3 reference manual](https://www.espressif.com/sites/default/files/documentation/esp32-c3_technical_reference_manual_en.pdf), the following table exists:

![ADC Channels](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pdn89lzeuf8jr898q90b.png)

The table shows which ADC channels are connected to which ADC. In this case, since we are using `gpio4` , then ADC1 is the one we should be configuring. To configure ADC1, there exists the `AdcDriver` `new` method that allows us to create an instance of a configured ADC. The `new` method takes two parameters which are the ADC peripheral instance (`adc1` according to the table above) and a configuration instance. As such I create an `adc` handle as follows:

```rust
let mut adc = AdcDriver::new(peripherals.adc1, &Config::new()).unwrap();
```

`Config` comes from the `esp_idf_hal::adc::config` module and contains configuration information like the resolution of the ADC. By default, the resolution used is 12-bits.

**3️⃣Configure and Obtain a handle for the ADC channel**: At this point, we configured the ADC but not yet the pin/channel. This is done using the `AdcChannelDriver` `new` method. The `new` method takes only one argument, which is an instance of a pin. I create an `adc_pin` handle that represents a configured channel as follows:

```rust
let mut adc_pin: esp_idf_hal::adc::AdcChannelDriver<'_, Gpio4, Atten11dB<_>> =
        AdcChannelDriver::new(peripherals.pins.gpio4).unwrap();
```

Note how an attenuation `Atten11dB` is specified in the type parameters. This is necessary to define the attenuation level of the pin and thus the voltage range the pin will be able to measure. The [documentation](https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32c3/api-reference/peripherals/adc.html) specifies four different attenuation levels of which the 11dB level supports the range of 0 mV ~ 2500 mV which is what we're going to use.

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 `VMAX` as follows:

```rust
const B: f64 = 3950.0; // B value of the thermistor
const VMAX: f64 = 2500.0; // Full Range Voltage
```

#### 🔁 The Application Loop

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`:

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

Note here that the `read` method in the `esp-idf-hal`, unlike the `read` method in the `no_std` libraries returns a voltage, not a raw value. Next, I convert the sample value to a temperature by implementing the earlier derived equations as follows:

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

A few things to note here; First, 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. Second, `log` is the natural logarithm obtained from the `libm` library that I imported earlier. Third, and last, the temperature is calculated in Kelvins, the `273.15` is what converts it to Celcius. Like mentioned earlier the Beta parameter is determined based on Kelvins.

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

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

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**](https://github.com/apollolabsdev/ESP32C3) git repo. Also, the Wokwi project can be accessed [**here**](https://wokwi.com/projects/373204441677131777).

```rust
/*
For a detailed explanation of this code check out the associated blog post:


GitHub Repo containing source code and other examples:
https://github.com/apollolabsdev

For notifications on similar examples and more, subscribe to newsletter here:
https://subscribepage.io/apollolabsnewsletter
*/

use esp_idf_sys::{self as _}; // If using the `binstart` feature of `esp-idf-sys`, always keep this module imported

use esp_idf_hal::adc::config::Config;
use esp_idf_hal::adc::*;
use esp_idf_hal::peripherals::Peripherals;
use esp_idf_hal::gpio::Gpio4;
use libm::log;

fn main() -> anyhow::Result<()> {
    let peripherals = Peripherals::take().unwrap();

    // Configure ADC Driver
    let mut adc = AdcDriver::new(peripherals.adc1, &Config::new()).unwrap();

    // Configure ADC Channel
    let mut adc_pin: esp_idf_hal::adc::AdcChannelDriver<'_, Gpio4, Atten11dB<_>> =
        AdcChannelDriver::new(peripherals.pins.gpio4).unwrap();

    const B: f64 = 3950.0; // B value of the thermistor
    const VMAX: f64 = 2500.0; // Full Range Voltage

    // Algorithm
    // 1) Get adc reading
    // 2) Convert to temperature
    // 3) Send over Serial
    // 4) Go Back to step 1

    loop {
        // Get ADC Reading
        let sample: u16 = adc.read(&mut adc_pin).unwrap();

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

        // Print the temperature output
        println!("Temperature {:02} Celcius\r", temperature);
    }
}
```

## Conclusion

In this post, an analog temperature measurement application was created by leveraging the `esp-idf-hal` ADC peripheral for the ESP32C3. The resulting measurement is also sent over to terminal output. 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 if Embedded Rust interests you, stay in the know and skyrocket your learning curve by subscribing to The Embedded Rustacean newsletter:

{% cta  http://www.theembeddedrustacean.com %} Subscribe Now to The Embedded Rustacean{% endcta%}