STM32 硬件spi通信篇:读写W25Q128

SPI外设简介

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
  • 可配置8/16位数据帧、高位先行/低位先行,常用8位&高位先行
  • 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
  • 支持多主机模型、主或从操作
  • 可精简为半双工/单工通信
  • 支持DMA
  • 兼容I2S协议

SPI框图

         左上角移位寄存器,右边的数据低位一位一位从MOSI移出,左边的数据高位一位一位的从MISO移入(低位先行);

        其左侧MOSI和MISO在作为主机时,方框中的交叉线无需使用,MOSI输出,MISO输入;做从机时,MOSI从机输入后经过交叉进入移位寄存器,MISO从机输出,数据从移位寄存器经过交叉后输出到MISO(箭头应该画反了,也许是往下指的);

        移位寄存器上下的接收缓存区RDR和发送缓存区TDR编程上为同一个地址,统一为DR;在具体数据流转运的过程中(作为主机),要发送的数据先进入发送缓存区TDR,当移位寄存器没有数据移位时,TDR中的数据整体转入移位寄存器,并开始移位至MOSI,同时MISO移入移位寄存器,随时钟信号进行一字节的数据交换,在TDR数据转入移位寄存器时,状态寄存器TXE会立刻置1,随后TDR传入下一字节数据;当移位寄存器中一字节数据交换完毕后,移位寄存器中的接收数据会转入接收缓存区RDR中,并置状态寄存器RXNE为1,表示接受寄存器非空,此时需要在下一个数据到来之前读出RDR的数据;

        需注意,TDR用于发送数据,RDR用于接收数据,它们在内核硬件内部是不同的物理寄存器,编程时映射到同一个地址,这是通过外设的硬件逻辑设计实现的。

/************************************************************************************/

与IIC、USART对比:

由于SPI为全双工,发送和接收同步进行,故数据寄存器缓冲区,发送和接收是分离的,移位寄存器则是共用的;

IIC为半双工,发送和接收不同时进行,故发送和接收缓冲区、移位寄存器都是共用的

串口为全双工,发送和接收异步进行,故发送和接收缓冲区、移位寄存器都是分离的

/************************************************************************************/

        波特率发生器用于产生SCK时钟,内部为分频器;右边的SPI_CR1寄存器的三个位:BR0、BR1、BR2用于控制分频系数

        SPI_CR1寄存器:LSB FIRST 决定高位/低位先行        SPE 为SPI使能位

                                     BR 配置波特率                                 MSTR 配置主从模式 1主0从

                                     CPOL、CPHA 选择SPI四种模式     NSS 从机选择,低电平有效,一般用于多主机选择

                                     TEX 发送寄存器空                           RXNE 接受寄存器非空

硬件SPI移位示意图、基本时序单元、模式选择与软件SPI一致:

STM32 软件spi通信篇

硬件SPI运行时序图

主模式全双工连续传输(下图为模式3):

发送运行逻辑:

1、TXE为1表示TDR为空,软件写入数据(0xF1)至TDR寄存器(发送数据缓存区),TXE为0,此时移位寄存器无数据,故TDR中的数据立刻转入移位寄存器并开始发送;

2、数据(0xF1)转入移位寄存器后,TXE置1表示TDR为空,软件写入下一数据(0xF2)至TDR寄存器(发送数据缓存区),等待移位寄存器中的数据发送/接收完毕;

3、移位寄存器中的数据发送/接收完毕后,TDR中的数据(0xF2)立刻转入移位寄存器并开始发送,同时TXE置1,软件写入下一数据(0xF3)至TDR寄存器(发送数据缓存区),等待移位寄存器中的数据发送/接收完毕,以此类推;

接收运行逻辑:

1、在第一个字节(0xF1)发送完毕时,移位寄存器中,第一个字节(0xA1)的接收也同步完成,此时移位寄存器的数据(0xA1)整体转入RDR,同时软件等待RXNE标志位置1,表示RDR中已存入数据,随后从RDR中读出数据(0xA1),同时软件清除RXNE标志位;

2、当下一个数据(0xF2)发送完毕时,移位寄存器中第二个字节的接收(0xA2)也同步完成,后面的步骤同上直至数据交换结束;需注意,RDR存入数据后需及时读出,避免后续数据覆盖导致数据丢失;

连续传输对软件的配合度要求较高,在标志位产生后需及时处理,通信时每个字节之间没有任何空袭,传输效率高

非连续传输

运行逻辑:

 1、TXE为1表示TDR为空,软件写入数据(0xF1)至TDR寄存器(发送数据缓存区),TXE为0,此时移位寄存器无数据,故TDR中的数据立刻转入移位寄存器并开始发送(同连续传输);

