STM32使用SPI + DMA 实现WIFI数据异步传输(SDIO 协议 + HAL库)

这部分核心代码在 spi_sdio.c中

1. SPI

  关于SPI相信大伙都很熟悉了,在这里做个简单介绍

  1. SPI协议是同步全双工的协议(同步/异步:看有无时钟线,像串口就是异步通信) 全双工(看看是不是能边收边发)
  2. SPI的通信模式:CPOL和CPHA的配置
  3. CPOL:时钟平时是高电平还是低电平
  4. CPHA:是在每个时钟周期的第一个跳变沿/第二个跳变沿对数据进行采样

2. SPI + DMA 实现异步

  试想一下,平时SPI同步传输不用DMA, 意味着我们在SPI传输的时候CPU不能干别的,这实在是有点浪费CPU的资源,所以我们可以使用 SPI + DMA + 中断的方式进行异步传输,想要传输数据的时候,交给DMA去做,CPU去做别的事情。DMA做完之后通过中断告诉CPU:数据发送/接收完了,看你CPU下一步怎么安排吧。

2.1 HAL库配置DMA

  配置过程如下,有以下几点注意:

  1. 主要是注意SPI外设绑定的是DMA哪个Stream和哪个通道。我这里用的是SMT32F407ZGT6的SPI2,查数据手册就知道DMA了
  2. 因为我们是在FreeRTOS下使用的 所以我们的DMA中断最好受FreeRTOS的管理,这样调用xxFromISR的API的时候才不会断言失败
  3. 对于SPI,就算主机只想单独发送或接收数据,但是作为主机要提供时钟信号,此时发送和接收中断还是会同时被触发
