目录
1、SPI介绍
2、SPI的优点、缺点、特点
3、SPI的物理架构
4、SPI的工作原理
5、SPI的工作模式
6、W25Q128介绍
7、实验:使用SPI通信读写W25Q128模块
cubeMX配置
代码实现
效果展示
8、推荐去看的博客
SPI是串行外设接口(Serial Peripheral Interface)的缩写。是 Motorola 公司推出的一 种同步串行接口技术,是一种高速的,全双工,同步的通信总线。
SPI接口是在CPU和外围低速器件之间进行同步串行数据传输,在主器件的移位脉冲下,数据按位传输,低位在前高位在后,是全双工通信,数据的传输速度总体比I2C总线快,速度可以达到几Mbps。
优点:
缺点:
特点:
SPI 包含 4 条总线,SPI 总线包含 4 条总线,分别为SS、SCK、MOSI、MISO。它们的作用介绍如下 :
在这里可以看出spi通信时,显现出一个环形结构 ,当主机发送一个长度的数据给从机的时候,从机也会相应的发送一个长度的数据给主机
说明:SPI只有主模式和从模式之分,没有读写的说法,外设的写操作和读操作是同步完成的。若只有写操作,主机只需忽略接收到的字节(虚拟数据);反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收到一个数据必须也要先发一个数据。
时钟极性(CPOL):
没有数据传输时时钟线的空闲状态电平
时钟相位(CPHA):
时钟线在第几个时钟边沿采样数据
模式0和模式3最常用
模式0时序图:
模式3时序图:
需要注意的是:我们的主设备能够控制时钟(CS),因为我们的SPI通信并不像UART或者IIC通信那样有专门的通信周期,有专门的通信起始信号,有专门的通信结束信号;所以我们的SPI协议能够通过控制时钟信号线,当没有数据交流的时候我们的时钟线要么是保持高电平要么是保持低电平
1.什么是W25Q128?
2.W25Q128存储架构
3.W25Q128常用指令
写使能(06H)
读状态寄存器(05H)
拉低CS片选 → 发送05H→ 返回SR1的值 → 拉高CS片选
读时序(03H)
页写时序(02H)
扇区擦除时序(20H)
4.W25Q128状态寄存器
5.W25Q128常见操作
读操作:拉低CS片选 → 发送读命令(03H)→ 发送24位地址 → 读取数据→拉高CS片选
擦除扇区:拉低CS片选 → 发送写使能命令(06H)→ 等待空闲→ 发送擦除扇区命令(20H)→ 发送24位地址 → 等待空闲→拉高CS片选
写操作:
实验内容:使用SPI通信输入数据给W25Q128模块,再读取模块内刚刚输入进去的数据,最后通过串口输出
用到的库函数
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
uint8_t *pTxData,
uint8_t *pRxData, uint16_t Size,
uint32_t Timeout)参数一:SPI_HandleTypeDef *hspi,spi句柄
参数二:uint8_t *pTxData,待主机输入的数据的指针(指向传输数据缓冲区的指针)
参数三:uint8_t *pRxData, 待从机输出的数据的指针(指向接收数据缓冲区的指针)
参数四:uint16_t Size,传输数据的长度
参数五:uint32_t Timeout,设置超时时间
返回值:HAL_StatusTypeDef,HAL状态(OK,busy,ERROR,TIMEOUT)
需要修改三个文件,需要添加两个文件
w25q128.c
#include "w25q128.h"
#include "spi.h"
#include "stdio.h"
/**
* @brief 初始化W25Q128
* @param 无
* @retval 无
*/
void w25q128_init(void)//已看
{
uint16_t flash_type;//芯片id
//spi1_read_write_byte(0xFF); /* 清除DR的作用,DR数据寄存器*/这里其实可以不用
W25Q128_CS(1);//拉高不选中
flash_type = w25q128_read_id(); /* 读取FLASH ID. */
if (flash_type == W25Q128)//芯片id16位站两个字节
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;//一个扇区有4096字节,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); //发送读取指令之后需要发送000000h24位的bit地址信号来启动命令
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,最大是24bit)
* @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; /* 扇区地址,一个扇区占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) /* 需要擦除,其实也就是上面for循环没结束之前就跳出循环了就说明我们写入数据的地方本来有数据需要我们擦除*/
{
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;/* 下一个扇区可以写完了 */
}
}
}
}
w25q128.h
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "stdint.h"
/* W25Q128片选引脚定义 */
#define W25Q128_CS_GPIO_PORT GPIOA
#define W25Q128_CS_GPIO_PIN GPIO_PIN_4
/* W25Q128片选信号 */
#define W25Q128_CS(x) do{ 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); \
}while(0)
/* FLASH芯片列表 */
#define W25Q128 0XEF17 /* W25Q128 芯片ID */
/* 指令表 */
#define FLASH_WriteEnable 0x06 //写使能
#define FLASH_ReadStatusReg1 0x05 //读SR1读取状态寄存器的值
#define FLASH_ReadData 0x03 //读数据
#define FLASH_PageProgram 0x02 //页写
#define FLASH_SectorErase 0x20 //扇区擦除
#define FLASH_ChipErase 0xC7
#define FLASH_ManufactDeviceID 0x90 //获取芯片id
/* 静态函数 */
static void w25q128_wait_busy(void); /* 等待空闲 */
static void w25q128_send_address(uint32_t address);/* 发送地址 */
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写入page */
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写flash,不带擦除 */
/* 普通函数 */
void w25q128_init(void); /* 初始化25QXX */
uint16_t w25q128_read_id(void); /* 读取FLASH ID */
void w25q128_write_enable(void); /* 写使能 */
uint8_t w25q128_rd_sr1(void); /* 读取寄存器1的值 */
void w25q128_erase_chip(void); /* 整片擦除 */
void w25q128_erase_sector(uint32_t saddr); /* 扇区擦除 */
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 读取flash */
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写入flash */
#endif
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;//返回接收的字节
}
usart.c(对printf进行重构)记得要勾上Use MicroLIB
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
main.c
//前面需要引用对应的头文件
//main函数外面
#define TEXT_SIZE 16
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
//mian函数里面
uint8_t datatemp[TEXT_SIZE];//需要发送的数据
w25q128_init();//模块初始化
/* 写入测试数据 */
sprintf((char *)datatemp, "liangxu shuai");//sprintf格式化输出到datatemp
w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);//写入liangxu shuai,FLASH_WriteAddress=0x000000
printf("数据写入完成!\r\n");
/* 读出测试数据 */
memset(datatemp, 0, TEXT_SIZE);//清空数据源
w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);//读取我们刚刚写进去的数据FLASH_ReadAddress=0x000000
printf("读出数据:%s\r\n", datatemp);//在串口输出
一文搞懂SPI通信协议
SPI通信协议(SPI总线)学习
SPI、I2C、UART三种串行总线的原理、区别及应用
SPI学习(二):SPI工作机制与协议解析
关于读的时候为什么要发送0xff问题
SPI写入数据的时候记得读取,不然会一直读出0xFF!
使用spi协议,接收来自slave的数据之前写0xff的原因
为什么SPI在读数据时要写入一个0xff先
写在最后:文章有向其他文章转载的内容,如有侵权请联系,可以马上删除