13. SPI通信

本章通过讲解在应用层中使用SPI总线与外部设备的通讯,讲解Linux系统总线类型设备驱动架构的应用,它与上一章的I2C 总线操作方法非常相似,可以对比学习。

在Linux内核文档的Documentation/SPI目录下有关于SPI驱动非常详细的说明。

本章的示例代码目录为:base_linux/spi

13.1. SPI通讯协议简介

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口, 是一种高速全双工的通信总线。它被广泛地使用在 ADC、 LCD 等设备与 MCU 间,要求通讯速率较高的场合。 学习本章时,可与 I2C 章节对比阅读,体会两种通讯总线的差异。下面我们分别对 SPI 协议的物理层及协议层进行讲解。

13.1.1. SPI物理层

SPI通讯设备之间的常用连接方式见下图。

未找到图片

SPI通讯使用3条总线及片选线,3条总线分别为SCK、MOSI、MISO,片选线为 ,它们的作用介绍如下:

(1) ( Slave Select):从设备选择信号线,常称为片选信号线,也称为NSS、CS,以下用NSS表示。 当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同只使用这3条总线; 而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而SPI协议中没有设备地址, 它使用NSS信号线来寻址,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。 所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号。

(2) SCK (Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通讯的速率, 不同的设备支持的最高时钟频率不一样,如RT1052的SPI时钟频率最大为fpclk/2, 两个设备之间通讯时,通讯速率受限于低速设备。

(3) MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条信号线输出, 从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

(4) MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线读入数据, 从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

13.1.2. 协议层

与I2C的类似,SPI协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。

13.1.2.1. SPI基本通讯过程

先看看SPI通讯的通讯时序,如下图所示。

未找到图片

这是一个主机的通讯时序。NSS、SCK、MOSI信号都由主机控制产生,而MISO的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与MISO的信号只在NSS为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。

以上通讯流程中包含的各个信号分解如下:

13.1.2.2. 通讯的起始和停止信号

在上图中的标号处,NSS信号线由高变低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线, 当从机检在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。 在图中的标号处,NSS信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

13.1.2.3. 数据有效性

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时, MSB先行或LSB先行并没有作硬性规定,但要保证两个SPI通讯设备之间使用同样的协定,一般都会采用上图中的MSB先行模式。

观察图中的标号处,MOSI及MISO的数据在SCK的上升沿期间变化输出,在SCK的下降沿时被采样。即在SCK的下降沿时刻,MOSI及MISO的数据有效,高电平时表示数据“1”, 为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及MISO为下一次表示数据做准备。

SPI每次数据传输可以8位或16位为单位,每次传输的单位数不受限制。

13.1.2.4. CPOL/CPHA及通讯模式

上面讲述的图中的时序只是SPI中的其中一种通讯模式,SPI一共有四种通讯模式,它们的主要区别是总线空闲时SCK的时钟状态以及数据采样时刻。 为方便说明,在此引入“时钟极性CPOL”和“时钟相位CPHA”的概念。

时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、 NSS线为高电平时SCK的状态)。 CPOL=0时, SCK在空闲状态时为低电平,CPOL=1时,则相反。

时钟相位CPHA是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。 当CPHA=1时,数据线在SCK的“偶数边沿”采样。

如下图:

未找到图片

我们来分析这个CPHA=0的时序图。首先,根据SCK在空闲状态时的电平,分为两种情况。SCK信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。

无论CPOL=0还是=1,因为我们配置的时钟相位CPHA=0,在图中可以看到,采样时刻都是在SCK的奇数边沿。注意当CPOL=0的时候,时钟的奇数边沿是上升沿,而CPOL=1的时候,时钟的奇数边沿是下降沿。 所以SPI的采样时刻不是由上升/下降沿决定的。MOSI和MISO数据线的有效信号在SCK的奇数边沿保持不变,数据信号将在SCK奇数边沿时被采样,在非采样时刻,MOSI和MISO的有效信号才发生切换。

类似地,当CPHA=1时,不受CPOL的影响,数据信号在SCK的偶数边沿被采样。

如下图:

