第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例
目录开篇先搞懂SPI 总线到底是什么和 I2C 有啥区别大白话定义SPI 和 I2C 的核心区别小白一眼看懂一、SPI 总线的核心原理小白必懂1. SPI 的 4 根信号线核心通信逻辑2. SPI 的 4 种工作模式CPOL/CPHA1CPOL时钟极性2CPHA时钟相位4 种工作模式二、RK3568 SPI 控制器详解核心特点三、实战前的硬件准备硬件清单硬件接线四、第一步设备树配置1. 修改板级设备树文件核心属性讲解2. 编译烧录验证五、第二步SPI 驱动内核代码开发1. 核心 SPI 子系统 API 讲解2. ST7789 驱动核心知识点3. 完整驱动代码编写4. 编译驱动烧录验证六、第三步HAL 层适配 安卓 App 开发1. HAL 层核心代码2. 安卓 App 功能七、小白 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、CS2 根SCL、SDA通信模式全双工收发可以同时进行半双工收发不能同时进行速率高速几十 MHz~ 上百 MHz低速最高 4Mbps常用 100kbps/400kbps多从机支持每个从机需要一个独立的 CS 片选引脚一条总线最多挂 127 个从机只用两根线通过地址区分硬件流控无应答机制需要软件保证通信可靠有应答机制能确认数据是否传输成功适用场景高速数据传输比如 LCD 屏、Flash、高速 ADC低速外设比如传感器、OLED 屏、低速率芯片简单总结I2C 的优势是引脚少布线简单适合低速、多设备的场景SPI 的优势是速率高全双工适合高速数据传输的场景就是多一个从机就要多一个 CS 引脚布线会复杂一点。一、SPI 总线的核心原理小白必懂1. SPI 的 4 根信号线SPI 总线需要 4 根信号线分别是表格信号线全称作用方向SCLKSerial Clock串行时钟线由主机提供同步数据传输主机→从机MOSIMaster Out Slave In主机输出从机输入主机向从机发送数据主机→从机MISOMaster In Slave Out主机输入从机输出从机向主机发送数据从机→主机CS/SSChip Select片选线主机通过拉低对应的 CS 引脚选中要通信的从机主机→从机核心通信逻辑片选机制SPI 总线上可以挂多个从机每个从机有独立的 CS 片选引脚。主机要和哪个从机通信就把对应的 CS 引脚拉低其他从机的 CS 引脚保持高电平不会响应总线的信号全双工通信在 SCLK 时钟的驱动下主机通过 MOSI 线向从机发送数据同时从机通过 MISO 线向主机发送数据收发是同时进行的一个时钟周期主机和从机各完成 1 位数据的收发主从架构只有主机能发起通信提供时钟信号从机只能响应主机的通信不能主动发起。2. SPI 的 4 种工作模式CPOL/CPHA这是 SPI 通信最核心的知识点也是新手最容易踩坑的地方。SPI 有 4 种工作模式由两个参数决定CPOL时钟极性和CPHA时钟相位。1CPOL时钟极性决定了 SPI 总线在空闲状态下SCLK 时钟线的电平CPOL0空闲状态下SCLK 是低电平CPOL1空闲状态下SCLK 是高电平。2CPHA时钟相位决定了数据在哪个时钟沿被采样读取CPHA0在 SCLK 的第一个跳变沿上升沿 / 下降沿采样数据第二个跳变沿更新数据CPHA1在 SCLK 的第二个跳变沿采样数据第一个跳变沿更新数据。4 种工作模式表格模式CPOLCPHA空闲电平采样沿模式 000低电平上升沿模式 101低电平下降沿模式 210高电平下降沿模式 311高电平上升沿小白必记主机和从机的工作模式必须完全一致不然数据收发会完全错乱通信失败。我们要驱动的 SPI 设备它的 datasheet 里会明确说明支持哪种工作模式我们只需要在驱动里配置成对应的模式就行。最常用的是模式 0CPOL0, CPHA0和模式 3CPOL1, CPHA1。二、RK3568 SPI 控制器详解RK3568 芯片内置了3 路硬件 SPI 控制器分别是 SPI0、SPI1、SPI2每一路都独立工作支持最高 50MHz 的时钟频率完全满足高速数据传输的需求。核心特点官方 SDK 里已经实现了完整的 Linux SPI 子系统驱动我们不用手动模拟时序不用操作底层寄存器只需要调用内核提供的 SPI 子系统 API就能实现 SPI 通信开发难度极低每一路 SPI 都对应了固定的 GPIO 引脚我们只需要在设备树里配置引脚复用就能使用我们这次实战用SPI1对应的引脚是SCLKGPIO1_B0复用功能 2MOSIGPIO1_B1复用功能 2MISOGPIO1_B2复用功能 2CS0GPIO1_B3复用功能 2。三、实战前的硬件准备我们这次的实战目标基于 RK3568 的 SPI1 控制器驱动 1.3 寸 ST7789 SPI LCD 屏240*240 分辨率SPI 接口实现屏幕清屏、画点、显示图片、显示字符串的功能并且打通安卓 App 控制屏幕显示的全链路。硬件清单RK3568 开发板 1 块1.3 寸 ST7789 SPI LCD 屏 1 个240*240 分辨率3.3V 供电杜邦线 6 根面包板 1 个可选。硬件接线表格RK3568 开发板引脚LCD 屏引脚说明3.3VVCC屏幕供电3.3V别接 5VGNDGND共地必须接GPIO1_B0SPI1_SCLKSCL/SCKSPI 时钟线GPIO1_B1SPI1_MOSISDA/MOSISPI 数据输入线主机向屏幕发数据GPIO1_B3SPI1_CS0CS/SS片选线GPIO0_A0普通 GPIODC数据 / 命令选择线高电平是数据低电平是命令GPIO0_A1普通 GPIORST复位引脚低电平复位小白避坑ST7789 SPI 屏一般有两个额外的引脚DC数据 / 命令选择和 RST复位这两个引脚用普通 GPIO 就能控制不用 SPI 引脚屏幕是 3.3V 供电的绝对不能接 5V不然直接烧屏接线的时候MOSI 接屏幕的 SDAMISO 不用接因为 LCD 屏只需要接收数据不需要向主机发送数据MISO 引脚可以悬空所有设备必须共地不然通信会乱码。四、第一步设备树配置我们需要在设备树里添加 SPI LCD 设备节点配置 SPI 引脚复用使能 SPI1 控制器配置 DC 和 RST 的 GPIO 引脚。1. 修改板级设备树文件进入设备树目录打开你的开发板对应的.dts 文件bash运行cd ~/RK3568_Android11_SDK/kernel/arch/arm64/boot/dts/rockchip/ vim rk3568-firefly.dts添加 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: st77890 { compatible st7789,lcd; reg 0; // SPI片选编号CS0对应0 spi-max-frequency 40000000; // SPI最大速率40MHz spi-cpol 0; // CPOL0空闲电平低 spi-cpha 0; // CPHA0模式0 dc-gpios gpio0 RK_PA0 GPIO_ACTIVE_HIGH; // DC引脚 reset-gpios gpio0 RK_PA1 GPIO_ACTIVE_LOW; // 复位引脚低电平复位 status okay; }; };核心属性讲解reg 0SPI 片选编号我们用的是 SPI1 的 CS0 引脚所以填 0spi-max-frequency 40000000设置 SPI 的最大时钟频率为 40MHzST7789 支持最高 60MHz我们用 40MHz 足够稳定spi-cpol和spi-cpha设置 SPI 的工作模式这里配置为模式 0和 ST7789 的要求一致dc-gpios和reset-gpios配置 DC 和 RST 引脚驱动里会通过 GPIO 子系统 API 来控制这两个引脚。2. 编译烧录验证编译设备树打包 boot.img烧录到开发板重启验证 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 总线向芯片写入命令和数据DC 引脚控制DC 引脚拉低的时候写入的是命令DC 引脚拉高的时候写入的是显示数据RST 复位引脚拉低 RST 引脚延时后拉高完成屏幕的硬件复位初始化流程通过 SPI 写入一系列的初始化命令配置屏幕的分辨率、颜色格式、扫描方向等参数显示数据设置显示窗口然后写入对应的像素数据每个像素用 2 个字节RGB565 格式。3. 完整驱动代码编写创建驱动文件bash运行cd ~/RK3568_Android11_SDK/kernel/drivers/char/my_drivers touch st7789_drv.c完整驱动代码核心部分全注释详解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); // RGB56516位色 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. 编译驱动烧录验证修改 Makefile添加 SPI LCD 驱动的编译makefileobj-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编译内核打包 boot.img烧录到开发板重启验证驱动加载成功bash运行adb shell su dmesg | grep st7789_drv能看到「ST7789 LCD 驱动加载成功」的日志同时 LCD 屏会显示我们设置的开机画面说明驱动工作正常查看设备文件设置权限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 驱动必踩的坑提前规避坑 1SPI 通信完全没反应屏幕不亮90% 的情况是这几个问题引脚复用配置错了没有把引脚配置为 SPI 功能接线错了SCLK、MOSI 接反了或者 CS 引脚接错了DC 和 RST 引脚配置错了没有正常复位屏幕SPI 工作模式不对主机和从机的 CPOL/CPHA 不一致。坑 2屏幕能初始化但是显示乱码、花屏SPI 速率太高超过了屏幕支持的最大速率降低 spi-max-frequency 试试屏幕的初始化命令不对不同厂家的 ST7789 屏初始化命令会有差异参考屏幕厂家提供的 datasheet颜色格式不对RGB565 的高低字节搞反了导致颜色错乱。坑 3SPI 读写返回错误片选引脚配置错了CS 引脚没有拉低从机没有被选中设备树里的 reg 属性和片选编号不一致CS0 对应 reg0CS1 对应 reg1。坑 4多线程调用 SPI 函数导致内核崩溃SPI 子系统的 API 不是线程安全的多线程同时调用必须加锁不然会导致内核崩溃。坑 5SPI 速率上不去检查设备树里的 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 驱动的问题评论区留言我都会一一回复。