Skip to content

Commit

Permalink
thermistor
Browse files Browse the repository at this point in the history
  • Loading branch information
ImplFerris committed Nov 17, 2024
1 parent f33185a commit 853339d
Show file tree
Hide file tree
Showing 7 changed files with 303 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,5 @@
- [Non-Linear](./thermistor/non-linear.md)
- [B Equation](./thermistor/b-equation.md)
- [Steinhart Equation](./thermistor/steinhart.md)
- [Temperature on OLED](./thermistor/oled.md)
- [Temperature on OLED](./thermistor/temperature/index.md)
- [Resources](./resources.md)
10 changes: 5 additions & 5 deletions src/thermistor/adc.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Where:
- R2: The resistance based on the ADC value.
- R1: Reference resistor value (typically 10kΩ)
- ADC_MAX: The maximum ADC value is 4095 (\\( 2^{12}\\) -1 ) for a 12-bit ADC
- adc_count: ADC reading (a value between 0 and ADC_MAX).
- adc_value: ADC reading (a value between 0 and ADC_MAX).


### Rust Function
Expand All @@ -32,16 +32,16 @@ Where:
const ADC_MAX: u16 = 4095;
const REF_RES: f64 = 10_000.0;

fn adc_to_resistance(adc_count: u16, ref_res:f64) -> f64 {
let x: f64 = (ADC_MAX as f64/adc_count as f64) - 1.0;
fn adc_to_resistance(adc_value: u16, ref_res:f64) -> f64 {
let x: f64 = (ADC_MAX as f64/adc_value as f64) - 1.0;
// ref_res * x // If you connected thermistor to power supply
ref_res / x
}

fn main() {
let adc_count = 2000; // Our example ADC value;
let adc_value = 2000; // Our example ADC value;

let r2 = adc_to_resistance(adc_count, REF_RES);
let r2 = adc_to_resistance(adc_value, REF_RES);
println!("Calculated Resistance (R2): {} Ω", r2);
}
```
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 0 additions & 9 deletions src/thermistor/oled.md

This file was deleted.

12 changes: 6 additions & 6 deletions src/thermistor/steinhart.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Where:


### Calibration
To determine the accurate values for A, B, and C, place the thermistor in three temperature conditions: room temperature, ice water, and boiling water. For each condition, measure the thermistor's resistance using the ADC count and use a reliable thermometer to record the actual temperature. Using the resistance values and corresponding temperatures, calculate the coefficients:
To determine the accurate values for A, B, and C, place the thermistor in three temperature conditions: room temperature, ice water, and boiling water. For each condition, measure the thermistor's resistance using the ADC value and use a reliable thermometer to record the actual temperature. Using the resistance values and corresponding temperatures, calculate the coefficients:
- Assign A to the ice water temperature,
- B to the room temperature, and
- C to the boiling water temperature.
Expand Down Expand Up @@ -70,12 +70,12 @@ $$

<span style="color: green;">Good news, Everyone!</span> You don't need to calculate the coefficients manually. Simply provide the resistance and temperature values for cold, room, and hot environments, and use the form below to determine A, B and C

### ADC Count and Resistance Calculation
### ADC value and Resistance Calculation
<span style="color: orange;">Note:</span> if you already have the temperature and corresponding resistance, you can directly use the second table to input those values.

If you have the ADC count and want to calculate the resistance, use this table to find the corresponding resistance at different temperatures. As you enter the ADC count for each temperature, the calculated resistance will be automatically updated in the second table.
If you have the ADC value and want to calculate the resistance, use this table to find the corresponding resistance at different temperatures. As you enter the ADC value for each temperature, the calculated resistance will be automatically updated in the second table.

To perform this calculation, you'll need the base resistance of the thermistor, which is essential for determining the resistance at a given temperature based on the ADC count.
To perform this calculation, you'll need the base resistance of the thermistor, which is essential for determining the resistance at a given temperature based on the ADC value.

Please note that the ADC bits may need to be adjusted if you're using a different microcontroller. In our case, for the the Raspberry Pi Pico, the ADC resolution is 12 bits.

Expand All @@ -92,7 +92,7 @@ Please note that the ADC bits may need to be adjusted if you're using a differen
<thead>
<tr>
<th>Environment</th>
<th>ADC Count</th>
<th>ADC value</th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -212,7 +212,7 @@ window.onload = function() {
calculateCoefficients();
};

// Function to calculate resistance based on base resistance and ADC count
// Function to calculate resistance based on base resistance and ADC value
function calculateResistance(baseResistance, adcCount, adcBits) {
const maxADCValue = Math.pow(2, adcBits) - 1; // Max ADC value for the given bits (e.g., 12 bits = 4095)

Expand Down
271 changes: 271 additions & 0 deletions src/thermistor/temperature/action.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
# Action

We'll use the Embassy HAL for this exercise.

## Project from template

To set up the project, run:
```sh
cargo generate --git https://github.com/ImplFerris/pico2-template.git
```
When prompted, give your project a name, like "thermistor" and select `embassy` as the HAL.

Then, navigate into the project folder:
```sh
cd PROJECT_NAME
# For example, if you named your project "thermistor":
# cd thermistor
```

### Additional Crates required
Update your Cargo.toml to add these additional crate along with the existing dependencies.
```rust
embedded-graphics = "0.8.1"
ssd1306 = "0.9.0"
embedded-hal-async = "1.0.0"
heapless = "0.8.0"
libm = "0.2.11"
```

## Interrupt Handler
We have set up only the ADC interrupt handler for the LDR exercises so far. For this exercise, we also need to set up an interrupt handler for I2C to enable communication with the OLED display.

```rust
use embassy_rp::adc::InterruptHandler;

bind_interrupts!(struct Irqs {
ADC_IRQ_FIFO => adc::InterruptHandler;
I2C1_IRQ => i2c::InterruptHandler<I2C1>;
});
```

### ADC related functions
We can hardcode 4095 for the Pico, but here’s a simple function to calculate ADC_MAX based on ADC bits:
```rust
const fn calculate_adc_max(adc_bits: u8) -> u16 {
(1 << adc_bits) - 1
}
const ADC_BITS: u8 = 12; // 12-bit ADC in Pico
const ADC_MAX: u16 = calculate_adc_max(ADC_BITS); // 4095 for 12-bit ADC
```

### Thermistor specific values
The thermistor I’m using has a 10kΩ resistance at 25°C and a B value of 3950.

```rust
const B_VALUE: f64 = 3950.0;
const REF_RES: f64 = 10_000.0; // Reference resistance in ohms (10kΩ)
const REF_TEMP: f64 = 25.0; // Reference temperature 25°C
```

### Helper functions

```rust
// We have already covered about this formula in ADC chpater
fn adc_to_resistance(adc_value: u16, ref_res: f64) -> f64 {
let x: f64 = (ADC_MAX as f64 / adc_value as f64) - 1.0;
// ref_res * x // If you connected thermistor to power supply
ref_res / x
}

// B Equation to convert resistance to temperature
fn calculate_temperature(current_res: f64, ref_res: f64, ref_temp: f64, b_val: f64) -> f64 {
let ln_value = libm::log(current_res / ref_res); // Use libm for `no_std`
let inv_t = (1.0 / ref_temp) + ((1.0 / b_val) * ln_value);
1.0 / inv_t
}

fn kelvin_to_celsius(kelvin: f64) -> f64 {
kelvin - 273.15
}

fn celsius_to_kelvin(celsius: f64) -> f64 {
celsius + 273.15
}
```

### Base setups
First, we set up the Embassy HAL, configure the ADC on GPIO 26, and prepare the I2C interface for communication with the OLED display
```rust
let p = embassy_rp::init(Default::default());
// ADC to read the Vout value
let mut adc = Adc::new(p.ADC, Irqs, adc::Config::default());
let mut p26 = Channel::new_pin(p.PIN_26, Pull::None);

// Setting up I2C send text to OLED display
let sda = p.PIN_18;
let scl = p.PIN_19;
let i2c = i2c::I2c::new_async(p.I2C1, scl, sda, Irqs, i2c::Config::default());
let interface = I2CDisplayInterface::new(i2c);
```

### Setting Up an SSD1306 OLED Display in Terminal Mode
Next, create a display instance, specifying the display size and orientation. And enable terminal mode.
```rust
let mut display =
Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0).into_terminal_mode();
display.init().unwrap();
```

### Heapless String
This is a heapless string set up with a capacity of 64 characters. The string is allocated on the stack, allowing it to hold up to 64 characters. We use this variable to display the temperature, ADC, and resistance values on the screen.
```rust
let mut buff: String<64> = String::new();
```

### Convert the Reference Temperature to Kelvin
We defined the reference temperature as 25°C for the thermistor. However, for the equation, we need the temperature in Kelvin. To handle this, we use a helper function to perform the conversion. Alternatively, you could directly hardcode the Kelvin value (298.15 K, which is 273.15 + 25°C) to skip using the function.
```rust
let ref_temp = celsius_to_kelvin(REF_TEMP);
```

### Read ADC
We read the ADC value; we also put into the buffer.
```rust
let adc_value = adc.read(&mut p26).await.unwrap();
writeln!(buff, "ADC: {}", adc_value).unwrap();
```

### ADC To Resistance
We convert the ADC To resistance; we put this also into the buffer.
```rust
let current_res = adc_to_resistance(adc_value, REF_RES);
writeln!(buff, "R: {:.2}", current_res).unwrap();
```

### Calculate Temperature from Resistance
We use the measured resistance to calculate the temperature in Kelvin using the B-parameter equation.Afterward, we convert the temperature from Kelvin to Celsius.
```rust
let temperature_kelvin = calculate_temperature(current_res, REF_RES, ref_temp, B_VALUE);
let temperature_celsius = kelvin_to_celsius(temperature_kelvin);
```

### Write the Buffer to Display
```rust
writeln!(buff, "Temp: {:.2} °C", temperature_celsius).unwrap();
display.write_str(&buff).unwrap();
Timer::after_secs(1).await;
```

### Final code
```rust
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_rp::block::ImageDef;
use embassy_rp::{self as hal, gpio::Pull};
use embassy_time::Timer;
use heapless::String;
use {defmt_rtt as _, panic_probe as _};

