一 硬件SPI和软件SPI
顾名思义这种方式是硬件层面的,需要开启并使用单片机里面的SPI外设,IO口要复用到外设,由外设来控制电平翻转;软件模拟就可以随便选择IO口进行配置,需要手动控制电平翻转
硬件接线:
我使用的是外接Flash模块无论是硬件还是软件接线都使用的是相同的IO口
对应关系:
A4-CS
A5-SCK时钟线
A6-MISO-D0
A7-MOSI-D1 接线口诀:主机MOSI接从机DI 主机 MISO接从机DO
二 配置GPIO
配置IO文字描述:
配置GPIO口
把CS SCK MOSI配置为推挽输出
把MISO配置为浮空输入
默认CS高电平抬起不选中状态
硬件版本
void W25Q_SPI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
SPI_InitTypeDef SPI_InitStruct;
// 开启时钟:GPIOA、SPI1(SPI1属于APB2)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
// PA5 SCK、PA7 MOSI:复用推挽输出 AF_PP
GPIO_InitStruct.GPIO_Pin = SPI_SCK_PIN | SPI_MOSI_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// PA6 MISO:浮空输入
GPIO_InitStruct.GPIO_Pin = SPI_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// PA4 CS:普通推挽输出
GPIO_InitStruct.GPIO_Pin = FLASH_CS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(FLASH_CS_PORT, &GPIO_InitStruct);
FLASH_CS_H(); // 默认片选拉高,不选中Flash
// SPI配置
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // CPOL=0 空闲SCK低
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // CPHA=0 第一个边沿采样(Mode0)
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 软件管理片选
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; //72M/4=18M
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; //高位先发,Flash强制要求
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStruct); //硬件SPI必须调用SPI_Init
SPI_Cmd(SPI1, ENABLE); //使能SPI外设
}
软件模拟版本
//软件模拟SPI初始化
void W25Q_SPI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
// 只开启GPIOA时钟,删掉SPI1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// SCK PA5、MOSI PA7:推挽输出
GPIO_InitStruct.GPIO_Pin = SPI_SCK_PIN | SPI_MOSI_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// MISO PA6:浮空输入
GPIO_InitStruct.GPIO_Pin = SPI_MISO_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);
// CS PA4:推挽输出
GPIO_InitStruct.GPIO_Pin = FLASH_CS_PIN;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(FLASH_CS_PORT, &GPIO_InitStruct);
FLASH_CS_H(); // 默认片选拉高
}
三 收发字节函数的区别
收发字节文字描述:
发送一字节数据
函数有返回值因为是全双工 有发送就有接收
要发送的数据作为函数参数传入
通过循环从最高位开始发送数据
每次都要提取出最高位数据进行发送
然后发送数据左移一位
选择模式0 就是上升沿采样
SCK输出高电平,保持不变
读取MISO电平,进行或操作拼接接收电平数据
SCK输出低电平 停止采样
保持一段时间后,进行下一轮数据的发送和读取
硬件版本
uint8_t SPI_RW_Byte(uint8_t txDat)
{
//等待发送缓冲区为空
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, txDat);
//等待接收完成
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
return SPI_I2S_ReceiveData(SPI1);
}
软件模拟版本
uint8_t SPI_RW_Byte(uint8_t txdata)
{
uint8_t i;
uint8_t rxData = 0;
for(i = 0;i<8;i++){
if(txdata & 0x80){
GPIO_SetBits(SPI_GPIO_PORT, SPI_MOSI_PIN);
}else{
GPIO_ResetBits(SPI_GPIO_PORT, SPI_MOSI_PIN);
}
txdata <<= 1;
GPIO_SetBits(SPI_GPIO_PORT, SPI_SCK_PIN);
delay_us(2);//延时函数可以去掉 我加上是为了健壮性
rxData <<= 1;
if(GPIO_ReadInputDataBit(SPI_GPIO_PORT, SPI_MISO_PIN)){
rxData |= 0x01;
}
GPIO_ResetBits(SPI_GPIO_PORT, SPI_SCK_PIN);
delay_us(2);//同理,为了健壮性
}
return rxData;
}
这个SPI手写时序虽然最开始也是参照AI和现成代码写出来的,但是我会一行一行的理解,然后独立参照datasheet写出来,刚开始接触的时候确实感觉很难,但现在感觉好一点了