十二、SPI通信

十、SPI通信

原文链接:https://blog.csdn.net/ma123456542/article/details/134925331

SPI通信协议

SPI通信协议简介

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线

  • 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)

    SCK(SCLK、CLK、CK):时钟线

    MOSI(DO):主机输出从机输入

    MISO(DI):主机输入从机输出

    SS(NSS、CS):从机选择线。每个从机都有一根SS通信线与主机连接,例如有三个从机,主机就要接6条通信线

  • 同步,全双工

  • 支持总线挂载多设备(一主多从)

image

硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

image

左边 SPI 主机实际上引出了 6 根通信线,因为有 3 个从机,所以 SS 线需要 3 根,再加 SCK、MOSI、MISO,就是 6 根通信线

SPI 所有通信线都是单端信号,它们的高低电平都是相对 GND 的电压差。所以所有的设备还需要供地,这里 GND 的线没画出来,但是是必须要接的。

然后如果从机没有独立供电的话,主机还需要再额外引出电源正极 VCC,给从机供电,这两根电源线 VCC 和 GND,也要注意接好

选择从机通信:

主机的 SS 线都是输出,从机的 SS 线都是输入,SS 线是低电平有效的,主机想指定谁,就把对应的 SS 输出线置低电平就行了

同一时间,主机只能置一个 SS 为低电平,只能选中一个从机,否则,如果主机同时选中多个从机,就会导致数据冲突

当从机的 SS 引脚为高电平,也就是从机未被选中时,它的 MISO 引脚,必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平,这样就可以防止,一条线有多个输出,而导致的电平冲突的问题了。在 SS 为低电平时,MISO 才允许变为推挽输出,这就是 SPI 对这个可能的冲突做出的规定

SPI移位示意图

移位示意图是 SPI 硬件电路设计的核心。 也可以说是主机与从机的字节交换

image

我们规定波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放到引脚上,波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位。

假设主机有个数据 1010101 要发送给从机,同时,从机有个数据 01010101 要发送到主机

  1. 那我们就可以驱动时钟,先产生一个上升沿,这时所有的位,就会往左移动一次,从最高位移出去的数据,就会放到通信线上,数据放到通信线上,实际上是放到了输出数据寄存器
  2. 可以看到,此时 MOSI 数据是 1,所以 MOSI 的电平就是高电平,MISO 的数据是 0,所以 MISO 的电平就是低电平,这就是第一个时钟上升沿执行的结果,就是把主机和从机中,移位寄存器的最高位,分别放到 MOSI 和 MISO 的通信线上,这就是数据的输出。
  3. 之后,时钟继续运行,上升沿之后,下一个边沿就是下降沿,在下降沿时,主机和从机内都会进行数据采样输入,也就是,MOSI 的 1,会采样输入到从机这里的最低位;MISO 的 0,会采样输入到主机这里的最低位,这就是第一个时钟结束后的现象。
  4. 那时钟继续运行,下一个上升沿,同样的操作,移位输出,主机现在的最高位,也就是原始数据的次高位,输出到 MOSI,从机现在的最高位,输出到 MISO;随后,下降沿,数据采样输入,MISO 数据到从机这里的最低位,MOSI 数据到主机这里的最低位。之后时钟继续运行,第三个时钟开始,上升沿,移位,主机输出,从机输出;下降沿,采样,主机输入,从机输入。之后,第 4 个时钟,第 5 个时钟,等等,一直到第 8 个时钟,都是同样的过程。
  5. 最终 8 个时钟之后,原来主机里的 10101010 跑到从机里了,原来从机里的 01010101 跑到主机里了,这就实现了,主机和从机一个字节的数据交换

实际上,SPI 的运行过程就是这样。SPI 的数据收发,都是基于字节交换,这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这样,主机要发送的数据,跑到从机,主机要从从机接收的数据,跑到主机,这就完成了发送同时接收的目的。

如果我只想发送,不想接收,怎么办呢?其实很简单,我们仍然调用交换字节的时序,发送,同时接收,只是,这个接收到的数据,我们不看它就行了。

那如果我只想接收,不想发送,怎么办呢?同理,我们还是调用交换字节的时序,发送,同时接收,只是,我们会随便发送一个数据,只要能把从机的数据置换过来就行了,我们读取置换过来的数据,不就是接收了嘛。这里我们随便发过去的数据,从机也不会去看它。当然这个随便的数据,我们不会真的随便发,一般在接收的时候,我们会统一发送 0x00 或 0xFF,去跟从机换数据。

SPI时序基本单元

起始条件和停止条件

  • 起始条件:SS从高电平切换到低电平
  • 停止条件:SS从低电平切换到高电平

image

交换一个字节

SPI 有两个可以配置的位,分别叫做 CPOL(Clock Polarity)时钟极性和 CPHA(Clock Phase)时钟相位,每一位可以配置为 1 或 0。总共组合起来,就有模式 0、模式 1、模式 2、模式3 这 4 种模式。

模式:0

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=0:SCK第一个边沿(上升沿)移入数据,第二个边沿(下降沿)移出数据

image

模式 0,CPHA = 0,表示 SCK 第一个边沿移入数据,第二个边沿移出数据,模式 0 在 SCK 第一个边沿就要移入数据,但是数据总得先移出,才能移入,所以在模式 0 的配置下,SCK 第一个边沿之前,就要提前开始移出数据了,或者把它称作是在第 0 个边沿移出,第 1 个边沿移入。

SS 下降沿开始通信,现在 SCK 还没有变化,但是 SCK 一旦开始变化就要移入数据了,所以此时趁 SCK 还没有变化,SS 下降沿时,就要立刻触发移位输出,所以这里 MOSI 和 MISO 的输出,是对齐到 SS 的下降沿的,或者说,这里把 SS 的下降沿,也当作时钟的一部分,那 SS 下降沿触发了输出,SCK 上升沿,就可以采样输入数据了,这样 B7 就传输完毕

之后,SCK 下降沿,移出 B6,SCK 上升沿,移入 B6,然后继续,下降沿移出数据,上升沿移入数据,最终在第 8 个上升沿时,B0 位移入完成,整个字节交换完成

模式:1

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

image

因为 CPHA = 1,SCK 第一个边沿移出数据,所以这里可以看出,SCK 第一个边沿就是上升沿,主机和从机同时移出数据,主机通过 MOSI 移出最高位,此时 MOSI 的电平就表示了主机要发送数据的 B7,从机通过 MISO 移出最高位,此时 MISO 表示从机要发送数据的 B7。然后时钟运行,产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样,这里主机移出的 B7,进入从机移位寄存器的最低位,从机移出的 B7,进入主机移位寄存器的最低位。这样一个时钟脉冲产生完毕,一个数据位传输完毕,接下来就是同样的过程,上升沿,主机和从机同时输出当前移位寄存器的最高位,第二次的最高位,就是原始数据的 B6,然后下降沿,主机和从机移入数据,B6 传输完成,之后时钟继续运行,数据依次移出、移入、移出、移入。最后一个下降沿,数据 B0 传输完成,至此,主机和从机就完成了一个字节的数据交换

模式:2

  • CPOL=1:空闲状态时,SCK为高电平
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

image

其中模式 0 和模式 2 的区别,就是模式 0 的 CPOL = 0,模式 2 的 CPOL = 1。两者的波形就是 SCK 的极性取反一下,剩下的流程上的东西,完全一致。

