1.SPI接口简介
1.1 SPI概述
1.2 硬件结构
1.3 主发从收
1.4主收从发
1.5四种SPI采样模式
1.6一主多从菊花链模式和菊花链模式
1.7I2C和SPI总线对比
2.STM32 SPI FLASH初始化和使用
2.1 SPI接口初始化
2.1.1 spi_gpio_init
2.1.2 spi_interface_init
2.2 SPI发送数据
2.2.发送和接收字节详细说明
编辑
2.2.2读取DeviceID
2.2.3读取JEDEC ID
3.操作FLASH
3.1擦除FLASH扇区
3.1.1写使能操作
3.1.2查看BUSY状态
3.1.3扇区擦除
3.2写入数据
3.2.1 写入缓存数据FLASH
3.3读取数据
3.4 比较写入数据和读取数据
3.5 测试写入和读出
1.SPI接口简介
SPI = Serial Peripheral Interface,是串行外围设备接口,是高速、全双工、同步通信总线。常规只占用四条线,节省芯片管脚,PCB节省空间的布局。我们每天使用最多SPI接口包括FLASH, OLED屏幕,AD采样芯片、加速度传感器等。
- 优点
支持全双工,push-pull与驱动性能相比open-drain信号完整性更好;
支持高速(100MHz以上),速率可以远高于I2C接口
协议支持的字长不限于8bits,新闻字长可根据应用特点灵活选择;
硬件连接简单;
- 缺点:
相比IIC多两根线;
没有寻址机制,只能靠片选择不同的设备;
不接受设备ACK,不知道主设备是否成功发送;
典型应用只支持单主控;
相比RS232 RS485和CAN总线,SPI传输距离短,通常只用于板通信;
1.2 硬件结构
SPI总线定义两个或两个以上设备之间的数据通信,并提供时钟设备Master,接收时钟的设备从设备开始Slave;
信号定义如下:
SCK : Serial Clock 串行时钟
MOSI : Master Output, Slave Input 主发从收信号
MISO : Master Input, Slave Output 主收从发信号
SS/CS : Slave Select 片选信号
数据收发顺序如上图所示。以下分为两种模式:主发从收和主收从发。SPI具体的通信时序。
上面这张图讲解了POL和PHA两种组合的含义:
1.POL=0点,低电平空闲;POL=一、高电平空闲;
2. PHA=0时,输入端沿采样上升,对应红线;
3. PHA=1, 输入端沿采样下降,对应蓝线
1.3 主发从收
首先从芯片中选择主端发送的低电平,上面的帽子表示低有效。在这个脚低电平期间,从设备中选择。主设备发送的时间序列报告对选定的从设备有效,其他装载在总线上的设备忽略了总线报告。
SCLK/SCK:发送同步移位时钟。
MOSI:将数据按照SCLK移位时钟周期将数据移位发送到引脚。根据设备选择SCLK/SCK上升或下降,按位采样,一般字节高位在前,具体必须遵循芯片手册的顺序。从端依赖SCK/SCLK对MOSI上述信号逐位采样,采样位置依次进入接收移位寄存器,完成字节重组。当字节接收完成后续数字电路行处理。、
1.4主收从发
主发从收执行顺序如下:
1. 主芯片发送低电平先选通从芯片;
2. SCLK/SCK:发送同步移位时钟;
3. MISO: 类似MOSI发送位流,依赖SCLK/SCK将位流依次发送至引脚上,主设备在同步时钟的跳变边沿采样该引脚,进而移位接收位流。
1.5四种SPI采样模式
在SPI中,主机可以选择时钟和时钟。在空闲状态期间,C位设置时钟信号的。空闲状态是指传输开始时CS为高电平且在向低电平转变的期间,以及传输结束时CS为低电平且在向高电平转变的期间。C位选择时钟。根据CPHA位的状态,使用时钟上升沿或下降沿来采样和/或移位数据。主机必须根据从机的要求选择时钟极性和时钟相位。根据CPOL和CPHA位的选择,有四种SPI模式可用。
1.CPOL = 0低电平为空闲,数据线在时钟为低时可改变,两根数据线在时钟高电平期间须保持稳定,分两种情况: 1.1 CPHA=0时,输入侧上升沿采样,输出侧在下降沿移位输出到线上 1.2 CPHA=1时,输入侧下降沿采样,输出侧在上升沿移位输出到线上
2.CPOL = 1高电平空闲,两根数据线在时钟为高时可改变,两根数据线在低电平期间须保持稳定。分两种情况: 2.1 CPHA=0,输入侧在下降沿采样,输出侧在上升沿移位输出到线上 2.2 CPHA=1,输入侧在上升沿采样,输出侧在下降沿移位输出到线上
整理成表格形式如上。
:CPOL数值0,空闲为低电平;CPHA为0,输入侧上升沿采样;其余条件可以自行推导完成。
1.6一主多从和菊花链模式
SPI接口最常用的是一主多从模式。具体来说:
- 每个从设备都有独立的片选引脚,主机同一时间段内,与一个从设备进行通信,也即选中一个从设备。
- MOSI/MISO/SCLK并联在一起
- MISO须是三态门,当从设备未选中时,该脚须设置为高阻态,而不能是输出态,否则会影响总线,这句话对于多从设备应用而言,请重点理解。尤其当用GPIO模拟SPI应用而言,须特别注意这一点!
- 对于MOSI/SCLK,虽然并联在一起,但是由于仅一个输出,多输入。输入引脚的阻抗本来就是高阻,所以不会有问题。
- 共用SCLK/,这两根线并联在一起;
- 主MOSI连次级MOSI,次级MISO连次次级的MOSI....,然后由最后一级的MISO再送回到主设备的MISO;
- 某级从设备在第N组时钟周期用MISO发送第N-1组时钟周期接收到位给下级设备,同时把本组时钟周期期间前级设备通过MISO移位进来的数据保存按位序保存进接收寄存器中。其实在底层是按照位进行流转的。这个传递过程当变为高电平时则停止,各从设备当前寄存器中内容锁定了。具体应用时,如果要将某一字节传递到某个设备,则需要组织好传递的码流,以及时钟控制。
- 对于菊花链数据传递过程,其实类似于击鼓传花游戏。鼓点的作用就是同步时钟,花则是要传递的信息数据,鼓点的起停则类似于片选控制,唯一不同的是,击鼓传花传的是一朵花,而菊花链总线传递的是二进制流,至于从设备究竟要怎么应用这些数据流,则具体实现各异。
:大部分项目中,采用一主多从模式进行SPI数据收发。
1.7 I2C和SPI总线对比
下面主要总结一下2种总线的异同点:
相同点:
1 :I2C总线空闲状态下SDA SCL都是高电平。spi总线空闲状态MOSI MISO也都是高电平, SCK是由CPOL决定的;
2 I2C总线scl高电平时sda下降沿标志传输开始,上升沿标志传输结束。spi总线cs拉低标志传输开始,cs拉高标志传输结束;
3 I2C总线和spi总线数据传输都是MSB在前,LSB在后(串口是LSB在前);
4I2C总线和spi总线时钟都是由主设备产生,并且只在数据传输时发出时钟;
不同点:
1 :I2C总线不是全双工,2根线SCL SDA。spi总线实现全双工,4根线SCK CS MOSI MISO;
2 :I2C总线是多主机总线,通过SDA上的地址信息来锁定从设备。spi总线只有一个主设备,主设备通过CS片选来确定从设备;
3 :I2C总线传输速度在100kbps左右,spi总线传输速度更快,可以达到100Mbsp以上;
4 I2C总线是SCL高电平采样。spi总线因为是全双工,因此是沿采样,具体要根据CPHA决定。一般情况下master device是SCK的上升沿发送,下降沿采集;
5 I2C总线读写时序比较固定统一,设备驱动编写方便,不同设备的I2C驱动基本一致。spi总线不同从设备读写时序差别比较大,因此必须根据具体的设备datasheet来实现读写,相对复杂一些。
如果面试嵌入式工程师或者单片机工程师,上面的相同点四点和不同点五点,如果能够答出来一半,基本上就可以入职,如果全部能够打出来,基本上可以横扫了!
2.STM32 SPI FLASH的初始化和使用
掌握了SPI的理论基础之后,我们就需要看一下,如何实现STM32的SPI初始化了。
2.1 SPI接口初始化
其中和usart初始化等相同,分为两部分:spi_gpio_init和spi_interface_init
2.1.1 spi_gpio_init
STM32 SPI1对应的GPIO如下表:
GPIO | SPI |
C0 | CS |
A5 | SCK |
A6 | SO |
A7 | SI |
spi gpio初始化步骤和前面非常类似,其中CS设置为输出,而其他管脚均设置为,也就是这个管脚对应的原生SPI功能。
2.1.2 spi_interface_init
spi初始化步骤如下:
1.设置SPI方向为:2线全双工;
2. 设置SPI模式为Master主模式
3. 设置数据长度为8b
4. 设置CPOL为1,也就是高电平为空闲;
5. 设置CPHA为1,也就是输入下降沿采样;
7.设置NSS为软件片选
8. 设置预分频为4
STM32F1 SPI1最大72M, SPI2最大36M。72M/4=18M,也就是SCK工作频率为18MHz。
9. FirstBit设置为MSB;
10.CRC二项式设置为7
这样就完成了SPI初始化工作。
2.2 SPI发送数据
2.2.1发送和接收字节详解
注意:本函数中不包含SPI起始和停止信号,只是收发的主要过程,所以在调用本函数前后要做好起始和停止信号的操作。
1. 对SPITimeout变量赋值为宏SPIT_FLAG_TIMEOUT。这个SPITimeout变量在下面的while循环中每次循环减1,该循环通过调用库函数SPI_I2S_GetFlagStatus检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测SPIT_FLAG_TIMEOUT次都还没等待到事件则认为通讯失败,调用的SPI_TIMEOUT_UserCallback输出调试信息,并退出通讯;
通过检测TXE标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的上一个数据已经发送完毕;
2. 等待至发送缓冲区为空后,调用库函数SPI_I2S_SendData把要发送的数据"byte"写入到SPI的数据寄存器DR,写入SPI数据寄存器的数据会存储到发送缓冲区,由SPI外设发送出去;
3. 写入完毕后等待RXNE事件,即接收缓冲区非空事件。由于SPI双线全双工模式下MOSI与MISO数据传输是同步的(请对比"SPI通讯过程"阅读),当接收缓冲区非空时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据;
4. 等待至接收缓冲区非空时,通过调用库函数SPI_I2S_ReceiveData读取SPI的数据寄存器DR,就可以获取接收缓冲区中的新数据了。代码中使用关键字"return"把接收到的这个数据作为SPI_FLASH_SendByte函数的返回值,所以我们可以看到在下面定义的SPI接收数据函数SPI_FLASH_ReadByte,它只是简单地调用了SPI_FLASH_SendByte函数发送数据"Dummy_Byte",然后获取其返回值(因为不关注发送的数据,所以此时的输入参数"Dummy_Byte"可以为任意值)。可以这样做的原因是SPI的接收过程和发送过程实质是一样的,收发同步进行,关键在于我们的上层应用中,关注的是发送还是接收的数据。
注意:为了确保数据能够发送成功,在发送和接收的时候,都会检查TXE和RXN3寄存器,确保发送和接收缓冲区都清空。在检查寄存器时,用了。需要注意,为了避免死等模式变成死机模式,在while循环中加上倒计时,如果倒计时时间结束后,还无法完成,则会退出死等模式,
5. 当从Flash中读取数据时,我们只需要调用Flash_SendByte发送一个Dummy_Byte,即可完成数据的读取。
2.2.2读取DeviceID
Flash采用的是winbond的25Q64,具体数据手册见上面链接。
当我们需要读取Deive ID时,首先发送ABh, 然后发送3个dummy字节,最后即可读取Device ID。
上面的代码按照手册,实现了读取Deivce ID的时序过程。
在main函数中,进行调用和读取。
读取的结果如上图所示。
,其实就是空闲字节。在读SPI信号时,通过发送空闲字节,来创建对应的SPI_CLK。因此Dummy_Byte可以是任意内容。
正是因为有Dummy_Byte这样的空闲字节,才能够实现SPI菊花链操作。例如:如果我们在SPI总线上挂接3个Flash,我们现在需要写入byte到第三个flash,我们可以在前面插入两个Dummy_Byte,这样正好可以把第三个字节,写入到第三个flash中。
我们在项目调试时,需要调试的板子通常为新研发出来的板子,可能在原理图设计,PCB设计,或者贴片焊接上存在某些问题,所以往往需要通过最简单的代码,来验证芯片焊接是否正常。读取Device ID和Manufacturing ID就是这里常用的最简单的代码,主要用来验证硬件和基础BSP驱动是否正确。
2.2.3读取JEDEC ID
JEDEC:全称是Joint Electron Device Engineering Council 即电子元件工业联合会。JEDEC是由生产厂商们制定的国际性协议,主要为内存制定。JEDEC用来帮助程序读取Flash的制造商ID和设备ID,以确定Flash的大小和算法,如果芯片不支持CFI,就需使用JEDEC了。工业标准的内存通常指的是符合JEDEC标准的一组内存。
根据手册,我们读取9F这个地址,即可获取JEDEC ID。
上面代码为时序的具体实现,可以看出,为了读取3个byte,需要发送三次Dummy_Byte。完成读取后,再将3个byte组合在一起。
JEDEC ID读取效果如图所示,可以看到,共计读取3个字节。
通过JEDEC ID即可判断FLASH的型号和容量空间大小。
根据检测到的JEDEC ID,可以判断FLASH为W25Q64。
3.操作FLASH
3.1擦除FLASH扇区
3.1.1写使能操作
在页面写入,扇区擦除,块擦除,芯片擦除之前,必须使能FLASH的写操作,向芯片写入0X06,即可完成写使能操作。
3.1.2查看BUSY状态
Flash读写需要耗费一些时间,在新的操作之前,需要了解Flash当前是否已经完成了之前的操作,我们通过检查Flash 状态寄存器中的BUSY即可获得Flash的状态信息。
通过读取05h,可以获取状态寄存器的值。
BUZY状态位可以连续不断的检测和读取。
代码实现时,采用do while结构,do while结构和while结构有所不同的是:do while至少会执行一次操作,因此代码会更加简洁一些。如果采用while操作,就需要先读取一次状态,再检查结果。
比如上面代码修改为while方式,就会变成:
FLASH_Status=SPI_FLASH_SendByte(Dummy_Byte);
while((FLASH_Status&WIP_Flag)==SET)
{
FLASH_Status=SPI_FLASH_SendByte(Dummy_Byte);
}
代码的结构就会不够简洁。
3.1.3扇区擦除
flash的物理特性是,写数据只能将1写为0,0不能写为1。擦除数据是将所有数据都写为1。因此如果想在已经数据的flash上写入新的数据,则必须先擦除。
在写入时,如果需要写入的bit是1,就不进行操作。如果需要写入的bit是0,则将其设置为0。
另外我们需要掌握FLASH的扇区,块等结构定义。
每块 | 每扇区 | 每页 |
16扇区 | 16页 | 256 Byte(2048 bit) |
上面表格详细描述了Flash中块,扇区,页的关系。其中16X16X256=65536B,也就是常用的W25Q64这个FLASH的分区方式。
- 最小擦除单位:扇区
- 可选择擦除单位:扇区、块、全片
- 最大编程(写入)单位:页( 256 Byte),大于256 Byte则需要循环写入。
- Flash 写入数据时和 EEPROM 类似,不能跨页写入,一次最多写入一页,W25Q128的一页是 256 字节。写入数据一旦跨页,必须在写满上一页的时候,等待 Flash 将数据从缓存搬移到非易失区,重新再次往里写。
- 最小编程(写入)单位:1 Byte,即一次可写入 1~256 Byte的任意长度字节。
- 未写入时FLASH里面的数据为全1,即0xFF。
- 只能由 1 —> 0 写入,不能由 0 —> 1 写入,即如果已经写入过了,则需要先擦除(擦除后数据变为全1)再写入。
- 示例:0xF0(1111 0000),即高4位可写入,低4位不可写入。
常用的FLASH擦除规则如上,需要熟练掌握。
擦除扇区的指令如上,发出20h后,依次发出扇区地址的最高位,中间位,最低位,完成地址的发送。
对应擦除扇区地址如上面代码所示。
1.打开写使能;
2. 等待操作结束;
3.CS选中,并发出0X20命令,再发出扇区地址信息;
4.结束选中,等待擦除完成。
通过上面步骤,即可完成擦除过程。
3.2写入数据
3.2.1按页写入数据
02h命令实现按页写入数据。当执行按页写入的命令时,需要注意以下几点:
1. 一次性写入的字节数量必须小于等于256个字节;
2.一开始需要开启写使能;
因此,代码处理时,如果发现NumByteToWrite大于256,则直接省略大于256BYTE的代码,只写入256BYTE,并且打印错误提醒。
3.2.1 将缓存数据写入FLASH
在3.1.3的基础上,可以实现批量数据写入flash中。
由于Flash按照页面操作,所以需要考虑写入地址和写入内容跨页面的情况。
第一种情况:写入地址正好按页面对齐:
这种情况就比较简单,如果需要写入的内容不超出一页,直接写入即可。如果需要写入的内容长度超出了一页,则前面按照整页完成写入,再单独写入后面的字节即可。
第二种情况:写入地址没有按照页面对齐:
例如:写入地址为325, 325%256=69 (余数为69),因此当前页还剩余69个空余位置。而要写入100个字节,则先在当前页写入69个字节,然后在下一页,写入31个字节。通过这种方式,完成100个字节写入。
根据上面的计算方法,实现代码。
1.计算出来addr_mod,为写入地址除以PageSize的余数;
2. rest_of_bytes为写入地址当前页面,还剩下多少bytes;
3. no_of_page为总共需要写入多少页面;
4. no_of_bytes为总字节数除以页面数,剩下不满一页的字节数量;
如果addr_mod为0,说明写入地址恰好为PageSize的整数倍,这种情况容易处理:如果写入长度不满一页,直接写入即可,如果写入长度超出一页,先写整页,再写剩余部分。
如果写入地址不是整数,如果写入数据总长小于页长度,则看下当前页面剩余的rest_of_bytes是否小于需要写入的数据,如果小于需要写入字节长度,则现在当前页面写满,然后在下一个页面写剩余值。
如果需要写入长度大于单页长度,则先按照单页写完,然后再处理超过一页的数据。
3.3读取数据
数据读取则相对简单,只需要按照指定的地址和长度,一直读入数据即可
代码处理上,采用我们一直用的while循环方式。while(rest_of_bytes--) {pBuffer++}
3.4 写入数据和读取数据比较
如果需要比较两个数据异同,需要如何做呢?
上面代码给出了简洁的答案。
3.5 测试写入和读出
我们定义了一个tx_buffer,然后用SPI_FLASH_BufferWrite
从上面结果,Flash测试准确通过。