手把手教你为i.MX6ULL开发板驱动1.3寸ST7789 TFT屏(含完整设备树与驱动代码)
i.MX6ULL开发板驱动1.3寸ST7789 TFT屏全流程实战指南
在嵌入式Linux开发中,显示设备的驱动往往是项目开发的关键环节之一。本文将详细介绍如何在i.MX6ULL开发板上驱动1.3寸ST7789 TFT屏幕的全过程,从硬件连接到设备树配置,再到驱动编写和测试,提供一套完整的解决方案。
1. 硬件准备与连接
在开始软件配置前,确保已准备好以下硬件组件:
- i.MX6ULL开发板(如正点原子阿尔法)
- 1.3寸ST7789驱动的TFT屏幕(240×240分辨率)
- 杜邦线若干
- 5V/3.3V电源适配器
ST7789通常通过SPI接口与主控通信,需要连接以下信号线:
| TFT屏引脚 | 开发板GPIO | 功能描述 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
| SCL | SPI3_SCLK | 时钟信号 |
| SDA | SPI3_MOSI | 数据输入 |
| RES | GPIO1_IO01 | 复位信号 |
| DC | GPIO1_IO04 | 数据/命令选择 |
| CS | GPIO1_IO20 | 片选信号 |
硬件连接注意事项:
- 确保电源电压匹配,ST7789通常工作在3.3V
- 信号线长度不宜过长,避免信号完整性问题
- 如果屏幕有背光控制,可连接到PWM输出引脚实现亮度调节
2. 设备树配置与编译
设备树是Linux内核识别硬件的重要配置文件,我们需要为SPI接口和GPIO添加相应节点。
2.1 修改设备树源文件
找到开发板对应的设备树文件(如imx6ull-alientek-emmc.dts),添加以下内容:
/* 在根节点下添加GPIO控制节点 */ / { ips_reset: ips-reset { compatible = "gpio-reset"; gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; status = "okay"; }; ips_dc: ips-dc { compatible = "gpio-control"; gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; status = "okay"; }; }; /* 在iomuxc节点中添加引脚复用配置 */ &iomuxc { pinctrl_ips: ipsgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO01__GPIO1_IO01 0x10B0 /* RESET */ MX6UL_PAD_GPIO1_IO04__GPIO1_IO04 0x10B0 /* DC */ MX6UL_PAD_UART2_CTS__GPIO1_IO20 0x10B0 /* CS */ >; }; }; /* 配置SPI3控制器 */ &ecspi3 { fsl,spi-num-chipselects = <1>; cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi3 &pinctrl_ips>; status = "okay"; st7789: st7789@0 { compatible = "sitronix,st7789"; spi-max-frequency = <50000000>; reg = <0>; reset-gpios = <&gpio1 1 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>; width = <240>; height = <240>; buswidth = <8>; fps = <30>; }; };2.2 设备树编译与更新
配置完成后,需要编译设备树并更新到开发板:
# 编译设备树 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs # 将生成的dtb文件拷贝到开发板 scp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb root@开发板IP:/boot/ # 在开发板上更新设备树 cp /boot/imx6ull-alientek-emmc.dtb /sys/firmware/devicetree/base/验证设备树节点是否成功创建:
# 检查SPI设备节点 ls /sys/bus/spi/devices/spi3.0/ # 检查GPIO控制节点 ls /sys/class/gpio/3. Linux驱动开发
ST7789驱动可以采用标准的SPI框架实现,下面展示关键部分的驱动代码。
3.1 驱动框架初始化
#include <linux/module.h> #include <linux/spi/spi.h> #include <linux/delay.h> #include <linux/gpio/consumer.h> #define DRIVER_NAME "st7789" struct st7789_data { struct spi_device *spi; struct gpio_desc *reset_gpio; struct gpio_desc *dc_gpio; u16 width; u16 height; }; static int st7789_write_command(struct st7789_data *st7789, u8 cmd) { int ret; struct spi_transfer xfer = { .len = 1, .tx_buf = &cmd, }; struct spi_message msg; gpiod_set_value(st7789->dc_gpio, 0); // 命令模式 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_data *st7789, u8 *data, size_t len) { int ret; struct spi_transfer xfer = { .len = len, .tx_buf = data, }; struct spi_message msg; gpiod_set_value(st7789->dc_gpio, 1); // 数据模式 spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); ret = spi_sync(st7789->spi, &msg); return ret; }3.2 屏幕初始化序列
ST7789需要按照特定序列进行初始化,以下是关键初始化步骤:
static int st7789_init_sequence(struct st7789_data *st7789) { int ret = 0; // 硬件复位 gpiod_set_value(st7789->reset_gpio, 0); msleep(20); gpiod_set_value(st7789->reset_gpio, 1); msleep(120); // 发送初始化命令 const u8 init_cmds[] = { 0x36, 0x00, // MADCTL: Memory Data Access Control 0x3A, 0x05, // COLMOD: Interface Pixel Format 0xB2, 0x0C, 0x0C, 0x00, 0x33, 0x33, // PORCTRL: Porch Setting 0xB7, 0x35, // GCTRL: Gate Control 0xBB, 0x19, // VCOMS: VCOM Setting 0xC0, 0x2C, // LCMCTRL: LCM Control 0xC2, 0x01, // VDVVRHEN: VDV and VRH Command Enable 0xC3, 0x12, // VRHS: VRH Set 0xC4, 0x20, // VDVS: VDV Set 0xC6, 0x0F, // FRCTRL2: Frame Rate Control 0xD0, 0xA4, 0xA1, // PWCTRL1: Power Control 1 0xE0, // PVGAMCTRL: Positive Voltage Gamma Control 0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23, 0xE1, // NVGAMCTRL: Negative Voltage Gamma Control 0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23, 0x21, // INVON: Display Inversion On 0x11, // SLPOUT: Sleep Out 0x29, // DISPON: Display On }; for (int i = 0; i < ARRAY_SIZE(init_cmds); ) { if (init_cmds[i] == 0xE0 || init_cmds[i] == 0xE1) { ret = st7789_write_command(st7789, init_cmds[i]); i++; ret |= st7789_write_data(st7789, (u8 *)&init_cmds[i], 14); i += 14; } else if (i < ARRAY_SIZE(init_cmds) - 1 && init_cmds[i+1] != 0xE0 && init_cmds[i+1] != 0xE1) { ret = st7789_write_command(st7789, init_cmds[i]); i++; ret |= st7789_write_data(st7789, (u8 *)&init_cmds[i], 1); i++; } else { ret = st7789_write_command(st7789, init_cmds[i]); i++; } if (ret < 0) { dev_err(&st7789->spi->dev, "Init sequence error at cmd 0x%02x\n", init_cmds[i-1]); return ret; } msleep(10); } return 0; }3.3 实现帧缓冲接口
为了与Linux显示子系统集成,需要实现帧缓冲(fb)接口:
#include <linux/fb.h> static int st7789_fb_setcolreg(u_int regno, u_int red, u_int green, u_int blue, u_int transp, struct fb_info *info) { // 设置颜色寄存器 return 0; } static int st7789_fb_blank(int blank_mode, struct fb_info *info) { // 控制屏幕空白模式 struct st7789_data *st7789 = info->par; switch (blank_mode) { case FB_BLANK_UNBLANK: st7789_write_command(st7789, 0x29); // DISPON break; case FB_BLANK_NORMAL: case FB_BLANK_VSYNC_SUSPEND: case FB_BLANK_HSYNC_SUSPEND: case FB_BLANK_POWERDOWN: st7789_write_command(st7789, 0x28); // DISPOFF break; } return 0; } static struct fb_ops st7789_fb_ops = { .owner = THIS_MODULE, .fb_setcolreg = st7789_fb_setcolreg, .fb_blank = st7789_fb_blank, .fb_fillrect = cfb_fillrect, .fb_copyarea = cfb_copyarea, .fb_imageblit = cfb_imageblit, }; static int st7789_probe(struct spi_device *spi) { struct fb_info *info; struct st7789_data *st7789; int ret = 0; // 分配帧缓冲结构 info = framebuffer_alloc(sizeof(*st7789), &spi->dev); if (!info) return -ENOMEM; st7789 = info->par; st7789->spi = spi; // 获取GPIO资源 st7789->reset_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_LOW); if (IS_ERR(st7789->reset_gpio)) { ret = PTR_ERR(st7789->reset_gpio); goto err_fb_release; } st7789->dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW); if (IS_ERR(st7789->dc_gpio)) { ret = PTR_ERR(st7789->dc_gpio); goto err_fb_release; } // 初始化帧缓冲信息 info->fbops = &st7789_fb_ops; info->flags = FBINFO_FLAG_DEFAULT; info->pseudo_palette = &st7789->pseudo_palette; // 设置显示参数 info->var.xres = st7789->width; info->var.yres = st7789->height; info->var.xres_virtual = info->var.xres; info->var.yres_virtual = info->var.yres; info->var.bits_per_pixel = 16; info->var.red.offset = 11; info->var.red.length = 5; info->var.green.offset = 5; info->var.green.length = 6; info->var.blue.offset = 0; info->var.blue.length = 5; // 分配显示缓冲区 info->screen_size = info->var.xres * info->var.yres * info->var.bits_per_pixel / 8; info->screen_buffer = dma_alloc_coherent(&spi->dev, info->screen_size, &info->fix.smem_start, GFP_KERNEL); if (!info->screen_buffer) { ret = -ENOMEM; goto err_fb_release; } info->fix.smem_len = info->screen_size; info->fix.type = FB_TYPE_PACKED_PIXELS; info->fix.visual = FB_VISUAL_TRUECOLOR; info->fix.line_length = info->var.xres * info->var.bits_per_pixel / 8; // 初始化屏幕 ret = st7789_init_sequence(st7789); if (ret) goto err_dma_free; // 注册帧缓冲设备 ret = register_framebuffer(info); if (ret < 0) goto err_dma_free; spi_set_drvdata(spi, info); dev_info(&spi->dev, "fb%d: %s frame buffer device\n", info->node, info->fix.id); return 0; err_dma_free: dma_free_coherent(&spi->dev, info->screen_size, info->screen_buffer, info->fix.smem_start); err_fb_release: framebuffer_release(info); return ret; }4. 驱动编译与测试
4.1 编写Makefile
obj-m := st7789.o KDIR := /path/to/kernel/source PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean4.2 编译与加载驱动
# 交叉编译驱动 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- # 将驱动拷贝到开发板 scp st7789.ko root@开发板IP:/lib/modules/$(uname -r)/kernel/drivers/video/ # 在开发板上加载驱动 depmod -a modprobe st77894.3 测试驱动功能
编写简单的测试程序验证驱动功能:
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/fb.h> #include <sys/mman.h> int main() { int fbfd = open("/dev/fb0", O_RDWR); if (fbfd == -1) { perror("open fb device"); return -1; } // 获取屏幕信息 struct fb_var_screeninfo vinfo; ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo); // 映射帧缓冲 char *fbp = mmap(0, vinfo.yres_virtual * vinfo.xres_virtual * 2, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0); // 绘制彩色条纹 for (int y = 0; y < vinfo.yres; y++) { for (int x = 0; x < vinfo.xres; x++) { int offset = (y * vinfo.xres + x) * 2; if (x < 80) { // 红色区域 *(unsigned short *)(fbp + offset) = 0xF800; } else if (x < 160) { // 绿色区域 *(unsigned short *)(fbp + offset) = 0x07E0; } else { // 蓝色区域 *(unsigned short *)(fbp + offset) = 0x001F; } } } munmap(fbp, 0); close(fbfd); return 0; }编译并运行测试程序:
arm-linux-gnueabihf-gcc -o fb_test fb_test.c ./fb_test5. 性能优化与调试技巧
5.1 SPI传输优化
ST7789支持最高80MHz的SPI时钟,但在实际应用中需要考虑信号完整性和功耗:
// 在驱动probe函数中添加 spi->max_speed_hz = 50000000; // 50MHz spi->mode = SPI_MODE_3; // CPOL=1, CPHA=1 spi_setup(spi);优化建议:
- 使用DMA传输减少CPU开销
- 批量发送像素数据而非单像素传输
- 合理使用双缓冲技术减少画面撕裂
5.2 常见问题排查
问题1:屏幕无显示
- 检查电源和背光是否正常
- 用逻辑分析仪确认SPI信号
- 验证复位时序是否正确
- 检查设备树配置是否生效
问题2:显示花屏
- 降低SPI时钟频率测试
- 检查数据/命令(DC)信号时序
- 确认颜色格式配置(通常为RGB565)
问题3:刷新率低
- 优化区域刷新而非全屏刷新
- 使用硬件加速功能
- 检查SPI总线是否被其他设备占用
5.3 高级功能实现
部分刷新(Partial Refresh)
void st7789_set_window(struct st7789_data *st7789, u16 x1, u16 y1, u16 x2, u16 y2) { u8 buf[4]; st7789_write_command(st7789, 0x2A); // CASET buf[0] = x1 >> 8; buf[1] = x1 & 0xFF; buf[2] = x2 >> 8; buf[3] = x2 & 0xFF; st7789_write_data(st7789, buf, 4); st7789_write_command(st7789, 0x2B); // RASET buf[0] = y1 >> 8; buf[1] = y1 & 0xFF; buf[2] = y2 >> 8; buf[3] = y2 & 0xFF; st7789_write_data(st7789, buf, 4); st7789_write_command(st7789, 0x2C); // RAMWR }睡眠模式与低功耗
void st7789_enter_sleep(struct st7789_data *st7789) { st7789_write_command(st7789, 0x10); // SLPIN msleep(120); // 等待屏幕完全进入睡眠 } void st7789_exit_sleep(struct st7789_data *st7789) { st7789_write_command(st7789, 0x11); // SLPOUT msleep(120); // 等待屏幕完全唤醒 }通过以上完整的开发流程,开发者可以成功在i.MX6ULL开发板上驱动ST7789 TFT屏幕,并实现高性能的图形显示功能。实际项目中,还可以进一步优化驱动性能,添加触摸支持等功能,打造更完善的嵌入式显示解决方案。
