SPI

SPI 总线是一种串行通信协议,主要用于短距离通信。本文介绍 STM32 中 SPI 外设的配置和使用方法。

SPI通信

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
  • 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
  • 同步,全双工
  • 支持总线挂载多设备(一主多从)

硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
硬件电路

移位示意图

移位示意图

SPI 通信的基本单位是一位数据的传输。每个 SPI 设备都有一个移位寄存器,用于存储要发送或接收的数据。

在通信过程中:

  1. 主机和从机的移位寄存器通过 MOSI 和 MISO 线相连
  2. 主机通过 SCK 时钟线同步数据传输
  3. 每个时钟周期:
    • 主机通过 MOSI 线发送移位寄存器最高位的数据到从机
    • 从机通过 MISO 线发送移位寄存器最高位的数据到主机
    • 两端的移位寄存器同时左移一位
  4. 8 个时钟周期后完成一个字节的传输

这种移位机制保证了数据的同步传输,并实现了全双工通信。

SPI时序基本单元

起始/终止条件

  • 起始条件:SS从高电平切换到低电平
  • 终止条件:SS从低电平切换到高电平
起始/终止条件

交换一个字节

参数全称作用描述取值具体含义
CPOL时钟极性(Clock Polarity)决定空闲时SCK线的电平状态0SCK空闲为低电平
1SCK空闲为高电平
CPHA时钟相位(Clock Phase)决定数据采样/移位发生在SCK哪个边沿0第一个时钟跳变沿采样数据(第一个边沿采样,第二个边沿移出)
1第一个时钟跳变沿移出数据(第一个边沿移出,第二个边沿采样)

CPOL和CPHA的组合影响数据采样和发送的时序(共4种SPI模式)。

模式0

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

特点:

  • 空闲时钟为低电平,第一个时钟跳变为上升沿
  • 在第一个上升沿采样输入数据
  • 在第二个下降沿更新输出数据
  • 适用于大多数简单的SPI设备
模式0

模式1

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

特点:

  • 空闲时钟为低电平,第一个时钟跳变为上升沿
  • 在第一个上升沿更新输出数据
  • 在第二个下降沿采样输入数据
  • 给设备更多准备数据的时间
模式1

模式2

  • CPOL=1:空闲状态时,SCK为高电平
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

特点:

  • 空闲时钟为高电平,第一个时钟跳变为下降沿
  • 在第一个下降沿采样输入数据
  • 在第二个上升沿更新输出数据
  • 适用于某些高速设备
模式2

模式3

  • CPOL=1:空闲状态时,SCK为高电平
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

特点:

  • 空闲时钟为高电平,第一个时钟跳变为下降沿
  • 在第一个下降沿更新输出数据
  • 在第二个上升沿采样输入数据
  • 常用于高速或复杂的SPI设备
模式3

四种模式总览

四种模式 = CPOL(0/1)× CPHA(0/1)。主机与从机 必须一致,否则采样时刻对不上,读出的数据全错。

模式CPOLCPHA空闲 SCK第 1 个边沿第 2 个边沿
000采样(读 MISO/MOSI)更新(输出下一位)
101更新采样
210采样更新
311更新采样
  • CPOL:SCK 不传输时 保持的电平(0=低,1=高)。
  • CPHA:每个 SCK 周期内,先采样还是先更新 下一位数据。

W25Q64、江协教程、STM32 默认配置均为模式 0。

标准库对应:

/* 模式 0 */
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;

/* 模式 3 示例 */
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;

模式 0 时序详解(重点)

模式 0 空闲 SCK 为 ;每个脉冲内 上升沿采样、下降沿更新

空闲:SCK = 0
CS↓ 后:

SCK:  ___/‾‾‾\___/‾‾‾\___
           ↑       ↑
        采样输入  更新输出(准备下一位)
MOSI:  ──X=======Y=======   ← 主机发出的 bit
MISO:  ──A=======B=======   ← 从机回传的 bit

江协 软件 SPI 与之一致:

