SPI通信
SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
SS线是用来选择和哪个从机通信,低电平有效。
同步,全双工
支持总线挂载多设备(一主多从)
硬件电路
所有SPI设备的SCK、MOSI、MISO分别连在一起。
主机另外引出多条SS控制线,分别接到各从机的SS引脚。
输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
移位设计图
移位寄存器有一个时钟输入端,因为SPI是高位先行,所以每来一个时钟,移位寄存器就会向左进行移位,从机中的移位寄存器也是同理。
移位寄存器的时钟源是由主机提供的,叫波特率发生器,它产生的时钟驱动主机的移位寄存器移位,同时,这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器里。主机左边移位寄存器移出的数据通过MOSI引脚,输入到从机移位寄存器的右边;从机移位寄存器左边移出的数据,通过MISO引脚,输入到主机移位寄存器的右边,这样组成一个圈。
波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放到引脚上,波特率时钟发生器的下降沿,引脚上的位,采样输入到移位寄存器的最低位。
这种方法就是下面的模式1。
SPI时序基本单元
起始条件:SS从高电平切换到低电平。
终止条件:SS从低电平切换到高电平。
模式1
交换一个字节(模式1)。
CPOL=0:空闲状态时,SCK为低电平。
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据。
第一个边沿就是SCK的上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MOSI的电平就表示了主机要发送数据的B7,从机通过MISO移出最高位,此时MISO表示从机要发送数据的B7。然后时钟运行,产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样,这里主机移出的B7,进入从机移位寄存器的最低位,从机移出的B7,进入主机移位寄存器的最低位,这样,一个时钟脉冲产生完毕,一个数据位传输完毕。依次移出、移入,最后数据传输完成之后,MOSI电平可以任意,MISO必须置回高阻态
因为有多个从机输出连在一起,如果同时开启输出,会造成冲突。所以在SS的未被选中状态,从机的MISO引脚必须关断输出,即配置输出为高阻状态。在下图中,MISO处于中间,在SS下降沿之后,从机的MISO被允许开启输出,SS上升沿之后,从机的MISO必须置回高阻态。
模式0
交换一个字节(模式0)。
CPOL=0:空闲状态时,SCK为低电平。
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据。
在第一个边沿移入数据,但是要先移出数据才能移入数据,所以在第一个上升沿之前就要提前移出数据,或者称为在第0个边沿移出,第1个边沿移入。所以在MOSI和MISO的输出是对齐SS的下降沿的。
如果要采样多个字节,在第一个字节的结束,也就是最后一个下降沿,主机放一个字节的B7,从机也放下一个字节的B7,SCK上升沿,正好接着采样第二个字节的B7
模式2
交换一个字节(模式2)
CPOL=1:空闲状态时,SCK为高电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
模式3
交换一个字节(模式3)
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
SPI时序
使用SPI模式0
发送指令
向SS指定的设备,发送指令(0x06)
指定地址写
向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)
第一个字节是发送指令,第二、三、四个字节是指定地址,第五个字节是写入的数据。
指定地址读
向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)
第一个字节表示读取数据,之后三个字节表示指定的地址,因为是读取数据,所以之后要接收数据,然后通过交换数据,把从机的数据传给主机。
W25Q64简介
W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
存储介质:Nor Flash(闪存)
时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
存储容量(24位地址):
W25Q40: 4Mbit / 512KByte
W25Q80: 8Mbit / 1MByte
W25Q16: 16Mbit / 2MByte
W25Q32: 32Mbit / 4MByte
W25Q64: 64Mbit / 8MByte
W25Q128: 128Mbit / 16MByte
W25Q256: 256Mbit / 32MByte
硬件电路
W25Q64框图
8MB先划分成128块(每一块64KB);然后每一块又划分成16个扇区,每一个扇区4KB;扇区里再进行划分成页,页的大小是256个字节,一个扇区是4KB,所以一个扇区可以分成16页。页就是每一行,如从000000h到0000FFh。
Status Register状态寄存器,比如芯片是否处于忙状态、是否写使能、是否写保护都可以在这个状态寄存器里体现。
Write Control Logic写控制逻辑,和外部的WP引脚相连,可以配合WP引脚实现硬件写保护的。
Page Address Latch / Counter页地址锁存/计数器,Byte Address Latch / Counter字节地址锁存/计数器,这两个地址锁存/计数器就是用来指定地址的。总共发过来3个字节地址,因为一页是256字节,所以一页内的字节地址,就取决于最低一个字节,而高位的两个字节就对应页地址。所以前两个字节会进入到页地址锁存/计数器,最后一个字节会进到字节地址锁存/计数器,然后页地址通过写保护和行解码,来选择要操作哪一页,字节地址,通过这个列解码和256字节页缓存来进行指定字节的读写操作。又因为地址锁存都是有一个计数器的,所以这个地址指针,在读写之后,可以自动加1,这样就可以实现,从指定地址开始,连续读写多个字节的目的了。
Flash操作注意事项
写入操作时:
- 写入操作前,必须先进行写使能。就使用SPI发送一个写使能的指令。
- 每个数据位只能由1改写为0,不能由0改写为1。所以只能通过擦除重新写入别的数据。
- 写入数据前必须先擦除,擦除后,所有数据位变为1。
- 擦除必须按最小擦除单元进行。
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。因为有一个页缓存区,只能写入256个字节。缓存区的原因是Flash的写入太慢了,跟不上SPI的频率,所以写入的数据会先放到RAM里暂存,等时序结束后,芯片再慢慢地把数据写入到Flash里。
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作。这段时间要把页缓存区的内容写到Flash里,在这段时间状态寄存器的BUSY为1。
读取操作时:
直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
软件SPI读写W25Q64
接线图
程序流程
在发送一个0x9Fh,表示要获取ID号,之后交换字节,第一个字节是厂商ID,第二个字节是设备ID的高八位,第三个字节是设备ID的低八位。
代码
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"void W25Q64_Init(void)
{MySPI_Init();
}void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 获取厂商ID*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 获取设备ID的高八位*DID <<= 8;*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 获取设备ID的低八位MySPI_Stop();
}void W25Q64_WriteEnable(void) // 写使能
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);Timeout = 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) // 因为busy位在最后一位{Timeout--;if (Timeout == 0){break;}}MySPI_Stop();
}void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);for (i = 0; i < Count; ++i){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();
}void W25Q64_SectorErase(uint32_t Address) // 指定地址所在扇区被擦除
{W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();
}void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) // 数据读取
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);for (i = 0; i < Count; ++i){DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}
W25Q64_Ins.h
#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
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#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
#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
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID;
uint16_t DID;uint8_t ArrayWrite[] = {0x55, 0x66, 0x77, 0x88};
uint8_t ArrayRead[4];int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, "MID: DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");W25Q64_ReadID(&MID, &DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000); // 如果不擦除,0是不能改写为1的,1可以改写为0W25Q64_PageProgram(0x000000, ArrayWrite, 4);W25Q64_ReadData(0x000000, ArrayRead, 4);OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2);OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while(1){}
}
SPI通信外设
STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担。
可配置8位/16位数据帧、高位先行/低位先行。
时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)。
支持多主机模型、主或从操作。
可精简为半双工/单工通信。
支持DMA。
兼容I2S协议。
STM32F103C8T6 硬件SPI资源:SPI1、SPI2。
SPI框图
移位寄存器右边的数据低位,一位一位地,从MOSI移出去,然后MISO的数据一位一位地移入到左边的数据高位,显然移位寄存器是一个右移的状态,图上显示的是低位先行,右下角的控制位LSBFIRST可以控制是低位先行还是高位先行,给1是低位先行,给0是高位先行。
发送缓冲区是发送数据寄存器TDR,接收缓冲区是接收数据寄存器RDR。写入DR时,数据写入到TDR,读取DR时,数据从RDR读出。
如果需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,转入时刻会置状态寄存器的TXE为1,表示发送寄存器空,当TXE置1后下一个数据就可以提前写入到TDR里候着了,一旦上一个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。
移位寄存器那里一旦有数据过来,就会自动产生时钟,把数据移出去,在移出的过程中,MISO的数据也会移入,当数据交换完成,移位进入的数据就会整体的从移位寄存器转入到接收缓冲区RDR,这个时刻会置状态寄存器的RXNE为1,表示接收寄存器非空,当检查到RXNE置1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR,就可以实现连续接收,否则RDR的数据会被覆盖。
波特率发生器是用来产生SCK时钟的,它的内部主要就是一个分频器,输入时钟是PCLK,72M或36M,经过分频器之后,输出到SCK引脚。
CR1寄存器:
LSBFIRST是控制高位先行还是低位先行;SPE是SPI使能,就是SPI_Cmd函数配置的位;BR0、BR1、BR2用来控制分频系数。;MSTR配置主从模式,1是主模式,0是从模式;CPOL和CPHA,用来选择SPI的4种模式。
SR状态寄存器:
TXE发送寄存器空,RXNE接收寄存器非空。
NSS引脚,SS就是从机选择,低电平有效,所以前面加了一个N,这个引脚更偏向于实现多主机模型。
SPI基本结构
TDR数据整体转入移位寄存器的时刻,置TXE标志位;移位寄存器数据整体转入RDR的时刻,置RXNE标志位。
波特率发生器产生时钟,输出到SCK引脚;数据控制器就看成一个管理员,它控制着所有电路的运行;最后开关控制,就是SPI_Cmd,初始化之后,给个ENABLE,使能整个外设。
主模式全双工连续传输(入门先不用)
CPOL=1,CPHA=1,使用的是SPI模式3,所以SCK默认是高电平。在SCK的第一个下降沿,MOSI和MISO移出数据,之后上升沿移入数据,依次这样进行。第二行是MOSI和MISO输出的波形,从前到后依次出现的是b0,b1,...b7,示例演示的是低位先行的模式。
首先SS置低电平,开始时序,刚开始TXE为1,表示TDR为空,可以写入数据开始传输,下面指示的第一步是软件写入0xF1至SPI_DR,0xF1就是要发送的第一个数据,写入之后,TDR变为0xF1,同时TXE变为0,表示TDR已经有数据了,然后将TDR里的数据转入到移位寄存器进行发送,转入瞬间,置TXE标志为1,表示发送寄存器空,之后,数据F1的波形就开始产生了。在移位产生F1波形的同时,等候区TDR是空的,为了移位完成时,下一个数据能不间断地跟随,这里我们就要提早把下一个数据写入到TDR里等着,写入F1之后,软件等待TXE=1,一旦TDR空了,就写入F2至SPI_DR,写入之后TDR的内容就变成F2了。之后类似。
在第一个字节发送之后,第一个字节的接收也完成了,接收的数据是A1,数据整体转入RDR,RDR随后存储的就是A1,转入的同时,RXNE标志位也置1,表示收到数据了,接收到数据之后,软件清除RXNE标志位,后面类似。
非连续传输
模式3。检测到TXE为1,TDR为空,就软件写入0xF1至SPI_DR,这时TDR的值变为F1,TXE变为0。目前移位寄存器也是空,这个F1会立刻转入移位寄存器开始发送,波形产生,TXE置回1,表示可以把下一个数据放到TDR候着了。在非连续传输这里,TXE=1了,不急着把下一个数据写进去,而是一直等待,等第一个字节时序结束,接收也完成了,这时接收的RXNE也会置1,等RXNE置1后,先把第一个接收到的数据读出来,之后再写入下一个字节数据0xF2到TDR,之后数据2开始发送,等把接收的数据2收着,再继续写入数据3到TDR。
1. 等待TXE为1。
2. 写入发送的数据至TDR
3. 等待RXNE为1
4. 读取RDR接收的数据
硬件SPI读写W25Q64
接线图
程序流程
- 开启时钟,开启SPI喝GPIO的时钟。
- 初始化GPIO口,其中SCK和MOSI,是由硬件外设控制的输出信号,配置为复用推挽输出,MISO是硬件外设的输入信号,可以配置为上拉输入,SS是软件控制的输出信号,配置为通用推挽输出,GPIO口的初始化配置。
- 配置SPI外设,使用一个结构体选参数即可。
- 开关控制,调用SPI_Cmd,给SPI使能即可。
库函数
// SPI_Init初始化
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
// SPI_Cmd外设使能
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
// 写DR数据寄存器
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
// 读DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
代码
因为只要修改底层的代码,所以这里只修改了MySPI.c。
MySPI.c:
#include "stm32f10x.h" // Device headervoid MySPI_W_SS(uint8_t BitValue) // 写SS的引脚
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);// PA6是主机输入,其他三个都是推挽输出GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);SPI_InitTypeDef SPI_InitStructure;SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;SPI_InitStructure.SPI_CRCPolynomial = 7;SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 选择SPI的模式,SPI主机还是从机SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;SPI_Init(SPI1, &SPI_InitStructure);SPI_Cmd(SPI1, ENABLE);MySPI_W_SS(1);
}void MySPI_Start(void) // 起始信号就是SS置低电平
{MySPI_W_SS(0);
}void MySPI_Stop(void) // 起始信号就是SS置高电平
{MySPI_W_SS(1);
}uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);SPI_I2S_SendData(SPI1, ByteSend);while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);return SPI_I2S_ReceiveData(SPI1);
}