SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总 线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提 供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如 AT91RM9200 。
SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如 下 :
时钟线在第几个时钟边沿采样数据 :
SCK的第一(奇数)边沿进行数据位采样,数据在第一个时 钟边沿被锁
SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存
其中模式0和模式3最常用
模式 3 时序图:
W25Q128 全部指令非常多,但常用的如下几个指令:
指令(HEX) | 名称 | 作用 |
0X06 | 写使能 | 写入数据/擦除之前,必须先发送该指令 |
0X05 | 读SR1 | 判定FLASH是否处于空闲状态,擦除用 |
0X03 | 读数据 | 用于读取NOR FLASH数据 |
0X02 | 页写 | 用于写入NOR FLASH数据,最多写256字节 |
0X20 | 扇区擦除 | 扇区擦除指令,最小擦除单位(4096字节) |
W25Q128 一共有 3 个状态寄存器,它们的作用是跟踪芯片的状态。
BUSY:指示当前的状态,0 表示空闲,1 表示忙碌 WEL:写使能锁定,为 1 时,可以操作页/扇 区/块。为 0 时,写禁止。
以下流程省略了拉低/拉高片选信号CS。
硬件接线
cubeMX配置
w25q128_write_nocheck流程图
w25q128.c
#include "w25q128.h"
#include "spi.h"
#include "stdio.h"
/**
* @brief 初始化W25Q128
* @param 无
* @retval 无v
*/
void w25q128_init(void)
{
uint16_t flash_type;
spi1_read_write_byte(0xFF); /* 清除DR的作用 */
W25Q128_CS(1);
flash_type = w25q128_read_id(); /* 读取FLASH ID. */
if (flash_type == W25Q128)
printf("检测到W25Q128芯片\r\n");
}
/**
* @brief 等待空闲
* @param 无
* @retval 无
*/
static void w25q128_wait_busy(void)
{
while ((w25q128_rd_sr1() & 0x01) == 0x01); /* 等待BUSY位清空 */
}
/**
* @brief 读取W25Q128的状态寄存器1的值
* @param 无
* @retval 状态寄存器值
*/
uint8_t w25q128_rd_sr1(void)
{
uint8_t rec_data = 0;
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ReadStatusReg1); /* 读状态寄存器1 */
rec_data = spi1_read_write_byte(0xFF);
W25Q128_CS(1);
return rec_data;
}
/**
* @brief W25Q128写使能
* @note 将S1寄存器的WEL置位
* @param 无
* @retval 无
*/
void w25q128_write_enable(void)
{
W25Q128_CS(0);
spi1_read_write_byte(FLASH_WriteEnable); /* 发送写使能 */
W25Q128_CS(1);
}
/**
* @brief W25Q128发送地址
* @param address : 要发送的地址
* @retval 无
*/
static void w25q128_send_address(uint32_t address)
{
spi1_read_write_byte((uint8_t)((address)>>16)); /* 发送 bit23 ~ bit16 地址 */
spi1_read_write_byte((uint8_t)((address)>>8)); /* 发送 bit15 ~ bit8 地址 */
spi1_read_write_byte((uint8_t)address); /* 发送 bit7 ~ bit0 地址 */
}
/**
* @brief 擦除整个芯片
* @note 等待时间超长...
* @param 无
* @retval 无
*/
void w25q128_erase_chip(void)
{
w25q128_write_enable(); /* 写使能 */
w25q128_wait_busy(); /* 等待空闲 */
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ChipErase); /* 发送读寄存器命令 */
W25Q128_CS(1);
w25q128_wait_busy(); /* 等待芯片擦除结束 */
}
/**
* @brief 擦除一个扇区
* @note 注意,这里是扇区地址,不是字节地址!!
* 擦除一个扇区的最少时间:150ms
*
* @param saddr : 扇区地址 根据实际容量设置
* @retval 无
*/
void w25q128_erase_sector(uint32_t saddr)
{
//printf("fe:%x\r\n", saddr); /* 监视falsh擦除情况,测试用 */
saddr *= 4096;
w25q128_write_enable(); /* 写使能 */
w25q128_wait_busy(); /* 等待空闲 */
W25Q128_CS(0);
spi1_read_write_byte(FLASH_SectorErase); /* 发送写页命令 */
w25q128_send_address(saddr); /* 发送地址 */
W25Q128_CS(1);
w25q128_wait_busy(); /* 等待扇区擦除完成 */
}
/**
* @brief 读取芯片ID
* @param 无
* @retval FLASH芯片ID
* @note 芯片ID列表见: w25q128.h, 芯片列表部分
*/
uint16_t w25q128_read_id(void)
{
uint16_t deviceid;
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ManufactDeviceID); /* 发送读 ID 命令 */
spi1_read_write_byte(0); /* 写入一个字节 */
spi1_read_write_byte(0);
spi1_read_write_byte(0);
deviceid = spi1_read_write_byte(0xFF) << 8; /* 读取高8位字节 */
deviceid |= spi1_read_write_byte(0xFF); /* 读取低8位字节 */
W25Q128_CS(1);
return deviceid;
}
/**
* @brief 读取SPI FLASH
* @note 在指定地址开始读取指定长度的数据
* @param pbuf : 数据存储区
* @param addr : 开始读取的地址(最大32bit)
* @param datalen : 要读取的字节数(最大65535)
* @retval 无
*/
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ReadData); /* 发送读取命令 */
w25q128_send_address(addr); /* 发送地址 */
for(i=0;i pageremain */
{
pbuf += pageremain; /* pbuf指针地址偏移,前面已经写了pageremain字节 */
addr += pageremain; /* 写地址偏移,前面已经写了pageremain字节 */
datalen -= pageremain; /* 写入总长度减去已经写入了的字节数 */
if (datalen > 256) /* 剩余数据还大于一页,可以一次写一页 */
{
pageremain = 256; /* 一次可以写入256个字节 */
}
else /* 剩余数据小于一页,可以一次写完 */
{
pageremain = datalen; /* 不够256个字节了 */
}
}
}
}
/**
* @brief 写SPI FLASH
* @note 在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
* SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block
* 擦除的最小单位为Sector.
*
* @param pbuf : 数据存储区
* @param addr : 开始写入的地址(最大32bit)
* @param datalen : 要写入的字节数(最大65535)
* @retval 无
*/
uint8_t g_w25q128_buf[4096]; /* 扇区缓存 */
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t *w25q128_buf;
w25q128_buf = g_w25q128_buf;
secpos = addr / 4096; /* 扇区地址 */
secoff = addr % 4096; /* 在扇区内的偏移 */
secremain = 4096 - secoff; /* 扇区剩余空间大小 */
//printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
if (datalen <= secremain)
{
secremain = datalen; /* 不大于4096个字节 */
}
while (1)
{
w25q128_read(w25q128_buf, secpos * 4096, 4096); /* 读出整个扇区的内容 */
for (i = 0; i < secremain; i++) /* 校验数据 */
{
if (w25q128_buf[secoff + i] != 0XFF)
{
break; /* 需要擦除, 直接退出for循环 */
}
}
if (i < secremain) /* 需要擦除 */
{
w25q128_erase_sector(secpos); /* 擦除这个扇区 */
for (i = 0; i < secremain; i++) /* 复制 */
{
w25q128_buf[i + secoff] = pbuf[i];
}
w25q128_write_nocheck(w25q128_buf, secpos * 4096, 4096); /* 写入整个扇区 */
}
else /* 写已经擦除了的,直接写入扇区剩余区间. */
{
w25q128_write_nocheck(pbuf, addr, secremain); /* 直接写扇区 */
}
if (datalen == secremain)
{
break; /* 写入结束了 */
}
else /* 写入未结束 */
{
secpos++; /* 扇区地址增1 */
secoff = 0; /* 偏移位置为0 */
pbuf += secremain; /* 指针偏移 */
addr += secremain; /* 写地址偏移 */
datalen -= secremain; /* 字节数递减 */
if (datalen > 4096)
{
secremain = 4096; /* 下一个扇区还是写不完 */
}
else
{
secremain = datalen;/* 下一个扇区可以写完了 */
}
}
}
}
main.c
int main(void)
{
uint8_t datatemp[TEXT_SIZE];
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
MX_USART1_UART_Init();
w25q128_init();
/* 写入测试数据 */
sprintf((char *)datatemp, "zgl nb");
w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);
printf("数据写入完成!\r\n");
/* 读出测试数据 */
memset(datatemp, 0, TEXT_SIZE);
w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);
printf("读出数据:%s\r\n", datatemp);
while (1)
{
}
}