模式:3

  • CPOL=1:空闲状态时,SCK为高电平
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

image

然后模式 1 和 模式 3 的区别,也是模式 1 的 CPOL = 0,模式 3 的 CPOL = 1。两者的波形,也是 SCK 的极性取反一下,其他地方,没有变化

CPHA 表示的是时钟相位,决定是第一个时钟采样移入还是第二个时钟采样移入,并不是规定上升沿采样还是下降沿采样的 ,但是当 CPOL 确定的情况下,CPHA 确实会改变采样时刻的上升沿和下降沿。比如,模式 0 的时候,是 SCK 上升沿采样移入;模式 1 的时候,是 SCK 下降沿采样移入,这个了解一下,CPHA 决定是第几个边沿采样,并不能单独决定是上升沿还是下降沿。然后在这 4 种模式里,模式 0 和 模式 3,都是 SCK 上升沿采样;模式 1 和 模式 2,都是 SCK 下降沿采样

完整的时序

每个芯片对 SPI 时序字节流功能的定义不一样,在这里,以本节使用的芯片 W25Q64 它的时序为例进行介绍,SPI 对字节流功能的规定,不像 I2C 那样。I2C 的规定一般是,有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型;而在 SPI 中,通常采用的是指令码加读写数据的模型,这个过程就是,SPI 起始后,第一个交换发送给从机的数据,一般叫做指令码,在从机中,对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节发送指令集里面的数据,这样就能指导从机完成相应的功能了。不同的指令,可以有不同的数据个数,有的指令,只需要一个字节的指令码就可以完成,比如 W25Q64 的写使能、写失能等指令,而有的指令,后面就需要再跟要读写的数据,比如 W25Q64 的写数据、读数据等,写数据,指令后面就得跟上,我要在哪里写,我要写什么;读数据,指令后面就得跟上,我要在哪里读,我读到的是什么,这就是指令码加读写数据的模型。在 SPI 从机的芯片手册里,都会定义好指令集,什么指令对应什么功能,什么指令,后面得跟上什么数据

发送指令

向SS指定的设备,发送指令(0x06)

在 W25Q64 芯片里,这个 0x06 代表的是写使能。

image

在这里,使用的是 SPI 模式 0,在空闲状态时,SS 为高电平,SCK 为低电平,MOSI 和 MISO 的默认电平没有严格规定。然后,SS 产生下降沿,时序开始,在这个下降沿时刻,MOSI 和 MISO 就要开始变换数据了。

指定地址写

image

之后我们再看一条指令,这条指令是指定地址写。
功能是:向 SS 指定的设备,先发送写指令(0x02),写指令在指令集中,规定是 0x02,随后在指定地址下(Address[23:0]),写入指定数据(Data)。因为 W25Q64 芯片有 8 M 字节的存储空间,一个字节的 8 位地址肯定不够,所以这里地址是 24 位的,分 3 个字节传输

由于 SPI 没有应答机制,所以交换一个字节后,就立刻交换下一个字节就行了。

由于整个流程,我们只需要发送的功能,并没有接收的需求,所以 MISO 这条接收的线路,就始终处于“挂机”的状态,并没有用到

也可以继续发送数据,SPI 里,也会有和 I2C 一样的地址指针,每读写一个字节,地址指针自动加 1,如果发送一个字节之后,不终止,继续发送的字节就会依次写入到后续的存储空间里,这样就可以实现从指定地址开始,写入多个字节了。这就是 SPI 写入的时序。

指定地址读

image

起始之后第一个字节,主机发送指令 0x03,代表我要读取数据了。

之后还是一样,主机再依次交换 3 个字节,分别是 0x12、0x34、0x56,组合到一起就是 0x123456,代表 24 位地址。

3 个字节地址交换完之后,我们要把从机的数据读过来,我们随便给从机一个数据,当然一般给 FF 就行了;然后从机就会把 0x123456 地址下的数据通过 MISO 发给主机,可以看到,这样的波形,就表示指定地址下的数据是 0x55,这样主机就实现了指定地址读一个字节的目的。

然后如果我们继续读,那么从机内部的地址指针自动加 1,从机就会继续把指定地址下一个位置的数据发过来,这样依次进行,就可以实现指定地址接收多个字节的目的了。

然后最后,数据传输完毕,SS 置回高电平,时序结束。当然,时序这里也会有一些细节,比如,由于 MISO 是硬件控制的波形,所以它的数据变化,都可以紧贴时钟下降沿;另外我们可以看到,MISO 数据的最高位,实际上是在上一个字节,最后一个下降沿,提前发生的,因为这时 SPI 模式 0,所以数据变化都要提前半个周期。

W25Q64闪存

W25Q64简介

  • W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景

  • 存储介质:Nor Flash(闪存)

  • 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

    • Dual SPI:双重SPI。 发的时候,可以同时用 MOSI 和 MISO 发送,在收的时候,也可以同时用 MOSI 和 MISO 接收,MOSI 和 MISO 同时兼具发送和接收的功能。一个 SCK 时钟,能同时发送或接收 2 位数据
    • Quad SPI:四重SPI。 这个芯片里,除了 SPI 通信引脚,还有两个引脚,一个是 WP 写保护,另一个是 HOLD,这两个引脚,如果不需要的话,也可以拉过来,充当数据传输引脚,加上 MOSI 和 MISO,这就可以 4 个数据位同时收发了
  • 存储容量(24位地址):
    W25Q40: 4Mbit / 512KByte
    W25Q80: 8Mbit / 1MByte
    W25Q16: 16Mbit / 2MByte
    W25Q32: 32Mbit / 4MByte
    W25Q64: 64Mbit / 8MByte
    W25Q128: 128Mbit / 16MByte
    W25Q256: 256Mbit / 32MByte

    24 位地址的最大寻址空间是 16 MB。所以 W25Q40 到 Q128,使用 3 字节 24 位的地址都是足够的。但是 W25Q256 就比较尴尬了,24 位地址,对于 32 MB 来说,是不够的,所以这最后一个型号比较特殊。根据手册里描述,W25Q256 分为 3 字节地址模式和 4 字节地址模式,在 3 字节地址模式下,只能读写前 16 MB 的数据,后面 16 MB,3 个字节的地址够不着;要想读写到所有存储单元,可以进入 4 字节地址的模式

引脚定义

image

引脚 功能
VCC、GND 电源(2.7~3.6V)
CS(SS) SPI片选
CLK(SCK) SPI时钟
DI(MOSI) SPI主机输出从机输入
DO(MISO) SPI主机输入从机输出
WP 写保护
HOLD 数据保持

HOLD引脚:意思就是数据保持,低电平有效。

就是如果你在进行正常读写时突然产生中断,然后想用 SPI 通信线去操纵其他器件,这时如果把 CS 置回高电平,那时序就终止了,但如果你又不想终止总线,又想操作其他器件,这就可以 HOLD 引脚置低电平,这样芯片就 HOLD 住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来,HOLD 置回高电平,然后继续 HOLD 之前的时序,相当于 SPI 总线进了一次中断,并且在中断里,还可以用 SPI 干别的事情,这就是 HOLD 的功能。

硬件电路

image

WP和HOLE引脚默认接VCC,也就是默认不使用的

电路框图

