STM32学习笔记(十一)丨SPI通信(W25Q64芯片简介,使用SPI读写W25Q64存储器芯片)

本文详细介绍了SPI通信协议的原理,包括SPI的四种模式、起始和终止条件以及指令操作。接着,讨论了STM32的SPI外设,包括其结构和不同传输模式。文章还详细阐述了W25Q64存储器芯片的工作原理和操作注意事项,如写入和读取操作。最后,提供了软件模拟和基于STM32硬件SPI的代码实现示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇文章包含的内容

  • 一、SPI的通信协议及其原理
    • 1.1 SPI简介
    • 1.2 SPI通信的硬件连接
    • 1.3 SPI的时序基本单元
      • 1.3.1 起始条件和终止条件
      • 1.3.2 交换字节(模式0,先移入,再移出)
      • 1.3.3 交换字节(模式1,先移出,再移入)
      • 1.3.4 交换字节(模式2,对应模式0,SCK极性取反)
      • 1.3.5 交换字节(模式3,对应模式1,SCK极性取反)
    • 1.4 SPI的指令操作
  • 二、STM32的SPI通信外设
    • 2.1 SPI外设简介
    • 2.2 SPI外设结构
    • 2.3 主模式全双工连续传输时序
    • 2.4 非连续传输时序
  • 三、W25Q64存储器芯片
    • 3.1 W25Q64简介及其工作原理
    • 3.2 Flash操作注意事项
      • 3.2.1 写入操作
      • 3.2.2 读取操作
  • 四、代码实现
    • 4.1 软件模拟SPI
    • 4.2 基于SPI外设实现硬件SPI

​  本次课程采用单片机型号为STM32F103C8T6。
​  课程链接:江协科技 STM32入门教程


  往期笔记链接:
  STM32学习笔记(一)丨建立工程丨GPIO 通用输入输出
  STM32学习笔记(二)丨STM32程序调试丨OLED的使用
  STM32学习笔记(三)丨中断系统丨EXTI外部中断
  STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)
  STM32学习笔记(五)丨TIM定时器及其应用(输出比较丨PWM驱动呼吸灯、舵机、直流电机)
  STM32学习笔记(六)丨TIM定时器及其应用(输入捕获丨测量PWM波形的频率和占空比)
  STM32学习笔记(七)丨TIM定时器及其应用(编码器接口丨用定时器实现编码器测速)
  STM32学习笔记(八)丨ADC模数转换器(ADC单、双通道转换)
  STM32学习笔记(九)丨DMA直接存储器存取(DMA数据转运、DMA+AD多通道转换)
  STM32学习笔记(十)丨I2C通信(使用I2C实现MPU6050和STM32之间通信)


一、SPI的通信协议及其原理

1.1 SPI简介

​  SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)。
​  SPI通信具有以下特点:

  • 同步,全双工;
  • 支持总线挂载多设备(SPI仅支持一主多从);
  • 在不同情况下,通信线的名称可能有所变化:
    • SCK:SCLK、CLK、CK;
    • MOSI:DI(对从机而言);
    • MISO:DO(对从机而言);
    • SS:CS(Chip Select)、NSS(Not Slave Select);
  • SPI通信的SS线可以有多条,即对每一个从机而言都有单独的从机选择线,一般为低电平有效。

1.2 SPI通信的硬件连接

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚,同一时间,主机只能选择一条SS线为低电平
  • 输出引脚配置为推挽输出(推挽输出能使SPI的通信速度轻松达到MHz的级别),输入引脚配置为浮空或上拉输入。当从机未被SS线选中时,从机的输出引脚MISO必须呈现高阻态,以防止数据冲突。

在这里插入图片描述
​  简化了数据输出寄存器的SPI通信移位示意图如下图所示。下面的图演示了主机和从机同时执行一个字节的字节交换的过程。实际上,如果只想发送不想接收,可以在执行这个时序后只关心输出,不关心从机输入的数据;如果只想接收不想发送,可以“随便”发送一个数据,关心被交换过来的从机的数据即可。
在这里插入图片描述
在这里插入图片描述
​  在只执行发送或只执行接收的情况下,SPI通信会存在通信资源浪费的情况。但是这种浪费是全双工通信的通病,对于SPI通信这个“富家子弟”而言,有一点浪费对其的影响是微乎其微的。

1.3 SPI的时序基本单元