2、与连续传输不同,数据(0xF1)转入移位寄存器后,TXE置1表示TDR为空后,不再要求立刻将下一数据写入TDR寄存器,而是等待第一个交换字节的时序结束,随后等待RXNE置1;

3、RXNE为1后,将第一个接收到的数据读出后,再在TDR中写入下一数据(0xF2),随后进入移位寄存器并交换字节;以此类推,重复上述时序直至通信完毕;

代码实现

初始化流程:

        1、开启SPI、GPIO时钟;

        2、初始化GPIO口:SCK、MOSI为输出口,配置为复用推挽输出;MISO为硬件外设的输入信号,可配置为上拉输入;SS为软件控制的输出信号,配置为通用推挽输出即可;

        3、配置SPI外设;

        4、开关控制,调用SPI_Cmd,给SPI使能

SPI相关库函数

//恢复缺省配置
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
//初始化
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
//结构体变量初始化
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
//外设使能
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
//中断使能
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
//DMA使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
//写DR数据寄存器
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
//读DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

//获取和清除标志位
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);

实现代码:

main:

#include "stm32f10x.h"                  // Device header
#include "delay.h"
#include "LED.h"
#include "KEY.h"
#include "OLED.h"
#include "W25Q128.h"

int main(void)
{
	delay_init();
	OLED_Init();
	W25Q128_Init();
	
	uint8_t MID;
	uint16_t DID;
	
	uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};	//定义要写入数据的测试数组
	uint8_t ArrayRead[4];								//定义要读取数据的测试数组
	
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	/*显示ID号*/
	W25Q128_ReadID(&MID, &DID);			//获取W25Q128的ID号
	OLED_ShowHexNum(1, 5, MID, 2);		
	OLED_ShowHexNum(1, 12, DID, 4);		
	
	/*W25Q64功能函数测试*/
	W25Q128_SectorErase(0x000000);					//指定扇区起始地址来扇区擦除
	W25Q128_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q128中。起始地址:0x000000。写入数组:把ArrayWrite传进去。写入数量:4个字节。
	                                                //这里00是最后2个16进制数的页内地址,前面4位是页地址
	W25Q128_ReadData(0x000000, ArrayRead, 4);		//读取刚写入的测试数据到读取数据的测试数组中。读取数组:把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)
	{
		
	}
}

My_SPI.c:

#include "stm32f10x.h"                  // Device header

void MySPI_W_CS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_12, (BitAction)BitValue);
}


void MySPI_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;	//MOSI、SCK
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;					//MISO
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;//64分频
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;				//模式0
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
	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;				//配置为主机
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;					//软件模拟
	SPI_Init(SPI2, &SPI_InitStructure);
	
	SPI_Cmd(SPI2, ENABLE);
	
	MySPI_W_CS(1);
}

void MySPI_Start(void)			//起始信号
{
	MySPI_W_CS(0);	
}

void MySPI_Stop(void)			//中止信号
{
	MySPI_W_CS(1);	
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)//交换一字节
{
	while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1,无需手动清除
	SPI_I2S_SendData(SPI2, ByteSend);
	
	while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1,无需手动清除
	return SPI_I2S_ReceiveData(SPI2);
}

W25Q127.c:

#include "stm32f10x.h"                  // Device header
#include "My_SPI.h"
#include "W25Q128_Ins.h"


void W25Q128_Init(void)
{
	MySPI_Init();
}

void W25Q128_ReadID(uint8_t *MID, uint16_t *DID)//读取W25Q128的厂商ID和设备ID
{	
	MySPI_Start();
	MySPI_SwapByte(W25Q128_JEDEC_ID);			//根据指令集,9H为读ID号的指令
	*MID = MySPI_SwapByte(W25Q128_DUMMY_BYTE);	//交换数据,用FF交换厂商ID
	*DID = MySPI_SwapByte(W25Q128_DUMMY_BYTE);	//交换数据,用FF交换设备ID高8位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q128_DUMMY_BYTE);	//交换数据,用FF交换设备ID低8位
	MySPI_Stop();
}//由于寄存器只读,故FF不会产生影响,且地址指针跟随时钟信号递增


void W25Q128_WriteEnable(void)
{
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q128_WRITE_ENABLE);		//交换发送写使能的指令
	MySPI_Stop();								//SPI终止
}


