STM32F103 SPI(踩坑日记)

news/2024/6/3 18:48:38 标签: stm32, 单片机, arm, spi, dma

SPI学习

  • 前言
  • 1.SPI 协议
    • 1.1SPI的4种模式
  • 2. STM32F103 硬件SPI
    • 2.1 标准库的发送函数
    • 2.2 HAL库发送函数
      • 2.2.1 这里有个小坑
  • 3. SPI的连续传输和非连续传输
  • 4.SPI+DMA传输的坑

前言

第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数据发送


http://www.niftyadmin.cn/n/520442.html

相关文章

安装交叉编译器arm-none-liunx-gnueabihf

arm-none-liunx-gnueabihf安装前言下载交叉编译器解压缩添加环境变量&#xff1a;安装相关库参考资料前言 本教程针对的是ubuntu系统下安装arm交叉编译器的。 交叉编译器版本&#xff1a;9.2-2019.12-x86_64 ubuntu编本&#xff1a;18.04 下载交叉编译器 GNU官网上面有最新的…

minicom入门

minicom前言minicom是什么1、安装2、使用方法参考资料前言 ubuntu版本&#xff1a;18.04 内核版本&#xff1a;5.4.0 minicom是什么 minicom 是一款启动速度快&#xff0c;功能强大的串口终端调试工具&#xff0c;当然缺点就是纯字符界面&#xff0c;没有图形界面的调试工具…

*p++、*++p、++*p、(*p)++

整理c语言指针的时候发现几个很有意思的东西&#xff0c;直接先上代码在解释。 背景条件 GCC 版本7.4.0 i&#xff1a;先使用i&#xff0c;再递增 i&#xff1a;先递增&#xff0c;再使用i 优先级表&#xff1a; 同一优先级的运算符&#xff0c;运算次序由结合方向所决定。…

c语言思维导图(学习笔记)

版本 V1.0下载版本 gitee链接 V1.0分享版本这个好像可以直接看&#xff0c;不需要下载。 说明 1、free 2、目前内容主要包含内容包含&#xff1a; 数据类型、运算符号、指针、字符串、关键词、函数、伪指令、编译、内存这几个方面。 后续还会更新完善&#xff0c;并提供xmin…

数据结构与算法思维导图(学习笔记)

版本 数据结构与算法思维导图V1.0 V1.0分享版本这个可以直接看&#xff0c;不需要下载。 说明 1、free 2、目前内容主要包含内容包含&#xff1a; 数据结构与算法思维导图 包含&#xff1a;线性表、顺序结构、链式结构&#xff0c;栈与队列&#xff0c;串&#xff0c;二叉树…

计算机网络和LWIP思维导图

版本 gitee链接 V1.0分享版本这个可以直接看&#xff0c;不需要下载。 说明 1、free 2、目前内容主要包含内容包含&#xff1a; 数据结构与算法思维导图 包含&#xff1a;包含&#xff1a;OSI七层模型、TCP/IP四层模型、TCP、UDP、Socket、计算机网络基础这几个方面。 后续还…

CCS6 配置工程头文件路径方法

软件版本 Code Composer Studio Version: 6.1.3.00034 配置头文件 给工程配置&#xff1a; 右键工程名字没然后选择Properties 选择CCS Build->Cxxxx Compiler->Include Options然后点击 Add dir to.....框框中的绿色加号 选择对应的路径。 然后一路OK。 只给文件…

attribute的section属性

官方文档 Normally, the compiler places the objects it generates in sectionslike data and bss. Sometimes, however, you need additional sections,or you need certain particular variables to appear in specialsections, for example to map to special hardware. Th…