use ssd1306::mode::DisplayConfig;
use ssd1306::prelude::DisplayRotation;
use ssd1306::size::DisplaySize128x64;
use ssd1306::{I2CDisplayInterface, Ssd1306};

use embassy_rp::adc::{Adc, Channel, InterruptHandler};
use embassy_rp::peripherals::I2C1;
use embassy_rp::{adc, bind_interrupts, i2c};

use core::fmt::Write;

/// Tell the Boot ROM about our application
#[link_section = ".start_block"]
#[used]
pub static IMAGE_DEF: ImageDef = hal::block::ImageDef::secure_exe();

bind_interrupts!(struct Irqs {
ADC_IRQ_FIFO => InterruptHandler;
I2C1_IRQ => i2c::InterruptHandler<I2C1>;
});

// Constants for ADC calculations
const fn calculate_adc_max(adc_bits: u8) -> u16 {
(1 << adc_bits) - 1
}
const ADC_BITS: u8 = 12; // 12-bit ADC in Pico
const ADC_MAX: u16 = calculate_adc_max(ADC_BITS); // 4095 for 12-bit ADC

const B_VALUE: f64 = 3950.0;
const REF_RES: f64 = 10_000.0; // Reference resistance in ohms (10kΩ)
const REF_TEMP: f64 = 25.0; // Reference temperature 25°C

