STM32 SPI通信教程(上篇)

一、SPI通信协议

SPI 为全双工

SPI共有四根通信线:SCK(串行时钟线)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、SS(从机选择)。

1.1、硬件电路

  1. 所有SPI设备的SCK、MOSI、MISO分别连在一起
  2. 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  3. 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
  4. SPI通信线为单端信号,器电平高低都是相对GND的电压差,因此所有设备需共地。

1.2、SPI 数据传输流程

在时钟驱动(波特率发生器)下,SPI主机与从机的移位寄存器在每个上升沿时,发生一个数据位的数据交换,主机移位寄存器数据左移一位数据,置于MOSI,从机数据寄存器同样左移一个数据位,将一位数据置于MISO,如下图。最终两个寄存器完成数据交换。

1.3、SPI 基本时序单元

1.3.1、SPI起始条件&终止条件

起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平

1.3.2、交换一个字节

SPI_CR寄存器的CPOLCPHA位,能够组合成四种可能的时序关系。CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。

  • 如果CPOL被清 '0',SCK引脚在空闲状态保持低电平;
  • 如果CPOL被置 '1',SCK引脚在空闲状态保持高电平
     
  • 如果 CPHA(时钟相位) 位被置  '1',SCK时钟的第二个边沿  (CPOL位为0时就是下降沿,CPOL位为'1'时就是上升沿)  进行数据位的采样,数据在第二个时钟边沿被锁存。
  • 如果CPHA位被清  '0’,SCK时钟的第一边沿  (CPOL位为0时就是上升沿,CPOL位为’1时就是下降沿)  进行数据位采样,数据在第一个时钟边沿被锁存
  • 1.3.2.1、模式0

    CPOL=0:空闲状态时,SCK为低电平
    CPHA=0: SCK第一个边沿移入数据,第二个边沿移出数据

            CPOL=0,  SCK引脚在空闲状态保持低电平CPHA=0,需要在SCK第一个边沿采集数据,即读取数据,根据1.2、SPI数据传输流程,需要我们在采集数据之前先移出数据,因此在SS下降沿时,MOSIMISO将数据提前放置到线上,待SCK在第一个边沿到来时(上升沿),即可读取数据。

            由于SPI同时连接多个从机,为避免多从机同时输入,因此在SS未被选中状态时,将从机MISO引脚关断输出,即配置为高组态。因此上图中的MISO开始与结束时都用一个处于中间的先表示处于高组态。

    1.3.2.2、模式1

    CPOL=0:空闲状态时,SCK为低电平
    CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

     CPOL=0,  SCK引脚在空闲状态保持低电平CPHA=1,需要在SCK第二个边沿采集数据,那第一个边沿需要移出数据。

    1.3.2.3、模式2

    CPOL=1:空闲状态时,SCK为高电平
    CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

    除 CPOL=1:空闲状态时,SCK为高电平外,其余与模式0无差。

    1.3.2.4、模式3

    CPOL=1:空闲状态时,SCK为高电平
    CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

    1.3.3、指定地址写

    SPI 对于字节流的传输采用的是 指令码+读写数据 的方式,即起始条件开始后,主机第一个交换发给从机的数据一般成为指令码, 在从机中有对应的指令集,从机会根据接受到的指令码完成相应的指令集内容。

    向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)。由于本例采用的W65Q24芯片有8M字节存储空间,需要用到24位地址。

    其中,第一个发送的数据 0x20,为指令码,对应写指令(W65Q24芯片)。

    1.3.4、指定地址读

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

    二、W25Q64芯片介绍

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

    2.1、硬件电路

    HOLD:遇中断离开时,可保存当前时序。

    2.2、W25Q64硬件结构介绍

    2.2.1、Flash 存储区域划分

    【1】蓝色框选部分为W25Q64全部存储空间,存储器以字节为单位,且每个字节有唯一一个地址k,首地址为000000h(h代表16进制),最后一个地址位7FFFFFh。

    【2】在蓝色框选部分内,又以 64KB为一个基本单元,分为若干块(Block)。由于W25Q64存储单元为8M,因此,蓝色框选部分共分为 8*1024/64=128 块。下图为 块0(Block0)地址,红色部分为起始地址,蓝色为结束地址。

    【3】在 块(Block) 中又分为 多个扇区(Sector)。在扇区中,以4KB为单位,将64KB的 块(Block)切分为16 个扇区(Sector)。下图为 扇区(Sector)地址,红色部分为起始地址,蓝色为结束地址。

    【4】在写入数据是,还会有一个比 扇区(Sector)更小的划分 页(Page),页大小为 256个字节,每个4KB大小的扇区(Sector)可分为 4*1024/256=16 页。图中黄色框选部分为页起始地址,绿色为页终止地址,变化范围是 xx xx 00~xx xx FF。

    2.2.2、W25Q64控制框图解析

  • 下图红色框选部分为 控制逻辑,管理整个芯片运作。蓝色部分为状态寄存器,用于检查芯片是否处于忙状态,是否写使能,是否写保护等;
  • 黄色框选部分为高压生成器,用于配合Flash 编程(Flash掉电不丢失,需要高压刺激。)
  • 绿色框选部分别为页地址锁存寄存器/计数器、字节锁存寄存器/计数器,用于指定地址。例如通过SPI 发送3个地址数据,高两位字节地址进入页地址锁存寄存器/计数器,低位字节地址进入字节锁存寄存器/计数器。
  • 页地址锁存寄存器/计数器会通过 写保护和行解码(红色圈起部分),选择要操作的页数。
  • 字节锁存寄存器/计数器会通过列解码和256字节页缓存,进行指定字节读写操作
  • 2.3、Flash 操作事项

    2.3.1、写操作

  • 写入操作前,必须先进行写使能(防止误操作)
  • 每个数据位只能由1改写为0,不能由0改写为1
  • 写入数据前必须先擦除,擦除后,所有数据位变为1
  • 擦除必须按最小擦除单元(4KB)进行
  • 连续写入多字节时,最多写入一页的数据(即256KB),超过页尾位置的数据,会回到页首覆盖写入。
  • 写入操作结束后,芯片进入忙状态,不响应新的读写操作
  • 2.3.2、读取操作

  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取。
  • 2.4、指令集

    页面程序为页编程,有256KB大小限制。

    为使代码具有可读性,在代码中一般通过宏定义将指令集转换成可读性语言,如下列代码。

    #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

    三、功能代码(SPI软件读写W25Q64)

    引脚接线图

    3.1、MySPI.c文件

    该层位SPI的通信协议层,可以认为是底层代码,用于开启时钟以及定义SPI工作基本时序。

    3.1.1、MySPI_Init

    void MySPI_Init(void)
    {
    	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    	
    	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);
    	
    	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);
    }

    其中引脚 4 5 7为推挽输出,引脚6为上拉输入(1.1、硬件电路已说明)。

    31.2、引脚操作

    //操作ss
    void MySPI_W_SS(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA ,GPIO_Pin_4,(BitAction)BitValue);
    }
    //操作SCLK
    void MySPI_W_SCLK(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA ,GPIO_Pin_5,(BitAction)BitValue);
    }
    //操作MOSI
    void MySPI_W_MOSI(uint8_t BitValue)
    {
    	GPIO_WriteBit(GPIOA ,GPIO_Pin_7,(BitAction)BitValue);
    }
    //操作MISO
    uint8_t MySPI_R_MISO(void)
    {
    	return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
    }

    由于需要人工手动翻转SCLK 以及 MISO、MOSI等引脚,所以为了便捷,将引脚翻转等操作封装为函数方便调用与移植。

    3.1.3、基本时序

    void MySPI_Star(void)
    {
    	MySPI_W_SS(0);
    	MySPI_W_SCLK(0);//使用模式0
    }
    
    void MySPI_Stop(void)
    {
    	MySPI_W_SS(1);
    }
    
    //模式0
    uint8_t MySPI_WriteRead_0(uint8_t Bytesend)
    {
    	uint8_t i, ByteReceive = 0x00;
    	for (i = 0; i < 8; i ++)
    	{
    		MySPI_W_MOSI(Bytesend & (0x80 >> i));
    		MySPI_W_SCLK(1);
    		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
    		MySPI_W_SCLK(0);
    	}
    	return ByteReceive;
    }

    对于模式的选择,是在  Star 中添加MySPI_W_SCLK()函数,通过翻转引脚高低电平实现。

    3.2、W25Q64.c文件

    该层是一个通信层,是基于底层文件MySPI.c文件运行。

    void W25Q64_Init(void)
    {
    	MySPI_Init();
    }
    
    //写使能
    void W25Q64_WriteEnable(void)
    {
    	MySPI_Star();
    	MySPI_WriteRead_0(W25Q64_READ_STATUS_REGISTER_1);
    	MySPI_Stop();
    }
    
    //读取状态寄存器
    void W25Q64_WaitBusy()
    {
    	uint32_t Timeout=100000;
    	MySPI_Star();
    	MySPI_WriteRead_0(W25Q64_READ_STATUS_REGISTER_1);
    	Timeout--;
    	while((MySPI_WriteRead_0(W25Q64_DUMMY_BYTE)&0x01)==1)
    	{
    		if(Timeout==0)
    		{
    			break;
    		}
    	}
    	MySPI_Stop();
    }
    
    //页编程
    void W25Q64_PageProgram(uint32_t Address,uint8_t *DataArray,uint16_t Count)
    {
    	W25Q64_WriteEnable();
    	uint16_t i;
    	MySPI_Star();
    	MySPI_WriteRead_0(W25Q64_PAGE_PROGRAM);
    	MySPI_WriteRead_0(Address>>16);
    	MySPI_WriteRead_0(Address>>8);
    	MySPI_WriteRead_0(Address);
    	for(i=0;i<Count;i++)
    	{
    		MySPI_WriteRead_0(DataArray[i]);
    	}
    	MySPI_Stop();
    	W25Q64_WaitBusy();
    }
    
    //扇区擦除
    void W25Q64_SectorErase(uint32_t Address)
    {
    	W25Q64_WriteEnable();
    	MySPI_Star();
    	MySPI_WriteRead_0(W25Q64_SECTOR_ERASE_4KB	);
    	MySPI_WriteRead_0(Address>>16);
    	MySPI_WriteRead_0(Address>>8);
    	MySPI_WriteRead_0(Address);
    	MySPI_Stop();
    	W25Q64_WaitBusy();
    }
    
    //读取数据
    void W25Q64_ReadData(uint32_t Address,uint8_t *DataArray,uint32_t Count)
    {
    	uint32_t i;
    	MySPI_Star();
    	MySPI_WriteRead_0(W25Q64_READ_DATA);
    	MySPI_WriteRead_0(Address>>16);
    	MySPI_WriteRead_0(Address>>8);
    	MySPI_WriteRead_0(Address);
    	for(i=0;i<Count;i++)
    	{
    		DataArray[i]=MySPI_WriteRead_0(W25Q64_DUMMY_BYTE);
    	}
    	MySPI_Stop();
    }

    【1】代码中写使能是因为2.3、Flash操作中规定,写入操作前需要先写使能。因此有页编程、扇区擦除需要添加写使能代码 W25Q64_WriteEnable()。并且由于写入操作结束后,芯片进入忙状态,不响应新的读写操作,因此需要再写入操作后加入等待状态标志位,所以页编程以及、扇区写入后会添加 等待标志位代码 W25Q64_WaitBusy()。读取数据无需添加(也可添加),因为芯片无非是 读、写以及擦除操作,写与擦书等待说明芯片处于读的忙状态。

    【2】读取状态寄存器,是为了判断芯片是否处于忙状态。添加Timeout 是为了防止程序在while中卡死,可以及时跳出。Timeout 数值不可过小,否则未等到芯片不忙状态就跳出。读取状态寄存器仅需要判断最后一位 BUSY 位,为1时 处于忙状态,0是为空闲状态。

    【3】页编程:即为写入数据,对应1.3.3、指定地址写,页编程实现的是数据的发送,而 MySPI_WriteRead_0 是页编程发送的基础或者手段。也可以理解为  MySPI_WriteRead_0 仅发送一个字节,对于长数据的发送不便,需要调用过多 MySPI_WriteRead_0 函数,如若封装,使用更加方便。

    注意,参数中由于W25Q64为24位地址,因此参数类型选择uint32_t,而参数3:coumt的类型为uint16_t,因为Flash为最大存储量256zi,若选择uint8_t,取值范围是0~255,不能取到256。
     

    以上即为本节内容(已舍去main 现象内容。后续会适当补充)

    作者:橘洲青年

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 SPI通信教程(上篇)

    发表回复