STM32F103 SPI(踩坑日记)

前言

第1部分针对的spi的基础知识
第2、3部分是使用中遇到的坑和自己的理解。也欢迎大佬对文章中错误内容指出、更正。
可以有选择的阅读。

1.SPI 协议

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线。分别是以下4根

  1. MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;
  2. MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;
  3. SCLK – Serial Clock,时钟信号,由主设备产生;
  4. CS – Chip Select,从设备使能信号,由主设备控制。

1.1SPI的4种模式

spi的4种模式是通过CPOL和CPHA设置0和1来决定的。排列组合一下一共4种:
CPOL: SPI空闲时的时钟信号电平(1:高电平, 0:低电平)
CPHA: SPI在时钟第几个边沿采样(1:第二个边沿开始, 0:第一个边沿开始)
在这里插入图片描述
在这里插入图片描述
那么在STM32中体现是在这个结构体中(有省略):

typedef struct
{
......
  uint16_t SPI_CPOL;                /*!< Specifies the serial clock steady state.
                                        This parameter can be a value of @ref SPI_Clock_Polarity */
  uint16_t SPI_CPHA;                /*!< Specifies the clock active edge for the bit capture.
......
}SPI_InitTypeDef;

这边需要根据从站的datasheet来配置,比如下面的时序图:
在这里插入图片描述
SCLK默认为高电平,从sck的第二个边沿采集数据位,所以:
CPOL=1;
CPHA=1;

2. STM32F103 硬件SPI

STMf103的SPI->DR寄存器是个16位的,从参考手册上来看F1的芯片支持8位和16位的数据发送,通过SPI->CR1的DFF标志位来决定传输的数据是8位还是16位。这个也是需要参考从站的datasheet来决定的。 主模式波特率预分频系数(最大为fPCLK/2) ,所以SPI可以最高分到一个36MHz的频率。关于SPI的发送函数,标准库函数版本和HAL库提供了两种解决办法:

2.1 标准库的发送函数

/**
  * @brief  Transmits a Data through the SPIx/I2Sx peripheral.
  * @param  SPIx: where x can be
  *   - 1, 2 or 3 in SPI mode 
  *   - 2 or 3 in I2S mode
  * @param  Data : Data to be transmitted.
  * @retval None
  */
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_SPI_ALL_PERIPH(SPIx));
  
  /* Write in the DR register the data to be sent */
  SPIx->DR = Data;
}

assert_param这个断言可以不管,其实就是把数据写到DR寄存器,什么判断都没有。所以一般我们在使用的时候超时跳出和检测发送是否成功,如下:

while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
		{
		retry++;
		if(retry>200)return 0;//重试200次
		}			

但是执行retry++和SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET需要占用CPU时间,这会导致SPI在发送时出现非连续传输详见3.节

2.2 HAL库发送函数

HAL库的发送函数比较长,我这边做了删减只看8位的发送的过程。

HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
	.......
	  /* Set the transaction information */
 	hspi->State       = HAL_SPI_STATE_BUSY_TX;
  	hspi->ErrorCode   = HAL_SPI_ERROR_NONE;
 	hspi->pTxBuffPtr  = (uint8_t *)pData;
  	hspi->TxXferSize  = Size;
  	hspi->TxXferCount = Size;
  	.......
    while (hspi->TxXferCount > 0U)
    {
      /* Wait until TXE flag is set to send data */
      if (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE))
      {
        *((__IO uint8_t *)&hspi->Instance->DR) = (*hspi->pTxBuffPtr);
        hspi->pTxBuffPtr += sizeof(uint8_t);
        hspi->TxXferCount--;
      }
      else
      {
        /* Timeout management */
        if ((((HAL_GetTick() - tickstart) >=  Timeout) && (Timeout != HAL_MAX_DELAY)) || (Timeout == 0U))
        {
          errorcode = HAL_TIMEOUT;
          goto error;
        }
      }
    }

}

核心部分:

if (__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE))
   *((__IO uint8_t *)&hspi->Instance->DR) = (*hspi->pTxBuffPtr);

也是检查发送完成标志位和把数据写到DR寄存器中。但是HAL库写的比较好的是支持写数据长度了。一般我们不会只写8位,一般一条指令都有32位或者更长,HAL库的封装我只需要提供一个首地址,把Size设置成对应的长度就可以了。点击跳转