void spi_sdio_gpio_configuration()
{
	
	// 使能DMA1时钟
    __HAL_RCC_DMA1_CLK_ENABLE();
	hdma_tx.Instance = DMA1_Stream4; // 根据实际硬件选择合适的DMA流
    hdma_tx.Init.Channel = DMA_CHANNEL_0; // 根据硬件手册选择合适的通道
    hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
	hdma_tx.Init.Mode = DMA_NORMAL;                 
	hdma_tx.Init.Priority = DMA_PRIORITY_LOW;
    hdma_tx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
	
		HAL_DMA_Init(&hdma_tx);


    // 关联DMA句柄到SPI的TX
		__HAL_DMA_ENABLE_IT(&hdma_tx, DMA_IT_TC | DMA_IT_TE); // 启用传输完成中断
		__HAL_DMA_CLEAR_FLAG(&hdma_tx,DMA_IT_TC |DMA_IT_TE);	
    __HAL_LINKDMA(&g_spi_handler, hdmatx, hdma_tx);
	
		// TX(发送)DMA中断优先级更高
		HAL_NVIC_SetPriority(DMA1_Stream4_IRQn, 4, 0); // 主优先级 0,子优先级 0
    HAL_NVIC_EnableIRQ(DMA1_Stream4_IRQn);
		
		
		// 设置接收的
		
		// 配置 RX DMA
		hdma_rx.Instance = DMA1_Stream3; // 根据实际硬件选择合适的 DMA 流 (SPI2 RX 为 DMA1_Stream3)
		hdma_rx.Init.Channel = DMA_CHANNEL_0; // 根据硬件手册选择合适的通道
		hdma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; // DMA 传输方向:外设到内存
		hdma_rx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增
		hdma_rx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
		hdma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据对齐:字节对齐
		hdma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 内存数据对齐:字节对齐
		hdma_rx.Init.Mode = DMA_NORMAL; // 启用普通模式,如果需要循环模式可以修改为 DMA_CIRCULAR
		hdma_rx.Init.Priority = DMA_PRIORITY_LOW; // 设置 DMA 的优先级(可以根据实际情况调整)
		hdma_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; // 禁用 FIFO 模式

		// 初始化 RX DMA
		HAL_DMA_Init(&hdma_rx);

		// 关联 DMA 句柄到 SPI2 的 RX
		__HAL_DMA_ENABLE_IT(&hdma_rx, DMA_IT_TC | DMA_IT_TE); // 启用传输完成中断(TC)和传输错误中断(TE)
		__HAL_DMA_CLEAR_FLAG(&hdma_rx, DMA_IT_TC | DMA_IT_TE); // 清除中断标志
		__HAL_LINKDMA(&g_spi_handler, hdmarx, hdma_rx); // 关联 SPI2 的 RX DMA 句柄

		// 设置 RX(接收)DMA 中断优先级
		HAL_NVIC_SetPriority(DMA1_Stream3_IRQn, 4, 0); // 主优先级 0,子优先级 0(确保 TX 和 RX 中断优先级设置合理)
		HAL_NVIC_EnableIRQ(DMA1_Stream3_IRQn); // 启用 DMA1 Stream3 IRQ(RX DMA 中断)
		
		SD_SPI_CLK(); // 使能SPI2时钟
		SD_SPI_SCK_GPIO_CLK(); //使能CLK对应的时钟
		SD_SPI_MISO_GPIO_CLK();
		SD_SPI_MOSI_GPIO_CLK();
		SD_CS_GPIO_CLK();
	
		SD_IRQ_GPIO_CLK();
		SD_PDN_GPIO_CLK();
	
	    /* 配置CRC外设时钟 */
    __HAL_RCC_CRC_CLK_ENABLE();
    

	
    GPIO_InitTypeDef GPIO_InitStruct;
	
		//配置PDN--是否上电 输出
		GPIO_InitStruct.Pin = SD_PDN_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SD_PDN_GPIO_PORT, &GPIO_InitStruct);
		SD_PDN(0);
		//配置SPI IRQ -- 使用该信号。
		//主机可以通过检测SPI_IRQ引脚的状态来知道Wi-Fi模块是否有数据需要处理
		GPIO_InitStruct.Pin = SD_IRQ_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLDOWN; //改成上拉试一试 不行就得是下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SD_IRQ_GPIO_PORT, &GPIO_InitStruct);
	
		// 配置SPI的NSS引脚
    GPIO_InitStruct.Pin = SD_CS_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
		GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
    HAL_GPIO_Init(SD_CS_GPIO_PORT, &GPIO_InitStruct);
		SD_CS(1);
		
    // 配置MISO引脚
    GPIO_InitStruct.Pin = SD_SPI_MISO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
    HAL_GPIO_Init(SD_SPI_MISO_GPIO_PORT, &GPIO_InitStruct);
    /* MISO引脚模式设置(复用输出) */
    GPIO_InitStruct.Pin = SD_SPI_MOSI_PIN;
    HAL_GPIO_Init(SD_SPI_MOSI_GPIO_PORT, &GPIO_InitStruct);
    /* SCK引脚模式设置(复用输出) */
    GPIO_InitStruct.Pin = SD_SPI_SCK_PIN;
		HAL_GPIO_Init(SD_SPI_SCK_GPIO_PORT, &GPIO_InitStruct);
		
	 __HAL_SPI_ENABLE(&g_spi_handler); /* 使能SPI */
    g_spi_handler.Instance = SD_SPI;                                	/* SPI2 */
    g_spi_handler.Init.Mode = SPI_MODE_MASTER;                        /* 设置SPI工作模式,设置为主模式 */
    g_spi_handler.Init.Direction = SPI_DIRECTION_2LINES;              /* 设置SPI单向或者双向的数据模式:SPI设置为双线模式 */
    g_spi_handler.Init.DataSize = SPI_DATASIZE_8BIT;                  /* 设置SPI的数据大小:SPI发送接收8位帧结构 */
    g_spi_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;               /* 串行同步时钟的空闲状态为高电平 */
    g_spi_handler.Init.CLKPhase = SPI_PHASE_2EDGE;                    /* 串行同步时钟的第二个跳变沿(上升或下降)数据被采样 */
    g_spi_handler.Init.NSS = SPI_NSS_SOFT ;                            /* NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 */
    g_spi_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 定义波特率预分频的值:波特率预分频值为256 */
    g_spi_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;                   /* 指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 */
    g_spi_handler.Init.TIMode = SPI_TIMODE_DISABLE;                   /* 关闭TI模式 */
		g_spi_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /* 启用CRC校验 */
		g_spi_handler.Init.CRCPolynomial = 0x1021;                   /* CRC-16-CCITT多项式 */
    HAL_SPI_Init(&g_spi_handler);                                     /* 初始化 */


}