未找到图片

由CPOL及CPHA的不同状态,SPI分成了四种模式,见下表,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。

表 SPI的四种模式

SPI模式

CPOL

CPHA

空闲时SCK时钟

采样时刻

0

0

0

低电平

奇数边沿

1

0

1

低电平

偶数边沿

2

1

0

高电平

奇数边沿

3

1

1

高电平

偶数边沿

13.1.3. 扩展SPI协议(Single/Dual/Quad/Octal SPI)

以上介绍的是经典SPI协议的内容,这种也被称为标准SPI协议(Standard SPI)或单线SPI协议(Single SPI), 其中的单线是指该SPI协议中使用单根数据线MOSI进行发送数据,单根数据线MISO进行接收数据。

为了适应更高速率的通讯需求,半导体厂商扩展SPI协议,主要发展出了Dual/Quad/Octal SPI协议, 加上标准SPI协议(Single SPI),这四种协议的主要区别是数据线的数量及通讯方式,具体见下表。

表 四种SPI协议的区别

协议

数据线数量及功能

通讯方式

Single SPI(标准SPI)

1根发送,1根接收

全双工

Dual SPI(双线SPI)

收发共用2根数据线

半双工

Quad SPI(四线SPI)

收发共用4根数据线

半双工

Octal SPI(八线SPI)

收发共用8根数据线

半双工

扩展的三种SPI协议都是半双工的通讯方式,也就是说它们的数据线是分时进行收发数据的。 例如,标准SPI(Single SPI)与双线SPI(Dual SPI)都是两根数据线, 但标准SPI(Single SPI)的其中一根数据线只用来发送,另一根数据线只用来接收, 即全双工;而双线SPI(Dual SPI)的两根线都具有收发功能,但在同一时刻只能是发送或者是接收, 即半双工,四线SPI(Quad SPI)和 八线SPI(Octal SPI)与双线SPI(Dual SPI)类似,只是数据线量的区别。

13.1.3.1. SDR和DDR模式

扩展的SPI协议还增加了SDR模式(单倍速率Single Data Rate)和DDR模式(双倍速率Double DataRate)。 例如在标准SPI协议的SDR模式下,只在SCK的单边沿进行数据传输,即一个SCK时钟只传输一位数据;而在它的DDR模式下, 会在SCK的上升沿和下降沿都进行数据传输,即一个SCK时钟能传输两位数据,传输速率提高一倍。

13.2. SPI相关数据结构与ioctl函数

编写应用程序需要使用到spi_ioc_transfer结构体,如下所示

linux/spi/spidev.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct spi_ioc_transfer {
   __u64             tx_buf;     //发送数据缓存
   __u64             rx_buf;     //接收数据缓存

   __u32             len;        //数据长度
   __u32             speed_hz;   //通讯速率

   __u16             delay_usecs;    //两个spi_ioc_transfer之间的延时,微秒
   __u8              bits_per_word;  //数据长度
   __u8              cs_change;      //取消选中片选
   __u8              tx_nbits;       //单次数据宽度(多数据线模式)
   __u8              rx_nbits;       //单次数据宽度(多数据线模式)
   __u16             pad;

};

在编写应用程序时还需要使用ioctl函数设置spi相关配置,其函数原型如下

1
2
3
 #include <sys/ioctl.h>

 int ioctl(int fd, unsigned long request, ...);

其中对于终端request的值常用的有以下几种

SPI_IOC_RD_MODE32

设置读取SPI模式(对应上文的SPI的四种模式的表格,SPI_MODE_x)

SPI_IOC_WR_MODE32

设置写入SPI模式(对应上文的SPI的四种模式的表格,SPI_MODE_x)

SPI_IOC_RD_LSB_FIRST

设置SPI读取数据模式(LSB先行返回1)

SPI_IOC_WR_LSB_FIRST

设置SPI写入数据模式。(0:MSB,非0:LSB)

SPI_IOC_RD_BITS_PER_WORD

设置SPI读取设备的字长

SPI_IOC_WR_BITS_PER_WORD

设置SPI写入设备的字长