MySPI_W_MOSI(位);   /* 先把当前位放到 MOSI */
MySPI_W_SCK(1);     /* 上升沿:采样 MISO */
MySPI_W_SCK(0);     /* 下降沿:为下一位做准备 */

模式 1 与模式 0 空闲电平相同,但 先更新、后采样(顺序相反)。模式 2/3 可理解为「空闲为高」的对应版本。


数据是怎么来的

硬件层:移位寄存器

SPI 传 1 字节 = 主机、从机各有一个 8 位移位寄存器,在 SCK 驱动下 同时移 8 位(通常 MSB 先行):

主机 TX 寄存器 ──MOSI──→ 从机 RX 寄存器
主机 RX 寄存器 ←─MISO──  从机 TX 寄存器
  • MOSI:电平由 主机 决定。
  • MISO:电平由 当前 CS 选中的从机 决定。

SPI 是 全双工:每发出 1 bit,必然同时收到 1 bit;不存在「只发不收」——若只关心发送,接收侧可以丢弃。

主机侧:CPU 到 MOSI

方式发送数据来源接收数据去向
软件 SPISwapByte(0x9F) 的参数函数返回值
硬件 SPISPI_I2S_SendData(SPI1, byte)SPI_I2S_ReceiveData(SPI1)

硬件 SPI 交换 1 字节流程:

1. CPU 写 DR(要发的字节)
2. 硬件按 SCK 从 DR 移出 bit → MOSI
3. 同时从 MISO 采 bit → 接收移位器
4. 8 个时钟后 RXNE=1,CPU 读 DR → 得到从机回传的字节

SCK 由主机产生。只读 Flash 时,主机也必须不断发字节(常为 0xFF dummy)来 产生时钟,从机才能把数据送到 MISO。

从机侧(W25Q64):MISO 上的数据从哪来

读 JEDEC ID 为例:

CS↓
主机 MOSI:  0x9F   0xFF   0xFF   0xFF    ← 命令 + dummy(只为打时钟)
从机 MISO:  —      0xEF   0x40   0x17    ← 厂商 ID + 设备 ID
CS↑
字节序主机发(MOSI)从机回(MISO)来源
第 10x9F 读 ID 命令从机解析中,多为无效位
第 20xFF dummyFlash 送出 Manufacturer ID
第 30xFF dummyDevice ID 高字节
第 40xFF dummyDevice ID 低字节

读存储数据(ReadData)同理:

主机:  0x03  Addr[23:0]  0xFF  0xFF  0xFF ...
从机:  —     —          D0    D1    D2   ...   ← 来自 Flash 该地址的存储单元

前几个字节主机发 命令 + 地址;之后每个 dummy 字节产生 8 个 SCK,Flash 从内部 Nor 存储阵列 读出 1 字节经 MISO 送出,地址自动 +1。

写 Flash(PageProgram)时方向相反:

主机 MOSI:  0x02  Addr...  Data0  Data1 ...
数据路径:   MCU 数组 → MOSI → Flash 页缓冲 → 物理存储

与教程代码的对应

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
    /* ByteSend:CPU 变量/常量 → MOSI */
    ...
    /* MISO 采样结果拼进 ByteReceive */
    return ByteReceive;   /* 从机在本字节 8 个时钟内送回的数据 */
}
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
/* 发 0xFF 仅产生时钟;返回值才是 Flash 读出的 DataArray[i] */

常见误区

误区正确理解
读 Flash「只收不发」必须发 dummy,SCK 才有,MISO 才有有效数据
MOSI 发 0xFF 就是读到 0xFFdummy 是占位;真实数据在 MISO / 返回值
模式 0 和 1 可随便选采样边沿不同,选错则 ID、数据全错
只接 MOSI 就能读 MISO还需 SCK、CS、模式一致,且从机已被选中

SPI时序

发送指令

向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

硬件电路