void W25Q128_WaitBusy(void)       //等待Busy为0
{
	uint32_t Timeout;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q128_READ_STATUS_REGISTER_1);				//交换发送读状态寄存器1的指令
	Timeout = 100000;							//给定超时计数时间
	while ((MySPI_SwapByte(W25Q128_DUMMY_BYTE) & 0x01) == 0x01)	//循环等待忙标志位。   //& 0x01,用掩码取出最低位;==0x01就是BUSY为1。BUSY为1,进入While死循环,进行等待,在次读出一次状态寄存器。BUSY为0,跳出循环。
	{
		Timeout --;								//等待时,计数值自减
		if (Timeout == 0)						//自减到0后,等待超时
		{
			/*超时的错误处理代码,可以添加到此处*/
			break;								//跳出等待,不等了
		}
	}
	MySPI_Stop();								//SPI终止
}


void W25Q128_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;                                 //定义变量
	
	W25Q128_WriteEnable();						//写使能,写使能仅对之后跟随一条时序有效
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q128_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终止
	
	W25Q128_WaitBusy();							//事后等待忙,就是写入后,立刻等待,不忙了再退出
}


void W25Q128_SectorErase(uint32_t Address)
{
	W25Q128_WriteEnable();						//写使能,写使能仅对之后跟随一条时序有效
	
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q128_SECTOR_ERASE_4KB);	//交换发送扇区擦除的指令
	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
	MySPI_SwapByte(Address);					//交换发送地址7~0位
	MySPI_Stop();								//SPI终止
	
	W25Q128_WaitBusy();							//事后等待忙,就是写入后,立刻等待,不忙了再退出
}


void W25Q128_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();								//SPI起始
	MySPI_SwapByte(W25Q128_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(W25Q128_DUMMY_BYTE);	//依次在起始地址后读取数据
	}
	MySPI_Stop();								//SPI终止
}

### 实现 W25Q128STM32SPI 通信 W25Q128 是一种基于 SPI 接口的大容量串行闪存器件,支持高速读写操作。STM32 微控制器通过其内置的 SPI 外设可以方便地与 W25Q128 进行数据交换。 #### 初始化配置 为了使能 SPI 功能并初始化外设,需设置 GPIO 和 SPI 参数: ```c void MX_SPI1_Init(void) { /* (省略部分代码) */ hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; // 设置为主模式 hspi1.Init.Direction = SPI_DIRECTION_2LINES;// 双向传输 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 数据大小为8位 hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // SCK 空闲状态低电平 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一跳变沿采样 hspi1.Init.NSS = SPI_NSS_SOFT; // 软件管理NSS信号 HAL_SPI_Init(&hspi1); } ``` 上述代码片段展示了如何配置 SPI1 作为主机来驱动外部设备[^1]。 #### 发送命令函数 定义一个通用发送指令给 Flash 存储器的方法如下所示: ```c HAL_StatusTypeDef SendCommand(uint8_t cmd, uint8_t *data, size_t length){ uint8_t txBuffer[length + 1]; memcpy(txBuffer + 1, data, length); // 将地址或数据复制到缓冲区偏移位置 txBuffer[0] = cmd; return HAL_SPI_Transmit(&hspi1, txBuffer, sizeof(txBuffer), HAL_MAX_DELAY); } ``` 此方法接受三个参数:要执行的操作码 `cmd` ,以及可选的数据指针和长度 `length`. 它构建了一个包含命令字节加上任意附加信息(如目标地址)的消息帧,并调用了底层硬件抽象层(HAL)库中的SPI传送服务[^2]. #### 写入单页数据 下面是一个用于向指定页面内连续存储空间写入数据的例子: ```c HAL_StatusTypeDef WritePageData(uint32_t pageAddress,uint8_t* buffer,size_t bufferSize){ const uint8_t WRITE_ENABLE_CMD = 0x06; const uint8_t PAGE_PROGRAM_CMD = 0x02; // Enable write operations on the flash chip. if(SendCommand(WRITE_ENABLE_CMD,NULL,0)!= HAL_OK)return HAL_ERROR; // Prepare address bytes for programming command. uint8_t addrBytes[]={(uint8_t)((pageAddress>>16)&0xFF),(uint8_t)((pageAddress>>8)&0xFF),(uint8_t)(pageAddress&0xFF)}; // Send program instruction along with target location and actual payload. if(SendCommand(PAGE_PROGRAM_CMD,addrBytes,sizeof(addrBytes))!= HAL_OK)return HAL_ERROR; if(HAL_SPI_Transmit(&hspi1,buffer,bufferSize,H lidt)>0?HAL_TIMEOUT:HAL_OK); return HAL_OK; } ``` 这段程序首先发出写保护解除请求(`WRITE_ENABLE_CMD`),接着准备目的地址并通过 `PAGE_PROGRAM_CMD` 向选定区域写入新内容。注意,在实际应用中可能还需要考虑错误处理机制和其他细节优化[^3].
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值