image

  • 存储区域划分

    右上角是存储器的规划示意图。 W25Q64的容量是 8 MB,一整块的容量太大,不利于管理;而且后续,我们涉及到 Flash 擦除,或者写入的时候,都会有个基本单元,我们得以这个基本单元为单位进行,所以8 MB 的存储空间,就有必要进行一些合理的划分。

    那常见的划分方式就是:一整块存储空间,先划分为若干的块 Block,其中每一块再划分为若干的扇区 Sector,对于每个扇区,内部又可以分成很多页 Page。

    • W25Q64 的地址宽度是 24 位,3 个字节,所以可以看到,左下角,第一个字节它的地址是 00 00 00 h,h 代表 16 进制,之后的空间,地址依次自增,直到最后一个字节,地址是 7F FF FF h,对应8 MB 的空间

    • 块区的划分:然后在这整个空间里,我们以 64 KB 为一个基本单元,把它划分为若干的块 Block,整个空间是 8 MB,以 64 KB 为一块进行划分,最后分得的块数,就是 8 MB/64 KB = 128,所以这里可以分得 128 块,那块序号,就是块 0,一直到最后一个,是块 127;

      块内地址值的变化规律:比如块 0 的起始地址是 00 00 00,结束地址是 00 FF FF,之后,块 31,起始是 1F 00 00,结束是 1F FF FF,之后的都可以观察一下,可以发现,在每一块内,它的地址变化范围就是最低的 2 个字节,每个块的起始是 xx 00 00,结束是 xx FF FF,这是块内地址的变化规律

    • 扇区的划分:在一个块区里,再以 4 KB 为一个单元,进行切分,总共切得 64 KB/4 KB = 16 份,所以在每一块里,都可以分为16个扇区,从扇区0,一直到扇区 15,

      扇区内的地址规律: xx x0 00,到 xx xF FF

    • 页:地址划分到扇区就结束了。但是当我们在写入数据时,还会有个更细的划分,这就是页 Page,页是对整个存储空间划分的,当然你也可以把它看作,在扇区里,再进行划分,都是一样,那页的大小呢,是 256 个字节,一个扇区是 4 KB,以 256 个字节划分,能分得 4 KB/256 B = 16 页;

      页的地址规律:在一页中,地址变化范围是 xx xx 00,到 xx xx FF,一页内的地址变化,仅限于地址的最低一个字节

    • 总结:一整个存储空间,首先划分为若干块,对于每一块,又划分为若干扇区,然后对于整个空间,会划分很多很多页,每页 256 字节

  • SPI 控制逻辑:也就是芯片内部进行地址锁存、数据读写等操作,都可以由控制逻辑来自动完成,这个不用我们操心,控制逻辑就是整个芯片的管理员,我们有什么事,只需要告诉这个管理员就行了

  • SPI 的通信引脚:在控制逻辑左边,有 WP、HOLD、CLK、CS、DI 和 DO,这些引脚,就和我们的主控芯片相连,主控芯片通过 SPI 协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能。

  • 状态寄存器:这个状态寄存器是比较重要的,比如芯片是否处于忙状态、是否写使能、是否写保护,都可以在这个状态寄存器里体现

  • 写控制逻辑:和外部的 WP 引脚相连,显然,这个是配合 WP 引脚实现硬件写保护的。

  • 高电压生成器:配合 Flash 进行编程,因为 Flash 是掉电不丢失的,一般都需要一个比较高的电压去刺激它,产生一个很强的变化,即使掉电了也不会变回来

  • 页地址锁存/计数器和字节地址锁存/计数器:用来指定地址。

    我们通过 SPI,总共发过来 3 个字节的地址,因为 1 页是 256 字节,所以 1 页内的字节地址,就取决于最低一个字节,而高位的 2 个字节,就对应的是页地址,所以发送过来的 3 个字节地址中,前两个字节,会进到这个页地址锁存/计数器里,最后一个字节,会进到这个字节地址锁存/计数器

    页地址通过这个写保护逻辑和行解码来选择我要操作哪一页;

    字节地址通过这个列解码和 256 字节页缓存来进行指定字节的读写操作。

    而这个地址锁存,都是有一个计数器的,所以这个地址指针,在读写之后,可以自动加 1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了。

  • 256 字节的页缓存区:它其实是一个 256 字节的 RAM 存储器,然后我们数据读写,就是通过这个 RAM 缓存区来进行的,我们写入数据,会先放到缓存区里,然后在时序结束后,芯片再将缓存区的数据复制到对应的 Flash 里进行永久保存。

    那为啥要弄个缓存区呢,我们直接往 Flash 里写不好吗?

    那这是因为,我们的 SPI 写入的频率是非常高的,而 Flash 的写入,由于需要掉电不丢失,要留下强烈的变化,他就比较慢,所以这个芯片的设计思路就是,你写入的数据,我先放在页缓存区里存着,因为缓存区是 RAM,他的速度非常快,可以跟的上 SPI 总线的速度

    注意:

    • 缓存区只有 256 字节,写入的一个时序,连续写入的数据量,不能超过 256 字节。而且只有从页起始位置开始,才能最大写入 256 字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱。所以在进行多字节写入时,一定要注意地址范围,不能跨越页的边沿

    • 写时序完成后芯片再把数据从缓存区转移到 Flash 存储器里,需要一定的时间,所以在写入时序结束后,芯片会给状态寄存器的 BUSY 位置 1,表示芯片很忙,并且芯片在忙的时候不会响应新的读写时序

Flash操作注意事项

写入操作时:

  • 写入操作前,必须先进行写使能

  • 每个数据位只能由1改写为0,不能由0改写为1

  • 写入数据前必须先擦除,擦除后,所有数据位变为1

  • 擦除必须按最小擦除单元进行

    该芯片擦除的最小单位是一个扇区,也就是4KB,4096个字节。

    当只想要擦除某个字节的数据时,没别的办法,只能把那个字节所在扇区的 4096 个字节全都擦掉,如果该扇区有其他数据,就只能先把这4096个字节都读,修改本来要擦除的字节,再把4096个字节读回去

  • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
    写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:
直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

状态寄存器

image

里面就状态寄存器1的BUSY和WEL位较为重要

  • BUSY:设备正在执行页编程,页编程就是写入数据;然后扇区擦除,块擦除,整片擦除;或者写状态寄存器指令时,BUSY 位置 1,在这期间,设备将会忽略进一步的指令,当然除了读状态寄存器和擦除挂起指令。然后是当编程、擦除、写状态寄存器指令结束后,BUSY 清零来指示设备准备好了,

  • WEL:在执行完写使能指令后,WEL 置 1,代表芯片可以进行写入操作了,当设备写失能时,WEL 位清 0

会写使能的情况

  • 上电后,芯片默认写失能;
  • 发送写失能指令
  • 页编程、扇区擦除等等,这些写入操作之后,WEL 会 = 0

这表明,当我们先写使能,再执行写入数据操作后,不需要再手动进行写失能了,因为写入操作后会顺便帮我们写失能,相当于有个顺手关门的操作。同时这也表明,我们在进行任何写入操作前,都得来一遍写使能,一个写使能,只能保证后续的一条写指令可以执行

指令集

常用指令集