fn adc_to_resistance(adc_value: u16, ref_res: f64) -> f64 {
let x: f64 = (ADC_MAX as f64 / adc_value as f64) - 1.0;
ref_res / x
}

fn calculate_temperature(current_res: f64, ref_res: f64, ref_temp: f64, b_val: f64) -> f64 {
let ln_value = libm::log(current_res / ref_res); // Use libm for `no_std`
let inv_t = (1.0 / ref_temp) + ((1.0 / b_val) * ln_value);
1.0 / inv_t
}

fn kelvin_to_celsius(kelvin: f64) -> f64 {
kelvin - 273.15
}

fn celsius_to_kelvin(celsius: f64) -> f64 {
celsius + 273.15
}

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut adc = Adc::new(p.ADC, Irqs, adc::Config::default());

let mut p26 = Channel::new_pin(p.PIN_26, Pull::None);

let sda = p.PIN_18;
let scl = p.PIN_19;
let i2c = i2c::I2c::new_async(p.I2C1, scl, sda, Irqs, i2c::Config::default());
let interface = I2CDisplayInterface::new(i2c);

let mut display =
Ssd1306::new(interface, DisplaySize128x64, DisplayRotation::Rotate0).into_terminal_mode();
display.init().unwrap();

let mut buff: String<64> = String::new();
let ref_temp = celsius_to_kelvin(REF_TEMP);

