STM32_11(SPI)
SPI通信
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
- MOSI:是主设备输出、从设备输入的数据信号线;、MISO:主设备输入、从设备输出的数据信号线;
- 同步,全双工
- 支持总线挂载多设备(一主多从)
SPI硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
- 主机只能选择一个从机,选择多个从机就会导致数据冲突。当从机的SS引脚为高电平,就是从机未被选中,它的MISO引脚必须切换为高阻态,也就是把引脚断开。这样就可以防止一条线有多个输出,而导致的电平冲突问题。
移位示意图
移位寄存器有一个时钟输入端,因为SPI都是高位先行,所以每来一个时钟,移位寄存器就会向左移位,从机的移位寄存器同理。移位寄存器的时钟源是由主机提供的,这里叫做波特率发生器,它产生的时钟驱动主机的移位寄存器进行移位。同时,这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器里。主机移位寄存器左边移出去的数据通过MOSI引脚,输入到从机的移位寄存器的右边,从机移位寄存器的数据根据MISO输入到主机移位寄存器的右边。
首先规定,波特率发生器时钟上升沿,所有移位寄存器向左移动一位,移出去的位放在引脚上,波特率发生器时钟的下降沿,引脚上的位,采样输入到移位寄存器的最低位。
SPI时序基本单元
起始条件和终止条件
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
模式0
- 交换一个字节(模式0)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
总体来说,MOSI和MISO都提前SCK半个周期。
模式1
- 交换一个字节(模式1)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=1(时钟相位:决定第一个时钟采样移入还是第二个时钟采样移入):SCK第一个边沿移出数据,第二个边沿移入数据
SS高电平时,MISO用一条中间的线表示高阻态,SS下降沿之后,从机的MISO被允许开启输出。SS上升沿之后,从机的MISO必须置回高阻态。当SCK为上升沿时,主机和从机同时移除数据,主机通过MOSI移除最高位,此时的MOSI的电平就表示主机要发送数据B7,从机通过MISO移除最高位,此时MISO就表示从机要发送数据B7。之后时钟运行,产生下降沿,此时的主机和从机需同时移入数据,也就是数据采样,这里主机移除B7,进入从机移位寄存器的最低位,从机移除B7,进去主机移位寄存器的最低位。一个时钟脉冲产生完毕,一位数据传输完毕。
SPI时序(发送指令)
在这里我们使用的是模式0,首先SS是高电平,SCK是低电平。SS产生下降沿,时序开始,在下降沿时刻,MOSI和MISO就开始比变换数据,MOSI的指令码仍为0,所以保持低电平不变,MISO从机没有数据发给主机,引脚电平没有变换。从机采样输入,得到0,主机采样输入,得到1。之后主机要发送数据1,SCK下降沿,数据移出,主机将1移出到MOSI,MOSI变高电平。当主机发送0时候,SCK下降沿,MOSI变为0。SCK上升沿,数据采样,从机接收数据为0。总体来说,SCK低电平时变化时期,高电平时读取时期(下降沿变换数据,上升沿采样数据)。
这个SPI时序代表,主机用0x06换来了从机的0xFF。
SPI时序(指定地址写)
向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。
SPI时序(指定地址读)
向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。
SPI外设
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
- 支持多主机模型、主或从操作
- 可精简为半双工/单工通信
- 支持DMA
- 兼容I2S协议
- STM32F103C8T6 硬件SPI资源:SPI1、SPI2
SPI框图
首先,移位寄存器右边低位的数据一位一位地从MOSI移出,MISO的数据一位一位地以到做左边移位寄存器的数据高位。这个可以控制是低位先行还是高位先行。
假如我们需要连续发送一串数据,第一个数据写入发送缓冲区(TDR),当移位寄存器没有数据移位时,TDR的数据就会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1(表示发送寄存器空),紧接着,下一个数据就可以提前写入TDR等待,一旦上个数据发送完成,下一个数据就可以立刻跟进,实现不间断的连续传输。当数据到移位寄存器后,它就会自动产生时钟,将数据移出去,在移除的过程中,MISO数据也会移入。一旦数据移除完成,数据移入也完成。这时,移入的数据就整体从移位寄存器转入到接收缓冲区RDR,这时会将状态寄存器RXNE置1(表示接收寄存器非空)。当我们检测RXNE置1后,就需要尽快把数据从RDR读出来,在下个数据来之前就可以读出RDR ,实现连续接收。
SPI基本结构
主模式全双工连续传输(效率更高)
非连续传输(代码友好)
步骤:
1、等待TXE置1;
2、写入数据到TDR寄存器;
3、等待RXNE为1;
4、读取RDR接收的数据。
W25Q64模块
- W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景
- 存储介质:Nor Flash(闪存)
- 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)
- 存储容量(24位地址):
W25Q64硬件电路
硬件原理图
/CS(/代表低电平有效或者CS上面有一横线也是低电平有效。
这里的CS对应SS,DI对应MOSI,DO对应MISO。
Flash操作注意事项
- 写入操作时:
- 写入操作前,必须先进行写使能
- 每个数据位只能由1改写为0,不能由0改写为1
- 写入数据前必须先擦除,擦除后,所有数据位变为1
- 擦除必须按最小擦除单元进行(最小的擦除单元是一个扇区4096字节,最大的能把全部擦除)
- 连续写入多字节时,最多写入一页(256字节)的数据,超过页尾位置的数据,会回到页首覆盖写入
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作
- 读取操作时:直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
代码部分
SPI软件配置代码
#include "Bsp_SPI.h"
/* SS写数据 */
void Bsp_SPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
/* SCK写数据 */
void Bsp_SPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
/* MOSI写数据 */
void Bsp_SPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
/* MISO读数据 */
uint8_t Bsp_SPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
/* SPI初始化 */
void Bsp_SPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | 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_Init(GPIOA, &GPIO_InitStructure);
Bsp_SPI_W_SS(1);
Bsp_SPI_W_SCK(0);
}
/* SPI起始 */
void Bsp_SPI_Start(void)
{
Bsp_SPI_W_SS(0);
}
/* SPI终止 */
void Bsp_SPI_Stop(void)
{
Bsp_SPI_W_SS(1);
}
/* MOSI和MISO交换字节 */
/* 模式0 */
uint8_t Bsp_SPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00;
for (uint8_t i = 0; i < 8; i++)
{
Bsp_SPI_W_MOSI(ByteSend & (0x80 >> i));
Bsp_SPI_W_SCK(1);
if (Bsp_SPI_R_MISO() == 1)
{
ByteReceive |= (0x80 >> i);
}
Bsp_SPI_W_SCK(0);
}
return ByteReceive;
}
/* 模式1 */
/*uint8_t Bsp_SPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00;
for (uint8_t i = 0; i < 8; i++)
{
Bsp_SPI_W_SCK(1);
Bsp_SPI_W_MOSI(ByteSend &(0x80 >> i));
Bsp_SPI_W_SCK(0);
if (Bsp_SPI_R_MISO == 1)
{
ByteReceive |= (0x80 >> i);
}
}
return ByteReceive;
}*/
/* 模式3 */
/*uint8_t Bsp_SPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00;
for (uint8_t i = 0; i < 8; i++)
{
Bsp_SPI_W_SCK(0);
Bsp_SPI_W_MOSI(ByteSend &(0x80 >> i));
Bsp_SPI_W_SCK(1);
if (Bsp_SPI_R_MISO == 1)
{
ByteReceive |= (0x80 >> i);
}
}
return ByteReceive;
}*/
/* 模式2 */
/*uint8_t Bsp_SPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00;
for (uint8_t i = 0; i < 8; i++)
{
Bsp_SPI_W_MOSI(ByteSend &(0x80 >> i));
Bsp_SPI_W_SCK(0);
if (Bsp_SPI_R_MISO == 1)
{
ByteReceive |= (0x80 >> i);
}
Bsp_SPI_W_SCK(1);
}
return ByteReceive;
}*/
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
#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
软件SPI读取W25Q64
#include "Bsp_W25Q64.h"
void W25Q64_Init(void)
{
Bsp_SPI_Init();
}
/* 读取ID */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_JEDEC_ID); // 发送0x9F指令码
*MID = Bsp_SPI_SwapByte(W25Q64_DUMMY_BYTE); // 返回制造厂商ID
*DID = Bsp_SPI_SwapByte(W25Q64_DUMMY_BYTE); // 返回设备ID的高8位
*DID <<= 8;
*DID |= Bsp_SPI_SwapByte(W25Q64_DUMMY_BYTE); // 返回设备ID的低8位
Bsp_SPI_Stop();
}
/* 使能 */
void W25Q64_WriteEnable(void)
{
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_WRITE_ENABLE);
Bsp_SPI_Stop();
}
/* 读状态寄存器1(主要读取BUSY位,判断是否在忙) */
void W25Q64_WaitBusy(void)
{
uint32_t TimeOut = 100000;
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
while ((Bsp_SPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) // 判断状态寄存器1的最低位,也就是BUSY位是否在忙。0:忙 1:在忙
{
TimeOut--;
if (TimeOut == 0)
{
break;
}
}
Bsp_SPI_Stop();
}
/* 页编程 */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataAarry, uint16_t Length)
{
W25Q64_WriteEnable(); // 使能
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_PAGE_PROGRAM);
Bsp_SPI_SwapByte(Address >> 16);
Bsp_SPI_SwapByte(Address >> 8);
Bsp_SPI_SwapByte(Address);
for (uint16_t i = 0; i < Length; i++)
{
Bsp_SPI_SwapByte(DataAarry[i]);
}
Bsp_SPI_Stop();
W25Q64_WaitBusy();
}
/* 扇区擦除 */
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable(); // 使能
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
Bsp_SPI_SwapByte(Address >> 16);
Bsp_SPI_SwapByte(Address >> 8);
Bsp_SPI_SwapByte(Address);
Bsp_SPI_Stop();
W25Q64_WaitBusy();
}
/* 读数据 */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataAarry, uint32_t Length)
{
Bsp_SPI_Start();
Bsp_SPI_SwapByte(W25Q64_READ_DATA);
Bsp_SPI_SwapByte(Address >> 16);
Bsp_SPI_SwapByte(Address >> 8);
Bsp_SPI_SwapByte(Address);
for (uint32_t i = 0; i < Length; i++)
{
DataAarry[i] = Bsp_SPI_SwapByte(W25Q64_DUMMY_BYTE);
}
Bsp_SPI_Stop();
}
硬件SPI读取W25Q64
#include "Bsp_SPI.h"
/* SS写数据 */
void Bsp_SPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
/* SPI初始化 */
void Bsp_SPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
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_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 分频系数,分频越高速度越快(注意:SPI1是72M,SPI2是36M。因为SPI1是在APB2,SPI2是在APB1)
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 选择第一个边沿采样还是第二个边沿采样
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟极性:因为在这里选择模式0
SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC校验多项式,默认值7
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据帧
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 双线全双工模式
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 选择向前设备时SPI主机
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // NSS引脚,一般没用到选择软件NSS
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
Bsp_SPI_W_SS(1);
}
/* SPI起始 */
void Bsp_SPI_Start(void)
{
Bsp_SPI_W_SS(0);
}
/* SPI终止 */
void Bsp_SPI_Stop(void)
{
Bsp_SPI_W_SS(1);
}
/* MOSI和MISO交换字节 */
/* 模式0 */
uint8_t Bsp_SPI_SwapByte(uint8_t ByteSend)
{
uint32_t TimeOut = 10000;
uint8_t ByteReceive;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != 1)
{
TimeOut--;
if (TimeOut == 0)
{
break;
}
}
SPI_I2S_SendData(SPI1, ByteSend);
TimeOut = 10000;
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != 1)
{
TimeOut--;
if (TimeOut == 0)
{
break;
}
}
ByteReceive = SPI_I2S_ReceiveData(SPI1);
return ByteReceive;
}
主程序
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Bsp_W25Q64.h"
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] = {0x11, 0x22, 0x33, 0x44};
uint8_t ArrayRead[4];
int main(void)
{
OLED_Init();
W25Q64_Init();
/* 显示厂商和设备ID号 */
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, 13, DID, 4);
W25Q64_SectorErase(0x000000); // 只要末尾3个十六进制数为0,那肯定是扇区的起始地址(不擦除,掉电不丢失)。如果不擦除则改写则会数据出错,因为只能1变0不能0变1的操作所以修改数据前得擦除。
W25Q64_PageProgram(0x00000, ArrayWrite, 4); // 写入数据ArrayWrite,并且写入的数据不能跨页
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)
{
}
}