手把手教你为i.MX6ULL开发板点亮1.3寸TFT屏(ST7789驱动,含设备树配置与驱动源码)
从零实战:i.MX6ULL开发板驱动1.3寸ST7789 TFT屏全流程解析
第一次拿到i.MX6ULL开发板和那块小巧的1.3寸TFT屏时,我盯着240x240的分辨率参数和ST7789驱动芯片的型号,既兴奋又忐忑。作为嵌入式Linux开发的新手,驱动一块SPI接口的显示屏听起来像是个不小的挑战。但事实证明,只要按照正确的步骤操作,从硬件连接到设备树配置,再到驱动编写和测试,整个过程可以变得非常清晰可控。本文将带你完整走一遍这个实战项目,避开那些我踩过的坑。
1. 硬件准备与连接
在开始编写代码之前,正确的硬件连接是基础。这块1.3寸TFT屏采用SPI接口通信,需要连接到i.MX6ULL开发板的相应引脚上。根据我的经验,连线时最容易出错的就是引脚对应关系,所以务必仔细检查。
所需材料清单:
- i.MX6ULL开发板(本文以NXP官方开发板为例)
- 1.3寸TFT显示屏(ST7789驱动芯片,240x240分辨率)
- 杜邦线若干
- 5V电源适配器
引脚连接对应表:
| TFT屏引脚 | i.MX6ULL开发板引脚 | 功能描述 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 地线 |
| SCL | ECSPI3_SCLK | SPI时钟线 |
| SDA | ECSPI3_MOSI | SPI数据线 |
| RES | GPIO1_IO01 | 复位信号 |
| DC | GPIO1_IO04 | 数据/命令选择 |
| CS | GPIO1_IO20 | 片选信号 |
| BLK | 不连接 | 背光控制(可选) |
注意:不同厂商的开发板引脚命名可能略有差异,建议查阅具体开发板的原理图确认SPI3接口对应的实际引脚编号。
连接完成后,建议用万用表检查各连接点是否导通,特别是电源和地线是否接触良好。我曾经因为一个看似连接的GND引脚实际接触不良,导致屏幕无法正常工作,排查了半天才发现是硬件连接问题。
2. 设备树配置详解
设备树(Device Tree)是现代Linux内核用于描述硬件配置的重要机制。要让Linux内核识别并正确驱动我们的TFT屏,需要在设备树中添加相应的节点配置。
2.1 添加GPIO控制节点
首先,我们需要为RESET和DC引脚添加GPIO控制节点。在i.MX6ULL的设备树文件(通常是imx6ull.dtsi或板级特定的.dts文件)中,找到根节点(/),添加以下内容:
/{ // 在根节点下添加屏幕控制引脚 ips_reset { compatible = "gpio-reset"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ips_reset>; gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; status = "okay"; }; ips_dc { compatible = "gpio-direction"; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ips_dc>; gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; status = "okay"; }; };2.2 配置引脚复用功能
接下来,在iomuxc节点中配置引脚的复用功能。这告诉SoC这些引脚将用作GPIO功能:
&iomuxc { pinctrl_ips_reset: ipsresetgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0x10B0 >; }; pinctrl_ips_dc: ipsdcgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x10B0 >; }; };2.3 配置SPI3控制器
最后,我们需要启用并配置SPI3控制器。找到设备树中的ecspi3节点(通常在imx6ull.dtsi中定义),添加我们的屏幕设备:
&ecspi3 { fsl,spi-num-chipselects = <1>; cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi3>; status = "okay"; display@0 { compatible = "sitronix,st7789v"; reg = <0>; spi-max-frequency = <50000000>; reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; width = <240>; height = <240>; buswidth = <8>; }; };配置完成后,使用dtc工具编译设备树,并将生成的.dtb文件部署到开发板上。可以通过以下命令检查设备树是否正确加载:
# 在开发板上执行 ls /proc/device-tree/soc/aips-bus@02000000/spba-bus@02000000/ecspi@020100003. 驱动开发实战
有了正确的硬件连接和设备树配置,现在可以开始编写显示屏的驱动代码了。我们将开发一个标准的Linux字符设备驱动,通过SPI接口与ST7789芯片通信。
3.1 驱动框架搭建
首先创建基本的驱动文件结构:
st7789_driver/ ├── Makefile ├── st7789.c └── st7789_test.cMakefile内容:
obj-m := st7789.o KDIR := /path/to/your/kernel/source PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean3.2 关键数据结构定义
在st7789.c中,我们定义驱动所需的主要数据结构:
#include <linux/module.h> #include <linux/spi/spi.h> #include <linux/delay.h> #include <linux/gpio/consumer.h> #include <linux/of.h> #define DRIVER_NAME "st7789v" #define LCD_WIDTH 240 #define LCD_HEIGHT 240 struct st7789_device { struct device *dev; struct spi_device *spi; struct gpio_desc *reset_gpio; struct gpio_desc *dc_gpio; struct mutex lock; };3.3 SPI通信基础函数
实现基本的SPI读写函数是驱动开发的核心:
static int st7789_write_command(struct st7789_device *st7789, u8 cmd) { int ret; struct spi_transfer xfer = { .len = 1, }; struct spi_message msg; u8 txbuf[1] = {cmd}; gpiod_set_value(st7789->dc_gpio, 0); // DC低电平表示命令 xfer.tx_buf = txbuf; spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); ret = spi_sync(st7789->spi, &msg); return ret; } static int st7789_write_data(struct st7789_device *st7789, u8 data) { int ret; struct spi_transfer xfer = { .len = 1, }; struct spi_message msg; u8 txbuf[1] = {data}; gpiod_set_value(st7789->dc_gpio, 1); // DC高电平表示数据 xfer.tx_buf = txbuf; spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); ret = spi_sync(st7789->spi, &msg); return ret; }3.4 屏幕初始化序列
ST7789芯片上电后需要发送一系列初始化命令才能正常工作:
static const struct st7789_cmd { u8 cmd; u8 data_len; const u8 *data; u16 delay_ms; } st7789_init_seq[] = { {0x36, 1, (u8[]){0x00}, 0}, // MADCTL: Memory Data Access Control {0x3A, 1, (u8[]){0x05}, 0}, // COLMOD: Interface Pixel Format {0xB2, 5, (u8[]){0x0C,0x0C,0x00,0x33,0x33}, 0}, // PORCTRL: Porch Setting {0xB7, 1, (u8[]){0x35}, 0}, // GCTRL: Gate Control {0xBB, 1, (u8[]){0x19}, 0}, // VCOMS: VCOM Setting {0xC0, 1, (u8[]){0x2C}, 0}, // LCMCTRL: LCM Control {0xC2, 2, (u8[]){0x01,0xFF}, 0}, // VDVVRHEN: VDV and VRH Command Enable {0xC3, 1, (u8[]){0x12}, 0}, // VRHS: VRH Set {0xC4, 1, (u8[]){0x20}, 0}, // VDVS: VDV Set {0xC6, 1, (u8[]){0x0F}, 0}, // FRCTRL2: Frame Rate Control {0xD0, 2, (u8[]){0xA4,0xA1}, 0}, // PWCTRL1: Power Control 1 {0xE0, 14, (u8[]){0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F,0x23}, 0}, // PVGAMCTRL: Positive Voltage Gamma Control {0xE1, 14, (u8[]){0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20,0x23}, 0}, // NVGAMCTRL: Negative Voltage Gamma Control {0x21, 0, NULL, 0}, // INVON: Display Inversion On {0x11, 0, NULL, 120}, // SLPOUT: Sleep Out {0x29, 0, NULL, 120}, // DISPON: Display On }; static int st7789_init_display(struct st7789_device *st7789) { int i, ret; // 硬件复位 gpiod_set_value(st7789->reset_gpio, 0); msleep(20); gpiod_set_value(st7789->reset_gpio, 1); msleep(120); // 发送初始化序列 for (i = 0; i < ARRAY_SIZE(st7789_init_seq); i++) { ret = st7789_write_command(st7789, st7789_init_seq[i].cmd); if (ret) return ret; if (st7789_init_seq[i].data_len > 0) { int j; for (j = 0; j < st7789_init_seq[i].data_len; j++) { ret = st7789_write_data(st7789, st7789_init_seq[i].data[j]); if (ret) return ret; } } if (st7789_init_seq[i].delay_ms) msleep(st7789_init_seq[i].delay_ms); } return 0; }3.5 实现字符设备操作
为了让用户空间能够与我们的设备交互,需要实现文件操作接口:
static int st7789_open(struct inode *inode, struct file *file) { struct st7789_device *st7789 = container_of(inode->i_cdev, struct st7789_device, cdev); file->private_data = st7789; return 0; } static ssize_t st7789_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) { struct st7789_device *st7789 = file->private_data; u8 *kbuf; int ret; kbuf = kmalloc(len, GFP_KERNEL); if (!kbuf) return -ENOMEM; if (copy_from_user(kbuf, buf, len)) { kfree(kbuf); return -EFAULT; } mutex_lock(&st7789->lock); ret = st7789_write_data_buffer(st7789, kbuf, len); mutex_unlock(&st7789->lock); kfree(kbuf); return ret < 0 ? ret : len; } static const struct file_operations st7789_fops = { .owner = THIS_MODULE, .open = st7789_open, .write = st7789_write, };3.6 驱动注册与注销
最后,实现驱动的注册和注销函数:
static int st7789_probe(struct spi_device *spi) { struct st7789_device *st7789; int ret; st7789 = devm_kzalloc(&spi->dev, sizeof(*st7789), GFP_KERNEL); if (!st7789) return -ENOMEM; st7789->spi = spi; st7789->dev = &spi->dev; mutex_init(&st7789->lock); // 获取GPIO资源 st7789->reset_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(st7789->reset_gpio)) return PTR_ERR(st7789->reset_gpio); st7789->dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW); if (IS_ERR(st7789->dc_gpio)) return PTR_ERR(st7789->dc_gpio); // 初始化SPI设备 spi->bits_per_word = 8; spi->mode = SPI_MODE_3; ret = spi_setup(spi); if (ret) return ret; // 初始化显示屏 ret = st7789_init_display(st7789); if (ret) return ret; // 注册字符设备 ret = alloc_chrdev_region(&st7789->devt, 0, 1, DRIVER_NAME); if (ret) return ret; cdev_init(&st7789->cdev, &st7789_fops); st7789->cdev.owner = THIS_MODULE; ret = cdev_add(&st7789->cdev, st7789->devt, 1); if (ret) { unregister_chrdev_region(st7789->devt, 1); return ret; } spi_set_drvdata(spi, st7789); dev_info(&spi->dev, "ST7789 display driver initialized\n"); return 0; } static int st7789_remove(struct spi_device *spi) { struct st7789_device *st7789 = spi_get_drvdata(spi); cdev_del(&st7789->cdev); unregister_chrdev_region(st7789->devt, 1); return 0; } static const struct of_device_id st7789_of_match[] = { { .compatible = "sitronix,st7789v", }, {}, }; MODULE_DEVICE_TABLE(of, st7789_of_match); static struct spi_driver st7789_driver = { .driver = { .name = DRIVER_NAME, .of_match_table = st7789_of_match, }, .probe = st7789_probe, .remove = st7789_remove, }; module_spi_driver(st7789_driver); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("ST7789 TFT LCD Driver"); MODULE_LICENSE("GPL");4. 测试与验证
驱动开发完成后,我们需要验证它是否能正常工作。首先编译并加载驱动模块:
# 在开发板上执行 insmod st7789.ko dmesg | grep st7789 # 查看驱动加载日志如果一切正常,你应该能看到类似以下的输出:
[ 12.345678] st7789v spi0.0: ST7789 display driver initialized4.1 简单的测试程序
创建一个简单的测试程序来验证基本功能:
// st7789_test.c #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <string.h> #define DEVICE_PATH "/dev/st7789v" int main(int argc, char **argv) { int fd = open(DEVICE_PATH, O_WRONLY); if (fd < 0) { perror("Failed to open device"); return -1; } // 发送简单的测试图案 unsigned short color = 0xF800; // 红色 write(fd, &color, sizeof(color)); close(fd); return 0; }编译并运行测试程序:
arm-linux-gnueabihf-gcc -o st7789_test st7789_test.c ./st7789_test如果屏幕变为纯红色,说明驱动工作正常。
4.2 高级测试:显示彩色条纹
为了更全面地测试显示功能,我们可以创建一个显示彩色条纹的程序:
// stripe_test.c #include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <stdint.h> #define WIDTH 240 #define HEIGHT 240 #define DEVICE_PATH "/dev/st7789v" void draw_stripes(int fd) { uint16_t buffer[WIDTH * HEIGHT]; int x, y; for (y = 0; y < HEIGHT; y++) { for (x = 0; x < WIDTH; x++) { // 每40像素改变一次颜色 int stripe = x / 40; switch (stripe % 6) { case 0: buffer[y * WIDTH + x] = 0xF800; break; // 红 case 1: buffer[y * WIDTH + x] = 0x07E0; break; // 绿 case 2: buffer[y * WIDTH + x] = 0x001F; break; // 蓝 case 3: buffer[y * WIDTH + x] = 0xFFFF; break; // 白 case 4: buffer[y * WIDTH + x] = 0x0000; break; // 黑 case 5: buffer[y * WIDTH + x] = 0xFFE0; break; // 黄 } } } write(fd, buffer, sizeof(buffer)); } int main() { int fd = open(DEVICE_PATH, O_WRONLY); if (fd < 0) { perror("Failed to open device"); return -1; } draw_stripes(fd); close(fd); return 0; }这个测试程序会在屏幕上绘制六种颜色的垂直条纹,可以直观地验证显示功能的正确性。
5. 性能优化与高级功能
基本的驱动功能实现后,我们可以考虑一些优化措施和高级功能的添加。
5.1 双缓冲技术
为了减少屏幕刷新时的闪烁,可以实现双缓冲技术:
struct st7789_device { // ... 原有成员 uint16_t *front_buffer; uint16_t *back_buffer; struct work_struct update_work; }; static void st7789_update_work(struct work_struct *work) { struct st7789_device *st7789 = container_of(work, struct st7789_device, update_work); mutex_lock(&st7789->lock); st7789_set_window(st7789, 0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); st7789_write_data_buffer(st7789, st7789->front_buffer, LCD_WIDTH * LCD_HEIGHT * 2); mutex_unlock(&st7789->lock); } static int st7789_swap_buffers(struct st7789_device *st7789) { uint16_t *temp = st7789->front_buffer; st7789->front_buffer = st7789->back_buffer; st7789->back_buffer = temp; schedule_work(&st7789->update_work); return 0; }5.2 部分区域刷新
对于只需要更新部分屏幕内容的应用,可以实现区域刷新功能:
static int st7789_update_region(struct st7789_device *st7789, int x1, int y1, int x2, int y2, const uint16_t *data) { int width = x2 - x1 + 1; int height = y2 - y1 + 1; int ret; mutex_lock(&st7789->lock); // 设置更新区域 st7789_write_command(st7789, 0x2A); // 列地址设置 st7789_write_data(st7789, x1 >> 8); st7789_write_data(st7789, x1 & 0xFF); st7789_write_data(st7789, x2 >> 8); st7789_write_data(st7789, x2 & 0xFF); st7789_write_command(st7789, 0x2B); // 行地址设置 st7789_write_data(st7789, y1 >> 8); st7789_write_data(st7789, y1 & 0xFF); st7789_write_data(st7789, y2 >> 8); st7789_write_data(st7789, y2 & 0xFF); st7789_write_command(st7789, 0x2C); // 内存写入 // 写入数据 ret = st7789_write_data_buffer(st7789, data, width * height * 2); mutex_unlock(&st7789->lock); return ret; }5.3 帧率控制
通过调整SPI时钟频率和优化数据传输,可以提高刷新率:
&ecspi3 { // ... display@0 { compatible = "sitronix,st7789v"; spi-max-frequency = <50000000>; // 提高到50MHz // ... }; };同时,在驱动中可以添加帧率统计功能:
static void st7789_fps_stats(struct st7789_device *st7789) { static ktime_t last_time; static int frame_count; static int fps; ktime_t now = ktime_get(); s64 delta = ktime_ms_delta(now, last_time); frame_count++; if (delta > 1000) { // 每1秒计算一次FPS fps = frame_count * 1000 / delta; frame_count = 0; last_time = now; dev_dbg(st7789->dev, "Current FPS: %d\n", fps); } }