目录
SPI通信详解可以看这篇文章:SPI通信详解-CSDN博客
SPI软件通信读写W25Q64可以看这篇文章:STM32通过SPI软件读写W25Q64-CSDN博客
STM32通过SPI硬件读写W25Q64
1. STM32的SPI外设简介
-
STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
-
可配置8位/16位数据帧、高位先行/低位先行
-
时钟频率:fpclk/(2,4,8,16,32,64,128,256) PCLK指的是总线时钟,比如APB1 APB2 所以最快的速度就是分频2了。
-
支持多主机模型、主或从操作
-
可精简为半双工/单工通信(很少用)
-
支持DMA
-
兼容12S协议(音频专用)
-
STM32F103C8T6 硬件SPI资源:SPI1、SPI2(SPI1为APB2外设)(其他位APB1)
2. STM32的SPI框图
这个框图是在做主机时的框图、移位寄存器右移
2.1 数据寄存器和移位寄存器(左上角部分)
这里和I2C、串口的设计是异曲同工的。
他们的目的都是实现连续的数据流
- 移位寄存器
- 移位寄存器可以由LSBFIRST控制位 控制是左移还是右移(0高位先、1低位先)
- 此时电路为右移状态。也就是低位先行
- MOSI和MISO
- 数据通过移位寄存器 一位一位 的从MOSI移出去。
- MISO的数据,一位一位的 移入到移位寄存器
- 输出部分的MOSI和MISO进行了交叉,这个是为了主从模式变换的
- 接收缓冲区 和发送缓冲区
- 接收缓冲区叫做RDR
- 发送缓冲区叫做TDR
- TDR和RDR占用同一个地址,统一叫做DR
- **在TDR转入移位寄存器时会产生TXE标志位为1。表示发送寄存器为空。**这时候就可以填装下一个字节了。实现不间断的连续发送
- 在移位寄存器移出完成时,数据移入也会同步完成。这时移入的数据会整体的转入到接收缓冲区RDR。**接收到后,会置RXNE标志位为1。表示接收寄存器非空。**这时就可以在下一个数据来之前,读出RDR。实现连续的接收
控制逻辑(其余右下角的部分)
-
波特率发生器
-
输入时钟为PCLK (36M或者72M)
-
经过分频之后输出到SCK引脚
-
与移位寄存器同步。每产生一个时钟,移位寄存器移入移出一个bit
-
CR1寄存器的BR0、BR1、BR2用来控制分频系数。写入不同的值,可以对PCLK时钟执行2~256分频。得到SCK时钟
000 /2 001 /4 010 /8 011 /16 100 /32 101 /64 110 /128 111 /256
-
-
其他的通信电
这些电路都是黑盒子电路。这里挑选几个重点的讲
- LSBFIRST — 决定高位先行还是低位先行
- SPE —(SPI ENABLE)决定SPI使能
- BR —(Baud Rate)配置波特率,也就是SCK
- MSTR —(Master)配置主从模式
- CPOL和CPHA — 选择SPI的四种模式
CR1寄存器
- TXE — 发送寄存器空
- RXNE — 接收寄存器非空
CR2寄存器
- 中断使能..DMA使能等
-
最后电路的左下角还有一个NSS位也就是SS位(不过我们一般直接用GPIO模拟,这个位置更偏向用于使用到多主机时采用的)
简要了解一下
- 多主机时,所有的主机连接到一条NSS线上。当别的NSS置0时。代表现在别人已经是主机了。会沿着线到一个数据选择器。告诉这个机器,已经有设备是主机了。自己不能跟他抢。
3.STM32的SPI基本框图
4. STM32的SPI主模式全双工连续传输 时序图
5. STM32的SPI主模式全双工非连续传输 时序图
这里可以理解为,一个一个发,发完一个收一个。收完之后再发下一个
并不会像连续一样 提前写入一个字节候着。会造成一定的资源浪费,拖慢传输速度
- 适用于对传输速率要求不高的场合。
6. STM32非连续传输时,不同时钟速率的区别
因为在非连续传输时,数据不是连续的流。所以在软件读取后才发送会浪费很长时间。
对于CLK时钟频率越快的情况下,造成资源浪费的情况就显得越严重
256分频
128分频
64分频
2分频(32MHZ)
这里是因为他的频率已经超过我的示波器的采样频率了。但是也是可以看个大概的
可以看到 ,时间浪费就很严重。
7. 编写SPI硬件读写W25Q64注意事项
-
硬件SPI外设不能随意指定,只能看复用到的引脚列表。
其中NSS片选引脚可以自己指定。用他指定的也可以
8. STM32的SPI外设相关库函数(只看SPI的)
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
- I2S 恢复缺省配置
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
- SPI 初始化
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
- I2S 初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
- SPI 初始化
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
- SPI 外设使能
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
- I2S 使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
- I2S 中断配置
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
- I2S DMA配置
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
- I2S 写数据到TDR寄存器(发送)
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
- I2S 读DR数据寄存器(接收)
void SPI_NSSInternalSoftwareConfig(SPI_TypeDef* SPIx, uint16_t SPI_NSSInternalSoft);
- NSS引脚配置
void SPI_SSOutputCmd(SPI_TypeDef* SPIx, FunctionalState NewState);
- NSS引脚配置
void SPI_DataSizeConfig(SPI_TypeDef* SPIx, uint16_t SPI_DataSize);
- 8位或16位数据帧配置
void SPI_TransmitCRC(SPI_TypeDef* SPIx);
- CRC校验配置
void SPI_CalculateCRC(SPI_TypeDef* SPIx, FunctionalState NewState);
- CRC校验配置
uint16_t SPI_GetCRC(SPI_TypeDef* SPIx, uint8_t SPI_CRC);
- CRC校验配置
uint16_t SPI_GetCRCPolynomial(SPI_TypeDef* SPIx);
- CRC校验配置
void SPI_BiDirectionalLineConfig(SPI_TypeDef* SPIx, uint16_t SPI_Direction);
- 半双时,双向线方向配置
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
- 获取标志位
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
- 清除标志位
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
- 获取中断标志位
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
- 清除中断标志位
7.编写SPI硬件读写W25Q64步骤
- 开启GPIO和SPI外设时钟
- 初始化GPIO口
- MISO为上拉输入,SCK、MOSI复用推挽输出
- SS 通用推挽输出
- 配置SPI外设
- 用结构体即可
- 开关控制
- 使能
- 参考时序图编写代码
8. 编写STM32SPI主模式全双工非连续传输
8.1 步骤
- 等待发送寄存器为空标志位TXE = 1
- 软件写入数据到发送寄存器
- 等待接收完成(这时发送也一定完成)
- 读取、返回RDR
- !!注意其中的软件清除 是不需要我们手动清除的。 因为在我们写入TDR时会顺便清除TXE标志位、读取RDR时会清除RXNE标志位
所以这个只需要把底层的MySPI.c代码改一下就可以了
8.2 程序文件简要说明:
- MySPI:完成SPI的三个基本时序
- MySPI.h:函数声明
- W25Q64.c:初始化W25Q64存储寄存器。完成读写、擦除存储器的时序
- W25Q64.h:函数声明、数据结构体声明
- W25Q64_Ins.h:控制存储器时需要用到的指令集
- main.c:测试硬件SPI读取存储器结果。
MySPI.c(相对软件,硬件仅需修改此文件)
#include "stm32f10x.h" // Device header
/*所用引脚列表*/
#define RCC_GPIO RCC_APB2Periph_GPIOA
#define RCC_SPI1 RCC_APB2Periph_SPI1
#define SCK_Port GPIOA
#define SCK_Pin GPIO_Pin_5
#define SS_Port GPIOA
#define SS_Pin GPIO_Pin_4
#define MOSI_Port GPIOA
#define MOSI_Pin GPIO_Pin_7
#define MISO_Port GPIOA
#define MISO_Pin GPIO_Pin_6
/**
* 函 数:写片选信号SS
* 参 数:BitValue:输入1片选信号SS为高电平
* 返 回 值:无
* 注意事项:无
*/
void MySPI_W_SS (uint8_t BitValue)
{
GPIO_WriteBit(SS_Port,SS_Pin,(BitAction)BitValue);
}
/**
* 函 数:MySPI初始化
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Init(void)
{
/*配置SPI、GPIO外设时钟*/
RCC_APB2PeriphClockCmd(RCC_SPI1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_GPIO,ENABLE);
/*配置引脚*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = SCK_Pin;
GPIO_Init(SCK_Port,&GPIO_InitStructure); //时钟配置为 复用 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = MOSI_Pin;
GPIO_Init(MOSI_Port,&GPIO_InitStructure); //MOSI配置为 复用 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = SS_Pin;
GPIO_Init(SS_Port,&GPIO_InitStructure); //片选配置为 通用 推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = MISO_Pin;
GPIO_Init(MISO_Port,&GPIO_InitStructure); //MISO配置为 上拉输入
/*配置SPI*/
SPI_InitTypeDef SPI_InitSturcture;
SPI_InitSturcture.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//分频系数
SPI_InitSturcture.SPI_CPHA = SPI_CPHA_1Edge; //第1个边沿采样(读入)
SPI_InitSturcture.SPI_CPOL = SPI_CPOL_Low; //CSK默认低电平 == 模式0
SPI_InitSturcture.SPI_CRCPolynomial = 7; //指定CRC计算的多项式(默认为7)
SPI_InitSturcture.SPI_DataSize = SPI_DataSize_8b;//8位数据帧
SPI_InitSturcture.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //两根线全双工
SPI_InitSturcture.SPI_FirstBit = SPI_FirstBit_MSB;//高位先行
SPI_InitSturcture.SPI_Mode = SPI_Mode_Master; //主机模式
SPI_InitSturcture.SPI_NSS = SPI_NSS_Soft; //软件NSS
SPI_Init(SPI1, &SPI_InitSturcture);
/*使能SPI*/
SPI_Cmd(SPI1,ENABLE);
MySPI_W_SS(1);//初始化不选中从机
}
/*******************/
/*SPI的三个时序单元*/
/*******************/
/**
* 函 数:起始信号
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
/**
* 函 数:终止条件
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/**
* 函 数:交换一个字节(模式0)(非连续)
* 参 数:SendByte 待发送的字节
* 返 回 值:ReceiveByte 接收到的字节
* 注意事项:这是使用了移位模型的方式。效率更快
如果要改为模式1,则先上升沿再发送。先下降沿再接收(2、3则直接改时钟极性就ok了)
*/
uint8_t MySPI_WarpByte(uint8_t SendByte)
{
//等待TXE标志位为1 ,发送寄存器空
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) != SET);
//软件写入数据到发送寄存器
SPI_I2S_SendData(SPI1,SendByte);
//等待接收完成(这时发送也一定完成)
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) != SET);
//读取RDR
return SPI_I2S_ReceiveData(SPI1);
}
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H
//初始化
void MySPI_Init(void);
//起始
void MySPI_Start(void);
//终止
void MySPI_Stop(void);
//交换
uint8_t MySPI_WarpByte(uint8_t SendByte);
#endif
W25Q64.c
#include "stm32f10x.h" // Device header
#include "W25Q64.h"
#include "W25Q64_Ins.h"
#include "MySPI.h"
/**
* 函 数:初始化W25Q64
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_Init(void)
{
MySPI_Init();
}
/********************/
/*拼接完整的通信时序*/
/********************/
/**
* 函 数:查看W25Q64的厂商号和设备号
* 参 数:ID* Str 存放了ID结构体的指针
* 返 回 值:无
* 注意事项:接收第八位时是|=
*/
ID W25Q64_ID;//存放设备ID号的结构体
void W25Q64_ReadID(ID* Str)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_JEDEC_ID);//发送读取设备号指令。返回值不要
Str->MID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收厂商ID 从机发来的设备号。发送值随便
Str->DID = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID高八位
Str->DID <<= 8;//把接收到的数据放到高八位
Str->DID |= MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收设备ID低八位
MySPI_Stop();//停止
}
/**
* 函 数:写使能
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_WRITE_ENABLE);//发送
MySPI_Stop();//停止
}
/**
* 函 数:写失能
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WriteDisable(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_WRITE_DISABLE);//发送
MySPI_Stop();//停止
}
/**
* 函 数:等待忙函数
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
void W25Q64_WaitBusy(void)
{
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_READ_STATUS_REGISTER_1); //发送
uint32_t TimeOut = 100000;
while((MySPI_WarpByte(W25Q64_DUMMY_BYTE)&0x01) == 0x01) //读取状态寄存器1的Busy位是否为1,为1则等待
{
TimeOut--;
if(TimeOut == 0)
{
break; //超时退出
}
}
MySPI_Stop(); //停止
}
/**
* 函 数:页编程
* 参 数:Address 要写入那个页地址
*DataArray 存储字节所用的数组
Count 一次写入多少字节
* 返 回 值:无
* 注意事项:一次只能写入最多0-256个字节
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count)//(0-256,所以要16位)
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_PAGE_PROGRAM);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
MySPI_WarpByte(Address >> 0);//发送页地址
uint16_t i = 0;
for(i = 0; i < Count; i++)
{
MySPI_WarpByte(DataArray[i]);//发送Count个数组的第i位
}
MySPI_Stop();//停止
}
/**
* 函 数:页擦除
* 参 数:Address 要擦除那一页
* 返 回 值:无
* 注意事项:最小的擦除单位。4kb 1扇区
*/
void W25Q64_PageErase(uint32_t Address)
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
W25Q64_WriteEnable();//写使能(每次写时都要先写使能)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_SECTOR_ERASE_4KB);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//发送地址
MySPI_WarpByte(Address >> 0);
MySPI_Stop();//停止
}
/**
* 函 数:读取数据
* 参 数:Address 要读取个地址
*DataArray 存储字节所用的数组
Count 一次读取多少字节
* 返 回 值:无
* 注意事项:读取可以无限制读取
*/
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count)//读取没有限制
{
W25Q64_WaitBusy();
//事前等待。(事后等待是先等待再退出,比较保险。 事前等待可以先做别的事,再进去。效率高)
MySPI_Start();//起始
MySPI_WarpByte(W25Q64_READ_DATA);//发送页编程指令
MySPI_WarpByte(Address >> 16 );
MySPI_WarpByte(Address >> 8 );//(接收只能接收8位。会自动舍弃)
MySPI_WarpByte(Address >> 0);//发送页地址
uint32_t i = 0;
for(i = 0; i < Count; i++)
{
DataArray[i] = MySPI_WarpByte(W25Q64_DUMMY_BYTE);//接收Count个字节,放到数组的第i位
}
MySPI_Stop();//停止
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H
//初始化W25Q64
void W25Q64_Init(void);
/*厂商和设备ID号*/
typedef struct ID
{
uint8_t MID;//8位厂商ID
uint16_t DID;//16位设备ID
}ID;
extern ID W25Q64_ID;
//获取厂商和设备号ID
void W25Q64_ReadID(ID* Str);
//页编程
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,uint16_t Count);
//页擦除
void W25Q64_PageErase(uint32_t Address);
//读取
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,uint32_t Count);
#endif
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 "oled.h"
#include "Delay.h"
#include "key.h"
#include "W25Q64.h"
/**
* 函 数:验证SPI控制W25Q64存储器
* 参 数:无
* 返 回 值:无
* 注意事项:无
*/
uint8_t ArrWrite[] = {0x01,0x02,0x03,0x04};
uint8_t ArrRead[4];
int main()
{
Delay_Init();//初始化演示
OLED_Init();//初始化OLED;
W25Q64_Init();//初始化W25Q64存储器
OLED_ShowString(1,1,"MID: ,DID: ");
W25Q64_ReadID(&W25Q64_ID);//读ID放到这个结构体中
OLED_ShowHexNum(1,5,W25Q64_ID.MID,2);
OLED_ShowHexNum(1,12,W25Q64_ID.DID,4);//显示MID DID
OLED_ShowString(2,1,"W:");
OLED_ShowString(3,1,"R:");
W25Q64_PageErase(0x000000); //擦除地址。写入前需要(最好定位到扇区的起始地址(后三位为0))
W25Q64_PageProgram(0x000000,ArrWrite,4); //写入数组中数据到存储器
W25Q64_ReadData(0x000000,ArrRead,4); //读取存储器中数据到数组
OLED_ShowHexNum(2, 3, ArrWrite[0], 2);
OLED_ShowHexNum(2 ,6, ArrWrite[1], 2);
OLED_ShowHexNum(2, 9, ArrWrite[2], 2);
OLED_ShowHexNum(2, 12, ArrWrite[3], 2);
OLED_ShowHexNum(3, 3, ArrRead[0], 2);
OLED_ShowHexNum(3 ,6, ArrRead[1], 2);
OLED_ShowHexNum(3, 9, ArrRead[2], 2);
OLED_ShowHexNum(3, 12, ArrRead[3], 2);
while(1)
{
}
}