Skip to content

Implementing DHT11 Temperature and Humidity Sensor Data Display on ST7789 Screen with Rust on ESP32S3

kingzcheung
Published date:
Edit this post

Previously, I introduced how to read DHT11 sensor data using Rust. Now I want to display this sensor data on a screen.

The screen I’m using is a 1.54-inch TFT LCD with ST7789 controller. It’s inexpensive, costing only about 12 yuan.

Here it is: st7789

Pin Connections

Screen PinConnect to ESP32S3
GNDGND
VCC3.3V
SCLGPIO5
SDAGPIO6
RESGPIO7
DCGPIO15
CSGPIO16
BLKGPIO8

The DHT11 sensor’s data line is connected to GPIO40.

C Implementation

Since the code is quite long, I won’t paste the C code here.

It mainly uses the esp_lvgl_port library for implementation. In reality, the ST7789 screen driver is already built-in and supported by Espressif officially. esp_lvgl_port is just for better drawing of graphics and text.

You might encounter an issue here where the display could show garbled/corrupted colors.

You may need the following code to fix it:

typedef struct
    {
        uint8_t cmd;
        uint8_t data[15];
        uint8_t len;
    } lcd_main_t;

lcd_main_t custom_lcd_init_cmds = {
    0xB0,
    {0x00, 0x18},
    2};

esp_lcd_panel_io_tx_color(io_handle, custom_lcd_init_cmds.cmd, custom_lcd_init_cmds.data, custom_lcd_init_cmds.len & 0x7f);

Research shows that this is caused by different endianness when LVGL calls the chip to send data, resulting in color errors. 0xB0 is the “RAM Control” command for the ST7789 driver chip, used to configure the display interface and color format.

Why does this fix the garbled display? Core issue: RGB565 color byte order mismatch

Color format: ST7789 uses RGB565 (16-bit) to represent colors

For example: Red is 0xF800 = 11111 00000 00000

Endianness issue: LVGL internally uses uint16_t to store colors (e.g., 0xF800) Stored in memory as: Big-endian F8 00, Little-endian 00 F8 When SPI transmits by byte order, if the controller expects a different byte order than what’s actually sent, colors will be corrupted.

The function of 0x18 parameter: 0x18 = 0001 1000

Why implement in C first?

Because C is a first-class citizen for ESP32 officially. Through the C implementation, you can quickly understand the paradigms for calling ESP32 peripherals and drivers.

While Rust is also an officially supported language for ESP32, there’s almost no documentation.

Rust Implementation

Since the previous DHT11 implementation in Rust was running on CPU0, to prevent the screen from affecting DHT11 data reading, I need to extract the DHT11 related code and run it on CPU1.

DHT11

First, encapsulate it:

#![no_std]

use embedded_dht_rs::dht11::Dht11;
use esp_hal::{
    delay::Delay,
    gpio::{DriveMode, Flex, OutputConfig, Pull},
};

/// DHT11 sensor manager
pub struct Dht11Manager<'a> {
    dht11: Dht11<Flex<'a>, Delay>,
}

impl<'a> Dht11Manager<'a> {
    /// Create a new DHT11 sensor manager
    pub fn new(pin: Flex<'static>, delay: Delay) -> Self {
        let mut dht11_pin = pin;
        let config = OutputConfig::default()
            .with_drive_mode(DriveMode::OpenDrain)
            .with_pull(Pull::None);
        dht11_pin.apply_output_config(&config);
        dht11_pin.set_output_enable(true);
        dht11_pin.set_input_enable(true);
        dht11_pin.set_high();

        let dht11 = Dht11::new(dht11_pin, delay);
        Self { dht11 }
    }

    /// Read sensor data
    pub fn read(&mut self) -> Result<(u8, u8), DhtError> {
        let reading = self.dht11.read().map_err(|_| DhtError)?;
        Ok((reading.temperature, reading.humidity))
    }
}

/// DHT11 Error
#[derive(Debug)]
pub struct DhtError;

To run this code on CPU1, you need to use the official RTOS library:

esp-rtos = { version = "0.2.0", features = ["esp32s3"] }

Create a task:

fn cpu1_task(delay: &Delay, dht11_pin: Flex<'static>) -> ! {
    let mut dht11 = Dht11Manager::new(dht11_pin, *delay);

    esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);

    loop {
        delay.delay_millis(2000);
        match dht11.read() {
            Ok((temp, hum)) => {
                info!("DHT11 - Temperature: {} °C, humidity: {} %", temp, hum);
                // Save data to shared storage
                critical_section::with(|cs| {
                    *DHT11_DATA.borrow(cs).borrow_mut() = Some((temp, hum));
                });
            }
            Err(_) => {
                defmt::dbg!("Failed to read DHT11 sensor");
            }
        }
    }

Use the RTOS library to create a task and run it on CPU1:


    let timg0 = TimerGroup::new(peripherals.TIMG0);
    let software_interrupt = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
    esp_rtos::start(timg0.timer0);

    let cpu1_task = move || cpu1_task(&delay, dht11_pin);

    let stack = unsafe { &mut *addr_of_mut!(APP_CORE_STACK) };
    esp_rtos::start_second_core(
        peripherals.CPU_CTRL,
        software_interrupt.software_interrupt0,
        software_interrupt.software_interrupt1,
        stack,
        cpu1_task,
    );

LCD Screen

The screen uses the following driver crates:

embedded-hal-bus = "0.3.0"
mipidsi = "0.9.0"
embedded-graphics = "0.8.1"

embedded-graphics is the Rust embedded graphics standard, and mipidsi is the ST7789 screen driver.

Here we mainly use the SPI host driver to control the LCD screen. There are several concepts to understand:

TermDefinition
HostThe SPI controller peripheral built into ESP32. Used as SPI host, initiates SPI transfers on the bus.
DeviceSPI slave device. One SPI bus connects to one or more devices. Each device shares MOSI, MISO, and SCLK signals, but a device is only active on the bus when the host signals to the device’s dedicated CS line.
BusSignal bus, shared by all devices connected to the same host. Generally, each bus includes the following lines: MISO, MOSI, SCLK, one or more CS lines, and optional QUADWP and QUADHD. Therefore, except for each device having its own CS line, all devices are connected to the same lines. Multiple devices can also share one CS line in a daisy-chain manner.
MOSIMaster Out Slave In, also written as D. Data is sent from host to device. In Octal/OPI mode, it also represents the data0 signal.
MISOMaster In Slave Out, also written as Q. Data is sent from device to host. In Octal/OPI mode, it also represents the data1 signal.
SCLKSerial Clock. Oscillating signal generated by the host to keep data bit transmission synchronized.
CSChip Select. Allows the host to select a single device connected to the bus for sending or receiving data.

So we need to initialize an SPI host and create an SPI device:

let spi = esp_hal::spi::master::Spi::new(
        peripherals.SPI2,
        Config::default().with_frequency(Rate::from_mhz(30)),
    )
    .unwrap()
    .with_sck(peripherals.GPIO5)
    .with_mosi(peripherals.GPIO6);

let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap();

Since we are outputting data to the screen, which is MOSI mode, we need to set the MOSI pin to GPIO6 (SDA data line) through the with_mosi method.

The cs parameter uses GPIO16 (clock line).

    let cs = gpio::Output::new(peripherals.GPIO16, Level::High, Default::default());

From the embedded-graphics library documentation, we know that we also need to set up a Display instance for drawing graphics.

let di = SpiInterface::new(spi_device, dc, &mut buffer);
let mut display = Builder::new(ST7789, di)
        .reset_pin(rst)
        .init(&mut delay)
        .unwrap();

From the display we can deduce that we also need dc and rst pins.

let dc = gpio::Output::new(peripherals.GPIO15, Level::Low, Default::default());
let mut rst = gpio::Output::new(peripherals.GPIO7, Level::Low, Default::default());
rst.set_high();

Now everything is ready, we can start drawing graphics.

Since the ST7789 screen refresh rate is not high, we need to refresh the screen only when the temperature or humidity data changes.

    let mut last_temp: u8 = 255;
    let mut last_hum: u8 = 255;
    loop {
        delay.delay_millis(2000);
        // Use get_dht11_data() to get temperature and humidity
        let (temp, hum) = get_dht11_data();
        if temp != last_temp || hum != last_hum {
            display.clear(Rgb565::BLACK).unwrap();
            draw_text(&mut display, temp, hum).unwrap();
            last_temp = temp;
            last_hum = hum;
        }
    }

Note that display.clear is slow.

Results:

This is the C language version:

c

This is the Rust version: rust

Note

There’s still a bug on the screen. Did you notice it? I’ll explain how to fix it in a later article.

Previous
在嵌入式 Rust 中显示中文字符的方法
Next
使用 Rust 在 ESP32S3 上实现 DHT11 温湿度传感器数据到 ST7789 屏幕显示