图片均来自江科大STM32教学PPT
SPI
简介
- SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
- 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
- 同步,全双工
- 支持总线挂载多设备(一主多从)
硬件电路
- 所有SPI设备的SCK、MOSI、MISO分别连在一起
- 主机另外引出多条SS控制线,分别接到各从机的SS引脚
- 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
移位示意图
波特率发生器时钟上升沿,所有移位寄存器向左移动一位,移除去的位放到引脚上(输出数据寄存器);波特率发射器时钟下降沿,引脚上的位,采样输入到移位寄存器的最低位。
上升沿数据移出
MOSI数据是1(高电平),MISO数据是0(低电平)
下降沿数据移入
MOSI的数据会被采样输入到从机最低位;MISO的数据会被采样输入到主机最低位。
八个时钟周期过后
SPI的数据收发都是基于字节交换。
- 发送同时接收
- 只发送,但也会接收到数据,只是不管接收到的数据。
- 只接收,可以随便发送一个数据(一般发送0x00或者0xFF)和从机交换。
SPI基本时序单元
起止/终止
- 起始条件:SS从高电平切换到低电平
- 终止条件:SS从低电平切换到高电平
- 从机被选中,SS始终保持低电平
数据交换
🌟🌟🌟交换一个字节(模式0)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
SS下降沿,就要立马触发数据移出,SCK第一个上升沿才可以移入数据。
交换一个字节(模式1)
- CPOL=0:空闲状态时,SCK为低电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
交换一个字节(模式2)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据
交换一个字节(模式3)
- CPOL=1:空闲状态时,SCK为高电平
- CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据
SPI时序
IIC:寄存器+读写数据
SPI:指令码+读写数据
- 发送指令
- 向SS指定的设备,发送指令(0x06)
指定地址写
- 向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)
指定地址读
- 向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)
SPI软件模拟代码示例
#include "stm32f10x.h" // Device header
void MySPI_W_SS(uint8_t BitValu)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValu);
}
void MySPI_W_SCK(uint8_t BitValu)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValu);
}
void MySPI_W_MOSI(uint8_t BitValu)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValu);
}
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// SCK MOSI SS 为推挽输出
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);
// MISO 为上拉输入
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);
}
// SPI 起始信号
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
// SPI 终止信号
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
// SPI模式0 交换数据
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); // 上升沿时钟
// 接收数据线的状态,如果为1,则设置接收字节的对应位为1
if(MySPI_R_MISO() == 1)
{
ByteReceive |= (0x80 >> i);
}
MySPI_W_SCK(0); // 下降沿时钟
}
return ByteReceive; // 返回接收到的字节
}
/*
第二种写法
// SPI 交换数据
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t i, ByteReceive = 0x00;
for(i = 0; i < 8; i++)
{
// 将发送字节的最高位发送到MOSI线
MySPI_W_MOSI(ByteSend & 0x80);
// 将发送字节左移一位,准备发送下一位
ByteSend <<= 1;
// 上升沿时钟,将数据发送到从机并等待接收
MySPI_W_SCK(1);
// 检查MISO线的状态,如果为高电平(1),则设置发送字节的最低位为1
if(MySPI_R_MISO() == 1)
{
ByteSend |= 0x01;
}
// 下降沿时钟,完成数据的接收
MySPI_W_SCK(0);
}
// 返回接收到的字节
return ByteSend;
}
*/
SPI外设
简介
- STM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担
- 可配置8位/16位数据帧、高位先行/低位先行
- 时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)
- 支持多主机模型、主或从操作
- 可精简为半双工/单工通信
- 支持DMA
- 兼容I2S协议
STM32F103C8T6 硬件SPI资源:SPI1、SPI2
SPI框图
做主机就不需要MOSI,MISO对调,做从机才对调。
- 发送数据:数据到TDR,移位寄存器如果为空,数据就转到移位寄存器,置TXE=1,表示TDR为空,就可以把后面的数据转到TDR。
- 一旦数据移出完成,就代表数据移入完成(数据交换完成),然后数据被整体移入到RDR,置RXNE=1,表示RDR非空,就需要把数据从RDR尽快读出来。
SPI基本结构
主模式全双工连续传输
非连续传输
- 等待RXE=1
- 写入发送数据至TDR
- 等待RXNE=1
- 读取RDR接收数据
软件/硬件波形对比
硬件SPI代码示例
#include "stm32f10x.h" // Device header
void MySPI_W_SS(uint8_t BitValu)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValu);
}
void MySPI_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
// SS 为推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
// SCK MOSI 复用推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
// MISO 上拉输入
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);
SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master; // SPI模式为主机模式
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 数据传输为全双工模式
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b; // 数据大小为8位
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB; // 数据传输的第一位为最高位
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; // 时钟分频为128,即时钟频率除以128得到SPI时钟
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low; // 时钟极性为低电平时空闲
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge; // 时钟相位为第一个边沿采样
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft; // 片选信号由软件控制
SPI_InitStruct.SPI_CRCPolynomial = 7; // CRC多项式为7
SPI_Init(SPI1, &SPI_InitStruct); // 初始化SPI1外设
SPI_Cmd(SPI1, ENABLE); // 使能SPI1外设
// 初始化默认不选中从机
MySPI_W_SS(1);
}
// SPI 起始信号
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
// SPI 终止信号
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
// SPI模式0 交换数据
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
// 等待 SPI 发送缓冲区为空,以确保可以发送数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
// 发送数据 顺带清除标志位
SPI_I2S_SendData(SPI1, ByteSend);
// 等待接收缓冲区中有数据,以确保可以接收数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
// 从 SPI 接收数据 顺带清除标志位
return SPI_I2S_ReceiveData(SPI1);
}
)
{
// 等待 SPI 发送缓冲区为空,以确保可以发送数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
// 发送数据 顺带清除标志位
SPI_I2S_SendData(SPI1, ByteSend);
// 等待接收缓冲区中有数据,以确保可以接收数据
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
// 从 SPI 接收数据 顺带清除标志位
return SPI_I2S_ReceiveData(SPI1);
}