指令名称 指令 Byte1 Byte2 Byte3 Byte4
写使能 0x06
读状态寄存器1 0x05 (S7~S0)
写数据(页编程) 0x02 A23~A16 A15~A8 A7~A0 D7~D0
扇区擦除(4KB) 0x20 A23–A16 A15~A8 A7~A0
读ID 0x9F 厂商ID 设备ID高八位(设备类型) 设备ID低八位(设备容量)
读数据 0x03 A23~A16 A15~A8 A7~A0 (D7~D0)

注:大写S开头表示寄存器,大写A开头表示地址,大写D开头表示数据。

使用括号括起来的表示读取的数据,没有用括号括起来的表示写入的数据

读ID:厂商ID:0xEF;设备ID高八位:0x40;设备ID低八位:0x17

例如:

写数据:主机发送指令0x02后,紧跟着的第一个字节是要写入的地址的1623位,第二个字节是要写入的地址的815位,第三个字节是地址的0~7位,第四个字节是要写入的数据。

需要连续写入时直接在第五个字节写数据即可,但是需要注意不要超过一页的范围了

全部指令集

image

image

案例:软件SPI通信

接线图

image

SPI的代码封装

MySPI.c

#include "stm32f10x.h"                  // Device header

#define SPI_GPIO	GPIOA			// SPI通信使用的GPIO口
#define SS 			GPIO_Pin_4		// SS从机选择线使用的引脚
#define SCK			GPIO_Pin_5		// SCK时钟线使用的引脚
#define MISO		GPIO_Pin_6		// MISO主机输入使用的引脚
#define MOSI		GPIO_Pin_7		// MOSI主机输出使用的引脚

/* 使用函数的方式封装通信线的读写有下面优点
	1、方便兼容其他单片机
	2、扩展性也高,例如主机频率太高从机跟不上,就可以在函数中加上延时。
	这里W25Q64的芯片频率最高有80MHz,完全跟的上STM32的引脚翻转,所以这里就没有加上延时
*/
// 写SS从机选择线
void MySPI_W_SS(uint8_t value)
{
	GPIO_WriteBit(SPI_GPIO, SS, (BitAction)value);
}
// 写SCK时钟线
void MySPI_W_SCK(uint8_t value)
{
	GPIO_WriteBit(SPI_GPIO, SCK, (BitAction)value);
}
// 写MOSI主机输出从机输入线
void MySPI_W_MOSI(uint8_t value)
{
	GPIO_WriteBit(SPI_GPIO, MOSI, (BitAction)value);
}
// 读MISO主机输入从机输出线
uint8_t MySPI_R_MISO()
{
	return GPIO_ReadInputDataBit(SPI_GPIO, MISO);
}

/**
  * 函    数:SPI软件读写初始化,使用SPI模式0
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;	// 主机的输出线都要求配置成推挽输出
	GPIO_InitStruct.GPIO_Pin = SS | SCK | MOSI;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;		// 主机的输入线路要求配置成上拉或浮空输入
	GPIO_InitStruct.GPIO_Pin = MISO;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
	
	// 模式0,SCK低电平时处于空闲状态
	MySPI_W_SCK(0);
	// SS默认高电平
	MySPI_W_SS(1);
}

/**
  * 函    数:SPI通信起始条件
  * 参    数:
  * 返 回 值:
  */
void MySPI_Start()
{
	// SS从高电平到低电平
	MySPI_W_SS(0);
}

/**
  * 函    数:SPI通信结束条件
  * 参    数:
  * 返 回 值:
  */
void MySPI_Stop()
{
	// SS从低电平到高电平
	MySPI_W_SS(1);
}

/**
  * 函    数:使用掩码的方法进行交换字节的操作(高位先行)
  * 参    数:Byte 主机交换给从机的一个字节
  * 返 回 值:从机交换给主机的一个字节
  */
//uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte)
//{
//	uint8_t S_Byte = 0x00;
//	
//	for(uint8_t i = 0; i < 8; i++)
//	{
//		MySPI_W_MOSI(M_Byte & (0x80 >> i));
//		MySPI_W_SCK(1);
//		if(MySPI_R_MISO() == 1){S_Byte |= (0x80 >> i);}
//		MySPI_W_SCK(0);
//	}
//	return S_Byte;
//}

/**
  * 函    数:使用移位的方法进行交换字节的操作(高位先行)[该方法效率较高,但是函数运行完毕后无法获取主机发送的数据]
  * 参    数:Byte 主机交换给从机的一个字节
  * 返 回 值:从机交换给主机的一个字节
  */
uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte)
{
	for(uint8_t i = 0; i < 8; i++)
	{
		// 将M_Byte的最高位放到MOSI上
		MySPI_W_MOSI(M_Byte & 0x80);
		
		// M_Byte最高位已经放到MOSI上了,将M_Byte左移1,最高位就被移出去了,最低位补0
		M_Byte <<= 1;
		
		MySPI_W_SCK(1);
		
		// MISO为高电平时,将M_Byte的最低位置1
		if(MySPI_R_MISO() == 1){M_Byte |= 0x01;}
		
		MySPI_W_SCK(0);
	}
	return M_Byte;
}


MySPI.h

#ifndef __MySPI_H
#define __MySPI_H

// 初始化SPI
void MySPI_Init(void);
// 主机和从机交换一个字节
uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte);
// 起始条件
void MySPI_Start(void);
// 停止条件
void MySPI_Stop(void);

#endif

W25Q64的代码封装

W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_cmd.h"

/**
  * 函    数:W25Q64初始化
  * 参    数:
  * 返 回 值:
  */
void W25Q64_Init()
{
	MySPI_Init();
}

/**
  * 函    数:写使能
  * 参    数:
  * 返 回 值:
  */
void W25Q64_Write_ENABLE()
{
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

/**
  * 函    数:读状态寄存器1
  * 参    数:
  * 返 回 值:状态寄存器的数据,其中BUSY在最低位
  */
uint8_t W25Q64_Read_SR1()
{
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_READ_STATUS_REGISTER_1);
	MySPI_Stop();
	return MySPI_ReadWrite_Byte(W25Q64_DUMMY_BYTE);
}

/**
  * 函    数:等待BUSY置1
  * 参    数:
  * 返 回 值:
  */
void W25Q64_Wite_BUSY()
{
	uint32_t TimeOut;
	
	// 状态寄存器1的最低位是BUSY,当BUSY为1时表示W25Q64正在写入或擦除
	while((W25Q64_Read_SR1() & 0x01) == 0x01)
	{
		// 防止程序意外卡死
		TimeOut++;
		if(TimeOut >=100000)
		{
			break;
		}
	}
}

/**
  * 函    数:写入数据
  * 参    数:Address 24位的地址。注意页的范围,在连续写入时如果超过了当前页的范围会覆盖当前页第一个地址的数据
  * 参    数:Data 装有数据的数组首地址
  * 参    数:Data_Count 数组的长度,也就是要写入的数据个数
  * 返 回 值:无
  */
void W25Q64_Write_Data(uint32_t Address, uint8_t *Data, uint16_t Data_Count)
{
	W25Q64_Write_ENABLE();
	
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_PAGE_PROGRAM);
	MySPI_ReadWrite_Byte(Address >> 16);
	MySPI_ReadWrite_Byte(Address >> 8);
	MySPI_ReadWrite_Byte(Address);
	for(uint8_t i = 0; i < Data_Count; i++)
	{
		MySPI_ReadWrite_Byte(Data[i]);
	}
	MySPI_Stop();
	
	W25Q64_Wite_BUSY();
}