SPI_IOC_RD_MAX_SPEED_HZ

设置读取SPI设备的最大通信频率。

SPI_IOC_WR_MAX_SPEED_HZ

设置写入SPI设备的最大通信速率

SPI_IOC_MESSAGE(N)

一次进行双向/多次读写操作

提示

SPI的读取和写入可以设置为不同的参数。

13.3. LubanCat板卡spi接口

本章主要围绕带有40Pin引脚的LubanCat-RK系列的板子以及带有30Pin引脚的Lubancat-Q1系列板子,如下

  • LubanCat-Zero W

  • LubanCat-Zero N

  • LubanCat-1

  • LubanCat-1N

  • LubanCat-2

  • LubanCat-2N

  • LubanCat-4

40pin引脚中只有一组spi接口SCK,MOSI,MISO,有两个默认片选信号CS0,CS1

其中SPI3的引脚关系如下表所示

SPI

物理引脚

功能

MOSI

19

主设备输出/从设备输入

MISO

21

主设备输入/从设备输出

CLOCK

23

时钟信号线

CS0

24

片选信号线0

CS1

26

片选信号线1

LubanCat0-2系列使用spi3。spidev3.0控制CS0,spidev3.1控制CS1

LubanCat4使用spi0。spidev0.0控制CS0,spidev0.1控制CS1

如下图:

未找到图片

对应实物的40pin接口

LubanCat-Q1使用spi0。spidev0.0控制CS0,spidev0.1控制CS1

如下图:

未找到图片

对应实物的30pin接口

13.3.1. 使能SPI通信接口

SPI接口在默认情况是关闭状态的,需要使能才能使用, 如果要开启spi需要配置两项,一个是spi通信引脚,一个是spi-cs引脚

13.3.1.1. 方法一

1
2
3
4
5
#进入工具配置
sudo fire-config

#移动光标到下图的位置
#按确认键进入配置
未找到图片

打开SPI通信接口和SPI片选接口

  1. 使用方向键移动光标到 SPI

  2. “空格键” 选中SPI(出现 “*” ),如下图

  3. 使用方向键移动光标到 SPI-CS

  4. “空格键” 选中SPI-CS(出现 “*” ),如下图

  5. “确认键” 进行设置

  6. “Esc键” 退出到终端,运行 sudo reboot 进行重启应用

未找到图片

13.3.1.2. 方法二

LubanCat各设备的配置文件

板子名称

配置文件名称

说明

当前使用的板卡

uEnv.txt

系统会自动把板卡的配置文件链接到该文件

LubanCat-RK

uEnvLubanCat-series.txt

通用板卡的配置文件,用于第一次启动配置系统

Lubancat-Zero-N

uEnvLubanCatZN.txt

适用于EBF410068 EBF410068V1

Lubancat-Zero-W

uEnvLubanCatZW.txt

适用于EBF410067 EBF410067V1

Lubancat-1

uEnvLubanCat1.txt

适用于EBF410077 EBF410077V1 EBF410077V2

Lubancat-1N

uEnvLubanCat1N.txt

适用于EBF410052 EBF410052V1

Lubancat-1金手指/BTB

uEnvLubanCat1IO.txt

适用于EBF410090 EBF410132

Lubancat-2

uEnvLubanCat2.txt

适用于EBF410044

Lubancat-2V1

uEnvLubanCat2-V1.txt

适用于EBF410044V1

Lubancat-2V2

uEnvLubanCat2-V2.txt

适用于EBF410044V2

Lubancat-2-n

uEnvLubanCat2N.txt

适用于EBF410076 EBF410076V1

Lubancat-2金手指/BTB

uEnvLubanCat2IO.txt

适用于EBF410298 EBF410297

Lubancat-4

uEnvLubanCat4.txt

适用于EBF410116

Lubancat-Q1

uEnvLubanCatQ1.txt

适用于EBF410434

可以通过打开 /boot/uEnv/board.txt (board是你所用的板子的名称),一般第一次启动已经初始化将板级uEnv.txt软连接到了/boot/uEnv/uEnv.txt,可以直接修改该文件。

