关于SPI那些事

写在前面

该系列文章是参考多篇优质文章,整合自己的学习笔记而成。有错误不足之处多多指教!

关于SPI

SPI是一种高速的,全双工,同步的通信总线,SPI分主、从两种模式,一个SPI的通讯系统需要包含一个(且只能是一个)主设备,一个或者多个从设备,主设备提供时钟,接收时钟的设备为从设备,SPI的读写操作都是由主设备发起的,当存在多个设备的时候,可以通过各自的片选信号进行管理。一般来说SPI具有四条通信线分别是:SCK(时钟);CS(片选);SDI/MISO(数据输入);SDO/MOSI(数据输出),SPI相比较IIC而言是借助于成本更大的硬件资源来获取更高的速度,SPI是全双工的,输出输入分别在不同的通信线是,因此其输出引脚一般都配置成为推挽输出,输入引脚配置成为浮空或者上拉输入,推挽输出的电平驱动能力较强,因此SPI的电平转换速度也是比较迅速的,从而具备较强的通讯速度,一般通讯速度通常可以达到甚至超过10Mbps。

SPI的硬件设计

SPI可以设计成为一主多从,或者一主一从的,对于SPI的多主机在这暂时不进行讨论。SPI的主机和从机都有一个串行移位寄存器,在主机波特率发生器的时钟驱使下当电平出现上升沿或者下降沿时,主机的移位寄存器从高位移出一位到MOSI下,同时从机的移位寄存器的高位也移出一位到MISO下,在下个电平转换的时候,将通信线上的电平采样到缓冲区中,通过一个循环就可以将主机和从机的数据进行交换。

 在SPI的通讯接线中,如下:

 

SPI的通信协议

首先拉低对应SS信号线,表示与该设备进行通信,(大多数片选信号是低电平有效),主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据(在模式0中时钟信号也作为数据移出的标志)。因为SPI的通信有四种模式,所以存在SCLK时钟信号可能是低电平有效,也可能是高电平有效。对于四种模式下面会有介绍。

主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。从机(Slave)也将自己的串行移位寄存器(0~7)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。

如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。这一点在SPI中十分的重要。

SPI的操作模式

SPI通信有4种不同的操作模式,不同的从设备可能在出厂是就是配置为某种模式,这是不能改变的;但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来控制我们主设备的通信模式,具体如下

 

 

 

 

 软件模拟SPI

写到这里我们已经了解了SPI的通信协议和四种操作模式,那么我们就一起来写一下软件模拟SPI的代码吧,我们写各种通信协议必然是需要用来应用的,所以学以致用很重要,我们先写好底层的SPI代码,再用SPI的代码拼凑起来,应用到SPI的外设当中,那成就感应该是不错的!

结合上文对SPI的介绍,我们在软件实现中,在底层的SPI中应该有SPI的初始化、SPI信号的开始、SPI信号的交换、SPI信号的结束。在这我们选用模式0进行模拟。

SPI的初始化中应该对SPI的输入输出端口进行初始化的配置。在进行配置之前,我们可以对四个通信引脚进行函数包装方便后续的使用

void MySPI_W_CS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}

uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}

在初始化中对输出引脚配置成为推挽输出,输入引脚配置成为浮空输出。

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);
	
	MySPI_W_CS(1);
	MySPI_W_SCK(0);
}

SPI的开始和介绍信号是由片选信号决定的,在模式0中片选信号拉低,通信开始,片选信号拉高,通信结束。

void MySPI_Start(void)
{
	MySPI_W_CS(0);
}

void MySPI_Stop(void)
{
	MySPI_W_CS(1);
}

大的要来了!!!如何实现数据的交换呢?

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	
	return ByteReceive;
}
  1. 初始化一个变量 ByteReceive,用于保存接收到的字节。
  2. 使用一个循环,循环8次,处理每个字节的位。
  3. 在每次循环中,首先将要发送的位通过 MySPI_W_MOSI 函数写入到MOSI引脚上,该函数的参数是 ByteSend和位掩码(通过右移操作)的与运算结果。
  4. 然后将SCK引脚设置为高电平,通过 MySPI_W_SCK 函数进行操作。
  5. 通过 MySPI_R_MISO 函数读取MISO引脚的状态,如果为1,则将相应位的接收字节 ByteReceive 置为1。不是1则为初始化的0
  6. 最后将SCK引脚设置为低电平,通过 MySPI_W_SCK 函数进行操作,以完成一个位的数据交换。
  7. 循环结束后,返回接收的字节 ByteReceive

利用SPI访问外接FLASH芯片

