一、软件SPI
有关SPI的介绍我这里就不说了,可以去看我上一条博客,这里直接切入主题
1.四根通信线:SCK(时钟)、MOSI(主设备输出从设备输入)、MISO(主设备输入从设备输出)、SS(片选)
2.同步、全双工 接收数据线和发送数据线可同时工作
3.支持总线挂载多个设备,实现一主多从模式
4.所有SPI设备的SCK、MOSI、MISO、SS分别连在一起,主机另外引出多条SS控制线,分别接到各从机SS引脚
5.输出引脚配置为推挽输出,输入引脚配置为浮空输入
🌕下图是 SPI 主从机的 MOSI 和 MISO 进行数据交换的示意图:
🐠SPI 主机内部有一个波特率发生器,用来产生时钟SCK,波特率发生器产生时钟驱动主机的移位寄存器进行移位,同时产生时钟驱动从机的移位寄存器。
SPI 通讯是高位在前, 当 SCK 上升沿时,主机移位寄存器的最高位移至 MOSI 数据线上,从机的最低位移至 MISO 数据线上; SCK 低电平期间,MOSI 数据线上的数据移至 SPI 从机,MISO 数据线上的数据移至 SPI 主机;以此完成一位的交换;
更通俗易懂点就是:
- 第一次交换时,SCK高电平期间,主机的最高位1放到MOSI数据线上,从机的最低位1放到MISO数据线上,
- SCK低电平期间,MOSI数据线上的数据移至从机的最低位,MISO数据线上的数据移至主机的最低位,
- 这样经过8次循环,就可以完成一个字节的交换,并且每次循环都会将最低位的值逐步移到移位寄存器的最高位,保证了SPI高位在前的原则)
🐠根据CPOL和CPHA的变化(CPOL和CPHA在STM32F4硬件驱动SPI有介绍),交换一个字节,可以产生4种模式:分别是模式0、模式1、模式2、模式3;
🌖这里模拟的时序是SPI的模式3(CPOL=1,CPHA=1),原因有两点:
1、模式3的SCK空闲电平为高,有高电平向低电平翻转较为容易和快,
2、模式3在偶数边沿采样,防止第一个信号没采到。
🌘首先,对于软件模拟SPI的GPIO初始化,可以参考SPI的GPIO初始化的配置,我们不用使用复用功能就行,使用普通推挽输出和浮空输入。
void SPI_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC,ENABLE); /*SPI CS GPIO Confgi*/ GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = EEPROM_SPI_CS_PIN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(EEPROM_SPI_CS_PORT,&GPIO_InitStruct); /*SPI SCK GPIO Config*/ GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = EEPROM_SPI_CLK_PIN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(EEPROM_SPI_CLK_PORT,&GPIO_InitStruct); /*MISO GPIO Config*/ GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStruct.GPIO_Pin = EEPROM_SPI_MISO_PIN; GPIO_Init(EEPROM_SPI_MISO_PORT,&GPIO_InitStruct); /*MOSI GPIO Config*/ GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Pin = EEPROM_SPI_MOSI_PIN; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(EEPROM_SPI_MOSI_PORT,&GPIO_InitStruct); EEPROM_SPI_CS_HIGH(); //位选拉高 EEPROM_SPI_CLK_HIGH();//时钟线拉高 }
🌑接下来就是最为关键的发送和接收数据函数了,
- 其中注意,使用SPI时,发送和接收其实是同一个函数,通过判断RXNE来确认发送结束,此时也接收完数据,接收数据同样要主机产生时序,时序通过主机发送数据产生,所以会发送无用的Dummy数据。
- 软件模拟不需要,直接接收自己本身会产生时序,不用发送DUMMY数据。Delay函数随便设置,不要小于手册的时间即可。
软件模拟发送数据
void SPI_SendData(uint8_t data) { uint8_t cnt; for(cnt=0;cnt<8;cnt++) { EEPROM_SPI_CLK_LOW();//拉低CLK SPI_Delay(10);//这个延时时间任意,但要大于芯片数据手册上的(纳秒级的) if(data &0x80) { EEPROM_SPI_MOSI_HIGH(); } else { EEPROM_SPI_MOSI_LOW(); } data <<= 1; EEPROM_SPI_CLK_HIGH();//拉高CLK SPI_Delay(10); } }
软件模拟接收数据
uint8_t SPI_ReadData(void) { uint8_t i = 0; uint8_t value=0; for(i=0;i<8;i++) { EEPROM_SPI_CLK_LOW(); SPI_Delay(10); value<<=1; if(EEPROM_SPI_MISO()) { value |= 0x01; } EEPROM_SPI_CLK_HIGH(); SPI_Delay(10); } return value; }
这个是模拟的连接W25Qxx的读写数据,接下来看下模式0的读写
二、基于模式0的模拟SPI---
🌎交换一个字节(模式0)
CPOL=0:空闲状态下,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据(在硬件驱动SPI时,将这一过程称作奇数边沿采样或者偶数边沿采样)
//通讯开始 void MySPI_Start(void) { GPIO_WriteBit(GPIOB,GPIO_Pin_6,Bit_RESET);//片选信号线拉低,表示SPI通讯的开始 } //通讯结束 void MySPI_Stop(void) { GPIO_WriteBit(GPIOB,GPIO_Pin_6,Bit_SET);//片选信号线拉高,表示SPI通讯的结束
//交换一个字节,SPI是全双工通讯,MOSI和MISO同时进行工作, 也就是发送和接收是同时开始的,所以SPI的通讯速率比IIC要快 unsigned char MySPI_SwapByte(unsigned char ByteSend) { //交换的原则就是根据SCK高低电平的变换,将MOSI和MISO数据线上数据进行移入和移出 unsigned char i,ByteReceive = 0x00; for(i=0;i<8;i++) { //初始化SCK低电平,所以SCK低电平期间,主机取出要发送数据的最高位放在MOSI数据线上 GPIO_WriteBit(GPIOB,GPIO_Pin_9,(ByteSend&(0x80>>i))); GPIO_WriteBit(GPIOB,GPIO_Pin_8,Bit_SET); //SCK置高电平 //因为SPI发送和接收是同步进行的,所以主机将数据放至MOSI数据线的同时,从机也将数据放至MISO数据线上了 if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7)==1) //此时读取MISO数据线的电平,因为PA7设置上拉输入,所以若PA7引脚为高电平,则表示有数据交换 { //将接收的这一位放至变量ByteReceive的最高位,依次移位,获取完成的一个字节 ByteReceive = ByteReceive | (0x80>>i); } GPIO_WriteBit(GPIOB,GPIO_Pin_8,Bit_RESET); //SCK置低电平,完成一位的发送接收 } return ByteReceive; //这里解释一下为什么只判断了主机的电平变换,因为SPI可以设置主发送主接收、主发送从接收、主接收从发送; //所谓设置模式,简单来说就是,你可以发送,但是我不看你发送的内容就是了; }
上面的三个函数分别要对应的通讯开始和通讯结束,,然后是字节的发送和接收
初始化的代码就不说了,大家参照头部的解说就行,主要是发送数据和接收数据部分重要
接下来在看下实例,
1.先配置4根通信线
先初始化4根通信线,给它们配置成相应的状态函数
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_SS(1); //初始化好给片选引脚拉高 MySPI_W_SCK(0); //时钟引脚拉低 } void MySPI_W_SS(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); } void MySPI_W_MOSI(uint8_t BitValue) //主机输出引脚高低电平配置函数 { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); } uint8_t MySPI_R_MISO(void) 主机输入引脚读取高低电平 { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); }
2.起始和终止信号
按照起始和终止时序图写出模拟的高低电平
void MySPI_Start(void) { MySPI_W_SS(0); } void MySPI_Stop(void) { MySPI_W_SS(1); }
3.交换一个字节
主机发送一个字节,并且从从机得到一个字节
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; }
上面就是用GPIO口模拟一个SPI通信的时序用于主机发送或接收一个字节,具体的某款芯片对应的SPI通信要根据芯片手册来看给芯片发送什么字节或者我们需要接收什么字节,但是上面是SPI通信的基础有了这个就可以进行我们想要的接收和发送数据了,这样才能进行接下来的通信,
简单的步骤就是
(1)配置GPIO工作模式,/CS,CLK,MOSI引脚配置为输出模式,MISO配置为输入模式。
(2)编写模拟SPI函数。在使用模拟SPI时,选择了模式0,时钟极性为0,时钟相位为0。时钟空闲电平为低电平,上升沿捕获数据。
- 定义一个无符号字符类型的变量data,用于保存接收到的数据。
- 拉低时钟管脚。
- for循环中循环8次,读取或发送一个字节。
- 上升沿时捕获数据,所以在时钟管脚低电平时输出高电平或低电平,使其在上升沿到来之前保持稳定。这里使用三目运算符获取要发送字节的最高位。并将低以为左移至最高位,便于下一位传输。
- 拉高时钟管脚。
- 在时钟管脚为高电平时读取数据。并移位至对应位。
- 拉低时钟为下一次上升沿到来做准备,也可以在最后以为数据传输完成后保持时钟为高电平。
这里用W25Qxx来使用SPI发送指令
W25Q64模块代码
🐶1.写使能
void W25Qxx_Write_Enable(void) { SPI_Start();//拉低 SPI_SwapByte(W25Q64_WRITE_ENABLE);//发送获取ID指令 SPI_Stop();//拉高 }
🐶2.忙等待
void W25Qxx_WaitBusy(void) { uint32_t TimeOut = 100000; W25Qxx_Write_Enable();//写使能 SPI_Start(); SPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //读取寄存器状态 while((SPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //最低位1:忙;0:空闲!!!!!!!这个地方写错了!!!!!!!!!!!! { if(--TimeOut == 0) //防止程序卡死 break; } SPI_Stop(); }
🐶3.擦除
void W25Qxx_SectorEreza(uint32_t Address)//擦除函数 每次写入的时候提前擦除下内存 否则写入数据出错(只能1写0 不能0写1 相当于&) { W25Qxx_Write_Enable();//写使能 SPI_Start();//开始 SPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//发送指令 SPI_SwapByte(Address >> 16); SPI_SwapByte(Address >> 8); SPI_SwapByte(Address); SPI_Stop(); W25Qxx_WaitBusy();//等待 }
。。。。。。。。。。。。。。。。
。。。。。。。。。。。。。。。。
无非就是拉低发送数据,拉高读取数据,只是发送数据的方式不同,
有关W25Qxx的函数在W25Qxx那章的博客会详细讲,现在只是详细说下软件模拟SPI。