从设备树到驱动:在RK3566上构建ST7789的SPI子系统框架
1. 理解SPI屏幕驱动开发的核心逻辑
当你第一次拿到ST7789这样的SPI屏幕时,可能会被各种专业术语搞得晕头转向。其实整个过程可以简化为三个关键步骤:硬件描述、驱动适配和功能实现。就像组装乐高积木一样,我们需要先把各个零件(硬件模块)正确连接(设备树描述),然后告诉系统如何操作这些零件(驱动开发),最后让积木动起来(屏幕显示)。
在RK3566开发板上,SPI子系统就像是一条高速公路,而ST7789屏幕则是这条路上的一个服务区。设备树的作用就是在地图上标注这个服务区的位置和特性,而驱动程序则是服务区的操作手册。我去年在泰山派开发板上调试这块屏幕时,发现很多问题都源于对这两个概念的混淆。
2. 设备树配置实战详解
2.1 解读Rockchip SPI控制器配置
打开rk3568.dtsi文件,你会看到类似这样的SPI控制器定义:
spi3: spi@fe640000 { compatible = "rockchip,rk3066-spi"; reg = <0x0 0xfe640000 0x0 0x1000>; interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru CLK_SPI3>, <&cru PCLK_SPI3>; pinctrl-0 = <&spi3m0_cs0 &spi3m0_pins>; status = "disabled"; };这里有几个关键点需要注意:
- reg属性指定了控制器的物理地址和地址范围
- clocks定义了SPI时钟源
- pinctrl-0确定了默认的引脚复用配置
- status初始为disabled表示需要手动启用
2.2 添加ST7789设备节点
在实际项目中,我们需要在自己的板级设备树文件中启用并扩展这个配置:
&spi3 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>; spi_lcd@0 { compatible = "sitronix,st7789v"; reg = <0>; spi-max-frequency = <32000000>; reset-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio3 RK_PC6 GPIO_ACTIVE_HIGH>; rotation = <90>; }; };我在调试时踩过一个坑:最初忘记指定pinctrl配置,导致SPI信号无法正常输出。后来用示波器抓取波形才发现这个问题,所以特别提醒大家检查引脚复用配置。
3. Linux SPI驱动开发指南
3.1 驱动框架搭建
SPI驱动的核心是实现spi_driver结构体:
static struct spi_driver st7789_driver = { .driver = { .name = "st7789", .of_match_table = st7789_of_match, }, .probe = st7789_probe, .remove = st7789_remove, .id_table = st7789_ids, };匹配表定义如下:
static const struct of_device_id st7789_of_match[] = { { .compatible = "sitronix,st7789v" }, {} };3.2 关键函数实现
probe函数是驱动的入口点,这里我们需要完成硬件初始化:
static int st7789_probe(struct spi_device *spi) { struct st7789 *lcd; lcd = devm_kzalloc(&spi->dev, sizeof(*lcd), GFP_KERNEL); spi_set_drvdata(spi, lcd); /* 初始化GPIO */ lcd->reset = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH); lcd->dc = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW); /* 硬件复位序列 */ gpiod_set_value(lcd->reset, 0); msleep(10); gpiod_set_value(lcd->reset, 1); msleep(120); /* 发送初始化命令 */ st7789_init_sequence(lcd); /* 注册framebuffer */ return st7789_fb_init(lcd); }4. 屏幕初始化与数据传输优化
4.1 初始化序列实现
ST7789需要特定的命令序列才能正常工作:
static void st7789_write_command(struct st7789 *lcd, u8 cmd) { gpiod_set_value(lcd->dc, 0); spi_write(lcd->spi, &cmd, sizeof(cmd)); } static void st7789_init_sequence(struct st7789 *lcd) { st7789_write_command(lcd, MIPI_DCS_SOFT_RESET); msleep(150); st7789_write_command(lcd, MIPI_DCS_SET_DISPLAY_OFF); /* 设置屏幕方向 */ st7789_write_command(lcd, MIPI_DCS_SET_ADDRESS_MODE); st7789_write_data(lcd, lcd->rotation); /* 更多初始化命令... */ }4.2 高效数据传输技巧
直接使用spi_write()发送大量数据效率很低,我们可以采用DMA传输:
static void st7789_write_data_dma(struct st7789 *lcd, const u8 *data, size_t len) { struct spi_transfer xfer = { .tx_buf = data, .len = len, }; struct spi_message msg; gpiod_set_value(lcd->dc, 1); spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); spi_sync(lcd->spi, &msg); }在实际测试中,使用DMA传输240x240的16位色图像,速度比普通SPI传输快3-5倍。不过要注意DMA缓冲区需要特殊处理,建议使用dma_alloc_coherent()分配内存。
5. 调试技巧与常见问题
5.1 必备调试工具
在开发过程中,这些工具特别有用:
- spidev_test:SPI通信测试工具
- devmem2:直接读写寄存器
- 示波器/逻辑分析仪:检查信号质量
- 内核printk:添加调试输出
5.2 典型问题解决方案
屏幕无反应:
- 检查电源和背光
- 确认复位时序正确
- 用逻辑分析仪抓取SPI信号
显示花屏:
- 检查像素格式设置
- 确认传输字节序
- 调整SPI时钟相位(polarity/phase)
性能低下:
- 启用DMA传输
- 提高SPI时钟频率
- 优化传输数据打包
记得我第一次调试时,屏幕显示全是乱码,花了整整两天才发现是字节序问题。后来在驱动中添加了详细的调试打印,大大提高了排查效率。