1.3.1 起始条件和终止条件

  1. 起始条件:SS从高电平切换到低电平
  2. 终止条件:SS从低电平切换到高电平

1.3.2 交换字节(模式0,先移入,再移出)

​  SPI外设的通信模式由控制寄存器中的CPOL(决定空闲时SCK的电平)和CPHA(时钟相位,决定第几个边沿采样)两个位控制。实际应用时,模式0的应用最广泛。之后的实验也基于模式0进行。

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=0:SCK第一个边沿移入数据(进行电平检测),第二个边沿移出数据(将数据移到数据输出寄存器)。但是数据必须要先移出,再移入,所以在SS的下降沿时 ,主机就已经将数据输出到MOSI上了,所以这里可以理解为第0个SCK边沿移出,第1个SCK边沿移入。
    在这里插入图片描述

1.3.3 交换字节(模式1,先移出,再移入)

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=1:SCK第一个边沿移出数据(将数据移到数据输出寄存器),第二个边沿移入数据(第二个边沿进行电平检测)
    在这里插入图片描述

1.3.4 交换字节(模式2,对应模式0,SCK极性取反)

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

在这里插入图片描述

1.3.5 交换字节(模式3,对应模式1,SCK极性取反)

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

在这里插入图片描述

1.4 SPI的指令操作

​  可以看到,SPI的通信相比于I2C而言是十分简单的。所以对于从机而言,不同的设备可以根据不同的需求定义指令集,有些指令仅需要一个字节就可完成,有些指令需要在操作指令后跟对应读写的数据。对应这些指令的操作,不同的设备都可以自由定义。

二、STM32的SPI通信外设

2.1 SPI外设简介

​  STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担

  • 可配置8位/16位数据帧、高位先行/低位先行
  • 时钟频率: f P C L K f_{PCLK} fPCLK / (2, 4, 8, 16, 32, 64, 128, 256),ABP2的 f P C L K f_{PCLK} fPCLK是72MHz,ABP1的 f P C L K f_{PCLK} fPCLK是36MHz。
  • 支持多主机模型、主或从操作(了解即可)
  • 可精简为半双工/单工通信(了解即可)
  • 支持DMA
  • 兼容I2S协议(I2S协议是一种数字音频信号传输的专用协议,它和SPI协议有一些共有的特征)

​  STM32F103C8T6 硬件SPI资源:SPI1(APB2)、SPI2(APB1)

2.2 SPI外设结构

在这里插入图片描述
​  读取和接收数据的流程和串口相似。如果有连续的数据需要发送,则需要检查TxE(发送寄存器空)标志位是否为1;如果要接收连续的数据,就需要检查RxNE(接收寄存器非空)是否为1。检测到RxNE为1后,需要尽快读出,否则接收寄存器的数据可能被覆盖。
​  上图中的NSS引脚是为了和多主机模型配合而设计的引脚,它和SPI通信协议中的SS引脚不同。实际上,在具体实现时,如果有多个设备,就需要多个SS线,一条NSS线显然无法满足需求。所以这里协议中要求的SS线用GPIO模拟即可。
​  下图展示了SPI外设在高位先行的情况下的简化结构。
在这里插入图片描述

2.3 主模式全双工连续传输时序

在这里插入图片描述
​  上图展示的是SPI模式3,低位先行下主模式连续传输(发送和接收)的时序图。
​  在实际应用中,由于这种方式的配置相对复杂,如果不是追求极致的传输效率性能要求,一般采用下面的方法进行数据传输:

2.4 非连续传输时序

在这里插入图片描述
​  非连续传输对于软件设计十分友好,仅需要四行代码就可以完成。它和连续传输的区别在于TxE为1后不立刻写入TDR,而是等待数据交换完成后,读取RDR,之后再写入下一个字节到TDR。只要稍作修改,就可以把软件SPI改为硬件SPI。

三、W25Q64存储器芯片

3.1 W25Q64简介及其工作原理

在这里插入图片描述

​  W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储(汉字字库的点阵数据)、固件程序存储等场景。

  • 存储介质:Nor Flash(闪存)(闪存分为Nor Flash和Nand Flash,这里不再详细展开)
  • 时钟频率:80MHz / 160MHz (双重SPI,Dual SPI,一个时钟周期发送/接收2位,这里的频率是等效的频率) / 320MHz (四重SPI,Quad SPI,4位并行)
  • 存储容量(24位地址):
    • W25Q40: 4Mbit / 512KByte
    • W25Q80: 8Mbit / 1MByte
    • W25Q16: 16Mbit / 2MByte
    • W25Q32: 32Mbit / 4MByte
    • W25Q64: 64Mbit / 8MByte
    • W25Q128: 128Mbit / 16MByte
    • W25Q256: 256Mbit / 32MByte
      在这里插入图片描述

