8. Linux内核SPI子系统¶
SPI(Serial Peripheral Interface,串行外设接口)是一种高速同步串行通信总线,由摩托罗拉(Motorola)公司推出, 凭借全双工、高速率、同步通信、抗干扰能力强等特点,广泛应用于嵌入式系统中,用于连接各类高速外设, 如OLED显示屏、SPI Flash、ADC转换器、传感器、SD卡等。
Linux内核为简化SPI设备驱动的开发,设计实现了SPI子系统,它是一套标准化的驱动框架,将SPI总线的管理、设备枚举、数据传输等功能进行抽象封装, 屏蔽了底层硬件(不同芯片的SPI控制器)的差异。开发者无需关注SPI控制器的底层实现细节,只需按照子系统规范编写设备驱动,即可实现SPI设备的正常通信。
8.1. SPI基础知识¶
8.1.1. SPI总线简介¶
SPI总线是一种同步串行通信总线,默认采用主从模式,主设备(如CPU)负责产生时钟信号(SCLK),控制通信节奏,发起数据传输; 从设备(如SPI Flash)被动响应主设备的指令,根据时钟信号同步发送或接收数据。
SPI总线的核心信号线(4根,标准配置)如下,不同场景下可省略部分引脚,如单工通信可省略MISO或MOSI:
SCLK(Serial Clock,串行时钟):由主设备产生,用于同步主从设备的数据传输,时钟频率决定通信速率。
MOSI(Master Out Slave In,主发从收):主设备向从设备发送数据的信号线。
MISO(Master In Slave Out,主收从发):从设备向主设备发送数据的信号线,全双工通信时与MOSI同时工作。
CS(Chip Select,片选信号):由主设备控制,默认低电平有效,用于选择当前要通信的从设备;一条SPI总线上可连接多个从设备,通过不同的CS引脚区分。
部分SPI设备支持SPI总线扩展(如SPI-4、QSPI),可实现更高的通信速率或多设备扩展,但核心通信原理与标准SPI一致。
8.1.2. SPI四种工作模式¶
SPI总线有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)两个参数决定,四种模式的核心区别在于SCLK的空闲电平(CPOL)和数据采样的时机(CPHA), 内核中通过struct spi_device的mode成员配置:
1 2 3 4 5 6 | #define SPI_CPHA 0x01 // 时钟相位
#define SPI_CPOL 0x02 // 时钟极性
#define SPI_MODE_0 (0|0) // CPOL=0(空闲时低电平),CPHA=0(第一个边沿(上升沿)采样)
#define SPI_MODE_1 (0|SPI_CPHA) // CPOL=0(空闲时低电平),CPHA=1(第二个边沿(下降沿)采样)
#define SPI_MODE_2 (SPI_CPOL|0) // CPOL=1(空闲时高电平),CPHA=0(第一个边沿(下降沿)采样)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA) // CPOL=1(空闲时高电平),CPHA=1(第二个边沿(上升沿)采样)
|
8.1.3. SPI时序¶
SPI总线的通信过程遵循简单的同步协议,核心由“片选拉低->时钟产生->数据传输->片选拉高”四个步骤组成。
片选拉低:主设备拉低对应从设备的CS引脚,告知该从设备准备进行通信,其他从设备保持高电平(未选中状态),不响应任何信号。
时钟产生:主设备根据配置的工作模式,产生SCLK时钟信号,时钟频率由主设备设定,但需匹配从设备的最大支持速率。
数据传输:主设备和从设备同步传输数据,每一个SCLK时钟周期传输1位数据:
MOSI线:主设备在时钟的指定沿(由CPHA决定)输出数据位,从设备在对应沿采样数据。
MISO线:从设备在时钟的指定沿输出数据位,主设备在对应沿采样数据。
数据传输为全双工,主从设备同时发送和接收数据,传输的字节数由主设备控制。
片选拉高:数据传输完成后,主设备拉高CS引脚,告知从设备通信结束,从设备进入空闲状态。
SCK(CPOL=0)/SCK(CPOL=1),采样(CPHA=0)的时序如下图:
SCK(CPOL=0),采样(CPHA=0):SPI_MODE_0模式,SCLK空闲电平为低电平,数据在SCLK的上降沿被采样,下升沿主从设备输出下一位数据。
SCK(CPOL=1),采样(CPHA=0):SPI_MODE_2模式,SCLK空闲电平为高电平,数据在SCLK的下降沿被采样,上升沿主从设备输出下一位数据。
SCK(CPOL=0)/SCK(CPOL=1),采样(CPHA=1)的时序如下图:
SCK(CPOL=0),采样(CPHA=1):SPI_MODE_1模式,SCLK空闲电平为低电平,数据在SCLK的下降沿被采样,上升沿主从设备输出下一位数据。
SCK(CPOL=1),采样(CPHA=1):SPI_MODE_3模式,SCLK空闲电平为高电平,数据在SCLK的上降沿被采样,下升沿主从设备输出下一位数据。
8.2. SPI子系统¶
8.2.1. 核心定义¶
Linux内核SPI子系统是一套用于管理SPI总线和SPI设备的标准化框架,其核心目标是“统一接口、简化开发、屏蔽硬件差异”。 与I2C子系统类似,SPI子系统也采用分层设计,将SPI相关的驱动分为两层:SPI总线驱动(也称为SPI主控制器驱动)和SPI设备驱动, 两层通过SPI核心层进行交互,实现总线管理、设备枚举和数据传输的标准化。
SPI子系统的核心逻辑:核心层提供统一的接口,总线驱动负责管理SPI主控制器(SPI Master),实现底层总线的初始化和数据传输; 设备驱动负责管理具体的SPI从设备(SPI Slave),通过核心层调用总线驱动的接口,实现与从设备的通信。
8.2.2. SPI核心层¶
SPI核心层是SPI子系统的枢纽,由内核源码中的drivers/spi/spi.c等文件实现,提供了总线驱动和设备驱动的注册、注销接口, 以及数据传输、设备枚举等核心功能。
核心层的主要职责包括:
管理所有已注册的SPI主控制器(SPI Master)和SPI从设备(SPI Slave)。
提供标准化的数据传输接口(如spi_sync、spi_async),供设备驱动调用。
实现SPI设备的枚举,根据设备树配置或板级信息,匹配总线驱动和设备驱动。
协调总线驱动与设备驱动的交互,屏蔽底层硬件差异,确保不同厂商的SPI控制器驱动可通用。
管理SPI总线的并发访问,通过自旋锁或互斥锁,防止多进程同时操作SPI总线导致的数据冲突。
8.2.3. SPI总线驱动¶
SPI总线驱动也称为SPI主控制器驱动,对应物理层面的SPI主控制器(如SOC内置的SPI控制器), 如内核源码中rockchip平台的drivers/spi/spi-rockchip.c。
其核心职责包括:
初始化SPI主控制器,配置通信速率、工作模式(CPOL/CPHA)、数据位长度、CS引脚等参数。
实现SPI总线的底层数据传输逻辑,响应核心层的数据传输请求(如spi_sync调用)。
向核心层注册SPI主控制器(struct spi_master),供设备驱动调用。
管理SPI控制器的硬件资源(如时钟、引脚、中断),驱动卸载时释放相关资源。
8.2.4. SPI设备驱动¶
SPI设备驱动对应具体的SPI从设备(如SPI Flash),由开发者根据具体设备编写。
其核心职责包括:
向核心层注册SPI设备驱动(struct spi_driver),告知核心层设备的兼容属性、支持的工作模式等信息。
实现设备的初始化(如寄存器配置、GPIO申请)、数据读取、数据写入等逻辑。
通过核心层提供的接口(如spi_sync、spi_write),调用总线驱动的底层功能,完成与从设备的通信。
驱动卸载时,释放设备占用的资源(如GPIO、字符设备节点等)。
8.3. SPI子系统工作流程¶
SPI子系统的完整工作流程可概括为以下步骤,基于通用场景说明:
系统启动时,SPI总线驱动初始化SPI主控制器(如SPI1、SPI2),配置控制器的时钟、引脚、工作模式等参数,并向核心层注册SPI主控制器(struct spi_master)。
设备树加载时,核心层解析设备树中SPI设备节点(如spi_oled@0),获取设备的从地址(片选编号)、兼容属性、最大通信速率、工作模式等信息。
SPI设备驱动加载时,向核心层注册设备驱动(struct spi_driver),核心层根据驱动的兼容属性(of_match_table),将设备驱动与对应的SPI主控制器和SPI从设备匹配。
匹配成功后,调用设备驱动的probe函数,完成设备初始化(如配置设备寄存器、申请GPIO、注册字符设备等)。
应用层通过字符设备接口(如/dev/spi_oled)发起数据读写请求,设备驱动通过核心层的spi_sync等接口,调用总线驱动的底层功能,与SPI从设备进行通信,完成数据的读取或写入。
驱动卸载时,调用设备驱动的remove函数,释放资源;总线驱动保持运行,可继续为其他SPI设备提供服务。
8.4. SPI子系统核心结构体¶
SPI子系统的核心结构体均由内核定义,开发者只需声明和使用,无需手动定义。
8.4.1. SPI主控制器结构体¶
SPI主控制器结构体(struct spi_master)是SPI主控制器的抽象,总线驱动的核心工作就是初始化该结构体,实现transfer、transfer_one等核心操作函数,将其注册到SPI核心层。 核心层通过该结构体管理SPI主控制器的功能、状态及事务队列,设备驱动通过该结构体提供的接口(如transfer)实现与从设备的数据传输。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 宏定义将spi_master映射为spi_controller,两者本质一致,便于代码兼容和统一管理
#define spi_master spi_controller
struct spi_controller {
struct device dev; // 继承通用设备结构体,用于Linux设备模型管理,统一管理控制器资源
struct list_head list; // 链表节点,用于将当前主控制器加入SPI核心层的主控制器链表,便于核心层统一管理
s16 bus_num; // SPI主控制器的总线编号,如0对应spi0、1对应spi1,用于区分不同主控制器
u16 num_chipselect; // 主控制器支持的片选引脚数量,决定该主控制器可挂载的最大从设备数量
...
int (*setup)(struct spi_device *spi); // 配置SPI从设备参数的函数,当从设备挂载或参数变更时调用,配置通信模式、速率等
int (*transfer)(struct spi_device *spi, // 兼容旧版本的SPI数据传输函数,用于实现底层数据传输逻辑
struct spi_message *mesg);
void (*cleanup)(struct spi_device *spi); // 清理SPI从设备相关资源的函数,当从设备卸载时调用,释放对应资源
...
struct list_head queue; // SPI事务队列,用于缓存待传输的spi_message,实现事务的有序执行
struct spi_message *cur_msg; // 指向当前正在传输的SPI事务spi_message,用于跟踪当前传输状态
...
// 核心传输函数:单次SPI传输段spi_transfer的底层实现,负责单个传输段的数据收发
int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi,struct spi_transfer *transfer);
// 传输前准备函数:在SPI事务传输前,初始化硬件资源
int (*prepare_transfer_hardware)(struct spi_controller *ctlr);
// 完整事务传输函数:实现单个SPI事务的完整传输逻辑,调用transfer_one完成每个段的传输
int (*transfer_one_message)(struct spi_controller *ctlr,struct spi_message *mesg);
// 片选控制函数:用于控制SPI从设备的片选引脚CS状态使能/禁用,替代默认片选控制逻辑
void (*set_cs)(struct spi_device *spi, bool enable);
...
int *cs_gpios; // 片选引脚的GPIO数组指针,当使用GPIO模拟片选时,存储各从设备的片选GPIO编号
}
|
8.4.2. SPI从设备结构体¶
SPI从设备结构体(struct spi_device)用于描述SPI从设备的基本信息,每个SPI从设备对应一个该结构体, 由核心层根据设备树配置或板级信息自动创建,设备驱动通过probe函数获取该结构体指针, 进而获取从设备的挂载主控制器、片选编号、最大速率等信息,实现与从设备的通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | struct spi_device {
struct device dev; // 继承通用设备结构体,用于Linux设备模型统一管理
struct spi_controller *controller; // 指向该从设备挂载的SPI主控制器spi_controller指针
struct spi_controller *master; // 兼容层字段,与controller等价,用于兼容旧版本驱动
u32 max_speed_hz; // 从设备支持的最大通信速率,需与主控制器配置匹配
u8 chip_select; // 片选编号,对应主控制器的CS引脚,用于主设备选择该从设备
u8 bits_per_word; // 从设备支持的数据位长度,默认8位,可配置
u16 mode; // 从设备的工作模式
...
int irq; // 从设备的中断号
...
int cs_gpio; // 片选引脚对应的GPIO编号,用于GPIO模拟片选的场景
...
};
|
8.4.3. SPI设备驱动结构体¶
SPI设备驱动结构体(struct spi_driver)用于描述SPI设备驱动的基本信息,是设备驱动的核心结构体, 开发者需配置该结构体的probe、remove函数,以及id_table用于与SPI从设备匹配,然后将其注册到核心层。
1 2 3 4 5 6 7 | struct spi_driver {
const struct spi_device_id *id_table; // 设备ID匹配表
in (*probe)(struct spi_device *spi); // probe函数
int (*remove)(struct spi_device *spi); // remove函数
void (*shutdown)(struct spi_device *spi);// shutdown函数,系统关机时执行
struct device_driver driver; // 继承通用设备驱动结构体,用于驱动模型管理
};
|
8.4.4. SPI传输段结构体¶
SPI传输段结构体(struct spi_transfer)用于描述一次SPI传输的单个段(如一次写操作或一次读操作),是数据传输的基本单元。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | struct spi_transfer {
const void *tx_buf; // 发送数据缓冲区
void *rx_buf; // 接收数据缓冲区
unsigned len; // 传输数据长度
dma_addr_t tx_dma; // 发送数据的DMA地址
dma_addr_t rx_dma; // 接收数据的DMA地址
...
unsigned cs_change:1; // 传输结束后是否切换CS引脚状态,1=切换,0=不切换
unsigned tx_nbits:3; // 发送数据的位数,默认8位,可配置为1、2、4、8、16、32
unsigned rx_nbits:3; // 接收数据的位数,默认8位,与tx_nbits一致
...
u32 speed_hz; // 本次传输的速率,优先级高于spi_device的max_speed_hz
...
struct list_head transfer_list; // 用于将多个传输段链接成SPI消息
};
|
一次SPI通信可包含多个传输段(如先写寄存器地址,再读寄存器数据),多个spi_transfer结构体通过transfer_list成员链接成一个spi_message结构体, 由总线驱动的transfer函数依次执行。
8.4.5. SPI传输事务结构体¶
SPI传输事务结构体(struct spi_message)用于描述一次完整的SPI通信事务,包含一个或多个spi_transfer传输段。
1 2 3 4 5 6 7 8 9 | struct spi_message {
struct list_head transfers; // 该事务包含的所有传输段链表
struct spi_device *spi; // 该事务对应的SPI从设备
unsigned is_dma_mapped:1; // 是否使用DMA传输,1=使用,0=不使用
void (*complete)(void *context); // 事务完成后的回调函数,用于异步传输
void *context; // 回调函数的参数
int status; // 事务传输状态,0=成功,负数=错误码
// 省略其他辅助成员
};
|
spi_message是SPI数据传输的最小单元,一个spi_message可包含多个spi_transfer,调用spi_sync函数时,会将整个spi_message传输完成后才返回。 异步传输时,可通过complete回调函数获取传输结果。
8.5. SPI驱动源码分析¶
8.5.1. SPI核心层¶
8.5.1.1. SPI总线注册¶
1 2 3 4 5 6 7 8 9 | static int __init spi_init(void)
{
int status;
...
status = bus_register(&spi_bus_type);
...
status = class_register(&spi_master_class);
...
}
|
在系统开机的时候就会执行,自动进行SPI总线注册。 当总线注册成功之后,会在/sys/bus下面生成一个SPI总线,然后在系统中新增一个设备类,/sys/class/目录下会可以找到spi_master类。
8.5.1.2. SPI总线定义¶
1 2 3 4 5 6 | struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
|
spi_bus_type总线定义,会在spi总线注册时使用,.match函数指针,设定了SPI设备和SPI驱动的匹配规则,具体如下spi_match_device()函数。
8.5.1.3. spi_match_device()函数¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | static int spi_match_device(struct device *dev, struct device_driver *drv)
{
const struct spi_device *spi = to_spi_device(dev);
const struct spi_driver *sdrv = to_spi_driver(drv);
/* Attempt an OF style match */
if (of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
|
spi_match_device()函数提供了四种匹配方式,设备树匹配方式、acpi匹配方式以及id_table匹配方式,如果前面三种都没有匹配成功,则通过设备名进行配对。
8.5.2. SPI控制器驱动¶
SPI控制器驱动,由硬件供应商提供,我们只需要了解,学习其原理就即可。
以rk3568的SPI控制器为例进行说明,使用的rk3568芯片有4个SPI控制器,对应的设备树存在4个SPI节点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | spi3: spi@fe640000 {
compatible = "rockchip,rk3066-spi";
reg = <0x0 0xfe640000 0x0 0x1000>;
interrupts = <GIC_SPI 106 IRQ_TYPE_LEVEL_HIGH>;
#address-cells = <1>;
#size-cells = <0>;
clocks = <&cru CLK_SPI3>, <&cru PCLK_SPI3>;
clock-names = "spiclk", "apb_pclk";
dmas = <&dmac0 26>, <&dmac0 27>;
dma-names = "tx", "rx";
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins>;
pinctrl-1 = <&spi3m0_cs0 &spi3m0_cs1 &spi3m0_pins_hs>;
status = "disabled";
};
|
reg:为spi3寄存器组相关的起始地址为0xfe640000,寄存器长度为0x1000。
compatible:属性值与主机驱动匹配;在rk3568的SPI控制器驱动文件spi-rockchip.c中可以找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 | static const struct of_device_id rockchip_spi_dt_match[] = {
{ .compatible = "rockchip,px30-spi", },
{ .compatible = "rockchip,rv1108-spi", },
{ .compatible = "rockchip,rv1126-spi", },
{ .compatible = "rockchip,rk3036-spi", },
{ .compatible = "rockchip,rk3066-spi", },
{ .compatible = "rockchip,rk3188-spi", },
{ .compatible = "rockchip,rk3228-spi", },
{ .compatible = "rockchip,rk3288-spi", },
{ .compatible = "rockchip,rk3368-spi", },
{ .compatible = "rockchip,rk3399-spi", },
{ },
};
|
驱动控制器通过下面module_platform_driver()进行注册:
1 2 3 4 5 6 7 8 9 10 11 | static struct platform_driver rockchip_spi_driver = {
.driver = {
.name = DRIVER_NAME,
.pm = &rockchip_spi_pm,
.of_match_table = of_match_ptr(rockchip_spi_dt_match),
},
.probe = rockchip_spi_probe,
.remove = rockchip_spi_remove,
};
module_platform_driver(rockchip_spi_driver);
|
当匹配到“”rockchip,rk3066-spi””时,调用rockchip_spi_probe函数,进行初始化,获取设备树节点信息、初始化spi时钟、dma、中断等, 最后控制器的注册。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | static int rockchip_spi_probe(struct platform_device *pdev)
{
int ret;
/* 定义瑞芯微SPI控制器私有数据结构体指针 */
struct rockchip_spi *rs;
/* 定义SPI主控制器结构体 */
struct spi_controller *ctlr;
/* 获取设备树节点:从platform device中取出对应的设备树节点 */
struct device_node *np = pdev->dev.of_node;
/* .............. */
/* 标记当前 SPI 是主机模式还是从机模式 */
bool slave_mode;
/* 定义引脚控制pinctrl结构体 */
struct pinctrl *pinctrl = NULL;
/* 从设备树读取属性,用来判断是否配置为SPI从机模式 */
slave_mode = of_property_read_bool(np, "spi-slave");
/* 如果是从机模式 */
if (slave_mode)
/* 分配一个SPI从机控制器,大小为struct rockchip_spi */
ctlr = spi_alloc_slave(&pdev->dev,
sizeof(struct rockchip_spi));
else
/* 分配一个SPI主机控制器,大小为struct rockchip_spi */
ctlr = spi_alloc_master(&pdev->dev,
sizeof(struct rockchip_spi));
/* 分配失败则返回内存不足错误 */
if (!ctlr)
return -ENOMEM;
/* 将SPI控制器结构体保存到platform device的私有数据中 */
platform_set_drvdata(pdev, ctlr);
/* 从SPI控制器中取出rockchip_spi私有数据 */
rs = spi_controller_get_devdata(ctlr);
/* 标记SPI控制器是否为从机模式 */
ctlr->slave = slave_mode;
/* .............. */
/* 关闭SPI芯片选择,根据控制器定义拉低/拉高CS */
spi_enable_chip(rs, false);
/* 获取设备树中配置的SPI中断号 */
ret = platform_get_irq(pdev, 0);
if (ret < 0)
goto err_disable_spiclk;
/* 申请中断,注册中断处理函数rockchip_spi_isr */
ret = devm_request_threaded_irq(&pdev->dev, ret, rockchip_spi_isr, NULL,
IRQF_ONESHOT, dev_name(&pdev->dev), ctlr);
if (ret)
goto err_disable_spiclk;
/* .............. */
/* 设置SPI总线编号 = platform device的ID */
ctlr->bus_num = pdev->id;
/* 设置SPI控制器支持的模式:极性、相位、环回、LSB 先行、CS 高有效 */
ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_LOOP | SPI_LSB_FIRST | SPI_CS_HIGH;
/* 如果是从机模式 */
if (slave_mode) {
/* 从机模式不需要片选引脚,添加NO_CS标志 */
ctlr->mode_bits |= SPI_NO_CS;
/* 注册从机中止回调函数 */
ctlr->slave_abort = rockchip_spi_slave_abort;
} else {
/* 主机模式:使用GPIO作为片选信号 */
ctlr->flags = SPI_MASTER_GPIO_SS;
}
/* 设置最大片选数量 */
ctlr->num_chipselect = ROCKCHIP_SPI_MAX_CS_NUM;
/* 将SPI控制器的设备树节点指向当前设备节点 */
ctlr->dev.of_node = pdev->dev.of_node;
/* 支持的数据位宽:16bit、8bit、4bit */
ctlr->bits_per_word_mask = SPI_BPW_MASK(16) | SPI_BPW_MASK(8) | SPI_BPW_MASK(4);
/* 设置SPI最小时钟频率 = 控制器时钟 / 最大分频系数 */
ctlr->min_speed_hz = rs->freq / BAUDR_SCKDV_MAX;
/* 设置SPI最大时钟频率 = 取(时钟/最小分频)与硬件最大频率之间较小值 */
ctlr->max_speed_hz = min(rs->freq / BAUDR_SCKDV_MIN, MAX_SCLK_OUT);
/* ..... */
/* 获取SPI控制器的pinctrl实例*/
pinctrl = devm_pinctrl_get(&pdev->dev);
/* 如果获取pinctrl成功 */
if (!IS_ERR(pinctrl)) {
/* 查找名为"high_speed"的引脚状态 */
rs->high_speed_state = pinctrl_lookup_state(pinctrl, "high_speed");
/* 如果没有高速状态,则打印警告并置空 */
if (IS_ERR_OR_NULL(rs->high_speed_state)) {
dev_warn(&pdev->dev, "no high_speed pinctrl state\n");
rs->high_speed_state = NULL;
}
}
/* 向内核注册SPI控制器 */
ret = devm_spi_register_controller(&pdev->dev, ctlr);
/* 注册失败则跳转错误处理 */
if (ret < 0) {
dev_err(&pdev->dev, "Failed to register controller\n");
goto err_free_dma_rx;
}
return 0;
/* ......... */
}
|
第28-39行:分配一个SPI主机控制器,申请内存,实例化master;
第50-58行:获取中断号,设置中断函数;
第63-93行:初始化控制器等;
第113行:使用devm_spi_register_controller函数向SPI子系统注册SPI控制器。
8.6. SPI子系统核心函数¶
Linux内核为SPI子系统提供了一系列标准化函数,涵盖主控制器注册、设备驱动注册、数据传输等核心操作,以下按功能分类,结合内核源码讲解。
8.6.1. 总线驱动核心函数¶
8.6.1.1. SPI主控制器注册函数(spi_register_master)¶
spi_register_master函数用于将初始化完成的SPI主控制器(struct spi_master)注册到SPI核心层,核心层会对主控制器进行管理,供设备驱动调用。
函数原型:
1 | int spi_register_master(struct spi_master *master);
|
参数说明:
master:指向已初始化完成的struct spi_master结构体指针,包含主控制器的名称、transfer函数、速率范围等参数。
返回值:成功返回0;失败返回负整数错误码。
8.6.1.2. SPI主控制器注销函数(spi_unregister_master)¶
spi_unregister_master函数用于将已注册的SPI主控制器从核心层注销,释放主控制器占用的资源,终止主控制器的工作。
函数原型:
1 | void spi_unregister_master(struct spi_master *master);
|
参数说明:
master:指向已注册的struct spi_master结构体指针,需与注册时的指针一致。
SPI主控制器申请函数(spi_alloc_master)
spi_alloc_master函数为struct spi_master结构体分配内存,并初始化默认值,同时为其私有数据分配内存,简化主控制器的初始化流程。
函数原型:
1 | struct spi_master *spi_alloc_master(struct device *dev, unsigned int size);
|
参数说明:
dev:指向与该主控制器关联的设备结构体指针;
size:主控制器私有数据的内存大小。
8.6.2. 设备驱动核心函数¶
8.6.2.1. SPI设备驱动注册函数(spi_register_driver)¶
spi_register_driver函数用于将初始化完成的SPI设备驱动(struct spi_driver)注册到SPI核心层, 核心层会根据驱动的匹配表,自动匹配对应的SPI从设备。
函数原型:
1 | int spi_register_driver(struct spi_driver *sdrv);
|
参数说明:
sdrv:指向已初始化完成的struct spi_driver结构体指针,包含驱动名称、匹配表、probe/remove函数等。
返回值:成功返回0;失败返回负整数错误码。
8.6.2.2. SPI设备驱动注销函数(spi_unregister_driver)¶
spi_unregister_driver函数用于将已注册的SPI设备驱动从核心层注销,释放驱动占用的资源,终止驱动的工作, 同时会调用该驱动所有匹配设备的remove函数。
函数原型:
1 | void spi_unregister_driver(struct spi_driver *sdrv);
|
参数说明:
sdrv:指向已注册的struct spi_driver结构体指针,需与注册时的指针一致。
8.6.3. 数据传输核心函数¶
8.6.3.1. 同步SPI数据传输函数(spi_sync)¶
spi_sync函数是SPI子系统最核心的数据传输函数,用于同步传输一个SPI事务(struct spi_message), 即等待整个事务传输完成后才返回,是设备驱动中最常用的数据传输接口,底层调用总线驱动的transfer函数。
函数原型:
1 | int spi_sync(struct spi_device *spi, struct spi_message *msg);
|
参数说明:
spi:指向struct spi_device结构体指针,即要通信的SPI从设备;
msg:指向struct spi_message结构体指针,包含要传输的所有传输段(spi_transfer)。
返回值:成功返回0;失败返回负整数错误码。
8.6.3.2. 异步SPI数据传输函数(spi_async)¶
spi_async函数与spi_sync功能类似,区别在于该函数为异步传输,调用后立即返回,不等待传输完成, 传输完成后通过回调函数通知结果,适用于非阻塞传输场景。
函数原型:
1 | int spi_async(struct spi_device *spi, struct spi_message *msg);
|
参数说明:
spi:指向struct spi_device结构体指针,即要通信的SPI从设备;
msg:指向struct spi_message结构体指针,需配置complete回调函数,用于获取传输结果。
返回值:成功返回0;失败返回负整数错误码,表示传输请求提交失败。
8.6.3.3. SPI事务初始化函数(spi_message_init)¶
spi_message_init函数用于初始化SPI事务结构体(struct spi_message),清空事务的传输段链表,初始化状态、回调函数等参数, 为后续添加传输段和传输做准备。
函数原型:
1 | void spi_message_init(struct spi_message *m);
|
参数说明:
m:指向struct spi_message结构体指针,要初始化的事务。
8.6.3.4. SPI传输段添加函数(spi_message_add_tail)¶
spi_message_add_tail函数用于将一个SPI传输段(struct spi_transfer)添加到SPI事务(struct spi_message)的传输段链表尾部,用于构建包含多个传输段的事务。
函数原型:
1 | void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);
|
参数说明:
t:指向struct spi_transfer结构体指针,要添加的传输段;
m:指向struct spi_message结构体指针,要添加到的事务。
8.7. SPI子系统实验¶
本实验基于Linux SPI子系统,实现主机与SPI OLED的通信,完成OLED初始化、SPI读写操作, 最终通过应用层显示字符串到OLED屏幕上,理解SPI子系统的核心架构、驱动注册流程、SPI通信协议及设备树配置规范。
本章的示例代码目录为: linux_driver/25_spi_subsystem
8.7.1. SPI OLED屏简介¶
本实验使用的128x64分辨率的OLED屏采用SPI接口,搭载SSD1306驱动芯片,通过SPI总线接收命令和数据。 SSD1306是一款常用的OLED驱动芯片,专门用于驱动128x64分辨率的单色OLED显示屏,支持SPI和I2C两种通信方式,本实验中采用SPI通信模式。
SSD1306芯片集成了OLED驱动所需的全部功能,包括显示RAM、时序控制、灰度调节等,具有低功耗、高兼容性、驱动简单等特点, 无需额外搭配复杂的驱动电路,只需通过SPI总线发送命令和数据,即可实现OLED屏的像素控制。
8.7.1.1. OLED屏工作原理¶
SSD1306驱动的OLED屏工作逻辑如下:
命令传输:DC引脚拉低(低电平),表示SPI传输的是OLED控制命令(如初始化、坐标设置等)。
数据传输:DC引脚拉高(高电平),表示SPI传输的是OLED显示数据(点阵数据,控制像素点亮/熄灭)。
显示方式:OLED屏按“页”显示,128x64分辨率分为8页(每页8行),显示时需先设置页地址和列地址,再写入对应点阵数据。
模块参考链接: 野火【OLED屏_SPI_0.96寸】模块
OLED屏_SPI_0.96寸模块手册在以上链接的配套资料可以找到:
8.7.1.2. SSD1306页分配及寻址说明¶
截取 SSD1306-Revision 1.1 (Charge Pump).pdf 手册关键说明并结合本实验情况整理得:
SSD1306芯片显存GDDRAM总共为128*64bit(128*8Byte)大小,对应屏幕的128(列)*64(行)个像素点, SSD1306将这些显存分成了8页,每页包含了128个字节。
SSD1306芯片提供三种内存寻址模式:
页寻址模式(默认):写入数据,列地址指针自动增加1,只能在当前页循环,不会自动换页,需要手动设置下一页,如PAGE0的COL127写完之后,地址指针会回到到PAGE0的COL0。通过命令0xB0~0xB7(对应PAGE0~PAGE7)设置目标显示位置的页面起始地址。
水平寻址模式:可以自动换页,如PAGE0的COL127写完之后,地址指针会移动到PAGE1的COL0。
垂直寻址模式:写入数据,页地址指针自动增加1,写完1列,自动换列,如PAGE7的COL0写完之后,地址指针会移动到PAGE0的COL1。
8.7.1.3. SSD1306列地址设置说明¶
SSD1306列地址由8位列地址组成,分为高4位列地址和低4位列地址,拼接规则如下:
1 | 完整8位列地址 = [高4位数据位] [低4位数据位]
|
通过命令00h~0Fh设定指针的 低4位 列地址。
指令范围 |
二进制格式 |
功能说明 |
|---|---|---|
0x00 ~ 0x0F |
0000 X3X2X1X0 |
设置列地址的低4位 |
0x00 ~ 0x0F对应二进制0000 0000 ~ 0000 1111
高4位0000是固定指令头,表示是设置列地址的低4位;X3~X0是数据位,即实际列地址低4位数据位。
通过命令10h~1Fh设定指针的 高4位 列地址。
指令范围 |
二进制格式 |
功能说明 |
|---|---|---|
0x10 ~ 0x1F |
0001 X3X2X1X0 |
设置列地址的高4位 |
0x10 ~ 0x1F对应二进制0001 0000 ~ 0001 1111
高4位0001是固定指令头,表示是设置列地址的高4位;X3~X0是数据位,即实际列地址高4位数据位。
如写入0x01、0x10,展开为二进制得:
1 2 3 4 5 6 7 | //高4位的0000是设置列地址的低4位的固定指令头,低4位的0001是列地址低4位数据位
0x01: 0000 0001
//高4位的0001是设置列地址的低4位的固定指令头,低4位的0000是列地址高4位数据位
0x10: 0001 0000
完整8位列地址 = [高4位数据位] [低4位数据位] = 0000 0001
|
即写入0x01、0x10实际是要选择写入第1列。
8.7.1.4. SSD1306初始化指令序列¶
截取 ZJY096S0600WG02.pdf 手册关键说明并结合本实验情况整理得:
指令码 |
指令功能 |
详细说明 |
|---|---|---|
0xAE |
关闭OLED面板 |
初始化阶段关闭显示,防止屏幕乱码 |
0x00 |
设置列地址低4位 |
页地址模式下,配置显示列地址的低 4 位 |
0x10 |
设置列地址高4位 |
页地址模式下,配置显示列地址的高 4 位 |
0x40 |
设置显示起始行地址 |
设置 GDDRAM 显示起始行,取值范围 0x00~0x3F |
0x81 |
对比度控制寄存器 |
进入屏幕亮度/对比度参数设置模式 |
0xCF |
设置SEG输出电流亮度 |
配置OLED显示对比度亮度参数 |
0xA1 |
设置SEG/列映射 |
0xA0=左右反置显示,0xA1=正常显示 |
0xC8 |
设置COM/行扫描方向 |
0xC0=上下反置显示,0xC8=正常显示 |
0xA6 |
设置正常显示模式 |
配置屏幕为正常非反色显示模式 |
0xA8 |
设置多路复用率 |
配置多路复用比例,参数范围 1~64 |
0x3F |
设置1/64占空比 |
配置OLED驱动为 1/64 duty 工作模式 |
0xD3 |
设置显示偏移 |
偏移GDDRAM映射计数器,范围 0x00~0x3F |
0x00 |
无显示偏移 |
关闭屏幕垂直偏移功能,默认配置 |
0xD5 |
设置显示时钟分频/震荡频率 |
配置时钟分频系数与内部振荡器频率 |
0x80 |
设置时钟分频比 |
配置时钟参数,屏幕刷新率 100 帧/秒 |
0xD9 |
设置预充电周期 |
配置OLED驱动电路预充电时间周期 |
0xF1 |
预充电参数配置 |
预充电15个时钟,放电1个时钟 |
0xDA |
设置COM引脚硬件配置 |
配置COM端口硬件工作模式 |
0x12 |
COM引脚配置参数 |
128x64 OLED 标准硬件配置值 |
0xDB |
设置VCOMH电压 |
配置VCOM去选电平电压等级 |
0x40 |
VCOM去选电平配置 |
设置 VCOM Deselect Level 工作参数 |
0x20 |
设置内存寻址模式 |
配置GDDRAM寻址模式,可选 0x00/0x01/0x02 |
0x02 |
页地址寻址模式 |
设置为页地址寻址模式 |
0x8D |
电荷泵使能控制 |
配置OLED内置升压电荷泵开关 |
0x14 |
使能电荷泵 |
开启电荷泵(0x10为关闭,必须开启才能显示) |
0xA4 |
关闭全屏点亮 |
0xA4=正常显示,0xA5=全屏所有像素点亮 |
0xA6 |
关闭反色显示 |
0xA6=正常显示,0xA7=屏幕反色显示 |
0xAF |
开启OLED面板 |
初始化完成,打开OLED正常显示 |
8.7.2. 设备树插件详解¶
本实验设备树插件(lubancat-spi-oled-overlay.dts)用于配置SPI控制器和SPI OLED从设备,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
/ {
fragment@0 {
target = <&spi3>;
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;
pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;
spi_oled@0 {
compatible = "fire,spi_oled";
reg = <0>; //chip select 0:cs0 1:cs1
spi-max-frequency = <2000000>; //spi output clock
dc_control_pin = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&spi_oled_pin>;
};
};
};
fragment@1 {
target = <&pinctrl>;
__overlay__ {
spi_oled {
spi_oled_pin: spi_oled_pin {
rockchip,pins = <3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
};
};
|
关键配置说明:
target = <&spi3>:指定修改spi3节点,需与实际要使用的硬件接口一致;
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;:对应default状态,此处使用的具体SPI引脚为spi3m1,片选为硬件片选spi3m1_cs0;
pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;:对应high_speed状态下的引脚配置;
spi_oled@0:从设备节点名称,@后的spi_oled地址;
reg = <0>;:地址为0对应cs0片选引脚;
compatible = “fire,spi_oled”:驱动匹配的核心标识,必须与SPI驱动中of_match_table的属性完全一致,否则驱动无法匹配设备。
spi-max-frequency = <2000000>:设置SPI通信速率为2MHz,OLED屏最大支持2MHz。
dc_control_pin:自定义引脚,用于向驱动传递DC引脚信息,驱动通过该属性获取DC引脚编号。
pinctrl-0 = <&spi_oled_pin>;:DC引脚的pinctrl配置。
注意
以上设备树插件是以SPI3_M1、DC脚使用GPIO3_A7为例,需根据板卡实际接口进行修改!
如果不清楚自己板卡有哪些可用的SPI接口,可在板卡执行以下命令确认:
1 2 3 4 5 6 | #查看配置文件可用spi插件
cat /boot/uEnv/uEnv.txt | grep spi
#以LubanCat2-V2板卡为例,信息打印如下
#dtoverlay=/dtb/overlay/rk356x-lubancat-spi3-m1-gpio-cs-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-spi3-m1-overlay.dtbo
|
从可用的设备树插件可以确认,LubanCat2-V2支持spi3-m1,从内核源码中找到对应的设备树插件源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #查看spi3-m1设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-spi3-m1-overlay.dts
#信息打印如下
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
/ {
compatible = "rockchip,rk3568";
fragment@0 {
target = <&spi3>;
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
// 40PIN引脚只预留SPI3 CS0引脚,如果有多个CS信号,可以使用gpio模拟cs
pinctrl-names = "default", "high_speed";
pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>;
pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>;
spi_dev@0 {
compatible = "rockchip,spidev";
reg = <0>; //chip select 0:cs0 1:cs1
spi-max-frequency = <24000000>; //spi output clock
};
};
};
};
|
可知,如果LubanCat2-V2使用spi3-m1接口,lubancat-spi-oled-overlay.dts配置的target和pinctrl-0就是 target = <&spi3>; 和 pinctrl-0 = <&spi3m1_cs0 &spi3m1_pins>; 、 pinctrl-1 = <&spi3m1_cs0 &spi3m1_pins_hs>; 。
结合板卡配套的快速使用手册的40pin引脚对照图章节,可确认spi3-m1接口对应的物理引脚,如下图:
然后再选个空闲的引脚做DC脚,同步修改lubancat-spi-oled-overlay.dts的 dc_control_pin = <&gpio3 RK_PA7 GPIO_ACTIVE_HIGH>; ,
以及 rockchip,pins = <3 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>; 。
8.7.3. 驱动代码详解¶
驱动代码是SPI子系统从设备驱动,整合了SPI读写、OLED初始化、GPIO控制、字符设备封装等功能。
核心定义与数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /* OLED 硬件参数宏定义 */
#define X_WIDTH 128 /* OLED显示屏列数 */
#define Y_WIDTH 64 /* OLED显示屏行数 */
#define DEV_NAME "spi_oled" /* 设备名称 */
#define DEV_CNT 1 /* 设备节点数量 */
/* 应用层传递的显示数据结构体 */
typedef struct oled_display_struct {
u8 x; /* 显示起始X坐标 */
u8 y; /* 显示起始Y坐标(页) */
u32 length; /* 显示数据长度 */
u8 display_buffer[]; /* 显示数据缓冲区 */
} oled_display_struct;
/* OLED 私有设备结构体,统一管理字符设备、SPI设备、GPIO、设备号等资源 */
struct oled_dev {
dev_t devno; /* 字符设备号 */
struct cdev cdev; /* 字符设备结构体 */
struct class *class; /* 设备类 */
struct device *device; /* 设备节点 */
struct spi_device *spi; /* SPI设备句柄 */
int dc_gpio; /* DC控制引脚编号 */
};
/* 静态全局OLED设备实例 */
static struct oled_dev oled_dev;
/* OLED 初始化指令序列 */
static const u8 oled_init_cmds[] = {
0xae, 0x00, 0x10, 0x40,
0x81, 0xcf, 0xa1, 0xc8,
0xa6, 0xa8, 0x3f, 0xd3,
0x00, 0xd5, 0x80, 0xd9,
0xf1, 0xda, 0x12, 0xdb,
0x40, 0x20, 0x02, 0x8d,
0x14, 0xa4, 0xa6, 0xaf
};
|
通过定义多个核心数据结构,实现对OLED设备资源、数据交互格式的统一管理,分别负责应用层与驱动的数据传输、OLED设备资源的整合管理, 以及OLED初始化指令的存储,其中OLED初始化指令就是前面介绍SPI OLED屏SSD1306初始化指令序列。
SPI驱动注册与注销
SPI驱动通过spi_register_driver注册到SPI子系统,当设备树中的从设备与驱动匹配时,执行probe函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static int __init oled_driver_init(void)
{
int ret;
printk(KERN_INFO "spi_oled driver init\n");
/* 注册SPI总线驱动 */
ret = spi_register_driver(&oled_spi_driver);
if (ret) {
printk(KERN_ERR "SPI driver register failed\n");
return ret;
}
return 0;
}
static void __exit oled_driver_exit(void)
{
/* 注销SPI驱动 */
spi_unregister_driver(&oled_spi_driver);
printk(KERN_INFO "spi_oled driver exit\n");
}
|
驱动初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | static int oled_spi_probe(struct spi_device *spi)
{
/* 定义返回值变量 */
int ret;
/* 定义主设备号变量 */
int major;
/* 定义次设备号变量 */
int minor;
/* 获取设备树中对应的节点指针 */
struct device_node *np = spi->dev.of_node;
printk(KERN_INFO "spi_oled driver probe\n");
/* 分配设备号 */
ret = alloc_chrdev_region(&oled_dev.devno, 0, DEV_CNT, DEV_NAME);
if (ret) {
printk(KERN_ERR "fail to alloc devno\n");
return ret;
}
/* 获取主设备号 */
major = MAJOR(oled_dev.devno);
/* 获取次设备号 */
minor = MINOR(oled_dev.devno);
/* 打印主设备号和次设备号 */
printk(KERN_INFO "major=%d, minor=%d\n", major, minor);
/* 初始化字符设备 */
cdev_init(&oled_dev.cdev, &oled_fops);
oled_dev.cdev.owner = THIS_MODULE;
/* 添加字符设备 */
ret = cdev_add(&oled_dev.cdev, oled_dev.devno, DEV_CNT);
if (ret) {
printk(KERN_ERR "fail to add cdev\n");
goto unreg_chrdev;
}
/* 创建设备类 */
oled_dev.class = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(oled_dev.class)) {
printk(KERN_ERR "fail to create class\n");
ret = PTR_ERR(oled_dev.class);
goto del_cdev;
}
/* 创建设备节点 */
oled_dev.device = device_create(oled_dev.class, &spi->dev,
oled_dev.devno, NULL, DEV_NAME);
if (IS_ERR(oled_dev.device)) {
printk(KERN_ERR "fail to create device\n");
ret = PTR_ERR(oled_dev.device);
goto destroy_class;
}
/* 从设备树获取DC GPIO引脚 */
oled_dev.dc_gpio = of_get_named_gpio(np, "dc_control_pin", 0);
if (oled_dev.dc_gpio < 0) {
printk(KERN_ERR "Get DC GPIO failed\n");
goto destroy_device;
}
/* 申请GPIO引脚 */
ret = gpio_request(oled_dev.dc_gpio, "oled_dc_gpio");
if (ret) {
printk(KERN_ERR "Request DC GPIO failed\n");
goto destroy_device;
}
/* 设置GPIO为输出模式,默认高电平 */
gpio_direction_output(oled_dev.dc_gpio, 1);
/* 配置SPI参数 */
spi->mode = SPI_MODE_0;
ret = spi_setup(spi);
if (ret) {
printk(KERN_ERR "spi setup failed\n");
goto free_gpio;
}
/* 保存SPI设备句柄到私有结构体 */
oled_dev.spi = spi;
/* 打印配置信息 */
printk(KERN_INFO "SPI max_speed: %dHz\n", spi->max_speed_hz);
printk(KERN_INFO "SPI mode: 0x%02X\n", spi->mode);
printk(KERN_INFO "SPI chip_select = %d\n", (int)spi->chip_select);
printk(KERN_INFO "SPI bits_per_word = %d\n", (int)spi->bits_per_word);
/* 初始化OLED硬件 */
oled_hw_init();
return 0;
free_gpio:
/* 释放GPIO */
gpio_free(oled_dev.dc_gpio);
destroy_device:
/* 销毁设备节点 */
device_destroy(oled_dev.class, oled_dev.devno);
destroy_class:
/* 销毁设备类 */
class_destroy(oled_dev.class);
del_cdev:
/* 删除字符设备 */
cdev_del(&oled_dev.cdev);
unreg_chrdev:
/* 释放设备号 */
unregister_chrdev_region(oled_dev.devno, DEV_CNT);
return ret;
}
|
设备树匹配成功后执行,依次完成设备号分配、字符设备注册、设备节点创建、DC GPIO获取与配置、SPI模式配置、保存SPI设备句柄,最后初始化OLED硬件。
SPI读写核心函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | /*
* 函数功能:OLED 写命令
* spi:SPI设备句柄
* cmd:要写入的命令
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmd(struct spi_device *spi, u8 cmd)
{
int ret;
/* 定义SPI传输段结构体,并把它的所有成员变量全部初始化为0*/
struct spi_transfer xfer = {0};
/* 定义SPI传输事务结构体 */
struct spi_message msg;
/* 设置DC引脚为低电平,表示传输命令 */
gpio_set_value(oled_dev.dc_gpio, 0);
/* 初始化SPI消息 */
spi_message_init(&msg);
xfer.tx_buf = &cmd; /* 发送数据缓冲区 */
xfer.len = 1; /* 传输长度1字节 */
/* 把单个传输段,添加到事务的队列尾部 */
spi_message_add_tail(&xfer, &msg);
/* 同步SPI传输 */
ret = spi_sync(spi, &msg);
if (ret) {
printk(KERN_ERR "spi_oled write cmd failed\n");
return ret;
}
/* 恢复DC引脚为高电平 */
gpio_set_value(oled_dev.dc_gpio, 1);
return 0;
}
/*
* 函数功能:OLED 批量写命令
* spi:SPI设备句柄
* cmds:命令缓冲区
* len:命令长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_cmds(struct spi_device *spi, const u8 *cmds, u16 len)
{
int i, ret;
/* 循环逐字节写入命令 */
for (i = 0; i < len; i++) {
ret = oled_write_cmd(spi, cmds[i]);
if (ret)
return ret;
}
return 0;
}
/*
* 函数功能:OLED 写单字节数据
* spi:SPI设备句柄
* data:要写入的数据
* 返回值:0-成功,负数-失败
*/
static int oled_write_data(struct spi_device *spi, u8 data)
{
int ret;
/* 定义SPI传输段结构体,并把它的所有成员变量全部初始化为0*/
struct spi_transfer xfer = {0};
/* 定义SPI传输事务结构体 */
struct spi_message msg;
/* 设置DC引脚为高电平,表示传输数据 */
gpio_set_value(oled_dev.dc_gpio, 1);
/* 初始化SPI消息 */
spi_message_init(&msg);
xfer.tx_buf = &data; /* 发送数据缓冲区 */
xfer.len = 1; /* 传输长度1字节 */
/* 把单个传输段,添加到事务的队列尾部 */
spi_message_add_tail(&xfer, &msg);
/* 同步SPI传输 */
ret = spi_sync(spi, &msg);
if (ret) {
printk(KERN_ERR "spi_oled write data failed\n");
return ret;
}
return 0;
}
/*
* 函数功能:OLED 批量写数据
* spi:SPI设备句柄
* buf:数据缓冲区
* len:数据长度
* 返回值:0-成功,负数-失败
*/
static int oled_write_datas(struct spi_device *spi, const u8 *buf, u16 len)
{
int i, ret;
/* 循环逐字节写入数据 */
for (i = 0; i < len; i++) {
ret = oled_write_data(spi, buf[i]);
if (ret)
return ret;
}
return 0;
}
|
oled_write_cmd和oled_write_cmds函数用于传输SSD1306芯片的控制命令,如初始化、坐标设置;
oled_write_data和oled_write_datas函数用于传输显示点阵数据;
所有函数均调用SPI子系统提供的spi_sync接口实现同步传输,该接口会阻塞等待传输完成,确保SPI通信的可靠性;
DC引脚的电平切换严格遵循SSD1306芯片的约定,确保命令和数据能够被芯片正确识别。
SPI OLED初始化
1 2 3 4 5 6 7 8 9 10 11 | /*
* 函数功能:OLED 硬件初始化
*/
static void oled_hw_init(void)
{
/* 写入初始化指令序列 */
oled_write_cmds(oled_dev.spi, oled_init_cmds, ARRAY_SIZE(oled_init_cmds));
/* 清屏,填充0x00 */
oled_fill(0x00);
}
|
oled_hw_init函数是OLED屏的初始化入口,先调用oled_write_cmds函数发送oled_init_cmds指令序列,完成SSD1306芯片的配置,再调用oled_fill函数清屏,为后续显示点阵数据做准备。
OLED全屏填充函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /*
* 函数功能:OLED 全屏填充
* dat:填充值
*/
static void oled_fill(u8 dat)
{
u8 y, x;
/* 遍历8个页 */
for (y = 0; y < 8; y++) {
/* 设置页地址 */
oled_write_cmd(oled_dev.spi, 0xb0 + y);
/* 设置列地址低4位 */
oled_write_cmd(oled_dev.spi, 0x00);
/* 设置列地址高4位 */
oled_write_cmd(oled_dev.spi, 0x10);
/* 整行填充数据 */
for (x = 0; x < X_WIDTH; x++) {
oled_write_data(oled_dev.spi, dat);
}
}
}
|
通过双重循环实现全屏填充,外层循环遍历8个页,内层循环遍历每个页的128列,逐列写入填充值;
其中0xb0 + y是SSD1306芯片 页寻址模式 下设置目标显示位置的页面起始地址命令,0x00和0x10是列地址低4位和高4位命令,表示要设置第0列,在SSD1306页分配及寻址说明及SSD1306列地址设置说明小节已经做出详细说明。
从第0页的第0列开始填充,写入数据,列地址指针自动增加1,填充到第127列,再从第1页的第0列开始填充,填充到第127列,依次循环,填满全部页全部列。
OLED指定位置显示数据函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /*
* 函数功能:OLED 指定位置显示数据
* buf:显示数据
* x:起始X坐标
* y:起始Y坐标
* len:数据长度
* 返回值:写入长度,负数-失败
*/
static int oled_display(u8 *buf, u8 x, u8 y, u16 len)
{
int ret = 0;
u16 index = 0;
do {
/* 设置显示起始坐标 */
ret += oled_write_cmd(oled_dev.spi, 0xb0 + y); /* 设置页 */
ret += oled_write_cmd(oled_dev.spi, ((x & 0xf0) >> 4) | 0x10); /* 设置地址高4位 */
ret += oled_write_cmd(oled_dev.spi, (x & 0x0f) | 0x00); /* 设置地址低4位 */
/* 剩余数据超过当前行宽度,换行显示 */
if (len > (X_WIDTH - x)) {
ret += oled_write_datas(oled_dev.spi, buf + index, X_WIDTH - x);
len -= (X_WIDTH - x);
index += (X_WIDTH - x);
x = 0;
y++;
} else {
/* 剩余数据不足一行,直接写入 */
ret += oled_write_datas(oled_dev.spi, buf + index, len);
index += len;
len = 0;
}
} while (len > 0);
if (ret) {
printk(KERN_ERR "spi_oled display failed\n");
return -EIO;
}
return index;
}
|
通过do-while循环处理剩余数据,当当前页剩余列数不足以容纳剩余数据时,自动切换到下一页,确保点阵数据能够连续显示。使用do-while而不是while的原因是至少要写一次数据,所以用do-while最合理。
列地址的设置通过“高4位+低4位”的方式,高列地址:((x & 0xf0) >> 4) | 0x10,说明如下:
x & 0xf0:取出x的高4位
>>4:右移4位,放到低4位位置
| 0x10:拼接成手册规定的:高列地址指令 0001 XXXX
低列地址:(x & 0x0f) | 0x00),说明如下:
x & 0x0f:取出x的低4位
| 0x00(可省略):拼接成手册规定的:低列地址指令 0000 XXXX
字符设备封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | /* 字符设备 open 实现 */
static int oled_open(struct inode *inode, struct file *filp)
{
/* 开启 OLED 显示 */
oled_write_cmd(oled_dev.spi, 0xaf);
printk(KERN_INFO "spi_oled device open\n");
return 0;
}
/* 字符设备 write 实现 */
static ssize_t oled_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *off)
{
oled_display_struct *data;
int ret;
/* 申请内存存储应用层数据 */
data = kzalloc(cnt, GFP_KERNEL);
if (!data) {
printk(KERN_ERR "spi_oled malloc failed\n");
return -ENOMEM;
}
/* 从用户空间拷贝数据 */
ret = copy_from_user(data, buf, cnt);
if (ret) {
printk(KERN_ERR "spi_oled copy from user failed\n");
ret = -EFAULT;
goto free_mem;
}
/* 执行显示 */
ret = oled_display(data->display_buffer, data->x, data->y, data->length);
return cnt;
free_mem:
/* 释放申请的内存 */
kfree(data);
return ret;
}
/* 字符设备 release 实现 */
static int oled_release(struct inode *inode, struct file *filp)
{
/* 关闭 OLED 显示 */
oled_write_cmd(oled_dev.spi, 0xae);
printk(KERN_INFO "spi_oled device release\n");
return 0;
}
/* 字符设备操作集 */
static const struct file_operations oled_fops = {
.owner = THIS_MODULE,
.open = oled_open,
.write = oled_write,
.release = oled_release,
};
|
oled_open函数:调用oled_write_cmd函数,发送0xaf命令开启OLED显示;
oled_write函数:先申请内核空间内存,再通过copy_from_user接口将应用层传递的点阵数据、显示坐标等信息拷贝到内核空间,最后调用oled_display函数实现显示;
oled_release函数:调用oled_write_cmd函数,发送0xae命令关闭OLED显示,降低设备功耗。
8.7.4. 应用代码详解¶
应用层无需关心底层SPI通信和OLED时序,只需按驱动约定的格式传递显示数据。
核心数据定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /* 类型定义 */
typedef unsigned char u8;
typedef unsigned int u32;
/* OLED 屏幕参数 */
#define X_WIDTH 128 // 屏宽
#define Y_WIDTH 64 // 屏高
/* 显示数据结构体 */
struct oled_display_struct {
u8 x; /* 显示起始X坐标 */
u8 y; /* 显示起始Y坐标(页) */
u32 length; /* 显示数据长度 */
u8 display_buffer[]; /* 显示数据缓冲区 */
};
/* hello world 8x16字模(16字节/字符)*/
unsigned char F8x16[] = {
0x10,0xF0,0x00,0x80,0x80,0x80,0x00,0x00,0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,/*"h",0*/
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x24,0x24,0x24,0x24,0x17,0x00,/*"e",1*/
0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,/*"l",2*/
0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,/*"l",3*/
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,/*"o",4*/
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,/*" ",5*/
0x80,0x80,0x00,0x80,0x80,0x00,0x80,0x80,0x01,0x0E,0x30,0x0C,0x07,0x38,0x06,0x01,/*"w",6*/
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,/*"o",7*/
0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00,/*"r",8*/
0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,/*"l",9*/
0x00,0x00,0x80,0x80,0x80,0x90,0xF0,0x00,0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20,/*"d",10*/
};
|
其中hello world 8x16字模是使用OLED屏_SPI_0.96寸模块配套资料中的取模工具生成的,工具如下:
双击打开PCtoLCD2002.exe,然后点击设置图标:
参考以下图片进行设置,修改完后点击确认:
确认字宽和字高,输入要生成的内容,然后点击生成字模就得到了对应的字模数据。
核心显示函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | /****************************************************************************************
* 函数:oled_show_one_letter
* 功能:显示单个8x16字符
* 参数:fd - OLED设备文件描述符
* x - 字符起始X坐标
* y - 字符起始Y页坐标
* width - 字符宽度(8像素)
* high - 字符高度(16像素)
* data - 字符字模数据指针
* 返回:0成功,-1失败
***************************************************************************************/
int oled_show_one_letter(int fd, u8 x, u8 y, u8 width, u8 high, u8 *data)
{
struct oled_display_struct *display_struct = NULL;
/* 校验:OLED按8行分页,高度必须是8的整数倍 */
if ((high % 8) != 0) {
printf("字符高度设置错误!\n");
return -1;
}
/* 计算字符占用页数:字符高度16像素对应16行,除以8行每页,得到占用页数为2页 */
high = high / 8;
/* 动态分配内存:结构体大小 + 字符总字节数(宽度*页数) */
display_struct = malloc(sizeof(struct oled_display_struct) + width * high);
if (!display_struct) {
printf("内存分配失败!\n");
return -1;
}
/* 循环:逐页显示字符,8x16字符需要显示2页 */
do {
/* 填充显示坐标 */
display_struct->x = x;
display_struct->y = y;
/* 每页显示的字节数 = 字符宽度 */
display_struct->length = width;
/* 复制字模数据到发送缓冲区 */
memcpy(display_struct->display_buffer, data, display_struct->length);
/* 写入驱动,完成显示 */
write(fd, display_struct, sizeof(struct oled_display_struct) + display_struct->length);
/* 指向下一页字模数据 */
data += display_struct->length;
high--; // 剩余页数-1
y++; // Y页坐标+1
} while (high > 0);
/* 释放内存 */
free(display_struct);
return 0;
}
/****************************************************************************************
* 函数:oled_fill
* 功能:OLED区域填充/清屏
* 参数:fd - 设备文件描述符
* start_x - 填充起始X坐标
* start_y - 填充起始Y页坐标
* end_x - 填充结束X坐标
* end_y - 填充结束Y页坐标
* data - 填充数据(0x00全黑,0xFF全亮)
* 返回:0成功,-1失败
***************************************************************************************/
int oled_fill(int fd, u8 start_x, u8 start_y, u8 end_x, u8 end_y, u8 data)
{
struct oled_display_struct *display_struct;
/* 坐标合法性校验 */
if (end_x < start_x || end_y < start_y)
return -1;
/* 分配内存:结构体 + 一行填充的字节数 */
display_struct = malloc(sizeof(struct oled_display_struct) + end_x - start_x + 1);
/* 设置一行填充的长度 */
display_struct->length = end_x - start_x + 1;
/* 缓冲区全部填充为指定数据 */
memset(display_struct->display_buffer, data, display_struct->length);
/* 逐页填充,直到结束页 */
for (; start_y <= end_y; start_y++) {
display_struct->x = start_x;
display_struct->y = start_y;
write(fd, display_struct, sizeof(struct oled_display_struct) + display_struct->length);
}
/* 释放内存 */
free(display_struct);
return 0;
}
/****************************************************************************************
* 函数:oled_show_F8x16
* 功能:通用8x16字模显示函数,自动计算字符个数,16字节/字符
* 参数:fd - 设备文件描述符
* x - 起始X坐标
* y - 起始Y页坐标
* font_buf - 8x16字模数组指针
* total_len - 字模总字节数,传入sizeof(font_buf)自动计算
* 返回:0成功,-1失败
***************************************************************************************/
int oled_show_F8x16(int fd, u8 x, u8 y, u8 *font_buf, u32 total_len)
{
u32 char_count = 0; // 字符总数
u32 i = 0; // 循环计数
/* 1. 字符个数 = 总字节数 / 单个字符字节数(每个字符16字节) */
char_count = total_len / 16;
if (char_count == 0)
return -1;
/* 2. 循环显示所有字符 */
for (i = 0; i < char_count; i++) {
/* 显示单个字符:字模偏移 = 字符索引 * 16字节 */
oled_show_one_letter(fd, x, y, 8, 16, &font_buf[i * 16]);
/* 字符宽度8像素,X坐标右移8列 */
x += 8;
/* 自动换行:超出屏幕宽度,切换到下一行 */
if (x > X_WIDTH - 8) {
x = 0; // X坐标归零
y += 2; // Y页+2,因为8x16字符占2页
if (y > 6) break; // 超出屏幕高度,停止显示
}
}
return 0;
}
|
oled_show_one_letter函数:核心是“单个字符显示”,先校验坐标合法性,再根据字符获取对应的点阵数据索引,动态分配内存存储数据传输结构体和点阵数据,最后调用write函数将数据发送到驱动层;需要注意的是内存分配后必须释放,避免内存泄漏,且点阵数据拷贝需严格对应16字节/字符。
oled_fill函数:核心是“区域清屏”,通过计算清屏区域的数据长度,动态分配内存并填充0x00(全黑),再发送到驱动层;
oled_show_F8x16函数:核心是“字符串显示”,遍历字符串,逐字符调用oled_show_one_letter函数显示,同时处理换行逻辑(X坐标超出128列时,切换到下2个页)。
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int main(int argc, char *argv[])
{
int fd;
/* 校验命令行参数:必须传入设备路径 */
if (argc != 2) {
printf("Usage: ./spi_oled_app /dev/spi_oled\n");
return -1;
}
/* 打开文件 */
fd = open(argv[1], O_RDWR);
if (fd < 0) {
printf("打开设备文件 %s 失败 !\n", argv[1]);
return -1;
}
printf("设备打开成功,开始显示...\n");
while (1) {
// 1. 全屏清屏
oled_fill(fd, 0, 0, 127, 7, 0x00);
sleep(1);
// 2. 通用函数居中显示 hello world
oled_show_F8x16(fd, 20, 2, F8x16, sizeof(F8x16));
printf("hello world 显示!\n");
sleep(3);
}
close(fd);
return 0;
}
|
循环显示,依次执行“全屏清屏->延时1s->显示字符串->延时3秒”,
清屏调用oled_fill函数,实现全屏黑屏,避免上一次显示残留;
显示字符串调用oled_show_F8x16函数,起始坐标X=20 (通过 (128列-11个字符*8列宽度)/2 得到)、Y=2,确保字符串显示在屏幕中间;
8.7.5. Makefile说明¶
本节实验使用的Makefile如下所示,编写该Makefile时,只需要根据实际情况修改变量KERNEL_DIR、obj-m和test_app即可。
8.7.6. 编译设备树和驱动¶
8.7.6.1. 编译设备树¶
修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件, 添加我们编辑好的设备树插件, 并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。
然后在内核源码顶层目录执行以下命令编译设备树插件:
1 2 3 | #这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
|
提示
其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。
8.7.6.2. 加载设备树¶
编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-spi-oled-overlay.dtbo,
将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。
1 2 3 4 | #先传输到板卡
#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-spi-oled-overlay.dtbo /boot/dtb/overlay/
|
然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。
重启板卡可以在uboot启动信息中看到设备树插件加载。
8.7.6.3. 编译驱动¶
在实验目录下输入 make 即可编译驱动和应用程序,编译得到内核模块spi_oled.ko和应用程序spi_oled_app。
8.7.7. 模块接线说明¶
通过杜邦线连接SPI OLED模块和板卡,接线如下:
1 2 3 4 5 6 7 | SPI OLED模块 板卡
VCC -------- 3.3V
GND -------- GND
CLK -------- SPI_CLK
MOSI -------- SPI_MOSI
CS -------- SPI_CS
D/C -------- 自己选定的DC脚
|
8.7.8. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
8.7.8.1. 实验操作¶
使用以下命令加载驱动和运行测试程序,加载驱动前需确保SPI OLED模块已经连接到板卡:
1 2 3 4 5 6 7 8 9 10 11 | #加载驱动
sudo insmod spi_oled.ko
#信息输出如下
[ 44.026150] spi_oled driver init
[ 44.026544] spi_oled driver probe
[ 44.026652] major=236, minor=0
[ 44.027456] SPI max_speed: 2000000Hz
[ 44.027571] SPI mode: 0x00
[ 44.027633] SPI chip_select = 0
[ 44.027700] SPI bits_per_word = 8
|
可以看到SPI的最大速度为2MHz,工作模式为模式0,片选信号为CS0,每个数据宽度为8位。
使用以下命令运行应用程序进行显示:
1 2 3 4 5 6 7 8 | #运行应用程序
sudo ./spi_oled_app /dev/spi_oled
#信息打印如下
[ 344.342242] spi_oled device open
设备打开成功,开始显示...
hello world 显示!
hello world 显示!
|
运行应用程序后,OLED屏会出现以下现象:
应用程序启动后,OLED屏先清屏(全屏黑屏),持续1秒;
随后在屏幕中间位置显示“hello world”字符串;
字符串显示3秒后,再次清屏,循环重复上述过程。
显示的“hello world”字符串如下图:
提示
如果希望显示6x8、16x16的字符串、中文、图片等,可参考linux_driver/25_spi_subsystem/spi_oled_app_full参考代码自行研究。
8.7.9. 实验注意事项¶
硬件连接正确性:OLED的SCLK、MOSI、CS0引脚需正确连接到开发板SPI对应的引脚,DC引脚需连接到与设备树配置一致的引脚;电源VCC须接3.3V,GND接GND,反接会烧毁OLED屏。
SPI速率:通信速率需设置为2MHz以内,过高会导致数据传输错误,屏幕显示乱码。
DC引脚控制:驱动中必须在传输命令/数据前切换DC电平,若DC引脚电平错误,OLED会将命令识别为数据,导致屏幕不亮或显示异常。
数据格式匹配:应用层定义的oled_display_struct必须与驱动完全一致,尤其是柔性数组的使用,需确保申请的内存大小足够,避免数组越界导致驱动崩溃。
点阵数据适配:OLED显示的点阵数据需与屏幕分辨率匹配(本实验为8x16点阵),若点阵数据分辨率不匹配,会导致字符显示畸形、错位。
故障排查:若屏幕不亮,硬件方面可确认接线是否接错或不牢固,软件方面可通过dmesg查看内核日志,排查驱动加载错误、GPIO申请失败、SPI通信失败等问题;若显示乱码,需检查SPI模式、速率、DC引脚电平及点阵数据格式。