本节我们将继续学习下一个通信协议 SPI,SPI 通信和我们刚学完的 I2C 通信差不多。两个协议的设计目的都一样,都是实现主控芯片和各种外挂芯片之间的数据交流,有了数据交流的能力,我们主控芯片就可以挂载并操纵各式各样的外部芯片,来实现一个功能更加强大的控制系统。那本节 SPI 通信的安排和上一节 I2C 的也是一样,我们先学习 SPI 协议的软硬件规定,先用软件模拟的 SPI,实现读写 W25Q64 Flash 存储器;之后,我们再学习 STM32 中的 SPI 外设,再用硬件 SPI 实现同样的功能。
W25Q64 是一个 Flash 存储器芯片,它内部可以存储 8 M 字节的数据,并且是掉电不丢失的。如果你之后的项目中,需要存储大量的数据,就可以考虑一下外挂这个芯片来实现。
那在这里,我们用 4 根 SPI 通信线把 W25Q64 和 STM32 连接在一起,STM32 操作引脚电平,实现 SPI 通信的时序,进而实现读写存储器芯片的目的。
那在 OLED 上,我们可以看到程序测试的现象:第一行,显示的是 ID 号,MID 是厂商 ID,读出来是 0xEF,DID 是设备 ID,读出来是 0x4017,这些 ID 号都是固定的数值,在手册里有写,我们用 SPI 读写 ID 号,就可以进行最简单的测试了,如果读取 ID 号和手册里 一样,说明 SPI 通信基本没问题。之后,既然是存储器芯片,我们肯定就是写几个数据,再读出来,看看对不对了,这里第二行,W,写的内容是 4 个字节:0x01,02,03,04。然后第三行,R,就是读到的内容了,显示出来,可以看到也是 0x01,02,03,04,读出来和写入的一样,这说明读写存储器芯片没问题。当然更进一步的测试,比如读写更多的数据、写入的数据是不是掉电不丢失,这些我们之后写程序的时候再来验证。程序现象就看到这里。
1. SPI 通信简介
1.1 SPI 的基本功能
SPI(Serial Peripheral Interface,串行外设接口)是由Motorola公司开发的一种通用数据总线
和 I2C 一样,它们都是通用的数据总线。同时,它们也都是用于主控和外挂芯片之间的通信,应用领域非常相似。当然,I2C 和 SPI,两者是各有优势和劣势的。在某些芯片呢,我们用 I2C 更好,在另一些芯片呢,我们用 SPI 更好。
上一节我们学习 I2C 的时候,可以发现 I2C,无论是硬件电路,还是软件时序,设计的都是相对比较复杂的。硬件上,我们要配置为开漏外加上拉的模式,软件上,我们有很多功能和要求,比如,一根通信线兼顾数据收发,应答位的收发、寻址机制的设计等等,最终,通过这么多的设计,就使得 I2C 通信的性价比非常高,I2C 可以在消耗最低硬件资源的情况下,实现最多的功能。在硬件上,无论挂载多少个设备,都只需要两根通信线,在软件上,数据双向通信、应答位,都可以实现。如果把通信协议比作是一个人的话,那 I2C 就属于精打细算、思维灵活这类型的人,既要实现硬件上最少的通信线,又要实现软件上最多的功能。最终,I2C 通过精心的设计,也确实实现了这么多功能,可以说是非常的优雅。当然,在这些优雅之中,也隐藏了一个缺点,就是我们上节说的,由于 I2C 开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱,这就会导致,通信线由低电平变到高电平的时候,这个上升沿耗时比较长,这会限制 I2C 的最大通信速度,所以,I2C 的标准模式,只有 100 KHz 的时钟频率,I2C 的快速模式,也只有 400 KHz,虽然 I2C 协议之后又通过改进电路的方式,设计出了高速模式,可以达到 3.4 MHz,但是高速模式目前普及程度不是很高。所以一般情况下,我们认为 I2C 的时钟速度最多就是 400 KHz,这个速度,相比较 SPI 而言,还是慢了很多,那了解完 I2C 的优势和缺点,我们就来看一下 SPI。
在学习之前,简单概括几点 SPI 相对于 I2C 的优缺点:
首先,SPI 传输更快,SPI 协议并没有严格规定最大传输速度,这个最大传输速度取决于芯片厂商的设计需求,比如说,我们这个 W25Q64 存储器芯片,手册里写的 SPI 时钟频率,最大可达 80 MHz,这比 STM32F1 的主频还要高。
其次,SPI 的设计比较简单粗暴,实现的功能没有 I2C 那么多,所以学习起来,SPI 还是比 I2C 简单很多的。
最后,SPI 的硬件开销比较大,通信线的个数比较多,并且通信过程中,经常会有资源浪费的现象,如果继续把通信协议比作一个人的话,那 SPI 就属于富家子弟、有钱任性这类型的人,SPI 说,我不在乎我花了多少钱,我只在乎我的任务有没有最简单、最快速的完成,这就是 SPI 的风格。
好,经过这么多的对比和铺垫,大家对 SPI 应该就有了一个第一印象了吧。
四根通信线:SCK(Serial Clock,串行时钟线)、MOSI(Master Output Slave Input,主机输出从机输入)、MISO(Master Input Slave Output,主机输入从机输出)、SS(Slave Select,从机选择)
这是 SPI 通信典型的引脚名称。当然在实际情况下,这些名称可能会有别的表述方式。比如,SCK,有的地方可能叫作 SCLK、CLK、CK;MOSI 和 MISO,有的地方可能直接叫作 DO(Data Output)和 DI(Data Input);SS,有的地方也可能叫作 NSS(Not Slave Select)、CS(Chip Select),这些不同的名称都是一个意思,大家了解一下。那这里,就以 SPI 官方文档的名称为准,统一都用这几个名词来表示。
那这四个引脚的意义和作用是什么呢?我们继续往后看
SPI 基本特性是:同步,全双工
首先既然是同步时序,肯定就得有时钟线了,所以 SCK 引脚,就是用来提供时钟信号的,数据位的输出和输入,都是在 SCK 的上升沿或下降沿进行的,这样,数据位的收发时刻就可以明确的确定。并且,同步时序,时钟快点慢点,或者中途暂停一会儿,都是没问题的,这就是同步时序的好处。那对照 I2C 总线,这个 SCK,就相当于 I2C 的 SCL,两者作用相同。
之后,SPI 是全双工的协议。全双工:就是数据发送和数据接收单独各占一条线,发送用发送的线路,接收用接收的线路,两者互不影响。所以这里,MOSI 和 MISO,就是分别用于发送和接收的两条线路,MOSI 线,是主机输出从机输入,如果是主机接在这条线上,那就是 MO,主机输出;如果是从机接在这条线上,那就是 SI,从机输入,意思就是一条通信线,如果主机接在上面配置为输出,那从机肯定得配置为输入,才能接收主机的数据对吧。主机和从机不能同时配置为输出或输入,要不然就没法通信了。所以这条 MOSI,就是主机向从机发送数据的线路,那同理,下面这条 MISO,就是主机从从机接收数据的线路,这就是全双工通信的两根通信线,那这两根通信线,加在一起,就相当于 I2C 总线的 SDA,当然 I2C 是一根线兼具发送和接收,是半双工。这里 SPI 是一根发送、一根接收,是全双工。全双工的好处就是简单高效,输出线就一直输出,输入线就一直输入,数据流的方向不会改变,也不用担心发送和接收没协调好冲突了,但是坏处就是多了一根线,会有通信资源的浪费,这就是全双工。
支持总线挂载多设备(使用的是 一主多从 的模型)
SPI 仅支持一主多从,不支持多主机,这一点,SPI 从功能上,没有 I2C 强大。那 I2C,实现一主多从的方式是在起始条件之后,主机必须先发送一个字节进行寻址,用来指定我要跟哪个从机进行通信,所以 I2C 这里,要涉及分配地址和寻址的问题。但是 SPI 表示,你这太麻烦了,我直接大手一挥,再开辟一条通信线,专门用来指定我要跟哪个从机进行通信,所以,这条专门用来指定从机的通信线,就是这里的 SS,从机选择线。并且,这个 SS 可能不止一条,SPI 的主机表示,我有几个从机,我就开几条 SS,所有从机,一人一根,都别抢,我需要找你的时候,我就控制接到你那一根的 SS 线,给你低电平,就说明我要找你了;给你高电平,就说明我不跟你玩了,那这样一来,指定从机不就是动动手指就能完成的事了么,哪还需要什么分配地址,先发一个字节寻址的操作啊。那这就是 SPI 实现一主多从,指定从机的方式,好处就是方便,坏处就是得加钱(线)。
那最后这里,SPI,没有写应答机制的介绍,SPI 没有应答机制的设计。发送数据就发送,接收数据就接收,至于对面是不是存在,SPI 是不管的。
然后看一下下面的图片,这些都是采用了 SPI 通信的芯片和模块。
第一个图,就是我们本节使用的芯片,型号是 W25Q64,是一个 Flash 存储器,这个模块的引脚,可以看到和刚才说的并不一样。这里 CLK 就是 SCK,DI 和 DO 就是 MOSI 和 MISO。那 DI 到底是 MOSI 还是 MISO 呢,我们要看一下这个芯片的身份,显然,这个芯片接在 STM32 上,应该是从机的身份,所以这里的 DI,数据输入,就是从机的数据输入 SI,对应需要接在主机的 MO 上,所以这里的 DI 就是 MOSI,那另一个 DO,就是 MISO 了,一般在这种始终作为从机的设备上,可能会用 DI 和 DO 的简写。像 STM32 这种可以进行身份转换的设备,一般都会把 MOSI、MISO 的全程写完整,当然,即使它简写了,只要明确了它的身份,是主机还是从机,之后再辨别这两个引脚,应该就好判断了。那最后一个 CS 片选,其实就是 SS 从机选择了。
然后继续下一个模块,这个是利用 SPI 通信的 OLED 屏幕,上面的引脚也不是标准的名称,所以这个模块要查一下手册,在手册里有写的。
之后下一个,这个是一个 2.4 G 无线通信模块,芯片型号是 NRF24L01,这个芯片使用的就是 SPI 通信协议,要想使用这个芯片来进行无线通信,那就需要利用 SPI,来读写这个芯片。
然后最后一个图片,就是常见的 Micro SD 卡了,这个 SD 卡,官方的通信协议是 SDIO,但是它也是支持 SPI 协议的,我们可以利用 SPI,,对这个 SD 卡进行读写操作。
那到这里,我们这个 SPI 通信的大体介绍,就完成了。
接下来我们来看一下 SPI 的硬件和软件规定。
1.2 SPI 硬件规定
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
首先是硬件电路
这个图,就是 SPI 一个典型的应用电路。我们看一下
- 左边这里,是 SPI 主机,主导整个 SPI 总线。主机,一般都是控制器来作,比如 STM32,下面这里,SPI 从机 1、2、3,就是挂载在主机上的从设备了,比如存储器、显示屏、通信模块、传感器等等等等。
- 左边 SPI 主机实际上引出了 6 根通信线,因为有 3 个从机,所以 SS 线需要 3 根,再加 SCK、MOSI、MISO,就是 6 根通信线,当然 SPI 所有通信线都是单端信号,它们的高低电平都是相对 GND 的电压差。所以,单端信号,所有的设备还需要供电,这里 GND 的线没画出来,但是是必须要接的。然后如果从机没有独立供电的话,主机还需要再额外引出电源正极 VCC,给从机供电,这两根电源线 VCC 和 GND,也要注意接好。
- 然后我们看一下这几根通信线,首先,SCK,时钟线,时钟线完全由主机掌控,所以对于主机来说,时钟线为输出,对于所有从机来说,时钟线都为输入,这样主机的同步时钟,就能送到各个从机了。然后下一个,MOSI,主机输出从机输入,这里左边是主机,所以就对应 MO,主机输出,下面三个都是从机,所以就对应 SI,从机输入,数据传输方向是,主机通过 MOSI 输出,所有从机通过 MOSI 输入。接着下一个,MISO,主机输入从机输出,左边是主机,对应 MI,下面三个都是从机,对应 SO,数据传输方向是,三个从机通过 MISO 输出,主机通过 MISO 输出。
那到这里,SCK、MOSI、MISO 的连接方式我们就清楚了。这就是上面写的第一条,所有SPI设备的SCK、MOSI、MISO分别连在一起,就是上面图示的这样,每条线的数据传输方向,图中都用箭头标出来了,可以看一下,应该都挺明确的。
之后我们继续看,时钟和数据传输没问题了。最后要解决的就是从机的选择问题了,为了确定通信的目标,主机就要另外引出多条 SS 通信线,分别接到各从机的 SS 引脚,上面图中有 3 个从机,我们需要在主机另外引出 3 根 SS 选择线,分别接到每个从机的 SS 输入端。主机的 SS 线都是输出,从机的 SS 线都是输入,SS 线是低电平有效的,主机想指定谁,就把对应的 SS 输出线置低电平就行了。比如,主机初始化之后,所有的 SS 都输出高电平,这样就是谁也不指定。当主机需要和,比如从机 1,进行通信了,主机就把 SS1 线输出低电平,这样从机 1 就知道,主机在找我,然后主机在数据引脚进行的传输,就只有从机 1 会响应,其他从机的 SS 线是高电平,所以它们都会保持沉默。当主机和从机 1 通信完成之后,就会把 SS1 置回高电平,这样从机 1 就知道,主机结束了和我的通信。之后主机需要和从机 2 和从机 3 通信时,也是同理,需要找谁通信,就置谁的 SS 为低电平。当然同一时间,主机只能置一个 SS 为低电平,只能选中一个从机,否则,如果主机同时选中多个从机,就会导致数据冲突,这就是 SPI 实现选择从机的方式。不需要像 I2C 一样进行寻址,是不是挺简单的。
然后我们继续看下一条,输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入,这就是 SPI 引脚的配置。在上图里,输出引脚和输入引脚都用箭头标出来了,哪个是输出哪个是输入,应该很好判断。对于输出,我们配置推挽输出,推挽输出,高低电平具有很强的驱动能力,这将使得 SPI 引脚信号的下降沿,非常迅速,上升沿,也非常迅速,不像 I2C 那样,下降沿非常迅速,但是上升沿,就比较缓慢了。那得益于推挽输出的驱动能力,SPI 信号变化的快,那自然它就能达到更高的传输速度,一般 SPI 信号都能轻松达到 MHz 的速度级别。然后这里 I2C 并不是不想使用更快的推挽输出,而是 I2C 要实现半双工,经常要切换输入输出,另外 I2C 又要实现多主机的时钟同步和总线仲裁,这些功能,都不允许 I2C 使用推挽输出,要不然一不小心,就电源短路了,所以 I2C 选择了更多的功能,自然就要放弃更强的性能了。对于 SPI 来说,首先 SPI 不支持多主机,然后 SPI 又是全双工,SPI 的输出引脚始终是输出,输入引脚始终是输入,基本不会出现冲突,所以 SPI 可以大胆地使用推挽输出。不过当然,SPI 其实还是有一个冲突点的,就是图上的 MISO 引脚,在这个引脚上,可以看到主机一个是输入,但是三个从机全都是输出,如果三个从机都始终是推挽输出,势必会导致冲突。所以在 SPI 协议里,有一条规定,就是当从机的 SS 引脚为高电平,也就是从机未被选中时,它的 MISO 引脚,必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平,这样就可以防止,一条线有多个输出,而导致的电平冲突的问题了。在 SS 为低电平时,MISO 才允许变为推挽输出,这就是 SPI 对这个可能的冲突做出的规定,当然这个切换过程都是在从机里,我们一般都是写主机的程序,所以我们主机的程序中,并不需要关注这个问题。
好,那有关 SPI 的硬件电路,就介绍到这里。
接下来,我们来看一下移位示意图
这个移位示意图是 SPI 硬件电路设计的核心。只要你把这个移位示意图搞懂了,那无论是上面的硬件电路,还是我们等会学习的软件时序,理解起来都会更加轻松。我们看一下:
- SPI 的基本收发电路,就是使用了这样一个移位的模型,左边是 SPI 主机,里面有一个 8 位的移位寄存器;右边是 SPI 从机,里面也有一个 8 位的移位寄存器。这里移位寄存器有一个时钟输入端,因为 SPI 一般都是高位先行的,所以,每来一个时钟,移位寄存器都会向左进行移位,从机中的移位寄存器也是同理,然后呢,移位寄存器的时钟源,是由主机提供的,这里叫作波特率发生器,它产生的时钟驱动主机的移位寄存器进行移位,同时,这个时钟也通过 SCK 引脚进行输出,接到从机的移位寄存器里。之后,上面移位寄存器的接法是,主机移位寄存器左边移出去的数据,通过 MOSI 引脚,输入到从机移位寄存器的右边;从机移位寄存器左边移出去的数据,通过 MISO 引脚,输入到主机移位寄存器的右边,这样组成一个圈。
- 接下来,我来演示一下这个电路如何工作。首先,我们规定波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放到引脚上,波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位。接下来,假设主机有个数据 1010101 要发送给从机,同时,从机有个数据 01010101 要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时所有的位,就会往左移动一次,那从最高位移出去的数据,就会放到通信线上,数据放到通信线上,实际上是放到了输出数据寄存器。可以看到,此时 MOSI 数据是 1,所以 MOSI 的电平就是高电平,MISO 的数据是 0,所以 MISO 的电平就是低电平,这就是第一个时钟上升沿执行的结果,就是把主机和从机中,移位寄存器的最高位,分别放到 MOSI 和 MISO 的通信线上,这就是数据的输出。之后,时钟继续运行,上升沿之后,下一个边沿就是下降沿,在下降沿时,主机和从机内,都会进行数据采样输入,也就是,MOSI 的 1,会采样输入到从机这里的最低位;MISO 的 0,会采样输入到主机这里的最低位,这就是第一个时钟结束后的现象。那时钟继续运行,下一个上升沿,同样的操作,移位输出,主机现在的最高位,也就是原始数据的次高位,输出到 MOSI,从机现在的最高位,输出到 MISO;随后,下降沿,数据采样输入,MISO 数据到从机这里的最低位,MOSI 数据到主机这里的最低位。之后时钟继续运行,第三个时钟开始,上升沿,移位,主机输出,从机输出;下降沿,采样,主机输入,从机输入。之后,第 4 个时钟,第 5 个时钟,等等,一直到第 8 个时钟,都是同样的过程。最终 8 个时钟之后,原来主机里的 10101010 跑到从机里了,原来从机里的 01010101 跑到主机里了,这就实现了,主机和从机一个字节的数据交换,实际上,SPI 的运行过程就是这样。SPI 的数据收发,都是基于字节交换,这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这样,主机要发送的数据,跑到从机,主机要从从机接收的数据,跑到主机,这就完成了发送同时接收的目的。
那你可能会问,如果我只想发送,不想接收,怎么办呢?其实很简单,我们仍然调用交换字节的时序,发送,同时接收,只是,这个接收到的数据,我们不看它就行了。那如果我只想接收,不想发送,怎么办呢?同理,我们还是调用交换字节的时序,发送,同时接收,只是,我们会随便发送一个数据,只要能把从机的数据置换过来就行了,我们读取置换过来的数据,不就是接收了嘛。这里我们随便发过去的数据,从机也不会去看它,当然这个随便的数据,我们不会真的随便发,一般在接收的时候,我们会统一发送 0x00 或 0xFF,去跟从机换数据。
好,以上就是 SPI 的基本原理。总结一下就是,SPI 通信的基础是交换一个字节,有了交换一个字节,就可以实现,发送一个字节、接收一个字节和发送同时接收一个字节,这三种功能。可以看出,SPI 在只执行发送或只执行接收的时候,会存在一些资源浪费现象。不过全双工的通信,本来就会有浪费的情况发生,SPI 表示:我不在乎。
那了解完这个移位示意图,再看一下硬件电路,是不是这三个引脚的功能,就很容易理解了。另外再加几根 SS 从机选择线,这就是 SPI 通信。
那硬件部分看完,我们来看一下 SPI 的时序。只要你把移位示意图理解了,SPI 时序其实也很简单。
1.3 SPI 软件规定
1.3.1 起始条件与结束条件
首先是 SPI 的起始和终止。其中:
起始条件:SS从高电平切换到低电平
也就是左边这个图,SS 是低电平有效的。那 SS 从高变到低,是不是就代表刚刚选中了某个从机了。这就是通信的开始。
终止条件:SS从低电平切换到高电平
也就是右边这个图,SS 从低电平变到高电平,就是结束了从机的选中状态。就是通信的结束。
那在从机的整个选中状态中,SS 要始终保持为低电平。就是 SS 低电平选中,高电平未选中,那低电平期间就代表正在通信,下降沿是通信的开始,上升沿是通信的结束,这个相比较 I2C,还是简单很多的,这就是起始条件和终止条件。
1.3.2 交换一个字节
接下就是数据传输的基本单元了。这个基本单元,就是建立在我们刚才说的移位模型上的,并且这个基本单元,什么时候开始移位?是上升沿移位还是下降沿移位?SPI 并没有限定死,给了我们可以配置的选择,这样的话,SPI 就可以兼容更多的芯片。
那在这里,SPI 有两个可以配置的位,分别叫做 CPOL(Clock Polarity)时钟极性和 CPHA(Clock Phase)时钟相位,每一位可以配置为 1 或 0。总共组合起来,就有模式 0、模式 1、模式 2、模式3 这 4 种模式。当然模式虽然多,但是它们的功能都是一样的,在实际使用的时候,我们主要学习其中一种就可以了剩下的模式,你知道有这些东西可以配置,如果到时候真的需要用,再过来了解一下就行了.
那我们先看一下模式 1,因为这个模式和我们刚才讲的移位模型是对应的,我们看一下。
这个时序的基本功能是交换一个字节,也就是移位示意图中我们展示的现象。
CPOL=0:空闲状态时,SCK为低电平
下面图中可以看到,在 SS 未被选中时,SCK 默认是低电平的。
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
当然这句话也有不同的描述方式,有的地方写的是,CPHA = 1 表示 SCK 的第二个边沿进行数据采样,或者是 SCK 的偶数边沿进行数据采样,这些不同的描述意思都是一样。这里为了照应刚才的移位模型,就写的是SCK第一个边沿移出数据,第二个边沿移入数据。
看一下上面的时序图,第一个 SS,从机选择,在通信开始前,SS 为高电平在通信过程中,SS 始终保持低电平,通信结束,SS 恢复高电平,这是 SS 信号。