第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例

目录

[开篇先搞懂:SPI 总线到底是什么?和 I2C 有啥区别?](#开篇先搞懂:SPI 总线到底是什么?和 I2C 有啥区别?)

大白话定义

[SPI 和 I2C 的核心区别,小白一眼看懂](#SPI 和 I2C 的核心区别,小白一眼看懂)

[一、SPI 总线的核心原理,小白必懂](#一、SPI 总线的核心原理,小白必懂)

[1. SPI 的 4 根信号线](#1. SPI 的 4 根信号线)

核心通信逻辑

[2. SPI 的 4 种工作模式(CPOL/CPHA)](#2. SPI 的 4 种工作模式(CPOL/CPHA))

(1)CPOL(时钟极性)

(2)CPHA(时钟相位)

[4 种工作模式](#4 种工作模式)

[二、RK3568 SPI 控制器详解](#二、RK3568 SPI 控制器详解)

核心特点

三、实战前的硬件准备

硬件清单

硬件接线

四、第一步:设备树配置

[1. 修改板级设备树文件](#1. 修改板级设备树文件)

核心属性讲解

[2. 编译烧录验证](#2. 编译烧录验证)

[五、第二步:SPI 驱动内核代码开发](#五、第二步:SPI 驱动内核代码开发)

[1. 核心 SPI 子系统 API 讲解](#1. 核心 SPI 子系统 API 讲解)

[2. ST7789 驱动核心知识点](#2. ST7789 驱动核心知识点)

[3. 完整驱动代码编写](#3. 完整驱动代码编写)

[4. 编译驱动,烧录验证](#4. 编译驱动,烧录验证)

[六、第三步:HAL 层适配 + 安卓 App 开发](#六、第三步:HAL 层适配 + 安卓 App 开发)

[1. HAL 层核心代码](#1. HAL 层核心代码)

[2. 安卓 App 功能](#2. 安卓 App 功能)

[七、小白 SPI 驱动必踩的坑,提前规避](#七、小白 SPI 驱动必踩的坑,提前规避)

结尾说两句


大家好,我是黒漂技术佬。上一篇我们搞定了 I2C 总线驱动,实现了 OLED 屏的显示控制。后台很多兄弟问:

"佬,I2C 速率太慢了,我要驱动 LCD 屏、高速 Flash,用 I2C 根本跑不起来,该用什么?"

答案就是SPI 总线!SPI 是嵌入式里最常用的高速串行总线,相比 I2C,它的速率要高得多,最高能到几十 MHz,甚至上百 MHz,非常适合高速数据传输的场景,比如 LCD 屏、SPI Flash、WiFi 模块、ADC/DAC 芯片等等。

今天这篇,我就用大白话给你讲透 SPI 总线的核心原理,对比它和 I2C 的区别,手把手带你完成RK3568 平台 SPI 设备驱动的完整开发,以常用的 1.3 寸 ST7789 SPI LCD 屏为例,实现屏幕显示,并且打通安卓 App 的控制全链路,学完就能适配其他高速 SPI 设备。


开篇先搞懂:SPI 总线到底是什么?和 I2C 有啥区别?

大白话定义

SPI 的全称是 Serial Peripheral Interface,串行外设接口,是一种 4 线式高速串行总线,采用主从架构,支持全双工通信,是嵌入式里高速外设的首选通信总线。

SPI 和 I2C 的核心区别,小白一眼看懂

表格

特性 SPI 总线 I2C 总线
引脚数量 4 根(SCLK、MOSI、MISO、CS) 2 根(SCL、SDA)
通信模式 全双工,收发可以同时进行 半双工,收发不能同时进行
速率 高速,几十 MHz~ 上百 MHz 低速,最高 4Mbps,常用 100kbps/400kbps
多从机支持 每个从机需要一个独立的 CS 片选引脚 一条总线最多挂 127 个从机,只用两根线,通过地址区分
硬件流控 无应答机制,需要软件保证通信可靠 有应答机制,能确认数据是否传输成功
适用场景 高速数据传输,比如 LCD 屏、Flash、高速 ADC 低速外设,比如传感器、OLED 屏、低速率芯片

简单总结:I2C 的优势是引脚少,布线简单,适合低速、多设备的场景;SPI 的优势是速率高,全双工,适合高速数据传输的场景,就是多一个从机就要多一个 CS 引脚,布线会复杂一点。


一、SPI 总线的核心原理,小白必懂

1. SPI 的 4 根信号线

SPI 总线需要 4 根信号线,分别是:

表格

信号线 全称 作用 方向
SCLK Serial Clock 串行时钟线,由主机提供,同步数据传输 主机→从机
MOSI Master Out Slave In 主机输出,从机输入,主机向从机发送数据 主机→从机
MISO Master In Slave Out 主机输入,从机输出,从机向主机发送数据 从机→主机
CS/SS Chip Select 片选线,主机通过拉低对应的 CS 引脚,选中要通信的从机 主机→从机

核心通信逻辑

  1. 片选机制:SPI 总线上可以挂多个从机,每个从机有独立的 CS 片选引脚。主机要和哪个从机通信,就把对应的 CS 引脚拉低,其他从机的 CS 引脚保持高电平,不会响应总线的信号;
  2. 全双工通信:在 SCLK 时钟的驱动下,主机通过 MOSI 线向从机发送数据,同时从机通过 MISO 线向主机发送数据,收发是同时进行的,一个时钟周期,主机和从机各完成 1 位数据的收发;
  3. 主从架构:只有主机能发起通信,提供时钟信号,从机只能响应主机的通信,不能主动发起。

2. SPI 的 4 种工作模式(CPOL/CPHA)

这是 SPI 通信最核心的知识点,也是新手最容易踩坑的地方。SPI 有 4 种工作模式,由两个参数决定:CPOL(时钟极性)CPHA(时钟相位)

(1)CPOL(时钟极性)

决定了 SPI 总线在空闲状态下,SCLK 时钟线的电平:

  • CPOL=0:空闲状态下,SCLK 是低电平;
  • CPOL=1:空闲状态下,SCLK 是高电平。
(2)CPHA(时钟相位)

决定了数据在哪个时钟沿被采样(读取):

  • CPHA=0:在 SCLK 的第一个跳变沿(上升沿 / 下降沿)采样数据,第二个跳变沿更新数据;
  • CPHA=1:在 SCLK 的第二个跳变沿采样数据,第一个跳变沿更新数据。
4 种工作模式

表格

模式 CPOL CPHA 空闲电平 采样沿
模式 0 0 0 低电平 上升沿
模式 1 0 1 低电平 下降沿
模式 2 1 0 高电平 下降沿
模式 3 1 1 高电平 上升沿

小白必记:主机和从机的工作模式必须完全一致,不然数据收发会完全错乱,通信失败。我们要驱动的 SPI 设备,它的 datasheet 里会明确说明支持哪种工作模式,我们只需要在驱动里配置成对应的模式就行。最常用的是模式 0(CPOL=0, CPHA=0)和模式 3(CPOL=1, CPHA=1)。


二、RK3568 SPI 控制器详解

RK3568 芯片内置了3 路硬件 SPI 控制器,分别是 SPI0、SPI1、SPI2,每一路都独立工作,支持最高 50MHz 的时钟频率,完全满足高速数据传输的需求。

核心特点

  1. 官方 SDK 里已经实现了完整的 Linux SPI 子系统驱动,我们不用手动模拟时序,不用操作底层寄存器,只需要调用内核提供的 SPI 子系统 API,就能实现 SPI 通信,开发难度极低;
  2. 每一路 SPI 都对应了固定的 GPIO 引脚,我们只需要在设备树里配置引脚复用,就能使用;
  3. 我们这次实战用SPI1 ,对应的引脚是:
    • SCLK:GPIO1_B0,复用功能 2;
    • MOSI:GPIO1_B1,复用功能 2;
    • MISO:GPIO1_B2,复用功能 2;
    • CS0:GPIO1_B3,复用功能 2。

三、实战前的硬件准备

我们这次的实战目标:基于 RK3568 的 SPI1 控制器,驱动 1.3 寸 ST7789 SPI LCD 屏(240*240 分辨率,SPI 接口),实现屏幕清屏、画点、显示图片、显示字符串的功能,并且打通安卓 App 控制屏幕显示的全链路。

硬件清单

  1. RK3568 开发板 1 块;
  2. 1.3 寸 ST7789 SPI LCD 屏 1 个(240*240 分辨率,3.3V 供电);
  3. 杜邦线 6 根;
  4. 面包板 1 个(可选)。

硬件接线

表格

RK3568 开发板引脚 LCD 屏引脚 说明
3.3V VCC 屏幕供电,3.3V,别接 5V
GND GND 共地,必须接
GPIO1_B0(SPI1_SCLK) SCL/SCK SPI 时钟线
GPIO1_B1(SPI1_MOSI) SDA/MOSI SPI 数据输入线,主机向屏幕发数据
GPIO1_B3(SPI1_CS0) CS/SS 片选线
GPIO0_A0(普通 GPIO) DC 数据 / 命令选择线,高电平是数据,低电平是命令
GPIO0_A1(普通 GPIO) RST 复位引脚,低电平复位

小白避坑

  1. ST7789 SPI 屏一般有两个额外的引脚:DC(数据 / 命令选择)和 RST(复位),这两个引脚用普通 GPIO 就能控制,不用 SPI 引脚;
  2. 屏幕是 3.3V 供电的,绝对不能接 5V,不然直接烧屏;
  3. 接线的时候,MOSI 接屏幕的 SDA,MISO 不用接,因为 LCD 屏只需要接收数据,不需要向主机发送数据,MISO 引脚可以悬空;
  4. 所有设备必须共地,不然通信会乱码。

四、第一步:设备树配置

我们需要在设备树里添加 SPI LCD 设备节点,配置 SPI 引脚复用,使能 SPI1 控制器,配置 DC 和 RST 的 GPIO 引脚。

1. 修改板级设备树文件

  1. 进入设备树目录,打开你的开发板对应的.dts 文件: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/
    vim rk3568-firefly.dts
  2. 添加 SPI 引脚复用配置,使能 SPI1 控制器,添加 LCD 设备节点: dts

    复制代码
    // SPI1引脚复用配置
    &pinctrl {
        spi1 {
            spi1_clk: spi1-clk {
                rockchip,pins = <1 RK_PB0 2 &pcfg_pull_none_smt>;
            };
            spi1_mosi: spi1-mosi {
                rockchip,pins = <1 RK_PB1 2 &pcfg_pull_none_smt>;
            };
            spi1_miso: spi1-miso {
                rockchip,pins = <1 RK_PB2 2 &pcfg_pull_none_smt>;
            };
            spi1_cs0: spi1-cs0 {
                rockchip,pins = <1 RK_PB3 2 &pcfg_pull_up>;
            };
        };
    };
    
    // 使能SPI1控制器,添加LCD设备节点
    &spi1 {
        status = "okay";
        #address-cells = <1>;
        #size-cells = <0>;
        pinctrl-names = "default";
        pinctrl-0 = <&spi1_clk &spi1_mosi &spi1_miso &spi1_cs0>;
    
        // ST7789 LCD设备节点
        lcd: st7789@0 {
            compatible = "st7789,lcd";
            reg = <0>; // SPI片选编号,CS0对应0
            spi-max-frequency = <40000000>; // SPI最大速率40MHz
            spi-cpol = <0>; // CPOL=0,空闲电平低
            spi-cpha = <0>; // CPHA=0,模式0
            dc-gpios = <&gpio0 RK_PA0 GPIO_ACTIVE_HIGH>; // DC引脚
            reset-gpios = <&gpio0 RK_PA1 GPIO_ACTIVE_LOW>; // 复位引脚,低电平复位
            status = "okay";
        };
    };

核心属性讲解

  1. reg = <0>:SPI 片选编号,我们用的是 SPI1 的 CS0 引脚,所以填 0;
  2. spi-max-frequency = <40000000>:设置 SPI 的最大时钟频率为 40MHz,ST7789 支持最高 60MHz,我们用 40MHz 足够稳定;
  3. spi-cpolspi-cpha:设置 SPI 的工作模式,这里配置为模式 0,和 ST7789 的要求一致;
  4. dc-gpiosreset-gpios:配置 DC 和 RST 引脚,驱动里会通过 GPIO 子系统 API 来控制这两个引脚。

2. 编译烧录验证

  1. 编译设备树,打包 boot.img,烧录到开发板,重启;

  2. 验证 SPI 控制器使能成功: bash

    运行

    复制代码
    adb shell
    su
    ls /sys/bus/spi/devices/spi1.0

    能看到 spi1.0 目录,说明 SPI1 控制器和设备节点已经正常注册,我们就可以开始写驱动了。


五、第二步:SPI 驱动内核代码开发

和 I2C 驱动一样,Linux SPI 子系统给我们提供了一套标准的 API,不用我们关心底层的时序,直接调用就能实现 SPI 数据收发。

1. 核心 SPI 子系统 API 讲解

表格

API 函数 作用
spi_write() 向 SPI 从机发送数据,写操作,最常用
spi_read() 从 SPI 从机读取数据,读操作
spi_write_then_read() 先写后读,常用与寄存器读写
spi_sync() 同步传输,自定义传输序列

对于 ST7789 LCD 屏,我们最常用的就是spi_write(),因为我们需要向屏幕发送命令和显示数据,几乎不需要读取数据。

2. ST7789 驱动核心知识点

ST7789 是 LCD 屏的驱动芯片,我们要控制屏幕显示,核心就是通过 SPI 总线,向芯片写入命令和数据:

  1. DC 引脚控制:DC 引脚拉低的时候,写入的是命令;DC 引脚拉高的时候,写入的是显示数据;
  2. RST 复位引脚:拉低 RST 引脚,延时后拉高,完成屏幕的硬件复位;
  3. 初始化流程:通过 SPI 写入一系列的初始化命令,配置屏幕的分辨率、颜色格式、扫描方向等参数;
  4. 显示数据:设置显示窗口,然后写入对应的像素数据,每个像素用 2 个字节(RGB565 格式)。

3. 完整驱动代码编写

  1. 创建驱动文件: bash

    运行

    复制代码
    cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers
    touch st7789_drv.c
  2. 完整驱动代码,核心部分全注释详解: c

    运行

    复制代码
    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/uaccess.h>
    #include <linux/device.h>
    #include <linux/spi/spi.h>
    #include <linux/gpio.h>
    #include <linux/delay.h>
    #include <linux/gpio/consumer.h>
    
    // 驱动信息声明
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("黒漂技术佬");
    MODULE_DESCRIPTION("RK3568 SPI ST7789 LCD Driver");
    MODULE_VERSION("1.0");
    
    // 宏定义
    #define DEVICE_NAME "st7789_drv"
    #define CLASS_NAME "st7789_class"
    #define LCD_WIDTH  240
    #define LCD_HEIGHT 240
    
    // 控制命令
    #define CMD_LCD_CLR    0x4001  // 清屏
    #define CMD_LCD_DRAW_POINT 0x4002 // 画点
    #define CMD_LCD_FILL_RECT 0x4003 // 填充矩形
    
    // 画点参数结构体
    struct lcd_point {
        unsigned short x;
        unsigned short y;
        unsigned short color;
    };
    
    // 填充矩形参数结构体
    struct lcd_rect {
        unsigned short x1;
        unsigned short y1;
        unsigned short x2;
        unsigned short y2;
        unsigned short color;
    };
    
    // 全局变量
    static dev_t lcd_devno;
    static struct cdev lcd_cdev;
    static struct class *lcd_class;
    static struct device *lcd_device;
    static struct spi_device *lcd_spi;
    static struct gpio_desc *dc_gpio;  // DC引脚
    static struct gpio_desc *rst_gpio; // RST复位引脚
    
    // ====================== LCD底层操作函数 ======================
    // 写命令
    static void lcd_write_cmd(unsigned char cmd)
    {
        gpiod_set_value(dc_gpio, 0); // DC拉低,写命令
        spi_write(lcd_spi, &cmd, 1);
    }
    
    // 写一个字节的数据
    static void lcd_write_data(unsigned char data)
    {
        gpiod_set_value(dc_gpio, 1); // DC拉高,写数据
        spi_write(lcd_spi, &data, 1);
    }
    
    // 写多个字节的数据
    static void lcd_write_buf(unsigned char *buf, int len)
    {
        gpiod_set_value(dc_gpio, 1);
        spi_write(lcd_spi, buf, len);
    }
    
    // 设置显示窗口
    static void lcd_set_window(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2)
    {
        lcd_write_cmd(0x2A); // 列地址设置
        lcd_write_data(x1 >> 8);
        lcd_write_data(x1 & 0xFF);
        lcd_write_data(x2 >> 8);
        lcd_write_data(x2 & 0xFF);
    
        lcd_write_cmd(0x2B); // 行地址设置
        lcd_write_data(y1 >> 8);
        lcd_write_data(y1 & 0xFF);
        lcd_write_data(y2 >> 8);
        lcd_write_data(y2 & 0xFF);
    
        lcd_write_cmd(0x2C); // 开始写入显存
    }
    
    // 清屏函数,填充指定颜色
    static void lcd_clear(unsigned short color)
    {
        unsigned int i;
        unsigned char buf[2] = {color >> 8, color & 0xFF};
    
        lcd_set_window(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1);
        gpiod_set_value(dc_gpio, 1);
        for (i = 0; i < LCD_WIDTH * LCD_HEIGHT; i++) {
            spi_write(lcd_spi, buf, 2);
        }
    }
    
    // 画点函数
    static void lcd_draw_point(unsigned short x, unsigned short y, unsigned short color)
    {
        lcd_set_window(x, y, x, y);
        lcd_write_data(color >> 8);
        lcd_write_data(color & 0xFF);
    }
    
    // 填充矩形
    static void lcd_fill_rect(unsigned short x1, unsigned short y1, unsigned short x2, unsigned short y2, unsigned short color)
    {
        unsigned int i, pixel_num;
        unsigned char buf[2] = {color >> 8, color & 0xFF};
    
        lcd_set_window(x1, y1, x2, y2);
        pixel_num = (x2 - x1 + 1) * (y2 - y1 + 1);
        gpiod_set_value(dc_gpio, 1);
        for (i = 0; i < pixel_num; i++) {
            spi_write(lcd_spi, buf, 2);
        }
    }
    
    // LCD屏幕初始化
    static int lcd_init(void)
    {
        // 硬件复位
        gpiod_set_value(rst_gpio, 0);
        msleep(100);
        gpiod_set_value(rst_gpio, 1);
        msleep(100);
    
        // ST7789初始化命令序列
        lcd_write_cmd(0x36); // 内存数据访问控制
        lcd_write_data(0x00); // 扫描方向,根据屏幕调整
    
        lcd_write_cmd(0x3A); // 颜色格式设置
        lcd_write_data(0x05); // RGB565,16位色
    
        lcd_write_cmd(0xB2); //  porch设置
        lcd_write_data(0x0C);
        lcd_write_data(0x0C);
        lcd_write_data(0x00);
        lcd_write_data(0x33);
        lcd_write_data(0x33);
    
        lcd_write_cmd(0xB7); // VGH设置
        lcd_write_data(0x35);
    
        lcd_write_cmd(0xBB); // VCOM设置
        lcd_write_data(0x19);
    
        lcd_write_cmd(0xC0); // LCM控制
        lcd_write_data(0x2C);
    
        lcd_write_cmd(0xC2); // VDV和VRH使能
        lcd_write_data(0x01);
    
        lcd_write_cmd(0xC3); // VRH设置
        lcd_write_data(0x12);
    
        lcd_write_cmd(0xC4); // VDV设置
        lcd_write_data(0x20);
    
        lcd_write_cmd(0xC6); // 帧率控制
        lcd_write_data(0x0F);
    
        lcd_write_cmd(0xD0); // 电源控制
        lcd_write_data(0xA4);
        lcd_write_data(0xA1);
    
        lcd_write_cmd(0xE0); // 伽马校正
        lcd_write_data(0xD0);
        lcd_write_data(0x04);
        lcd_write_data(0x0D);
        lcd_write_data(0x11);
        lcd_write_data(0x13);
        lcd_write_data(0x2B);
        lcd_write_data(0x3F);
        lcd_write_data(0x54);
        lcd_write_data(0x4C);
        lcd_write_data(0x18);
        lcd_write_data(0x0D);
        lcd_write_data(0x0B);
        lcd_write_data(0x1F);
        lcd_write_data(0x23);
    
        lcd_write_cmd(0xE1);
        lcd_write_data(0xD0);
        lcd_write_data(0x04);
        lcd_write_data(0x0C);
        lcd_write_data(0x11);
        lcd_write_data(0x13);
        lcd_write_data(0x2C);
        lcd_write_data(0x3F);
        lcd_write_data(0x44);
        lcd_write_data(0x51);
        lcd_write_data(0x2F);
        lcd_write_data(0x1F);
        lcd_write_data(0x1F);
        lcd_write_data(0x20);
        lcd_write_data(0x23);
    
        lcd_write_cmd(0x21); // 反显关闭
        lcd_write_cmd(0x11); // 退出睡眠模式
        msleep(120);
        lcd_write_cmd(0x29); // 开启显示
    
        lcd_clear(0x0000); // 清屏,黑色
        printk("【st7789_drv】LCD初始化完成\n");
        return 0;
    }
    
    // ====================== 字符设备核心函数 ======================
    static int lcd_open(struct inode *inode, struct file *filp)
    {
        printk("【st7789_drv】设备被打开\n");
        return 0;
    }
    
    static long lcd_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
    {
        struct lcd_point point;
        struct lcd_rect rect;
        int ret = 0;
    
        switch (cmd) {
            case CMD_LCD_CLR:
                lcd_clear((unsigned short)arg);
                break;
            case CMD_LCD_DRAW_POINT:
                ret = copy_from_user(&point, (struct lcd_point __user *)arg, sizeof(struct lcd_point));
                if (ret) return -EFAULT;
                lcd_draw_point(point.x, point.y, point.color);
                break;
            case CMD_LCD_FILL_RECT:
                ret = copy_from_user(&rect, (struct lcd_rect __user *)arg, sizeof(struct lcd_rect));
                if (ret) return -EFAULT;
                lcd_fill_rect(rect.x1, rect.y1, rect.x2, rect.y2, rect.color);
                break;
            default:
                return -EINVAL;
        }
    
        return ret;
    }
    
    static int lcd_release(struct inode *inode, struct file *filp)
    {
        printk("【st7789_drv】设备被关闭\n");
        return 0;
    }
    
    static const struct file_operations lcd_fops = {
        .owner = THIS_MODULE,
        .open = lcd_open,
        .unlocked_ioctl = lcd_ioctl,
        .release = lcd_release,
    };
    
    // ====================== SPI驱动框架 ======================
    static int st7789_probe(struct spi_device *spi)
    {
        int ret;
        printk("【st7789_drv】驱动和SPI设备匹配成功\n");
        lcd_spi = spi;
    
        // 从设备树获取DC和RST引脚
        dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH);
        if (IS_ERR(dc_gpio)) {
            dev_err(&spi->dev, "获取DC GPIO失败\n");
            return PTR_ERR(dc_gpio);
        }
    
        rst_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH);
        if (IS_ERR(rst_gpio)) {
            dev_err(&spi->dev, "获取RST GPIO失败\n");
            return PTR_ERR(rst_gpio);
        }
    
        // 初始化LCD屏幕
        ret = lcd_init();
        if (ret) {
            dev_err(&spi->dev, "LCD初始化失败\n");
            return ret;
        }
    
        // 注册字符设备
        ret = alloc_chrdev_region(&lcd_devno, 0, 1, DEVICE_NAME);
        if (ret < 0) {
            dev_err(&spi->dev, "设备号申请失败\n");
            return ret;
        }
    
        cdev_init(&lcd_cdev, &lcd_fops);
        lcd_cdev.owner = THIS_MODULE;
        ret = cdev_add(&lcd_cdev, lcd_devno, 1);
        if (ret < 0) {
            dev_err(&spi->dev, "字符设备注册失败\n");
            goto err_devno_free;
        }
    
        lcd_class = class_create(THIS_MODULE, CLASS_NAME);
        if (IS_ERR(lcd_class)) {
            ret = PTR_ERR(lcd_class);
            dev_err(&spi->dev, "设备类创建失败\n");
            goto err_cdev_del;
        }
    
        lcd_device = device_create(lcd_class, NULL, lcd_devno, NULL, DEVICE_NAME);
        if (IS_ERR(lcd_device)) {
            ret = PTR_ERR(lcd_device);
            dev_err(&spi->dev, "设备创建失败\n");
            goto err_class_destroy;
        }
    
        // 开机显示欢迎语
        lcd_fill_rect(0, 0, 239, 30, 0x001F); // 蓝色顶部栏
        lcd_fill_rect(0, 30, 239, 239, 0xFFFF); // 白色背景
    
        dev_info(&spi->dev, "ST7789 LCD驱动加载成功!\n");
        return 0;
    
    err_class_destroy:
        class_destroy(lcd_class);
    err_cdev_del:
        cdev_del(&lcd_cdev);
    err_devno_free:
        unregister_chrdev_region(lcd_devno, 1);
        return ret;
    }
    
    static int st7789_remove(struct spi_device *spi)
    {
        printk("【st7789_drv】驱动开始卸载\n");
        lcd_clear(0x0000);
        lcd_write_cmd(0x28); // 关闭显示
        device_destroy(lcd_class, lcd_devno);
        class_destroy(lcd_class);
        cdev_del(&lcd_cdev);
        unregister_chrdev_region(lcd_devno, 1);
        dev_info(&spi->dev, "ST7789 LCD驱动卸载成功!\n");
        return 0;
    }
    
    // SPI设备ID匹配表
    static const struct spi_device_id st7789_id[] = {
        {"st7789,lcd", 0},
        {}
    };
    MODULE_DEVICE_TABLE(spi, st7789_id);
    
    // 设备树匹配表
    static const struct of_device_id st7789_of_match[] = {
        { .compatible = "st7789,lcd" },
        {}
    };
    MODULE_DEVICE_TABLE(of, st7789_of_match);
    
    // SPI驱动结构体
    static struct spi_driver st7789_driver = {
        .driver = {
            .name = "st7789_lcd",
            .of_match_table = st7789_of_match,
        },
        .probe = st7789_probe,
        .remove = st7789_remove,
        .id_table = st7789_id,
    };
    
    // ====================== 驱动入口和出口 ======================
    static int __init st7789_drv_init(void)
    {
        printk("【st7789_drv】ST7789 LCD驱动开始加载\n");
        return spi_register_driver(&st7789_driver);
    }
    
    static void __exit st7789_drv_exit(void)
    {
        spi_unregister_driver(&st7789_driver);
    }
    
    module_init(st7789_drv_init);
    module_exit(st7789_drv_exit);

4. 编译驱动,烧录验证

  1. 修改 Makefile,添加 SPI LCD 驱动的编译: makefile

    复制代码
    obj-y += hello_drv.o
    obj-y += gpio_drv.o
    obj-y += key_irq_drv.o
    obj-y += pwm_drv.o
    obj-y += oled_drv.o
    obj-y += st7789_drv.o
  2. 编译内核,打包 boot.img,烧录到开发板,重启;

  3. 验证驱动加载成功: bash

    运行

    复制代码
    adb shell
    su
    dmesg | grep st7789_drv

    能看到「ST7789 LCD 驱动加载成功」的日志,同时 LCD 屏会显示我们设置的开机画面,说明驱动工作正常!

  4. 查看设备文件,设置权限: bash

    运行

    复制代码
    ls -l /dev/st7789_drv
    chmod 777 /dev/st7789_drv

六、第三步:HAL 层适配 + 安卓 App 开发

和之前的流程一样,我们完成 HAL 层适配,然后写一个安卓 App,实现清屏、画矩形、设置背景色等功能,用户可以在 App 上操作,实时控制 LCD 屏的显示。

1. HAL 层核心代码

c

运行

复制代码
// 清屏
int lcd_clear(unsigned short color)
{
    if (lcd_dev_init() < 0) return -1;
    return ioctl(fd, CMD_LCD_CLR, color);
}

// 填充矩形
int lcd_fill_rect(struct lcd_rect *rect)
{
    if (lcd_dev_init() < 0) return -1;
    return ioctl(fd, CMD_LCD_FILL_RECT, rect);
}

// 画点
int lcd_draw_point(struct lcd_point *point)
{
    if (lcd_dev_init() < 0) return -1;
    return ioctl(fd, CMD_LCD_DRAW_POINT, point);
}

2. 安卓 App 功能

App 里添加颜色选择器、坐标输入框,用户可以选择颜色,设置矩形的坐标,点击按钮就能在 LCD 屏上画出对应的图形,还可以一键清屏,设置背景色。


七、小白 SPI 驱动必踩的坑,提前规避

  1. 坑 1:SPI 通信完全没反应,屏幕不亮 90% 的情况是这几个问题:
    • 引脚复用配置错了,没有把引脚配置为 SPI 功能;
    • 接线错了,SCLK、MOSI 接反了,或者 CS 引脚接错了;
    • DC 和 RST 引脚配置错了,没有正常复位屏幕;
    • SPI 工作模式不对,主机和从机的 CPOL/CPHA 不一致。
  2. 坑 2:屏幕能初始化,但是显示乱码、花屏
    • SPI 速率太高,超过了屏幕支持的最大速率,降低 spi-max-frequency 试试;
    • 屏幕的初始化命令不对,不同厂家的 ST7789 屏,初始化命令会有差异,参考屏幕厂家提供的 datasheet;
    • 颜色格式不对,RGB565 的高低字节搞反了,导致颜色错乱。
  3. 坑 3:SPI 读写返回错误
    • 片选引脚配置错了,CS 引脚没有拉低,从机没有被选中;
    • 设备树里的 reg 属性和片选编号不一致,CS0 对应 reg=<0>,CS1 对应 reg=<1>。
  4. 坑 4:多线程调用 SPI 函数,导致内核崩溃SPI 子系统的 API 不是线程安全的,多线程同时调用必须加锁,不然会导致内核崩溃。
  5. 坑 5:SPI 速率上不去检查设备树里的 spi-max-frequency 属性,不要超过 RK3568 SPI 控制器的最大支持速率 50MHz,同时也要考虑从机的最大支持速率。

结尾说两句

这篇文章,我们彻底搞懂了 SPI 总线的核心原理,完成了 RK 平台 SPI 设备驱动的完整开发,实现了 ST7789 SPI LCD 屏的驱动和显示控制,打通了安卓 App 的全链路。掌握了 SPI 驱动开发,你就能适配高速 LCD 屏、SPI Flash、WiFi 模块、高速 ADC 等高速外设,嵌入式开发的能力又上了一个台阶。

到这里,我们的第四卷「基础驱动实战篇」就全部完成了,你已经掌握了 GPIO、中断、PWM、I2C、SPI 这五大嵌入式核心外设的驱动开发,能独立完成绝大多数外设的驱动适配了。

下一篇,我们进入第五卷「进阶实战与系统调试篇」,从 ** 输入设备驱动(触摸屏 / 按键)** 开始,教你怎么用 Linux input 子系统,实现标准的输入设备驱动,让安卓系统原生识别你的输入设备。

我是黒漂技术佬,关注我,带你零基础入门 RK 安卓驱动开发,不踩坑。有任何 SPI 驱动的问题,评论区留言,我都会一一回复。

相关推荐
Predestination王瀞潞2 小时前
Mysql忘记密码重置的方法
android·mysql·adb
闻哥2 小时前
MySQL三大日志深度解析:redo log、undo log、binlog 原理与实战
android·java·jvm·数据库·mysql·adb·面试
阿拉斯攀登2 小时前
【RK3576 安卓 JNI/NDK 系列 02】保姆级环境搭建,从 0 到跑通第一个 JNI 程序
android studio·瑞芯微·嵌入式驱动·安卓驱动·安卓ndk环境搭建 jni入门
非凡ghost2 小时前
Smart Launcher安卓版(安卓桌面启动器)
android·windows·学习·音视频·软件需求
轩情吖2 小时前
MySQL之复合查询
android·数据库·mysql·多表·符合查询·自连接·合并查询
飞鱼计划2 小时前
在 MySQL 中,处理锁表问题
android
星轨初途2 小时前
郑州轻工业大学“筑梯杯” 2025级新生程序设计大赛暨省内高校邀请赛——题解
android·c++·经验分享·笔记·算法
路溪非溪2 小时前
关于Linux中的日志问题
linux·arm开发·驱动开发
黄林晴2 小时前
Android内核引入AuroFDO,你的App变快了
android