实验(三):SPI应用:读写串行FLASH 实验

一、实验目的与任务

实验目的:

1. 学习对SPI的使用;

2. 掌握KEIL5的仿真与调试。

任务:

1. 根据要求编写程序,并写出原理性注释;

2. 将检查程序运行的结果,分析一下是否正确;

3. 完成所建工程的验证调试。

二、实验要求

以一种使用SPI 通讯的串行FLASH 存储芯片的读写实验为大家讲解STM32 的SPI 使用方法。实验中STM32 的SPI 外设采用主模式,通过查询事件的方式来确保正常通讯。

三、实验内容及步骤

本实验板中的FLASH 芯片(型号:W25Q64)是一种使用SPI 通讯协议的NOR FLASH存储器, 它的CS/CLK/DIO/DO 引脚分别连接到了STM32 对应的SPI 引脚NSS/SCK/MOSI/MISO 上,其中STM32 的NSS 引脚是一个普通的GPIO,不是SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式。

FLASH 芯片中还有WP 和HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯,该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。我们直接接电源,不使用通讯暂停功能。

1. 软件设计

① 实验新建文件步骤:

运行Keil 5开发环境。首先编写两个SPI底层驱动文件,MySPI.c 和 MySPI.h,用来存放SPI通讯协议的驱动,接着编写三个W25Q64使用的功能函数文件,W25Q64.c、W25Q64.h、W25Q64_Ins.h,其中W25Q64.c文件存放使用的功能函数,W25Q64_Ins.h中存放指令码。

② 编程要点:

  • 初始化通讯使用的目标引脚及端口时钟;
  • 使能SPI 外设的时钟;
  • 配置SPI 外设的模式、地址、速率等参数并使能SPI 外设;
  • 编写基本SPI 按字节收发的函数;
  • 编写对FLASH擦除及读写操作的的函数;
  • 编写测试程序,对读写数据进行校验。

2. 实验步骤

(1)运行Keil uVision5开发环境,建立一个项目工程。

(2)在工程中添加main.c文件,因需要用到OLED显示屏,所以将之前实验写好的OLED文件移植到该工程中,然后在main.c中调用,如图1所示。

图1 移植程序

(3)在工程中添加SPI底层驱动文件,因此需要创建MySPI.c文件,编写SPI通讯时序,如图2所示。

图2 编写MySPI.c代码

 (4)编写MySPI.h程序,方便以后工程文件以移植,使项目工程工具有移植性,如图3所示。

图3 MySPI.h程序

(5)在工程中添加W25Q64使用的功能函数文件,因此需要创建W25Q64_Ins.h文件,存放指令码,如图4所示。

图4 编写W25Q64指令码

(6)创建W25Q64.c文件,编写需要使用的功能函数,如图5所示。

图5 编写W25Q64功能函数

(7)编写W25Q64.h程序,方便以后工程文件以移植,使项目工程工具有移植性,如图6所示。

图6 编写W25Q64.h程序

(8)编写main.c程序,读取设备ID,写入数据,读出数据,如图7所示。

图7 main.c程序

运行并调试成功并无错误和警告。

3. 调试验证及结果

(1)将开发板连接到电脑上,使用跳线在面包板上将W25Q64和单片机连接,使用STLINK将程序烧录到STM32中,如图8所示。

图8 线路连接

(2)程序烧录后,实验现象如图9所示:

图9 实验现象

四、实验代码分析

(1)MySPI.c:

#include "MySPI.h"

/**
  * @brief  引脚封装
  * @param  
  * @retval 
  */
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);
}

/**
  * @brief  SPI初始化
  * @param  
  * @retval 
  */
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);//SPI模式0
}

/**
  * @brief  起始信号
  * @param  SS置低电平 
  * @retval 
  */
void MySPI_Start(void)
{
	MySPI_W_SS(0);
}

/**
  * @brief  终止信号
  * @param  SS置高电平
  * @retval 
  */
void MySPI_Stop(void)
{
	MySPI_W_SS(1);
}

/**
  * @brief  交互数据     模式0   SS下降沿->移出数据->SCK上升沿->移入数据->SCK下降沿->移出数据
  * @param  ByteSend     需要发送出去的数据
  * @retval ByteReceive  需要接收到的数据
  */
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	
	return ByteReceive;
}
/**
*模式2,在模式0的基础上,把出现SCK的地方,1改0,0改1
*/


