硬件SPI通信协议

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、协议层

通过CPOLCPHA的不同组合,SPI可以有四种不同的通信方式。

CPOL(时钟极性):表示SCK在空闲时为高电平还是低电平,当CPOL=0时,SCK空闲时为低电平;当CPOL=1时,SCK空闲时为高电平。

CPHA(时钟相位):表示SCK在第几个时钟边缘采样数据,当CPHA=0时,在SCK第一个时钟边缘采样;当CPHA时,在SCK第二个时钟边缘采样。

四种模式分别为:

SPI模式CPOLCPHA说明
000时钟空闲状态为低电平,在第一个时钟边缘采样,即上升沿
101时钟空闲状态为低电平,在第二个时钟边缘采样,即下降沿
210时钟空闲状态为高电平,在第一个时钟边缘采样,即下降沿
311时钟空闲状态为高电平,在第二个时钟边缘采样,即上升沿

时序图如下:

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);//清除发送完成中断标志位
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值