/**
  * 函    数:擦除扇区。4KB大小
  * 参    数:Address 要擦除的扇区地址
  * 返 回 值:无
  */
void W25Q64_Clear_Section(uint32_t Address)
{
	// 写使能
	W25Q64_Write_ENABLE();
	
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_ReadWrite_Byte(Address >> 16);
	MySPI_ReadWrite_Byte(Address >> 8);
	MySPI_ReadWrite_Byte(Address);
	MySPI_Stop();
	
	// 等待BUSY置1
	W25Q64_Wite_BUSY();
}

uint8_t ID_Data[3];
/**
  * 函    数:返回ID号
  * 参    数:无
	* 返 回 值:数组。索引0:厂商地址;索引1+索引2=设备ID
  */
uint8_t *W25Q64_ReadID()
{
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_JEDEC_ID);
	ID_Data[0] = MySPI_ReadWrite_Byte(W25Q64_DUMMY_BYTE);
	ID_Data[1] = MySPI_ReadWrite_Byte(W25Q64_DUMMY_BYTE);
	ID_Data[2] = MySPI_ReadWrite_Byte(W25Q64_DUMMY_BYTE);
	MySPI_Stop();
	return ID_Data;
}

/**
  * 函    数:读数据
  * 参    数:Address 24位的地址
  * 参    数:Data 将读出的数据放到该数组
  * 参    数:Data_Count 数组的长度,也就是要读出的数据个数
  * 返 回 值:无
  */
void W25Q64_Read_Data(uint32_t Address, uint8_t *DataArry, uint32_t Data_Count)
{
	MySPI_Start();
	MySPI_ReadWrite_Byte(W25Q64_READ_DATA);
	MySPI_ReadWrite_Byte(Address >> 16);
	MySPI_ReadWrite_Byte(Address >> 8);
	MySPI_ReadWrite_Byte(Address);
	for(uint8_t i = 0;i < Data_Count; i++)
	{
		DataArry[i] = MySPI_ReadWrite_Byte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}


W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);

void W25Q64_Write_ENABLE(void);

uint8_t W25Q64_Read_SR1(void);

void W25Q64_Write_Data(uint32_t Address, uint8_t *Data, uint16_t Data_Count);

void W25Q64_Clear_Section(uint32_t Address);

uint8_t* W25Q64_ReadID(void);

void W25Q64_Read_Data(uint32_t Address, uint8_t *DataArry, uint32_t Data_Count);

#endif

W25Q64_cmd.h

W25Q64相关指令集

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H

#define W25Q64_WRITE_ENABLE							0x06	// 写使能
#define W25Q64_WRITE_DISABLE						0x04	// 写失能
#define W25Q64_READ_STATUS_REGISTER_1				0x05	// 读SR1
#define W25Q64_READ_STATUS_REGISTER_2				0x35	// 读SR2
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02	// 页编程(写数据)
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20	// 扇区擦除
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F	// 读ID
#define W25Q64_READ_DATA							0x03	// 读数据
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3

#define W25Q64_DUMMY_BYTE							0xFF	// 主机与从机交换数据时主机发送的无用数据

#endif

SPI外设

SPI外设简介

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

  • 可配置8位/16位数据帧、高位先行/低位先行

  • 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

    SPI 的时钟,是由 PCLK 分频得来的,APB2 的 PCLK 是 72 MHz,APB1 的 PCLK 是 36 MHz

    分频系数可以配置为 2 或 4 或 8、16、32、64、128、256。

  • 支持多主机模型、主或从操作

  • 可精简为半双工/单工通信

    • 半双工就是,如果我们去掉一根数据线,只在其中一根线分时进行发送或接收,这就是半双工的通信,节省一根通信线。
    • 单工就是如果我们直接去掉接收的数据线,在发送线进行只发的数据传输,那就是单工通信,只发模式。如果直接去掉发送的数据线,在接收线进行只收的数据传输,那就是单工通信,只收模式。
  • 支持DMA

  • 兼容I2S协议

    这个 I2S 是一种音频传输协议,它和 I2C 的区别还是挺大的,不要搞混了。I2S 和 I2C,除了名字差不多,都是飞利浦公司提出的外,其他地方,基本上是完全不一样的

STM32F103C8T6 硬件SPI资源:SPI1(挂载于APB2)、SPI2(挂载于APB1)

SPI框图

image

  • LSBFIRST 控制位:这一位可以控制是低位先行还是高位先行,手册里,寄存器描述可以查一下(LSBFIRST 帧格式,给 0,先发送 MSB,MSB 就是高位的意思;给 1,先发送 LSB,LSB 就是低位的意思)。目前的状态 LSBFIRST 应该是 1,低位先行;

  • 移位寄存器:右边的数据低位,一位一位的,从 MOSI 移出去;然后 MISO 的数据,一位一位的,移入到左边的数据高位,显然移位寄存器应该是一个右移的状态,所以目前图上表示的是低位先行的配置。

  • MOSI 和 MISO交叉的部分: 这一块主要是用来进行主从模式引脚变换的

    我们这个 SPI 外设,可以做主机,也可以做从机。做主机时,这个交叉就不用,MOSI 为 MO,主机输出;MISO 为 MI,主机输入,这是主机的情况。如果我们 STM32 作为从机的话,MOSI 为 SI,从机输入,这时它就要走交叉的这一路,输入到移位寄存器;同理,MISO,为 SO,从机输出,这时,输出的数据也走交叉的这一路,输出到 MISO。当然这里,如果这样理解没错的话,内部向上的这个箭头可能是画错方向了,应该是往下指的,这样才符合逻辑,那这就是这个交叉的作用。简而言之,就是主机和从机的输入输出模式不同,如果要切换主机和从机的话,线路就需要交叉一下;当然如果我们始终做主机的话,那这个交叉就不用看了。

  • 接收缓冲区和发送缓冲区:实际上就是数据寄存器 DR,下面发送缓冲区,就是发送数据寄存器 TDR,上面接收缓冲区,就是接收数据寄存器 RDR,TDR 和 RDR 占用同一个地址,统一叫作 DR。写入 DR 时,数据从地址和数据总线写入到 TDR;读取 DR 时,数据从接收缓冲区(RDR)读出。数据寄存器和移位寄存器打配合,可以实现连续的数据流。

    具体流程就是:比如我们需要连续发送一批数据。第一个数据,写入到 TDR,当移位寄存器没有数据移位时,TDR 的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的 TXE 为 1,表示发送寄存器空。当我们检查 TXE 置 1 后,紧跟着,下一个数据,就可以提前写入到 TDR 里候着了,一旦上个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来了,它就会自动产生时钟,将数据移出去,在移出的过程中,MISO 的数据也会移入,一旦数据移出完成,数据移入是不是也完成了,这时,移入的数据,就会整体的从移位寄存器转入到接收缓冲区 RDR,这个时刻,会置状态寄存器的 RXNE 为 1,表示接收寄存器非空,当我们检查 RXNE 置 1 后,就要尽快把数据从 RDR 读出来,在下一个数据到来之前,读出 RDR,就可以实现连续接收。否则,如果下一个数据已经收到了,上一个数据还没从 RDR 读出来,那 RDR 的数据就会被覆盖,就不能实现连续的数据流了。这就是移位寄存器配合数据寄存器实现连续数据流的过程。

  • 波特率发生器:这个主要就是用来产生 SCK 时钟的。

    他的内部,主要就是一个分频器,输入时钟是 PCLK,72M 或 36M,经过分频器之后,输出到 SCK 引脚,当然这里生成的时钟肯定是和移位寄存器同步的了,每产生一个周期的时钟,移入移出一个 bit。

    CR1 寄存器的三个位 BR0、BR1、BR2 用来控制分频系数

  • SPI_CR1:

    • SPE(SPI Enable)是 SPI 使能,就是 SPI_Cmd 函数配置的位;

    • BR(Baud Rate)配置波特率,就是 SCK 时钟频率;

    • MSTR(Master)配置主从模式,1 是主模式,0 是从模式,我们一般用主模式

    • CPOL 和 CPHA,这个之前讲过,用来选择 SPI 的 4 种模式

  • SPI_SR:

    • TXE,发送寄存器空;

    • RXNE,接收寄存器非空

    这两个比较重要,我们发送接收数据的时候,需要关注这两位。

  • SPI_SR2:就是一些使能位,比如中断使能,DMA 使能等,然后剩下的一些位,用的不多

