Write Embedded Rust code to Display Temperature on OLED Display
In this section, we move to the coding part. We write the code that reads the thermistor value using the ADC, converts it into temperature using the B equation, and displays the result on the OLED over I2C.
Project from template
Generate a new project using the custom Embassy template.
cargo generate --git https://github.com/ImplFerris/rp2040-embassy-template.git --tag v0.1.4
Additional Crates required
We need a few additional crates to support the OLED display, format text, and mathematical operations. Add the following entries to Cargo.toml along with the existing dependencies.
#![allow(unused)]
fn main() {
ssd1306 = { version = "0.10.0", features = ["async"] }
heapless = "0.9.2"
libm = "0.2.15"
embedded-graphics = "0.8"
}
-
ssd1306: Driver crate for controlling SSD1306-based OLED displays. -
heapless: In a no_std environment, Rust’s standard String type is not available because it requires heap allocation. This crate provides stack-allocated, fixed-size data structures. We use it to store formatted text such as ADC values, resistance, and temperature before sending them to the OLED. -
libm: Provides mathematical functions for no_std environments. This is required to compute the natural logarithm when using the B equation. -
embedded-graphics:
The SSD1306 driver supports different ways of writing content to the display.When you use
into_buffered_graphics_mode, the display is treated like a pixel buffer. Text and shapes are first drawn into an in-memory framebuffer using the embedded-graphics API, and then the whole buffer is sent to the OLED. This mode requires the embedded-graphics crate.When you use
into_terminal_mode, the driver provides a simple text-based interface. You write characters directly to the display without drawing pixels or shapes yourself. In this case, embedded-graphics is not required.
Additional imports
#![allow(unused)]
fn main() {
// Text formatting without heap allocation
use core::fmt::Write;
use heapless::String;
// For OLED display
use ssd1306::{I2CDisplayInterface, Ssd1306Async, prelude::*};
// For ADC
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::gpio::Pull;
// Interrupt Binding
use embassy_rp::bind_interrupts;
use embassy_rp::peripherals::I2C0;
use embassy_rp::{adc, i2c};
// I2C
use embassy_rp::i2c::{Config as I2cConfig, I2c};
// Embedded Graphics
use embedded_graphics::{
mono_font::{MonoTextStyle, iso_8859_13::FONT_7X13_BOLD},
pixelcolor::BinaryColor,
prelude::*,
text::Text,
};
}
Interrupt Handler
In this project, we use both the ADC and I2C peripherals. Each of these peripherals generate interrupts, and Embassy requires that those interrupts are explicitly bound at compile time.
#![allow(unused)]
fn main() {
bind_interrupts!(struct Irqs {
ADC_IRQ_FIFO => adc::InterruptHandler;
I2C0_IRQ => i2c::InterruptHandler<I2C0>;
});
}
ADC_IRQ_FIFO is the interrupt generated by the ADC when data is available in its FIFO. This interrupt is required for ADC operation in Embassy. I2C0_IRQ is the interrupt used by the I2C0 peripheral. This interrupt is required for asynchronous I2C communication with the OLED display.
Thermistor Constants
We define a few constants that describe the thermistor and ADC behavior.
#![allow(unused)]
fn main() {
const ADC_LEVELS: f64 = 4096.0;
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
}
The thermistor we used has a resistance of 10 kΩ at 25°C and a B value of 3950. The pico has a 12-bit ADC resolution, so 4096 possible ADC levels.
Helper functions
We will define few helper functions.
This function that converts ADC values into resistance uses the voltage divider equation. We have already covered this formula earlier, so here we simply reuse it.
#![allow(unused)]
fn main() {
// We have already covered about this formula in ADC chapter
fn adc_to_resistance(adc_value: u16, r2_res: f64) -> f64 {
let adc = adc_value as f64;
((ADC_LEVELS / adc) - 1.0) * r2_res
}
}
This function that converts resistance into temperature applies the B equation. Because we are in a no_std environment, we use the libm crate to compute the natural logarithm.
#![allow(unused)]
fn main() {
// 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
}
}
We also define small helper functions to convert between Kelvin and Celsius.
#![allow(unused)]
fn main() {
fn kelvin_to_celsius(kelvin: f64) -> f64 {
kelvin - 273.15
}
fn celsius_to_kelvin(celsius: f64) -> f64 {
celsius + 273.15
}
}
Display Setup
We configure the OLED to use I2C with SDA on GPIO 16 and SCL on GPIO 17. We set the I2C frequency to 400 kHz.
We initialize the SSD1306 display in buffered graphics mode. In this mode, all drawing operations happen in memory first. The content is sent to the OLED only when we flush the buffer. We also define the text style, you can adjust the font size.
#![allow(unused)]
fn main() {
// Display Setup
let sda = p.PIN_16;
let scl = p.PIN_17;
let mut i2c_config = I2cConfig::default();
i2c_config.frequency = 400_000; //400kHz
let i2c_bus = I2c::new_async(p.I2C0, scl, sda, Irqs, i2c_config);
let i2c_interface = I2CDisplayInterface::new(i2c_bus);
let mut display = Ssd1306Async::new(i2c_interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display
.init()
.await
.expect("failed to initialize the display");
let text_style = MonoTextStyle::new(&FONT_7X13_BOLD, BinaryColor::On);
}
ADC Setup
We configure the ADC channel connected to the thermistor pin. We then initialize the ADC peripheral using the interrupt bindings defined earlier.
#![allow(unused)]
fn main() {
// ADC Setup for thermistor
let mut adc_pin = Channel::new_pin(p.PIN_28, Pull::None);
let mut adc = Adc::new(p.ADC, Irqs, AdcConfig::default());
}
Heapless String
We create a heapless string with a fixed capacity of 64 characters. This string lives on the stack and is reused on every iteration of the loop. We use it to store formatted values such as temperature, ADC reading, and resistance before drawing them on the OLED.
#![allow(unused)]
fn main() {
let mut buff: String<64> = String::new();
}
Convert the Reference Temperature to Kelvin
We define the reference temperature for the thermistor as 25°C. Since the B equation requires temperature in Kelvin, we convert this value once during initialization. This avoids repeating the conversion inside the loop.
#![allow(unused)]
fn main() {
let ref_temp = celsius_to_kelvin(REF_TEMP);
}
Main Loop
In each iteration of the loop, we read the thermistor value using the ADC, convert the reading into temperature, format the result as text, and update the OLED display.
We first clear both the string buffer and the display buffer.
#![allow(unused)]
fn main() {
buff.clear();
display
.clear(BinaryColor::Off)
.expect("failed to clear the display");
}
Read ADC
We then read the ADC value from the thermistor pin.
#![allow(unused)]
fn main() {
let adc_value = adc
.read(&mut adc_pin)
.await
.expect("failed to read adc value");
}
Convert ADC Value to Temperature
We convert the ADC reading into resistance using the voltage divider equation. We then convert the resistance into temperature using the B equation, which gives the temperature in Kelvin. Finally, we convert the value to Celsius.
#![allow(unused)]
fn main() {
let current_res = adc_to_resistance(adc_value, REF_RES);
let temperature_kelvin = calculate_temperature(current_res, REF_RES, ref_temp, B_VALUE);
let temperature_celsius = kelvin_to_celsius(temperature_kelvin);
}
Format Output Text
We format the temperature, ADC value, and resistance into the heapless string.
#![allow(unused)]
fn main() {
writeln!(buff, "Temp: {:.2} °C", temperature_celsius)
.expect("failed to format temperature");
writeln!(buff, "ADC: {}", adc_value).expect("failed to format ADC value");
writeln!(buff, "R: {:.2}", current_res).expect("failed to format Resistance");
}
Update OLED Display
We draw the formatted text to the display buffer, flush the buffer to update the OLED, and wait before the next iteration.
#![allow(unused)]
fn main() {
Text::new(&buff, Point::new(5, 20), text_style)
.draw(&mut display)
.expect("Failed to write the text");
display.flush().await.expect("failed to send to display");
Timer::after_secs(2).await;
}
Flash
Once you flash the firmware, you should see the temperature along with the resistance and ADC values. You can move the setup to a different room, observe how the readings change between day and night, or take it outdoors using an external power supply to see how the temperature responds in different conditions.
The full code
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_time::Timer;
// defmt Logging
use defmt::info;
use defmt_rtt as _;
use panic_probe as _;
// Text formatting without heap allocation
use core::fmt::Write;
use heapless::String;
// For OLED display
use ssd1306::{I2CDisplayInterface, Ssd1306Async, prelude::*};
// For ADC
use embassy_rp::adc::{Adc, Channel, Config as AdcConfig};
use embassy_rp::gpio::Pull;
// Interrupt Binding
use embassy_rp::bind_interrupts;
use embassy_rp::peripherals::I2C0;
use embassy_rp::{adc, i2c};
// I2C
use embassy_rp::i2c::{Config as I2cConfig, I2c};
// Embedded Graphics
use embedded_graphics::{
mono_font::{MonoTextStyle, iso_8859_13::FONT_7X13_BOLD},
pixelcolor::BinaryColor,
prelude::*,
text::Text,
};
bind_interrupts!(struct Irqs {
ADC_IRQ_FIFO => adc::InterruptHandler;
I2C0_IRQ => i2c::InterruptHandler<I2C0>;
});
const ADC_LEVELS: f64 = 4096.0;
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 chapter
fn adc_to_resistance(adc_value: u16, r2_res: f64) -> f64 {
let adc = adc_value as f64;
((ADC_LEVELS / adc) - 1.0) * r2_res
}
// 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());
info!("Initializing the program");
// Display Setup
let sda = p.PIN_16;
let scl = p.PIN_17;
let mut i2c_config = I2cConfig::default();
i2c_config.frequency = 400_000; //400kHz
let i2c_bus = I2c::new_async(p.I2C0, scl, sda, Irqs, i2c_config);
let i2c_interface = I2CDisplayInterface::new(i2c_bus);
let mut display = Ssd1306Async::new(i2c_interface, DisplaySize128x64, DisplayRotation::Rotate0)
.into_buffered_graphics_mode();
display
.init()
.await
.expect("failed to initialize the display");
let text_style = MonoTextStyle::new(&FONT_7X13_BOLD, BinaryColor::On);
// ADC Setup for thermistor
let mut adc_pin = Channel::new_pin(p.PIN_28, Pull::None);
let mut adc = Adc::new(p.ADC, Irqs, AdcConfig::default());
let mut buff: String<64> = String::new();
let ref_temp = celsius_to_kelvin(REF_TEMP);
loop {
buff.clear();
display
.clear(BinaryColor::Off)
.expect("failed to clear the display");
let adc_value = adc
.read(&mut adc_pin)
.await
.expect("failed to read adc value");
let current_res = adc_to_resistance(adc_value, REF_RES);
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)
.expect("failed to format temperature");
writeln!(buff, "ADC: {}", adc_value).expect("failed to format ADC value");
writeln!(buff, "R: {:.2}", current_res).expect("failed to format Resistance");
Text::new(&buff, Point::new(5, 20), text_style)
.draw(&mut display)
.expect("Failed to write the text");
display.flush().await.expect("failed to send to display");
Timer::after_secs(2).await;
}
}
Clone the existing project
You can clone (or refer) project I created and navigate to the temperature-oled folder.
git clone https://github.com/ImplFerris/rp2040-projects
cd rp2040-projects/embassy/temperature-oled/