一文详解SPI串行外设接口

描述

SPI也是MCU最常见的对外通信口之一,由摩托罗拉在上世纪80年代中开发,用于嵌入式系统中器件之间的短距离数据通信,标准模式使用四条信号线。目前常见的应用器件有:LCD模组、以太网模块、SPI串行Flash和很多传感器等,大部分SD卡都具有SPI操作模式。

SPI的特点是主从结构、协议简单、成本低廉、串行传输等,具有同步时钟信号,传输速率可达几兆至十几兆(近来也有达到二、三十兆速率的器件),适合于中等数据量、点对点的传输环境。

1.1 SPI通信协议

SPI是点对点的全双工串行通信协议,用于两个设备间的通信。基本的连线方式是四条信号线,如下图:

接口

图1.SPI信号线连接示意图

四条信号线中有两条数据线,分别用于主机向从机发送数据(MOSI: Master OutSlave In),和从机向主机发送数据(MISO: MasterIn Slave Out)。

主机器件通过SCK时钟信号线向从机输出时钟,同时主机输出SSEL信号作为从机的片选。

1.1.1 SPI数据传输

SPI的数据传输流程十分简单,在每个SCK的时钟周期中有如下操作:

▲主机在MOSI向从机发送一个数据位;

▲从机在MOSI上接收一个数据位;

▲从机在MISO向主机发送一个数据位;

▲主机在MISO上接收一个数据位。

一般SPI的主机和从机都是各由一个移位寄存器实现数据的发送与接收,示意图如下:

接口

                       图2.SPI数据移位示意图

在同一个时钟信号的驱动下,主机和从机的移位寄存器同时向相同方向移位,经过n个时钟周期的n次移位,主机的数据与从机的数据正好进行了交换。

通常每个字符数据的长度n=8或n=16。LPC800允许每个数据长度可以是1~16中任意数值。

1.1.2 SPI的时钟信号

SPI的时钟信号SCK除了频率特性外,还需要考虑它的极性和相位,分别由CPOL和CPHA表示,具有以下意义:

▲时钟极性:

CPOL=0:空闲时,时钟信号为‘0’。时钟信号的前沿为上升沿,后沿为下降沿。

CPOL=1:空闲时,时钟信号为‘1’。时钟信号的前沿为下降沿,后沿为上升沿。

▲时钟相位:

CPHA=0:发送方在前一个时钟周期的后沿改变输出信号;接收方在时钟周期的前沿采样输入信号。

CPHA=1:发送方在时钟周期的前沿改变输出信号;接收方在时钟周期的后沿采样输入信号。

通常CPOL和CPHA的四种组合被定义为四种模式,如下表:

接口

接口

                     图3.SPI四种操作模式波形示意图

在模式0和模式2的SCK第一个时钟边沿之前,SPI主机利用内部时钟在MOSI上输出第一个数据位(最高位),SPI从机则使用前一个数据帧的最后一个边沿输出第一个数据位(最高位)。

1.1.3 SPI 设备的互连

SPI设备间的互连是主从关系,一个主设备可以连接多个从设备,主设备通过从设备片选信号区别与那个从设备进行通信。

接口

                            图4.SPI设备间互连示意图

一个主机设备能够连接的从机数量,由能够输出的片选信号(SSELn)的个数,和MOSI、SCK信号线的驱动能力限制。

1.2 LPC800的SPI特性

LPC800的SPI非常简单,但配置丰富并且很方便使用。

▲每个数据帧的长度可以直接配置为1~16位的任一种,通过软件操作还可以支持任意长度的数据帧。

▲支持主机模式或从机模式。

▲主机可以在发送数据时,不必理会从机返回的数据。这有助于优化软件的操作,例如对LCD模组刷屏时(现实中有不少LCD模组是不可读的),或写入SPI存储器时。

▲控制信息可以与发送的数据一起写入寄存器,这样可以实现各种灵活的操作要求。

▲最多有四个直接控制的从机片选信号,并且可以配置极性。

▲支持DMA操作。

▲可以灵活地控制每个数据帧中的各种时序。

1.3 SPI Flash读写例程 下面以几个例程示范使用SPI对SPI Flash的 读写操作。

这些例程都是在LPC824-Lite上,对板上的W25Q32BV的操作,下面先抄录这个存储器芯片的部分命令格式,方便例程的理解。以下例程会用到表中带阴影的命令。

接口

表1.W25Q32BV部分命令列表

接口

    图5.开发板上的SPI Flash线路图

1.3.1 SPI的轮询方式操作

以下所有的SPI例程都使用相同的初始化程序。

代码片段1. SPI主机初始化函数

接口

