赞
踩
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如flash闪存芯片W25Q64等。
多NSS片选图如下(还有其他方式,如菊花链等)
- 一般的SPI为4-wire模式
芯片的管脚上只占用四根线。
MISO(Master Input Slave Output): 主器件数据输出,从器件数据输入。
MOSI(Master Output Slave Input):主器件数据输入,从器件数据输出。
SCK(Slava Clock): 时钟信号,由主设备控制发出。
NSS(CS): 从设备选择信号,由主设备控制。当NSS为低电平则选中从器件。
- SPI还有3-wire模式
芯片的管脚只占用3根
MISO/MOSI: 主从双向通信。
SCK(Slava Clock): 时钟信号,由主设备控制发出。
NSS(CS): 从设备选择信号,由主设备控制。当NSS为低电平则选中从器件。
其实质就是把MISO和MIOSI合成一条了,如下图所示。
W25Q64。W25Q 系列为台湾华邦公司推出的是一种使用 SPI 通讯协议的 NOR FLASH 存储器,64代表的是64Mbit的大小,也就是8MB的大小。
> 8MB被分为128个块(一个块64KB),而一个块又被分为16个扇区(一个扇区4KB)。
W25Q64的最小擦除单位是一个扇区,而我们有时候不想擦除一个扇区里面的全部内容,于是我们需要开辟一个至少4KB的缓存,假如我们当前要擦掉一个字节,我们可以临时开辟一个4KB的缓冲区,把扇区中的内容读入到缓冲区中,我们只要对应删掉缓冲区里的那个字节,然后擦除扇区,再把缓存区里的内容拿出来,存入这个扇区,我们就可以保证不会丢失不必要的数据了。
- 芯片的特性:
- 1.芯片擦除后,不是把擦除的位置清0,而是全部置1。
- 2.芯片只有在擦除后,才能往里面写入数据(原先写入过数据的情况),若是未擦除就写入,则会导致,写入数据的地方全部清零。
引脚介绍(前面带个斜杠的都是低电平有效):- CS:片选端,当被主机拉低时代表选中该器件。
- DO:数据输出端,主机读取该引脚发出的数据。
- DI:数据输入端,主机输出数据到该引脚。
- WP:写保护,低电平有效,当拉低电平时,不允许单片机写入数据,一般给他拉高。
- HOLD:低电平时,芯片停止工作,数据输出呈现高阻态,数据输入无效,一般给他拉高。
一般用的到的指令也就下面被红色框住的这几个,加上一个蓝色的那个
这些指令分别是:
我要写使能,我先把片选拉低选中芯片,然后我会发0x06(来之上面的表)指令,然后片选拉高,具体实现如下:
HAL_StatusTypeDef Write_Enable(uint8_t Type)
{
uint8_t cmd = 0x06;
HAL_StatusTypeDef STD = HAL_ERROR;
W25Q_CS_Level(0);
STD = w25q64_Transmit(&cmd, 1);
W25Q_CS_Level(1);
return STD;
}
先通过状态寄存器判断是否在忙,然后写使能,接着判忙,拉底片选选中,发命令字节0x02,发三个地址,高位优先,然后发你要写的数据,最后拉底片选,代码如下:
HAL_StatusTypeDef Page_Write(uint32_t WriteAddr, uint8_t * PW_Data, uint16_t PW_Size) { uint8_t cmd = W25Q_Page_Program; HAL_StatusTypeDef STD = HAL_ERROR; WriteAddr <<= 8; //只要24位,3个字节 LargeToSmall((uint8_t *)&WriteAddr, sizeof(WriteAddr)); Judge_Busy(); //判忙 Write_Enable(1); Judge_Busy(); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&WriteAddr, 3) == HAL_OK) { if(w25q64_Transmit(PW_Data, PW_Size) == HAL_OK) { STD = HAL_OK; } } } W25Q_CS_Level(1); return STD; }
总共有128个块,选择块的范围在0~127,加入我们要找第2块,要找到芯片对应的块地址,得先把2*64KB(块对应的大小),得到第二给块区的位置。
先通过状态寄存器判断是否在忙,然后写使能,接着判忙,拉底片选选中,发命令字节0xD8,发三个地址,高位优先,最后拉底片选,代码如下:
HAL_StatusTypeDef Block_Erase(uint32_t Block_Addr) { uint8_t cmd = W25Q_Block_Erase; HAL_StatusTypeDef STD = HAL_ERROR; //总共有128个块,而一个块有64KB的大小, //为了使找到对应的块地址,所以要乘以64KB Block_Addr *= (1<<16); Block_Addr <<= 8; //只需要24位表示地址,并且高位先传 LargeToSmall((uint8_t *)&Block_Addr, sizeof(Block_Addr)); Judge_Busy(); Write_En_De(1); Judge_Busy(); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&Block_Addr, 3) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); Judge_Busy(); return STD; }
这些配置从上到下分别是:
- Frame Format:主从模式选择
- Data Size:一次传输数据大小
- First Bit:数据是高位在先还是低位在先
- prescaler(for Baud Rate):分频值
- Clock Polarity(CPOL):时钟空闲时的电平
- Clock Phase(CPHA):数据取样是在奇数取样还是在偶数取样(1Edge是奇数取样,2Edge是偶数取样)
#include "w25q64.h" #include "redirect.h" /*内部函数声明区*/ static void LargeToSmall(uint8_t* type, uint8_t typeSize); static HAL_StatusTypeDef w25q64_Transmit(uint8_t * T_pData, uint16_t T_Size); static HAL_StatusTypeDef w25q64_Receive(uint8_t * R_pData, uint16_t R_Size); /*内部函数定义区*/ //大端转小端 static void LargeToSmall(uint8_t* type, uint8_t typeSize) { // 当大小小于1或者不是2的幂次位时退出 if( typeSize <= 1 ) { return; } for(uint8_t i=0; i<typeSize/2; i++) { *(type+i) = *(type+i) ^ *(type+typeSize-i-1); *(type+typeSize-i-1) = *(type+i) ^ *(type+typeSize-i-1); *(type+i) = *(type+i) ^ *(type+typeSize-i-1); } } /* 函数参数: 1、T_pData:发送数据缓冲区中取出数据发送出去 2、T_Size :需要发送的数据的长度 */ static HAL_StatusTypeDef w25q64_Transmit(uint8_t * T_pData, uint16_t T_Size) { return HAL_SPI_Transmit(&W25Q_SPI, T_pData, T_Size, 0xff); } /* 函数参数: 1、R_pData:接收数据并放置到接收数据缓冲区中 2、R_Size :需要接收的数据的长度 */ static HAL_StatusTypeDef w25q64_Receive(uint8_t * R_pData, uint16_t R_Size) { return HAL_SPI_Receive(&W25Q_SPI, R_pData, R_Size, 0xff); } /* 写使能或失能 参数: Type: 1、为1时使能 2、为0时失能 */ HAL_StatusTypeDef Write_En_De(uint8_t Type) { uint8_t cmd; HAL_StatusTypeDef STD = HAL_ERROR; W25Q_CS_Level(0); switch(Type) { case 1: cmd = W25Q_W_ENA; break; case 0: cmd = W25Q_W_DIS; break; default: cmd = W25Q_W_DIS; break; } if(w25q64_Transmit(&cmd, 1) == HAL_OK) { STD = HAL_OK; } W25Q_CS_Level(1); return STD; } /* 读状态寄存器 参数:Select 1、为0时是寄存器1 2、为1时是寄存器2 参数:State(指针) 1、返回的状态标志 流程:先写入命令,然后读取状态 */ HAL_StatusTypeDef Read_State_Reg(uint8_t Select, uint8_t* State) { uint8_t cmd[4] = {0,0,0,0}; HAL_StatusTypeDef STD = HAL_ERROR; W25Q_CS_Level(0); switch(Select) { case 0: cmd[0] = W25Q_R_STA_REG1; break; case 1: cmd[0] = W25Q_R_STA_REG2; break; default: cmd[0] = W25Q_R_STA_REG1; break; } if(w25q64_Transmit(cmd, 4) == HAL_OK) { if(w25q64_Receive(State,1) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); return STD; } /* 判忙 用处:判断当前flash是否在忙碌状态 */ void Judge_Busy(void) { uint8_t State; do{ Read_State_Reg(0, &State); //不要用指针类型局部变量传进去,必被卡死 State &= 0x01; }while(State == 0x01); } /* 写状态寄存器 参数:State(数组指针) 参数解释:长度为两个字节的数组指针, 第一个字节写入状态寄存器1; 第二个字节写入状态寄存器2。 流程:先写命令,再写状态 */ HAL_StatusTypeDef Write_State_Reg(uint8_t * State) { uint8_t cmd = W25Q_W_STA_REG_; HAL_StatusTypeDef STD = HAL_ERROR; Judge_Busy(); Write_En_De(1); W25Q_CS_Level(0); // Judge_Busy(); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit(State, 2) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); Write_En_De(0); return STD; } /* 读数据 参数:R_Addr 1、读取数据的地址 参数:R_Data(数组指针) 1、获取读取的数据 参数:R_Size 1、读取的数据的大小 */ HAL_StatusTypeDef Read_Data(uint32_t R_Addr, uint8_t * R_Data, uint16_t R_Size) { uint8_t cmd = W25Q_R_Dat; HAL_StatusTypeDef STD = HAL_ERROR; R_Addr <<= 8; //只要24位,3个字节 LargeToSmall((uint8_t *)&R_Addr, sizeof(R_Addr)); Judge_Busy(); //判忙 W25Q_CS_Level(0); // Judge_Busy(); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&R_Addr, 3) == HAL_OK) { if(w25q64_Receive(R_Data,R_Size) == HAL_OK) { STD = HAL_OK; } } } W25Q_CS_Level(1); return STD; } /* 页编程 页的描述:1字节到 256 字节(一页) 页写的前提条件:编程之前必须保证额你存空间是0xff, 所以得先进行擦除(擦除后模式全为1) 页写的注意事项:进行页编程时,如果数据字节数超过了 256 字节, 地址将自动回到页的起始地址,覆盖掉之前的数据。 参数:WriteAddr 1、地址,三个字节地址 参数:PW_Data(数组指针) 1、要写入的数据,长度根据PW_size来定 2、高位先传 参数:PW_Size 2、要写入的数据长度 流程:先开写使能、判忙,再写命令, 再写3个字节的地址,后写入数据,最后写失能 */ HAL_StatusTypeDef Page_Write(uint32_t WriteAddr, uint8_t * PW_Data, uint16_t PW_Size) { uint8_t cmd = W25Q_Page_Program; HAL_StatusTypeDef STD = HAL_ERROR; WriteAddr <<= 8; //只要24位,3个字节 LargeToSmall((uint8_t *)&WriteAddr, sizeof(WriteAddr)); Judge_Busy(); //判忙 Write_En_De(1); Judge_Busy(); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&WriteAddr, 3) == HAL_OK) { if(w25q64_Transmit(PW_Data, PW_Size) == HAL_OK) { STD = HAL_OK; } } } W25Q_CS_Level(1); Judge_Busy(); return STD; } /* 扇区擦除 扇区的描述:W25Q64总共8MB,分为128块(每块64KB), 每块16个扇区,每个扇区4K个字节。 扇区的备注:W25Q64的最小擦除单位就是一个扇区 所以至少给芯片开辟一个4KB的缓存区, 以防止一次性删除太多,而丢失数据。(显然单片机不会给他开辟这么大的空间) 参数:Sector_Addr 1、扇区地址,以4KB为单位寻址 2、高位先发 */ HAL_StatusTypeDef Sector_Erase(uint32_t Sector_Addr) { uint8_t cmd = W25Q_Sector_Erase; HAL_StatusTypeDef STD = HAL_ERROR; //一个扇区有4KB的大小, //为了使找到对应的扇区地址,所以要乘以4KB Sector_Addr *= (1<<12); Sector_Addr <<= 8; //只需要24位表示地址,并且高位先传 LargeToSmall((uint8_t *)&Sector_Addr, sizeof(Sector_Addr)); Judge_Busy(); Write_En_De(1); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&Sector_Addr, 3) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); Judge_Busy(); return STD; } /* 块擦除 块的描述:W25Q64有8MB的容量,而8MB有128个块,所以1块有64kB的大小, 所以这个函数一次能擦除64KB的大小。 参数:Block_Addr 1、块地址,共128个块,对应128个地址,以64K为单位寻址 2、高位先传 流程:先开写使能、判忙,再写命令, 再写3个字节的地址,最后写失能 */ HAL_StatusTypeDef Block_Erase(uint32_t Block_Addr) { uint8_t cmd = W25Q_Block_Erase; HAL_StatusTypeDef STD = HAL_ERROR; //总共有128个块,而一个块有64KB的大小, //为了使找到对应的块地址,所以要乘以64KB Block_Addr *= (1<<16); Block_Addr <<= 8; //只需要24位表示地址,并且高位先传 LargeToSmall((uint8_t *)&Block_Addr, sizeof(Block_Addr)); Judge_Busy(); Write_En_De(1); Judge_Busy(); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Transmit((uint8_t *)&Block_Addr, 3) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); Judge_Busy(); return STD; } /* 全片擦除 描述:直接把芯片全部擦除 */ HAL_StatusTypeDef Full_Erase(void) { uint8_t cmd = W25Q_Full_Erase; HAL_StatusTypeDef STD = HAL_ERROR; Judge_Busy(); Write_En_De(1); W25Q_CS_Level(0); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { STD = HAL_OK; } W25Q_CS_Level(1); Judge_Busy(); return STD; } /* 读ID 描述:读3个字节分别是生产厂家、存储器类型、容量 */ HAL_StatusTypeDef Read_Jedec_ID(uint8_t * R_Jedec_ID) { uint8_t cmd = W25Q_JEDEC_ID; HAL_StatusTypeDef STD = HAL_ERROR; Judge_Busy(); W25Q_CS_Level(0); // Judge_Busy(); if(w25q64_Transmit(&cmd, 1) == HAL_OK) { if(w25q64_Receive(R_Jedec_ID, 3) == HAL_OK) { STD = HAL_OK; } } W25Q_CS_Level(1); return STD; }
#ifndef W25Q64__H__ #define W25Q64__H__ #include "spi.h" /*句柄重命名*/ #define W25Q_SPI hspi1 /*片选引脚定义与函数调用*/ #define W25Q_CS_Pin GPIO_PIN_0 #define W25Q_CS_Port GPIOC #define W25Q_CS_Level(_CS_STATE__) HAL_GPIO_WritePin(W25Q_CS_Port, W25Q_CS_Pin, (GPIO_PinState)_CS_STATE__) //#define W25Q_CS_Level(_CS_STATE__) (*((volatile unsigned int *)(0x42000000+((uint32_t)&GPIOC->ODR-0x40000000)*32+0*4))) = _CS_STATE__ #define W25Q_W_ENA 0x06 //写使能 #define W25Q_W_DIS 0x04 //写禁止 #define W25Q_R_Dat 0x03 //读数据 #define W25Q_R_STA_REG1 0x05 //读状态寄存器1,紧跟着的字节就是当前状态 #define W25Q_R_STA_REG2 0x35 //读状态寄存器2,紧跟着的字节就是当前状态 #define W25Q_W_STA_REG_ 0x01 //写状态寄存器,写入两个字节,分别到寄存器1,和寄存器2 #define W25Q_Page_Program 0x02 //页编程,先跟3个地址字节,再跟一个数据字节 #define W25Q_Block_Erase 0xD8 //块擦除64k,三个地址字节 #define W25Q_Sector_Erase 0x20 //扇区擦除,跟三个地址 #define W25Q_Full_Erase 0xC7 //全片擦除 //0x60 #define W25Q_Susp_Erase 0x75 //暂停擦除 #define W25Q_Rest_Erase 0x7A //恢复擦除 #define W25Q_PowDow_Mode 0xB9 //掉电模式 #define W25Q_HPer_Mode 0xA3 //高性能模式 #define W25Q_JEDEC_ID 0x9F //读3个字节分别是生产厂家、存储器类型、容量 /*写使能或失能*/ HAL_StatusTypeDef Write_En_De(uint8_t Type); /*读状态寄存器*/ HAL_StatusTypeDef Read_State_Reg(uint8_t Select, uint8_t* State); /*判忙*/ void Judge_Busy(void); /*写状态寄存器*/ HAL_StatusTypeDef Write_State_Reg(uint8_t * State); /*读数据*/ HAL_StatusTypeDef Read_Data(uint32_t R_Addr, uint8_t * R_Data, uint16_t R_Size); /*页写*/ HAL_StatusTypeDef Page_Write(uint32_t WriteAddr, uint8_t * PW_Data, uint16_t PW_Size); /*扇区擦除*/ HAL_StatusTypeDef Sector_Erase(uint32_t Sector_Addr); /*块擦除*/ HAL_StatusTypeDef Block_Erase(uint32_t Block_Addr); /*全片擦除*/ HAL_StatusTypeDef Full_Erase(void); /*读ID*/ HAL_StatusTypeDef Read_Jedec_ID(uint8_t * R_Jedec_ID); #endif /*W25Q64__H__*/
#include "w25q64.h"
uint8_t device_id[3];
uint8_t read_buf[10] = {0};
uint8_t write_buf[10] = {0};
int i;
Read_Jedec_ID((uint8_t *)device_id); printf("W25Q64 ID 0x%x, 0x%x, 0x%x\r\n", device_id[0], device_id[1], device_id[2]); /* 为了验证,首先读取要写入地址处的数据 */ printf("-------- read data before write -----------\r\n"); Read_Data(0, read_buf, 10); for(i = 0; i < 10; i++) { printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i)); } /* 擦除该扇区 */ printf("\r\n-------- erase sector 0 -----------\r\n"); Sector_Erase(0); /* 再次读数据 */ printf("-------- read data after erase -----------\r\n"); Read_Data(0, read_buf, 10); for(i = 0; i < 10; i++) { printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i)); } /* 写数据1,擦除扇区后的写入 */ printf("\r\n-------- write data 11111 -----------\r\n"); for(i = 0; i < 10; i++) { write_buf[i] = i; } Page_Write(0, write_buf, 10); /* 再次读数据 */ printf("-------- read data after write -----------\r\n"); Read_Data(0, read_buf, 10); for(i = 0; i < 10; i++) { printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i)); } // Sector_Erase(0); /* 写数据2,未擦除连续写入,观察未擦除能否写入 */ printf("\r\n-------- write data 22222 -----------\r\n"); for(i = 0; i < 10; i++) { write_buf[i] = i*16; } Page_Write(0, write_buf, 10); /* 再次读数据 */ printf("-------- read data after write -----------\r\n"); Read_Data(0, read_buf, 10); for(i = 0; i < 10; i++) { printf("[0x%08x]:0x%02x\r\n", i, *(read_buf+i)); }
基于3线spi通信的oled(cubemx图形化编程软件).
嵌入式硬件入门——Flash Memory(W25Q64+SPI协议).
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。