/**
  * @brief  交互数据     模式1
  * @param  ByteSend     需要发送出去的数据
  * @retval ByteReceive  需要接收到的数据
  */
uint8_t MySPI_SwapByte1(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_SCK(1);
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(0);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}

	}
	
	return ByteReceive;
}
/**
*模式3,在模式1的基础上,把出现SCK的地方,1改0,0改1
*/

(2)W25Q64.c:

#include "W25Q64.h"

//初始化
void W25Q64_Init(void)
{
	MySPI_Init();
}
/**
  * @brief  获取ID号
  * @param  MID厂商ID  DID设备ID
  * @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 
	
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID高八位
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID低八位
	
	MySPI_Stop();
}

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

/**
  * @brief  查看芯片是否处于忙状态
  * @param  
  * @retval 
  */
void W25Q64_WaitBusy(void)
{
	uint32_t Timeout;
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//发送读状态寄存器1指令
	Timeout = 100000;
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//获取最低位进行查忙判断
	{
		//避免死循环,程序卡死
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
	MySPI_Stop();
}

/**
  * @brief  页编程
  * @param  Address24位地址  DataArray  Count
  * @retval 
  */
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	
	W25Q64_WriteEnable();
	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_WaitBusy();
}

/**
* @brief  扇区擦除
  * @param  
  * @retval 
  */
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();
	MySPI_Start();
	
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//发送指令
	
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	
	MySPI_Stop();
	W25Q64_WaitBusy();
}

/**
  * @brief  读数据
  * @param  
  * @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();
}

(3)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

(4)main函数程序:

#include "stm32f10x.h"                  // Device header
#include "Delay.h" 
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t ArrayWrite[] = {0xAA, 0x02, 0xFE, 0x05};
uint8_t ArrayRead[4];

int main(void)
{
	OLED_Init();
	W25Q64_Init();
	
	OLED_ShowString(1, 1, "MID:   DID:");
	OLED_ShowString(2, 1, "W:");
	OLED_ShowString(3, 1, "R:");
	
	W25Q64_ReadID(&MID, &DID);
	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)
	{
		
	}
}

五、实验总结

本次实验的主要目的是学习并掌握SPI(串行外围接口)的使用,以及使用KEIL5的仿真与调试工具进行验证。通过实验,我对SPI的原理和使用方法有了更深入的理解,并且掌握了在STM32中配置和操作SPI外设的技巧。以下是我对本次实验的总结和心得体会:

在本次实验中,我首先了解了SPI的基本原理。SPI是一种串行通信协议,通过四根信号线进行通信,包括时钟信号线(SCLK)、主设备输出从设备输入的数据线(MOSI)、主设备输入从设备输出的数据线(MISO)和片选信号线(SS)。SPI支持全双工通信,可以同时进行数据发送和接收。了解了SPI的基本原理之后,我明白了如何在STM32中配置和使用SPI外设。

在编写程序时,我按照实验要求,首先进行SPI外设的初始化,确保其处于就绪状态。然后,我根据目标设备的通信协议,设置正确的片选信号,以选择要与之通信的设备。接下来,我使用HAL库提供的函数进行数据的发送和接收,确保数据在SPI总线上的正确传输。在编写过程中,我注重添加适当的注释,以便于代码的理解和维护。

在验证和调试阶段,我使用了KEIL5的仿真与调试工具。通过设置断点、监视变量的值,我可以逐步执行程序,观察代码的运行情况。在调试过程中,我发现一些潜在的问题,并进行了修复和调试。调试工具为我提供了一个实时的、可视化的方式来观察代码的执行情况,提高了调试的效率。

通过本次实验,我不仅学会了配置和使用SPI外设,还加深了对SPI原理的理解。我意识到SPI是一种高速的串行通信协议,适用于与外部设备进行数据交换,如存储芯片、传感器等。SPI的全双工特性使得数据的发送和接收可以同时进行,提高了数据传输的效率。同时,我也意识到在实际应用中,需要根据不同的设备和通信要求,灵活配置SPI的参数。

总的来说,本次实验使我对SPI的应用有了更深入的了解。通过实际操作和调试,我掌握了SPI的配置和使用方法,并提高了对SPI外设的理解能力。这对我今后的嵌入式系统开发和硬件通信方面的工作都具有重要的意义。我将继续深入学习和探索SPI的更高级特性和应用场景,以提升自己在嵌入式系统开发领域的能力。

源码:实验3 

猜你喜欢

转载自blog.csdn.net/qq_61228493/article/details/131058076