初始化函数非常简单直接,和所有其它模块的初始化过程一样,都是“开启时钟→映射引脚→复位模块→配置参数。

下图显示了所有SPI配置寄存器的配置位。

接口

                   图6.SPI配置寄存器(CFG)一览

代码片段1中只设置了CFG寄存器的第0、2位,其它位均为’0’。

LPC800的SPI发送数据/控制寄存器是非常有特色的。

接口

                图7.SPI发送数据和控制寄存器一览

上图是完整的发送数据控制寄存器的所有位,用户可以使用TXDATCTL寄存器,同时写入数据和所有的控制位,也可以使用TXCTL单独写入控制位,或使用TXDAT单独写入数据位。

对于输出至从机的SSEL片选信号,用户可以随时按照需要输出对应的电平,甚至可以按字符单独控制SSEL的电平。

如果需要使用超过16位的数据帧,则可以用EOF控制位实现。例如需要每个数据帧长度为24位,则可以输出两个12位的字符,在输出第一个12位的字符时,配置EOF=0,在输出第二个12位的字符时,配置EOF=1;也可以输出3个8位字符,并在输出第三个字符时,配置EOF=1实现。

由图2可以看出,SPI在发送数据的同时,也会接收对方的数据,但很多时候在发送的过程中,程序并不需要关心接收到什么数据,例如在向LCD屏输出数据时。在这种情况下,设置RXIGNORE=1可以让SPI模块不产生接收状态位,也不会因为没有读出接收的数据而产生接收溢出等错误。

下面通过代码,具体看看如何灵活使用这几个寄存器。

下面定义两个宏,用于发送与接收状态的判断:

接口

再定义两个宏,用于发送控制位的设置:

接口

在代码片段1的第08行把SSEL0映射到引脚P0_15,图5中看到P0_15是SPI Flash的片选信号。因此这两个宏中都设置了SSEL控制位为仅输出SSEL0为有效,其它的SSEL片选均为无效(不选中)。

OUT_CTL将用于发送命令和数据时,此时不需要关系输入的数据,所有设置RXIGNORE=1。

IN_DATA则用于需要接收数据时,向从机发送一串时钟脉冲,此时SPI Flash不理会接收的数据,可以发送任意数值。

结合上面的初始化代码和宏定义,下面的代码用于读入制造商代码和芯片的代码。

代码片段2.读取制造商ID和存储器容量的轮询函数

接口

在上述代码的第11、15行之前,并没有WaitForSPI0txRdy语句,这是因为前面已经接收到有效数据,这表示前一次的发送已经完成,不再需要等待发送寄存器就绪,可以直接发送。

下面的代码用于读出设备ID,流程中与上面不同的只是发送命令串时,分别操作TXDAT和TXCTL。

代码片段3.读取制造商ID和设备ID的轮询函数

接口

最后是非常简单的主函数,代码片段4.SPI轮询例程主函数

 

01  int main(void)
02  {
03      SPI0_init();      // SPI初始化
04      Read_JEDEC_ID();
05      Read_Device_ID();
06  
07      while (1) {
08      }
09  }

 

1.3.2 SPI的中断方式操作

下面是一种简单的中断方式操作的例程。

以读取制造商ID和存储器容量命令为例,这里我们通过TXDATCTL寄存器,每次都同时写入发送控制字和要发送的数据,把每次要写入TXDATCTL的内容事先保存在一个数组中,然后在中断处理程序中,逐个把数组中的数据输出到寄存器中。

数组定义如下:

  const uint32_t CMD_JedecID[] = {
       OUT_CTL | 0x9F,          // 读取制造商ID和存储器容量命令
       IN_DATA | 0xFF,          // 发送一个字符的脉冲,读回制造商ID MF7~MF0
      IN_DATA | 0xFF,          // 发送一个字符的脉冲,读回存储器ID
       IN_DATA | 0xFF, CTL_EOT  // 发送一个字符的脉冲,读回存储器容量
  };

再通过几个变量,控制整个操作流程。

  uint32_t Tx_Cnt;    // 用于控制当前数组发送的进度
  uint32_t Tx_Num;    // 用于记录上述数组的长度
  uint32_t *Tx_Buf;   //  一个指向发送数组的指针
  uint32_t Rx_Cnt;    // 用于控制当前接收数据的进度
  uint8_t Rx_Buf[10]; // 用于存放接收到的数据

中断处理程序和预备函数如下:

代码片段5. 中断模式读取制造商ID和存储器容量

接口

上述代码中的第06行,是为了防止由于无数据发送,不能清除STAT寄存器的发送就绪状态,而导致的频繁进入中断。