2.2.1 这里有个小坑

数据在单片机中存储是按小端格式存储的,但是往往数据发送是按大端格式发送的。
比如想发送01 02 03 04
但是我们很容易存成:

uint32_t data =0x01020304

然后把*pData指向data的地址,那么通过SPI发出去的数据会变成 04 03 02 01所以大端小端转换需要软件去做转换。

3. SPI的连续传输和非连续传输


前提条件,这里设置的是8位的spi传输,先看下传输32位数据时时序图的区别:
在这里插入图片描述

图3.1 SPI非连续传输

在这里插入图片描述

图3.2 SPI连续传输
结果是如果是非连续传输的时候,每8位之间sck信号会断开,产生一个1us左右的延时,这个延时大小可能不一样。我这边SPI速度是2.25MHz,那么我使用连续传输32位数据会快20%。 翻阅《STM32中文参考手册》

当在主模式下发送数据时,如果软件足够快,能够在检测到每次TXE的上升沿(或TXE中断),并立即在正在进行的传输结束之前写入SPI_DR寄存器,则能够实现连续的通信;此时,在每个数据项的传输之间的SPI时钟保持连续,同时BSY位不会被清除。如果软件不够快,则会导致不连续的通信;这时,在每个数据传输之间会被清除

从这段描述来说,只要你数据写的够快,他就可以连续传输。这显然是不靠谱的,SPI速度越快软件写入的速度会跟不上。那么怎么充分发挥ST硬件SPI的性能呢? 开启DMA模式。

4.SPI+DMA传输的坑

根据自己SPI通道选择DMA配置,使用STM32Cube进行配置
这是我写的一个发送函数,但是抓波形的时候发现发送的数据不对,但是不使用DMA发送就正常了。后来研究了很久才发现问题。

int sgm5349_RegWrite(sgm5349Device_t *device, uint8_t channel, uint16_t data, uint8_t update){
    HAL_StatusTypeDef status = HAL_OK;
    uint32_t regVal = 0;   
    /* Input Validation Check */
    if(device == NULL)    return -1;
    if(isChannelValid(channel) != 0)    return -1;
    /* 32bit register value generation with command = 0 or 2 or 3*/
    regVal = (channel << 20) | (data << 4);
    if(update == 1)        regVal |= (3 << 24);
    else if(update == 2)   regVal |= (2 << 24);
	regVal = LittleEndian2BigEndian(regVal);
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,0);
	HAL_SPI_Transmit_DMA(device->hspi, (uint8_t *)&regVal, 4);
	HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,1);
	
    if(status != HAL_OK)    return (int)status;
    return 0;
}

DMA发送代码如下所示(部分省略):

HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size)
{
......
  /* Set the transaction information */
  hspi->State       = HAL_SPI_STATE_BUSY_TX;
  hspi->ErrorCode   = HAL_SPI_ERROR_NONE;
  hspi->pTxBuffPtr  = (uint8_t *)pData;
  hspi->TxXferSize  = Size;
  hspi->TxXferCount = Size;
/* Enable the Tx DMA Stream/Channel */
  if (HAL_OK != HAL_DMA_Start_IT(hspi->hdmatx, (uint32_t)hspi->pTxBuffPtr, (uint32_t)&hspi->Instance->DR,
                                 hspi->TxXferCount))
  {
    /* Update SPI error code */
    SET_BIT(hspi->ErrorCode, HAL_SPI_ERROR_DMA);
    errorcode = HAL_ERROR;

    hspi->State = HAL_SPI_STATE_READY;
    goto error;
  }
 ......
}

问题出在DMA发送函数中,(uint32_t)hspi->pTxBuffPtr强制转换成了数据的地址。但是我们传入HAL_SPI_Transmit_DMA的可是一个局部变量regVal。当跳出sgm5349_RegWrite函数时局部变量就会释放regVal,那么这个局部变量的地址就没意义了。所以做了个简单测试加了个延时,就能正常发送数据。当然实际并不能这么做,可以通过加给局部变量+static修饰,也可以让程序通过查询发送完成标志位死等来解决这个问题。
最后获得了一个完整的连续的SPI数据发送

  • 10
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柒妖71

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值