查看是否启用了spi相关设备设备树插件。

编辑文件,将带有 spispi-cs 的两行的注释符号去掉 如下图:

未找到图片

然后重启激活设备

注解

如果是直接拔电源的方式重启,会有可能出现文件没能做出修改 (原因:文件未能及时从内存同步到存储设备中,解决方法,在终端上输入 “sync” 再拔电关机)

13.3.2. 检查SPI设备

使能spi设备树插件之后重新启动板卡,通过SPI设备文件来判断spi驱动是否加载成功。 SPI_3对应的设备文件是spidev3.0和spidev3.1

如下所示

1
2
3
root@lubancat:~# ls /dev/spi*
/dev/spidev3.0  /dev/spidev3.1
root@lubancat:~#

spidev3.0和spidev3.1的区别在于片选信号的不同,spidev3.0使用CS0 , spidev3.1使用CS1

13.4. SPI回环通讯测试实验

13.4.1. 硬件连接:

只需要将SPI3的 MIOS与MOSI引脚(板卡上的19和21)使用跳线帽短接即可。

如下图所示

未找到图片spi_show

13.4.2. 编写程序

base_linux/spi/spi_selftest/spi_selftest.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
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <linux/spi/spidev.h>

#define SPI_DEV_PATH "/dev/spidev3.0"

/*SPI 接收 、发送 缓冲区*/
unsigned char tx_buffer[100] = "hello the world !";
unsigned char rx_buffer[100];


int fd;                  					// SPI 控制引脚的设备文件描述符
static unsigned  mode = SPI_MODE_2;         //用于保存 SPI 工作模式
static uint8_t bits = 8;        			// 接收、发送数据位数
static uint32_t speed = 10000000; 			// 发送速度
static uint16_t delay;          			//保存延时时间

void transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
    int ret;

    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .delay_usecs = delay,
        .speed_hz = speed,
        .bits_per_word = bits,
        .tx_nbits = 1,
        .rx_nbits = 1
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
    
    if (ret < 1)
        printf("can't send spi message\n");
}

void spi_init(void)
{
    int ret = 0;
    //打开 SPI 设备
    fd = open(SPI_DEV_PATH, O_RDWR);
    if (fd < 0)
        printf("can't open %s\n",SPI_DEV_PATH);

    //spi mode 设置SPI 工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
    if (ret == -1)
        printf("can't set spi mode\n");

    //bits per word  设置一个字节的位数
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret == -1)
        printf("can't set bits per word\n");

    //max speed hz  设置SPI 最高工作频率
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret == -1)
        printf("can't set max speed hz\n");

    //打印
    printf("spi mode: 0x%x\n", mode);
    printf("bits per word: %d\n", bits);
    printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);
}

int main(int argc, char *argv[])
{
    /*初始化SPI */
    spi_init();

    /*执行发送*/
    transfer(fd, tx_buffer, rx_buffer, sizeof(tx_buffer));

    /*打印 tx_buffer 和 rx_buffer*/
    printf("tx_buffer: \n %s\n ", tx_buffer);
    printf("rx_buffer: \n %s\n ", rx_buffer);
    
    close(fd);
    return 0;
}

13.4.3. 编译&运行

13.4.3.1. 编译

方法1:

1
make

方法2:

1
gcc -o spi_selftest spi_selftest.c

13.4.3.2. 运行

1
2
#必须使用管理者权限运行
sudo ./spi_selftest

如下图:

未找到图片

如果没有连接mosi和miso,则会出现以下情况

未找到图片

13.4.4. 代码分析

SPI代码与上一章的IIC 非常类似。首先打开SPI对应的设备文件,然后写 入配置参数,这样就完成了SPI的初始化,后面的具体功能实现只需要调用对应的API函数即可。SPI初始化代码如下:

13.4.4.1. SPI 初始化代码

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
void spi_init(void)
{
    int ret = 0;
    //打开 SPI 设备
    fd = open(SPI_DEV_PATH, O_RDWR);
    if (fd < 0)
        printf("can't open %s\n",SPI_DEV_PATH);

    //spi mode 设置SPI 工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
    if (ret == -1)
        printf("can't set spi mode\n");

    //bits per word  设置一个字节的位数
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret == -1)
        printf("can't set bits per word\n");

    //max speed hz  设置SPI 最高工作频率
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret == -1)
        printf("can't set max speed hz\n");

    //打印
    printf("spi mode: 0x%x\n", mode);
    printf("bits per word: %d\n", bits);
    printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);
}

结合代码,简单说明如下:

  • 第3-6行,打开SPI 总线的设备文件。设备文件路径“/dev/spidev3.0”,如果打开失败首先要 查看路径是否正确以及设备文件是否存在。打开方式“O_RDWR”,我们要做SPI回环通信测试,所以要以读、写方式打开。

  • 第9-12行,设置SPI 工作模式。根据之前讲解,SPI根据相位和极性的不同分为四 种工作模式,在这里四种工作模式为SPI_MODE_x (x = 0、1、2、3)。这里是回环 测试所以设置为任意一种工作模式都可以。 需要注意的是我们可以分开设置SPI的读、写工作模式。

  • 第14-17行,设置SPI通信过程中一个字节所占的 位数。默认情况下设置为8即可。 同样,这里的读、写是分开设置的。

  • 第19-22行,设置SPI通信的波特率,这里设置为1M。

  • 经过以上四部分的初始化,SPI已经可以通信了。

13.4.4.2. spi发送函数

SPI借助SPI发送结构体spi_ioc_transfer 实现发送,程序中我们只需要将要发送的数据以及必要 的配置参数填入结构体,然后调用ioctl函数执行发送即可。发送函数如下所示:

SPI 发送函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
    int ret;

    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .delay_usecs = delay,
        .speed_hz = speed,
        .bits_per_word = bits,
        .tx_nbits = 1,
        .rx_nbits = 1
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
    
    if (ret < 1)
  • 函数共有四个参数,fd, 打开SPI设备文件时得到的SPI设备文件描述符, tx,要发送的数据地址,rx,如果是双向传输,rx 用于指定接收缓冲区的地址。 len, 指定本次传输的数据长度,单位为字节。

  • 函数实现非常简单,结合代码介绍如下:

  • 第5-14行,定义并初始化SPI传输结构体。SPI传输结构体的完整定义如下所示:

  • 结合注释很容易理解,简单说明如下:

    1. tx_buf**和 **rx_buf:分别为发送、接收缓冲区地址,数据类型为“__u64”,兼容64位系统,64位或32位由系统自动处理,我们不必关心。

    2. len:一次传输的数据长度。speed_hz,指定SPI通信的比特率。

    3. delay_usecs:如果不为零则用于设置两次传输之间的时间延迟。

    4. bits_per_word:指定字节长度,既一个字节占用多少比特。

    5. cs_change:取消选中,如果设置为真,则在下次传输之前会取消选中当前的SPI设备,更新片选。

    6. tx_nbits:指定“写”数据宽度,SPI 支持1、2、4位宽度,如果没有特殊数据要求的话,一般设置为1或0(设置为0表示使用默认的宽度既宽度为1)。

    7. pad :参数我们没有用到,不用设置。

  • 第16行,调用ioctl执行发送,参数fd,是SPI设备文件描述符,参数SPI_IOC_MESSAGE(1)用于指定执 行传输次数,我们这里只定义并初始化了一个传输结构体tr,所以传输次数为1。tr 是第一部分设置的传输结构体变量。

13.4.4.3. main函数

有了初始化函数和发送函数,SPI回环测试就非常简单了。我们只需要初始化SPI然后调用发送函数,最后打印传输结果如下所示:

SPI 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
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
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <linux/spi/spidev.h>

#define SPI_DEV_PATH "/dev/spidev3.0"

/*SPI 接收 、发送 缓冲区*/
unsigned char tx_buffer[100] = "hello the world !";
unsigned char rx_buffer[100];


