Action
We'll use the Embassy HAL for this exercise.
Project from template
To set up the project, run:
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:
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.
#![allow(unused)] fn main() { ssd1306 = "0.9.0" heapless = "0.8.0" libm = "0.2.11" }
ssd1306
: Driver for controlling SSD1306 OLED display.heapless
: In ano_std
environment, Rust's standardString
type (which requires heap allocation) is unavailable. This provides stack-allocated, fixed-size data structures. We will be using to store dynamic text, such as ADC, resistance, and temperature values, for display on the OLED screenlibm
: Provides essential mathematical functions for embedded environments. We need this to calculate natural logarithm.
Additional imports
#![allow(unused)] fn main() { use heapless::String; use ssd1306::mode::DisplayConfig; use ssd1306::prelude::DisplayRotation; use ssd1306::size::DisplaySize128x64; use ssd1306::{I2CDisplayInterface, Ssd1306}; use embassy_rp::adc::{Adc, Channel}; use embassy_rp::peripherals::I2C1; use embassy_rp::{adc, bind_interrupts, i2c}; use embassy_rp::gpio::Pull; use core::fmt::Write; }
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.
#![allow(unused)] fn main() { 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:
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { // 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
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { let ref_temp = celsius_to_kelvin(REF_TEMP); }
Loop
In a loop that runs every 1 second(adjust as you require), we read the ADC value, calculate the resistance from ADC, then derive the temperature from resistance, and display the results on the OLED.
Read ADC
We read the ADC value; we also put into the buffer.
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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.
#![allow(unused)] fn main() { 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
#![allow(unused)] fn main() { writeln!(buff, "Temp: {:.2} °C", temperature_celsius).unwrap(); display.write_str(&buff).unwrap(); Timer::after_secs(1).await; }
Clear the Buffer and Screen
#![allow(unused)] fn main() { buff.clear(); display.clear().unwrap(); }
Final code
#![no_std] #![no_main] use embassy_executor::Spawner; use embassy_rp as hal; use embassy_rp::block::ImageDef; use embassy_rp::gpio::Pull; use embassy_time::Timer; use heapless::String; use ssd1306::mode::DisplayConfig; use ssd1306::prelude::DisplayRotation; use ssd1306::size::DisplaySize128x64; use ssd1306::{I2CDisplayInterface, Ssd1306}; use {defmt_rtt as _, panic_probe as _}; use embassy_rp::adc::{Adc, Channel}; 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 => adc::InterruptHandler; I2C1_IRQ => i2c::InterruptHandler<I2C1>; }); 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 // 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 } #[embassy_executor::main] async fn main(_spawner: Spawner) { 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); 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`. // This isn't needed, but it's recomended to have these minimal entries. #[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 LED, connected to gpio 25" ), embassy_rp::binary_info::rp_cargo_version!(), embassy_rp::binary_info::rp_program_build_attribute!(), ]; // End of file
Clone the existing project
You can clone (or refer) project I created and navigate to the thermistor
folder.
git clone https://github.com/ImplFerris/pico2-embassy-projects
cd pico2-embassy-projects/thermistor/