之前介绍过 DHT11 传感器用 Rust 怎么读取数据,现在我要把这个传感器的数据显示到屏幕上。
我用的屏幕是一个1.54寸主控为ST7789 的TFT LCD 屏幕。不贵,也就12块钱。
就是这个:

引脚接法
| 屏幕引脚 | 连接到 ESP32S3 |
|---|---|
| GND | GND |
| VCC | 3.3V |
| SCL | GPIO5 |
| SDA | GPIO6 |
| RES | GPIO7 |
| DC | GPIO15 |
| CS | GPIO16 |
| BLK | GPIO8 |
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语言版本:

这是 Rust 版本:

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