对于我们的中断服务函数配置如下

// DMA发送完成中断处理函数
void DMA1_Stream4_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_tx);
}

// DMA接收完成中断处理函数 当接收完成的时候就会被调用然后可能会产生任务的切换
void DMA1_Stream3_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_rx);
}
// 会在传输完成时被调用
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
    if (hspi->Instance == SD_SPI) {
        spi_dma_done = 1;  // 设置标志,表示DMA完成
		BaseType_t xHigherPriorityTaskWoken = pdFALSE;
		xSemaphoreGiveFromISR(dma_finish_semaphore,&xHigherPriorityTaskWoken);/* 释放二值信号量 */
		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

  读到这里你可能会担心 ,前面说到SPI传输完成的时候DMA接收中断/DMA发送中断都会触发,HAL_SPI_TxRxCpltCallback是不是会被调用两次呢?—-答案是否定的,下面看看具体这个回调函数调用时机就知道了
  PS:这里写的不太好,后续应该吧HAL_SPI_ErrorCallback也写进来,也得释放信号量;

2.2 HAL库发送/接收API

在这里我只推荐HAL_SPI_TransmitReceive_DMA这一个函数,原因如下:

  1. 前面提到了,SPI的特性就决定了就算单独的发送和接收,只要作为主机同时使能了发送/接收中断,还是会这俩中断都触发
  2. HAL库的 HAL_SPI_Transmit_DMA / HAL_SPI_Receive_DMA 本质还是调用了HAL_SPI_TransmitReceive_DMA
  3. HAL_SPI_Receive_DMA
      因为我们满足了if判断,所以本质上还是调用了这个HAL_SPI_TransmitReceive_DMA,此时数据发送接收都用的同一个缓冲区,在我测试下有时会出问题
    HAL_SPI_Receive_DMA
  4. 要注意 HAL_SPI_TransmitReceive_DMA是一个异步函数,他不会死等发送结束后才返回,而是开启发送之后直接就会返回一个状态

2.2.1 HAL_SPI_TxRxCpltCallback的调用时机

对于 HAL_SPI_TxRxCpltCallback会在 SPI_DMATransmitReceiveCplt的最后调用该函数

  • SPI_DMATransmitReceiveCplt函数的功能
  • 重置状态
  • 错误清0
  • 调用回调函数
  • static void SPI_DMATransmitReceiveCplt(DMA_HandleTypeDef *hdma)
    {
      SPI_HandleTypeDef *hspi = (SPI_HandleTypeDef *)(((DMA_HandleTypeDef *)hdma)->Parent); /* Derogation MISRAC2012-Rule-11.5 */
     /*..省略代码..*/
     /* 根据状态调用回调函数*/
        if (hspi->ErrorCode != HAL_SPI_ERROR_NONE)
        {
          /* Call user error callback */
    #if (USE_HAL_SPI_REGISTER_CALLBACKS == 1U)
          hspi->ErrorCallback(hspi);
    #else
          HAL_SPI_ErrorCallback(hspi);
    #endif /* USE_HAL_SPI_REGISTER_CALLBACKS */
          return;
        }
      }
      /* Call user TxRx complete callback */
    #if (USE_HAL_SPI_REGISTER_CALLBACKS == 1U)
      hspi->TxRxCpltCallback(hspi);
    #else
      HAL_SPI_TxRxCpltCallback(hspi);
    #endif /* USE_HAL_SPI_REGISTER_CALLBACKS */
    }
    

    在HAL_SPI_TransmitReceive_DMA函数中指定了这个函数作为回调函数,可以看到此时只设定了hdmarx描述符,只是绑定了一次

    HAL_StatusTypeDef HAL_SPI_TransmitReceive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,
                                                  uint16_t Size)
    {
      // step1 : 判定参数合法性等等
      // step2 : 更新参数
    ...
      /* Check if we are in Rx only or in Rx/Tx Mode and configure the DMA transfer complete callback */
      if (hspi->State == HAL_SPI_STATE_BUSY_RX)
      {
        /* Set the SPI Rx DMA Half transfer complete callback */
        hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfReceiveCplt;
        hspi->hdmarx->XferCpltCallback     = SPI_DMAReceiveCplt;
      }
      else
      {
        /* Set the SPI Tx/Rx DMA Half transfer complete callback */
        hspi->hdmarx->XferHalfCpltCallback = SPI_DMAHalfTransmitReceiveCplt;
        hspi->hdmarx->XferCpltCallback     = SPI_DMATransmitReceiveCplt;
      }
        /* Set the SPI Tx/Rx DMA Half transfer complete callback *
      /* Set the DMA error callback */
      /* Set the DMA AbortCpltCallback */
      /* Enable the Rx DMA Stream/Channel  */
        /* Update SPI error code */
      /* Enable the Tx DMA Stream/Channel  */
      /* Check if the SPI is already enabled */
      /* Enable the SPI Error Interrupt Bit */
      /* Enable Tx DMA Request *
      SET_BIT(hspi->Instance->CR2, SPI_CR2_TXDMAEN);
    error :
    
    }
    

    而在HAL_DMA_IRQHandler中调用了 这个XferCpltCallback

    void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
    {
      	// 只看这部分代码就行
      	// IRQHandler的作用就是读取寄存器 根据寄存器的不同状态调用不一样的回调函数 
      	//并更新hdma的状态
          else
          {
            if((hdma->Instance->CR & DMA_SxCR_CIRC) == RESET)
            {
              /* Disable the transfer complete interrupt */
              hdma->Instance->CR  &= ~(DMA_IT_TC);
    
              /* Process Unlocked */
              __HAL_UNLOCK(hdma);
    
              /* Change the DMA state */
              hdma->State = HAL_DMA_STATE_READY;
            }
    
            if(hdma->XferCpltCallback != NULL)
            {
              /* Transfer complete callback */
              hdma->XferCpltCallback(hdma);
            }
          }
        }
      }
    

    2.3 使用二值信号量

      因为我们想要实现阻塞效果,我们的CPU在发起传输后就切换线程,可以去干别的事情了,此时我们可以通过信号量来确定什么时候返回这个wifi数据的处理线程。
      我们的回调函数实现如下所示,注意到因为是在中断中调用,所以要使用xxFromISR的API

    void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
        if (hspi->Instance == SD_SPI) {
            spi_dma_done = 1;  // 设置标志,表示DMA完成
    		BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    		xSemaphoreGiveFromISR(dma_finish_semaphore,&xHigherPriorityTaskWoken);/* 释放二值信号量 */
    		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
        }
    }
    

    2.3.1问题1: 为什么要有xxFromISR的API呢?

       这就涉及到FreeRTOS的中断管理了,我个人理解的原因有以下俩个方面:

    1. 中断是不能阻塞的,像在线程中那样设定阻塞时间这是不允许的
    2. 方便FreeRTOS对中断的管理
         这话听起来很奇怪,实际上通过阅读xxFromISR的API,可以发现进入API后总是先调用了portASSERT_IF_INTERRUPT_PRIORITY_INVALID()这个函数,这个函数做了什么呢
    3. portASSERT_IF_INTERRUPT_PRIORITY_INVALID函数
    4. step1:从中断寄存器读取当前中断的中断优先级
    5. step2:断言查看当前中断优先级是否满足中断管理要求
      step2就明白了,我们不是希望所有的中断处理函数都能调用xxFromISR这个API的,只有受FreeRTOS管理的中断才有资格调用这个API
       void vPortValidateInterruptPriority( void )
        {
            uint32_t ulCurrentInterrupt;
            uint8_t ucCurrentPriority;
            ulCurrentInterrupt = vPortGetIPSR();
            if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
            {
                /* Look up the interrupt's priority. */
                ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
                configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
            }
            configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
        }
    

    这个ucMaxSysCallPriority 等于多少呢?通过阅读源码会发现他就等于我们在FreeRTOSConfig.h中设定的configMAX_SYSCALL_INTERRUPT_PRIORITY
    configMAX_SYSCALL_INTERRUPT_PRIORITY这段代码在xPortStartScheduler也就是开启任务调度器时会设定

    2.3.2 问题2 : 为什么不在中断使用互斥信号量

    1. 互斥信号量会涉及到优先级继承问题,中断没有优先级啦。
    2. 我自己想不到其它的,欢迎大家补充

    2. 4封装SPI + DMA 发送和接收字节的API

    客观来说这里写的不太好,应该是:

    1. 传输完成的回调函数(HAL_SPI_TxRxCpltCallback) 和 传输错误的回调函数(HAL_SPI_ErrorCallback)都应该可以释放信号量,这里 xSemaphoreTake到信号量之后,应该根据状态HAL_SPI_GetState(&g_spi_handler)确认时传输完成还是错误发生
    2. xSemaphoreTake的参数不应该是portMAX_DELAY,设个较大值就行。然后根据xSemaphoreTake的返回值判定是超时还是得到了…
    3. void不行 应该定义一个枚举变量表明返回的状态

    2.4.1 发送一个字节

       因为后面涉及到sdio协议,可能有时候只需要发送一个两个字节,那发送一个字节还切换任务多不合理呀,毕竟任务的切换调用了PendSVHandler也是需要消耗资源的。所以这里的实现用的是异步非阻塞的方式。
      定义一个 static volatile变量 等待在回调函数中把spi_dma_done置为,此时 xSemaphoreTake直接得到,不会切换线程。

    uint8_t sd_spi_read_write_byte(uint8_t txdata)
    {
        uint8_t rxdata = 0x00;
    		spi_dma_done = 0;
    	//	xQueueReset(dma_finish_semaphore);  // 重置信号量;
        HAL_SPI_TransmitReceive_DMA(&g_spi_handler, &txdata, &rxdata, 1);
    		while(spi_dma_done == 0)
    		{
    			// 有个超时时间更好
    		}
        xSemaphoreTake(dma_finish_semaphore, portMAX_DELAY);		// 等待finiish
        return rxdata; /* 返回收到的数据 */
    }
    

    2.4.2 发送/接收多个字节

    发送和接收多个字节时,就要好好利用CPU的资源了,采用异步阻塞的模式。

    #define DUMMY_SIZE  (1024)
    static uint8_t dummy_buf[DUMMY_SIZE];						// 大可放心 每次最多512byte
    static void sd_spi_write_bytes(uint8_t* txdata,uint16_t len)
    {
    		if(txdata == NULL || len == 0)
    				return;
    		HAL_SPI_TransmitReceive_DMA(&g_spi_handler,txdata,dummy_buf,len);
    		// 加一个configASSERT更好
    		// HAL_SPI_TransmitReceive_DMA的返回值看看不是HAL_OK
    //		uint32_t count = 0;
    		xSemaphoreTake(dma_finish_semaphore, portMAX_DELAY);
    		// 测试用的 实际使用不用while等---也可以使用防止超时
    //		while (HAL_SPI_GetState(&g_spi_handler) != HAL_SPI_STATE_READY)	 	//按理来说是瞬间完成的
    //    {
    //        // 等待SPI DMA传输完成
    //				//count++;
    //				count++;
    //				if(count > 0xffff)
    //				{
    //						printf("发生溢出都没有准备好 写出问题了\r\n");
    //						break;
    //				}			
    //    }
    }
    static void sd_spi_read_bytes(uint8_t* rxdata,uint16_t len)
    {
    		if(rxdata == NULL || len == 0)
    				return;
    		HAL_SPI_TransmitReceive_DMA(&g_spi_handler,dummy_buf,rxdata,len);
    		// 加一个configASSERT更好
    		// HAL_SPI_TransmitReceive_DMA的返回值看看不是HAL_OK
    		xSemaphoreTake(dma_finish_semaphore, portMAX_DELAY);
    //		uint32_t count = 0;
    //		while (HAL_SPI_GetState(&g_spi_handler) != HAL_SPI_STATE_READY)
    //    {
    //				count++;
    //				if(count > 0xffff)
    //				{
    //						printf("发生溢出都没有准备好 读出问题了\r\n");
    //						break;
    //				}		
    //    }
    
    }
    

    对于这个程序来说1024字节的dummy肯定是够用了,下面sdio协议会讲清楚的

    2.4.3 同步与异步、阻塞与非阻塞

      PS:本人写这篇博客写到这里,也是想请大家能帮我解惑(QAQ)。关于这四个概念的排列组合,毕竟看了不少的文章了,但总归认知还是迷迷糊糊的,在这里献上自己的 一些浅薄理解。

  • 阻塞与非阻塞
      阻塞与非阻塞是描述当前线程的(在这里就是wifi线程)的行为,如果wifi线程在等待DMA发送的结果的时候把自己挂起了,那就是阻塞;相反如果没挂起自己,而是轮询之类的方式就算是非阻塞。
  • 同步与异步
    同样针对SPI 发送数据的请求,看看同步/异步实现都是咋样的。
  • 使用 HAL_SPI_TransmitReceive + 不使用 dma
      此时对于wifi线程,在数据发送这个请求结束之前,它不能做其它事情(虽然它已经把活(发送数据)交给底层SPI外设了,此时是底层的SPI外设在干活)。必须等待传输这件事结束才能干其它事情,这就是同步。
  • 使用 HAL_SPI_TransmitReceive_DMA
      此时对于wifi线程来说,它准备好数据并调HAL_SPI_TransmitReceive_DMA后,剩下的就是交给DMA和SPI外设去干活(发送数据)去了,此时也不会检查发送还有多久完成啊,SPI外设的状态啊,wifi线程直接去做其它事情了,此时就是异步的。
  • 中断作为实现异步的一种机制,其实在这里没有必要非得用DMA的,配置好SPI的中断(发送/接收完成使能),使用HAL_SPI_TransmitReceive_IT这个API也可以实现异步
  •   
    HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,
                                              uint32_t Timeout)
    {
    .........
     // 可以看到此时就是while循环在不断查询底层SPI的状态 等着它干完
    while ((hspi->TxXferCount > 0U) || (hspi->RxXferCount > 0U))
        {
          /* Check TXE flag */
          if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXE)) && (hspi->TxXferCount > 0U) && (txallowed == 1U))
          {
            hspi->Instance->DR = *((uint16_t *)hspi->pTxBuffPtr);
            hspi->pTxBuffPtr += sizeof(uint16_t);
            hspi->TxXferCount--;
            /* Next Data is a reception (Rx). Tx not allowed */
            txallowed = 0U;
    
    #if (USE_SPI_CRC != 0U)
            /* Enable CRC Transmission */
            if ((hspi->TxXferCount == 0U) && (hspi->Init.CRCCalculation == SPI_CRCCALCULATION_ENABLE))
            {
              SET_BIT(hspi->Instance->CR1, SPI_CR1_CRCNEXT);
            }
    #endif /* USE_SPI_CRC */
          }
    
          /* Check RXNE flag */
          if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXNE)) && (hspi->RxXferCount > 0U))
          {
            *((uint16_t *)hspi->pRxBuffPtr) = (uint16_t)hspi->Instance->DR;
            hspi->pRxBuffPtr += sizeof(uint16_t);
            hspi->RxXferCount--;
            /* Next Data is a Transmission (Tx). Tx is allowed */
            txallowed = 1U;
          }
          if (((HAL_GetTick() - tickstart) >=  Timeout) && (Timeout != HAL_MAX_DELAY))
          {
            errorcode = HAL_TIMEOUT;
            goto error;
          }
        }
      }
    
  • 我理解的同步阻塞 同步非阻塞 异步阻塞 异步非阻塞
  • 同步阻塞:wifi线程每阻塞xms后,就去检查SPI寄存器的状态,然后再阻塞xms;
  • 同步非阻塞: wifi线程使用while循环一直检查SPI寄存器的状态,直到发送完毕,才能做其它的事情;
  • 异步阻塞: 把发送数据的任务给SPI外设后,阻塞本线程xms,阻塞超时后可以做点其他的事情,直到中断给出响应;
  • 异步非阻塞: 把发送数据的任务给SPI外设后,本线程不阻塞可以做点其他的事情,直到中断给出响应。
  • 3. SDIO协议

      SPI只管发送,但是问题是对于wifi模块,你总是需要设置参数呀,或者对这个模块做一些操作,此时wifi模块和我们的开发板就得遵守相同的协议才行,此时开发板发送一堆数据过去后,wifi模块根据命令格式一解析就知道该怎么做了。
      SDIO是这样的,SPI是要关心发送就行了,SDIO需要考虑的可就多了。
      关于SDIO协议这里不做太多的介绍,我们需要实现的功能有:

    1. 完成SDIO的初始化配置–sd_init()
    unsigned char sd_init()
    {
        uint16_t retry;     /*  用来进行超时计数 */
        uint8_t ocr[10];
    	
    	spi_sdio_gpio_configuration();//初始化SPI的GPIO引脚
        sd_spi_speed_low(); /* 设置到低速模式 注:初始化时,时钟频率<400kHz */
    	dma_finish_semaphore = xSemaphoreCreateBinary();  // 最大值2,初始值0
    	UBaseType_t count = uxSemaphoreGetCount(dma_finish_semaphore);
        // 打印计数值
        printf("Current semaphore count: %u\n", (unsigned int)count);
        retry = 10;
    	//一旦通信的时候发现CS = 0就知道SPI了
        do
        {
    		SD_PDN(0);
    		delay_ms(10);
    		SD_PDN(1);
    		delay_ms(100);
            /* 重置SD卡进入默认状态,如果返回值为0x01(R1响应的in idle state为1),则表示SD卡复位成功 */
    		sd_send_cmd(CMD0, 0,ocr,1);         //res要么是timeOut 要么就是收到的长度
        } while ((ocr[0] != 0X01) && retry--);
    	printf("%c %c\r\n", ocr[0],ocr[1]);
    	if(ocr[0] == 0x01)
    	{
    		//printf("找到了\r\n");
    		/* 发送cmd5 */
    		sd_send_cmd(CMD5,0,ocr,sizeof(ocr));
    		// 重新上电就可以 但是不重新上电就不行
    		//PDN这条线的控制有问题 明明初始化好了插上就GG了
    		// 不插上就得重新断电
    		printf("CMD5, R1_%02X, RESP1_:%02x %02x %02x %02x\r\n", ocr[0], ocr[1], ocr[2], ocr[3], ocr[4]);
    		/* ocr 3.2V~3.4V*/
    		//查看电压支持不支持
    		sd_send_cmd(CMD5,0x300000,ocr,sizeof(ocr));
    		printf("CMD5_VOL, R1_%02X, RESP1_:%02x %02x %02x %02x\r\n", ocr[0], ocr[1], ocr[2], ocr[3], ocr[4]);
    		// 没有找到R4是MAarvell88w8801定义的
    		if (ocr[1] & _BV(7)) //最高位是不是1
    		{
    			// Card is ready to operate after initialization
    			sdio_func_num = (ocr[1] >> 4) & 7; //高8位的低三位就是func_num
    			printf("Number of I/O Functions: %d\r\n", sdio_func_num);
    			printf("Memory Present: %d\r\n", (ocr[1] & _BV(3)) != 0);
    			return sdio_func_num;
    			}else
    				printf("最高位不是1\r\n");
    		}
    		else
    			printf("GG了超时也没找到\r\n");
    		//SPI模式不需要CMD3 和CMD7 因为内是SD模式在选中卡 但是SPI靠的是CS引脚
    		return 0;
    }
    
    1. 完善SDIO发送指令的函数–sd_send_cmd()
    static int sd_send_cmd(uint8_t cmd, uint32_t arg,uint8_t *resp,uint8_t resp_len)
    {
    		//__disable_irq(); // 关闭总中断
        int res;
        uint8_t crc = 0X01;     /* 默认 CRC = 忽略CRC + 停止 */
    	memset(resp,0x00,resp_len);
        sd_deselect();      /* 取消上次片选 */
        if (sd_select()){
    		printf("在send cmd中发生了选中失败\r\n");
              return 0xFF;    /* 选中失败 */
         }
        // Prepare the data array
        uint8_t data[5];
        data[0] = cmd | 0x40;       // Command byte
        data[1] = (arg >> 24) & 0xFF;  // Argument byte 1
        data[2] = (arg >> 16) & 0xFF;  // Argument byte 2
        data[3] = (arg >> 8) & 0xFF;   // Argument byte 3
        data[4] = arg & 0xFF;          // Argument byte 4
    	for(uint8_t i = 0; i < 5; i ++)
    		sd_spi_read_write_byte(data[i]);
        crc = WiFi_LowLevel_CalcCRC7(data, 5); // Calculate CRC7 for the command and argument bytes
    	crc = (crc << 1) | 1; //这就是数据
        // Send the CRC7 value
        if (cmd == CMD0) crc = 0X95;        /* CMD0 的CRC值固定为 0X95 */
        if (cmd == CMD8) crc = 0X87;        /* CMD8 的CRC值固定为 0X87 */
        sd_spi_read_write_byte(crc);
        if (cmd == CMD12)   /* cmd 等于 多块读结束命令(CMD12)时 */
        {
            sd_spi_read_write_byte(0xff);   /* CMD12 跳过一个字节 */
        }
    	res = sd_get_receive_response(resp,resp_len); 	//接收到的消息有两种 要么超时-1 要么正常0
    	configASSERT(res != SD_TIMEOUT);
    	//__enable_irq(); 
    	return res; /* 返回状态值 */
    }
    

    4. 88w8801WIFI

      这个模块的话我建议大伙看看我的第一篇文章里推荐的几位博主的博客,Marvell88w8801WIFI专栏总结的很好很全面我就不赘述了
    对于一个wifi模块核心就是做三件事:

  • 读/写寄存器配置这个wifi模块,得到模块的状态—在这里是CMD52指令
  • 读取wifi模块收到的数据向上提交——CMD53指令
  • 对于lwip封装好的包进一步发送
      而完成者三件事 实际上就是进一步的封装 sd卡的CMD52号指令和CMD53号指令,然后根据sdio协议的连续读/连续写格式来写函数,怎么样连续读连续写可以参考这个视频。
    正点原子73讲 SD卡实验SPI模式
  • void WiFi_LowLevel_SendCMD52(uint8_t func, uint32_t addr, uint8_t data,
    	uint32_t flags, uint8_t *resp, uint8_t resp_len)
    {
    		// 31位 R/W FLAG:read 还是 write
    		// 30 29 28 fun num:那一类function
    		// 27 RAW flag  是否允许写入寄存器后直接读取寄存器装填 这关系到返回值
    		// 不设置的话 写入后 的response 和你写入的一样(实际可能不一样)
    		// 25-9 寄存器的地址
    		// 7-0 如果是写入的话就是要写入的数据 要是读的话就随便乱写
    		uint32_t arg = (func << 28) | (addr << 9) | data | flags;
    		// 响应是16位长度的
    		sd_send_cmd(CMD52, arg, resp, resp_len);
    }
    
    static int WiFi_LowLevel_SendCMD53(uint8_t func, uint32_t addr, uint16_t count,
    	uint32_t flags, uint8_t *resp, uint8_t resp_len)
    {
    	// 32位是需要我们设置的
    	// 31位 R/W flag
    	// 30 29 28:Fun Num
    	//	27:Bolck Mode: 为1的话,此时读取写入以块而不是字节为基本单位
    	// 块的大小 对于Fun1 - 7写入FBR中的IO块大小寄存器的
    	//					对于Fun0 	写入CCCR中的FN0块大小寄存器
    	// 设置之前先读取CCCR中的位判定是否支持
    	// 26: OP Mode:
    	// 0代表只从一个寄存器地址传送数据 FIFO是
    	// 1代表地址自增 当RAM等缓存区存在大量数据使用此命令 地址范围  [1FFFFh:0]
    	// 25 - 9:address 地址
    	// 8-0: count:如果不是块模式 此字段是要读取或者写入的字节数, 000h就是512字节
    	//						如果是块模式 那就是数据块数 000h就是无限 此时只能通过CCCR中的
    	//						I/O中止函数来做  
    	
    	//这里0x1ff是因为最大count就这么大了可不能再大了
      uint32_t arg = (func << 28) | (addr << 9) | (count & 0x1ff) | flags;
      // 只要是-1就是GG了 0就是正常
    	int res = sd_send_cmd(53, arg, resp, resp_len);
      return res;
    }
    

    5. 结语

      感谢您阅读到此,您的点赞鼓励都是给孩子莫大的鼓励感谢感谢,欢迎大家在评论区讨论并指出我在想法上的问题和错误!

    作者:听风lighting

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32使用SPI + DMA 实现WIFI数据异步传输(SDIO 协议 + HAL库)

    发表回复