在第20、21行分别是两个循环语句,等待整个的发送与接收流程结束。数据送到发送寄存器后,发送并没有结束,硬件还在逐位移位,需要等到SSEL回复高电平时,才能确认信号线上的发送已经结束。注意这两个语句的顺序不能颠倒,否则在一次都没有进入中断处理程序前,由于SSEL还未变为有效(低电平),而使等待SSEL的语句失去作用。

在这两行等待语句之前,用户程序可以做些其它事情,有效地利用CPU的时间。

上述代码十分简单,也很有效,但不适合处理较长的数据块。下面使用另一种方法,用中断方式实现对SPIFlash存储单元的读写操作。

1.3.3 中断方式访问SPI Flash的完整例程

集中审视W25Q32BV的命令列表(见表1),可以归纳所有命令为以下四类:

1.仅发送命令(若有24位地址,也归于命令字,下同),例如“写使能”、“整片擦除”、“扇区擦除”等命令。

2.发送命令和发送数据缓冲区,例如“页编程”命令。

3.发送命令和接收数据串,例如“读制造商和设备ID”、“读数据”等命令。

4.发送命令和就收状态字,并等待某个指定状态。

“写状态寄存器”命令既可以是上面第1类,也可以是第2类。

下面的结构体将用于分别向四个不同的中断处理程序传递命令和数据缓冲区,结构体中的各个分量在不同的中断程序中有不同的意义。

接口

按照不同的命令类,分别使用四种中断处理程序,分别处理不同的流程,用结构体中IRQHandler指定使用哪个流程。

下面是四个不同的中断处理程序。

一. 仅发送命令流程

代码片段6. 仅发送命令缓冲区处理流程

接口

仅发送命令缓冲区处理流程中由于只有发送流程,在使用时只需要使能发送就绪中断,所以上述处理中不需要判断状态寄存器而区分是发送还是接收。

这个处理流程的使用方式,以“写使能”(Write Enable)命令介绍如下。

代码片段7. 使用仅发送命令缓冲区处理流程实现“写使能”命令

接口

上述函数,在所有变量、状态寄存器和中断寄存器配置完毕后,第10行使能中断后,所有产生的SPI0中断处理,将会在第02行被引导到预先设定的SPI0_Cmd_IRQHandler()处理程序。

第12行的作用和代码片段5的第20、21行作用相同,都是先等待发送命令缓冲区完成,然后再等待所有流程结束,即SSEL恢复到高电平。

二. 发送命令和数据缓冲区流程

代码片段8. 发送命令和发送数据缓冲区处理流程

接口

该处理流程的前半段很简单,逐个发送命令缓冲区的数据,直到所有数据发送完毕。

发送数据缓冲区的流程,是和仅发送命令缓冲区处理流程一样的,所以代码片段8的后半段把数据缓冲区的参数,拷贝至命令缓冲区的控制变量中,然后转入前面的仅发送命令缓冲区处理流程,完成发送数据缓冲区。

“页编程”命令就是典型的发送命令和发送数据缓冲区处理流程,传输结构体的设置如下。

代码片段9.使用发送命令和发送数据缓冲区处理流程实现“页编程”命令

接口

使用要编程的页地址、数据缓冲区指针和数据长度调用这个函数,函数中逐项配置好传输结构体各个变量,然后通过代码片段7的Execution()函数配置好相应寄存器,并等待传输流程结束。

三. 发送命令和接收数据串流程

代码片段10. 发送命令和接收数据串处理流程

接口

发送命令和接收数据串处理流程,比前面两个流程增加了数据接收部分。在处理发送就绪(TXRDY)中断部分又分为两个阶段,第一个阶段是发送命令缓冲区。第二个阶段是发送任意字符(此处为0xFF),用于产生从机输出数据的时钟信号;当只剩一个要接收的数据时,发送最后一个任意字符的同时,失能发送就绪中断,此后将不再产生这个中断,达到控制接收数据数目的目的。

在处理接收就绪(RXRDY)中断中,直接读出接收数据并保存到数据缓冲区。

“读数据”、“读芯片唯一ID”等命令都执行该流程,读出的数据存放再Data_Buf指示的缓冲区。

代码片段11. 使用发送命令和接收数据串处理流程实现“读数据”

接口

Page_Read()函数的参数与前面的Page_Program()基本相同:读出的数据区地址、数据缓冲区指针和数据长度三个参数。

四. 发送命令和就收状态字流程

该流程要求先发送命令字,然后读出状态字,检测状态字的指定位,如果所关心的状态位不是希望的数值,则重复读出状态字-检测状态字的过程,直到所关心的状态位达到期望的数值。

