7. SPI子系统¶
串行外设接口(Serial Peripheral interface)简称SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚。 本章我们介绍下SPI相关的基础知识、内核SPI框架和以spi接口的oled显示屏为例讲解spi驱动程序的编写。
本章主要分为五部分内容。
第一部分,spi驱动基本知识,简单讲解SPI物理总线、时序和模式。
第二部分,分析spi驱动框架和后续使用到的核心数据结构。
第三部分,分析spi总线驱动和spi核心层以及spi控制器。
第四部分,编写驱动时会使用到的函数,如同步、异步等。
第五部分,实验,spi驱动oled液晶屏。
本章配套源码位于:linux_driver/spi_oled
7.1. spi基本知识¶
7.1.1. spi物理总线¶
spi总线都可以挂载多个设备,spi支持标准的一主多从,全双工半双工通信等。其中四根控制线包括:
SCK:时钟线,数据收发同步
MOSI:数据线,主设备数据发送、从设备数据接收
MISO:数据线,从设备数据发送,主设备数据接收
NSS:片选信号线

i2c通过i2c设备地址选择通信设备,而spi通过片选引脚选中要通信的设备。
spi接口支持有多个片选引脚,连接多个SPI从设备,当然也可以使用外部GPIO扩展SPI设备的数量, 这样一个spi接口可连接的设备数由片选引脚树决定。
如果使用spi接口提供的片选引脚,spi总线驱动会处理好什么时候选spi设备。
如果使用外部GPIO作为片选引脚需要我们在spi设备驱动中设置什么时候选中spi。(或者在配置SPI时指定使用的片选引脚)。
通常情况下无特殊要求我们使用spi接口提供的片选引脚。
7.1.2. spi时序¶

起始信号:NSS 信号线由高变低
停止信号:NSS 信号由低变高
数据传输:在 SCK的每个时钟周期 MOSI和 MISO同时传输一位数据,高/低位传输没有硬性规定
传输单位:8位或16位
单位数量:允许无限长的数据传输
7.1.3. spi通信模式¶
根据总线空闲时 SCK 的时钟状态以及数据采样时刻,SPI的工作模式分为四种:

时钟极性 CPOL:指 SPI 通讯设备处于空闲状态时,SCK信号线的电平信号:
CPOL=0时,SCK在空闲状态时为低电平
CPOL=1时,SCK在空闲状态时为高电平
时钟相位 CPHA:数据的采样的时刻:
CPHA=0时,数据在SCK时钟线的“奇数边沿”被采样
CPHA=1时,数据在SCK时钟线的“偶数边沿”被采样

如上图所示:
SCK信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。
CPHA=0,数据在 SCK 时钟线的“奇数边沿”被采样,当 CPOL=0 的时候,时钟的奇数边沿是上升沿,当 CPOL=1 的时候,时钟的奇数边沿是下降沿。
在linux内核中,有定义这几种通讯模式,一般使用较多的是模式0和模式3
1 2 3 4 5 6 | #define SPI_CPHA 0x01 /* clock phase */
#define SPI_CPOL 0x02 /* clock polarity */
#define SPI_MODE_0 (0|0) /* (original MicroWire) */
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
|
更多有关SPI通信协议的内容可以参考 【野火®】零死角玩转STM32 中spi章节。
7.2. spi驱动框架¶
spi设备驱动和i2c设备驱动非常相似,可对比学习。这一小节主要介绍spi驱动框架以及主要的结构体。

