今天介绍一种新的通信协议——SPI,相比于之前学习的I2C,SPI通信的速率更快,相对而言,需要的资源就比较多,SPI通信需要六根线:四根通信线,SCK(时钟线),MOSI(主机输出从机输入),MISO(主机输入从机输出),SS(从机选择线),然VCC,GND;由于拥有时钟线所以采用同步时序,有两条数据传输线(I2C只有一条SDA)所以采用全双工;与I2C类似,支持挂载多个设备;SPI相比于I2C的好处是通信速率快,且时序简单易于学习,不足之处就是资源浪费
硬件电路要求:所有SPI通信设备的SCK,MOSI,MISO分别连在一起;主机的多条SS引脚分别接在从机的SS引脚上,SS引脚低电平代表主机开始跟从机通信了(主机一次只能选择一个从机进行通信),SS高电平代表主机结束通信;输出引脚(SCK,MOSI)配置为推挽输出(推挽输出:高低电平均有很强的驱动能力,使得SPI的上升沿与下降沿都十分迅速,从而通信速率得到提高),输入引脚(MISO)配置为浮空或上拉输入;
SPI交换字节移位示意图:
当时钟的上升沿的时候移位寄存器的高位将数据放到数据线上(主机数据放MOSI,从机数据放MISO),时钟的下降沿将数据线中的数据转移到移位寄存器的低位,过程重复八次,达到交换一个字节的目的
待交换的数据:(主机发送0xAA,从机发送0x55)
第一个时钟上升沿:(高位先行,所以向左移位)
第一个时钟下降沿:(在下降沿进行采样:将数据线上的数据转移到寄存器中)
第二个时钟上升沿:
第二个时钟下降沿:
直到第八个时钟,一个字节交换完毕,这就是SPI通信最基本的交换字节了,SPI的数据通信都是基于字节交换的,如果只想发送不想接收,那就不用在意从机发送过来的数据,直接执行一遍交换字节的时序,如果只想接收而不想发送,则可以随便发送一个数据(通常为0x00或0xFF),然后就接收到从机发过来的数据了;
SPI时序基本单元:
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平(SS在通信的过程中始终保持低电平)
SPI的四种模式:(通过CPOL,CPHA两个寄存器来配置SPI模式)
模式0:(MISO开始的一段中间线表示高阻态)
“SCK第一个边沿移入数据”,中的边沿不管是上升沿还是下降沿都可以;
由于第一个边沿移入数据(数据采样),所以在第0个边沿的时候就要移出数据,即在SS下降沿的时候就要移出数据了;
其余模式类似模式0:
模式1:
模式2:
模式3:
软件SPI工程代码:
写SCK,SS,MOSI,读MISO:
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);
}
软件SPI初始化:
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_Start(void)
{
MySPI_W_SS(0);
}
终止条件:
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
软件SPI交换字节:
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;//接收的数据
}
利用SPI读写W25Q64:
W25Q64指令集宏定义:
#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写使能:
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(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
{
Timeout --;
if (Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
按页写(不能跨页写,如果跨页写入,则会直接回到页首):
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i;
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//按页写指令
MySPI_SwapByte(Address >> 16);//写入24位地址
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
for (i = 0; i < Count; i ++)
{
MySPI_SwapByte(DataArray[i]);
}
MySPI_Stop();
W25Q64_WaitBusy();
}
按扇区擦除:
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable();
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//按扇区擦除指令
MySPI_SwapByte(Address >> 16);//写入24位地址
MySPI_SwapByte(Address >> 8);
MySPI_SwapByte(Address);
MySPI_Stop();
W25Q64_WaitBusy();
}
读数据:
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
uint32_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(W25Q64_DUMMY_BYTE);
}
MySPI_Stop();
}
读设备ID:
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_JEDEC_ID);//读ID指令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//厂商ID
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID高八位
*DID <<= 8;
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);设备ID低八位
MySPI_Stop();
}
硬件SPI:
硬件SPI简介:
SPI基本结构:发送数据时,先将数据放到发送数据寄存器TDR中,当移位寄存器中没有数据时,TDR中的数据转入移位寄存器中,置TXE标志位(TDR空标志位),接着时钟产生时序将移位寄存器中的数据发送出去;当发送完一个数据时,此时移位寄存器中也接收到了来自从机发送过来的数据,接着将数据转入接受数据寄存器RDR中,并置RXNE标志位(RDR非空标志位)
非连续传输数据(发送一个数据之后接收一个数据):
发送数据示意图:
实例解读:当准备发数据时,如果检测到TXE=1(TDR为空),则软件写入数据(0xF1),此时TDR为0xF1,TXE则变为0,如果移位寄存器为空,则数据(0xF1)转入移位寄存器里,然后移位寄存器就将数据发送出去了,接着等待RXNE为1,即接收到数据,此时可以读取RDR里的数据,然后将下个数据(0xF2)写入TDR,接着重复以上过程,循环八次即可交换一个字节
非连续传输的缺点:在TXE置1后没有及时将下个数据写入TDR,而是等待RXNE置1后才写入下一个数据,导致传输速率变慢
硬件SPI工程代码:由于起始条件和停止条件都只是简单的写SS电平,而SS引脚又是用GPIO模拟,所以起始条件和停止条件的代码与软件SPI一样,这里不在重复了
硬件SPI初始化:
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;//GPIO模拟SS引脚配置为通用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;//SCK,MOSI引脚配置为复用推挽输出
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;//MISO引脚配置为上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
SPI_InitTypeDef SPI_InitStructure;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;//配置当前设备为主机
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//配置双线全双工模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;//配置八位数据帧
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//配置高位先行
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;//配置波特率预分频系数
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;//配置时钟极性,默认低电平(模式0)
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//配置第一个边沿开始采样(模式0)
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//配置软件模拟SS引脚
SPI_InitStructure.SPI_CRCPolynomial = 7;//配置CRR校验多项式
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);//使能SPI1
MySPI_W_SS(1);//默认SS高电平
}
硬件交换字节:
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);//等待TXE为1
SPI_I2S_SendData(SPI1, ByteSend);//将数据写入TDR
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);//等待RXNE为1
return SPI_I2S_ReceiveData(SPI1);//读取从机数据
}
由于只是修改了通信底层代码,所以硬件SPI不需要重新写W25Q64代码,与软件相同