int fd;                  					// SPI 控制引脚的设备文件描述符
static unsigned  mode = SPI_MODE_2;         //用于保存 SPI 工作模式
static uint8_t bits = 8;        			// 接收、发送数据位数
static uint32_t speed = 10000000; 			// 发送速度
static uint16_t delay;          			//保存延时时间

void transfer(int fd, uint8_t const *tx, uint8_t const *rx, size_t len)
{
    int ret;

    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = len,
        .delay_usecs = delay,
        .speed_hz = speed,
        .bits_per_word = bits,
        .tx_nbits = 1,
        .rx_nbits = 1
    };

    ret = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
    
    if (ret < 1)
        printf("can't send spi message\n");
}

void spi_init(void)
{
    int ret = 0;
    //打开 SPI 设备
    fd = open(SPI_DEV_PATH, O_RDWR);
    if (fd < 0)
        printf("can't open %s\n",SPI_DEV_PATH);

    //spi mode 设置SPI 工作模式
    ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
    if (ret == -1)
        printf("can't set spi mode\n");

    //bits per word  设置一个字节的位数
    ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    if (ret == -1)
        printf("can't set bits per word\n");

    //max speed hz  设置SPI 最高工作频率
    ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    if (ret == -1)
        printf("can't set max speed hz\n");

    //打印
    printf("spi mode: 0x%x\n", mode);
    printf("bits per word: %d\n", bits);
    printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);
}

int main(int argc, char *argv[])
{
    /*初始化SPI */
    spi_init();

    /*执行发送*/
    transfer(fd, tx_buffer, rx_buffer, sizeof(tx_buffer));

    /*打印 tx_buffer 和 rx_buffer*/
    printf("tx_buffer: \n %s\n ", tx_buffer);
    printf("rx_buffer: \n %s\n ", rx_buffer);
    
    close(fd);
    return 0;
}
  • 在main函数中依次调用函数spi_init初始化SPI、调用函数transfer执行发送。

  • 最后分别打印tx_buffer和rx_buffer的内容,正常情况下,程序运行后我们可以在控制终端发现tx_buffer和rx_buffer的内容一致。

13.5. SPI_OLED 显示实验

上一小节我们实现了SPI 回环通信,这一小节实现SPI驱动SPI_OLED显示屏。 本小节与上一章的IIC驱动IIC_OLED非常相似,仅仅是发送函数不同。

