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:片选信号线

3

i2c通过i2c设备地址选择通信设备,而spi通过片选引脚选中要通信的设备。

spi接口支持有多个片选引脚,连接多个SPI从设备,当然也可以使用外部GPIO扩展SPI设备的数量, 这样一个spi接口可连接的设备数由片选引脚树决定。

  • 如果使用spi接口提供的片选引脚,spi总线驱动会处理好什么时候选spi设备。

  • 如果使用外部GPIO作为片选引脚需要我们在spi设备驱动中设置什么时候选中spi。(或者在配置SPI时指定使用的片选引脚)。

通常情况下无特殊要求我们使用spi接口提供的片选引脚。

7.1.2. spi时序

spi时序
  • 起始信号:NSS 信号线由高变低

  • 停止信号:NSS 信号由低变高

  • 数据传输:在 SCK的每个时钟周期 MOSI和 MISO同时传输一位数据,高/低位传输没有硬性规定

    • 传输单位:8位或16位

    • 单位数量:允许无限长的数据传输

7.1.3. spi通信模式

根据总线空闲时 SCK 的时钟状态以及数据采样时刻,SPI的工作模式分为四种:

spi模式
  • 时钟极性 CPOL:指 SPI 通讯设备处于空闲状态时,SCK信号线的电平信号:

    • CPOL=0时,SCK在空闲状态时为低电平

    • CPOL=1时,SCK在空闲状态时为高电平

  • 时钟相位 CPHA:数据的采样的时刻:

    • CPHA=0时,数据在SCK时钟线的“奇数边沿”被采样

    • CPHA=1时,数据在SCK时钟线的“偶数边沿”被采样

spi模式示例

如上图所示:

SCK信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。

CPHA=0,数据在 SCK 时钟线的“奇数边沿”被采样,当 CPOL=0 的时候,时钟的奇数边沿是上升沿,当 CPOL=1 的时候,时钟的奇数边沿是下降沿。

在linux内核中,有定义这几种通讯模式,一般使用较多的是模式0和模式3

(内核源码/include/linux/spi/spi.h)
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控制器驱动和设备驱动的注册方法、注销方法,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结构体。

 spi_controller (内核源码/include/linux/spi/spi.h)
1
    #define spi_master  spi_controller

7.2.1.2. spi_controller

spi_controller结构体部分成员变量已经被省略,下面列出的是spi_controller结构体关键成员变量:

 spi_controller结构体(内核源码/include/linux/spi/spi.h)
 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结构体

 spi_driver结构体 (内核源码/include/linux/spi/spi.h)
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结构体。

 spi_driver结构体(内核源码/include/linux/spi/spi.h)
 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结构体用于指定要发送的数据,后面称为 传输结构体

 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结构体定义如下所示:

 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总线注册。

spi总线注册 (内核源码/drivers/spi/spi.c)
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总线注册时使用。

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()函数

spi总线注册 (内核源码/drivers/spi/spi.c)
 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控制器,如下:

spi1设备树节点(sun50iw9.dtsi)
 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中可以找到:

(内核源码/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()注册:

(内核源码/drivers/spi/spi-sunxi.c)
 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函数代码如下:

sunxi_spi_probe函数 (内核源码/drivers/spi/spi-sunxi.c)
  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设备注册和注销函数如下:

 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;”。

spi_setup函数(内核源码/drivers/spi/spi.c)
1
int spi_setup(struct spi_device *spi)

参数:

  • spi spi_device spi设备结构体

返回值:

  • 成功: 0

  • 失败: 其他任何值都为错误码

7.2.4.2. spi_message_init()函数

初始化spi_message,

spi_message_init函数(内核源码/include/linux/spi/spi.h)
1
2
3
4
5
static inline void spi_message_init(struct spi_message *m)
{
    memset(m, 0, sizeof *m);
    spi_message_init_no_memset(m);
}

参数:

  • m spi_message 结构体指针,spi_message结构体定义和介绍可在前面关键数据结构中找到。

返回值: 无。

7.2.4.3. spi_message_add_tail()函数

spi_message_init函数(内核源码/include/linux/spi/spi.h)
1
2
3
4
static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{
    list_add_tail(&t->transfer_list, &m->transfers);
}

这个函数很简单就是将将spi_transfer结构体添加到spi_message队列的末尾。

7.2.5. spi同步与互斥

spi_message通过成员变量queue将一系列的spi_message串联起来,第一个spi_message挂在struct list_head queue下面 spi_message还有struct list_head transfers成员变量,transfer也是被串联起来的,如下图所示。

spi传输同步与互斥

7.2.5.1. SPI同步传输数据

阻塞当前线程进行数据传输,spi_sync()内部调用__spi_sync()函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。

spi_sync()函数(内核源码/drivers/spi/spi.c)
 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()函数实现如下:

__spi_sync()函数(内核源码/drivers/spi/spi.c)
 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异步传输数据

spi_async()函数(内核源码/drivers/spi/spi.c)
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结构体。

__spi_async()函数(内核源码/drivers/spi/spi.c)
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引脚对照图章节找到对应引脚,如下图所示:

../../../_images/spi005.png

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控制器的设备树代码:

sun50iw9.dtsi
 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通道、片选数量等硬件信息。

sun50iw9-lubancat-a1.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
&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片选的设备节点

sun50iw9-h618-pinctrl.dtsi
 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, 内容如下所示

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,源码如下所示。

 驱动入口函数实现(linux_driver/spi_oled/spi_oled.c)
 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。

 .prob函数实现(linux_driver/spi_oled/spi_oled.c)
 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的初始化,代码如下:

.open函数实现(linux_driver/spi_oled/spi_oled.c)
 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用于发送多个命令。以下是两函数实现:

.open函数实现(linux_driver/spi_oled/spi_oled.c)
 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函数用于接收来自应用程序的数据,并显示这些数据。函数实现如下所示:

 .write函数实现(linux_driver/spi_oled/spi_oled.c)
 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文件如下所示,也可以直接使用命令编译:

 Makefile(linux_driver/spi_oled/test_app/Makefile.c)
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)

下面是我们的测试程序源码。如下所示:

 测试应用程序 Makefile(linux_driver/spi_oled/test_app/test_app.c)
 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添加编译该设备树配置,如下图所示:

../../../_images/spi006.jpg

在内核源码顶层目录执行以下命令编译设备树插件:

#加载配置文件
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和前面章节大致相同,如下:

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.4.3. 编译应用程序

可参考配套源码linux_driver/spi_oled/test_app目录下的文件,Makefile如下:

Makefile
1
2
3
4
5
6
7
8
9
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)

编译得到test_app应用程序。

7.3.5. 程序运行结果

7.3.5.1. 加载设备树插件

可以通过SCP、NFS或者sftp等将编译好的设备树插件拷贝到开发板上,然后把设备树插件 h618-lubancat-spi1-oled-overlay.dtbo 复制到 /boot/dtb/sunxi/overlay/ 目录下。

打开/boot/uEnv.txt文件,添加设备树插件配置,如图所示:

../../../_images/spi007.jpg

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

../../../_images/spi008.jpg

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

正常情况下显示屏会显示并自动切换设定的内容,如下所示:

../../../_images/spi009.jpg