STM32系列:SPI通信详解与实现

11 SPI通信

  • I2C通信的优缺点:
  • 优点:在硬件上使用最少的通信线,实现软件上最多的功能;
  • 缺点:由于I2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力较弱,这会导致通信线由低电平变成高电平的时候,上升沿耗时比较长,这会限制I2C的最大通信速度。所以I2C的标准模式只有100KHz的时钟频率,I2C的快速模式也只有400KHz。虽然I2C协议后面通过改进电路的方式设计出了高速模式,可以达到3.4MHz,但是目前该模式的普及程度不是很高,所以一般认为I2C的时钟速度最多就是400KHz,这个速度相对于SPI是慢了很多的。
  • 11.1 SPI通信简介

  • SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线;

  • 四根通信线:

  • SCK(Serial Clock):串行时钟线;
  • 也可以称作SCLK、CLK、CK;
  • MOSI(Master Output Slave Input):主机输出,从机输入;
  • 也可以称作DO(Data Output);
  • MISO(Master Input Slave Output):主机输入,从机输出;
  • 也可以称作DI(Data Input);
  • SS(Slave Select):从机选择;
  • 也可以称作NS(Not Slave Select)、CS(Chip Select);
  • 同步,全双工;

  • SCK就是用来提供时钟信号的,相当于I2C通信协议中的SCL;
  • MOSI和MISO就是分别用于发送和接收的两条全双工线路;
  • 支持总线挂载多设备,只一主多从;

  • 通过SS从机选择线来选择通信从机;
  • 相对于I2C的优缺点:

  • SPI传输速度快,SPI通信协议并没有严格规定最大传输速度,这个最大传输速度取决于芯片厂商的设计需求;
  • 其设计简单粗暴,实现的功能比I2C少;
  • SPI硬件开销大,通信线个数较多,且通信过程中经常会有资源浪费的情况;
  • 11.2 SPI硬件电路

  • 所有SPI设备的SCK、MOSI、MISO分别连在一起;

  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚;

  • 当从机的SS引脚为高电平时,也就是从机未被选中时,其MISO引脚必须切换为高阻态,相当于引脚断开,不输出任何电平,这样就可以防止一条MISO线上有多个从机的输出而导致电平冲突的问题;
  • 在SS为低电平时,MISO才允许变成推挽输出;
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入;

  • 11.3 移位示意图

  • 两个移位寄存器都有一个时钟输入端,因为SPI一般都是高位先行,所以每来一个时钟,移位寄存器都会向左进行移位;
  • 移位寄存器的时钟源,是有主机的波特率发生器提供的,其产生的时钟驱动主机的移位寄存器进行移位,同时这个时钟也通过SCK引脚进行输出,接到从机的移位寄存器中;
  • 主机移位寄存器移出去的数据,通过MOSI引脚,输入到从机移位寄存器的右边;从机移位寄存器移出去的数据,通过MISO引脚,输入到主机移位寄存器的右边。这其实是一个字节交换的过程,实现了主机发送数据的同时接收数据的目的;
  • 如果只想发送,不想接收,那么不理会从机发送过来的数据就好了;
  • 如果只想接收,不想发送,那么给从机随便发送一个字节的数据,只需要把从机的数据置换过来就好了;
  • 11.4 SPI时序基本单元

  • 起始条件:SS从高电平切换到低电平,表示选中某个从机;

  • 终止条件:SS从低电平切换到高电平,表示结束某个从机的选中状态;

  • 交换一个字节(模式0);

  • 与模式1的区别:将MOSI和MISO中数据变换的时机提前了;
  • CPOL=0:空闲状态时,SCK为低电平;
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据;
  • CPHA指的是时钟相位,决定是第一个时钟采样移入,还是第二个时钟采样移入,并不是规定是上升沿采样还是下降沿采样。在CPOL确定的情况下,CPHA会改变采样时刻的上升沿和下降沿。模式0和模式3,都是SCK上升沿采样;模式1和模式2,都是SCK下降沿采样;
  • 交换一个字节(模式1);

  • CPOL=0:空闲状态时,SCK为低电平;
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据;
  • 交换一个字节(模式2);

  • 与模式0的区别:二者的CPOL不一致,在波形上的体现就是模式2的SCK波形是模式0的取反;
  • CPOL=1:空闲状态时,SCK为高电平;
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据;
  • 交换一个字节(模式3);

  • 与模式1的区别:二者的CPOL不一致,在波形上的体现就是模式3的SCK波形是模式1的取反;
  • CPOL=1:空闲状态时,SCK为高电平;
  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据;
  • 11.5 SPI时序

  • SPI对字节流功能的规定与I2C不同;

  • I2C规定:有效数据流的第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器的模型;
  • SPI规定:采用的是指令码+读写数据的模型,SPI起始后,第一个交换发送给从机的数据是指令码。在从机中,对应的有一个指令集;
  • 发送指令:向SS指定的设备,发送指令(0x06,在W25Q64中,代表写使能);

  • 使用的是SPI模式0;
  • 在空闲状态时,SS为高电平,SCK为低电平,MOSI和MISO的默认电平没有严格规定;
  • SS产生下降沿,时序开始,此时MOSI和MISO开始变换数据;
  • 随后开始交换一个字节,因为写使能是单独的指令,不需要跟随数据,SPI只需要交换一个字节即可;
  • 最后在SCK下降沿结束后,SS置回高电平,结束通信;
  • 总结:主机用0x06换来了从机的0xFF,该0xFF没有意义,不用理会。随后从机会根据接收到的0x06,发现是写使能,那么从机就会控制硬件,进行写使能;
  • 指定地址写:向SS指定的设备,发送写指令(0x02), 随后在指定地址(Address[23:0])下,写入指定数据(Data);

  • 指定地址读:向SS指定的设备,发送读指令(0x03), 随后在指定地址(Address[23:0])下,读取从机数据(Data);

  • 11.6 W25Q64简介

  • W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景;

  • 存储介质:Nor Flash(闪存);

  • 时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI);

  • 存储容量(24位地址):

  • W25Q40:4Mbit / 512KByte;
  • W25Q80:8Mbit / 1MByte;
  • W25Q16:16Mbit / 2MByte;
  • W25Q32:32Mbit / 4MByte;
  • W25Q64:64Mbit / 8MByte;
  • W25Q128:128Mbit / 16MByte;
  • W25Q256:256Mbit / 32MByte;
  • 11.7 W25Q64硬件电路

    引脚 功能
    VCC、GND 电源(2.7~3.6V)
    CS(SS) SPI片选
    CLK(SCK) SPI时钟
    DI(MOSI) SPI主机输出从机输入
    DO(MISO) SPI主机输入从机输出
    WP 写保护
    HOLD 数据保持

    11.8 W25Q64框图

  • 右边的大方框是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址;
  • W25Q64的地址宽度是24位3个字符,左下角第一个字节是000000h,h代表16进制。之后的空间,地址依次自增,直到最后一个字节,地址是7FFFFFh;
  • 以64KB为一个基本单元,将整个存储器划分为若干个块Block,8MB/1024/64=128块,编号从块0~块127;
  • 左上是对每一块进行更细的划分,将一块划分为多个扇区Sector;
  • 在一块里,以4KB为一个基本单元划分为一个个扇区,64KB/4KB=16份扇区,编号从扇区0~扇区15;
  • 在写入数据时,还有更细的划分,即页Page,一页是256个字节,4KB*1024/256=16页;
  • 左下角是SPI控制逻辑,其左边是SPI的通信引脚,与主控芯片相连;
  • 主控芯片通过SPI协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路;
  • Status Register:状态寄存器,芯片是否处于忙状态、是否写使能、是否写保护,都在状态寄存器中体现;
  • Write Control Logic:写控制逻辑,和外部的WP引脚相连,显示是与WP引脚实现硬件写保护的;
  • High Vottage Generators:高电压生成器,是配合Flash进行编程的;
  • Page Address Latch/Counter:页地址锁存/计数器;
  • Byte Address Latch/Counter:字节地址锁存/计数器,与上面那个一起用来指定地址;
  • 通过SPI,总共发过来3个字节的地址;
  • 因为一页是256字节,所以一页内的字节地址取决于最低一个字节,而高位的两个字节就是对应的页地址;
  • 所以发送过来的3个字节的地址,前两个字节就会进入页地址锁存/计数器,最后一个字节会进入字节地址锁存/计数器;
  • 页地址通过写保护和行解码,来选择要操作哪一页;
  • 字节地址通过列解码和256字节页缓存,来进行指定字节的读写操作;
  • 因为这两个地址锁存都带有一个计数器,所以地址指针在读写之后,可以自动加1,这样就可以实现从指定地址开始,连续读写多个字节的目的;
  • Column Decode And 256-Byte Page Buffer:256字节的页缓冲区,其实是一个256字节的RAM存储器,数据读写就是通过这个RAM缓冲区进行的;
  • 写入数据,会先放到缓冲区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的Flash里,进行永久保存;
  • 为什么不可以直接往Flash中写,而是需要经过一个缓冲区?因为SPI写入的频率非常高,而Flash的写入由于需要掉电不丢失,写入的就比较慢,所以需要一个缓冲区缓冲;
  • 11.9 Flash操作注意事项

  • 写入操作时:
  • 写入操作前,必须先进行写使能;
  • 每个数据位只能由1改写为0,不能由0改写为1;
  • 写入数据前必须先擦除,擦除后,所有数据位变为1;
  • 擦除必须按最小擦除单元(一个扇区,4KB)进行;
  • 连续写入多字节时,最多写入一页(256字节)的数据,超过页尾位置的数据,会回到页首覆盖写入;
  • 因为缓冲区大小就是256字节;
  • 写入操作结束后,芯片进入忙状态,不响应新的读写操作;
  • 读取操作时:
  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
  • 11.10 软件SPI读写W24Q64

  • 项目目录结构:

  • MySPI.h

    #ifndef __MYSPI_H
    #define __MYSPI_H
    
    void MySPI_Init(void);
    void MySPI_Start(void);
    void MySPI_Stop(void);
    uint8_t MySPI_SwapByte(uint8_t ByteSend);
    
    #endif
    
  • 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_Ins.h

    #ifndef __W25Q64_INS_H
    #define __W25Q64_INS_H
    
    #define W25Q64_WRITE_ENABLE							0x06
    #define W25Q64_WRITE_DISABLE						0x04
    #define W25Q64_READ_STATUS_REGISTER_1				0x05
    #define W25Q64_READ_STATUS_REGISTER_2				0x35
    #define W25Q64_WRITE_STATUS_REGISTER				0x01
    #define W25Q64_PAGE_PROGRAM							0x02
    #define W25Q64_QUAD_PAGE_PROGRAM					0x32
    #define W25Q64_BLOCK_ERASE_64KB						0xD8
    #define W25Q64_BLOCK_ERASE_32KB						0x52
    #define W25Q64_SECTOR_ERASE_4KB						0x20
    #define W25Q64_CHIP_ERASE							0xC7
    #define W25Q64_ERASE_SUSPEND						0x75
    #define W25Q64_ERASE_RESUME							0x7A
    #define W25Q64_POWER_DOWN							0xB9
    #define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
    #define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
    #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
    #define W25Q64_MANUFACTURER_DEVICE_ID				0x90
    #define W25Q64_READ_UNIQUE_ID						0x4B
    #define W25Q64_JEDEC_ID								0x9F
    #define W25Q64_READ_DATA							0x03
    #define W25Q64_FAST_READ							0x0B
    #define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
    #define W25Q64_FAST_READ_DUAL_IO					0xBB
    #define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
    #define W25Q64_FAST_READ_QUAD_IO					0xEB
    #define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3
    
    #define W25Q64_DUMMY_BYTE							0xFF
    
    #endif
    
  • W25Q64.h

    #ifndef __W25Q64_H
    #define __W25Q64_H
    
    void W25Q64_Init(void);
    void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
    void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
    void W25Q64_SectorErase(uint32_t Address);
    void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);
    
    #endif
    
  • W25Q64.c

    #include "stm32f10x.h"                  // Device header
    #include "MySPI.h"
    #include "W25Q64_Ins.h"
    
    /**
      * 函    数:W25Q64初始化
      * 参    数:无
      * 返 回 值:无
      */
    void W25Q64_Init(void)
    {
    	MySPI_Init();					//先初始化底层的SPI
    }
    
    /**
      * 函    数:MPU6050读取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终止
    }
    
  • main.c

    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "W25Q64.h"
    
    uint8_t MID;							//定义用于存放MID号的变量
    uint16_t DID;							//定义用于存放DID号的变量
    
    uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};	//定义要写入数据的测试数组
    uint8_t ArrayRead[4];								//定义要读取数据的测试数组
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();						//OLED初始化
    	W25Q64_Init();						//W25Q64初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "MID:   DID:");
    	OLED_ShowString(2, 1, "W:");
    	OLED_ShowString(3, 1, "R:");
    	
    	/*显示ID号*/
    	W25Q64_ReadID(&MID, &DID);			//获取W25Q64的ID号
    	OLED_ShowHexNum(1, 5, MID, 2);		//显示MID
    	OLED_ShowHexNum(1, 12, DID, 4);		//显示DID
    	
    	/*W25Q64功能函数测试*/
    	W25Q64_SectorErase(0x000000);					//扇区擦除
    	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
    	
    	W25Q64_ReadData(0x000000, ArrayRead, 4);		//读取刚写入的测试数据到读取数据的测试数组中
    	
    	/*显示数据*/
    	OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);		//显示写入数据的测试数组
    	OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
    	OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
    	OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
    	
    	OLED_ShowHexNum(3, 3, ArrayRead[0], 2);			//显示读取数据的测试数组
    	OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
    	OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
    	OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
    	
    	while (1)
    	{
    		
    	}
    }
    
  • 11.11 SPI外设简介

  • STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担;
  • 可配置8位/16位数据帧、高位先行/低位先行;
  • 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256);
  • PCLK:外设时钟,APB2的PCLK是72MHz,APB1的PCLK是36MHz;
  • SPI1挂载在APB2,SPB2挂载在APB1;
  • 支持多主机模型、主或从操作;
  • 可精简为半双工/单工通信;
  • 支持DMA;
  • 兼容I2S协议;
  • I2S是一种音频传输协议;
  • STM32F103C8T6 硬件SPI资源:SPI1、SPI2。
  • 11.12 SPI框图

  • LSBFIRST控制位:可以控制是低位先行还是高位先行;
  • MOSI和MISO右边的交叉电路:用来进行主从引脚模式变化的(此处的图可能有错);
  • 波特率发生器:用来产生SCK时钟;
  • 11.13 SPI基本结构

    11.14 主模式全双工连续传输

  • 因为SPOL=1,CPHA=1,所以使用的是SPI模式3;
  • 11.15 非连续传输

  • 因为SPOL=1,CPHA=1,所以使用的是SPI模式3;
  • 11.16 软件/硬件波形对比

    11.17 硬件SPI读写W24Q64

  • 项目目录结构:

  • MySPI.h

    #ifndef __MYSPI_H
    #define __MYSPI_H
    
    void MySPI_Init(void);
    void MySPI_Start(void);
    void MySPI_Stop(void);
    uint8_t MySPI_SwapByte(uint8_t ByteSend);
    
    #endif
    
  • MySPI.c

  • 删除void MySPI_W_SCK(uint8_t BitValue)void MySPI_W_MOSI(uint8_t BitValue)uint8_t MySPI_R_MISO(void)三个函数;
  • MySPI_Init函数中的内容替换为SPI外设的初始化;
  • MySPI_SwapByte函数中的内容替换为硬件SPI的代码;
  • #include "stm32f10x.h"                  // Device header
    
    /**
      * 函    数:SPI写SS引脚电平,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初始化
      * 参    数:无
      * 返 回 值:无
      */
    void MySPI_Init(void)
    {
    	/*开启时钟*/
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
    	
    	/*GPIO初始化*/
    	GPIO_InitTypeDef GPIO_InitStructure;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4引脚初始化为推挽输出
    	
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将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引脚初始化为上拉输入
    	
    	/*SPI初始化*/
    	SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
    	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
    	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
    	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
    	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
    	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
    	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
    	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
    	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
    	SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
    	SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
    	
    	/*SPI使能*/
    	SPI_Cmd(SPI1, ENABLE);									//使能SPI1,开始运行
    	
    	/*设置默认电平*/
    	MySPI_W_SS(1);											//SS默认高电平
    }
    
    /**
      * 函    数: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)
    {
    	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	//等待发送数据寄存器空
    	
    	SPI_I2S_SendData(SPI1, ByteSend);								//写入数据到发送数据寄存器,开始产生时序
    	
    	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	//等待接收数据寄存器非空
    	
    	return SPI_I2S_ReceiveData(SPI1);								//读取接收到的数据并返回
    }
    
  • main.c

    #include "stm32f10x.h"                  // Device header
    #include "Delay.h"
    #include "OLED.h"
    #include "W25Q64.h"
    
    uint8_t MID;							//定义用于存放MID号的变量
    uint16_t DID;							//定义用于存放DID号的变量
    
    uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};	//定义要写入数据的测试数组
    uint8_t ArrayRead[4];								//定义要读取数据的测试数组
    
    int main(void)
    {
    	/*模块初始化*/
    	OLED_Init();						//OLED初始化
    	W25Q64_Init();						//W25Q64初始化
    	
    	/*显示静态字符串*/
    	OLED_ShowString(1, 1, "MID:   DID:");
    	OLED_ShowString(2, 1, "W:");
    	OLED_ShowString(3, 1, "R:");
    	
    	/*显示ID号*/
    	W25Q64_ReadID(&MID, &DID);			//获取W25Q64的ID号
    	OLED_ShowHexNum(1, 5, MID, 2);		//显示MID
    	OLED_ShowHexNum(1, 12, DID, 4);		//显示DID
    	
    	/*W25Q64功能函数测试*/
    	W25Q64_SectorErase(0x000000);					//扇区擦除
    	W25Q64_PageProgram(0x000000, ArrayWrite, 4);	//将写入数据的测试数组写入到W25Q64中
    	
    	W25Q64_ReadData(0x000000, ArrayRead, 4);		//读取刚写入的测试数据到读取数据的测试数组中
    	
    	/*显示数据*/
    	OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);		//显示写入数据的测试数组
    	OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
    	OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
    	OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
    	
    	OLED_ShowHexNum(3, 3, ArrayRead[0], 2);			//显示读取数据的测试数组
    	OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
    	OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
    	OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
    	
    	while (1)
    	{
    		
    	}
    }
    
  • 作者:木木慕慕

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32系列:SPI通信详解与实现

    发表回复