硬件电路1 硬件电路2
引脚功能
VCC、GND电源(2.7~3.6V)
CS(SS)SPI片选
CLK(SCK)SPI时钟
DI(MOSI)SPI主机输出从机输入
DO(MISO)SPI主机输入从机输出
WP写保护
HOLD数据保持

W25Q64框图

W25Q64框图

Flash操作注意事项

  • 写入操作时:
    • 写入操作前,必须先进行写使能
    • 每个数据位只能由1改写为0,不能由0改写为1
    • 写入数据前必须先擦除,擦除后,所有数据位变为1
    • 擦除必须按最小擦除单元进行
    • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
    • 写入操作结束后,芯片进入忙状态,不响应新的读写操作
  • 读取操作时:
    • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

SPI外设简介

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
  • 可配置8位/16位数据帧、高位先行/低位先行
  • 时钟频率:fSPI=fPCLK2n+1f_{SPI} = \frac{f_{PCLK}}{2^{n+1}},其中n=0,1,2,3,4,5,6,7
  • 支持多主机模型、主或从操作
  • 可精简为半双工/单工通信
  • 支持DMA
  • 兼容I2S协议

SPI框图

SPI框图

SPI初始化结构体

SPI初始化结构体

主模式全双工连续传输

主模式全双工连续传输

非连续传输

非连续传输

软件/硬件波形对比

软件波形 硬件波形

实例

软件SPI读写W25Q64

MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H

void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);

#endif
MySPI.c
#include "stm32f10x.h"                  // Device header

/*引脚配置层*/

/**
  * 函    数:SPI写SS引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI写SCK引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
  */
void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);		//根据BitValue,设置SCK引脚的电平
}

/**
  * 函    数:SPI写MOSI引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue为1时,需要置MOSI为高电平
  */
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);		//根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}

/**
  * 函    数:I2C读MISO引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
  */
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);			//读取MISO电平并返回
}

/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);					//将PA4、PA5和PA7引脚初始化为推挽输出
	
	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);					//将PA6引脚初始化为上拉输入
	
	/*设置默认电平*/
	MySPI_W_SS(1);											//SS默认高电平
	MySPI_W_SCK(0);											//SCK默认低电平
}

/*协议层*/

/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);				//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);				//拉高SS,终止时序
}

/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	
	for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据
	{
		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
		MySPI_W_MOSI(!!(ByteSend & (0x80 >> i)));	//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
		MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据
		if (MySPI_R_MISO()){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量
															//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
		MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据
	}
	
	return ByteReceive;								//返回接收到的一个字节数据
}
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
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

#endif
W25Q64.c
#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

/**
  * 函    数:W25Q64初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
	MySPI_Init();					//先初始化底层的SPI
}

/**
  * 函    数:W25Q64读取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终止
}
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功能函数测试*/
	W25Q64_SectorErase(0x000000);					//扇区擦除
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
	
	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读写W25Q64

MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_H

void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);

#endif
MySPI.c
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:SPI写SS引脚电平,SS仍由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}

/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
	
	/*GPIO初始化*/
	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);					//将PA4引脚初始化为推挽输出
	
	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);					//将PA5和PA7引脚初始化为复用推挽输出
	
	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);					//将PA6引脚初始化为上拉输入
	
	/*SPI初始化*/
	SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
	SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
	SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
	
	/*SPI使能*/
	SPI_Cmd(SPI1, ENABLE);									//使能SPI1,开始运行
	
	/*设置默认电平*/
	MySPI_W_SS(1);											//SS默认高电平
}

/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);				//拉低SS,开始时序
}

/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);				//拉高SS,终止时序
}

/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
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);								//读取接收到的数据并返回
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

#endif
W25Q64.c
#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

/**
  * 函    数:W25Q64初始化
  * 参    数:无
  * 返 回 值:无
  */
void W25Q64_Init(void)
{
	MySPI_Init();					//先初始化底层的SPI
}

/**
  * 函    数:W25Q64读取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终止
}
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功能函数测试*/
	W25Q64_SectorErase(0x000000);					//扇区擦除
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
	
	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)
	{
		
	}
}