目录
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速
的,全双工
,同步
的通信总 线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提 供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如 AT91RM9200
SPI 工作模式
时钟极性(CPOL):
没有数据传输时时钟线的空闲状态电平
0:SCK在空闲状态保持低电平
1:SCK在空闲状态保持高 电平
时钟相位(CPHA):
时钟线在第几个时钟边沿采样数据
0:SCK的第一(奇数)边沿进行数据位采样,数据在第一个时 钟边沿被锁存
1:SCK的第二(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存
工作原理
类似于佛珠
w25Q128
W25Q128 是华邦公司推出的一款 SPI 接口的 NOR Flash 芯片,其存储空间为 128 Mbit,相当于 16M 字节。 Flash 是常用的用于储存数据的半导体器件,它具有容量大,可重复擦写、按“扇区/块”擦除、掉 电后数据可继续保存的特性。 Flash 是有一个物理特性:只能写 0 ,不能写 1 ,写 1 靠擦除
。
W25Q128 常用指令
W25Q128 全部指令非常多,但常用的如下几个指令:
- 0X06 写使能 写入数据/擦除之前,必须先发送该指令
- 0X05 读SR1 判定FLASH是否处于空闲状态,擦除用
- 0X03 读数据 用于读取NOR FLASH数据
- 0X02 页写 用于写入NOR FLASH数据,最多写256字节
- 0X20 扇区擦除 扇区擦除指令,
最小擦除单位
(4096字节)
写使能 (06H)
执行页写,扇区擦除,块擦除,片擦除,写状态寄存器等指令前,需要写使能。
拉低CS片选 → 发送06H → 拉高CS片选
读状态寄存器(05H)
拉低CS片选 → 发送05H→ 返回SR1的值 → 拉高CS片选
读时序(03H)
拉低CS片选 → 发送03H→ 发送24位地址 → 读取数据(1~n) → 拉高CS片选
页写时序 (02H)
页写命令最多可以向FLASH传输256个字节的数据。
拉低CS片选 → 发送02H→ 发送24位地址 → 发送数据(1~n) → 拉高CS片选
扇区擦除时序(20H)
写入数据前,检查内存空间是否全部都是 0XFF ,不满足需擦除。
拉低CS片选 → 发送20H→ 发送24位地址 → 拉高CS片选
示例
入门demo代码图分解
初始化函数的图分解
向SPI设备写入数据图分析
读取数据数据图分析
总体图
里面漏了部分的拉高和拉低片选,但结合代码应该不会特别难以理解。
代码
w25q128.h
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "stdio.h"
#include "stdint.h"
#include "spi.h"
#include "usart.h"
#define W25Q128_CS_GPIO_PORT GPIOA
#define W25Q128_CS_GPIO_PIN GPIO_PIN_4
#define W25Q128_CS(x) x ? \
HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT,W25Q128_CS_GPIO_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT,W25Q128_CS_GPIO_PIN,GPIO_PIN_RESET)
#define W25Q128 0xEF17 /*W25Q128芯片ID*/
/*指令表*/
#define FLASH_WriteEnable 0x06
#define FLASH_ReadStatusReg1 0x05
#define FLASH_ReadData 0x03
#define FLASH_PageProgram 0x02
#define FLASH_SectorErase 0x20
#define FLASH_ChipErase 0xC7
#define FLASH_ManufactDeviceID 0x90
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);
void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);
uint16_t w25q128_read_id(void);
void w25q128_erase_sector(uint32_t saddr);
void w25q128_erase_chip(void);
void w25q128_write_enable(void);
uint8_t w25q128_rd_sr1(void);
void w25q128_init(void);
#endif
w25q128.c
#include "w25q128.h"
/*初始化w25q128*/
void w25q128_init(void)
{
uint16_t flash_type;
spi1_read_write_byte(0xFF);//清除数据缓冲寄存器
W25Q128_CS(1);//拉高CS
flash_type = w25q128_read_id();//读取FLASH的ID
printf("flash_type = %x\r\n",flash_type);
if(flash_type == W25Q128){
printf("check W25Q128\r\n");
}
}
/*等待空闲*/
static void w25q128_wait_busy(void)
{
//状态寄存器的最后一位BUSY是表示忙等待的,通过&运算进行获得最后一位
while((w25q128_rd_sr1() & 0x01) == 0x01);
}
/*读取W25Q128的状态寄存器1的值*/
uint8_t w25q128_rd_sr1(void)
{
uint8_t rec_data=0;
W25Q128_CS(0);
//发送读取指定状态寄存器的指令
spi1_read_write_byte(FLASH_ReadStatusReg1);
//0xff无效数据,只是为了发送而发送,不需要纠结
rec_data = spi1_read_write_byte(0xff);
W25Q128_CS(1);
return rec_data;
}
/*W25Q128写使能*/
void w25q128_write_enable(void)
{
W25Q128_CS(0);
//发送写使能指令
spi1_read_write_byte(FLASH_WriteEnable);
W25Q128_CS(1);
}
/*w25q128发送地址*/
static void w25q128_send_address(uint32_t address)
{
//w25q128的最大地址是24位的,
//即(256个块*16扇区*16页*256个字) - 1=0xFF FFFF,减1是因为地址从0开始
//而数据缓冲寄存器是8位的,所以要通过右移,来不断的进行数据传输
//SPI那边应该是不断的左移,然后得到最终地址
spi1_read_write_byte((uint8_t)(address >> 16));
spi1_read_write_byte((uint8_t)(address >> 8));
spi1_read_write_byte((uint8_t)address);
}
/*擦除整个芯片*/
void w25q128_erase_chip(void)
{
w25q128_write_enable();//写使能
w25q128_wait_busy();//忙等待
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ChipErase);//发送擦除整个芯片的命令:0xC7
W25Q128_CS(1);
w25q128_wait_busy();
}
/*擦除一个扇区*/
void w25q128_erase_sector(uint32_t saddr)
{
saddr *= 4096;//因为一个扇区的空间大小就是16页*256字=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();//等待空闲
}
/*读取芯片ID*/
uint16_t w25q128_read_id(void)
{
uint16_t deviceid=0;
W25Q128_CS(0);
spi1_read_write_byte(FLASH_ManufactDeviceID);//固定写法
spi1_read_write_byte(0);//固定写法
spi1_read_write_byte(0);//固定写法
spi1_read_write_byte(0);//固定写法
deviceid = spi1_read_write_byte(0xff) << 8;//因为寄存器总共只有8位,而芯片ID是16位
deviceid |= spi1_read_write_byte(0xff);
W25Q128_CS(1);
return deviceid;
}
/*读取SPI FLASH*/
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<datalen;i++)
{
pbuf[i] = spi1_read_write_byte(0xFF);
}
W25Q128_CS(1);
}
/*SPI在一页内写入少于256个字节的数据*/
//指向数据的指针、起始地址、数据长度
void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t i;
w25q128_write_enable();//写使能
W25Q128_CS(0);
spi1_read_write_byte(FLASH_PageProgram);//发送页写指令:0x02
w25q128_send_address(addr);//发送数据起始地址
//循环写入
for(i=0; i<datalen;i++)
{
spi1_read_write_byte(pbuf[i]);
}
W25Q128_CS(1);
w25q128_wait_busy();//忙等待
}
/*无检验写SPI_FLASH*/
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
uint16_t pageremain;
pageremain = 256 - addr % 256;//获取当前地址到当前页末尾剩余的地址空间大小
if(datalen <= pageremain){
pageremain = datalen;//如果剩余需要写入的长度小于页空间大小,直接赋值写入即可
}
while(1){
//当写入字节比页内剩余地址还少的时候,一次性写完
//多的话,先写一部分
w25q128_write_page(pbuf, addr, pageremain);
if(datalen == pageremain){
break;
}else {
//如果没写完,那么更新数据指针的指向
pbuf += pageremain;
//更新起始地址
addr += pageremain;
//剩余数据长度
datalen -= pageremain;
//判断下次循环能否一次性写完
if(datalen > 256){
pageremain = 256;
}else {
pageremain = datalen;
}
}
}
}
/*写SPI_FLASH*/
uint8_t g_w25q128_buf[4096];
// w25q128_write(datatemp,FLASH_WriteAddress,TEXT_SIZE);
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;//扇区剩余空间大小
if(datalen <= secremain){
secremain = datalen;
}
while(1){
w25q128_read(w25q128_buf, secpos * 4096, 4096);//备份扇区数据
//遍历扇区剩余空间的数据,检测是否需要进行擦除
//因为写入1只能靠擦除
for(i=0; i<secremain; i++){
if(w25q128_buf[secoff + i] != 0xFF){
break;
}
}
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){
/*
if(datalen <= secremain){
secremain = datalen;
}
*/
//如果剩余空间刚好《=要写入的数据的大小,那么直接break,因为已经写完了
break;
}else {
//如果剩余空间不够
secpos++;//扇区地址加1,相当于加了4096B
secoff = 0;//偏移量设置为0
pbuf += secremain;//数据指针移动到剩余数据的头
addr += secremain;//数据写入SPI设备的地址
datalen -= secremain;//数据长度降低
if(datalen > 4096){
//如果还是大于4096,那么代表下一个扇区直接覆盖
secremain = 4096;
}else {
//如果不是,那么赋值
secremain = datalen;
}
}
}
}
spi.c
uint8_t spi1_read_write_byte(uint8_t data)
{
uint8_t rec_data=0;
HAL_SPI_TransmitReceive(&hspi1,&data,&rec_data,1,1000);
return rec_data;
}
main.c
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
#define TEXT_SIZE 16
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
char datatemp[TEXT_SIZE];
int main(){
...............
w25q128_init();
sprintf((char *)datatemp,"huanghaibo");
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){.........};
......
}
附件
总体图如下,用Visio画的。如果看不清可以考虑下载
SPI入门流程图