1、简介
SPI(Serial Peripheral Interface,串行外设接口)是一种全双工同步串行通信接口,用于MCU与各种外围设备以串行方式进行通信以交换信息,通信速度最高可达25MHz以上。
SPI通常由四条线组成,一条主设备输出与从设备输入(Master Output Slave Input,MOSI),一条主设备输入与从设备输出(Master Input Slave Output,MISO),一条时钟信号(Serial Clock,SCLK),一条从设备使能选择(Chip Select,CS)。
2、物理层
1、连接方式
SPI可以一个主机连接多个从机,通过CS线来确定当前与哪一个从机进行通信,SPI的连接方式如下图所示:
1、当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同使用这3条总线;而每个从设备都有独立的这一条CS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。
2、输入的MISO为了防止两个设备同时响应造成短路,所以这里主机没有选中到的时候他们片选线都为高组态的状态
3、SPI使用CS信号线来进行寻址,当把要通信的设备的CS线拉低时,该设备被选中,即片选有效,开始进行通信。当CS线拉高时停止通信。
2、数据传输
数据的传输方式如图所示:
1、主机和从机都有一个移位寄存器,主机移位寄存器数据经过MOSI将数据写入从机的移位寄存器,此时从机移位寄存器的数据也通过MISO传给了主机,实现了两个移位寄存器的数据交换。无论主机还是从机,发送和接收都是同时进行的。
2、如果主机只对从机进行写操作,主机只需忽略接收的从机数据即可。如果主机要读取从机数据,需要发送一个空字节(0xff)来引发从机发送数据。
3、数据的传输可以选择低位先传输或者是高位先传输。
3、协议层
通过CPOL和CPHA的不同组合,SPI可以有四种不同的通信方式。
CPOL(时钟极性):表示SCK在空闲时为高电平还是低电平,当CPOL=0时,SCK空闲时为低电平;当CPOL=1时,SCK空闲时为高电平。
CPHA(时钟相位):表示SCK在第几个时钟边缘采样数据,当CPHA=0时,在SCK第一个时钟边缘采样;当CPHA时,在SCK第二个时钟边缘采样。
四种模式分别为:
SPI模式 | CPOL | CPHA | 说明 |
---|---|---|---|
0 | 0 | 0 | 时钟空闲状态为低电平,在第一个时钟边缘采样,即上升沿 |
1 | 0 | 1 | 时钟空闲状态为低电平,在第二个时钟边缘采样,即下降沿 |
2 | 1 | 0 | 时钟空闲状态为高电平,在第一个时钟边缘采样,即下降沿 |
3 | 1 | 1 | 时钟空闲状态为高电平,在第二个时钟边缘采样,即上升沿 |
时序图如下:
4、固件库的使用
1、初始化
/***********************************************
* @brief : SPI模式配置
* @param : void
* @return: void
* @date : 2023.10.20
* @author: L
************************************************/
void Spi1Init(void)
{
Spi1GpioInit();//初始化SPI使用的端口
SPI_InitTypeDef spi_config = {0};//spi初始化结构体句柄
//配置SPI模式
RCC_APB2PeriphClockCmd(SPI1_CLOCK,ENABLE);//开启SPI1时钟
spi_config.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;//4分频,即72M / 4,stm32f103c8t6在SPI最高支持18M
spi_config.SPI_DataSize = SPI_DataSize_8b;//8位数据
spi_config.SPI_Mode = SPI_Mode_Master;//主机模式
spi_config.SPI_CPOL = SPI_CPOL_Low;//时钟空闲时为低电平
spi_config.SPI_CPHA = SPI_CPHA_1Edge;//时钟的第一个跳变沿采样
spi_config.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//全双工
spi_config.SPI_FirstBit = SPI_FirstBit_MSB;//高位数据先发送
spi_config.SPI_NSS = SPI_NSS_Soft;//软件控制片选信号
spi_config.SPI_CRCPolynomial = 7;//CRC值计算的多项式
SPI_Init(SPI1,&spi_config);//初始化SPI1
SPI_Cmd(SPI1,ENABLE);
}
2、SPI+DMA模式(以LCD刷屏为例)
首先先初始化DMA:
/***********************************************
* @brief : 屏幕刷新使用的DMA初始化
* @param : buffer_size:传送的数据量
* paddr:外设基地址
* maddr:内存基地址
* @return: void
* @date : 2023.11.4
* @author: L
************************************************/
void LcdDmaInit(uint32_t buffer_size,uint32_t paddr,uint32_t maddr)
{
DMA_InitTypeDef lcd_dma = {0};//DMA句柄结构体
//DMA模式配置
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//开启DMA1时钟
DMA_DeInit(DMA1_Channel3);//复位DMA1的通道3,SPI1_Tx
lcd_dma.DMA_Mode = DMA_Mode_Normal;//不循环
lcd_dma.DMA_DIR = DMA_DIR_PeripheralDST;//从内存到外设
lcd_dma.DMA_PeripheralBaseAddr = paddr;//SPI1数据寄存器地址
lcd_dma.DMA_MemoryBaseAddr = maddr;//存储区地址
lcd_dma.DMA_BufferSize = buffer_size;//传送的数据量
lcd_dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//16位
lcd_dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//16位
lcd_dma.DMA_M2M = DMA_M2M_Disable;//关闭存储器到存储器模式
lcd_dma.DMA_Priority = DMA_Priority_Low;//最低等优先级
lcd_dma.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储区数据地址自增
lcd_dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不自增
DMA_Init(DMA1_Channel3,&lcd_dma);//初始化DMA1通道3
DMA_ClearITPendingBit(DMA1_IT_TC3);//清除DMA1通道3发送完成标志位
DMA_ITConfig(DMA1_Channel3,DMA_IT_TC,ENABLE);//使能传送完成中断
}
由于初始化屏幕发送的命令和数据都是8位的,所以SPI初始化的时候配置为8位数据的模式,但是屏幕显示的颜色是是16位的数据,所以DMA设置为16位的传输,SPI也需要修改为16位,可以使用这几句:
SPI1->CR1 &= 0xFFDF;//失能SPI1,CR1的SPE位置0,否则写入传输位数时会出错
//这里没使用库函数给的失能函数是因为发现里面好像是写错的
SPI1->CR1 |= 1 << 11;//传输位数设置为16位,CR1的DFF位置1
SPI_Cmd(SPI1, ENABLE);//使能SPI1
SPI_I2S_DMACmd(SPI1,SPI_I2S_DMAReq_Tx,ENABLE);//发送DMA请求
同时DMA的计数值每次都要重新载入:
/***********************************************
* @brief : DMA计数值重新载入
* @param : buffer_size:传送的数据量
* paddr:外设基地址
* maddr:内存基地址
* @return: void
* @date : 2023.10.23
* @author: L
************************************************/
void DmaRestart(uint32_t buffer_size,uint32_t paddr,uint32_t maddr)
{
DMA1_Channel3->CPAR = paddr;//重新写入外设地址
DMA1_Channel3->CMAR = maddr;//重新写入内存地址
DMA_Cmd(DMA1_Channel3,DISABLE);//失能DMA1通道3,才能重新写入计数值
DMA_SetCurrDataCounter(DMA1_Channel3,buffer_size);//重新写入计数值
DMA_Cmd(DMA1_Channel3,ENABLE);//使能DMA1通道3,开始传输数据
}
同时配置了DMA传输完成中断:
/***********************************************
* @brief : DMA1通道2中断优先级配置
* @param : void
* @return: void
* @date : 2023.11.2
* @author: L
************************************************/
void DmaNvicConfig(void)
{
NVIC_InitTypeDef dma_nvic = {0};
dma_nvic.NVIC_IRQChannel = DMA1_Channel3_IRQn;
dma_nvic.NVIC_IRQChannelPreemptionPriority = 5;
dma_nvic.NVIC_IRQChannelSubPriority = 0;
dma_nvic.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&dma_nvic);
NVIC_EnableIRQ(DMA1_Channel3_IRQn);//使能NVIC控制器
}
根据参考手册所说(如下图),DMA传送完成后需要等待SPI全部发送完成。
/***********************************************
* @brief : DMA1通道2中断回调函数
* @param : void
* @return: void
* @date : 2023.11.2
* @author: L
************************************************/
void DMA1_Channel3_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC3))
{
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET)
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_BSY));
SPI1->CR1 &= 0xFFDF;//失能SPI1,CR1的SPE位置0,否则写入传输位数时会出错
SPI1->CR1 &= 0xF7FF;//传输位数设置为8位,CR1的DFF位置0
SPI1->CR1 |= 1 << 6;;//使能SPI1
show_over = 1;//刷新完成
DMA_ClearFlag(DMA1_FLAG_TC3);//清除发送完成标志位
DMA_ClearITPendingBit(DMA1_IT_TC3);//清除发送完成中断标志位
}
}