当前位置: 首页 > news >正文

手把手教你为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开发板引脚功能描述
VCC3.3V电源正极
GNDGND地线
SCLECSPI3_SCLKSPI时钟线
SDAECSPI3_MOSISPI数据线
RESGPIO1_IO01复位信号
DCGPIO1_IO04数据/命令选择
CSGPIO1_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@02010000

3. 驱动开发实战

有了正确的硬件连接和设备树配置,现在可以开始编写显示屏的驱动代码了。我们将开发一个标准的Linux字符设备驱动,通过SPI接口与ST7789芯片通信。

3.1 驱动框架搭建

首先创建基本的驱动文件结构:

st7789_driver/ ├── Makefile ├── st7789.c └── st7789_test.c

Makefile内容

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

3.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 initialized

4.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); } }
http://www.jsqmd.com/news/665757/

相关文章:

  • 如何从零开始快速部署EspoCRM开源客户关系管理系统?
  • AGI如何真正“看懂”世界?:从视觉-语音-文本跨模态对齐到因果推理的5层理解跃迁
  • 别再只盯着数据手册了!手把手教你用MPU6500的DMP实现姿态解算(附STM32代码)
  • 性价比高的超耐磨地坪施工队怎么选,专业施工经验很重要 - 工业品网
  • 2026年3月有实力的OMO模式数字经济电商系统口碑推荐,电商4.0数字经济电商,OMO模式数字经济电商系统怎么选择 - 品牌推荐师
  • 别再死记硬背了!用Python和C语言两种方式,彻底搞懂CRC32查表法里的反转(附完整代码)
  • 保姆级教程:从SRA下载到binning,用metaWRAP搞定宏基因组数据分析全流程
  • 如何用Python财经数据接口库AKShare快速构建金融数据分析系统
  • 解读湘潭捷诚财务咨询公司,与其他公司对比及服务选择指南 - 工业设备
  • 保姆级教程:用Python+Wechaty+PadLocal协议,5分钟给你的微信号装上AI助理
  • Qwen3.5-2B惊艳效果:GIF动图时序理解+关键帧事件描述能力展示
  • B站视频下载终极指南:3分钟掌握BilibiliDown高效批量下载技巧
  • 别再只盯着SM9了!聊聊BLS12-381曲线如何成为零知识证明和聚合签名的‘基建狂魔’
  • 告别迷茫!ESP8266 WiFiClient库实战:从连接百度到收发数据的保姆级代码解析
  • VH6501干扰测试避坑指南:Repetitions参数设置不当,小心你的ECU‘假通过’!
  • 探究科力风机稳定性与售后服务,风机品牌选购干货大揭秘 - 工业推荐榜
  • Simplicity Studio v5 找不到Zigbee SDK?手把手教你从GitHub下载并安装EmberZNet 4.3.2
  • 从游戏物理引擎到推荐系统:LU分解在实际项目里到底怎么用?
  • 别再为MAC地址发愁了!三种为W5500/W5100等网络芯片生成合法地址的实战方法
  • 从BJT到MOSFET:LDO内部功率管演变史及其对现代电路设计的影响
  • OpenVINO AI插件深度解析:专业级音频处理的本地化AI解决方案
  • 泉盛UV-K5/K6终极解锁:从普通对讲机到专业无线电分析仪
  • 电机驱动板过热的系统性解决方案
  • 手把手教你用Verilog实现一个二倍抽取的多相滤波器(附MATLAB系数生成)
  • 告别梯度消失:用STBP算法手把手教你训练高性能脉冲神经网络(附PyTorch代码)
  • 探讨铝瓦楞板厂家哪家性价比高,费用和质量如何平衡 - 工业品牌热点
  • 从‘三方一轮密钥协商’到‘聚合签名’:手把手图解双线性对如何给密码学‘偷懒’
  • 软件商业中的盈利模式与增长策略
  • ANSYS、MATLAB等专业软件安装前必看:如何检查并设置纯英文用户名环境(Win系统)
  • 别再死记硬背了!用Python的NumPy和Matplotlib,5分钟搞懂RGB图像的矩阵本质