如框架图所示,spi可分为spi总线驱动和spi设备驱动。spi总线驱动已经由芯片厂商提供,我们适当了解其实现机制。 而spi设备驱动由我们自己编写,则需要明白其中的原理。spi设备驱动涉及到字符设备驱动、SPI核心层、SPI主机驱动,具体功能如下。
SPI核心层:提供SPI控制器驱动和设备驱动的注册方法、注销方法,SPI Core提供操作接口函数,允许一个spi master,spi driver 和spi device初始化时在SPI Core中进行注册,以及推出时进行注销,也提供上层API接口。
SPI主机驱动:也就是spi控制器驱动(SPI Master Driver),主要包含SPI硬件体系结构中适配器(spi控制器)的控制,实现spi总线的硬件访问操作。
SPI设备驱动:对应于spi设备端的驱动程序,通过SPI主机驱动与CPU交换数据。
7.2.1. 关键数据结构¶
这小节将列出整个spi驱动框架所涉及的关键数据结构,可以先浏览下,后续代码中遇到这些数据结构时再回来看详细定义。
7.2.1.1. spi_master¶
spi_master是SPI控制器接口,实际也就是spi_controller结构体。
1 | #define spi_master spi_controller
|
7.2.1.2. spi_controller¶
spi_controller结构体部分成员变量已经被省略,下面列出的是spi_controller结构体关键成员变量:
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 | struct spi_controller {
struct device dev;
...
struct list_head list;
s16 bus_num;
u16 num_chipselect;
...
struct spi_message *cur_msg;
...
int (*setup)(struct spi_device *spi);
int (*transfer)(struct spi_device *spi,
struct spi_message *mesg);
void (*cleanup)(struct spi_device *spi);
struct kthread_worker kworker;
struct task_struct *kworker_task;
struct kthread_work pump_messages;
struct list_head queue;
struct spi_message *cur_msg;
...
int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi,struct spi_transfer *transfer);
int (*prepare_transfer_hardware)(struct spi_controller *ctlr);
int (*transfer_one_message)(struct spi_controller *ctlr,struct spi_message *mesg);
void (*set_cs)(struct spi_device *spi, bool enable);
...
int *cs_gpios;
}
|
spi_controller中包含了各种函数指针,这些函数指针会在SPI核心层中被使用。
list: 链表节点,芯片可能有多个spi控制器
bus_num: spi控制器编号
num_chipselect: spi片选信号的个数,对不同的从设备进行区分
cur_msg: spi_message结构体类型,我们发送的信息都会被封装在这个结构体中。cur_msg,当前正带处理的消息队列
transfer: 用于把数据加入控制器的消息队列中
cleanup: 当spi_master被释放的时候,完成清理工作
kworker: 内核线程工人,spi可以使用异步传输方式发送数据
pump_messages: 具体传输工作
queue: 所有等待传输的消息队列挂在该链表下
transfer_one_message: 发送一个spi消息,类似IIC适配器里的algo->master_xfer,产生spi通信时序
cs_gpios: 记录spi上具体的片选信号。
7.2.1.3. spi_driver结构体¶
1 2 3 4 5 6 7 | struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
|
id_table: 用来和spi进行配对。
.probe: spi设备和spi驱动匹配成功后,回调该函数指针
可以看到spi设备驱动结构体和我们之前讲过的i2c设备驱动结构体 i2c_driver 、平台设备驱动结构体 platform_driver 拥有相同的结构,用法也相同。
7.2.1.4. spi_device设备结构体¶
在spi驱动中一个spi设备结构体代表了一个具体的spi设备,它保存着这个spi设备的详细信息,也可以说是配置信息。 当驱动和设备匹配成功后(例如设备树节点)我们可以从.prob函数的参数中得到spi_device结构体。
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 | struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_controller *master; /* compatibility layer */
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
#define SPI_CPHA 0x01 /* clock phase */
#define SPI_CPOL 0x02 /* clock polarity */
#define SPI_MODE_0 (0|0) /* (original MicroWire) */
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
#define SPI_CS_HIGH 0x04 /* chipselect active high? */
#define SPI_LSB_FIRST 0x08 /* per-word bits-on-wire */
#define SPI_3WIRE 0x10 /* SI/SO signals shared */
#define SPI_LOOP 0x20 /* loopback mode */
#define SPI_NO_CS 0x40 /* 1 dev/bus, no chipselect */
#define SPI_READY 0x80 /* slave pulls low to pause */
#define SPI_TX_DUAL 0x100 /* transmit with 2 wires */
#define SPI_TX_QUAD 0x200 /* transmit with 4 wires */
#define SPI_RX_DUAL 0x400 /* receive with 2 wires */
#define SPI_RX_QUAD 0x800 /* receive with 4 wires */
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
int cs_gpio; /* chip select gpio */
/* the statistics */
struct spi_statistics statistics;
};
|
dev: device类型结构体。这是一个设备结构体,我们把它称为spi设备结构体、i2c设备结构体、平台设备结构体都是“继承”自设备结构体。它们根据各自的特点添加自己的成员,spi设备添加的成员就是后面要介绍的成员
controller: 当前spi设备挂载在那个spi控制器
master: spi_master类型的结构体。在总线驱动中,一个spi_master代表了一个spi总线,这个参数就是用于指定spi设备挂载到那个spi总线上
max_speed_hz: 指定SPI通信的最大频率
chip_select: spi总选用于区分不同SPI设备的一个标号,不要误以为他是SPI设备的片选引脚。指定片选引脚的成员在下面
bits_per_word: 指定SPI通信时一个字节多少位,也就是传输单位
mode: SPI工作模式,工作模式如以上代码中的宏定义。包括时钟极性、位宽等等,这些宏定义可以使用或运算“|”进行组合,这些宏定义在SPI协议中有详细介绍,这里不再赘述
irq: 如果使用了中断,它用于指定中断号
cs_gpio: 片选引脚。在设备树中设置了片选引脚,驱动和设别树节点匹配成功后自动获取片选引脚,我们也可以在驱动总通过设置该参数自定义片选引脚
statistics: 记录spi名字,用来和spi_driver进行配对。
7.2.1.5. spi_transfer结构体¶
在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 | struct spi_transfer {
/* it's ok if tx_buf == rx_buf (right?)
* for MicroWire, one buffer must be null
* buffers must work with dma_*map_single() calls, unless
* spi_message.is_dma_mapped reports a pre-existing mapping
*/
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma;
dma_addr_t rx_dma;
struct sg_table tx_sg;
struct sg_table rx_sg;
unsigned cs_change:1;
unsigned tx_nbits:3;
unsigned rx_nbits:3;
#define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */
#define SPI_NBITS_DUAL 0x02 /* 2bits transfer */
#define SPI_NBITS_QUAD 0x04 /* 4bits transfer */
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
struct list_head transfer_list;
};
|
传输结构体的成员较多,需要我们自己设置的很少,这里只介绍我们常用的配置项。
tx_buf: 发送缓冲区,用于指定要发送的数据地址。
rx_buf: 接收缓冲区,用于保存接收得到的数据,如果不接收不用设置或设置为NULL.
len: 要发送和接收的长度,根据SPI特性发送、接收长度相等。
tx_dma、rx_dma: 如果使用了DAM,用于指定tx或rx DMA地址。
bits_per_word: speed_hz,分别用于设置每个字节多少位、发送频率。如果我们不设置这些参数那么会使用默认的配置,也就是我初始化spi是设置的参数。
7.2.1.6. spi_message结构体¶
总的来说spi_transfer结构体保存了要发送(或接收)的数据,而在SPI设备驱动中数据是以“消息”的形式发送。 spi_message是消息结构体,我们把它称为消息结构体,发送一个消息分四步, 依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
spi_message结构体定义如下所示:
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 | struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
/* REVISIT: we might want a flag affecting the behavior of the
* last transfer ... allowing things like "read 16 bit length L"
* immediately followed by "read L bytes". Basically imposing
* a specific message scheduling algorithm.
*
* Some controller drivers (message-at-a-time queue processing)
* could provide that as their default scheduling algorithm. But
* others (with multi-message pipelines) could need a flag to
* tell them about such special cases.
*/
/* completion is reported through a callback */
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
/* for optional use by whatever driver currently owns the
* spi_message ... between calls to spi_async and then later
* complete(), that's the spi_master controller driver.
*/
struct list_head queue;
void *state;
};
|
spi_message结构体成员我们比较陌生,如果我们不考虑具体的发送细节我们可以不用了解这些成员的含义,因为spi_message的初始化以及“绑定”spi_transfer传输结构体都是由内核函数实现。 唯一要说明的是第二个成员“spi”,它是一个spi_device类型的指针,我们讲解spi_device结构体时说过,一个spi设备对应一个spi_device结构体,这个成员就是用于指定消息来自哪个设备。
7.2.2. SPI核心层¶
7.2.2.1. spi 总线注册¶
linux系统在开机的时候就会执行,自动进行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);
...
}
|
当总线注册成功之后,会在sys/bus下面生成一个spi总线,然后在系统中新增一个设备类,sys/class/目录下会可以找到spi_master类。
7.2.2.2. spi总线定义¶
spi_bus_type 总线定义,会在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,
};
|
.match函数指针,设定了spi设备和spi驱动的匹配规则,具体如下spi_match_device。
7.2.2.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;
}
|
函数提供了四种匹配方式,设备树匹配方式和acpi匹配方式以及id_table匹配方式,如果前面三种都没有匹配成功,则通过设备名进行配对。
7.2.3. spi控制器驱动¶
这小节简单介绍下spi控制器,以H618芯片为例,该芯片有2个spi控制器,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | spi1: spi@5011000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "allwinner,sun50i-spi";
device_type = "spi1";
reg = <0x0 0x05011000 0x0 0x1000>;
interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_PLL_PERIPH0>, <&ccu CLK_SPI1>, <&ccu CLK_BUS_SPI1>;
clock-names = "pll", "mod", "bus";
resets = <&ccu RST_BUS_SPI1>;
clock-frequency = <100000000>;
spi1_cs_number = <1>;
spi1_cs_bitmap = <1>;
dmas = <&dma 23>, <&dma 23>;
dma-names = "tx", "rx";
status = "disabled";
};
|
spi@5011000 :spi1的设备节点,其寄存器基地址为0x5011000。
device_type :指定设备类型为spi1。
reg :指定设备的寄存器地址范围。这里的0x05011000是基地址,0x1000是地址范围的大小。
interrupts :指定设备使用的中断。GIC_SPI 13表示中断号13,IRQ_TYPE_LEVEL_HIGH表示中断类型为高电平触发。
clocks :指定设备使用的时钟源,&ccu表示时钟控制单元,CLK_PLL_PERIPH0等是具体时钟。
clock-names :为时钟源命名。
resets :指定设备的复位信号。
clock-frequency :指定时钟频率为100MHz。
spi1_cs_number :指定SPI1控制器的片选数量为1。
spi1_cs_bitmap :指定SPI1控制器的片选位图,表示使用哪个片选信号。
dmas :指定设备使用的DMA通道。
dma-names :为DMA通道命名,分别为tx和rx。
compatible :属性值与主机驱动匹配;在内核源码/drivers/spi/spi-sunxi.c中可以找到:
1 2 3 4 5 6 7 | static const struct of_device_id sunxi_spi_match[] = {
{ .compatible = "allwinner,sun8i-spi", },
{ .compatible = "allwinner,sun20i-spi", },
{ .compatible = "allwinner,sun50i-spi", },
{},
};
MODULE_DEVICE_TABLE(of, sunxi_spi_match);
|
驱动控制器通过下面module_platform_driver()注册:
1 2 3 4 5 6 7 8 9 10 | static struct platform_driver sunxi_spi_driver = {
.probe = sunxi_spi_probe,
.remove = sunxi_spi_remove,
.driver = {
.name = SUNXI_SPI_DEV_NAME,
.owner = THIS_MODULE,
.pm = SUNXI_SPI_DEV_PM_OPS,
.of_match_table = sunxi_spi_match,
},
};
|
当匹配到“allwinner,sun50i-spi”时,调用sunxi_spi_probe函数,进行初始化,获取设备树节点信息,初始化spi时钟、dma、中断等, 最后控制器的注册。
spi-sunxi.c中sunxi_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 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 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | static int sunxi_spi_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node; //获取设备树节点of_node,用于从设备树中读取配置信息
struct resource *mem_res;
struct sunxi_spi *sspi;
struct sunxi_spi_platform_data *pdata;
struct spi_master *master;
struct sunxi_slave *slave = NULL;
char spi_para[16] = {0};
int ret = 0, err = 0, irq;
if (np == NULL) { //检查设备树节点是否存在
SPI_ERR("SPI failed to get of_node\n");
return -ENODEV;
}
pdev->id = of_alias_get_id(np, "spi"); //从设备树中获取SPI控制器别名,spi0、spi1等
if (pdev->id < 0) {
SPI_ERR("SPI failed to get alias id\n");
return -EINVAL;
}
#if IS_ENABLED(CONFIG_DMA_ENGINE) //如果内核启用了DMA引擎支持,设置设备的DMA掩码和一致性DMA掩码
pdev->dev.dma_mask = &sunxi_spi_dma_mask;
pdev->dev.coherent_dma_mask = DMA_BIT_MASK(32);
#endif
pdata = kzalloc(sizeof(struct sunxi_spi_platform_data), GFP_KERNEL); //分配内存用于存储平台数据
if (pdata == NULL) {
SPI_ERR("SPI failed to alloc mem\n");
return -ENOMEM;
}
pdev->dev.platform_data = pdata;
mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //获取设备的内存资源(寄存器地址范围)
if (mem_res == NULL) {
SPI_ERR("Unable to get spi MEM resource\n");
ret = -ENXIO;
goto err0;
}
irq = platform_get_irq(pdev, 0); //获取设备的中断号
if (irq < 0) {
SPI_ERR("No spi IRQ specified\n");
ret = -ENXIO;
goto err0;
}
snprintf(spi_para, sizeof(spi_para), "spi%d_cs_number", pdev->id);
ret = of_property_read_u32(np, spi_para, &pdata->cs_num); //从设备树中读取SPI控制器的片选数量
if (ret) {
SPI_ERR("Failed to get cs_number property\n");
ret = -EINVAL;
goto err0;
}
/* create spi master */
master = spi_alloc_master(&pdev->dev, sizeof(struct sunxi_spi)); //分配一个SPI主控制器spi_master对象
if (master == NULL) {
SPI_ERR("Unable to allocate SPI Master\n");
ret = -ENOMEM;
goto err0;
}
platform_set_drvdata(pdev, master); //将SPI主控制器对象与平台设备关联
sspi = spi_master_get_devdata(master); //获取SPI主控制器的私有数据(sunxi_spi结构体)
memset(sspi, 0, sizeof(struct sunxi_spi));
sspi->master = master;
#if IS_ENABLED(CONFIG_DMA_ENGINE)
sspi->dma_rx.dir = SPI_DMA_RWNULL;
sspi->dma_tx.dir = SPI_DMA_RWNULL;
#endif
sspi->busy = SPI_FREE;
sspi->mode_type = MODE_TYPE_NULL;
sspi->irq = irq;
master->max_speed_hz = SPI_MAX_FREQUENCY; //设置SPI控制器的最大时钟频率
master->dev.of_node = pdev->dev.of_node;
master->bus_num = pdev->id;
master->setup = sunxi_spi_setup; //设置SPI控制器的setup回调函数,用于配置SPI设备
master->can_dma = sunxi_spi_can_dma;
master->transfer_one = sunxi_spi_transfer_one; //设置SPI控制器的transfer_one回调函数,用于处理单个SPI传输
master->use_gpio_descriptors = true;
master->set_cs = sunxi_spi_set_cs;
master->num_chipselect = pdata->cs_num; //设置SPI控制器的片选数量
master->bits_per_word_mask = SPI_BPW_MASK(8);
/* the spi->mode bits understood by this driver: */
master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH
| SPI_LSB_FIRST | SPI_TX_DUAL | SPI_TX_QUAD
| SPI_RX_DUAL | SPI_RX_QUAD;
ret = of_property_read_u32(np, "spi_dbi_enable", &sspi->dbi_enabled); //从设备树中读取是否启用DBI(Display Bus Interface)模式
if (ret == 0)
dprintk(DEBUG_INIT, "[spi%d] SPI DBI INTERFACE\n",
sspi->master->bus_num);
else
sspi->dbi_enabled = 0;
snprintf(sspi->dev_name, sizeof(sspi->dev_name), SUNXI_SPI_DEV_NAME"%d", pdev->id);
err = devm_request_irq(&pdev->dev, irq, sunxi_spi_handler, 0, //注册中断处理函数sunxi_spi_handler
sspi->dev_name, sspi);
if (err) {
SPI_ERR("[spi%d] Cannot request IRQ\n", sspi->master->bus_num);
ret = -EINVAL;
goto err1;
}
if (request_mem_region(mem_res->start,
resource_size(mem_res), pdev->name) == NULL) {
SPI_ERR("[spi%d] Req mem region failed\n", sspi->master->bus_num);
ret = -ENXIO;
goto err2;
}
sspi->base_addr = ioremap(mem_res->start, resource_size(mem_res)); //将设备的寄存器地址映射到内核虚拟地址空间
if (sspi->base_addr == NULL) {
SPI_ERR("[spi%d] Unable to remap IO\n", sspi->master->bus_num);
ret = -ENXIO;
goto err3;
}
sspi->base_addr_phy = mem_res->start;
sspi->pdev = pdev;
pdev->dev.init_name = sspi->dev_name;
err = sunxi_spi_resource_get(sspi);
if (err) {
SPI_ERR("[spi%d] resource get error\n", sspi->master->bus_num);
ret = -EINVAL;
goto err1;
}
/* Setup Deufult Mode */
ret = sunxi_spi_hw_init(sspi, pdata, &pdev->dev); //初始化SPI控制器的硬件
if (ret != 0) {
SPI_ERR("[spi%d] spi hw init failed!\n", sspi->master->bus_num);
ret = -EINVAL;
goto err4;
}
spin_lock_init(&sspi->lock); //初始化自旋锁,用于保护共享资源
init_completion(&sspi->done); //初始化完成量,用于同步操作
if (sspi->mode) { //如果SPI控制器处于从模式,创建一个内核线程来处理从设备的数据传输
slave = kzalloc(sizeof(*slave), GFP_KERNEL);
if (IS_ERR_OR_NULL(slave)) {
SPI_ERR("[spi%d] failed to alloc mem\n", sspi->master->bus_num);
ret = -ENOMEM;
goto err5;
}
sspi->slave = slave;
sspi->slave->set_up_txdata = sunxi_spi_slave_set_txdata;
sspi->task = kthread_create(sunxi_spi_slave_task, sspi, "spi_slave");
if (IS_ERR(sspi->task)) {
SPI_ERR("[spi%d] unable to start kernel thread.\n", sspi->master->bus_num);
ret = PTR_ERR(sspi->task);
sspi->task = NULL;
ret = -EINVAL;
goto err6;
}
wake_up_process(sspi->task);
} else {
if (spi_register_master(master)) { //如果SPI控制器处于主模式,注册SPI主控制器
SPI_ERR("[spi%d] cannot register SPI master\n", sspi->master->bus_num);
ret = -EBUSY;
goto err6;
}
}
sunxi_spi_create_sysfs(pdev); //创建sysfs接口,用于用户空间与驱动程序的交互
dprintk(DEBUG_INFO, "[spi%d] loaded for Bus with %d Slaves at most\n", //打印调试信息,表示驱动加载成功
master->bus_num, master->num_chipselect);
dprintk(DEBUG_INIT, "[spi%d]: driver probe succeed, base %px, irq %d\n",
master->bus_num, sspi->base_addr, sspi->irq);
return 0;
//以下错误处理标签用于在发生错误时释放已分配的资源,如内存、中断、寄存器映射等,避免资源泄漏
err6:
if (sspi->mode)
if (!IS_ERR_OR_NULL(slave))
kfree(slave);
err5:
sunxi_spi_hw_exit(sspi, pdata);
err4:
iounmap(sspi->base_addr);
err3:
release_mem_region(mem_res->start, resource_size(mem_res));
err2:
free_irq(sspi->irq, sspi);
err1:
platform_set_drvdata(pdev, NULL);
spi_master_put(master);
err0:
kfree(pdata);
return ret;
}
|
7.2.4. spi设备驱动¶
spi总线驱动,由硬件供应商提供,我们只需要了解,学习其原理就可以, 下面涉及的函数,我们将会在spi设备驱动中使用。
spi设备的注册和注销函数分别在驱动的入口和出口函数中调用,这与平台设备驱动、i2c设备驱动相同,
spi设备注册和注销函数如下:
1 2 | int spi_register_driver(struct spi_driver *sdrv)
static inline void spi_unregister_driver(struct spi_driver *sdrv)
|
对比i2c设备的注册和注销函数,不难发现把“spi”换成“i2c”就是i2c设备的注册和注销函数了,并且用法相同。
参数:
spi spi_driver类型的结构体(spi设备驱动结构体),一个spi_driver结构体就代表了一个spi设备驱动
返回值:
成功: 0
失败: 其他任何值都为错误码
7.2.4.1. spi_setup()函数¶
函数设置spi设备的片选信号、传输单位、最大传输速率等,函数中调用spi控制器的成员controller->setup(), 也就是master->setup,在前面的函数rockchip_spi_probe()中初始化了“ctlr->setup = rockchip_spi_setup;”。
1 | int spi_setup(struct spi_device *spi)
|
参数:
spi spi_device spi设备结构体
返回值:
成功: 0
失败: 其他任何值都为错误码
7.2.5. spi同步与互斥¶
spi_message通过成员变量queue将一系列的spi_message串联起来,第一个spi_message挂在struct list_head queue下面 spi_message还有struct list_head transfers成员变量,transfer也是被串联起来的,如下图所示。