在这里插入图片描述

  • VCC,GND:电源(2.7~3.6V)
  • CS(SS)
  • CLK(SCK)
  • DI(MOSI)
  • DO(MISO)
  • WP:写保护,低电平有效
  • HOLD:数据保持。在正常数据读写时,产生中断,想让SPI通信线去操作其他设备,且不想终止SPI总线的时序,这时就可以将HOLD引脚置低电平,这时芯片的状态和时序都将保持,并释放总线。之后完成操作后,将HOLD总线置回高电平,继续时序。

      W25Q64的内部框图如下图所示。对于W25Q64的存储器组织,8MB划分为128个块(Block),每个块又划分为16个扇区(Sector);而总的8MB地址空间又可以划分为很多的页(Page),每一页有256个字节。按这样的规律划分存储地址,可以更高效地管理内存。
    在这里插入图片描述

3.2 Flash操作注意事项

3.2.1 写入操作

  • 写入操作前,必须先进行写使能
  • 每个数据位只能由1改写为0,不能由0改写为1(成本和技术原因)
  • 写入数据前必须先擦除,擦除后,所有数据位变为1(Flash有专门的擦除命令,操作时仅需要发送擦除命令即可),在Flash中0FFH代表空白
  • 擦除必须按最小擦除单元(在本芯片中,最小的擦除单元是一个扇区Sector)进行
  • 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入(页缓存器的限制),在写入时,要注意写入的地址范围不能跨越页尾
  • 写入操作结束后,芯片进入忙状态,不响应新的读写操作

3.2.2 读取操作

  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

四、代码实现

4.1 软件模拟SPI

  • 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

#define 	MySPI_MOSI_GPIO_CLK			RCC_APB2Periph_GPIOA
#define 	MySPI_MOSI_GPIO				GPIOA
#define 	MySPI_MOSI_GPIO_Pin			GPIO_Pin_7

#define 	MySPI_MISO_GPIO_CLK			RCC_APB2Periph_GPIOA
#define 	MySPI_MISO_GPIO				GPIOA
#define 	MySPI_MISO_GPIO_Pin			GPIO_Pin_6

#define 	MySPI_SCLK_GPIO_CLK			RCC_APB2Periph_GPIOA
#define 	MySPI_SCLK_GPIO				GPIOA
#define 	MySPI_SCLK_GPIO_Pin			GPIO_Pin_5

#define		MySPI_SS_GPIO_CLK			RCC_APB2Periph_GPIOA
#define 	MySPI_SS_GPIO				GPIOA
#define 	MySPI_SS_GPIO_Pin			GPIO_Pin_4

/**
  * @brief  改变SS电平
  * @param  BitValue	改变的目标值,0为低电平,1为高电平
  * @retval 无
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(MySPI_SS_GPIO, MySPI_SS_GPIO_Pin, (BitAction)BitValue);
}

/**
  * @brief  改变SCLK电平
  * @param  BitValue	改变的目标值,0为低电平,1为高电平
  * @retval 无
  */
void MySPI_W_SCLK(uint8_t BitValue)
{
	GPIO_WriteBit(MySPI_SS_GPIO, MySPI_SCLK_GPIO_Pin, (BitAction)BitValue);
}

/**
  * @brief  改变MOSI电平
  * @param  BitValue	改变的目标值,0为低电平,1为高电平
  * @retval 无
  */
void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(MySPI_SS_GPIO, MySPI_MOSI_GPIO_Pin, (BitAction)BitValue);
}

/**
  * @brief  读取MISO电平
  * @param  无
  * @retval 读取到的逻辑电平值
  */
uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(MySPI_MISO_GPIO, MySPI_MISO_GPIO_Pin);
}

/**
  * @brief  软件SPI的GPIO初始化函数,更换GPIO时仅需要更改文件开始的宏定义即可
  * @param  无
  * @retval 无
  */
