1. SPI 是什么
SPI (Serial Peripheral Interface):是由美国摩托罗拉公司(Motorala)最先推出的一种同步全双工串行传输规范,也是一种单片机外设芯片串行扩展接口
SPI 接口主要应用在 EEPROM,FLASH,实时时钟, AD转换器,数字信号处理器和数字信号解码器之间,等要求通讯速率较高的场合
SPI 是一种高速、全双工、串行、同步通信总线
全双工意味着至少需要有两根数据线,串行意味着是按 bit 位一位一位的传输
同步意味着通信双方共时钟线:
SDO Serial Data Output 串行数据输出
SDI Serial Data Input 串行数据输入
SCLK Serial Clock 时钟线
SPI 采用的是主从架构,支持多 slave 模式:
Master:SCK ---> 同时接所有的 Slave 的时钟线
MOSI ---> 同时接所有的 Slave 的 MOSI 数据线
MISO ---> 同时接所有的 Slave 的 MISO 数据线
SS1 ---> 接第一个 Slave 的片选信号
SS2 ---> 接第二个 Slave 的片选信号
......
当 Master 要和某个 Slave 通信时:
enable 这个 Slave
disable 其他的 Slave
2. SPI 物理层
SPI 总线上可以同时接多个 SPI 设备 (一主多从)
那么总线上的时钟由谁产生呢?
谁产生都可以,只要同一时刻没有多个设备同时控制总线就可以了
同一时刻也只能有一个设备发送数据,但是可以有多个设备同时接收
通过谁控制时钟线,我们人为的将 SPI 总线上的设备分为:
Master 主设备:产生时钟信号的设备
Slave 从设备:接收时钟信号的设备
一主一从:
一主多从:
根据主从设备的区别,有时候两个数据接口命名也会有所不同:
MOSI:Master Output Slave Input 主设备发送,从设备接收线
MISO:Master Input Slave Output 主设备接收,从设备发送线
MOSI 和 MISO 只是 SDO 和 SDI 的不同叫法而已SCLK:即时钟信号线,用于通讯同步
另外,SPI 还有一根 NSS(Slave Select)线。因为SPI总线上的设备不像 IIC 总线上的设备有地址,所以我们需要一根额外的线来确定到底与谁进行通信
这个NSS(CS)就是片选信号线,用于选择通讯的从设备,也可用CS表示,每个从设备都有一条独立的NSS信号线,主机通过将某个设备的NSS线置低电平来选择与之通讯的从设备。所以 SPI 通讯以NSS线电平置低为起始信号,以NSS线电平被拉高为停止信号,此线可以利用任何一个GPIO引脚,发送0/1进行片选 (比如#SS表示低电平有效,则发送0表示与该设备通信)
所以 SPI 一般需要 4 根线:SDO / SDI / SCLK / CS (单向传输时可以不需要 CS 线)
3. SPI 协议
**协议就是规定数据是如何收发的,SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等内容,**无应答机制
(1) 通信起始信号和停止信号
NSS 信号线由高变低,是 SPI 通讯的起始信号**,当从机检测到自己的NSS线为起始信号时,就 知道主机要与自己进行通讯了**
NSS 信号线由低变高,是 SPI 通讯的停止信号
(2) 数据有效性
SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI 及 MISO 数据线在 SCLK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时, MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用 MSB 先行模式
(3) 通讯模式****CPOL:Clock Polarity 时钟极性时钟极性决定不传任何数据时(SPI空闲时)时钟线的电平状态
CPOL = 1 不传数据(SPI空闲)时,时钟线保持高电平
CPOL = 0 不传数据(SPI空闲)时,时钟线保持低电平
CPHA:Clock PHAse 时钟相位
时钟相位决定 SPI 在何时锁定数据(决定数据采集的时刻)
CPHA = 1 在时钟线 SCLK 的第二个边沿锁存数据(数据采集)CPHA = 0 在时钟线 SCLK 的第一个边沿锁存数据(数据采集)
也就是说CPOL和CPHA的组合决定数据采集和数据发送的时刻
共四种通讯模式(Mode 0,Mode 1,Mode 2,Mode 3)
CPOL = 1,CPHA = 1 通讯模式:
(4) 数据帧格式
SPI 数据的收发是按Frame为单位,那么一帧数据传送的 bit 位数由什么决定?DFF:Data Frame Format 数据帧格式
8bits 一帧数据 8 bits
16bits 一帧数据 16 bits
SPI 数据的收发是按 Frame 为单位
那么一帧数据又是从LSB还是MSB开始发送?
LSBFIRST = 1 LSB(低bit)先发送LSBFIRST = 0 MSB(高bit)先发送
(5) 传输速率
传输速率由时钟频率决定,有一个原则:就低不就高
4. 模拟 SPI 时序收发数据
5. STM32 SPI 控制器
6. STM32 固件库 SPI 函数接口说明
SDO / SDI / SCLK都是复用自GPIO(无论是主从都没有区别)。如果作为从设备,则NSS(片选)也是复用自GPIO,如果是主设备,则不需要利用GPIO去复用为NSS
(1) 配置 SPI 的 GPIO 引脚
cppa. 使能 GPIO 时钟 RCC_APB1PeriphClockCmd(); b. GPIO_Init(); ---> AF c. 配置具体的复用功能 GPIO_PinAFConfig();
(2) 配置 SPI 控制器
cppa. 使能 SPI 时钟 RCC_APBxPeriphClockCmd(); b. 配置 SPI 控制器 void SPI_Init(SPI_TypeDef *SPIx, SPI_InitTypeDef *SPI_InitStruct); @SPIx:指定要配置的SPI控制器编号 SPI1, SPI2 ... SP6 @SPI_InitStruct:指向初始化信息结构体 typedef struct { uint16_t SPI_Direction; 设置SPI的通信方式 可以是半双工/全双工/串行发/串行收 SPI_Direction_2Lines_FullDuplex 全双工 SPI_Direction_2Lines_RxOnly (两根线半双工)只收 SPI_Direction_1Line_Rx (一根线半双工)串行接收 SPI_Direction_1Line_Tx (一根线半双工)串行发 uint16_t SPI_Mode; 设置SPI的主从模式 SPI_Mode_Master 主设备 SPI_Mode_Slave 从设备 uint16_t SPI_DataSize; 设置SPI传输的数据位长度(数据帧的bit位数) SPI_DataSize_16b 数据位 16 SPI_DataSize_8b 数据位 8 uint16_t SPI_CPOL; 设置SPI的时钟极性(在空闲时SCK时钟线的电平状态) SPI_CPOL_Low 空闲时SCK为低电平 SPI_CPOL_High 空闲时SCK为高电平 uint16_t SPI_CPHA; 设置SPI的时钟相位(决定在SCK时钟线的第几个边沿采集数据) SPI_CPHA_1Edge 在第一个边沿采集数据 SPI_CPHA_2Edge 在第二个边沿采集数据 CPOL和CPHA的组合决定数据采集和数据发送的时刻 需要看从设备支持哪种模式,主模式和从模式必须统一 uint16_t SPI_NSS; 设置SPI的NSS信号是由硬件(NSS引脚)还是软件控制,可以理解为片选信号 SPI_NSS_Hard 由硬件NSS引脚控制 SPI_NSS_Soft 由用户软件代码控制 ----> GPIO 引脚 uint16_t SPI_BaudRatePrescaler; 设置SPI波特率的预分频值 SPI波特率(传输速率) = SPI输入时钟频率(APBx时钟) / SPI_BaudRatePrescaler SPI_BaudRatePrescaler_2 2分频 SPI_BaudRatePrescaler_4 4分频 ... SPI_BaudRatePrescaler_256 256分频 uint16_t SPI_FirstBit; 指定SPI传输数据顺序是高位先发还是低位先发 SPI_FirstBit_MSB 高位先发 SPI_FirstBit_LSB 低位先发 uint16_t SPI_CRCPolynomial; 指定CRC(循环冗余校验)校验多项式 这个系数只需要大于1就可以了,为了提高通信可靠性 } SPI_InitTypeDef;
(3) 使能 SPI
cppSPI_Cmd();
(4) 收发 SPI 数据,以及一些状态标志位
cppa. 读数据:只有 RXNE == 1 时才能读(可以利用中断去读也可以轮询) 怎么去获取标志位: FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef *SPIx, uint16_t SPI_I2S_FLAG); 返回值:SET ->1 / RESET -> 0 标志位有: #define SPI_I2S_FLAG_RXNE ((uint16_t)0x0001) #define SPI_I2S_FLAG_TXE ((uint16_t)0x0002) #define SPI_FLAG_CRCERR ((uint16_t)0x0010) #define SPI_FLAG_MODF ((uint16_t)0x0020) #define SPI_I2S_FLAG_OVR ((uint16_t)0x0040) #define SPI_I2S_FLAG_BSY ((uint16_t)0x0080) .... 怎么读:uint16_t SPI_I2S_ReceiveData(SPI_TypeDef *SPIx); -------------------------------------------------------------------------- 举例子:利用轮询去接收数据 unsigned char SPI1_Recv_Byte(void) { while (SPI_GetFlagStatus(SPI1, SPI_FLAG_RXNE) != SET); return SPI_ReceiveData(SPI1); } ---------------------------------------------------------------------------- b. 发数据:只有 TXE == 1 时才能发送数据 如何发:void SPI_I2S_SendData(SPI_TypeDef *SPIx, uint16_t Data); ----------------------------------------------------------------------------- 举例子: Void SPI1_Send_Byte(unsigned char ch) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); SPI_I2S_SendData(SPI1, ch); }
7. W25Q128
W25Q128 是一款 SPI 接口的 flash 存储器芯片
内存:易失性存储器
EEPROM:非易失性存储器,小容量
FLASH:非易失性存储器,容量比较大
磁盘:非易失性存储器
......
易失 / 非易失:指存储器断电后,它存储的数量内容是否会丢失
实现 在 SPI 串行Flash W25Q128 存储芯片的任意地址读写任意字节的数据
(1) 接口说明(见原理图)
引脚:W25Q128 MCU
CS ---------- PB14
CLK <---------- PB3 ----- SPI1_SCK
SO ----------> PB4 ----- SPI1_MISO
SI <---------- PB5 ----- SPI1_MOSI物理上,全双工的
CS:片选引脚(低电平有效)
当CS管脚为高电平时,芯片处在不选择的状态
当CS管脚为低电平时,芯片处于被选择的状态(此时才能够与MCU通信)
(2) W25Q128 内部存储结构
内部存储地址是24bits,这24bits分为了4个部分:
8bits 4bits 4bits 8bits
block number sector number page number byte number
块地址 扇地址 页地址 字节地址
那么 W25Q128 共有 = 256 block(块) 一块65536个字节
每块 block = 16 sector(扇) 一扇4096个字节(4K)
每扇 sector = 16 page(页) 一页256个字节
每页 page = 256 byte(字节)
1GB=1024MB,1MB=1024KB,1KB=1024Bytes,2^10 = 1024
所以 W25Q128 的存储大小 = 256 * 16 * 16 * 256 = 2^24 Bytes = 16 M
反推可知,假设某个字节的地址为x,则该字节位于:
(x >> 16) & 0xFF 块号
(x >> 12) & 0xF 扇号
(x >> 8) & 0xF 页码
x & 0xFF 字地址(页内偏移地址)
如果要往W25Q128中写入数据的话,需要分为两个步骤:
a. 擦除 (擦除区域被置位1)
因为数据修改,二进制:1可以变成0,0不能变成1
擦除:给存储单元充电 ----> 电压升高 ----> 写1 ----> 0xFF 放电:写0
以 块 或者 半块 或者 扇区 或者整个芯片为单位,清除数据
b. 写入(编码)
W25Q128芯片所有的操作,全部都是通过指令来进行的
所以,我们的STM32要想控制W25Q128工作,只需要利用SPI总线,给W25Q128发送相应的指令就可以了(不能像内存一样直接寻址)
(3) W25Q128 的指令(详情见中文手册<9.2 指令>)
W25Q128一共有34个基本指令,指令都是以/CS下降沿开始的
/CS下降沿开始:表示发送指令前,必须先将对应的片选引脚输出为低电平,也就是MCU要先将PB14拉低才能发送指令
第一个传输的字节是指令码
指令其实就是一个字节的十六进制数(如手册表中所示)
在DI上传输的数据是在时钟的上升沿被锁存的
MCU发给W25Q128的数据(指令),必须在时钟的上升沿锁存,也就是说W25Q128是在时钟的上升沿去采样所以这里就注定了CPOL和CPHA该如何配置
英文版手册<6.1.1 Standard SPI Instructions>中介绍了W25Q128 SPI Bus支持Mode 0(0,0)和Mode 3(1,1)MSB首先被传输 --->高位先发,SPI_FirstBit = SPI_FirstBit_MSB
/CS拉高(MCU将PB14拉高指令发送完成)
所有的读指令可以在任何时钟周期结束。但是,所有的擦写指令必须在 /CS 拉高之后还有一个8位的时钟间隔,否则前面的擦写指令将被忽略
发送擦写指令后,必须再过8个 SPI 时钟周期才能发送下一个指令a. 读状态寄存器指令(指令:0x05 / 0x35)
读取状态寄存器SR1 ---> 0x05
读取状态寄存器SR2 ---> 0x35
MCU向W25Q128发送读取状态寄存器指令完成后,W25Q128回复一个字节的状态寄存器SRx的状态码给MCU
cppSR1:<9.控制和状态寄存器> Bit7 Bit6 Bit5 Bit4 Bit3 Bit2 Bit1 Bit0 SRP-0 SEC TB BP2 BP1 BP0 WEL BUSY BUSY:为1表示W25Q128忙碌,为0表示空闲 主机在发送指令之前,必须要先判断从设备的状态是否忙碌 BUSY == 0时,才能发送指令 WEL:为1表示写使能,可以写入 为0表示写禁止,不能写入 BP0-BP2:块保护(指定某些块只能读不能写) TB:顶部/底部块保护 SEC:扇区/块保护 SRP-0:保护的方法
cpp举个例子: #define W25Q128_ENBALE() GPIO_ResetBits(GPIOB, GPIO_Pin_14) // CS->0 #define W25Q128_DISBALE() GPIO_SetBits(GPIOB, GPIO_Pin_14) // CS->1 unsigned char Get_W25Q128_SR1(void) { // 使能片选,选中W25Q128 W25Q128_ENBALE(); // 发送读取SR1的指令 Write_Bytes_To_W25Q128(0x05); /* MCU向W25Q128发送读取状态寄存器指令完成后 W25Q128回复一个字节的状态寄存器SRx的状态码给MCU */ // 接收SR1的值 unsigned char status = Read_Bytes_From_W25Q128(); // 禁止芯片 W25Q128_DISBALE(); return status; // if (status & 0x01 != 0) ---> 表示W25Q128忙碌 }
b. 读取 W25Q128 的 ID(指令:0x90)
1 2 3 4 5 6
Manufacturer/Device ID(2) 90h 00 00 00h (MF7-MF0) (ID7-ID0)
cppuint16_t Read_W25Q128_ID(void) { // 使能片选,选中W25Q128 W25Q128_ENBALE(); // 发送读取ID的指令 Write_Bytes_To_W25Q128(0x90); Write_Bytes_To_W25Q128(0x00); Write_Bytes_To_W25Q128(0x00); Write_Bytes_To_W25Q128(0x00); // 厂商ID uint8_t idh = Read_Bytes_From_W25Q128(); // 设备ID uint8_t idl = Read_Bytes_From_W25Q128(); // 禁止芯片 W25Q128_DISBALE(); uint16_t id = idh << 8 | idl; printf("厂商:%x 设备:%x\n", id >> 8, id & 0xFF); return id; }