一、W25Q64简介
1、W25Q64的内存空间结构: 一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。
2、W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。
3、擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。
二、电路图
1、软件模拟的SPI:线可以任意接
2、硬件模拟的SPI:要按以下方式连接
3、本次软件模拟和硬件模拟使用同一个电路图,方便切换
CS(片选):PA4 DO(从机输出):PA6
CLK(时钟):PA5 DI(从机输入):PA7
三、软件SPI读写W25Q64
1、SPI.c(初始化寄存器,实现读取一个字节的功能)
#include "stm32f10x.h" // Device header
void MySPI_W_SS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
void MySPI_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_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
MySPI_W_SS(1); //SS默认高电平(下降沿为开始工作,低电平状态为工作中,上升沿为结束工作)
MySPI_W_SCK(0); //SCK默认为低电平(上升沿移入数据,下降沿移出数据)
}
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
//模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00;
for (i = 0; i < 8; i ++)
{
//先SS下降沿,移出数据,SCK上升沿,移入数据,再SCK下降沿,移出数据,下面只管主机,
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //将数据移出到MOSI线
MySPI_W_SCK(1); //上升沿移入数据
//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //将移入的数据读取出来
MySPI_W_SCK(0); //下降沿移出数据
}
return ByteReceive; //读取出来的数据
}
/*
1、for循环的优化
MySPI_W_MOSI(ByteSend & 0x80 ); //将数据移出到MOSI线
ByteSend << 1; //将数据左移动1位,去掉最高位,最低位置0
MySPI_W_SCK(1); //上升沿移入数据
if (MySPI_R_MISO() == 1){ByteSend |= 0x01;} //将移入的数据读取出来,如果是0不管,如果是1,将最低位置1
MySPI_W_SCK(0); //下降沿移出数据
2、模式1
MySPI_W_SCK(1); //上升沿移出来数据
MySPI_W_MOSI(ByteSend & (0x80 >> i)); //将数据移出到MOSI线
MySPI_W_SCK(0); //下降沿移入数据
if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //将移入的数据读取出来
*/
2、W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"
/**
* 函 数:W25Q64初始化
* 参 数:无
* 返 回 值:无
*/
void W25Q64_Init(void)
{
MySPI_Init(); //先初始化底层的SPI
}
/**
* 函 数:MPU6050读取ID号
* 参 数:MID 工厂ID,使用输出参数的形式返回
* 参 数:DID 设备ID,使用输出参数的形式返回
* 返 回 值:无
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位
*DID <<= 8; //高8位移到高位
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回
MySPI_Stop(); //SPI终止
}
/**
* 函 数:W25Q64写使能
* 参 数:无
* 返 回 值:无
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令
MySPI_Stop(); //SPI终止
}
/**
* 函 数:W25Q64等待忙
* 参 数:无
* 返 回 值:无
*/
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令
Timeout = 100000; //给定超时计数时间
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
MySPI_Stop(); //SPI终止
}
/**
* 函 数:W25Q64页编程
* 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
* 参 数:DataArray 用于写入数据的数组
* 参 数:Count 要写入数据的数量,范围:0~256
* 返 回 值:无
* 注意事项:写入的地址范围不能跨页
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i;
W25Q64_WriteEnable(); //写使能
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据
}
MySPI_Stop(); //SPI终止
W25Q64_WaitBusy(); //等待忙
}
/**
* 函 数:W25Q64扇区擦除(4KB)
* 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
* 返 回 值:无
*/
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable(); //写使能
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
MySPI_Stop(); //SPI终止
W25Q64_WaitBusy(); //等待忙
}
/**
* 函 数:W25Q64读取数据
* 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
* 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回
* 参 数:Count 要读取数据的数量,范围:0~0x800000
* 返 回 值:无
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
uint32_t i;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据
}
MySPI_Stop(); //SPI终止
}
3、main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
uint8_t MID; //定义用于存放MID号的变量
uint16_t DID; //定义用于存放DID号的变量
uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
W25Q64_Init(); //W25Q64初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "MID: DID:");
OLED_ShowString(2, 1, "W:");
OLED_ShowString(3, 1, "R:");
/*显示ID号*/
W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号
OLED_ShowHexNum(1, 5, MID, 2); //显示MID
OLED_ShowHexNum(1, 12, DID, 4); //显示DID
W25Q64_SectorErase(0x000000); //扇区擦除
/*
验证扇区擦除功能(方法:注释掉这一句)
如果不擦除,一开始写入AA、BB、CC、DD,后面再次写入55、66、77、88,则读出来00、22、44、88
即如果不进行擦除,则读出的数据=原始数据&写入的数据
*/
W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中
W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中
/*
数据是不能跨页写入
验证:
若写入55 66 77 88
W25Q64_PageProgram(0x0000FF, ArrayWrite, 4); //数据不能跨页写入,66 77 88返回到页首写入
W25Q64_ReadData(0x0000FF, ArrayRead, 4); //读取数据可以跨页读出
则读出的是55 FF FF FF ,FF为第二页的数据,第二页是擦除了的,没有写入,默认是FF
W25Q64_PageProgram(0x0000FF, ArrayWrite, 4);
W25Q64_ReadData(0x000000, ArrayRead, 4); //读出:66 77 88 FF
*/
/*显示数据*/
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)
{
}
}
四、硬件读写I2C
只需要在软件的基础上添加以下的代码
#include "stm32f10x.h" // Device header
void MySPI_W_SS(uint8_t BitValue)//SS还是软件模拟
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
/*
初始化步骤
1、开启时钟
2、初始化GPIO口
(1)SCK、MOSI是由硬件外设控制的输出信号:复用推挽输出
(2)MISO是硬件外设的输入信号:上拉输入(输入设备可以有很多个,不存在复用输入)
(3)SS为软件控制的输出信号,配置为通用推挽输出
3、配置SPI外设: 用结构体
4、开关控制:调用SPI_Cmd,给SPI使能
*/
void MySPI_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_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_Mode = SPI_Mode_Master; //主机
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //双线全双工
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //8位数据帧
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率预分频器,配置SCK时钟的频率.SPI1:72MHz/128,SPI2:36MHz/128
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //默认低电平,空闲默认低电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //第一个边沿开始采样(移入)
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //一般选择软件NSS模式(不用了解)
SPI_InitStructure.SPI_CRCPolynomial = 7; //随便填
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
MySPI_W_SS(1);//默认不选中从机
}
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
/*
等待TXE为1,发送寄存器为空,发送寄存器不为空,先不着急写
过程:
写入数据到TDR-->转移到移位寄存器——>一旦移位寄存器有数据,时序波形就会自动产生(则ByteSend就会通过MOSI一位一位地移出去)
——>在MOSI线上,就自动产生发送的时序波形
由于是非连续传输,时序产生的时间内,不必提前把下个数据放到TDR,直接等待这段时间过去就行
在发送的同时,MISO会移位进行接收
发送和接收是同步
接收移位完成时,会收到一个字节数据这时会置标志位置RXNE
*/
/*
步骤总结(完成一个字节的交换)
1、等待TXE为1
2、写发送的数据至TDR,一旦TDR写出数据来。时序就会自动生成
3、等待RXNE为1,发送完成,即接收完成,RXNE置1
4、读取RDR接收的数据,就是置换接收的一个字节
注意:(1)必须是发送,同时接收,要先写东西,触发时序
(2)根据手册,
发送缓冲器空闲标志(TXE):写入DR时,会顺便执行清楚TXE的操作,无须手动清除
接收缓冲器非空(RXNE):读取SPI数据寄存器可以清除此标志
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//若没有数据,则MOSI线为SET
SPI_I2S_SendData(SPI1, ByteSend);//ByteSend为要写入到DR,即TDR的数据(要发送的数据),之后会转入到移位寄存器
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//接收数据完毕的时候,MISO线有标志位RXNE
return SPI_I2S_ReceiveData(SPI1);//读取DR,从RDR中,把交换接收的数据读取出来,返回值为RDR接收的数据
}