7.2.5.1. SPI同步传输数据¶
阻塞当前线程进行数据传输,spi_sync()内部调用__spi_sync()函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。
1 2 3 4 5 6 7 8 9 10 | int spi_sync(struct spi_device *spi, struct spi_message *message)
{
int ret;
mutex_lock(&spi->controller->bus_lock_mutex);
ret = __spi_sync(spi, message);
mutex_unlock(&spi->controller->bus_lock_mutex);
return ret;
}
|
__spi_sync()函数实现如下:
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 | static int __spi_sync(struct spi_device *spi, struct spi_message *message)
{
int status;
struct spi_controller *ctlr = spi->controller;
unsigned long flags;
status = __spi_validate(spi, message);
if (status != 0)
return status;
message->complete = spi_complete;
message->context = &done;
message->spi = spi;
...
if (ctlr->transfer == spi_queued_transfer) {
spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags);
trace_spi_message_submit(message);
status = __spi_queued_transfer(spi, message, false);
spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags);
} else {
status = spi_async_locked(spi, message);
}
if (status == 0) {
...
wait_for_completion(&done);
status = message->status;
}
message->context = NULL;
return status;
}
|
第7-9行: 函数内部首先调用__spi_validate对spi各个通信参数进行校验
第11-13行: 对message结构体进行初始化,其中第11行,当消息发送完毕后,spi_complete回调函数将被执行。
第30行: 阻塞当前线程,当message发送完成时结束阻塞。
7.2.5.2. SPI异步传输数据¶
1 2 3 4 5 6 | int spi_async(struct spi_device *spi, struct spi_message *message)
{
...
ret = __spi_async(spi, message);
...
}
|
在驱动程序中调用async时不会阻塞当前进程,只是把当前message结构体添加到当前spi控制器成员queue的末尾。 然后在内核中新增加一个工作,这个工作的内容就是去处理这个message结构体。
1 2 3 4 5 6 | static int __spi_async(struct spi_device *spi, struct spi_message *message)
{
struct spi_controller *ctlr = spi->controller;
...
return ctlr->transfer(spi, message);
}
|
7.3. oled屏幕驱动实验¶
spi_oled驱动和我们上一节介绍的i2c_mpu6050设备驱动非常相似,可对比学习,推荐先学习i2c_mpu6050驱动。
本章配套源码和设备树插件位于:linux_driver/spi_oled
7.3.1. 硬件介绍¶
本实验以LubanCat-A1板卡为例,其他板卡同样可以,需要自行查找选定spi接口,oled模块使用的是 《野火【OLED屏_SPI_0.96寸】模块资料》 。
7.3.1.1. 硬件连接¶
在oled驱动中我们使用LubanCat-A1板卡的spi1,可以通过快速使用手册的pin引脚对照图章节找到对应引脚,如下图所示:

oled屏和板卡引脚对应连接如下表:
OLED显示屏引脚 |
板卡GPIO |
说明” |
---|---|---|
MOSI |
PH7 |
MOSI引脚 |
未使用 |
MISO引脚 |
|
CLK |
PH6 |
SPI时钟引脚 |
D/C |
PC7 |
数据、命令控制引脚,可任选 |
CS |
PH5 |
片选引脚 |
GND |
GND |
地 |
VCC |
3.3V |
电源 |
7.3.2. spi设备树和设备树插件¶
使用spi1和oled屏通信,下面是spi1控制器的设备树代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | spi1: spi@5011000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "allwinner,sun50i-spi";
device_type = "spi1";
reg = <0x0 0x05011000 0x0 0x1000>;
interrupts = <GIC_SPI 13 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&ccu CLK_PLL_PERIPH0>, <&ccu CLK_SPI1>, <&ccu CLK_BUS_SPI1>;
clock-names = "pll", "mod", "bus";
resets = <&ccu RST_BUS_SPI1>;
clock-frequency = <100000000>;
spi1_cs_number = <1>;
spi1_cs_bitmap = <1>;
dmas = <&dma 23>, <&dma 23>;
dma-names = "tx", "rx";
status = "disabled";
};
|
定义了spi1控制器的寄存器地址、中断、时钟、复位信号、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 | &spi1 {
spi_slave_mode = <0>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi1_pins_a &spi1_pins_b>;
pinctrl-1 = <&spi1_pins_c>;
spi1_cs_number = <2>; //控制器cs脚数量
spi1_cs_bitmap = <3>; // cs0‑ 0x1; cs1‑0x2, cs0&cs1‑0x3.
status = "disabled";
// /dev/spidev1.0
spi_board1@0 {
device_type = "spi_board1";
compatible = "rohm,dh2228fv";
spi-max-frequency = <100000000>;
reg = <0x0>;
spi-rx-bus-width = <0x1>;
spi-tx-bus-width = <0x1>;
};
// /dev/spidev1.1
spi_board1@1 {
device_type = "spi_board1";
compatible = "rohm,dh2228fv";
spi-max-frequency = <100000000>;
reg = <0x1>;
spi-rx-bus-width = <0x1>;
spi-tx-bus-width = <0x1>;
};
};
|
设置spi总线引脚复用、cs脚数量、cs脚使用cs0&cs1,spi_board1@0和spi_board1@1分别对应cs0、cs1片选的设备节点
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 | spi1_pins_a: spi1@0 {
allwinner,pins = "PH6", "PH7", "PH8";
allwinner,pname = "spi1_sclk", "spi1_mosi", "spi1_miso";
allwinner,function = "spi1";
allwinner,muxsel = <4>;
allwinner,drive = <2>;
allwinner,pull = <0>;
};
spi1_pins_b: spi1@1 {
allwinner,pins = "PH5", "PH9";
allwinner,pname = "spi1_cs0", "spi1_cs1";
allwinner,function = "spi1";
allwinner,muxsel = <4>;
allwinner,drive = <2>;
allwinner,pull = <1>; /* only CS should be pulled up */
};
spi1_pins_c: spi1@2 {
allwinner,pins = "PH5", "PH6", "PH7", "PH8", "PH9";
allwinner,function = "gpio_in";
allwinner,muxsel = <0>;
allwinner,drive = <2>;
allwinner,pull = <0>;
};
|
配置引脚复用,确保spi1控制器在正常工作和休眠状态下使用正确的引脚配置。
以上内容是默认配置好了的,但是本实验希望使用cs0片选连接oled屏,不需要开启第二个cs片选引脚。
本章节使用设备树插件方式,在内核源码/arch/arm64/boot/dts/sunxi/overlay/目录创建h618-lubancat-spi1-oled-overlay.dts, 内容如下所示
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 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/gpio/sun4i-gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
/ {
fragment@0 {
target = <&spi1>;
__overlay__ {
spi_slave_mode = <0>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&spi1_pins_a &spi1_pins_b>;
pinctrl-1 = <&spi1_pins_c>;
spi1_cs_number = <1>; //控制器cs脚数量
spi1_cs_bitmap = <1>; // cs0‑ 0x1; cs1‑0x2, cs0&cs1‑0x3.
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
spi_oled@0 {
compatible = "fire,spi_oled";
reg = <0>;
spi-max-frequency = <24000000>; //spi output clock
dc_control_pin = <&pio PC 7 GPIO_ACTIVE_HIGH>;
};
};
};
};
|
第12行: 配置spi从机模式0。
第13-15行: 配置spi1相关引脚,正常状态和休眠状态的引脚配置。
第16-17行: 设置cs引脚数量,使用哪个cs引脚,此处使用cs0。
第23行: 设置compatible属性,用于与驱动进行匹配。
第25行: 设置SPI传输的最大频率,根据实际oled设备。
第26行: 指定D/C引脚,此处使用PC7。
向pinctrl子系统添加引脚具体内容可参考 GPIO子系统章节 ,设备树的几个引脚与spi_oled显示屏引脚对应关系、引脚的功能、以及在开发板上的位置如前面表格所示。 需要注意的是spi_oled显示屏没有MISO引脚,直接空出即可,spi_oled显示屏需要一个额外的引脚连接D/C, 用于控制spi发送的是数据还是控制命令(高电平是数据,低电平是控制命令)。
7.3.3. 实验代码讲解¶
7.3.3.1. 编程思路¶
spi_oled驱动使用设备树插件方式开发,主要工作包三部分内容。
第一,编写spi_oled的设备树插件,开启spi1,在spi1设备节点下添加spi_oled节点。
第二,编写spi_oled驱动程序,包含驱动的入口、出口函数实现,.prob函数实现,file_operations函数集实现。
第三,编写简单测试应用程序。
以下内容请结合源码查看,本章配套源码位于:linux_driver/spi_oled/
7.3.3.2. 驱动的入口和出口函数实现¶
驱动入口和出口函数与I2C_mpu6050驱动相似,只是把i2c替换为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 | /*字符设备操作函数集*/
static struct file_operations oled_chr_dev_fops = {
.owner = THIS_MODULE,
.open = oled_open,
.write = oled_write,
.release = oled_release};
static int oled_remove(struct spi_device *spi)
{
/*删除设备*/
device_destroy(class_oled, oled_devno); //清除设备
class_destroy(class_oled); //清除类
cdev_del(&oled_chr_dev); //清除设备号
unregister_chrdev_region(oled_devno, DEV_CNT); //取消注册字符设备
return 0;
}
/*指定 ID 匹配表*/
static const struct spi_device_id oled_device_id[] = {
{"fire,spi_oled", 0},
{}};
/*指定设备树匹配表*/
static const struct of_device_id oled_of_match_table[] = {
{.compatible = "fire,spi_oled"},
{}};
/*spi 总线设备结构体*/
struct spi_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.id_table = oled_device_id,
.driver = {
.name = "spi_oled",
.owner = THIS_MODULE,
.of_match_table = oled_of_match_table,
},
};
/*
*驱动初始化函数
*/
static int __init oled_driver_init(void)
{
int error;
pr_info("oled_driver_init\n");
error = spi_register_driver(&oled_driver);
return error;
}
/*
*驱动注销函数
*/
static void __exit oled_driver_exit(void)
{
pr_info("oled_driver_exit\n");
spi_unregister_driver(&oled_driver);
}
|
第18-26行: 这里定义了两个匹配表,第一个是传统的匹配表(可省略)。第二个是和设备树节点匹配的匹配表,保证与设备树节点.compatible属性设定值相同即可。
第29-38行: 定义spi_driver类型结构体。该结构体可类比i2c_driver和platform_driver。
第43-58行: 驱动的入口和出口函数,在入口函数只需要注册一个spi设备驱动,在出口函数中注销。
7.3.3.3. .prob函数实现¶
在.prob函数中完成两个主要工作是,申请gpio控制D/C引脚和初始化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 | /*----------------平台驱动函数集-----------------*/
static int oled_probe(struct spi_device *spi)
{
int ret = -1; //保存错误状态码
struct device_node *node;
printk(KERN_EMERG "\t match successed \n");
/*获取 spi_oled 设备树节点*/
node = spi->dev.of_node;
if (!node) {
printk(KERN_ERR "Failed to get device tree node\n");
return -ENODEV;
}
/*获取 oled 的 D/C 控制引脚并设置为输出,默认高电平*/
oled_control_pin_number = of_get_named_gpio(node, "dc_control_pin", 0);
printk("oled_control_pin_number = %d,\n ", oled_control_pin_number);
gpio_direction_output(oled_control_pin_number, 1);
/*初始化spi*/
oled_spi_device = spi;
spi_setup(oled_spi_device);
/*---------------------注册 字符设备部分-----------------*/
//采用动态分配的方式,获取设备编号,次设备号为0,
//设备名称为rgb-leds,可通过命令cat /proc/devices查看
//DEV_CNT为1,当前只申请一个设备编号
ret = alloc_chrdev_region(&oled_devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0)
{
printk("fail to alloc oled_devno\n");
goto alloc_err;
}
//关联字符设备结构体cdev与文件操作结构体file_operations
oled_chr_dev.owner = THIS_MODULE;
cdev_init(&oled_chr_dev, &oled_chr_dev_fops);
// 添加设备至cdev_map散列表中
ret = cdev_add(&oled_chr_dev, oled_devno, DEV_CNT);
if (ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
/*创建类 */
class_oled = class_create(THIS_MODULE, DEV_NAME);
/*创建设备 DEV_NAME 指定设备名,*/
device_oled = device_create(class_oled, NULL, oled_devno, NULL, DEV_NAME);
/*打印oled_spi_device 部分内容*/
printk("max_speed_hz = %d\n", oled_spi_device->max_speed_hz);
printk("chip_select = %d\n", (int)oled_spi_device->chip_select);
printk("bits_per_word = %d\n", (int)oled_spi_device->bits_per_word);
printk("mode = %02X\n", oled_spi_device->mode);
printk("cs_gpio = %02X\n", oled_spi_device->cs_gpio);
return 0;
add_err:
// 添加设备失败时,需要注销设备号
unregister_chrdev_region(oled_devno, DEV_CNT);
printk("\n error! \n");
alloc_err:
return -1;
}
|
.prob函数介绍如下:
第6-15行: 获取SPI设备对应的设备树节点
第18-20行: 获取D/C控制引脚并配置,将GPIO引脚配置为输出模式,并设置默认电平为高。
第23-25行: 保存SPI设备结构体指针,供后续使用。配置SPI设备的模式、时钟频率等参数。
第32-55行: 注册初始化字符设备,创建设备类和设备节点。
第58-62行: 打印SPI设备的配置信息,包括最大时钟频率、片选号、数据位宽、模式和CS引脚。
第66-72行: 错误处理,如果字符设备注册失败,释放已分配的设备号并返回错误。
7.3.3.4. 字符设备操作函数集实现¶
字符设备操作函数集是驱动对外的接口,我们要在这些函数中实现对spi_oled的初始化、写入、关闭等等工作。 这里共实现三个函数,.open函数用于实现spi_oled的初始化,.write函数用于向spi_oled写入显示数据,.release函数用于关闭spi_oled。
.open函数实现
在open函数中完成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 | /*oled 初始化函数*/
void spi_oled_init(void)
{
/*初始化oled*/
oled_send_commands(oled_spi_device, oled_init_data, sizeof(oled_init_data));
/*清屏*/
oled_fill(0x00);
}
/*字符设备操作函数集,open函数实现*/
static int oled_open(struct inode *inode, struct file *filp)
{
spi_oled_init(); //初始化显示屏
return 0;
}
/*字符设备操作函数集,.write函数实现*/
static ssize_t oled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off)
{
int copy_number=0;
/*申请内存*/
oled_display_struct *write_data;
write_data = (oled_display_struct*)kzalloc(cnt, GFP_KERNEL);
copy_number = copy_from_user(write_data, buf,cnt);
oled_display_buffer(write_data->display_buffer, write_data->x, write_data->y, write_data->length);
/*释放内存*/
kfree(write_data);
return 0;
}
/*字符设备操作函数集,.release函数实现*/
static int oled_release(struct inode *inode, struct file *filp)
{
oled_send_command(oled_spi_device, 0xae);//关闭显示
return 0;
}
|
如上代码所示,open函数只调用了自定义spi_oled_init函数,在spi_oled_init函数函数最终会调用oled_send_commands函数初始化spi_oled,然后调用清屏函数。 而oled_release函数调用oled_send_command关闭显示。oled_send_command函数用于发送单个命令,oled_send_commands用于发送多个命令。以下是两函数实现:
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 | /*
*函数功能:向oled发送一个命令
*spi_device oled设备驱动对应的spi_device结构体。
*command 要发送的数据。
*返回值:成功,返回0 失败返回负数。
*/
static int oled_send_command(struct spi_device *spi_device, u8 command)
{
int error = 0;
u8 tx_data = command;
struct spi_message *message; //定义发送的消息
struct spi_transfer *transfer; //定义传输结构体
/*设置 D/C引脚为低电平*/
gpio_direction_output(oled_control_pin_number, 0);
/*申请空间*/
message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
/*填充message和transfer结构体*/
transfer->tx_buf = &tx_data;
transfer->len = 1;
spi_message_init(message);
spi_message_add_tail(transfer, message);
error = spi_sync(spi_device, message);
kfree(message);
kfree(transfer);
if (error != 0)
{
printk("spi_sync error! \n");
return -1;
}
gpio_direction_output(oled_control_pin_number, 1);
return 0;
}
/*
*函数功能:向oled发送一组命令
*spi_device oled设备驱动对应的spi_device结构体。
*commands 要发送的数据。
*返回值:成功,返回0 失败返回负数。
*/
static int oled_send_commands(struct spi_device *spi_device, u8 *commands, u16 lenght)
{
int error = 0;
struct spi_message *message; //定义发送的消息
struct spi_transfer *transfer; //定义传输结构体
/*申请空间*/
message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
/*设置 D/C引脚为低电平*/
gpio_direction_output(oled_control_pin_number, 0);
/*填充message和transfer结构体*/
transfer->tx_buf = commands;
transfer->len = lenght;
spi_message_init(message);
spi_message_add_tail(transfer, message);
error = spi_sync(spi_device, message);
kfree(message);
kfree(transfer);
if (error != 0)
{
printk("spi_sync error! \n");
return -1;
}
return error;
}
|
第22-23行: 设置transfer->tx_buf为命令数据,transfer->len为1,表示发送1个字节。
第24-27行: 使用spi_message_init初始化消息,spi_message_add_tail将传输添加到消息中,spi_sync同步发送数据。
第28-35行: 释放动态分配的内存,将D/C引脚恢复为高电平。
第59-60行: 设置transfer->tx_buf为命令数组,transfer->len为lenght,表示发送lenght个字节。
.write函数实现
.write函数用于接收来自应用程序的数据,并显示这些数据。函数实现如下所示:
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 | /*字符设备操作函数集,.write函数实现*/
static ssize_t oled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off)
{
int copy_number=0;
/*申请内存*/
oled_display_struct *write_data;
write_data = (oled_display_struct*)kzalloc(cnt, GFP_KERNEL); //使用kzalloc动态分配内存,用于存储从用户空间复制的数据
copy_number = copy_from_user(write_data, buf,cnt); //将用户空间的数据复制到内核空间
oled_display_buffer(write_data->display_buffer, write_data->x, write_data->y, write_data->length); //将数据显示到OLED屏幕上
/*释放内存*/
kfree(write_data);
return 0;
}
/*
*向 oled 发送要显示的数据,x, y 指定显示的起始位置,支持自动换行
*spi_device,指定oled 设备驱动的spi 结构体
*display_buffer, 数据地址
*x, y,起始坐标。
*length, 发送长度
*/
static int oled_display_buffer(u8 *display_buffer, u8 x, u8 y, u16 length)
{
u16 index = 0;
int error = 0;
do
{
/*设置写入的起始坐标*/
error += oled_send_command(oled_spi_device, 0xb0 + y);
error += oled_send_command(oled_spi_device, ((x & 0xf0) >> 4) | 0x10);
error += oled_send_command(oled_spi_device, (x & 0x0f) | 0x01);
if (length > (X_WIDTH - x)) //处理自动换行,如果显示长度超过当前行的剩余宽度(X_WIDTH - x),则自动换行
{
error += oled_send_data(oled_spi_device, display_buffer + index, X_WIDTH - x); //将数据发送到OLED屏幕
length -= (X_WIDTH - x);
index += (X_WIDTH - x);
x = 0;
y++;
}
else
{
error += oled_send_data(oled_spi_device, display_buffer + index, length);
index += length;
// x += length;
length = 0;
}
} while (length > 0);
if (error != 0)
{
/*发送错误*/
printk("oled_display_buffer error! %d \n",error);
return -1;
}
return index;
}
/*数据发送结构体*/
typedef struct oled_display_struct //用于存储显示数据和显示参数
{
u8 x;
u8 y;
u32 length;
u8 display_buffer[];
}oled_display_struct;
|
当用户空间调用write系统调用时,内核会调用该函数,从用户空间接收数据,并将其发送到OLED显示屏。
7.3.3.5. 编写测试应用程序¶
测试应用程序主要用来测试驱动,实现oled显示屏实现刷屏、显示文字、显示图片。 测试程序需要用到字符以及图片的点阵数据保存在oled_code_table.c文件,为方便管理我们编写了一个简单makefile文件方便我们编译程序。
其makefile文件如下所示,也可以直接使用命令编译:
1 2 3 4 5 6 7 8 | out_file_name = "test_app"
all: test_app.c oled_code_table.c
aarch64-linux-gnu-gcc $^ -o $(out_file_name)
.PHONY: clean
clean:
rm $(out_file_name)
|
下面是我们的测试程序源码。如下所示:
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 | /*点阵数据*/
extern unsigned char F16x16[];
extern unsigned char F6x8[][6];
extern unsigned char F8x16[][16];
extern unsigned char BMP1[];
int main(int argc, char *argv[])
{
int error = -1;
/*打开文件*/
int fd = open("/dev/spi_oled", O_RDWR);
if (fd < 0)
{
printf("open file : %s failed !\n", argv[0]);
return -1;
}
while(1)
{
/*显示图片*/
show_bmp(fd, 0, 0, BMP1, X_WIDTH*Y_WIDTH/8);
sleep(2);
oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏
oled_show_F16X16_letter(fd,0, 0, F16x16, 4); //显示汉字
oled_show_F8X16_string(fd,0,2,"F8X16:THIS IS SPI TEST APP");
oled_show_F6X8_string(fd, 0, 6,"F6X8:THIS IS SPI TEST APP");
sleep(2);
oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏
oled_show_F8X16_string(fd,0,0,"Testing is completed");
sleep(2);
oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏
}
/*关闭文件*/
error = close(fd);
if(error < 0)
{
printf("close file error! \n");
}
return 0;
}
|
测试程序很简单,完整代码请参考配套例程,结合代码简单介绍如下:
第2-5行: 测试程序要用到的点阵数据,我们显示图片、汉字之前都要把它们转化为点阵数据。野火spi_oled模块配套资料提供有转换工具以及使用说明。
第11行: 打开spi_oled的设备节点,这个根据自己的驱动而定,我们使用的驱动源码就是这个路径。
第18行: 显示图片测试,这里需要说明的是由于测试程序不那么完善,图片显示起始位置x坐标应当设置为0,这样在循环显示时才不会乱。显示长度应当为显示屏的像素数除以8,因为每个字节8位,这8位控制8个像素点。
第22-25行: 测试显示汉字和不同规格的字符。
第28-33行: 显示测试结束提示语。
7.3.4. 实验准备¶
7.3.4.1. 编译设备树插件¶
前面介绍了在内核源码/arch/arm64/boot/dts/sunxi/overlay/目录下添加h618-lubancat-spi1-oled-overlay.dts设备树插件, 为编译该设备树插件需要在内核源码/arch/arm64/boot/dts/sunxi/overlay/目录下的Makefile添加编译该设备树配置,如下图所示:

在内核源码顶层目录执行以下命令编译设备树插件:
#加载配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- linux_h618_defconfig
#使用dtbs参数单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
编译成功后生成的设备树插件文件 h618-lubancat-spi1-oled-overlay.dtbo
位于内核源码/arch/arm64/boot/dts/sunxi/overlays/目录下
7.3.4.2. 编译驱动程序¶
可参考配套源码linux_driver/spi_oled/目录下的文件,Makefile和前面章节大致相同,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | KERNEL_DIR=../../kernel/
ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-nu-
export ARCH CROSS_COMPILE
obj-m := spi_oled.o
CFLAGS_spi_oled.o := -fno-stack-protector
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
|
编译得到spi_oled.ko驱动模块。
7.3.5. 程序运行结果¶
7.3.5.1. 加载设备树插件¶
可以通过SCP、NFS或者sftp等将编译好的设备树插件拷贝到开发板上,然后把设备树插件 h618-lubancat-spi1-oled-overlay.dtbo
复制到 /boot/dtb/sunxi/overlay/
目录下。
打开/boot/uEnv.txt文件,添加设备树插件配置,如图所示:

重启开发板,设备树插件加载成功后会有:

7.3.5.2. 测试效果¶
将先前编译好的spi_oled.ko驱动及测试test_app上传至开发板中。
1 2 3 4 5 6 7 8 | #加载驱动
sudo insmod spi_oled.ko
#查看设备文件
ls /dev/spi_oled
#运行应用程序
sudo ./test_app
|
正常情况下显示屏会显示并自动切换设定的内容,如下所示:
