Feb 06, 2026
4 min read
ESP32,
Rust,
C,
Embedded Systems,

使用 Rust 在 ESP32S3 上实现 DHT11 温湿度传感器数据到 ST7789 屏幕显示

本文详细介绍了如何在 ESP32S3 上使用 Rust 语言实现 DHT11 温湿度传感器数据读取并将数据显示在 ST7789 TFT LCD 屏幕上。涵盖了 C 和 Rust 两种语言的实现方式,特别关注了 Rust 中的多核任务分配、SPI 总线配置、屏幕驱动初始化以及避免屏幕花屏等问题。文章还提供了完整的代码示例和关键技术点解析。

之前介绍过 DHT11 传感器用 Rust 怎么读取数据,现在我要把这个传感器的数据显示到屏幕上。

我用的屏幕是一个1.54寸主控为ST7789 的TFT LCD 屏幕。不贵,也就12块钱。

就是这个: st7789

引脚接法

屏幕引脚连接到 ESP32S3
GNDGND
VCC3.3V
SCLGPIO5
SDAGPIO6
RESGPIO7
DCGPIO15
CSGPIO16
BLKGPIO8

DTH11 传感器引脚的数据线接在 GPIO40 上。

C 的实现

由于代码比较长,这里不贴C代码了。

主要用到了 esp_lvgl_port 库来实现。现实上, st7789 屏幕的驱动 espressif 官方就已经内置支持了。 esp_lvgl_port 只是为了更好地绘制图形和文本。

这里可能会碰到一个问题,就是显示有可能会花屏。

可能需要下面这个代码来修正:

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);

查资料得知,由于 lvgl 调用芯片发送数据时采用的大小端序不一样,造成颜色错误。0xB0 是 ST7789 驱动芯片的 “RAM Control”(内存控制)命令,用于配置显示接口和颜色格式。

为什么能解决花屏? 核心问题:RGB565 颜色字节序不匹配

颜色格式:ST7789 使用 RGB565(16位)表示颜色

例如:红色是 0xF800 = 11111 00000 00000

大小端问题: LVGL 内部用 uint16_t 存储颜色(如 0xF800) 内存中存储为:大端 F8 00,小端 00 F8 SPI 发送时按字节序传输,如果控制器期望的字节序与实际不符,颜色就会错乱。

0x18 参数的作用: 0x18 = 0001 1000

  • bit4=1: 设置 262K 颜色模式
  • bit3=1: DOTCLK 上升沿采样
  • bit2=0: DBI-B 接口模式 这个配置使控制器与 LVGL 的字节序匹配。

为什么要用C实现一次?

因为 C 语言是 esp32 官方的第一等公民。通过 C的实现可快速了解 esp32 的外设和驱动的调用范式。

而 Rust 虽然也是 esp32 官方支持的语言,但是,几乎没有文档。

Rust 的实现

由于之前的DHT11 在rust 中的实现是放在cpu0 上的,为了让屏幕不影响 dht11 的数据读取,我这里需要把dht11的相关代码抽离出来,放在cpu1 上运行。

DHT11

先封装:

#![no_std]

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

/// DHT11 传感器管理器
pub struct Dht11Manager<'a> {
    dht11: Dht11<Flex<'a>, Delay>,
}

impl<'a> Dht11Manager<'a> {
    /// 创建新的 DHT11 传感器管理器
    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 }
    }

    /// 读取传感器数据
    pub fn read(&mut self) -> Result<(u8, u8), DhtError> {
        let reading = self.dht11.read().map_err(|_| DhtError)?;
        Ok((reading.temperature, reading.humidity))
    }
}

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

把这个代码放到 cpu1 上运行,需要用到官方的 rtos 库:

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

创建一个任务:

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);
                // 保存数据到共享存储
                critical_section::with(|cs| {
                    *DHT11_DATA.borrow(cs).borrow_mut() = Some((temp, hum));
                });
            }
            Err(_) => {
                defmt::dbg!("Failed to read DHT11 sensor");
            }
        }
    }
}

使用 rtos 库创建一个任务,并放到 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 屏幕

屏幕用到的驱动程序有用到下面这几个crates:

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

embedded-graphics 是 rust 嵌入式图形标准,而 mipidsi 是st7789 屏幕的驱动程序。

这里主要使用SPI 主机驱动程序来控制 LCD 屏幕。这里会有几个概念需要理解:

术语定义
主机 (Host)ESP32 内置的 SPI 控制器外设。用作 SPI 主机,在总线上发起 SPI 传输。
设备 (Device)SPI 从机设备。一条 SPI 总线与一或多个设备连接。每个设备共享 MOSI、MISO 和 SCLK 信号,但只有当主机向设备的专属 CS 线发出信号时,设备才会在总线上处于激活状态。
总线 (Bus)信号总线,由连接到同一主机的所有设备共用。一般来说,每条总线包括以下线:MISO、MOSI、SCLK、一条或多条 CS 线,以及可选的 QUADWP 和 QUADHD。因此,除每个设备都有单独的 CS 线外,所有设备都连接在相同的线下。多个设备也可以菊花链的方式共享一条 CS 线。
MOSI主机输出,从机输入,也写作 D。数据从主机发送至设备。在 Octal/OPI 模式下也表示为 data0 信号。
MISO主机输入,从机输出,也写作 Q。数据从设备发送至主机。在 Octal/OPI 模式下也表示为 data1 信号。
SCLK串行时钟。由主机产生的振荡信号,使数据位的传输保持同步。
CS片选。允许主机选择连接到总线上的单个设备,以便发送或接收数据。

所以我们需要初始化一个SPI 主机,并创建一个SPI 设备:

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();

由于我们是把数据输出到屏幕,也就是MOSI 模式,因此需要通过 with_mosi 方法设置MOSI 引脚为 GPIO6(sda 数据线)。

cs参数用到了 GPIO16 时钟线)。

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

通过 embedded-graphics 库文档可知,我们还需要设置一个 Display 实例,用于绘制图形。

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

可以通过 display 倒推可知道还需要 dc 和 rst 引脚。

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();

现在,一切已经准备就绪,我们可以开始绘制图形了。

由于 st7789 的屏幕刷新率不高,因此我们需要让温度或者湿度数据变化时再刷新屏幕。

    let mut last_temp: u8 = 255;
    let mut last_hum: u8 = 255;
    loop {
        delay.delay_millis(2000);
        // 使用 get_dht11_data() 获取温度和湿度
        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;
        }
    }

需要注意的是,display.clear 很慢。

效果如下:

这是 C语言版本:

c

这是 Rust 版本: rust

注意

这里屏幕上还有一个BUG,你是否有发现?后面的文章会介绍如何解决它。