NSS 引脚:

SS 就是从机选择,低电平有效,所以这里前面加了个 N,这个 NSS,和我们想象的从机选择可能不太一样,我们想象的应该是,用来指定某个从机,对吧。但是根据手册里的描述研究了一下,这里的 NSS 设计,可能更偏向于实现简介里说的多主机模型。总的来说,这个 NSS 我们并不会用到,SS 引脚,我们直接使用一个 GPIO 模拟就行,因为 SS 引脚很简单,就置一个高低电平就行了。而且多从机的情况下,SS 还会有多个,这里硬件的 NSS 也完成不了我们想要的功能,那这个 NSS 是如何实现多主机切换的功能呢?简单介绍一下,大家看一看就行,不用掌握。假如有 3 个 STM32 设备,我们需要把这 3 个设备的 NSS 全都连接在一起。首先,这个 NSS 可以配置为输出或者输入,当配置为输出时,可以输出电平告诉别的设备,我现在要变为主机,你们其他设备都给我变为从机,不要过来捣乱;当配置为输入时,可以接收别设备的信号,当有设备是主机,拉低 NSS 后,我就无论如何也变不成主机了,这是它的作用。然后内部电路的设计,当 SPI_CR2 寄存器中的 SSOE = 1 时,NSS 作为输出引脚,并在当前设备变为主设备时,给 NSS 输出低电平,这个输出的低电平,就是告诉其他设备,我现在是主机了;当主机结束后,SSOE 要清 0,NSS 变为输入,这时,输入信号就会跑到通信电路右边的数据选择器,SSM 位决定选择哪一路,当选择上面一路时,是硬件 NSS 模式,也就是说,这时外部如果输入了低电平,那当前的设备就进入不了主模式了,因为 NSS 低电平,肯定是外部已经有设备进入了主模式,它已经提前告诉我它是主模式了,我就不能再跟它抢了;当数据选择器选择下面一路时,是软件管理 NSS 输入,NSS 是 1 还是 0,由这一位 SSI 来决定。这个多主机的模型,举个例子就是,NSS 输入,就是留了个小辫子,一旦我的小辫子被别人揪住了,我就只能乖乖听话,所以我可以把所有人的小辫子接在一起,谁要当主机,就先跳出来,从自己的小辫子输出低电平,揪住其他所有人的小辫子,这时其他所有人都得乖乖听话了。如果这时,其他人也想跳出来做主机呢,它就会发现自己的小辫子被别人揪住了,那它就做不了主机了,只能先乖乖听话,这就是这个 NSS 实现多主机的思路。当然这个设计,使 NSS 作为多从机选择的作用消失了,揪住所有人的小辫子之后,主机发送的数据,就只能是广播发送给所有人的了,如果想实现指定设备通信,可能还需要再加入寻址机制,所以实现起来还是比较复杂的。当然我自己其实也没试过这种玩法,这里是根据看手册的理解,觉得应该是这样玩的,实际使用的话,可能还有很多事情要考虑。不过 SPI最多的情况还是一主多从,或者一主一从,我们掌握一主多从就行,多主机的情况,了解即可。

SPI基本结构

image

其中核心部分就是这个数据寄存器和移位寄存器,这里,发送和接收就直接叫作发送数据寄存器 TDR 和接收数据寄存器 RDR 了,移位寄存器画的是左移,高位移出去,通过 GPIO,到 MOSI。之后移入的数据,从 MISO 进来,通过 GPIO,到移位寄存器的低位,这样循环 8 次,就能实现主机和从机交换一个字节,然后 TDR 和 RDR 的配合,可以实现连续的数据流,

另外,TDR 数据,整体转入移位寄存器的时刻,置 TXE 标志位,移位寄存器数据,整体转入 RDR 的时刻,置 RXNE 标志位,

波特率发生器,产生时钟,输出到 SCK 引脚;

数据控制器呢,就看成是一个管理员,它控制着所有电路的运行;

最后,开关控制,就是 SPI_Cmd,初始化之后,给个 ENABLE,使能整个外设。

这里没有画 SS从机选择引脚,这个引脚,我们还是使用普通的 GPIO 口来模拟即可。在一主多从的模型下,GPIO 模拟的 SS 是最佳选择。

我们在写代码的时候,会用一个结构体来统一配置这些部分,比如高位先行还是低位先行,CPOL 和 CPHA 选择 SPI 模式,主机还是从机,时钟频率是多少,等等等等,这些参数,用一个结构体统一配置,选选参数就行了

主模式全双工连续传输

image

CPOL = 1,CPHA = 1,示例使用的是 SPI 模式 3

发送流程

  1. TXE 为 1,表示 TDR 空,可以写入数据开始传输
  2. 软件写入 0xF1 至 SPI_DR,0xF1,就是要发送的第一个数据
  3. 写入之后TDR 变为 0xF1,同时,TXE 变为 0,表示 TDR 已经有数据了
  4. 但是TDR 是等侯区,移位寄存器才是真正的发送区,移位寄存器刚开始肯定没有数据,所以在等候区 TDR 里的 F1,就会立刻转入移位寄存器,此时置 TXE 标志为 1,表示发送寄存器空。然后移位寄存器有数据了,波形就自动开始生成
  5. 在移位产生 F1 波形的同时,等候区 TDR 是空的,为了移位完成时,下一个数据能不间断地跟随,我们就要提早把下一个数据写入到 TDR 里等着了。所以下面指示第二步的操作是写入 F1 之后,软件等待 TXE = 1,一旦 TDR 空了,我们就写入 F2 至 SPI_DR , 写入之后TDR 的内容就变成 F2 了,也就是把第二个数据放到 TDR 里候着
  6. 之后的发送流程也是同理,F1 数据波形产生完毕后,F2 转入移位寄存器开始发送,这时 TXE = 1, 我们尽快把下一个数据 F3 放到 TDR 里等着,这就是这里的操作。软件等待 TXE = 1,然后写入 F3 至 DR,写入之后 TDR 变为 F3。最后在这里,如果我们只想发送 3 个数据,F3 转入移位寄存器之后,TXE = 1,我们就不需要继续写入了,TXE 之后一直是 1。注意,在最后一个 TXE = 1 之后,还需要继续等待一段时间,F3 的波形才能完整发送,等波形全部完整发送之后,BUSY 标志由硬件清除,这才表示,波形发送完成了

