简介
特性
- SS:从设备选择信号线,常称为片选信号线,也称为NSS、CS
-
SCK(Serial Clock):时钟信号线,用于通讯数据同步
-
MOSI (Master Output , Slave Input) : 主设备输出/ 从设备输入引脚。
-
MISO(Master Input, , Slave Output) : 主设备输入/ 从设备输出引脚。
3、 多从机只需要增加SS片选信号线
4、速率高,最高频率可达到fplck/2,受限于低速设备(例如STM32F407的APB2总线最高可达42MHz)
通讯过程
- NSS(片选信号线)由高变低,是SPI的起始信号
- 触发:是数据在交换位,此时数据无线
- 采样:是数据有效,读取数据采样
- NSS线又低变高,意味着SPI通讯结束
- MOSI和MISO是同步的,每发送一位就可以接收一位
采样模式
通过切换时钟极性(CPOL)和时钟相位(CPHA)可以更改SPI的采样模式
CPOL = 0 :SCK起始信号为低电平
CPOL = 1 :SCK起始信号为高电平
CPHA = 0 :对奇数边缘采样
CPHA = 1 :对偶数边缘采样
一般常用的是模式0和模式3,例如flash的W25Q128只支持模式0和模式3
详细通讯过程
模式3
对flash发送数据需要等待TXE发送寄存器为reset,接收则需要等待RXNE接收非空寄存器reset。
下面是对flash写入和接收1字节数据的函数代码
/* ------------------SPI对flash写入1字节数据----------------------- */
// data :要写入flash的数据
uint32_t SPI_WriteByte(uint8_t data)
{
//等待事件响应
TimeOut_count = SPI_time_out;
while (SPI_I2S_GetFlagStatus(SPI_FLASH,SPI_I2S_FLAG_TXE) == RESET)
{
if ((TimeOut_count--) == 0) return SPI_timeout_callback(0);
}
//发送要写入的数据
SPI_I2S_SendData(SPI_FLASH, data);
//等待事件响应
TimeOut_count = SPI_time_out;
while (SPI_I2S_GetFlagStatus(SPI_FLASH,SPI_I2S_FLAG_RXNE) == RESET)
{
if ((TimeOut_count--) == 0) return SPI_timeout_callback(1);
}
//接受返回的数据
return SPI_I2S_ReceiveData(SPI_FLASH);
}
发送和接收都是这个函数,因为SPI是全双工的,在发送1个字节的同时就会返回1个字节的数据
代码编写过程
对SPI在总线上查找
查找spi1对应引脚,对应开发板硬件原理图,我的开发板是STMF407
根据开发板原理图
cs片选引脚:PG6
SCK:PB3
MISO:PB4
MOSI:PB5
根据引脚可以编写对应的SPI头文件宏
/*SPI引脚参数定义*/
#define SPI_FLASH SPI1
#define SPI_FLASH_CLK RCC_APB2Periph_SPI1
#define SPI_FLASH_INIT RCC_APB2PeriphClockCmd
/*SCK引脚*/
#define SPI_FLASH_SCK_PIN GPIO_Pin_3
#define SPI_FLASH_SCK_GPIO_PORT GPIOB
#define SPI_FLASH_SCK_GPIO_CLK RCC_AHB1Periph_GPIOB
#define SPI_FLASH_SCK_SOURCE GPIO_PinSource3
#define SPI_FLASH_SCK_AF GPIO_AF_SPI1
/*MISO引脚*/
#define SPI_FLASH_MISO_PIN GPIO_Pin_4
#define SPI_FLASH_MISO_GPIO_PORT GPIOB
#define SPI_FLASH_MISO_GPIO_CLK RCC_AHB1Periph_GPIOB
#define SPI_FLASH_MISO_SOURCE GPIO_PinSource4
#define SPI_FLASH_MISO_AF GPIO_AF_SPI1
/*MOSI引脚*/
#define SPI_FLASH_MOSI_PIN GPIO_Pin_5
#define SPI_FLASH_MOSI_GPIO_PORT GPIOB
#define SPI_FLASH_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOB
#define SPI_FLASH_MOSI_SOURCE GPIO_PinSource5
#define SPI_FLASH_MOSI_AF GPIO_AF_SPI1
/*CS引脚*/
#define SPI_FLASH_CS_PIN GPIO_Pin_6
#define SPI_FLASH_CS_GPIO_PORT GPIOG
#define SPI_FLASH_CS_GPIO_CLK RCC_AHB1Periph_GPIOG
/*拉高拉低CS引脚*/
#define SPI_FLASH_CS_LOW() GPIO_ResetBits(SPI_FLASH_CS_GPIO_PORT,SPI_FLASH_CS_PIN)
#define SPI_FLASH_CS_HIGH() GPIO_SetBits(SPI_FLASH_CS_GPIO_PORT,SPI_FLASH_CS_PIN)
功能函数
对应flash W25Q128数据手册,编写对应的功能函数
根据对应的功能写出对应的宏增加代码可读性
上图的DUMMY是无效数据就用0xFF
#define DUMMY 0xFF
/*命令定义-开头*******************************/
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
下面是函数介绍
写使能功能函数
/*
写使能函数
*/
void SPI_FLASH_WriteEnable(void)
{
SPI_FLASH_CS_LOW();
SPI_WriteByte(W25X_WriteEnable);
SPI_FLASH_CS_HIGH();
}
等待写完毕状态函数
/*
等待BUSY位为0,即等待Flash内部数据写入完毕
*/
void SPI_FLASH_WaitForWriteEnd(void)
{
uint8_t flash_status = 0;
SPI_FLASH_CS_LOW();
SPI_WriteByte(W25X_ReadStatusReg);
TimeOut_count = SPI_time_out;
do
{
flash_status = SPI_WriteByte(DUMMY);
if((TimeOut_count--) == 0)
{
SPI_timeout_callback(2);
break;
}
} while((flash_status & 0x01) == SET);
SPI_FLASH_CS_HIGH();
}
扇区擦除功能函数(Sector_Erase)
/*
扇区擦除函数
addr:要擦除的扇区
*/
void Sector_Erase(uint32_t addr)
{
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
SPI_FLASH_CS_LOW();
SPI_WriteByte(W25X_SectorErase);
SPI_WriteByte((addr>>16) & 0xFF);
SPI_WriteByte((addr>>8) & 0xFF);
SPI_WriteByte(addr & 0xFF);
SPI_FLASH_CS_HIGH();
SPI_FLASH_WaitForWriteEnd();
}
页读取功能函数(Page_write)
/*
写一页flash数据
addr:要写入的地址起始
buff:写入的的暂存缓冲区
size:写的字节数 page一定要在256以内
*/
void Page_write(uint32_t addr,uint8_t *buff,uint32_t size)
{
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
SPI_FLASH_CS_LOW();
SPI_WriteByte(W25X_PageProgram);
SPI_WriteByte((addr>>16) & 0xFF);
SPI_WriteByte((addr>>8) & 0xFF);
SPI_WriteByte(addr & 0xFF);
while(size--)
{
SPI_WriteByte(*buff);
buff++;
}
SPI_FLASH_CS_HIGH();
SPI_FLASH_WaitForWriteEnd();
}
读取Flash_ID函数
/*
发送0xAB读取flashID
*/
uint8_t Read_flash_ID(void)
{
uint8_t id;
//拉低CS片选引脚
SPI_FLASH_CS_LOW();
//写指令
SPI_WriteByte(W25X_ReleasePowerDown);
SPI_WriteByte(DUMMY);
SPI_WriteByte(DUMMY);
SPI_WriteByte(DUMMY);
//读指令
id = SPI_WriteByte(DUMMY);
//拉高CS片选引脚 传输结束
SPI_FLASH_CS_HIGH();
return id;
}
大量数据写入函数(不限制与page页大小)
基于Page_write函数做了逻辑处理
/*
写flash数据
addr:要写入的地址起始
buff:写入的的暂存缓冲区
size:写的字节数
*/
void Buffer_write(uint32_t addr,uint8_t *buff,uint32_t size)
{
u8 num_signgle , num_page , count ,temp;
num_signgle = addr % 256; //求出首地址是否对齐
count = 256 - num_signgle; //首页剩余要写的字节
num_page = size / 256; //若对齐的页数
temp = size % 256; //如果对齐的话最后一页剩余要补的字节
if(num_signgle != 0) //首页没对齐的情况
{
num_page = (size - count)/256; //重新算出没对齐后的页数
if(num_page == 0)
{
if(size > count) //虽然是0页但也可能存在尾部跨页数的存在
{
Page_write(addr , buff , count);
addr += count;
buff += count;
Page_write(addr , buff , (size-count));
}
else //没跨页数
{
Page_write(addr , buff , size);
}
}
else //没对齐且不止一页
{
Page_write(addr , buff , count); //补齐首页
addr += count;
buff += count;
while(num_page--) //写中间完整页
{
Page_write(addr , buff , 256);
addr += 256;
buff += 256;
}
temp = (size-count)%256; //若有剩余补尾页页
if(temp != 0)
{
Page_write(addr , buff , temp);
}
}
}
else
{
if(num_page == 0) //对齐0页直接写
{
Page_write(addr , buff , size);
}
else
{
while(num_page--) //对齐直接完整页
{
Page_write(addr , buff , 256);
addr += 256;
buff += 256;
}
if(temp != 0) //若有剩余补尾页页
{
Page_write(addr , buff , temp);
}
}
}
}