13. SPI协议
13.1 什么是SPI协议
SPI(Serial Peripheral Interface)是一种同步串行通信协议,用于在微控制器和外部设备之间进行数据传输。它由一个主设备(通常是微控制器MCU)和一个或多个从设备组成,即一主多从模式。它通常用于短距离、高速、全双工的通信,它在许多嵌入式系统和电子设备中被广泛应用,如存储器芯片、传感器、显示器驱动器、无线模块等。
在SPI协议中,主设备是通信的发起方和控制方,而从设备则是被动接收和响应主设备的命令和数据。主设备通过时钟信号来同步数据传输,同时使用多个双向数据线来实现数据的传输和接收。
SPI协议是一种全双工通信方式,意味着主设备和从设备可以同时发送和接收数据。它还使用一种选择信号(通常称为片选或使能信号),用于选择与主设备进行通信的特定从设备。
- 主设备通过MOSI线向从设备发送数据。在每个时钟周期中,主设备将一个位发送到MOSI线上,从设备在下一个时钟周期中读取该位。
- 从设备通过MISO线向主设备发送数据。在每个时钟周期中,从设备将一个位发送到MISO线上,主设备在下一个时钟周期中读取该位。
- 数据传输可以是全双工的,即主设备和从设备可以同时发送和接收数据。
- 数据传输的长度可以是可变的,通常以字节为单位。
- 数据传输可以是单向的,即主设备只发送数据或只接收数据。
- 数据传输可以是多主设备的,即多个主设备可以与多个从设备进行通信。
13.2 SPI的硬件接口
SPI主要使用4根线,时钟线(SCLK),主输出从输入线(MOSI),主输入从输出线(MISO)和片选线(CS)。
通信线 | 说明 |
---|---|
SCLK | 时钟线,也叫做SCK。由主机产生时钟信号。 |
MOSI | 主设备输出从设备输入线,也叫做SDO。意为主机向从机发送数据。 |
MISO | 主设备输入从设备输出线,也叫做SDI。意为主机接收从机的数据。 |
CS | 片选线,也叫做NSS。从机使能信号,由主机控制。当我们的主机控制某个从机时,需要将从机对应的片选引脚电平拉低或者是拉高。 |
主设备是通过片选线选择要与之通信的从设备。每个从设备都有一个片选线,当片选线为低电平时,表示该从设备被选中。(也有一些设备以高电平有效,需要根据其数据手册确定)。主设备通过控制时钟线的电平来同步数据传输。时钟线的上升沿和下降沿用于控制数据的传输和采样。SPI的主从接线方式需要对应,主从机设定后身份固定。
13.3 SPI的模式选择
SPI协议定义了多种传输模式,也称为SPI模式或时序模式,用于控制数据在时钟信号下的传输顺序和数据采样方式。SPI的传输模式主要由两个参数决定:时钟极性 (CKPL) 和相位 (CKPH)。 时钟极性 (CKPL):时钟极性定义了时钟信号在空闲状态时的电平。
各信号说明
CKPL = 0:时钟信号在空闲状态时为低电平。
CKPL = 1:时钟信号在空闲状态时为高电平。
时钟相位 (CKPH):相位定义了数据采样和更新发生在时钟信号的哪个边沿上。
CKPH = 0:数据采样发生在时钟的第一个边沿,数据更新发生在第二个边沿。
CKPH = 1:数据采样发生在时钟的第二个边沿,数据更新发生在第一个边沿。
以下是常见的SPI模式:
模式0(CKPL=0,CKPH=0):
- 时钟极性(Clock Polarity)为0,表示时钟空闲状态为低电平。
- 时钟相位(Clock Phase)为0,表示数据在时钟信号的第一个边沿(时钟上升沿)进行采样和稳定。
模式1(CKPL=0,CKPH=1):
- 时钟极性为0,时钟空闲状态为低电平。
- 时钟相位为1,数据在时钟信号的第二个边沿(时钟下降沿)进行采样和稳定。
模式2(CKPL=1,CKPH=0):
- 时钟极性为1,时钟空闲状态为高电平。
- 时钟相位为0,数据在时钟信号的第一个边沿(时钟下降沿)进行采样和稳定。
模式3(CKPL=1,CKPH=1):
- 时钟极性为1,时钟空闲状态为高电平。
- 时钟相位为1,数据在时钟信号的第二个边沿(时钟上升沿)进行采样和稳定。
选择SPI模式的决策通常取决于从设备的规格要求和通信协议。不同的设备可能采用不同的模式,所以在与特定从设备通信之前,必须了解从设备所需的SPI模式。如果没有明确指定SPI模式,通常可以根据从设备的规格手册或通信协议选择最常见的模式0或模式3进行尝试。此外,还需要注意SPI模式时钟的频率限制,以确保主设备和从设备之间的时钟频率匹配。
13.4 软件SPI与硬件SPI
SPI与IIC类似,都分有软件SPI和硬件SPI,软件SPI部分不再讲解,本章节着重讲解硬件SPI。
MSPM0L系列中的串行外设接口 (SPI) 外设支持以下主要特性:
- 支持 ULPCLK/2 比特率,最高可达 16Mb/s(在控制器和外设模式下)
- 可配置为控制器或外设(主机和从机)
- 控制器和外设的可配置芯片选择(CS引脚选择)
- 可编程时钟预分频器和比特率
- 可编程数据帧大小从 4 位到 16 位(控制器模式)和 7 位到 16 位(外设模式)
- 支持 PACKEN 功能,允许将 2 个 16 位 FIFO 条目打包为一个 32 位值以提高 CPU 性能
- 发送和接收 FIFO(4 个条目,每个条目 16 位),支持 DMA 数据传输
- 支持 TI 模式、Motorola 模式和 National Microwire 格式
使用硬件SPI的优势:
支持中断和DMA: 硬件SPI可以与中断控制器和DMA控制器配合使用,实现数据的高效处理和传输。
硬件缓冲区: 硬件SPI具有内部缓冲区,可以在主机和外设之间进行数据中转,提高数据的传输效率。
高速传输: 硬件SPI使用硬件模块进行数据传输,速度通常比软件实现的SPI更快。
13.5. SPI协议的优劣势
优势
简单易实现: SPI协议的硬件和软件实现相对简单,通信过程清晰明了,易于理解和调试。这使得SPI协议在嵌入式系统中得到广泛应用。
高速传输: SPI协议可以实现高速的数据传输。由于SPI是点对点的通信协议,数据直接在主设备和从设备之间传输,无需地址和冲突检测,发送与接收分别在不同的信号线上,因此具有较高的传输速度。
灵活性强: SPI协议允许主设备与多个从设备进行通信。主设备可以通过选择从设备的片选信号来与特定设备通信,从而实现多设备的串行通信。
劣势
信号线长度有限: 由于SPI协议没有特定的规范限制信号线的长度,较长的信号线可能会引入稳定性和传输速度方面的问题。在长距离通信时需要考虑信号传输的可靠性。
信号线数量多: SPI协议在每个从设备之间都需要一条单独的控制线,因此对于连接大量设备的系统来说,所需的信号线数量会相对较多。
无手握通信: SPI协议没有提供像I2C协议那样的“握手”机制,即从设备无法主动向主设备发送数据请求。因此,对于需要主动主设备交互的应用场景来说,SPI可能不太适合。
13.6. SPI协议的应用领域
SPI是一种常见的串行通信接口,广泛应用于数字和模拟电子设备之间的通信。它具有简单、高效和灵活的特点,因此与I2C一样,在各种领域都被广泛采用。
在存储器和闪存芯片领域,SPI协议被用于与存储器芯片(如EEPROM、SRAM)和闪存芯片(如SD卡、SPI Flash)进行通信。通过SPI协议,这些芯片可以与主控制器进行数据交换,实现数据的高速读写和存储。
SPI协议在显示器和液晶屏领域也有广泛的应用。许多液晶显示器和OLED显示器使用SPI协议传输图像数据和控制信号。
还有传感器领域、通信领域等,需要注意的是,SPI协议是一种点对点的通信协议,一般通过主从架构进行通信,其中一个设备充当主设备,其他设备充当从设备。
13.7 SPI实验介绍
以W25Q64-FLASH存储模块作为实验案例。通过硬件SPI的方式与其进行通信,实现数据的读写。
13.7.1 W25Q64介绍
W25Q64是一种常见的串行闪存器件,它采用SPI(Serial Peripheral Interface)接口协议,具有高速读写和擦除功能,可用于存储和读取数据。W25Q64芯片容量为64 Mbit(8 MB),其中名称后的数字代表不同的容量选项。不同的型号和容量选项可以满足不同应用的需求,比如W25Q16、W25Q64、W25Q128等。通常被用于嵌入式设备、存储设备、路由器等高性能电子设备中。
W25Q64闪存芯片的内存分配是按照扇区(Sector)和块(Block)进行的,每个扇区的大小为4KB,每个块包含16个扇区,即一个块的大小为64KB。
13.7.2 实验硬件连接
W25Q64存储芯片,其引脚的说明,见下表。
它与开发板的连接如下:
需要注意的是,我们使用的是硬件SPI方式驱动W25Q64,因此我们需要确定我们设置的引脚是否有硬件SPI外设接口。在数据手册中,PA3~6可以复用为SPI0的4根通信线。
这里需要注意的是PICO,表示的是外设输入控制器输出,即对应SPI中的MOSI。POCI表示外设输出控制器输入,即对应MISO。
13.8 SPI的参数配置
1.8.1 开启配置工具
在CCS中新建一个空白工程 empty。
在CCS的左侧工作区中找到并打开empty.syscfg文件。
1.8.2 SPI参数的配置
在sysconfig界面中,左侧可以选择MCU的外设,我们找到并点击SPI选项卡,在SPI中点击ADD,就可以添加SPI外设。
配置SPI的参数。
SPI引脚配置
这里需要注意,由于大多数的SPI协议中,整个时序里在发送接收时片选是要一直拉低的,而SPI外设的片选在每次发送和接收完一帧后会拉高,所以CS片选线需要用MCU的IO口独立控制,没有办法使用SPI外设的CS管脚。这里使用GPIO的方式(软件方式)去控制CS引脚的输出。
新建一个GPIO。命名为CS,引脚选择我们现在接入模块CS的引脚PA3。其配置如下:
1.8.3 其他的配置
本案例是通过串口输出的方式,将结果通过串口输出给上位机(电脑)显示,故还需要配置串口的发送功能。
未显示的部分,是默认选项。
将以上配置保存,可以通过快捷键Ctrl+S进行快速保存。
更新完成之后,我们所有设定的引脚和功能就会在ti_msp_dl_config.h中定义。
1.9 SPI协议的使用
1.9.1 工程文件管理
我们在工程文件夹下新建一个文件夹:Hardware
。
在Hardware文件夹下再新建两个文件,分别是 bsp_w25q64.c
和 bsp_w25q64.h
。
更新头文件路径,新增我们保存bsp_w25q64.c和.h的文件夹路径。
${PROJECT_ROOT}/Hardware
以上文件的操作完成之后,在bsp_w25q64.h
中编写以下代码:
#ifndef _BSP_W25Q64_H__
#define _BSP_W25Q64_H__
#include "ti_msp_dl_config.h"
//CS引脚的输出控制
//x=0时输出低电平
//x=1时输出高电平
#define SPI_CS(x) ( (x) ? DL_GPIO_setPins(CS_PORT,CS_PIN_PIN) : DL_GPIO_clearPins(CS_PORT,CS_PIN_PIN) )
uint16_t W25Q64_readID(void);//读取W25Q64的ID
void W25Q64_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte); //W25Q64写数据
void W25Q64_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length);//W25Q64读数据
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
SPI的初始化在SYSCONFIG中已经配置完成,但是我们还需要准备SPI的读写步骤。同时为确保发送和接收数据成功,在发送时,需要确保发送缓冲区里的数据发送完毕,即发送缓冲区为空,才可以进行下一个数据的发送;在接收时,需要确保接收缓冲区里有数据才能够进行接收。
在bsp_w25q64.c
中补充以下代码:
#include "bsp_w25q64.h"
uint8_t spi_read_write_byte(uint8_t dat)
{
uint8_t data = 0;
//发送数据
DL_SPI_transmitData8(SPI_INST,dat);
//等待SPI总线空闲
while(DL_SPI_isBusy(SPI_INST));
//接收数据
data = DL_SPI_receiveData8(SPI_INST);
//等待SPI总线空闲
while(DL_SPI_isBusy(SPI_INST));
return data;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.9.2 读取设备ID案例
根据W25Q64的数据手册可知,读取ID的指令有很多个,ABH/90H/92H/94H/9FH等。而90H的命令,在数据手册中给出了其读取的时序图。
读取步骤:
- 将CS端拉低为低电平;
- 发送指令 90H(1001_0000);
- 发送地址 000000H(0000_0000_0000_0000_0000_0000);
- 读取制造商ID,根据数据手册可以知道制造商ID为
EFh
; - 读取设备ID,根据数据手册可以知道设备ID为
15h
; - 恢复CS端为高电平;
实现代码:
//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q64
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
//读取设备ID
uint16_t W25Q64_readID(void)
{
uint16_t temp = 0;
//将CS端拉低为低电平
SPI_CS(0);
//发送指令90h
spi_read_write_byte(0x90);//发送读取ID命令
//发送地址 000000H
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
//接收数据
//接收制造商ID
temp |= spi_read_write_byte(0xFF)<<8;
//接收设备ID
temp |= spi_read_write_byte(0xFF);
//恢复CS端为高电平
SPI_CS(1);
//返回ID
return temp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1.9.3 写入数据流程案例
写入数据步骤如右图。在FLASH存储器中,每次写入数据都要确保其中的数据为0xFF,是因为FLASH存储器的写入操作是一种擦除-写入操作。擦除操作是将存储单元中的数据全部置为1,也就是0xFF。然后,只有将要写入的数据位为0的位置才能进行写入操作,将其改变为0。这个过程是不可逆的,所以在写入数据之前,需要先确保要写入的位置为0xFF,然后再写入数据。
这种擦除-写入的操作是由于FLASH存储器的特殊结构决定的。FLASH存储器中的存储单元是通过电子门的状态进行控制的,每个门可以存储一个二进制位。擦除操作需要将门的状态恢复为初始状态,即全部为1。然后通过改变门的状态,将需要存储的数据位改变为0。所以在写入数据之前,需要确保存储单元的状态为1,以便进行正确的写入操作。
另外,FLASH存储器的擦除操作是以块为单位进行的,而不是单个存储单元。所以如果要写入数据的位置上已经有数据存在,需要进行擦除操作,将整个块的数据都置为1,然后再写入新的数据。这也是为什么在FLASH写入数据之前需要确保其中的数据为0xFF的原因。
1.9.4 写使能
在进行写入操作之前,需要使用到写使能(Write Enable)命令。写使能的作用是启用对闪存芯片的写入操 作。在默认情况下,闪存芯片处于保护状态,禁止对其进行写入操作,主要是为了防止误操作对数据的损坏。写使 能命令可以解除这种保护状态,将闪存芯片设置为可以进行写入操作。
通过发送写使能命令,闪存芯片将进入一个特定的状态,使得后续的写入命令可以被接受和执行。在写入数据 之前,需要发送写使能命令来确保闪存芯片处于可写状态。然后,才能发送写入命令将数据写入指定的存储位置。 使用写使能命令可以有效地保护数据的完整性和安全性,防止误操作对数据进行写入或者修改。同时,也能够确保 数据的一致性,避免写入过程中出现错误或者干扰。因此,在使用W25Q64进行写入操作时,需要先发送写使能命 令,以确保闪存芯片处于可写状态,再进行数据的写入操作。
W25Q64的数据手册中,关于写使能的时序如下:
操作步骤:
- 将CS端拉低为低电平;
- 发送指令 06H(0000_0110);
- 恢复CS端为高电平;
具体实现代码如下:
//发送写使能
void W25Q64_write_enable(void)
{
//拉低CS端为低电平
SPI_CS(0);
//发送指令06h
spi_read_write_byte(0x06);
//拉高CS端为高电平
SPI_CS(1);
}
2
3
4
5
6
7
8
9
10
1.9.5 器件忙判断
在W25Q64的数据手册中,有3个状态寄存器,可以判断当前W25Q64是否正在传输、写入、读取数据等,我们每一次要对W25Q64进行操作时,需要先判断W25Q64是否在忙。如果在忙的状态,我们去操作W25Q64,很可能会导致数据丢失,并且操作失败。而判断是否忙,是通过状态寄存器1的S0位(BUSY)进行判断,状态寄存器1的地址为0X05。
读取状态寄存器的时序图如下:
- 拉低CS端为低电平;
- 发送指令05h(0000_0101);
- 接收状态寄存器值;
- 恢复CS端为高电平;
具体实现代码如下:
void W25Q64_wait_busy(void)
{
unsigned char byte = 0;
do
{
//拉低CS端为低电平
SPI_CS(0);
//发送指令05h
spi_read_write_byte(0x05);
//接收状态寄存器值
byte = spi_read_write_byte(0Xff);
//恢复CS端为高电平
SPI_CS(1);
//判断BUSY位是否为1 如果为1说明在忙,重新读写BUSY位直到为0
}while( ( byte & 0x01 ) == 1 );
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.9.6 扇区擦除
W25Q64闪存芯片的内存分配是按照扇区(Sector)和块(Block)进行的,每个扇区的大小为4KB,每个块包含16个扇区,即一个块的大小为64KB。
W25Q64闪存芯片的扇区擦除是指将某个特定扇区中的数据全部擦除的操作。擦除操作会将扇区中的所有数据都置为1(即0xFF),恢复到初始状态。下面是W25Q64扇区擦除的一般流程:
- 写使能(Write Enable):首先,要确保闪存芯片处于可写状态。发送写使能命令,将闪存芯片设置为可写模式,解除写保护。
- 扇区擦除设置(Sector Erase Setup):向W25Q64发送扇区擦除设置命令,并指定要擦除的扇区地址。W25Q64支持多种扇区擦除命令,可以根据需要选择擦除一个或多个扇区。
- 扇区擦除确认(Sector Erase Confirm):等待扇区擦除确认。W25Q64芯片进行擦除操作需要一定的时间,具体时间可参考该芯片的规格书。在擦除操作进行期间,通常会读取状态寄存器忙位的方法来确定擦除是否完成。过早地读取擦除操作中的数据可能会导致不正确的结果。
- 扇区擦除完成:当扇区擦除成功后,状态寄存器将指示擦除操作完成。此时,该扇区中的数据已经全部被擦除为1。
扇区擦除操作是一种高级操作,需要小心谨慎地使用。在实际应用中,通常会结合编程逻辑和相应的控制器来管理闪存芯片的擦除和写入操作,以确保数据的安全性和完整性。
在使用扇区擦除操作时,有几个注意事项需要特别关注:
- 擦除范围:要确保擦除的范围是正确的,仅擦除目标扇区,避免误擦除其他扇区中的数据。在执行擦除操作之前,请务必仔细检查要擦除的扇区地址,并确保没有错误。
- 数据备份:由于扇区擦除操作将数据全部擦除为1(0xFF),在执行擦除之前,应该确保重要数据已经备份。擦除后,数据将无法恢复,因此在执行重要数据的扇区擦除操作之前,请务必做好数据备份的工作。
扇区擦除的时序图如下:
- 拉低CS端为低电平;
- 发送指令20h(0010_0000);
- 发送24位的扇区首地址;
- 恢复CS端为高电平;
具体实现代码如下:
以下代码跟扇区擦除时序图有一些差别,多了忙判断和写使能。
/**********************************************************
* 函 数 名 称:W25Q64_erase_sector
* 函 数 功 能:擦除一个扇区
* 传 入 参 数:addr=擦除的扇区号
* 函 数 返 回:无
* 作 者:LC
* 备 注:addr=擦除的扇区号,范围=0~15
**********************************************************/
void W25Q64_erase_sector(uint32_t addr)
{
//计算扇区号,一个扇区4KB=4096
addr *= 4096;
W25Q64_write_enable(); //写使能
W25Q64_wait_busy(); //判断忙,如果忙则一直等待
//拉低CS端为低电平
SPI_CS(0);
//发送指令20h
spi_read_write_byte(0x20);
//发送24位扇区地址的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送24位扇区地址的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送24位扇区地址的低8位
spi_read_write_byte((uint8_t)addr);
//恢复CS端为高电平
SPI_CS(1);
//等待擦除完成
W25Q64_wait_busy();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1.9.7 写入数据
现在写入数据的前置步骤:擦除数据->写使能->判断忙 我们都完成了,只剩下将数据写入到对应地址中保存即可。
具体写入数据代码如下:
/**********************************************************
* 函 数 名 称:W25Q64_write
* 函 数 功 能:写数据到W25Q64进行保存
* 传 入 参 数:buffer=写入的数据内容 addr=写入地址 numbyte=写入数据的长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
unsigned int i = 0;
//擦除扇区数据
W25Q64_erase_sector(addr/4096);
//写使能
W25Q64_write_enable();
//忙检测
W25Q64_wait_busy();
//写入数据
//拉低CS端为低电平
SPI_CS(0);
//发送指令02h
spi_read_write_byte(0x02);
//发送写入的24位地址中的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送写入的24位地址中的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送写入的24位地址中的低8位
spi_read_write_byte((uint8_t)addr);
//根据写入的字节长度连续写入数据buffer
for(i=0;i<numbyte;i++)
{
spi_read_write_byte(buffer[i]);
}
//恢复CS端为高电平
SPI_CS(0);
//忙检测
W25Q64_wait_busy();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
1.9.8 读取数据
读取数据的时序图如下:
- 拉低CS端为低电平;
- 发送指令03h(0000_0011);
- 发送24位读取数据地址;
- 接收读取到的数据;
- 恢复CS端为高电平;
具体实现代码如下:
/**********************************************************
* 函 数 名 称:W25Q64_read
* 函 数 功 能:读取W25Q64的数据
* 传 入 参 数:buffer=读出数据的保存地址 read_addr=读取地址 read_length=读去长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
uint16_t i;
//拉低CS端为低电平
SPI_CS(0);
//发送指令03h
spi_read_write_byte(0x03);
//发送24位读取数据地址的高8位
spi_read_write_byte((uint8_t)((read_addr)>>16));
//发送24位读取数据地址的中8位
spi_read_write_byte((uint8_t)((read_addr)>>8));
//发送24位读取数据地址的低8位
spi_read_write_byte((uint8_t)read_addr);
//根据读取长度读取出地址保存到buffer中
for(i=0;i<read_length;i++)
{
buffer[i]= spi_read_write_byte(0XFF);
}
//恢复CS端为高电平
SPI_CS(1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
将以上相关的代码全部加入到 bsp_W25Q64.c
中。
#include "bsp_W25Q64.h"
uint8_t spi_read_write_byte(uint8_t dat)
{
uint8_t data = 0;
//发送数据
DL_SPI_transmitData8(SPI_INST,dat);
//等待SPI总线空闲
while(DL_SPI_isBusy(SPI_INST));
//接收数据
data = DL_SPI_receiveData8(SPI_INST);
//等待SPI总线空闲
while(DL_SPI_isBusy(SPI_INST));
return data;
}
//读取芯片ID
//返回值如下:
//0XEF13,表示芯片型号为W25Q80
//0XEF14,表示芯片型号为W25Q16
//0XEF15,表示芯片型号为W25Q64
//0XEF16,表示芯片型号为W25Q64
//0XEF17,表示芯片型号为W25Q128
//读取设备ID
uint16_t W25Q64_readID(void)
{
uint16_t temp = 0;
//将CS端拉低为低电平
SPI_CS(0);
//发送指令90h
spi_read_write_byte(0x90);//发送读取ID命令
//发送地址 000000H
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
spi_read_write_byte(0x00);
//接收数据
//接收制造商ID
temp |= spi_read_write_byte(0xFF)<<8;
//接收设备ID
temp |= spi_read_write_byte(0xFF);
//恢复CS端为高电平
SPI_CS(1);
//返回ID
return temp;
}
//发送写使能
void W25Q64_write_enable(void)
{
//拉低CS端为低电平
SPI_CS(0);
//发送指令06h
spi_read_write_byte(0x06);
//拉高CS端为高电平
SPI_CS(1);
}
//器件忙判断
void W25Q64_wait_busy(void)
{
unsigned char byte = 0;
do
{
//拉低CS端为低电平
SPI_CS(0);
//发送指令05h
spi_read_write_byte(0x05);
//接收状态寄存器值
byte = spi_read_write_byte(0Xff);
//恢复CS端为高电平
SPI_CS(1);
//判断BUSY位是否为1 如果为1说明在忙,重新读写BUSY位直到为0
}while( ( byte & 0x01 ) == 1 );
}
/**********************************************************
* 函 数 名 称:W25Q64_erase_sector
* 函 数 功 能:擦除一个扇区
* 传 入 参 数:addr=擦除的扇区号
* 函 数 返 回:无
* 作 者:LC
* 备 注:addr=擦除的扇区号,范围=0~15
**********************************************************/
void W25Q64_erase_sector(uint32_t addr)
{
//计算扇区号,一个扇区4KB=4096
addr *= 4096;
W25Q64_write_enable(); //写使能
W25Q64_wait_busy(); //判断忙,如果忙则一直等待
//拉低CS端为低电平
SPI_CS(0);
//发送指令20h
spi_read_write_byte(0x20);
//发送24位扇区地址的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送24位扇区地址的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送24位扇区地址的低8位
spi_read_write_byte((uint8_t)addr);
//恢复CS端为高电平
SPI_CS(1);
//等待擦除完成
W25Q64_wait_busy();
}
/**********************************************************
* 函 数 名 称:W25Q64_write
* 函 数 功 能:写数据到W25Q64进行保存
* 传 入 参 数:buffer=写入的数据内容 addr=写入地址 numbyte=写入数据的长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_write(uint8_t* buffer, uint32_t addr, uint16_t numbyte)
{
unsigned int i = 0;
//擦除扇区数据
W25Q64_erase_sector(addr/4096);
//写使能
W25Q64_write_enable();
//忙检测
W25Q64_wait_busy();
//写入数据
//拉低CS端为低电平
SPI_CS(0);
//发送指令02h
spi_read_write_byte(0x02);
//发送写入的24位地址中的高8位
spi_read_write_byte((uint8_t)((addr)>>16));
//发送写入的24位地址中的中8位
spi_read_write_byte((uint8_t)((addr)>>8));
//发送写入的24位地址中的低8位
spi_read_write_byte((uint8_t)addr);
//根据写入的字节长度连续写入数据buffer
for(i=0;i<numbyte;i++)
{
spi_read_write_byte(buffer[i]);
}
//恢复CS端为高电平
SPI_CS(0);
//忙检测
W25Q64_wait_busy();
}
/**********************************************************
* 函 数 名 称:W25Q64_read
* 函 数 功 能:读取W25Q64的数据
* 传 入 参 数:buffer=读出数据的保存地址 read_addr=读取地址 read_length=读去长度
* 函 数 返 回:无
* 作 者:LC
* 备 注:无
**********************************************************/
void W25Q64_read(uint8_t* buffer,uint32_t read_addr,uint16_t read_length)
{
uint16_t i;
//拉低CS端为低电平
SPI_CS(0);
//发送指令03h
spi_read_write_byte(0x03);
//发送24位读取数据地址的高8位
spi_read_write_byte((uint8_t)((read_addr)>>16));
//发送24位读取数据地址的中8位
spi_read_write_byte((uint8_t)((read_addr)>>8));
//发送24位读取数据地址的低8位
spi_read_write_byte((uint8_t)read_addr);
//根据读取长度读取出地址保存到buffer中
for(i=0;i<read_length;i++)
{
buffer[i]= spi_read_write_byte(0XFF);
}
//恢复CS端为高电平
SPI_CS(1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
1.10 SPI通信实验
在empty.c(要main函数的文件)中编写如下代码:
#include "ti_msp_dl_config.h"
#include "bsp_W25Q64.h"
#include "stdio.h"
#define delay_ms( X ) delay_cycles( (CPUCLK_FREQ/1000) * X )
//串口发送字符串
void uart0_send_string(char* str)
{
//当前字符串地址不在结尾 并且 字符串首地址不为空
while(*str!=0&&str!=0)
{
//当串口0忙的时候等待,不忙的时候再发送传进来的字符
while( DL_UART_isBusy(UART_0_INST) == true );
//发送字符串首地址中的字符,并且在发送完成之后首地址自增
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
int main(void)
{
char uart_output_buff[50]={0};
unsigned char read_write_buff[10] = {0};
SYSCFG_DL_init();
delay_ms(1);//等待器件部署
//读取W25Q64的ID
sprintf(uart_output_buff,"ID = %X\r\n",W25Q64_readID());
uart0_send_string(uart_output_buff);
//读取0地址的5个字节数据到buff
W25Q64_read(read_write_buff, 0, 5);
//串口输出读取的数据
sprintf(uart_output_buff, "read_write_buff = %s\r\n",read_write_buff);
uart0_send_string(uart_output_buff);
//往0地址写入5个字节长度的数据 lckfb
W25Q64_write("lckfb", 0, 5);
delay_ms(10);//等待写入完毕
//读取0地址的5个字节数据到buff
W25Q64_read(read_write_buff, 0, 5);
//串口输出读取的数据
sprintf(uart_output_buff, "read_write_buff = %s\r\n", read_write_buff);
uart0_send_string(uart_output_buff);
while (1) {
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
实验结果: