一、SPI简介

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
  • 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
  • 同步,全双工
  • 支持总线挂载多设备(一主多从)
  • SCK:提供时钟信号,数据位的输出和输入,都是在SCK的上升沿或下降沿进行的。 

    MOSI:主机输出从机输入,主机配置为输出。从机配置为输入。  

    MISO:主机输入从机输出

    SS:从机选择线。有几个从机就有几条SS

    二、硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
  • 解释:

  • 因为有三个从机,所以有三个SS线,一共就是6跟线。因为都是单端型号,所以所有的设备还需要共地。从机没有独立供电的话,主机还需要额外引出电源正极VCC,给从机供电。 
  • SS线是低电平有效,同一时间只能置一个SS为低电平,只能选中一个从机,同时未选中的从机的MISO引脚,为高阻态,否则三个输出同时进入主机的输入会产生冲突
  • 对于输出,选择推挽输出,使得上升沿、下降沿非常迅速
  • 上图是SPI内部移位示意图

    步骤:SPI高位先行,每来一个时钟,移位寄存器都会向左进行移位。

    假设现在主机要将10101010发给从机,从机要将01010101发给主机。

    首先向左移位

     移出去的数据会在输出数据寄存器

    此时MOSI为高电平1,MISO为低电平0。这就是第一个时钟上升沿的结果 。那么在第一个时钟的下降沿,寄存器里的数据被分别采样输入到对应要去的最低位。

    这就是第一个时钟结束后的现象。

    三、SPI时序基本单元

  • 起始条件:SS从高电平切换到低电平
  • 终止条件:SS从低电平切换到高电平
  • 住:这里一共有四种模式,由CPOL和CPHA变换1、0来选择。是为了适配更多的设备而设置的。传输流程看上面的硬件电路部分。

    四、W25Q64简介

    注:上面有横线的都是低电平有效

    电路框图:

  • (1)和(2)描述的是存储器规划示意图,(2)被划分为若干个块“Block”,其中每一块再划分为若干个扇区(1)“Sector”。对于每个扇区又可以分为很多页  “Page” 。 
  • 在(2)里面,以64KB为一个基本单元,分了128块(因为一共8MB)。   
  • 在(1)里面,是对于块的更细的划分,以4KB为一份,分了16份。
  • 在写入数据时,还会有个更细的划分Page,256字节为一份。
  • (7)为SPI控制逻辑,接收指令和数据等
  • (8)状态寄存器,表示芯片是否处于忙状态、是否写使能、是否写保护
  • (9)写控制逻辑,配合WP引脚实现硬件写保护
  • (4)高电压生成器,配合Flash编程,Flash要实现掉电不丢失,就需要高电压来刺激。
  • (5)页地址锁存/计数器、(6)字节地址锁存/计数器,用来指定地址,读写操作
  • (3)256字节的RAM存储器,由于SPI写入频率非常高,所有写入数据先到RAM缓存区里,再写入Flash(2)里面。写入时会给状态寄存器(8)的BUSY位置1
  • 五、Flash操作注意事项

    写入操作时:

  • 写入操作前,必须先进行写使能
  • 每个数据位只能由1改写为0,不能由0改写为1
  • (由于上一条)写入数据前必须先擦除,擦除后,所有数据位变为1
  • 擦除必须按最小擦除单元进行(一个扇区)
  • 连续写入多字节时,最多写入一页的数据(256字节),超过页尾位置的数据,会回到页首覆盖写入
  • 写入操作结束后(写到RAM),芯片进入忙状态(当前在RAM转Flash,或者在擦除当中),不响应新的读写操作
  • 读取操作时:

  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
  • 六、代码部分

    MySPI.c

    #include "stm32f10x.h"                  // Device header
    
    /*引脚配置层*/
    
    /**
      * 函    数:SPI写SS引脚电平
      * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
      * 返 回 值:无
      * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
      */
    void MySPI_W_SS(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
    }
    
    /**
      * 函    数:SPI写SCK引脚电平
      * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
      * 返 回 值:无
      * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
      */
    void MySPI_W_SCK(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);		//根据BitValue,设置SCK引脚的电平
    }
    
    /**
      * 函    数:SPI写MOSI引脚电平
      * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~1
      * 返 回 值:无
      * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue为1时,需要置MOSI为高电平
      */
    void MySPI_W_MOSI(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);		//根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
    }
    
    /**
      * 函    数:I2C读MISO引脚电平
      * 参    数:无
      * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
      * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
      */
    uint8_t MySPI_R_MISO(void)
    {
    	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);			//读取MISO电平并返回
    }
    
    /**
      * 函    数:SPI初始化
      * 参    数:无
      * 返 回 值:无
      * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
      */
    void MySPI_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4、PA5和PA7引脚初始化为推挽输出
    	
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA6引脚初始化为上拉输入
    	
    	/*设置默认电平*/
    	MySPI_W_SS(1);											//SS默认高电平
    	MySPI_W_SCK(0);											//SCK默认低电平
    }
    
    /*协议层*/
    
    /**
      * 函    数:SPI起始
      * 参    数:无
      * 返 回 值:无
      */
    void MySPI_Start(void)
    {
    	MySPI_W_SS(0);				//拉低SS,开始时序
    }
    
    /**
      * 函    数:SPI终止
      * 参    数:无
      * 返 回 值:无
      */
    void MySPI_Stop(void)
    {
    	MySPI_W_SS(1);				//拉高SS,终止时序
    }
    
    /**
      * 函    数:SPI交换传输一个字节,使用SPI模式0
      * 参    数:ByteSend 要发送的一个字节
      * 返 回 值:接收的一个字节
      */
    uint8_t MySPI_SwapByte(uint8_t ByteSend)
    {
    	uint8_t i, ByteReceive = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
    	
    	for (i = 0; i < 8; i ++)						//循环8次,依次交换每一位数据
    	{
    		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
    		MySPI_W_MOSI(!!(ByteSend & (0x80 >> i)));	//使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
    		MySPI_W_SCK(1);								//拉高SCK,上升沿移出数据
    		if (MySPI_R_MISO()){ByteReceive |= (0x80 >> i);}	//读取MISO数据,并存储到Byte变量
    															//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
    		MySPI_W_SCK(0);								//拉低SCK,下降沿移入数据
    	}
    	
    	return ByteReceive;								//返回接收到的一个字节数据
    }
    

    W25Q64.c

    #include "stm32f10x.h"                  // Device header
    #include "MySPI.h"
    #include "W25Q64_Ins.h"
    
    /**
      * 函    数:W25Q64初始化
      * 参    数:无
      * 返 回 值:无
      */
    void W25Q64_Init(void)
    {
    	MySPI_Init();					//先初始化底层的SPI
    }
    
    /**
      * 函    数:W25Q64读取ID号
      * 参    数:MID 工厂ID,使用输出参数的形式返回
      * 参    数:DID 设备ID,使用输出参数的形式返回
      * 返 回 值:无
      */
    void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
    {
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_JEDEC_ID);			//交换发送读取ID的指令
    	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收MID,通过输出参数返回
    	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//交换接收DID高8位
    	*DID <<= 8;									//高8位移到高位
    	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//或上交换接收DID的低8位,通过输出参数返回
    	MySPI_Stop();								//SPI终止
    }
    
    /**
      * 函    数:W25Q64写使能
      * 参    数:无
      * 返 回 值:无
      */
    void W25Q64_WriteEnable(void)
    {
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_WRITE_ENABLE);		//交换发送写使能的指令
    	MySPI_Stop();								//SPI终止
    }
    
    /**
      * 函    数:W25Q64等待忙
      * 参    数:无
      * 返 回 值:无
      */
    void W25Q64_WaitBusy(void)
    {
    	uint32_t Timeout;
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);				//交换发送读状态寄存器1的指令
    	Timeout = 100000;							//给定超时计数时间
    	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)	//循环等待忙标志位
    	{
    		Timeout --;								//等待时,计数值自减
    		if (Timeout == 0)						//自减到0后,等待超时
    		{
    			/*超时的错误处理代码,可以添加到此处*/
    			break;								//跳出等待,不等了
    		}
    	}
    	MySPI_Stop();								//SPI终止
    }
    
    /**
      * 函    数:W25Q64页编程
      * 参    数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
      * 参    数:DataArray	用于写入数据的数组
      * 参    数:Count 要写入数据的数量,范围:0~256
      * 返 回 值:无
      * 注意事项:写入的地址范围不能跨页
      */
    void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
    {
    	uint16_t i;
    	
    	W25Q64_WriteEnable();						//写使能
    	
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);		//交换发送页编程的指令
    	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
    	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
    	MySPI_SwapByte(Address);					//交换发送地址7~0位
    	for (i = 0; i < Count; i ++)				//循环Count次
    	{
    		MySPI_SwapByte(DataArray[i]);			//依次在起始地址后写入数据
    	}
    	MySPI_Stop();								//SPI终止
    	
    	W25Q64_WaitBusy();							//等待忙
    }
    
    /**
      * 函    数:W25Q64扇区擦除(4KB)
      * 参    数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF
      * 返 回 值:无
      */
    void W25Q64_SectorErase(uint32_t Address)
    {
    	W25Q64_WriteEnable();						//写使能
    	
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);	//交换发送扇区擦除的指令
    	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
    	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
    	MySPI_SwapByte(Address);					//交换发送地址7~0位
    	MySPI_Stop();								//SPI终止
    	
    	W25Q64_WaitBusy();							//等待忙
    }
    
    /**
      * 函    数:W25Q64读取数据
      * 参    数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
      * 参    数:DataArray 用于接收读取数据的数组,通过输出参数返回
      * 参    数:Count 要读取数据的数量,范围:0~0x800000
      * 返 回 值:无
      */
    void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
    {
    	uint32_t i;
    	MySPI_Start();								//SPI起始
    	MySPI_SwapByte(W25Q64_READ_DATA);			//交换发送读取数据的指令
    	MySPI_SwapByte(Address >> 16);				//交换发送地址23~16位
    	MySPI_SwapByte(Address >> 8);				//交换发送地址15~8位
    	MySPI_SwapByte(Address);					//交换发送地址7~0位
    	for (i = 0; i < Count; i ++)				//循环Count次
    	{
    		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);	//依次在起始地址后读取数据
    	}
    	MySPI_Stop();								//SPI终止
    }
    

    作者:2401_88885971

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32软件SPI通信详解

    发表回复