找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1508

积分

0

好友

198

主题
发表于 2026-2-14 04:36:31 | 查看: 32| 回复: 0

年前最后一更,提前祝大家新年快乐!

之前我们介绍了如何在 Rust 中读取 DHT11 温湿度传感器的数据。这次,我想更进一步,将这些实时数据显示在一块液晶屏幕上。我手头的是一块物美价廉的 1.54 英寸 TFT LCD 屏幕,主控芯片为 ST7789,大概只要十来块钱。

1.54英寸ST7789 LCD屏幕引脚定义图

引脚接法

我们需要将屏幕的各个引脚正确连接到 ESP32S3 开发板的对应 GPIO 上。连接关系如下:

屏幕引脚 连接到 ESP32S3
GND GND
VCC 3.3V
SCL GPIO5
SDA GPIO6
RES GPIO7
DC GPIO15
CS GPIO16
BLK GPIO8

DHT11 传感器的数据线则连接至 GPIO40。

C 的实现

在深入 Rust 代码之前,先用 C 语言实现一遍是很有价值的思路。这样可以帮助我们快速理解 ESP32 官方 SDK 中外设和驱动的标准调用范式。对于 ST7789 屏幕,espressif 官方的 esp_lvgl_port 库提供了很好的支持,可以方便地进行图形和文本的绘制。

不过,你可能会遇到一个常见问题:屏幕显示花屏。这通常是由于 RGB565 颜色格式的字节序不匹配导致的。可以通过发送一个特定的初始化命令来解决:

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

0xB0 是 ST7789 的 “RAM Control” 命令,而 0x18 参数中的一个关键位(Bit 2)用于设置数据接口的传输顺序。这个配置确保了 LVGL 内部的字节序与屏幕控制器期望的顺序一致,从而修正了颜色错乱的问题。

为什么先要尝试用 C/C++ 实现呢?因为在 ESP32 的生态中,C 语言拥有最成熟、最完善的官方文档和支持。通过 C 版本的实践,我们能建立起对硬件驱动流程的直观理解,这在后续使用其他语言(如 Rust)时,能够更准确地定位和解决问题。

Rust 的实现

现在回到 Rust 的世界。为了让 DHT11 的数据读取不阻塞屏幕的刷新,我决定将 DHT11 的读取任务放到第二个 CPU 核心(CPU1)上运行。

DHT11 任务封装与多核调度

首先,对 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;

为了使用多核,需要引入 esp-rtos 库:

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

接着,定义将在 CPU1 上运行的任务函数。该任务会周期性地读取传感器数据,并将其存入一个全局共享变量中:

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

最后,在主核心(CPU0)上启动 RTOS 并初始化第二个核心:

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 屏幕驱动

屏幕驱动主要依赖以下几个 crate:

embedded-hal-bus = “0.3.0”
mipidsi = “0.9.0”
embedded-graphics = “0.8.1”

其中 embedded-graphics 是 Rust 嵌入式图形事实上的标准库,而 mipidsi 则提供了 ST7789 芯片的驱动程序。我们使用 SPI 协议与屏幕通信,这里有几个关键概念需要厘清:

术语 定义
主机 (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 cs = gpio::Output::new(peripherals.GPIO16, Level::High, Default::default());
let spi_device = ExclusiveDevice::new_no_delay(spi, cs).unwrap();

接下来,根据 embedded-graphics 的要求,我们需要设置数据/命令选择引脚(DC)和复位引脚(RST),并最终创建显示(Display)实例。

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

// … (假设有一个帧缓冲区 buffer)
let di = SpiInterface::new(spi_device, dc, &mut buffer);
let mut display = Builder::new(ST7789, di)
    .reset_pin(rst)
    .init(&mut delay)
    .unwrap();

一切准备就绪后,就可以在主循环中绘制图形了。为了优化性能(因为 ST7789 的 clear() 操作较慢),我们只在温湿度数据发生变化时才刷新整个屏幕。

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

效果展示

下图是 C 语言版本(使用 LVGL)的运行效果:
C语言版本(使用LVGL)在OLED上显示温湿度

下图是 Rust 版本(使用 embedded-graphics)的运行效果:
Rust版本(使用embedded-graphics)在ST7789屏幕上显示温湿度

遗留问题与后续

细心的你可能会发现,Rust 版本的显示效果图上存在一个小瑕疵(比如文本位置或刷新残留)。这正是嵌入式开发有趣的地方——从“能跑”到“跑得好”之间,还有很多细节需要打磨。如何在 Rust 中更优雅地处理屏幕刷新、解决潜在的视觉问题,将是后续文章探讨的主题。欢迎在技术社区交流你的想法和解决方案。


本教程涉及了从传感器读取、SPI 通信到多任务调度的完整嵌入式开发流程,希望对你有所启发。更多深入的嵌入式及系统编程讨论,可以关注 云栈社区 的相关板块。




上一篇:解决用户态spinlock争用:Linux内核延迟抢占与时间片扩展详解
下一篇:eBPF技术深度盘点:虚拟网络、云观测、FinOps等六大核心应用领域
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 13:00 , Processed in 0.640241 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表