一、简介
1、SPI(Serial Peripheral Interface),串行外设接口,由Motorola公司开发的一种高速同步串行数据总线;近距离板内芯片通信首选(MCU↔Flash、LCD、传感器、ADC)。
同步:拥有独立时钟线,收发频率不受晶振偏差影响。
全双工:同一时刻可以同时发、同时收数据(I2C半双工、UART异步)。
支持总线挂载多设备(一主多从),不支持多主机;
缺点:无内置应答机制、无硬件地址,需要片选区分设备。
SPI支持:SPI1(APB2)、SPI2(APB1);
2、硬件--标准4线SPI
1)SCLK(SCK):串行时钟,主机输出,驱动数据移位;
2)MOSI:主机输出→从机输入(Master Out Slave In);
3)MISO:从机输出→主机输入(Master In Slave Out);
4)SPI支持:SPI1(APB2)、SPI2(APB12)。
5)NSS/CS:片选信号(低电平有效居多);
6)STM32F103系列引脚分配:
| 外设 | SCK时钟 | MOSI主机发/从机收 | MISO主机收/从机发 | NSS片选 | 总线时钟 |
| SPI1 | PA5 | PA7 | PA6 | PA4 | APB2最大36M |
| SPI2 | PB13 | PB15 | PB14 | PB12 | APB2最大18M |
3、主从架构
1)主机Master:产生SCLK时钟,控制通信发起(MCU一般作为主机);
2)从机(Slave):跟随SCLK,收到时钟移位数据;
3)一条SPI总线可以挂载多个从设备,依靠CS片选区分,总线不能自动寻址,SPI没有设备地址,想要切换设备必须先拉低对应CS,拉高其他CS。
二、SPI四大模式(CPOL+CPHA)
1、CPOL(Clock Polarity,时钟极性),空闲状态(无通信)时,SCLK电平:
CPOL=0:空闲SCLK=低电平;
CPOL=1:空闲SCLK=高电平;
2、CPHA(Clock Phase,时钟相位)
数据采样时刻:
CPHA=0:第一个时钟边沿采样数据;
CPHA=1:第二个时钟边沿采样数据;
3、4种常用模式
| 模式 | CPOL | CPHA | 说明 | 应用场景 |
| Mode1 | 0 | 0 | 最常用 | WQ25 Flash、LCD |
| Mode2 | 0 | 1 | 较少 | 部分ADC |
| Mode3 | 1 | 0 | 较少 | 部分射频芯片 |
| Mode4 | 1 | 1 | 较常用 | OLED、部分屏幕 |
三、SPI通信时序原理
1、移位寄存器交换机制
主机和从机内部各有一个8bit移位寄存器,每来一个SCLK脉冲:
主机移出1bit→MOSI发给从机;
从机移出1bit→MISO发给主机;
SPI本质:数据交换!只要发数据,必然同时收数据。全双工移位寄存器通信,写DR寄存器时就开始收发,发1字节同时收1字节。SPI没有单独只读函数读,读数据也要发送哑字节(0xFF);
即:主机写DR→移位寄存器移出数据到MOSI,同时MISO移入数据;等待TXE、RXNE标志;读DR得到收到的数据。
2、数据位序:MSB先行
绝大多数器件都是:高位先传(MSB First)。
3、CS时序
通信前:CS拉低;持续通信期间,CS保持低;
一组数据传输完成后,CS拉高;
很多芯片要求:CS拉高后,才能锁存收到的数据,连续传输不要频繁翻转CS。
四、通信速率
SCLK时钟由主机提供,常见范围:几十KHz到几十MHz。
限制因素:PCB走线长度、芯片最高支持频率,比如OLED一般10MHz。
五、SPI硬件架构两种方式
1、硬件SPI(MCU内置SPI外设)
芯片外设自动生成SCLK、移位、CPU只需要读写DR数据寄存器。
优势:速度高、不占用CPU,支持DMA。
缺点:固定引脚、资源有限。
2、软件SPI:GPIO模拟SCLK、MOSI、MISO。
任意GPIO引脚,软件循环翻转电平模拟时序。
优点:引脚自由,无硬件通道限制。
缺点:占用CPU,速率低,时序受代码影响,高频不稳定。
六、SPI vs UART vs I2C
| 特性 | SPI | I2C | UART |
| 连线 | 4线 | 2线 | 2线 |
| 通信方式 | 同步全双工 | 同步半双工 | 异步全双工 |
| 寻址 | CS片选、无地址 | 7/10位设备地址 | 点对点 |
| 速度 | 最高几十MHz | 最高400KHz | 波特率<1MHz |
| 应答 | 无硬件ACK | 自带ACK | 无 |
七、常见问题
1、SPI是交换数据,只读必须发送0xFF哑字节。
2、MISO:主机是输入,严禁配置为推挽输出。
3、CS片选极性问题。
八、标准库SPI结构体SPI_InitTypeDef
uint16_t SPI_Mode:主机/从机,(宏定义)
uint16_t SPI_Direction:全双工/半双工/单工,(宏定义);
uint16_t SPI_DataSize:8/16bit,一般选8bit(宏定义);
uint16_t SPI_CPOL:时钟极性(宏定义);
uint16_t SPI_CPHA:时钟相位(宏定义);
uint16_t SPI_NSS:NSS软件/硬件模式(宏定义);
uint16_t SPI_BaudRatePrescaler:波特率分频;
uint16_t SPI_FirstBit;:MSB先行/LSB先行;
uint16_t SPI_CRCPolynomial:CRC校验,不用就随便写7;
九、SPI软件模拟
1、模拟SPI4根线:SCK、MOSI(主机输出)、MISO(主机输入)、CS片选;STM32F103系列:
SCK:推挽输出GPIO_Mode_Out_PP;
MOSI:推挽输出GPIO_Mode_Out_PP;
CS:推挽输出GPIO_Mode_Out_PP;
MISO:上拉输入GPIO_Mode_IPU,是flash发给STM32,主机只读,不能配为输出;如果配为浮空输入,外部干扰易读到乱码。
2、Mode0时序:
片选CS拉低→先放MOSI数据→SCK拉高数据→读MISO→SCK拉低。
1)先放MOSI数据(准备阶段)
前提:主机先将片选信号(CS)拉低,激活从机。在SCK产生第一个上升沿之前,主机必须把第一位数据(通常是最高位MSB)稳定放MOSI线上。从机也会同步把第一位数据放在MISO线上。
2)SCK拉高数据(锁存阶段)
SCK产生上升沿,在这个瞬间,主从机同时读取对方数据线上的电平状态,并锁存到各自的移位寄存器中。
3)读MISO(接收阶段)
主机内部硬件自动将MISO线上刚锁存的状态读入,完成1位数据的接收。
4)SCK拉低(切换阶段)
SCK产生下降沿,主机和从机同时改变各自数据线的电平,准备下一位数据,然后流程回到第2步,直到8位数据传完。
总结:放数据→拉高采样→读数据→拉低换数据。也就是:数据在SCK为低电平时准备好,在上升沿被读取。
3、代码部分
MySPI.c文件
#include "stm32f10x.h" 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); } uint8_t MySPI_R_MISO() { 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() { 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_SCK(0); MySPI_W_SS(1); } void MySPI_Start() { MySPI_W_SS(0); } void MySPI_Stop() { MySPI_W_SS(1); } uint8_t MySPI_SwapByte(uint8_t SendByte) { uint8_t i; uint8_t ReceiveByte =0x00; for(i =0;i<8;i++) { MySPI_W_MOSI(SendByte &(0x80>>i)); MySPI_W_SCK(1); if(MySPI_R_MISO()==1) { ReceiveByte|=(0x80>>i); } MySPI_W_SCK(0); } return ReceiveByte ; }2、MySPI.h文件
#ifndef __MYSPI_H #define __MYSPI_H void MySPI_Init(); void MySPI_Start(); void MySPI_Stop(); uint8_t MySPI_SwapByte(uint8_t SendByte); #endif本文完!