STM32 SPI通信教程(上篇)
一、SPI通信协议
SPI 为全双工
SPI共有四根通信线:SCK(串行时钟线)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、SS(从机选择)。
1.1、硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
- SPI通信线为单端信号,器电平高低都是相对GND的电压差,因此所有设备需共地。
1.2、SPI 数据传输流程
在时钟驱动(波特率发生器)下,SPI主机与从机的移位寄存器在每个上升沿时,发生一个数据位的数据交换,主机移位寄存器数据左移一位数据,置于MOSI,从机数据寄存器同样左移一个数据位,将一位数据置于MISO,如下图。最终两个寄存器完成数据交换。
1.3、SPI 基本时序单元
1.3.1、SPI起始条件&终止条件
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
1.3.2、交换一个字节
SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系。CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。
1.3.2.1、模式0
CPOL=0:空闲状态时,SCK为低电平
CPHA=0: SCK第一个边沿移入数据,第二个边沿移出数据
CPOL=0, SCK引脚在空闲状态保持低电平,CPHA=0,需要在SCK第一个边沿采集数据,即读取数据,根据1.2、SPI数据传输流程,需要我们在采集数据之前先移出数据,因此在SS下降沿时,MOSI与MISO将数据提前放置到线上,待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控制框图解析
2.3、Flash 操作事项
2.3.1、写操作
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 现象内容。后续会适当补充)
作者:橘洲青年