目录
STM32通过SPI硬件读写W25Q64
1. STM32的SPI外设简介
2. STM32的SPI框图
2.1 数据寄存器和移位寄存器(左上角部分)
控制逻辑(其余右下角的部分)
3.STM32的SPI基本框图
4. STM32的SPI主模式全双工连续传输 时序图
5. STM32的SPI主模式全双工非连续传输 时序图
6. STM32非连续传输时,不同时钟速率的区别
256分频
128分频
64分频
2分频(32MHZ)
7. 编写SPI硬件读写W25Q64注意事项
8. STM32的SPI外设相关库函数(只看SPI的)
7.编写SPI硬件读写W25Q64步骤
8. 编写STM32SPI主模式全双工非连续传输
8.1 步骤
8.2 程序文件简要说明:
MySPI.c(相对软件,硬件仅需修改此文件)
MySPI.h
W25Q64.c
W25Q64.h
W25Q64_Ins.h
main.c
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)
{
}
}