SPI 相比其他协议算是比较简单的,本来是去年要总结的一篇文章,但由于偷懒只把部分参考链接放出来了,隔了很长时间,才下定决心将内容整理出来,之前写过 uart 还有其他一些内容,要完整的表述出来,还是相当占用篇幅的。

简述

SPI, Serial Perripheral Interface, 串行外围设备接口, 是 Motorola 公司推出的一种同步串行接口技术. SPI 总线在物理上是通过接在外围设备微控制器(PICmicro) 上面的微处理控制单元 (MCU) 上叫作同步串行端口(Synchronous Serial Port) 的模块(Module)来实现的, 它允许 MCU 以全双工的同步串行方式, 与各种外围设备进行高速数据通信.SPI最大的特点是由主设备时钟信号的出现与否来确定主/从设备间的通信。一旦检测到主设备的时钟信号,数据开始传输。SPI 是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,比如 AT91RM9200,stm32,efm32等等。

SPI 主要应用在 EEPROM, Flash, 实时时钟(RTC), 数模转换器(ADC), 数字信号处理器(DSP) 以及数字信号解码器之间. 它在芯片中只占用四根管脚 (Pin) 用来控制以及数据传输, 节约了芯片的 pin 数目, 同时为 PCB 在布局上节省了空间. 正是出于这种简单易用的特性, 现在越来越多的芯片上都集成了 SPI技术.

接口定义

SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)。

  • SDO/MOSI:主设备数据输出,从设备数据输入
  • SDI/MISO:主设备数据输入,从设备数据输出
  • SCLK:时钟信号,由主设备产生(除非需要指定频率如: uart ,其他通讯一些一般都会配上一个时钟总线)
  • CS:从设备使能信号,由主设备控制,主设备可以通过 CS 来选择究竟读写哪个从设备

CS: 其中CS是控制芯片是否被选中的,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),对此芯片的操作才有效,这就允许在同一总线上连接多个SPI设备成为可能。

SDI/SDO/SCLK: 通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCK时钟线存在的原因,由SCK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。这样,在至少8次时钟信号的改变(上沿和下沿为一次),就可以完成8位数据的传输。

特性

采用主-从模式(Master-Slave) 的控制方式

SPI 规定了两个 SPI 设备之间通信必须由主设备 (Master) 来控制次设备 (Slave). 一个 Master 设备可以通过提供 Clock 以及对 Slave 设备进行片选 (Slave Select) 来控制多个 Slave 设备, SPI 协议还规定 Slave 设备的 Clock 由 Master 设备通过 SCK 管脚提供给 Slave 设备, Slave 设备本身不能产生或控制 Clock, 没有 Clock 则 Slave 设备不能正常工作。

采用同步方式(Synchronous)传输数据

Master 设备会根据将要交换的数据来产生相应的时钟脉冲(Clock Pulse), 时钟脉冲组成了时钟信号(Clock Signal) , 时钟信号通过时钟极性 (CPOL) 和 时钟相位 (CPHA) 控制着两个 SPI 设备间何时数据交换以及何时对接收到的数据进行采样, 来保证数据在两个设备之间是同步传输的。

数据交换(Data Exchanges)

SPI 设备间的数据传输之所以又被称为数据交换, 是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 “发送者(Transmitter)” 或者 “接收者(Receiver)”. 在每个 Clock 周期内, SPI 设备都会发送并接收一个 bit 大小的数据, 相当于该设备有一个 bit 大小的数据被交换了。

一个 Slave 设备要想能够接收到 Master 发过来的控制信号, 必须在此之前能够被 Master 设备进行访问 (Access)。 所以, Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。

工作模式

四种时钟

spi四种模式SPI的相位(CPHA)和极性(CPOL)分别可以为0或1,对应的4种组合构成了SPI的4种模式(mode)

Mode 0 CPOL=0, CPHA=0
Mode 1 CPOL=0, CPHA=1
Mode 2 CPOL=1, CPHA=0
Mode 3 CPOL=1, CPHA=1

spi 没有直接规定是从上升沿还是下降沿采集,但规定了从第几个边沿开始采集和空闲时的电平状态。
时钟极性CPOL: 即 SPI 空闲时,时钟信号 SCLK 的电平(1:空闲时高电平; 0:空闲时低电平)
时钟相位CPHA: 即 SPI 在 SCLK 第几个边沿开始采样(0:第一个边沿开始; 1:第二个边沿开始)

具体使用哪个模式,并不固定,需要根据具体芯片手册开发 spi 的驱动,sd 卡的 spi 常用的是 mode 0 和 mode 3,这两种模式的相同的地方是都在时钟上升沿采样传输数据,区别这两种方式的简单方法就是看空闲时,时钟的电平状态,低电平为 mode 0 ,高电平为 mode 3。

主从

spi slave 是从模式,是被控制的一方,接收时钟、片选和写的数据;并发送回复的读数据;
spi master 是主模式,来控制从模式,发送时钟、片选和写的数据,并接受读的数据;
带有 SPI 功能的 MCU 往往需要选择主从模式。

大端小端

使用片上 SPI 资源或者模拟 SPI 时还需要注意一个字节的数据是高位优先传输还是低位优先传输,不同于 I2C,一般情况下 SPI 会采用 MSB (高位优先传输)。

波特率

模拟 SPI

