7.0 SPI原理
7.0.1 SPI
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)
主输出从输入
、MISO(Master Input Slave Output)、SS(Slave Select)同步,全双工 - 支持总线挂载多设备(一主多从)
7.0.2硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
- SS默认状态的高电平,需要用到时拉低电平
- MISO为了避免多个数据冲突,不使用时,为高阻态状态,即为关断输出。当ss为低电平时,才允许变为推挽输出。
7.0.3 移位示意图
移位寄存器的时钟源为主机提供的,叫波特率发生器。
1.一开始我们给一个脉冲信号。(模式1)
2.当产生上升沿的时候,所有移位寄存器向左移出一位到引脚,实际是放到了输出数据寄存器,然后1为高电平,MOSI就变为高电平,从机移位为0,低电平,所有MISO为低电平。
3.下降沿的时候,采样输入到移位寄存器的最低位。相当于下降沿时,采样数据
4.如果只是输出到从机的话,我们就不用管从机输入进来的数据就可以。
如果是输入到主机的话,我们就不用管主机输出的数据就可以。这样就完成了单项输入输出。一般不用的数据我们都为0X00或者0XFF。
7.0.4 SPI时序
读数据的时候都为下降沿
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
1.交换一个字节(模式0)
CPOL(Clock Polarity 时钟极性)= 0:空闲状态时,SCK为低电平
CPHA(Clock Phase 时钟相位)= 0:SCK第一个边沿移入数据,第二个边沿移出数据
解释:当SS变成低电平的时候,SCK产生变化之前,我们要先移出数据,当SCK第一个边沿时,移入数据,第二个边沿移出数据。
2.交换一个字节(模式1)
CPOL=0:空闲状态时,SCK为低电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
解释:CPOL=0,相当于SCK默认为低电平;当来上升沿时,主机从机同时移出数据,当下降沿时,同时采样数据。
3.交换一个字节(模式2)
CPOL(Clock Polarity 时钟极性)=1:空闲状态时,SCK为高电平
CPHA(Clock Phase 时钟相位)=0:SCK第一个边沿移入数据,第二个边沿移出数据
解释:和模式0相比,就相当于默认SCK为高电平
4.交换一个字节(模式3)
CPOL=1:空闲状态时,SCK为高电平
CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
解释:和模式1相比就相当于SCK为高电平
总结:
7.1W25Q64简介
cs上面的横线,表示低电平有效。
1.W25Q40-W25Q128都是16位寻址的,所以内存就是00 00 00-FF FF FF,我们的芯片是64Mbit,所以就是到7F FF FF。
2.快分配,block,8M*1024/64=128个快,一个Block是64kb。
3.每个块的起始地址位XX0000—XXFFFF。
4.每个快在按照4KB来划分。64Kb/4kb=16个扇区(0-15),每个扇区内的地址XXX 000-XXXFFF
5.一个页是256byte,一个扇区是4*1024=16*256.所以一个扇区可以分为16页。
6.一页地址的变化XXXX00-XXXXFF。
状态寄存器需要注意:
11.1.1 BUSY
BUSY是状态寄存器(S0)中的一个只读位,当设备正在执行页面程序、扇区擦除、块擦除、芯片擦除或写状态寄存器时,它被设置为1状态 指示在此期间,设备将忽略除读取状态寄存器和擦除暂停指令外的其他指令(参见交流特性中的tW、tPP、tSE、tBE和tCE)。Wh 在程序、擦除或写入状态寄存器指令完成后,BUSY位将被清除为0状态,表明设备已经准备好接受进一步的指示。
11.1.2写入启用锁存(WEL)
写入启用锁存(WEL)是状态寄存器(S1)中的只读位,在执行写入启用指令后被设置为1。WEL状态位已被清除 当设备被禁用写入时,发送到0。写入关闭状态发生在通电或执行以下任何指令之后:写入关闭、页面程序、扇区擦除、块擦除、芯片擦除 d写入状态寄存器。
7.2 SPI函数详解
在stm32f1xx_hal_spi.h头文件中可以看到spi的操作函数。分别对应轮询,中断和DMA三种控制方式。
· 轮询: 最基本的发送接收函数 ,就是正常的发送数据和接收数据
· 中断: 在SPI发送或者接收完成的时候,会进入SPI回调函数,用户可以编写回调函数,实现设定功能
· DMA: DMA传输SPI数据
利用SPI接口发送和接收数据主要调用以下两个函数:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
SPI发送数据函数:
HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
参数:
· *hspi: 选择SPI1/2,比如&hspi1,&hspi2
· *pData : 需要发送的数据,可以为数组
· Size: 发送数据的字节数,1 就是发送一个字节数据
· Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数
SPI接收数据函数:
HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
参数:
· *hspi: 选择SPI1/2,比如&hspi1,&hspi2
· *pData : 接收发送过来的数据的数组
· Size: 接收数据的字节数,1 就是接收一个字节数据
· Timeout: 超时时间,就是执行接收函数最长的时间,超过该时间自动退出接收函数
SPI接收回调函数:
HAL_SPI_TransmitReceive_IT(&hspi1, TXbuf,RXbuf,CommSize);
当SPI上接收出现了 CommSize个字节的数据后,中断函数会调用SPI回调函数
HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
用户可以重新定义回调函数,编写预定功能即可,在接收完成之后便会进入回调函数
片选引脚:
因为我们是软件使能片选,定义片选引脚,CS片选低电平为有效使能, CS片选高电平不使能
7.3 STM32CubeMx之硬件SPI驱动W25Q64
7.3.1 硬件接口
引脚 | 说明 |
CS | 片选(低电平选中) PG6 |
SPI2_MISO | 主机输入从机输出PB4 |
SPI2_MOSI | 主机输出从机输入PB5 |
SPI_SCK | 时钟线PB3 |
7.3.2CUBE设置
7.3.3Keil 5代码编写
1.先写一下spi的驱动,CS引脚和读一个字节就好了
/* USER CODE BEGIN 1 */
//CS引脚0:启动。1:关闭
void MySPI_W_CS(uint8_t BitValue)
{
HAL_GPIO_WritePin(W25Q128_CS_GPIO_Port,W25Q128_CS_Pin,BitValue);
}
//SPI读一个字节
uint8_t SPI_writeOneByte(uint8_t data)
{
uint8_t Rx_data=0;
HAL_SPI_TransmitReceive(&hspi1,&data,&Rx_data,1,100);
return Rx_data;
}
/* USER CODE END 1 */
/* USER CODE BEGIN Prototypes */
void MySPI_W_CS(uint8_t BitValue);
uint8_t SPI_writeOneByte(uint8_t data);
/* USER CODE END Prototypes */
2.开始写W25Q128的驱动
#include "W25Q128.h"
/*******************************************************************************
* Function Name : W25Q128_ReadID
* Description : 获取W25Q128设备号
*
*
* Input : *MID 厂家ID号
* *DID 设备ID号
*
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_ReadID(uint8_t *MID,uint16_t *DID)
{
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_JEDEC_ID);
*MID=SPI_writeOneByte(0xFF);
*DID=SPI_writeOneByte(0xFF);
*DID<<=8;
*DID|=SPI_writeOneByte(0xFF);
MySPI_W_CS(1);
}
/*******************************************************************************
* Function Name : W25Q128_WriteEnable
* Description : 写使能
*
*
* Input : None
* None
*
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_WriteEnable(void)
{
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_WRITE_ENABLE);
MySPI_W_CS(1);
}
/*******************************************************************************
* Function Name : W25Q128_WaitBusy
* Description : 等待忙碌 写入操作结束后,芯片进入忙状态,不响应新的读写操作
* 读到的最低为为0x01时代表忙碌0x00代表不忙
*
* Input : None
* None
*
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_WaitBusy(void)
{
uint32_t Timeout=10000;
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_READ_STATUS_REGISTER_1);
while ((SPI_writeOneByte(0xFF) & 0x01) == 0x01)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
MySPI_W_CS(1);
}
/*******************************************************************************
* Function Name : W25Q128_SectorErase
* Description : 扇区擦除 256个字节
*
*
* Input : Address:擦除地址所在的扇区。
* None
*
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_SectorErase(uint32_t Address)
{
W25Q128_WriteEnable();
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_SECTOR_ERASE_4KB);
SPI_writeOneByte(Address>>16);
SPI_writeOneByte(Address>>8);
SPI_writeOneByte(Address);
MySPI_W_CS(1);
W25Q128_WaitBusy();
}
/*******************************************************************************
* Function Name : W25Q128_ChipErase
* Description : 芯片擦除
*
*
* Input : None
* None
*
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_ChipErase(void)
{
W25Q128_WriteEnable();
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_CHIP_ERASE);
MySPI_W_CS(1);
W25Q128_WaitBusy();
}
/*******************************************************************************
* Function Name : W25Q128_PageProgram
* Description : 页写入,只能在当前地址页写入,超过会从当前页起始位置开始写。
写入前擦除当前页,芯片规则:只能1改0,不能0改1,相当于前一个数据&之后数据。
例:之前为0x01改为0x02,如果不拆除就变为:0x01&0x02=0x00。
*
*
* Input : Address:写入地址
* *DataArray:写入数据数组
* Count:数组长度
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_PageProgram(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
W25Q128_WriteEnable();
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_PAGE_PROGRAM);
SPI_writeOneByte(Address>>16);
SPI_writeOneByte(Address>>8);
SPI_writeOneByte(Address);
for(int i=0;i<Count;i++)
{
SPI_writeOneByte(DataArray[i]);
}
MySPI_W_CS(1);
W25Q128_WaitBusy();
}
/*******************************************************************************
* Function Name : W25Q128_ReadData
* Description : 读数据,可以读任意地址,任意个数,可跨页读
*
*
* Input : Address:写入地址
* *DataArray:写入数据数组
* Count:数组长度
* Output : None
* Return : None
*******************************************************************************/
void W25Q128_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
MySPI_W_CS(0);
SPI_writeOneByte(W25Q128_READ_DATA);
SPI_writeOneByte(Address>>16);
SPI_writeOneByte(Address>>8);
SPI_writeOneByte(Address);
for(int i=0;i<Count;i++)
{
DataArray[i]=SPI_writeOneByte(0xFF);
}
MySPI_W_CS(1);
}
/*******************************************************************************
* Function Name : dev_flash_write_bytes_nocheck
* Description : 写数据 自动查询当前地址 自动越扇区写入 注意这里只能写擦除过的
*
*
* Input : pdata 数据
* write_addr 写入地址
* write_length 写入长度
* Output : None
* Return : None
*******************************************************************************/
void dev_flash_write_bytes_nocheck(uint8_t *pdata, uint32_t write_addr, uint16_t write_length) // 0x01 0x02 0x03 , 0x0000FE ,3
{
// 计算当前页剩余的字节数
uint16_t PageByte = 256 - write_addr % 256; // PageByte= 2;
// 如果写入长度小于等于剩余字节数,则实际写入的字节数等于写入长度
if(write_length <= PageByte)
{
PageByte = write_length;
}
// 进入循环
while(1)
{
// 调用 W25Q128_PageProgram 函数将数据写入一页闪存
W25Q128_PageProgram(write_addr,pdata,PageByte); // 1:0x0000FE=0x01,0x0000FF=0x02
// 2:write_addr=0x000100,pdata=0x03,PageByte=1.0x000100=0x003
// 如果写入长度等于实际写入的字节数,则退出循环
if(write_length == PageByte) //1=1,break
break;
else
{
// 更新数据指针、写入地址和写入长度
pdata += PageByte; //pdata 直接指向0x03,相当于指针右移了PageByte位
write_addr += PageByte; //write_addr = 0x000100
write_length -= PageByte; //write_length=3-2=1
// 根据剩余的写入长度确定下一次循环中实际写入的字节数
if(write_length > 256)
{
PageByte = 256;
}
else
{
PageByte = write_length; //PageByte =1
}
}
}
}
#ifndef __W25Q128_H__
#define __W25Q128_H__
#include "main.h"
#include "gpio.h"
#include "spi.h"
#include "stm32F4xx_hal.h"
#include <string.h>
#define W25Q128_WRITE_ENABLE 0x06
#define W25Q128_WRITE_DISABLE 0x04
#define W25Q128_READ_STATUS_REGISTER_1 0x05
#define W25Q128_READ_STATUS_REGISTER_2 0x35
#define W25Q128_WRITE_STATUS_REGISTER 0x01
#define W25Q128_PAGE_PROGRAM 0x02
#define W25Q128_QUAD_PAGE_PROGRAM 0x32
#define W25Q128_BLOCK_ERASE_64KB 0xD8
#define W25Q128_BLOCK_ERASE_32KB 0x52
#define W25Q128_SECTOR_ERASE_4KB 0x20
#define W25Q128_CHIP_ERASE 0xC7
#define W25Q128_ERASE_SUSPEND 0x75
#define W25Q128_ERASE_RESUME 0x7A
#define W25Q128_POWER_DOWN 0xB9
#define W25Q128_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q128_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q128_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q128_MANUFACTURER_DEVICE_ID 0x90
#define W25Q128_READ_UNIQUE_ID 0x4B
#define W25Q128_JEDEC_ID 0x9F
#define W25Q128_READ_DATA 0x03
#define W25Q128_FAST_READ 0x0B
#define W25Q128_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q128_FAST_READ_DUAL_IO 0xBB
#define W25Q128_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q128_FAST_READ_QUAD_IO 0xEB
#define W25Q128_OCTAL_WORD_READ_QUAD_IO 0xE3
//获取W25Q128设备号
void W25Q128_ReadID(uint8_t *MID,uint16_t *DID);
//写使能
void W25Q128_WriteEnable(void);
//等待忙碌
void W25Q128_WaitBusy(void);
//扇区擦除
void W25Q128_SectorErase(uint32_t Address);
//芯片擦除
void W25Q128_ChipErase(void);
//页写入
void W25Q128_PageProgram(uint32_t Address, uint8_t *DataArray, uint32_t Count);
//读数据
void W25Q128_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);
//连续页写入
void dev_flash_write_bytes_nocheck(uint8_t *pdata, uint32_t write_addr, uint16_t write_length);
#endif
7.3.4代码验证
1.首先我们创建一些数组
/* USER CODE BEGIN PV */
uint8_t MID=0;
uint16_t DID=0;
uint8_t ArrayWrite[3];
uint8_t ArrayRead[3];
/* USER CODE END PV */
2.查看一下ID号
W25Q128_ReadID(&MID,&DID);
printf("W25Q128_ReadID = %X, %X\r\n",MID,DID);
现象:
3.写入一些数据,读一些数据
for(int i=0;i<3;i++)
{
ArrayWrite[i]=i;
}
W25Q128_SectorErase(0x000000);
HAL_Delay(10);
W25Q128_PageProgram(0x0000FE,ArrayWrite,3);
W25Q128_ReadData(0x0000FE,ArrayRead,3);
for(int i=0;i<3;i++)
{
printf("ArrayWrite[%d]= %X ",i,ArrayWrite[i]);
}
printf("\r\n");
for(int i=0;i<3;i++)
{
printf("ArrayRead[%d]= %X ",i,ArrayRead[i]);
}
printf("\r\n");
现象:
ArryRead[2]=FF,正好说明,不能跨页写入。可以跨页读
4.跨页写入代码测试
for(int i=0;i<3;i++)
{
ArrayWrite[i]=i;
}
W25Q128_SectorErase(0x000000);
HAL_Delay(10);
W25Q128_SectorErase(0x000100);
HAL_Delay(10);
dev_flash_write_bytes_nocheck(ArrayWrite,0x0000FE,3);
// W25Q128_PageProgram(0x0000FE,ArrayWrite,3);
W25Q128_ReadData(0x0000FE,ArrayRead,3);
for(int i=0;i<3;i++)
{
printf("ArrayWrite[%d]= %X ",i,ArrayWrite[i]);
}
printf("\r\n");
for(int i=0;i<3;i++)
{
printf("ArrayRead[%d]= %X ",i,ArrayRead[i]);
}
printf("\r\n");
现象:
说明跨页写入代码没问题。
5.验证只能1改0,不能0改1
uint8_t ArrayWrite[3]={0x01,0x02,0x03};
uint8_t ArrayRead[3];
W25Q128_SectorErase(0x000000);
HAL_Delay(10);
dev_flash_write_bytes_nocheck(ArrayWrite,0x0000FE,3);
// W25Q128_PageProgram(0x0000FE,ArrayWrite,3);
W25Q128_ReadData(0x0000FE,ArrayRead,3);
for(int i=0;i<3;i++)
{
printf("ArrayWrite[%d]= %X ",i,ArrayWrite[i]);
}
printf("\r\n");
for(int i=0;i<3;i++)
{
printf("ArrayRead[%d]= %X ",i,ArrayRead[i]);
}
printf("\r\n");
现象:
当我们不擦除页,改变数组时
uint8_t ArrayWrite[3]={0x02,0x03,0x04};
uint8_t ArrayRead[3];
// W25Q128_SectorErase(0x000000);
// HAL_Delay(10);
// W25Q128_SectorErase(0x000100);
// HAL_Delay(10);
dev_flash_write_bytes_nocheck(ArrayWrite,0x0000FE,3);
// W25Q128_PageProgram(0x0000FE,ArrayWrite,3);
W25Q128_ReadData(0x0000FE,ArrayRead,3);
for(int i=0;i<3;i++)
{
printf("ArrayWrite[%d]= %X ",i,ArrayWrite[i]);
}
printf("\r\n");
for(int i=0;i<3;i++)
{
printf("ArrayRead[%d]= %X ",i,ArrayRead[i]);
}
printf("\r\n");
现象:
0x01&0x02=0,0x02&0x03=2,0x03&0x04=0.所以说明只能由1改0,不能0改1。
结束