代码位置
1
2
#代码位置
base_linux/spi_oled
代码结构
1
2
3
4
5
6
7
8
.
|-- Makefile
|-- includes
|   `-- spi_oled_app.h
`-- sources
    |-- main.c
    |-- oled_code_table.c
    `-- spi_oled_app.c

13.5.1. 硬件说明:

OLED使用的是SPI接口0.96寸单色显示屏,详细资料可以在野火电子官网找到。实物如下所示:

未找到图片

SPI_OLED显示屏与板卡连接关系如下表所示

SPI_OLED显示屏

板卡引脚编号

MOSI

19(MOSI)

没有

21(MISO)

CLK

23(SCLK)

D/C

自己设定

CS

24(CS0)

GND

25(GND)

VCC

17(VCC)

13.5.1.1. 编译与运行

1
2
3
4
5
6
7
8
#在文件目录下
make
#在spi_oled文件目录下会生成test文件,软连接到生成的执行文件上
#直接执行软链接文件就可以了
sudo ./test /dev/spidev3.0 42

#/dev/spidev3.0 使用CS0驱动屏幕
#42为D/C引脚,需要根据自己的实际情况去修改

13.5.2. 代码分析

SPI_OLED显示实验由SPI回环测试程序修改得到。其中OLED显示相关代码参照IIC驱动OLED显示屏程序。

13.5.2.1. spi初始化函数

这里的SPI初始化函数与上一小节有两点不同。 第一,这里增加了一个GPIO设备,用于控制SPI_OLED的 D/C引脚, 第二,这里要设置SPI工作模式为模式二, 并且可以不设置读(SPI_OLED是只写的一个设备)。

spi_oled/sources/spi_oled_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
void spi_and_gpio_init(char *name)
{
  	int ret = 0;
  	gpio_init(name);
  	/*
	 * spi mode 设置SPI 工作模式
	 */
  	ret = ioctl(fd, SPI_IOC_WR_MODE32, &mode);
  	if (ret == -1)
  	  	pabort("can't set spi mode");

  	/*
	 * bits per word  设置一个字节的位数
	 */
  	ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
  	if (ret == -1)
    	pabort("can't set bits per word");

  	/*
	 * max speed hz  设置SPI 最高工作频率
	 */
  	ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
  	if (ret == -1)
    	pabort("can't set max speed hz");

		
   	printf("spi mode: 0x%x\n", mode);
    printf("bits per word: %d\n", bits);
    printf("max speed: %d Hz (%d KHz)\n", speed, speed / 1000);
}

13.5.2.2. SPI_OLED命令发送和数据发送函数

根据之前讲解,SPI_OLED的D/C引脚用于表示显示屏接收到的是命令还是数据, D/C为低电平,发送的是命令,D/C为高电平发送的是数据。 命令发送函数和数据发送函数仅仅是在执行发送之前设置D/C对应引脚的高、低电平。 如下所示。

spi_oled/sources/spi_oled_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
/*
* 向 SPI_OLED 发送控制命令
* cmd, 要发送的命令。 
*/
void spi_oled_send_commend(unsigned char cmd)
{
  	uint8_t tx = cmd;
  	uint8_t rx;

  	gpio_low(); //设置 SPI_OLED 的 D/C 为低电平
  	transfer(fd, &tx, &rx, 1);        //发送控制命令
}

/*
* 向 SPI_OLED 发送数据
* cmd, 要发送的数据。 
*/
void spi_oled_send_data(unsigned char dat)
{
  	uint8_t tx = dat;
  	uint8_t rx;

  	gpio_high(); //设置 SPI_OLED 的 D/C 为高电平
  	transfer(fd, &tx, &rx, 1);      //发送数据
}

13.5.2.3. SPI_OLED初始化代码

SPI_OLED初始化分为两部分,首先初始化SPI,然后通过SPI向OLED发送配置参数。 初始化代码如下所示:

spi_oled/sources/spi_oled_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
/*
* oled 初始化函数
*/
void oled_init(char *name)
{

  	spi_and_gpio_init(name);

  	spi_oled_send_commend(0xae);
  	spi_oled_send_commend(0xae); //--turn off oled panel
  	spi_oled_send_commend(0x00); //---set low column address
  	spi_oled_send_commend(0x10); //---set high column address
  	spi_oled_send_commend(0x40); //--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
  	spi_oled_send_commend(0x81); //--set contrast control register
  	spi_oled_send_commend(0xcf); // Set SEG Output Current Brightness
  	spi_oled_send_commend(0xa1); //--Set SEG/Column Mapping     0xa0,0xa1
  	spi_oled_send_commend(0xc8); //Set COM/Row Scan Direction   0xc0,0xc8
  	spi_oled_send_commend(0xa6); //--set normal display
  	spi_oled_send_commend(0xa8); //--set multiplex ratio(1 to 64)
  	spi_oled_send_commend(0x3f); //--1/64 duty
  	spi_oled_send_commend(0xd3); //-set display offset	Shift Mapping RAM Counter (0x00~0x3F)
  	spi_oled_send_commend(0x00); //-not offset
  	spi_oled_send_commend(0xd5); //--set display clock divide ratio/oscillator frequency
  	spi_oled_send_commend(0x80); //--set divide ratio, Set Clock as 100 Frames/Sec
  	spi_oled_send_commend(0xd9); //--set pre-charge period
  	spi_oled_send_commend(0xf1); //Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
  	spi_oled_send_commend(0xda); //--set com pins hardware configuration
  	spi_oled_send_commend(0x12);
  	spi_oled_send_commend(0xdb); //--set vcomh
  	spi_oled_send_commend(0x40); //Set VCOM Deselect Level
  	spi_oled_send_commend(0x20); //-Set Page Addressing Mode (0x00/0x01/0x02)
  	spi_oled_send_commend(0x02); //
  	spi_oled_send_commend(0x8d); //--set Charge Pump enable/disable
  	spi_oled_send_commend(0x14); //--set(0x10) disable
  	spi_oled_send_commend(0xa4); // Disable Entire Display On (0xa4/0xa5)
  	spi_oled_send_commend(0xa6); // Disable Inverse Display On (0xa6/a7)
  	spi_oled_send_commend(0xaf); //--turn on oled panel

  	OLED_Fill(0x00);
  	OLED_SetPos(0, 0);
}

结合代码,简单说明如下:

  • 第7行,调用spi_and_gpio_init初始化函数,根据之前讲解, 该函数初始化了SPI和一个GPIO。 初始化完成后我们就可以使用SPI与OLED通信同时也可以通过GPIO控制发送的是命令还是数据。

  • 第9-37行,发送OLED初始化参数。

  • 第39行,依次调用清屏函数(使屏幕不显示)和OLED光标设置函数(将光标设置到起始位置)。

  • 程序中OLED清屏函数、显示字符函数、显示汉字函数以及显示图片函数与IIC驱动IIC_OLED非常相似,只是函数的简单替换,这里不再赘述。

13.5.2.4. main 函数实现

main函数中调用OLED基本的显示函数测试OLED,完整代码如下所示:

spi_oled/sources/main.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
int main(int argc, char *argv[])
{
	int i = 0; //用于循环
	if(argc < 3){
    printf("Wrong use !!!!\n");
        printf("Usage: %s [dev] [D/C PinNum]\n",argv[0]);
        return -1;
    }
	printf("%s\n",argv[1]);
	/*打开 SPI 设备*/
    fd = open(argv[1], O_RDWR); // open file and enable read and  write
    if (fd < 0){
        printf("Can't open %s \n",argv[1]); // open i2c dev file fail
        exit(1);
    }

	oled_init(argv[2]);
	printf("oled_init\n");
	OLED_Fill(0xFF);
	while (1){
		OLED_Fill(0xff); //清屏
		sleep(1);
		OLED_Fill(0x00); //清屏
		OLED_ShowStr(0, 3, (unsigned char *)"Wildfire Tech", 1);  //测试6*8字符
		OLED_ShowStr(0, 4, (unsigned char *)"Hello wildfire", 2); //测试8*16字符
		sleep(1);
		OLED_Fill(0x00); //清屏

		for (i = 0; i < 4; i++)
			OLED_ShowCN(22 + i * 16, 0, i); //测试显示中文
	
		sleep(1);
		OLED_Fill(0x00); //清屏

		OLED_DrawBMP(0, 0, 128, 8, (unsigned char *)BMP1); //测试BMP位图显示
		sleep(1);
		OLED_Fill(0x00); //清屏
	}

	close(fd);
	return 0;
}

结合代码各部分简单说明如下:

第17-19行,初始化oled并全屏显示,我们使用的oled分辨率是128*64(64行,128列)。每个像素点只有亮、灭两种状态(0或1)。

第21行,清屏。清屏与全屏填充只是函数参数不同,清屏将每个像素点都设置为0,屏幕不亮,全屏填充将所有像素点设置为1,屏幕全亮。

第23-26行,设置显示字符串。字符串显示函数在IIC驱动OLED章节已经详细介绍,这里再次简单说明,函数前两个参数分别用于设置字符串起始位置的x、y 坐标,根据选择的字体不同(第四个参数)x,y 的取值范围也不同,以6*8字体为例,x可取0到(128-1-6),减1是因为从零开始计数,减6是因为一个字符宽度为6个像素点,一行剩余的像素点小于6则显示不全,y可取0到7,oled显示屏有64行,每8行像素点被分成一组,所以共有8组,字符的其实位置的y坐标只能取0到7。

第27-32行部分,显示中文。使用中文要有对应的点阵字库,字库制作工具以及制作方法请参考SPI_OLED模块资料,这里不再介绍。

第33-37行部分,显示图片。与显示中文相似,使用显示图片函数之前要将图片转化为点阵数据,制作工具以及制作方法请参考SPI_OLED模块资料。