接收流程

接收的流程,SPI 是全双工,发送的同时,还有接收

  1. 在第一个字节发送完成后,第一个字节的接收也完成了,接收到了数据 A1。这时,移位寄存器的数据整体转入 RDR,RDR 随后存储的就是 A1。转入的同时,RXNE 标志位也置 1,表示收到数据了
  2. 软件等待 RXNE = 1, 然后从 SPI_DR,也就是 RDR,读出数据 A1,这就是第一个接收到的数据。接收之后,软件清除 RXNE 标志位
  3. 当下一个数据 2 收到之后,RXNE 重新置 1,我们监测到 RXNE = 1 时,就继续读出 RDR,这是第二个数据 A2。在最后一个字节时序完全产生之后,数据 3 才能收到
  4. 注意,一个字节波形收到后,移位寄存器的数据自动转入 RDR,会覆盖原有的数据,所以,我们读出 RDR 要及时。比如 A1 这个数据,收到之后,最迟,也要在 A2 收到之前把它读走,否则,下一个数据 A2,覆盖 A1,就不能实现连续数据流的接收了。

优缺点:

连续数据流,对软件的配合要求较高。在每个标志位产生后,你的数据都要及时处理。配合的好,时钟可以连续不间断的产生,每个字节之间,没有任何空隙,传输效率是最高的

操作比较复杂,而且数据的位置交叉比较多。比如我们发送数据 1,按理说,在进行交换字节时,发送数据后,我们就要看一下接收的是什么,但实际情况是在接收数据 1 之前,我们就要把发送的数据 2 写入到 TDR 了。所以它的流程并不是我们想象的发送数据 1、接收数据 1、发送数据 2、接收数据 2,这样依次交换,而是发送数据 1、发送数据 2、之后接收数据 1,然后再发送数据 3、接收数据 2、发送数据 4、接收数据 3 这个交换的流程是交错的

非连续传输

image

  1. 检测TXE=1,TDR为空,写入第一个数据0xF1到SPI_DR中,TXE=0;但是此时移位寄存器也为空,所以该数据会立刻转入到移位寄存器中,TXE=1

    非连续传输与连续传输的区别就在这里,连续传输时,只要检测到TXE=1就要写入数据了,但是非连续传输不光要检测到TXE=1,还要等待数据交换完成,也就是RXNE=1才写入数据

  2. 等待数据发送完成,发送完成的同时数据也接收完成,移位寄存器中的数据转入RDR中,RXNE=1,接收缓冲寄存器不为空,读取SPI_DR,读取完毕后软件置RXNE=0

  3. 把第一个接收到的数据读出来,之后,再写入下一个字节数据,也就是下面的软件等待 TXE = 1,但是 较晚写入 0xF2 至 SPI_DR;较晚写入 TDR 后,数据 2 开始发送,我们还是不着急写数据 3,等到了时序结束时,先把接收的数据 2 收着,再继续写入数据 3,数据 3 时序结束后,最后再接收数据 3 置换回来的数据

缺点:在 TXE 置 1 时没有及时把下一个数据写入 TDR 候着,所以等到第一个字节时序完成后,第二个字节还没有送过来,那这个数据传输,就会在这里等着,所以这里时钟和数据的时序,在字节和字节之间会产生间隙,拖慢了整体数据传输的速度。这个间隙,在 SCK 频率低的时候,影响不大,但是在 SCK 频率非常高时,间隙拖后腿的现象,就比较严重了

用示波器观察不同 SCK 频率,间隙的影响情况,有 4 个波形,它们的 SCK 分频系数分别是 256、128、64、2

256 分频,这个 SCK 时钟频率是 72M/256,大概 280K,这里使用 SPI 模式 0

image

**128 分频,SCK 频率,大概 560K **

image

**64 分频,SCK 频率,大概 1M 多点 **

image

2 分频 ,36MHz

image

SPI库函数

SPI_InitTypeDef

typedef struct
{
  uint16_t SPI_Direction;           /*!< 指定SPI的单向或双向数据模式 */
  uint16_t SPI_Mode;                /*!< 指定SPI是从机还是主机 */
  uint16_t SPI_DataSize;            /*!< 指定SPI的数据帧大小,8位还是16位 */
  uint16_t SPI_CPOL;                /*!< 设置SPI的CPOL(SCK空闲时候的电平) */
  uint16_t SPI_CPHA;                /*!< 设置SPI的CPHA(第一个边沿移入数据还是第二个边沿移入数据) */
  uint16_t SPI_NSS;                 /*!< 指定NSS信号是由硬件(NSS引脚)管理还是由软件(SSI位)管理。 */
  uint16_t SPI_BaudRatePrescaler;   /*!< 指定分频系数对PCLK进行分频,用于SCK */
  uint16_t SPI_FirstBit;            /*!< 指定数据位是高位先行还是低位先行 */
  uint16_t SPI_CRCPolynomial;       /*!< CRC校验的多项式(使用他给的默认值7即可) */
}SPI_InitTypeDef;

/* SPI_Direction */
SPI_Direction_2Lines_FullDuplex		// 双线全双工
SPI_Direction_2Lines_RxOnly			// 双线只接收模式
SPI_Direction_1Line_Rx				// 单线半双工的接收模式
SPI_Direction_1Line_Tx				// 单线半双工的发送模式

/* SPI_Mode */
SPI_Mode_Master		// SPI作为主机
SPI_Mode_Slave		// SPI作为从机

/* SPI_DataSize */
SPI_DataSize_16b	// 16位数据帧
SPI_DataSize_8b		// 8位数据帧

/* SPI_CPOL */
SPI_CPOL_Low		// SCK默认高电平。CPOL=0
SPI_CPOL_High		// SCK默认低电平。CPOL=1

/* SPI_CPHA */
SPI_CPHA_1Edge		// 第一个边沿采样。CHPA=0
SPI_CPHA_2Edge		// 第二个边沿采样。CPHA=1

/* SPI_NSS */
SPI_NSS_Soft		// NSS引脚由软件管理
SPI_NSS_Hard		// NSS引脚由硬件管理

/* SPI_BaudRatePrescaler */
SPI_BaudRatePrescaler_2		// 对PCLK进行2分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_4		// 对PCLK进行4分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_8		// 对PCLK进行8分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_16	// 对PCLK进行16分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_32	// 对PCLK进行32分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_64	// 对PCLK进行64分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_128	// 对PCLK进行128分频后作为SCK的时钟频率
SPI_BaudRatePrescaler_256	// 对PCLK进行256分频后作为SCK的时钟频率
// PCLK:APB2或APB1的时钟频率,分别为72MHz和36MHz

/* SPI_FirstBit */
SPI_FirstBit_MSB	// 高位先行
SPI_FirstBit_LSB	// 低位先行

/* SPI_CRCPolynomial */
使用他给的默认值7即可

库函数

/* 注意:SPI和I2S是共用一个电路的 */
// 恢复SPI和I2S的初始配置
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);

// 初始化SPI
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);

// 初始化I2S
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);

// 使用默认值填充SPI的结构体
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);

// 使用默认值填充I2S的结构体
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);

// SPI使能或失能
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// I2S使能或失能
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// SPI和I2S的中断使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);

// SPI和I2S的DMA使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);

// 发送数据(写DR寄存器)。SPI和I2S共用该寄存器
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);

// 读取数据(读DR寄存器)
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

/* NSS引脚配置 */
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);

// 8位或16位数据帧的配置
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);

/* CRC校验的配置 */
void SPI_TransmitCRC(SPI_TypeDef* SPIx);
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);

// 半双工时,双向线的方向配置
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);

// 获取SPI和I2S的标志位
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

// 清除SPI和I2S的标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);

// 获取SPI和I2S的中断标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

// 清除SPI和I2S的中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

案例:SPI硬件通信

接线图

与软件通信一致

示例代码

将软件SPI通信的:MySPI.c文件修改一下即可

#include "stm32f10x.h"                  // Device header

// 使用SPI1外设,根据引脚定义可知,SPI1的通信线复用在PA4~PA7。其中PA4的SS使用软件控制
#define SPI_GPIO	GPIOA			// SPI通信使用的GPIO口
#define SS 			GPIO_Pin_4		// SS从机选择线使用的引脚
#define SCK			GPIO_Pin_5		// SCK时钟线使用的引脚
#define MISO		GPIO_Pin_6		// MISO主机输入使用的引脚
#define MOSI		GPIO_Pin_7		// MOSI主机输出使用的引脚

/* 使用函数的方式封装通信线的读写有下面优点
	1、方便兼容其他单片机
	2、扩展性也高,例如主机频率太高从机跟不上,就可以在函数中加上延时。
	这里W25Q64的芯片频率最高有80MHz,完全跟的上STM32的引脚翻转,所以这里就没有加上延时
*/
// 写SS从机选择线
void MySPI_W_SS(uint8_t value)
{
	GPIO_WriteBit(SPI_GPIO, SS, (BitAction)value);
}

//// 写SCK时钟线
//void MySPI_W_SCK(uint8_t value)
//{
//	GPIO_WriteBit(SPI_GPIO, SCK, (BitAction)value);
//}
//// 写MOSI主机输出从机输入线
//void MySPI_W_MOSI(uint8_t value)
//{
//	GPIO_WriteBit(SPI_GPIO, MOSI, (BitAction)value);
//}
//// 读MISO主机输入从机输出线
//uint8_t MySPI_R_MISO()
//{
//	return GPIO_ReadInputDataBit(SPI_GPIO, MISO);
//}

/**
  * 函    数:SPI软件读写初始化,使用SPI模式0
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init()
{
//	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//	
//	GPIO_InitTypeDef GPIO_InitStruct;
//	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;	// 主机的输出线都要求配置成推挽输出
//	GPIO_InitStruct.GPIO_Pin = SS | SCK | MOSI;
//	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
//	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
//	
//	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;		// 主机的输入线路要求配置成上拉或浮空输入
//	GPIO_InitStruct.GPIO_Pin = MISO;
//	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
//	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
//	
//	// 模式0,SCK低电平时处于空闲状态
//	MySPI_W_SCK(0);
//	// SS默认高电平
//	MySPI_W_SS(1);
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;	// 软件控制SS从机选择线,是由GPIO控制高低电平,所以是哟普通推挽输出
	GPIO_InitStruct.GPIO_Pin = SS;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;	// MOSI和SCK都是SPI外设控制输出,所以使用复用推挽输出
	GPIO_InitStruct.GPIO_Pin = MOSI | SCK;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
	
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;		// 根据手册,MISO配置上拉或浮空输入
	GPIO_InitStruct.GPIO_Pin = MISO;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(SPI_GPIO, &GPIO_InitStruct);
	
	SPI_InitTypeDef SPI_InitStruct;
	SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	// 对APB2的时钟进行128分频,作为SCK的时钟
	SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;							// CPHA=0,第一个边沿采样
	SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;								// CPOL=0,SCK默认低电平
	SPI_InitStruct.SPI_CRCPolynomial = 7;								// CRC校验,用不上,使用他给的默认值7
	SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;						// 一帧数据8位
	SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;		// 双线全双工模式
	SPI_InitStruct.SPI_Mode = SPI_Mode_Master;							// 作为主机
	SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;								// NSS使用软件控制
	SPI_Init(SPI1, &SPI_InitStruct);
	
	SPI_Cmd(SPI1, ENABLE);
	
	// SS初始化高电平
	MySPI_W_SS(1);
}

/**
  * 函    数:SPI通信起始条件
  * 参    数:
  * 返 回 值:
  */
void MySPI_Start()
{
	// SS从高电平到低电平
	MySPI_W_SS(0);
}

/**
  * 函    数:SPI通信结束条件
  * 参    数:
  * 返 回 值:
  */
void MySPI_Stop()
{
	// SS从低电平到高电平
	MySPI_W_SS(1);
}

/**
  * 函    数:使用掩码的方法进行交换字节的操作(高位先行)
  * 参    数:Byte 主机交换给从机的一个字节
  * 返 回 值:从机交换给主机的一个字节
  */
//uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte)
//{
//	uint8_t S_Byte = 0x00;
//	
//	for(uint8_t i = 0; i < 8; i++)
//	{
//		MySPI_W_MOSI(M_Byte & (0x80 >> i));
//		MySPI_W_SCK(1);
//		if(MySPI_R_MISO() == 1){S_Byte |= (0x80 >> i);}
//		MySPI_W_SCK(0);
//	}
//	return S_Byte;
//}

///**
//  * 函    数:使用移位的方法进行交换字节的操作(高位先行)[该方法效率较高,但是函数运行完毕后无法获取主机发送的数据]
//  * 参    数:Byte 主机交换给从机的一个字节
//  * 返 回 值:从机交换给主机的一个字节
//  */
//uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte)
//{
//	for(uint8_t i = 0; i < 8; i++)
//	{
//		// 将M_Byte的最高位放到MOSI上
//		MySPI_W_MOSI(M_Byte & 0x80);
//		
//		// M_Byte最高位已经放到MOSI上了,将M_Byte左移1,最高位就被移出去了,最低位补0
//		M_Byte <<= 1;
//		
//		MySPI_W_SCK(1);
//		
//		// MISO为高电平时,将M_Byte的最低位置1
//		if(MySPI_R_MISO() == 1){M_Byte |= 0x01;}
//		
//		MySPI_W_SCK(0);
//	}
//	return M_Byte;
//}

/**
  * 函    数:使用硬件进行交换字节的操作
  * 参    数:Byte 主机交换给从机的一个字节
  * 返 回 值:从机交换给主机的一个字节
  */
uint8_t MySPI_ReadWrite_Byte(uint8_t M_Byte)
{
	// 等待TXE=1,发送缓冲寄存器空
	while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
	// 发送数据,写DR寄存器
	SPI_I2S_SendData(SPI1, M_Byte);		// 写DR寄存器后自动清除TXE标志位
	// 等待RXNE=1,接收缓冲寄存器非空
	while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
	// 接收数据,读取DR寄存器
	return SPI_I2S_ReceiveData(SPI1);	// 读DR寄存器后自动清除RXNE标志位
}



posted @ 2024-02-27 20:04  7七柒  阅读(38)  评论(0编辑  收藏  举报