当我们在工程中遇到一款芯片的时候,如何可以使用好这款芯片呢,利用其官方的数据手册可以详细了解,结合其例程可以快速上手。

在本文中介绍W25QXX系列,这是一款低成本、小型化、使用简单的非易失存储器,常用于数据存储、字库存储,固件程序等,是Nor Flash类的产品,本文选用W25Q64为一款64Mbit(兆比特)/8MByte(兆字节)存储容量的芯片(24位地址)。对于24位的地址,其寻址空间可达16MB。

W25Q64BV(FLASH)(SPI)中文手册

 8MB的存储空间分为128个块(Block),一块一共有64KB,每一块又分为16个扇区(Sector),每个扇区的大小为4KB。在扇区里又可以细化为页,一页是256个字节,那么4*1024/256=16,即可以划分为16页。

我们主控芯片可以通过SPI协议,将指令和数据发给控制逻辑,控制逻辑会自动去操作内部电路来完成我们需要的功能。控制逻辑是有一个状态寄存器,芯片是否处于忙状态,是否是写使能,是否写保护都是可以在这个状态寄存器里面体现。在芯片内部是有一个高压生成器的,因为Flash是掉电不丢失的,那么就需要电子击穿效应,在这里要注意,芯片内部有一个256字节的缓冲区,也就是我们写入的数据是先放到缓冲区,缓冲区再写入到Flash里面,因为SPI的写入数据比较快,但是Flash掉电不丢失的特性决定其写入数据需要一定的时间。因此我们一次性写入的数据量最多是256字节。

Flash的操作注意事项

写入操作前,需要进行写使能

每个数据位只能由1改写成为0,不能由0改写成为1(它不如RAM没有完全覆盖改写的能力)

写入数据前必须要先擦除,擦除后,所有的数据位变为1 (FF表示空白)

擦除必须要按照最小单元进行(一个扇区)

连续写入多字节时,最多写入一页的数据,超过页位位置的的数据,会回到页首覆盖写入

写入操作结束后,芯片会进入忙状态,不响应新的读写操作

读操作时(只需要看一下电流状态,不会对电路进行改变,所以一般比较快,也没有什么限制),直接调用读取时序,无需使能,无需额外操作,没有页的现在,读取操作结束后不会进入忙状态,但是不能在忙状态时读取。

程序实战

以下是W25Q64的一些关键信息:

 

 

 

void W25Q64_Init(void)
{
	MySPI_Init();
}

void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);
	*MID = MySPI_SwapByte(0xFF);//数据置换 厂商ID
	*DID = MySPI_SwapByte(0xFF);//数据置换  设备ID
	*DID <<= 8;
	*DID |= MySPI_SwapByte(0xFF);//或结合数据
	MySPI_Stop();
}

void W25Q64_WriteEnable(void) //写使能
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}

void W25Q64_WaitBusy(void)
{
	uint32_t Timeout;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	Timeout = 100000;
	while ((MySPI_SwapByte(0xFF) & 0x01) == 0x01)
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
	MySPI_Stop();
}

void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	MySPI_Stop();
	W25Q64_WaitBusy();
}

void W25Q64_ChipErase(void)
{
	W25Q64_WriteEnable();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_CHIP_ERASE);
	MySPI_Stop();
	W25Q64_WaitBusy();
}

void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint8_t i;
	W25Q64_WriteEnable();
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for (i = 0; i < Count; i ++)
	{
		 MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
	W25Q64_WaitBusy();
}

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint8_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for (i = 0; i < Count; i ++)
	{
		DataArray[i] = MySPI_SwapByte(0xFF);
	}
	MySPI_Stop();
}

这段代码是用于等待W25Q64闪存器忙碌状态的函数。

函数的具体步骤如下:

  1. 首先调用 MySPI_Start() 函数来启动SPI通信。
  2. 使用 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1) 发送读取状态寄存器的命令给W25Q64闪存器。
  3. 初始化超时变量 Timeout 为一个较大的值,比如100000。
  4. 进入一个循环,条件是闪存器的忙碌标志位为1(即未完成操作)。
  5. 在每次循环中,通过 MySPI_SwapByte(0xFF) 发送一个空字节来读取状态寄存器的值,并与0x01(忙碌标志位)进行比较。
  6. 如果闪存器仍然忙碌,则减少 Timeout 的值。
  7. 如果达到超时条件(即 Timeout 等于0),则跳出循环。
  8. 调用 MySPI_Stop() 函数停止SPI通信。

该函数的目的是等待W25Q64闪存器完成之前的操作,并确保闪存器不再忙碌后再继续执行后续的操作。超时的作用是避免在某些异常情况下陷入无限等待的状态。

剩下的 晚点再写吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值