loop {
buff.clear();
display.clear().unwrap();

let adc_value = adc.read(&mut p26).await.unwrap();
writeln!(buff, "ADC: {}", adc_value).unwrap();

let current_res = adc_to_resistance(adc_value, REF_RES);
writeln!(buff, "R: {:.2}", current_res).unwrap();

let temperature_kelvin = calculate_temperature(current_res, REF_RES, ref_temp, B_VALUE);
let temperature_celsius = kelvin_to_celsius(temperature_kelvin);

writeln!(buff, "Temp: {:.2} °C", temperature_celsius).unwrap();
display.write_str(&buff).unwrap();
Timer::after_secs(1).await;
}
}

// Program metadata for `picotool info`
#[link_section = ".bi_entries"]
#[used]
pub static PICOTOOL_ENTRIES: [embassy_rp::binary_info::EntryAddr; 4] = [
embassy_rp::binary_info::rp_program_name!(c"Blinky Example"),
embassy_rp::binary_info::rp_program_description!(
c"This example tests the RP Pico on-board ADC with a thermistor"
),
embassy_rp::binary_info::rp_cargo_version!(),
embassy_rp::binary_info::rp_program_build_attribute!(),
];
```


## Clone the existing project
You can clone (or refer) project I created and navigate to the `thermistor` folder.

```sh
git clone https://github.com/ImplFerris/pico2-projects
cd pico2-projects/thermistor/
```
20 changes: 20 additions & 0 deletions src/thermistor/temperature/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Temperature on OLED

In this section, we'll calculate the temperature using the B equation and display it on an OLED screen.

## Hardware Requirments

- An OLED display: (0.96 Inch I2C/IIC 4-Pin, 128x64 resolution, SSD1306 chip)
- Jumper wires
- NTC 103 Thermistor: 10K OHM, 5mm epoxy coated disc
- 10kΩ Resistor: Used with the thermistor to form a voltage divider

## Circuit to connect OLED, Thermistor with Raspberry Pi Pico

<img style="display: block; margin: auto;" alt="pico2" src="../images/thermistor-pico-oled-circuit.jpg"/>

1. **One side of the Thermistor** is connected to **AGND** (Analog Ground).
2. The **other side of the Thermistor** is connected to **GPIO26 (ADC0)**, which is the analog input pin of the pico2
3. A **resistor** is connected in series with the Thermistor to create a voltage divider between the Thermistor and **ADC_VREF** (the reference voltage for the ADC).

<span style="color:orange">Note:</span> In this example, one side of the thermistor is connected to ground, as shown. If you've connected it to the power supply instead, you'll need to use the alternate formula mentioned earlier.

0 comments on commit 853339d

Please sign in to comment.