void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(MySPI_MOSI_GPIO_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(MySPI_MISO_GPIO_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(MySPI_SCLK_GPIO_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(MySPI_SS_GPIO_CLK, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;		// 主机输入,上拉输入
	GPIO_InitStructure.GPIO_Pin = MySPI_MISO_GPIO_Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(MySPI_MISO_GPIO, &GPIO_InitStructure);
	
	// 其余三个引脚均为推挽输出
	// MOSI
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = MySPI_MOSI_GPIO_Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(MySPI_MOSI_GPIO, &GPIO_InitStructure);
	
	// SCLK
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = MySPI_SCLK_GPIO_Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(MySPI_SCLK_GPIO, &GPIO_InitStructure);
	
	// SS
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = MySPI_SS_GPIO_Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(MySPI_SS_GPIO, &GPIO_InitStructure);
	
	MySPI_W_SS(1);
	MySPI_W_SCLK(0);
}

/**
  * @brief  生成SPI的起始信号
  * @param  无
  * @retval 无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

/**
  * @brief  生成SPI的结束信号
  * @param  无
  * @retval 无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/**
  * @brief  交换数据函数
  * @param  ByteSend	发送到从机的数据,长度为一个字节
  * @retval 接收到的数据,长度为一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));		// 在下降沿,把数据移到MOSI总线上
		
		MySPI_W_SCLK(1);					// 上升沿读取数据
		if (MySPI_R_MISO() == 1)
		{
			ByteReceive |= (0x80 >> i);		// 掩码提取数据
		}
		MySPI_W_SCLK(0);					// 下降沿
	}
	
	return ByteReceive;
}

  • 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_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.c
#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"

/**
  * @brief  芯片读写初始化函数
  * @param  无
  * @retval 无
  */
void W25Q64_Init(void)
{
	MySPI_Init();
}

/**
  * @brief  读取设备ID
  * @param  MID		指向厂商ID变量的指针,厂商ID为8位ID变量 
  * @param  DID		指向设备ID变量的指针,设备ID为16位变量
  * @retval 无
  */
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	
	MySPI_SwapByte(W25Q64_JEDEC_ID);		// 读ID号指令
	
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 厂商ID,默认为0xEF
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 设备ID,表示存储类型,默认为0x40
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);	// 设备ID,表示容量,默认为0x17
	
	MySPI_Stop();
}

/**
  * @brief  发送写使能命令
  * @param  无
  * @retval 无
  */
void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

/**
  * @brief  带超时的等待忙状态函数
  * @param  无
  * @retval 无
  */
void W25Q64_WaitBusyWithTimeout(void)
{
	uint32_t Timeout = 100000;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 1)	// 利用连续读出状态寄存器,实现等待Busy的功能
	{
		Timeout --;
		if (Timeout == 0)
		{
			/* 可以在这里添加超时错误函数 */
			break;
		}
	}
	MySPI_Stop();
}

/**
  * @brief  页编程(写入)函数
  * @param  Address		写入目标的24位首地址,连续写入时地址指针自动增1
  * @param  DataArray	写入数组的地址指针
  * @param  Count		写入数据的长度
  * @retval 无	
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	
	W25Q64_WriteEnable();		// 时序结束后W25Q64会自动写失能
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);		// 自动舍弃高位
	MySPI_SwapByte(Address);			// 自动舍弃高位
	for (i = 0; i < Count; i ++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
	
	W25Q64_WaitBusyWithTimeout();
}

/**
  * @brief  页擦除函数,在执行写入操作前要进行擦除
  * @param  Address		擦除页的首地址
  * @retval 无
  */
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();		// 时序结束后W25Q64会自动写失能
	
	MySPI_Start();
	
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);		// 自动舍弃高位
	MySPI_SwapByte(Address);			// 自动舍弃高位
	MySPI_Stop();
	
	W25Q64_WaitBusyWithTimeout();		// 事后等待,优点是函数之外存储器一定不忙,缺点是会牺牲一点代码执行效率
}


/**
  * @brief  读取数据函数
  * @param  Address		读取目标的24位首地址,连续写入时地址指针自动增1
  * @param  DataArray	存放数据的数组的地址指针
  * @param  Count		读取数据的长度
  * @retval 无	
  */
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);		// 自动舍弃高位
	MySPI_SwapByte(Address);			// 自动舍弃高位
	
	for (i = 0; i < Count; i ++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	
	MySPI_Stop();
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t ArrayWrite[] = {0x55, 0x66, 0x77, 0x88};
uint8_t ArrayRead[4] = {0};

int main()
{
	OLED_Init();
	
	W25Q64_Init();
	W25Q64_ReadID(&MID, &DID);
	
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	OLED_ShowHexNum(1, 5, MID, 2);
	OLED_ShowHexNum(1, 12, DID, 4);
	
	W25Q64_SectorErase(0x000000);		// 擦除扇区的起始地址
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	// 写入数据
	
	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)
	{

	}
}

4.2 基于SPI外设实现硬件SPI

​  在硬件中,任然采用分层管理的思想,但是这里我们采用非连续传输的时序,只需要在软件SPI的基础上更改MySPI.c中底层通信协议的代码即可。

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

#define		MySPI_SS_GPIO_CLK			RCC_APB2Periph_GPIOA
#define 	MySPI_SS_GPIO				GPIOA
#define 	MySPI_SS_GPIO_Pin			GPIO_Pin_4

/**
  * @brief  GPIO改变SS电平
  * @param  BitValue	改变的目标值,0为低电平,1为高电平
  * @retval 无
  */
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(MySPI_SS_GPIO, MySPI_SS_GPIO_Pin, (BitAction)BitValue);
}

/**
  * @brief  硬件SPI的初始化函数
  * @param  无
  * @retval 无
  */
void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(MySPI_SS_GPIO_CLK, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = MySPI_SS_GPIO_Pin;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(MySPI_SS_GPIO, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;	// SPI1的SCK和MOSI
	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;				// SPI1的MISO
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	// f_SCLK = 72MHz / 128
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;		// SCLK的第一个边沿采样(移入)
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;			// SCLK的极性选择
	SPI_InitStructure.SPI_CRCPolynomial = 7;			// CRC校验的默认值
	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;		// 选择STM32为主机
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;			// 选择NSS为软件配置还是硬件配置(这里不用)
	SPI_Init(SPI1, &SPI_InitStructure);
	
	SPI_Cmd(SPI1, ENABLE);
	
	MySPI_W_SS(1);
}

/**
  * @brief  生成SPI的起始信号
  * @param  无
  * @retval 无
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

/**
  * @brief  生成SPI的结束信号
  * @param  无
  * @retval 无
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/**
  * @brief  硬件交换数据函数
  * @param  ByteSend	发送到从机的数据,长度为一个字节
  * @retval 接收到的数据,长度为一个字节
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	// 等待TxE为1
	
	SPI_I2S_SendData(SPI1, ByteSend);
	
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	// 发送完成即接收完成,等待RxNE为1
	
	return SPI_I2S_ReceiveData(SPI1);
}


​  课程链接:江协科技 STM32入门教程,欢迎大家一起交流学习。


  原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、机器学习方面的学习笔记~

03-11
### W25Q64 Flash Memory Specifications The W25Q64 is a serial interface, low-power, 8-Megabyte (64 Mbit) flash memory device that supports standard SPI protocols as well as dual and quad I/O modes to increase data throughput[^1]. The device operates from a single 2.7V to 3.6V supply with an extended temperature range of -40°C to +85°C. #### Key Features - **Capacity**: 8 Megabytes (64 Mbits), organized into pages of 256 bytes each. - **Interfaces Supported**: - Standard SPI mode at up to 104 MHz clock rate. - Dual-SPI mode doubling the transfer speed by using two data lines simultaneously. - Quad-SPI mode quadruples the transfer speed through four data lines concurrently. - **Power Consumption**: - Active current consumption ranges between 3 mA and 9 mA depending on frequency and operation type. - Standby power consumption less than 1 μA when placed in deep power-down mode. #### Usage Considerations For applications requiring high-speed data access or boot code storage, the W25Q64 offers fast read/write performance along with reliable endurance characteristics suitable for industrial environments[^2]. When integrating this component within embedded systems design projects, developers should consider implementing error correction mechanisms such as ECC (Error Correction Code). This ensures robustness against potential bit errors during long-term use under varying environmental conditions. ```cpp // Example C++ code snippet demonstrating how to initialize communication with W25Q64 over SPI bus #include <SPI.h> void setup() { pinMode(SS_PIN, OUTPUT); digitalWrite(SS_PIN, HIGH); // Ensure SS line starts high SPI.begin(); } void loop() { // Select slave device digitalWrite(SS_PIN, LOW); // Send command sequence here... // Deselect slave device after transaction completes digitalWrite(SS_PIN, HIGH); } ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Include everything

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值