SPI介绍
SPI协议,用来传输数据的一种标准化协议。
SPI包括这些独特的特点:
-
主模式和从模式 -
双向模式 -
从模式选择输出 -
模式故障错误标志与CPU中断能力 -
双缓冲数据寄存器 -
具有可编程极性和相位的串行时钟 -
在等待模式下对SPI操作的控制
引脚描述:
? MOSI:此引脚用于在配置为主主模块时从SPI模块中传输数据,并在配置为从主模块时接收数据。(主出从入)
? MISO:在配置为SPI模块时从SPI模块中传输数据,在配置为主模块时接收数据。(主入从出)
? SS:(低有效)用于将选择信号从SPI模块输出到另一个外设,当其配置为主控时进行数据传输,当SPI配置为从控时作为输入来接收从选择信号。 ? 该引脚相当于片选。
? SCK:此引脚用于输出SPI传输数据或接收从属时钟的时钟。
时序分析
首先要了解两个概念:CPHA(Clock Phase,时钟相位)和CPOL(Clock Polarity,时钟极性)
-
时钟极性:是指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。 -
时钟相位:是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的”奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。
因此SPI关于时钟的配置,就有了以下4种情况:
模式 | CLK |
---|
CPOL = 0;CPHA = 0 | SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样 | CPOL = 1;CPHA = 0 | SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样 | CPOL = 0;CPHA = 1 | SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样 | CPOL = 1;CPHA = 1 | SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样 |
时序1:CPHA = 0
从时序图可以看出,当 CPHA = 0 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在奇数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。
时序2:CPHA = 1
从时序图可以看出,当 CPHA = 1 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在偶数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。
综上所述,可以发现SPI的协议自由度是比IIC要高一些的,给了开发者更多的自由搭配的空间。
比如在写OLED的spi的时候,可能就不用加上MISO引脚,但是要额外搭配DC(Data/Command)引脚,区别发送的是数据还是命令。
接下来我们分别看看硬件SPI和软件模拟SPI
STM32的硬件SPI
接下来我们看看STM32中的硬件SPI,这里以STM32F103RCT6为例。
功能框图
- MOSI、MISO、SCK、NSS与前文说过的一样,四根引脚。
- 波特率发生器:由框图可以看出,波特率发生器链接的是SCK,那么可想而知,这是用来产生时钟信号的,既然用来产生时钟信号,那么肯定和STM32的时钟有关,并且也能够进行分频之类的操作。并且框图也指出,寄存器SPI_CR1的BR[2,0] 位指向波特率发生器,由数据手册得知,该位是对 fpclk时钟的分频因子,对 fpclk的分频结果就是 SCK 引脚的输出时钟频率。其中的 fpclk频率是指 SPI 所在的 APB 总线频率。
计算结果如下:
BR[0:2] | 分频结果(SCK 频率) |
---|
000 | fpclk/2 | 001 | fpclk/4 | 010 | fpclk/8 | 011 | fpclk/16 | 100 | fpclk/32 | 101 | fpclk/64 | 110 | fpclk/128 | 111 | fpclk/256 |
-
数据控制单元:该部分包含接收缓冲区、发送缓冲区、数据移位寄存器。 发送数据的时候,数据移位寄存器将发送缓冲区内的数据一位一位发出去;接收数据的时候,数据移位寄存器则把把接收缓冲区内的数据一位一位读进来。并且每个数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式。配置“LSBFIRST 位”可选择高位先行(MSB)还是低位先行(LSB)。 对于数据寄存器(DR),通过写 SPI的数据寄存器可以把数据填入发送缓冲区,通过读SPI的数据寄存器可以获取接收缓冲区中的内容。 -
剩下的部分则是整体配置SPI相关的控制部分。SPI的运行模式,则随着我们这部分的配置的不同而不同。除了基本的SPI相关参数的配置,还包括SPI的中断信号、DMA请求、NSS信号线配置等。
从选择(NSS)脚管理,即SS引脚,进行片选
有2种NSS模式:
● 软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式。内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动,就可以将NSS引脚用作别的功能。
● 硬件NSS模式,分两种情况:
─ NSS输出被使能:当STM32作为主机,并且NSS输出已经通过SPI_CR2寄存器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并配置为硬件NSS的SPI设备,将自动变成从机。当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它设备它是主机;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。
─ NSS输出被关闭:允许操作于多主机环境。
通讯过程
(来自野火的《零死角玩转STM32》)
主模式收发流程及事件说明如下:
(1) 控制 NSS 信号线,产生起始信号(图中没有画出),即先将NSS拉低;
(2) 把要发送的数据写入到“数据寄存器 DR”中,该数据会被存储到发送缓冲区;
(3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则把数据一位一位地存储进接收缓冲区中;
(4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置 1,表示传输完一帧,接收缓冲区非空;这里的两个标志位要由软件清零。
(5) 等待到 “TXE 标志位” 为 1 时,若还要继续发送数据,则再次往 “数据寄存器DR” 写入数据即可;等待到 “RXNE标志位” 为 1 时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。
初始化结构体
typedef struct
{
uint16_t SPI_Direction;
uint16_t SPI_Mode;
uint16_t SPI_DataSize;
uint16_t SPI_CPOL;
uint16_t SPI_CPHA;
uint16_t SPI_NSS;
uint16_t SPI_BaudRatePrescaler;
uint16_t SPI_FirstBit;
uint16_t SPI_CRCPolynomial;
} SPI_InitTypeDef;
在与其他SPI从机搭配使用时,有时还要参考从机的数据手册;这类从机一般分为两类:
- 仅收发数据,比如FLASH芯片中的W25Q64;
- 收发数据和指令,这类从机额外使用DC引脚来区别SPI上发送的数据是单纯的数据还是指令,比如OLED等。
软件SPI
软件SPI就是用普通IO口模拟SPI的时序和通讯方法。这里我用SPI通讯方式的LCD搭配STM32CubeMX来做介绍
GPIO配置
其中DC用来区分写数据还是写指令
BLK调节LCD背光
CS即片选
SCK即时钟
SDA即MOSI信号线
需要注意的是,这里的SCK配置的是上拉,即使SCK时钟在空闲时为高电平,即CPOL = 1
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOA, DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
|RES_Pin, GPIO_PIN_SET);
GPIO_InitStruct.Pin = BLK_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(BLK_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
|RES_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
模拟时序
很典型的一个写8bit数据的函数,入口数据dat,首先选中LCD,进入循环,对dat拆出高位bit分析是1还是0,再决定MOSI输出的是1还是0,并且这个过程是在一个时钟脉冲内完成的。一次循环结束后,dat左移一位,将低一位的bit推向高位,为下一次循环做好准备。在8次循环过后,就完成了一个8bit数据的发送。其实LCD的使用中,SPI相关的只有以下函数,其他的都是在此之上进行的扩展。(没有明显区分采样时刻是奇数还是偶数边沿)
void LCD_Writ_Bus(u8 dat)
{
u8 i;
LCD_CS_Clr();
for(i=0;i<8;i++)
{
LCD_SCLK_Clr();
if(dat&0x80)
{
LCD_MOSI_1();
}
else
{
LCD_MOSI_0();
}
LCD_SCLK_Set();
dat<<=1;
}
LCD_CS_Set();
}
当然读取一个数据也可由上面推导出来,但是,LCD上并没有MISO引脚,所以各位看看就好。用不上。
uint8_t LCD_Read_Bus(void)
{
uint8_t i;
uint8_t value = 0;
LCD_CS_Clr();
for(i=0;i<8;i++)
{
LCD_SCLK_Clr();
value <<= 1;
if(LCD_MISO_READ() == 1)
{
value = value + 1;
}
LCD_SCLK_Set();
}
LCD_CS_Set();
return value;
}
比如写一个16位的数据
void LCD_WR_DATA(u16 dat)
{
LCD_Writ_Bus(dat>>8);
LCD_Writ_Bus(dat);
}
比如LCD写命令
void LCD_WR_REG(u8 dat)
{
LCD_DC_Clr();//写命令
LCD_Writ_Bus(dat);
LCD_DC_Set();//写数据
}
//因为大多数情况下是写数据,所以这里配置完写命令后要及时切换回写数据
总结
单单从SPI的基本协议来看,SPI可能比IIC更简单一些,只是分出了四种模式,但是单纯的SPI根据不同的从机要对协议进行不一样的扩展,这就提升了编程的难度,但是也加大了协议本身的自由度。速率方面的话,同一个芯片的硬件SPI的速率是绝对远超软件SPI的。
|