代码片段12. 发送命令和就收状态字处理流程

接口

发送命令和就收状态字处理流程与前面的发送命令和接收数据串处理流程一样,在处理发送就绪(TXRDY)中断部分也分为两个阶段,第一个阶段是发送命令缓冲区。第二个阶段是发送任意字符(此处为0xFF),用于产生从机输出状态字的时钟信号;当接收到的状态字与要求的数值匹配时,则通过再发送一个任意字符的方式,使SSEL变为无效(高电平)。

在处理接收就绪(RXRDY)中断中,直接读出接收数据并作为状态字保存,留待第11行与预定的匹配数值进行检测。

“页编程”和几个擦除命令之后都需要使用发送命令和就收状态字处理流程,发送“读状态寄存器1”然后反复检测“忙”标志位,直到编程或擦除命令完成,“忙”标志位变为’0’。

代码片段13.使用发送命令和就收状态字处理流程等待“忙”标志位

接口

Wait_Status1()函数有两个参数。mask一个屏蔽字,在需要检测的状态字的对应位为’1’,不需要检测的对应位为’0’;value是需要检测的状态字的期望值,读出的状态字和mask屏蔽字进行“与”运算后的结果,需要和value相同。

例如等待“忙”标志位变为’0’的调用方式是:

接口

五. 测试SPI Flash的主函数

介绍完所有的中断处理流程,最后这个主函数就非常简单了:

代码片段14.测试SPI Flash主函数

接口

运行这个程序后,读者可以检查Buffer缓冲区的前后半段,数据应该相同,表示写入和读出成功。

1.4 主机产生信号的时序控制 首先澄清一个概念——字符与数据帧。字符是指SPI发送或接收时,TXDAT、TXDATCTL或RXDAT寄存器中一次所容纳的数据位。一个数据帧可以是直接对应一个字符,也可以是多个连续的字符组合。LPC800 的SPI模块,可以在数据帧之间引入帧延迟,但不能在字符之间插入延迟,除非数据帧与字符长度相同。

接口

                    图8.字符与数据帧之间的关系

上图中的上面一行,显示了当一个数据帧的长度小于16位时,一个字符就是一个数据帧。图的下面一行显示了,当一个数据帧的长度大于16位时,一个数据帧中包含了若干个字符。

发送每个数据帧的最后一个字符时,设置发送控制寄存器的EOF位(见图7),表示帧结束。使用EOF控制位,即可支持任意长度的数据帧。

SPI模块中有一个延迟寄存器,可以为用户提供多种时序延迟控制。延迟寄存器的各个控制位如下图。

接口

                图9.SPI延迟寄存器(DLY)控制位

1.4.1 片选信号与数据帧信号之间的间隔控制

PRE_DELAY和POST_DELAY域各有4位,可以分别配置为插入0~15个时钟周期的延迟。

PRE_DELAY表示在SSEL变为有效后至开始传输数据之间,需要插入的延迟时间。

POST_DELAY表示在数据传输结束至SSEL变为无效之间,需要插入的延迟时间。

下面两个图显示了PRE_DELAY和POST_DELAY的作用位置。

接口

    图10.PRE_DELAY和POST_DELAY的作用位置示意图(CPHA=0)

接口

    图11.PRE_DELAY和POST_DELAY的作用位置示意图(CPHA=1)

图中显示在SSEL变低之后插入了2个时钟周期的延迟(PRE_DELAY=2);在最后一位(LSB)发送完毕后插入了1个时钟周期的延迟(POST_DELAY=1)。

1.4.2 数据帧之间的间隔控制

FRAME_DELAY有4位,当发送的字符控制位EOT=1时,在字符的最后一位后插入0~15个时钟周期的延迟。这个延迟有利于某些器件在接收到一个数据帧后,有足够的时间进入下一帧的接收。

FRAME_DELAY的作用位置如下图

接口

            图12.FRAME_DELAY作用位置示意图

1.4.3 两次传输之间的间隔控制

TRANSFER _DELAY有4位,当发送的字符控制位EOF=1时,在SSEL变为无效(高电平)之后,需要至少有1~16个时钟周期的延迟才能开始下一次传输,即SSEL至少要保持一个时钟周期的高电平,才能再次变为低电平。这个延迟有利于某些器件在接收到一次传输的最后一个数据帧后,有足够的时间进入下一次传输的接收。

TRANSFER _DELAY只是给出了SSEL保持无效(高电平)的长度低限,这个时间有可能因为软件的原因变得更长,例如软件需要时间准备下一个传输的数据。

TRANSFER _DELAY的作用位置如下图

接口

            图13.TRANSFER_DELAY的作用位置示意图

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分