SPI 并非芯片必须集成的功能,很多过去的芯片都没有集成该功能,但我们依然可以通过控制 IO 高低电平/输入输出,来模拟出芯片手册上面要求的信号,下面我以 FM25W256-G 为例做个简单说明。

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
/* 以下程序临时改写,未经过实验验证 */
/* SPI端口初始化 */
void spi_init()
{
set_gpio_direction(SS, OUTP);
set_gpio_direction(SCLK, OUTP);
set_gpio_direction(MOSI, OUTP);
set_gpio_direction(MISO, INP);
set_gpio_value(SCLK, 0); //CPOL=0
set_gpio_value(MOSI, 0);
}
/*
从设备使能 enable:为1时,使能信号有效,SS低电平为0时,使能信号无效,SS高电平
*/
void ss_enable(int enable)
{
if (enable)
set_gpio_value(SS, 0); //SS低电平,从设备使能有效
else
set_gpio_value(SS, 1); //SS高电平,从设备使能无效
}
/* SPI字节写 */
void spi_write_byte(unsigned char b)
{
int i;
for (i=7; i>=0; i--) {
set_gpio_value(SCLK, 0);
set_gpio_value(MOSI, b&(1<<i)); //从高位7到低位0进行串行写入
delay(); //延时
set_gpio_value(SCLK, 1); // CPHA=1,在时钟的第一个跳变沿采样
delay();
}
}
/* SPI字节读 */
unsigned char spi_read_byte(void)
{
int i;
unsigned char r = 0;
for (i=0; i<8; i++) {
set_gpio_value(SCLK, 0);
delay(); //延时
set_gpio_value(SCLK, 1); // CPHA=1,在时钟的第一个跳变沿采样
r = (r <<1) | get_gpio_value(MISO); //从高位7到低位0进行串行读出
delay();
}
}
/*
SPI写操作 buf:写缓冲区 len:写入字节的长度
*/
void spi_write (unsigned int add, unsigned char date)
{
spi_init(); //初始化GPIO接口
ss_enable(1); //从设备使能有效,通信开始
delay(); //延时
spi_write_byte(0x06); // 写使能指令
delay(); //延时
ss_enable(0);
delay(); //延时
ss_enable(1); //从设备使能有效,通信开始
delay(); //延时
spi_write_byte(0x02); // 写指令
spi_write_byte((add&0xff00)>>8); // 写高地址
spi_write_byte(add&0xff); // 写低地址
spi_write_byte(date); // 写数据
delay();
ss_enable(0); //从设备使能无效,通信结束
}
/*
SPI读操作 buf:读缓冲区 len:读入字节的长度
*/
unsigned char spi_read(unsigned char* buf, int len)
{
unsigned char res;
spi_init(); //初始化GPIO接口
ss_enable(1); //从设备使能有效,通信开始
delay(); //延时
spi_write_byte(0x03); // 写指令
spi_write_byte((add&0xff00)>>8); // 写高地址
spi_write_byte(add&0xff); // 写低地址
res = spi_read_byte(); // 写数据
delay();
ss_enable(0); //从设备使能无效,通信结束
}
int main(void)
{
unsigned int tmp = 0;
spi_init();
spi_write_byte(0xaa, 0x22);
tmp = spi_read_byte(0xaa);
}

通过上述代码我们不难发现,在使用模拟 SPI 时,可以方便我们移植,且不会增加特定寄存器的配置难度,但同时也以为着会增加代码量,且时钟要控制的比较精准才可以读取成功。

寄存器操作 SPI

使用寄存器配置 SPI,每个芯片厂家都不尽相同,会有自己的独家配置,这边为了文章的完整性,以 stm32 为例举个简答的例子。

注意点

相比 IIC 等其他协议而言,SPI 操作比较简单,但会占用更多的资源。同任何其他协议一样,在使用 SPI 进行通信时,也需要仔细阅读芯片手册,以下介绍一些可以会引起读写失败,容易忽视的问题:

时钟生效

在看芯片手册时,往往不同的 SPI 通讯设备,会存在不同的时钟周期,有效,如上面 FM25W256-G 就是在上升沿时有效,而少数芯片却可能会是下降沿有效,在使用 SPI 控制芯片时首先第一步就是该仔细看时钟周期,如果你在配置寄存器时,忽略了这点往往会读写失败。

大小端

同样芯片手册会详细指定是 MSB 还是 LSB 这点也要仔细阅读,不然容易造成数据错误。

指令操作顺序

操作指令的顺序很重要,FM25W256-G 这款芯片就仅指定了每次写时需要先使能,我们需要仔细阅读手册的时序图,查看使能时是否作为一个完整的执行周期。

小结

SPI 是计算机通讯中,最常见的几种协议之一,其具备:协议简单,所需资源不算太多,成本不高,通讯速度较快等特点,因此虽然经历了很多的发展,但仍然没有被淘汰,至今仍然活跃在工业领域的各种数字传感器中,因此能够根据芯片手册开发相关的 SPI 驱动对嵌入式程序员来说是相当重要的。


写作时间:20:30-22:30

To be continued…

参考链接:
http://wenku.baidu.com/view/2a0a7f9869dc5022abea001d.html
http://baike.baidu.com/item/SPI
https://my.oschina.net/freeblues/blog/67400
http://www.cnblogs.com/hello2mhb/p/3518974.html
http://www.cnblogs.com/aaronLinux/p/6219146.html
http://blog.csdn.net/special_lin/article/details/12835863
http://blog.csdn.net/zyboy2000/article/details/11861329
https://blog.csdn.net/yihui8/article/details/54316888
https://blog.csdn.net/lyj19960106/article/details/77718930