RM遥控器接收程序的分析

2023-05-16

由遥控器接收分析串口与DMA

RM的遥控器在使用的过程中在大体上可以分成两个部分:“信息的接收”与“信息的解析”,在信息的接收中主要用到了串口的空闲中断和DMA双缓冲区接收在本篇的信息接收部分主要根据RM官方给出的代码来研究一下串口的空闲中断与DMA双缓冲区如何配合使用,在信息解析的时候主要来研究一下RM官方给出的代码例程是怎么在那解析的。

1. 信息的接收

1.1 串口初始化(寄存器)

例程

首先我们给出串口的初始化部分官方给出的代码

void RC_init(uint8_t *rx1_buf, uint8_t *rx2_buf, uint16_t dma_buf_num)
{
    //enable the DMA transfer for the receiver request
    //使能DMA串口接收
    SET_BIT(huart1.Instance->CR3, USART_CR3_DMAR);
    //enalbe idle interrupt
    //使能空闲中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

    //disable DMA
    //失效DMA
    __HAL_DMA_DISABLE(&hdma_usart1_rx);
    while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN)
    {
        __HAL_DMA_DISABLE(&hdma_usart1_rx);
    }
            hdma_usart1_rx.Instance->PAR = (uint32_t) & (USART1->DR);

    //memory buffer 1
    //内存缓冲区1
    hdma_usart1_rx.Instance->M0AR = (uint32_t)(rx1_buf);

    //memory buffer 2
    //内存缓冲区2
    hdma_usart1_rx.Instance->M1AR = (uint32_t)(rx2_buf);
    //data length
    //数据长度
    hdma_usart1_rx.Instance->NDTR = dma_buf_num;
    //enable double memory buffer
    //使能双缓冲区
    SET_BIT(hdma_usart1_rx.Instance->CR, DMA_SxCR_DBM);

    //enable DMA
    //使能DMA
    __HAL_DMA_ENABLE(&hdma_usart1_rx);

}

从上面的代码中我们也可以看出其中有很多直接对寄存器进行的操作,但是这些寄存器完成的功能看起来好像跟一些函数完成的相同的工作,这也给了我们一个深入研究HAL库或者说是stm32的方向。那么之后我们的分析方法就是将例程中的寄存器操作对照着参考手册搞懂他们是在干嘛的,并且那这些东西跟功能类似的HAL库提供的接口函数进行一个对照与比较。

注意:参考手册一定要找对,是F4参考手册,可不是F1的,否则会让你非常地困惑,我就是拿着F1的参考手册对着找了两三个小时,发现很多地方都对不上,时间就白白浪费掉了。

既然是想要以寄存器为切入点,那么我们就以用到的寄存器来做分段

USART_CR3

SET_BIT

程序一开始就来了一个SET_BIT来使能DMA串口接收。我们点到它的定义处可以发现,SET_BIT实际上是一个宏定义,而这个宏定义的作用就是将BIT赋值给寄存器REG。 而我们开启DMA串口接收则是将USART_CR3_DMAR赋值给串口1的CR3寄存器,当然,这里说到的赋值是“或等于”,这样可以不影响该寄存器的其他标志位。

#define SET_BIT(REG, BIT)     ((REG) |= (BIT))
SET_BIT(huart1.Instance->CR3, USART_CR3_DMAR);

寄存器值宏定义
#define USART_CR3_DMAR_Msk            (0x1UL << USART_CR3_DMAR_Pos)             /*!< 0x00000040 */
#define USART_CR3_DMAR                USART_CR3_DMAR_Msk                       /*!<DMA Enable Receiver*/

USART_CR3_DMAR同样是一个宏定义,它的值是0x00000040,也就是在CR3寄存器的第7位上赋值了1.我们可以查一下参考手册,看看这一位是什么作用。


注意,我们说的第七位是从1 开始的“第七个空”,而在手册上的位7是从0开始的第七位,因此我们要找的是位6,可以看到该位是DMA使能接收器

USART_CR1

__HAL_UART_ENABLE_IT

在使能了DMA串口接收后,打开了串口的空闲中断

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

__HAL_UART_ENABLE_IT又是一个宏定义,它里面干的活儿和上面的SET_BIT基本上是一样的,都是将宏定义好了的寄存器数值填入对应的寄存器中

#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__)   ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)): \
                                                           (((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)): \
                                                           ((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))

而UART_IT_IDLE是一个宏定义

#define UART_IT_IDLE                     ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_IDLEIE))

它是UART_CR1_REG_INDEX左移了28位后或上了一个USART_CR1_IDLEIE,关键点在于USART_CR1_IDLEIE,它的值是

#define USART_CR1_IDLEIE_Pos          (4U)
#define USART_CR1_IDLEIE_Msk          (0x1UL << USART_CR1_IDLEIE_Pos)           /*!< 0x00000010 */
#define USART_CR1_IDLEIE              USART_CR1_IDLEIE_Msk                     /*!<IDLE Interrupt Enable 

可以看到实际上UART_IT_IDLE就是将CR1的第五个空填上了1,也就是位4置了1. 第四位是啥?我们看手册

可以看到,是IDLE中断使能,也就是空闲中断使能

DMA_SXCR

__HAL_DMA_DISABLE

之后我们关闭了DMA,关闭DMA?为什么? 这也是我看到的时候的第一反应,别急,我们顺着我们的这个方法往下走,当答案浮现的时候,你会感到欣喜的。

__HAL_DMA_DISABLE,这又是一个宏定义,而且还是一对儿,他之前还有一个__HAL_DMA_ENABLE,我们把他俩一起给看了

#define __HAL_DMA_ENABLE(__HANDLE__)      ((__HANDLE__)->Instance->CR |=  DMA_SxCR_EN)
#define __HAL_DMA_DISABLE(__HANDLE__)     ((__HANDLE__)->Instance->CR &=  ~DMA_SxCR_EN)

可以看到,这个宏定义又是一个直接的寄存器赋值操作,赋的值都是DMA_SxCR_EN,只不顾另一个取反了。我们看一下DMA_SxCR_EN是个啥,然后去找它对应的寄存器位。

#define DMA_SxCR_EN_Pos          (0U)                                          
#define DMA_SxCR_EN_Msk          (0x1UL << DMA_SxCR_EN_Pos)                     /*!< 0x00000001 */
#define DMA_SxCR_EN              DMA_SxCR_EN_Msk     

可以看出,DMA_SxCR_EN它是想要置寄存器的第一个空,也就是位0

看到了么,该位是数据流使能,也就是这一位置1,DMA才能真正发挥作用,因为DMA就是传输数据流的嘛。

在关闭DMA后紧跟着一个While循环

while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN)
{
__HAL_DMA_DISABLE(&hdma_usart1_rx);
}

我们可以试着看一下这个循环到底在干嘛,用人话讲出来就是:如果CR寄存器与上DMA_SxCR_EN为1(也就是如果SXCR寄存器的第一位只要还是1),就再给我关掉DMA.意思是非得给人家关了不行?

这一步是干啥的,我们还不太清楚,没事儿继续往下走。但是在这里我们先做一个知识的补充:

知识补充:

我们知道HAL库是将每一个外设都给封装成了一个句柄,具体来说就是一个“外设_HandleTypeDef” 实例化了的一个对象。例如,我们操作的串口1,就是在操作UART_HandleTypeDef示例化了的huart1。

这个huart1,里面包含了很多的东西。例如这个外设现在的状态,以及很多的配置项。其中有一项非常重要,可以说我们对这个外设的大部分操作都是在修改这个东西里面的值。这个很重要的“东西”就是Instance,我们以huart1中的instance为例,来看看这里面到底是啥

USART_TypeDef                 *Instance;        /*!< UART registers base address        */
typedef struct
{
  __IO uint32_t SR;         /*!< USART Status register,                   Address offset: 0x00 */
  __IO uint32_t DR;         /*!< USART Data register,                     Address offset: 0x04 */
  __IO uint32_t BRR;        /*!< USART Baud rate register,                Address offset: 0x08 */
  __IO uint32_t CR1;        /*!< USART Control register 1,                Address offset: 0x0C */
  __IO uint32_t CR2;        /*!< USART Control register 2,                Address offset: 0x10 */
  __IO uint32_t CR3;        /*!< USART Control register 3,                Address offset: 0x14 */
  __IO uint32_t GTPR;       /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

我们看到Instance是一个USART_TypeDef类型的结构体,里面装的是 与串口相关的寄存器,,准确得说,结构体里面装的是串口相关的寄存器的值。而这里面的寄存器名字和参考手册上的名字是一一对应的,因此我们可以通过这里来判断我们去参考手册的哪里找。

有的时候我们需要知道某个外设的某个寄存器的地址,例如在DMA从外设到内存传输的时候,我们需要知道外设的对应存放数据的那个寄存器的具体地址,例如在串口DMA中我们就要知道串口的DR寄存器(因为串口接收的数据是存到这个寄存器里的)的地址,好让DMA知道从哪拿数据。 如果要那某个外设的寄存器的地址,我们就要用“外设名->寄存器名” 例如:

USART1->DR
DMA1->HIFCR
CAN2->BTR
...

DMA_SxPAR

在有了上面的那个补充的知识后我们可以更加得心应手地去查看手册并且可以很自信地明白下面这些操作是在干啥。

在我们退出while循环之后,我们把USART1的DR寄存器地址赋值到了DMA的PAR寄存器中,我们来看一下PAR寄存器是有什么本事

这个寄存器是存放读/写数据的外设数据寄存器的地址的。 所谓PAR就是peripheral address register外设地址寄存器。

它的作用就是DMA在用外设到存储器模式的时候,高速DMA外设是在哪,该去哪拿数据。

在看这个寄存器的时候注意最下面的一句话“这些位收到写保护,只有DMA_SxCR寄存器中的EN为0时才可以写入”,你可能会联想到些什么,可能还没有完全醒悟,我们可以接着往下看,到时候会给你揭晓。

DMA_SxM0/1AR

在将外设地址写入到DMA的PAR寄存器中之后,我们又紧接着进行了两此赋值操作,将两个数组的地址赋值给了DMA的M0AR和M1AR寄存器中。

这两个寄存器是用来存放存储器的地址的,作用就是告诉DMA数据拿到了以后拿去哪、放到哪。

他们和PAR寄存器一样,都有这样一句话这些位收到写保护

DMA_SxNDTR

之后我们在DMA的NDTR寄存器中写入了一个16位的数,这个数是DMA传输的大小,

这个寄存器只有16位可用,最大值是65535。它的作用就是告诉DMA传多少个 数据以后结束(当然如果开了循环模式的话不会停,会进入DMA传输完成中断,然后再重新装填该寄存器,然后继续传输)。因为每次DMA传输后此寄存器将递减,该寄存器还有计数的作用,这一点我们后面在信息解析的时候会提到。

这里面同样有一句话需要注意:“只有在禁止数据流时,才能向此寄存器执行写操作

DMA_SxCR

在我们通过填写寄存器的值告诉了DMA,明确了从哪拿拿哪去拿多少的问题后我们使能了双缓冲区,也就是告诉DMA:我可是给你开了两个缓冲区的,目的地有两个,别忘了“雨露均沾”,为什么要用雨露均沾?因为使能了双缓冲区并且开启了循环模式之后,在一个缓冲区填满后,DMA会自动地去把数据sei到另一个缓冲区中,这可不就是雨露均沾么

#define DMA_SxCR_DBM_Msk         (0x1UL << DMA_SxCR_DBM_Pos)                    /*!< 0x00040000 */
#define DMA_SxCR_DBM             DMA_SxCR_DBM_Msk  


 SET_BIT(hdma_usart1_rx.Instance->CR, DMA_SxCR_DBM);

这一步同样是用SET_BIT来操作的,赋值的是DMA的CR寄存器的位18

这里又出现了那句话 “此位受到保护”

最后我们又通过调用__HAL_DMA_DISABLE的对象 __HAL_DMA_ENABLE打开了DMA。

流程总结:

  1. 通过向usart的CR3寄存器的第7位置1,开启串口外设的DMA
  2. 调用__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); 使能串口的空闲中断
  3. 调用__HAL_DMA_DISABLE(&hdma_usart1_rx); 失能DMA,来保证之后的数据流传输地址能够被顺利写入。
  4. 向DMA的PAR寄存器中写入USART的数据寄存器的地址即源地址
  5. 分别向DMA的寄存器SxM0AR和SxM1AR中写入两个存储区的地址
  6. 向DMA寄存器SxCR的第19位DBM,注意不是位19 ,位19说的是从0开始数。第19位是从1开始数)置1,开启双缓冲区模式
  7. 向DMA的NDTR寄存器中写入数据流长度,因为是双缓冲区,因此接了一帧数据(18个字节)的双倍,也就是36个字节
  8. 调用__HAL_DMA_ENABLE(&hdma_usart1_rx);开启DMA

解开疑惑

相信在这之前,大家心中的疑惑便已经解开了。疑惑是什么?疑惑就是下面这段代码的意义。

__HAL_DMA_DISABLE(&hdma_usart1_rx);
while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN)
{
    __HAL_DMA_DISABLE(&hdma_usart1_rx);
}

第一个问题,为什么要把DMA给关掉?我给大家的暗示已经够多了。

因为接下来我们要对DMA的相关寄存器进行配置,,明确从哪拿拿哪去拿多少。为了达到这个目的,我们需要向对应的寄存器写入数据、配置,而这些寄存器都有写保护,也就是说“当DMA已经被开启,DMA的SXCR寄存器的第一位被置1时,这些配置寄存器是无法进行写入的”因此我们需要将DMA给关掉,才能把配置写入,让DMA按照我们想要的方式运行。

第二个问题,后面这个while循环是在干嘛?

因为参考手册中有这样一段话:

警告: 要关闭连接到 DMA 数据流请求的外设,必须首先关闭外设连接 DMA 数据流,然后等待 EN = 0**。只有这样才能安全地禁止外设 这就是为什么要加那个while等待的原因==

如果使能了数据流,通过重置 DMA_SxCR 寄存器中的 EN 位将其禁止,然后读取此位

以确认没有正在进行的数据流操作。将此位写为 0 不会立即生效,因为实际上只有所有

当前传输都已完成时才会将其写为 0。当所读取 EN 位的值为 0 时,才表示可以配置数

据流。因此在开始任何数据流配置之前,需要等待 EN 位置 0。应将先前的数据块 DMA

传输中在状态寄存器(DMA_LISR 和 DMA_HISR)中置 1 的所有数据流专用的位置 0,

然后才可重新使能数据流。

也就是说:参考手册可以解决掉我们大部分的问题,但是关键是我们要找到它到底写在哪。反正这个警告是我无意间翻到的…

1.2 DMA与双缓冲区的开启(API对比)

通过1.1的讲解,相信大家对寄存器如何配置串口DMA有了比较详细的认识,但是毕竟相比较于调用HAL给我们的函数,我们还是很少会用到寄存器直接编程的,那么他们的区别到底在哪里?搞懂这些区别与联系,相信会对HAL库编程有一个更详细的理解。下面我们开始对比吧

库函数开启双缓冲区

那在网上看到的配置过程:

HAL_DMAEx_MultiBufferStart() 用了这个函数来配置双缓冲区

HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)

SrcAddress:源内存缓冲区地址;
DstAddress:目标内存缓冲区地址;
SecondMemAddress:第二个内存缓冲区地址;
DataLength:从源传输到目标的数据长度;

HAL_DMAEx_MultiBufferStart具体代码长这样:

HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)
{
  HAL_StatusTypeDef status = HAL_OK;
  
  /* Check the parameters */
  assert_param(IS_DMA_BUFFER_SIZE(DataLength));
  
  /* Memory-to-memory transfer not supported in double buffering mode */
  if (hdma->Init.Direction == DMA_MEMORY_TO_MEMORY)
  {
    hdma->ErrorCode = HAL_DMA_ERROR_NOT_SUPPORTED;
    status = HAL_ERROR;
  }
  else
  {
    /* Process Locked */
    __HAL_LOCK(hdma);
    
    if(HAL_DMA_STATE_READY == hdma->State)
    {
      /* Change DMA peripheral state */
      hdma->State = HAL_DMA_STATE_BUSY; 
      
      /* Enable the double buffer mode */
      hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM;  //DMA_SxCR_DBM : 0x00040000 
      
      /* Configure DMA Stream destination address */
      hdma->Instance->M1AR = SecondMemAddress;
      
      /* Configure the source, destination address and the data length */
      DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);
      
      /* Enable the peripheral */
      __HAL_DMA_ENABLE(hdma);
    }
    else
    {
      /* Return error status */
      status = HAL_BUSY;
    }
  }
  return status;
}

这个开启双缓存区的函数干了哪些工作:

  1. 首先进来先判断了传递的参数的正确性和目前DMA的模式的正确与否。
assert_param(IS_DMA_BUFFER_SIZE(DataLength));

IS_DMA_BUFFER_SIZE(SIZE) (((SIZE) >= 0x01U) && ((SIZE) < 0x10000U))

这个函数是在检验设置的数据长度是否合规长度要大于1小于10000

并且判断如果是从内存到内存模式的话是不允许循环模式的,也就不能够开启双缓存区。

  1. 如果满足条件的话就先锁上dma。

    __HAL_LOCK(hdma);
    

    这个锁是Process Locked,起到的作用类似于上厕所的时候厕所门的那个“有人”标志,如果上了操作系统,多个进程运行,那么就要避免同时去操作dma的情况,尤其是同时用dma去写入东西,因为那样就不知道数据到底是谁写的了。因此有进程在用dma的时候就先把DMA给占住,说:我在用它了 。

  2. 在DMA外设的CR寄存器中赋值

/* Enable the double buffer mode */
hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM;

这个DMA_SxCR_DBM的值就是0x00040000

  1. 将第二个内存缓冲区地址写入DMA的M1AR寄存器中

    /* Configure DMA Stream destination address */
          hdma->Instance->M1AR = SecondMemAddress;
    
  2. 调用这个函数来配置dma的source、destnation address和数据长度

     DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);
    

    这个函数里的内容和直接用寄存器操作相同,也是在NDTR寄存器中写入数据长度,在PAR寄存器和M0AR寄存器中分别写入源地址和目标地址。

  3. 开启DMA。

这样分析下来,我们可以发现,不论是调用函数还是直接使用寄存器赋值,流程上几乎是一样的,都是都是先关闭DMA,然后给各种相关寄存器进行赋值,最后开启DMA。

库函数开启串口DMA

同样,我们在网上看到的的一些教程是如何教我们开启DMA的呢? 大多都是让调用下面这个函数

HAL_UART_Receive_DMA

那么用SET_BIT写入usart的CR3寄存器开启DMA接收和直接用HAL_UART_Receive_DMA函数开启DMA接收有什么区别?

我认为:这两种形式都可以起到开启DMA的作用,但是用SET_BIT直接操作寄存器赋值更单纯,仅仅开启了DMA的接收,而HAL_UART_Receive_DMA在开启DMA传输的时候会打开DMA传输完成中断。

我们可以来看一下HAL_UART_Receive_DMA这个函数内部到底在干嘛

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* Check that a Rx process is not already ongoing */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    /* Set Reception type to Standard reception */
    huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

    return (UART_Start_Receive_DMA(huart, pData, Size));   ☆☆☆☆☆☆
  }
  else
  {
    return HAL_BUSY;
  }
}

我们可以发现,在这个函数中很大一部分的内容都是在维护USART的状态,以保证这个外设不会被我们用着用着给搞得烂七八糟的。

其中真正起到“实质性作用”是我上面打了星星的那一行,HAL库主要调用UART_Start_Receive_DMA这个函数来开启USART的DMA。

我们再看看这个函数里是在干嘛

HAL_StatusTypeDef UART_Start_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  uint32_t *tmp;

  huart->pRxBuffPtr = pData;
  huart->RxXferSize = Size;

  huart->ErrorCode = HAL_UART_ERROR_NONE;
  huart->RxState = HAL_UART_STATE_BUSY_RX;

  /* Set the UART DMA transfer complete callback */
  huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;

  /* Set the UART DMA Half transfer complete callback */
  huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;

  /* Set the DMA error callback */
  huart->hdmarx->XferErrorCallback = UART_DMAError;

  /* Set the DMA abort callback */
  huart->hdmarx->XferAbortCallback = NULL;

  /* Enable the DMA stream */
  tmp = (uint32_t *)&pData;
  HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);  ☆☆☆☆

  /* Clear the Overrun flag just before enabling the DMA Rx request: can be mandatory for the second transfer */
  __HAL_UART_CLEAR_OREFLAG(huart);

  /* Process Unlocked */
  __HAL_UNLOCK(huart);

  /* Enable the UART Parity Error Interrupt */
  ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);

  /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
  ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

  /* Enable the DMA transfer for the receiver request by setting the DMAR bit
  in the UART CR3 register */
  ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

  return HAL_OK;
}

其中开局又是一套状态的维护,紧接着设置了一系列的回调函数。然后我们往下找找找,找到到了熟悉的字眼“SET_BIT”

/* Enable the DMA transfer for the receiver request by setting the DMAR bit
in the UART CR3 register */
ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

这句话开启了我们的USART的DMA传输,和我们的例程中的操作可谓是一模一样。

但是,在我们从上往下找的时候,发现了标注五角星的那条语句

HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);

Start_IT!!!,谁让这家伙给我开启DMA中断的,在百度中没有人跟我说过还有个DMA的中断呀?不都是用的串口的中断么?

开中断?开了什么中断?我再看看你这家伙偷偷干了些啥

HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
{
  HAL_StatusTypeDef status = HAL_OK;

  /* calculate DMA base and stream number */
  DMA_Base_Registers *regs = (DMA_Base_Registers *)hdma->StreamBaseAddress;
  
  /* Check the parameters */
  assert_param(IS_DMA_BUFFER_SIZE(DataLength));
 
  /* Process locked */
  __HAL_LOCK(hdma);
  
  if(HAL_DMA_STATE_READY == hdma->State)
  {
    /* Change DMA peripheral state */
    hdma->State = HAL_DMA_STATE_BUSY;
    
    /* Initialize the error code */
    hdma->ErrorCode = HAL_DMA_ERROR_NONE;
    
    /* Configure the source, destination address and the data length */
    DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);
    
    /* Clear all interrupt flags at correct offset within the register */
    regs->IFCR = 0x3FU << hdma->StreamIndex;
    
    /* Enable Common interrupts*/
    hdma->Instance->CR  |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME;		☆☆
    																☆	
    if(hdma->XferHalfCpltCallback != NULL)    						☆
    {										  						☆
      hdma->Instance->CR  |= DMA_IT_HT;		  						☆
    }										  						☆☆
    
    /* Enable the Peripheral */
    __HAL_DMA_ENABLE(hdma);
  }
  else
  {
    /* Process unlocked */
    __HAL_UNLOCK(hdma);	  
    
    /* Return error status */
    status = HAL_BUSY;
  }
  
  return status;
}

首先上来又先是一套状态维护服务安排上。然后通过DMA_SetConfig函数,将source, destination address and the data length写入寄存器,直到个函数里面,才真真正正的将HAL_UART_Receive_DMA函数中的参数写入相对应的寄存器中,可见HAL的封装真是一层一层的呀,层数真不少!

我们最重要要看的是上面我标注五角星的语句。

hdma->Instance->CR |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME;

这句话中的DMA_IT_TC 、DMA_IT_TE 和DMA_IT_DME都是宏定义:

#define DMA_IT_TC                     ((uint32_t)DMA_SxCR_TCIE)			/*!< 0x00000010 */
#define DMA_IT_TE                     ((uint32_t)DMA_SxCR_TEIE)			/*!< 0x00000008 */
#define DMA_IT_DME                    ((uint32_t)DMA_SxCR_DMEIE)		/*!< 0x00000002 */

可以看到把CR同时赋值了3个标志位:TCIE、TEIE、DMEIE。然后我们翻看F4的参考手册,看看他们都是干啥的。

可以看到,都是中断使能。这个小兔崽子,给打开了一堆的中断。而且还在之后打开了半传输完成中断

if(hdma->XferHalfCpltCallback != NULL)
{
hdma->Instance->CR |= DMA_IT_HT;
}

这里面的hdma->XferHalfCpltCallback 是不是有点眼熟?没错,就是在UART_Start_Receive_DMA函数中写入了UART_DMARxHalfCplt的。所以这个if语句中的指令是会被执行的。也就是说,我们的半传输完成中断会被开启。

把一切都设置好了以后,开启了DMA外设

__HAL_DMA_ENABLE(hdma);

总结起来,这个HAL_UART_Receive_DMA函数干了这些事:

  1. 把串口到内存的DMA传输给打开了
  2. 打开了DMA的传输完成中断,和DMA数据“半传输完成中断”。这个半就是我们填入的size参数的一半。

等等,有点蒙了。再回一下例程中的操作:

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

我们不是当初是用这个函数打开中断的么,而且还清清楚楚地知道是打开了空闲中断,而且之后的数据解析操作都是在这个串口的空闲中断里进行了呀。咋又给打开了个“完成中断”和“半传输完成中断”这是要干嘛,我到底该在哪个中断里进行操作?

当然还是在空闲中断里进行操作啦。要注意分清,串口中断是串口上的,DMA中断时DMA上的,这两个人是没有关系的。要在观念中去把这两个东西给分离开来。

串口的空闲中断是在串口接收数据的时候会根据接收的数据帧的“结束标识”后面跟不跟“起始标识”而选择进入的,如果说我们发的数据帧在一段时间内(这个时间是很短的,但是很精准的,不用担心)结束标识后面没有再跟着起始标识,那么就判断这一次的数据发送完毕了,进入空闲中断。而DMA的中断,是在DMA外设中设置的,DMA接收到从UART外设来的数据后进行传输,传输到原来设定的值的一半的时候会进一次半传输完成中断,传输完之后会进一次传输完成中断。

我们大概也可以理解为什么我们在cubemx里选择对应外设的DMA的时候cubemx会自动给我们把DMA的中断给我们打开的原因了。

在上面的程序中调用了DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);这个函数才是真真正正地在设置DMA的寄存器

static void DMA_SetConfig(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)
{
  /* Clear DBM bit */
  hdma->Instance->CR &= (uint32_t)(~DMA_SxCR_DBM);

  /* Configure DMA Stream data length */
  hdma->Instance->NDTR = DataLength;

  /* Memory to Peripheral */
  if((hdma->Init.Direction) == DMA_MEMORY_TO_PERIPH)
  {
    /* Configure DMA Stream destination address */
    hdma->Instance->PAR = DstAddress;

    /* Configure DMA Stream source address */
    hdma->Instance->M0AR = SrcAddress;
  }
  /* Peripheral to Memory */
  else
  {
    /* Configure DMA Stream source address */
    hdma->Instance->PAR = SrcAddress;

    /* Configure DMA Stream destination address */
    hdma->Instance->M0AR = DstAddress;
  }
}

可以看到这里的寄存器操作,和我们直接操作寄存器的那一套几乎上是一模一样了,只不过是考虑到了更多的场景。

2. 信息的解析

例程

void USART1_IRQHandler(void)
{
    if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到数据
    {
        __HAL_UART_CLEAR_PEFLAG(&huart1);
    }
    else if(USART1->SR & UART_FLAG_IDLE)
    {
        static uint16_t this_time_rx_len = 0;

        __HAL_UART_CLEAR_PEFLAG(&huart1);

        if ((hdma_usart1_rx.Instance->CR & DMA_SxCR_CT) == RESET)
        {
            /* Current memory buffer used is Memory 0 */

            //disable DMA
            //失效DMA
            __HAL_DMA_DISABLE(&hdma_usart1_rx);

            //get receive data length, length = set_data_length - remain_length
            //获取接收数据长度,长度 = 设定长度 - 剩余长度
            this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;

            //reset set_data_lenght
            //重新设定数据长度
            hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;

            //set memory buffer 1
            //设定缓冲区1
            hdma_usart1_rx.Instance->CR |= DMA_SxCR_CT;

            //enable DMA
            //使能DMA
            __HAL_DMA_ENABLE(&hdma_usart1_rx);

            if(this_time_rx_len == RC_FRAME_LENGTH)
            {
                sbus_to_rc(sbus_rx_buf[0], &rc_ctrl);
            }
        }
        else
        {
            /* Current memory buffer used is Memory 1 */
            //disable DMA
            //失效DMA
            __HAL_DMA_DISABLE(&hdma_usart1_rx);

            //get receive data length, length = set_data_length - remain_length
            //获取接收数据长度,长度 = 设定长度 - 剩余长度
            this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;

            //reset set_data_lenght
            //重新设定数据长度
            hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;

            //set memory buffer 0
            //设定缓冲区0
            DMA2_Stream2->CR &= ~(DMA_SxCR_CT);

            //enable DMA
            //使能DMA
            __HAL_DMA_ENABLE(&hdma_usart1_rx);

            if(this_time_rx_len == RC_FRAME_LENGTH)
            {

                //处理遥控器数据
                sbus_to_rc(sbus_rx_buf[1], &rc_ctrl);
            }
        }
    }
}

RXNE与RXNEIE

​ 在分析这个例程的代码时上来就让我迷惑了,咋上来就是一个RXNE标志??咋没见过这个玩意儿。去查看寄存器

当串口收到数据之后该位会被置1,并且如果RXNEIE这个时候也被置1时,就会进入中断。于是便产生了一个疑问:一有数据进来就会被置位?还是等数据足够了再置位?经过百度,知道了,每接收一个字节就会被置位,也就是说如果这个时候RXNEIE如果被使能,那么串口将每接收一个字节的数据就会进一次中断。是不是有点熟悉?HAL_UART_Recesive_IT这家伙不就是干这个事的么?找一下

HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
huart->pRxBuffPtr = pData;
huart->RxXferSize = Size;
huart->RxXferCount = Size;

huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;

/* Process Unlocked */
__HAL_UNLOCK(huart);

/* Enable the UART Parity Error Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_PE);

/* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
__HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

/* Enable the UART Data Register not empty Interrupt */
__HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);       ☆☆☆☆☆☆

return HAL_OK;
}

可以看到HAL_UART_Recesive_IT函数实际上就是调用了__HAL_UART_ENABLE_IT,然后使能了RXNE中断,那这个UART_IT_RXNE实际上是个啥?

define UART_IT_RXNE ((uint32_t)(UART_CR1_REG_INDEX <<28U | USART_CR1_RXNEIE))

#define USART_CR1_RXNEIE_Msk          (0x1UL << USART_CR1_RXNEIE_Pos)           /*!< 0x00000020 */
#define USART_CR1_RXNEIE              USART_CR1_RXNEIE_Msk                     /*!<RXNE Interrupt Enable     

喏,RXNEIE出来了。也就是说HAL_UART_Recesive_IT这个函数本质上就是在RXNEIE位置了1,使能了接收完成中断

回到上面的程序的分析

  if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到数据
    {
        __HAL_UART_CLEAR_PEFLAG(&huart1);
    }

如果进入中断,并且读取数据寄存器非空,那么就清除PE这个标志。PE是个什么标志?

奇偶校验错误??? 也就是说先判断一手RXNE是为了清除奇偶校验错误标志位,主体还是为了避免在传输的时候出现信号干扰,出现校验错误时如果不及时清除校验错误标志位,那么会一直进中断,而进不去主程序。 但是这里有一个前提昂,就是PEIE要被使能才能因为该错误进入中断。

在信息接收那一节中的RC_Init函数中,是直接操作寄存器开启IDLE中断的,因此不管有没有校验错误,都不会以内校验错误进入中断,因此这个判断函数在直接赋值寄存器的方法开启中断时是不起作用的。那么为什么要加一个这样的判断呢?因为我们大多数都不是直接赋值寄存器开启IDLE中断的而是调用HAL_UART_Recesive_IT、HAL_UART_Recesive_DMA来开启中断的**(HAL_UART_Recesive_DMA会主动调用HAL_UART_Recesive_IT这个函数,给你把接收中断打开**),这些函数因为是HAL给封装的,所以很“规矩”,它会主动地在你开启IT、DMA的时候给你把错误中断也打开。

如果你用HAL库函数打开中断,就需要在IRQHandle中进行相应的标志位的处理,但是HAL库在中断中调用下面这个函数把这些工作都给你做了

HAL_UART_IRQHandler(&huart1);

串口的DMA请求机制

串口通信的时候是一个字节一个字节的传输的,因为设置的数据位是8嘛。每一帧数据除了八个数据位还有一些校验位之类的,还有两个很重要的地方,就是“起始位”和“停止位”。很多地方可以用到这两个位来进行一些判断,例如,USART的空闲中断。串口怎么知道现在空闲了呢?因为他在检测到一个停止位之后如果没有检测到起始位,那么就会认为这一大串数据是一次发送的数据。还有一个地方的应用就在DMA。

思考一下,我们在Cubemx中设置的DMA传输大小是bit 、Half word 还是 Words是干嘛用的?是确定DMA转发阈值用的,也就是说,我DMA在传输的时候不是你来一个bit我就送走一个bit,跟入栈似的送到目标寄存器。而是等存够一定的数量的时候才会进行一次传输。 那么DMA怎么知道我是不是要传输了呢? 外设在需要DMA传输的时候会发起一个DMA请求,DMA在收到这个请求的时候就知道自己该送数据走了,于是便会进行一次传输(这次传输的大小自然就是之前设置好的bit、Half Word…)。 也就是说,DMA之所以能做到的一次传输传输固定大小的数据,是因为使用DMA的外设会在收到这个固定大小的数据之后对DMA发起一个请求,让DMA帮忙把这么长的数据给送到要去的地方。(感受一下就可以发现,我们所说的配置DMA,并不是配置DMA,而是配置外设,是配置外设什么时候呼叫DMA,这也就是为什么USART中的DMA设置部分叫做**“DMA Request Settings”) 那么外设是如何做到精准的每8bit、或者16bit、32bit请求一次DMA呢?答案是停止位**

因为每传输8bit的数据就会发送一个停止位,那么串口在使能了接收DMA之后.只要一接收到一个Bit就会产生一个**“DMA request”,DMA收到这个request之后就可以访问外设中的数据进行发送了。并且在访问数据的时候会给外设一个应答,这时候外设就知道有人来读取了,这也就是为啥DMA可以消除RXNE标志位了。(RXNE在接收到一字节**的数据之后会被置1,如果RXNEIE位已经被置1,也就是打开了接收中断后,RXNE一旦置1便进入中断)

发现错误

再往下走,判断USART1->SR & UART_FLAG_IDLE,如果IDLE标志位被置1,说明遥控器已经发完了一帧数据,进入这个中断的原因是IDLE中断(空闲中断)。然后上来又是一个奇偶校验错误位清空.

__HAL_UART_CLEAR_PEFLAG(&huart1);

奇偶校验错误难道就这么频繁?需要这么小心地去处理它么?更何况我们根本就没使能PEIE位,所以我感觉如果是用寄存器开启的串口+DMA接收,那么没有必要这样小心地去处理这个PEFLAG,但是我们看到它这么小心地在处理,可能在串口通讯的时候奇偶校验错误很容易出现的,而且加上我们很多时候用HAL_UART_Recesive_DMA这个函数来开启的,那么就很有可能被奇偶校验的错误卡在中断中出不来。但是根据自己的实际测试,如果是按照第一节寄存器的方法开启串口DMA的话,去掉那两个PEFLAG的清除也是可以使用的。

而且这里我强烈怀疑是代码写错了,应该是清除IDLE标志位,这样才是一个正常地有逻辑的流程

__HAL_UART_CLEAR_IDLEFLAG(&huart1);

传输数据长度计算

回到代码,如果是IDLE标志位被置1,也就是我们现在进入了空闲中断,遥控器已经发完了一帧数据,我们就要首先记录一下当前到底收到了多少字节,和我们预期的一帧数据长度是否一致,如果不一致,那么就说明这一帧数据是不准确的,我们就直接废弃这一帧数据了,这也就是后面的那个

if(this_time_rx_len == RC_FRAME_LENGTH)

的作用。

而我们计算数据长度的方法就是根据DMA的NDTR寄存器的特性,

image-20220312231118009

因为我们在设置串口的DMA Request的时候设置的是一个字节申请一次DMA传输,而我们进入USART的空闲中断是接收完一帧数据之后才进入的,遥控器的一帧数据长度是18字节

image-20220312231721384

因此,在我们进入空闲中断的时候,dma已经转移了18次数据了,也就是说DMA的NDTR寄存器已经有所改变了,准确来说是减少了很多次了,我们可以根据NDTR中还剩下的字节数来简介判断出它到底传输了多少次,也就是说我们从上一次空闲中断到这一次进入中断之间到底接收了多少个字节的数据,这个数就是我们真实接收到的一帧的字节数。因此就有了下面的这个计算公式

//获取接收数据长度,长度 = 设定长度 - 剩余长度
  this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;

当然这里面还有很重要的一个问题,在我们进入空闲中断后如果串口又接收到数据,并且传够了一个字节甚至多个字节的话,那么就会在我们在串口中处理数据的时候产生一个甚至多个DMA请求,那么我们的接收数据的计算不就不准了么?这个NDTR正在看的时候还在减少,这肯定不会准嘛,因此就需要在我们计算的时候,最好是一进入空闲中断就先把DMA给关了,不让它再传送数据,也就保证了NDTR保持的是我们进入空闲中断的时刻之前剩余的长度。这就是下面这句话的作用

//失效DMA
__HAL_DMA_DISABLE(&hdma_usart1_rx);

当我们获取完这次接收的数据长度以后,我们要重新给NDTR寄存器赋值,以便下次我们可以再次通过上面的公式进行长度的计算。

hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;

双缓冲区的使用

但是别忘了,我们是有两个缓冲区来接收数据的。为什么要用双缓冲区呢?我们知道,普通DMA的目标数据储存区域只有一个,也就是如果当数据存满后,新的数据又传输过来了,那么旧的数据会被新的数据覆盖。(这就是普通DMA的缺点)而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。

那么双缓冲区是怎么用的呢?

在网上搜到一篇帖子,给出了两种方法,原文链接如下

STM32的DMA双缓冲模式详解_zhang1079528541的博客-CSDN博客_dma双缓冲模式

两种方法的区别在于我们设置的缓冲区的大小。

第一种方法:我们可以设置两个18字节大小的缓冲区,也就是设置两个大小刚刚好可以sei下一帧数据的缓冲区,因为我们设置的是循环模式,因此当数据传输量为0时,DMA会自动去换到另一个缓冲区中并且将DMA的传输值给自动填充满.参考手册中是这么说的

image-20220312234750739

用这种方法的时候我们就不需要手动转换当前缓冲区了,我们只要关注我们收的是不是18字节,然后解析即可

第二种方法:我们将每一个缓冲区的大小改为比一帧数据长度大的值(比18大),这样可以在一帧数据传输完成后不会因Counter值变0导致DMA指向下一内存区域。DMA传输值不会自动填满,且内存区域还是指向当前缓冲区,然后我们将剩余数据量保存下来,再将DMA传输值填满,接着把DMA指向另一个缓冲区,最后通过判断剩余数据量来决定是否对数据进行处理。

用这种方法更加地保险,我们可以很安全、“悠闲”地获取到这一帧数据。我们这部分给出的代码就是用的第二种方法

其中

if ((hdma_usart1_rx.Instance->CR & DMA_SxCR_CT) == RESET)
{

​ …

​ //设定缓冲区1
​ hdma_usart1_rx.Instance->CR |= DMA_SxCR_CT;

}

else

{

​ …

​ //设定缓冲区0
​ DMA2_Stream2->CR &= ~(DMA_SxCR_CT);

}

进行的操作就是根据DMA CR寄存器的CT位的值来判断当前的缓冲区是谁,然后在处理过数据之后再将CT值设置为另一个缓冲区,让数据网另一个缓冲区中存。

image-20220312235842139

数据内容解析

最后重头戏来了,将收到的数据的内容给解析出来

sbus_to_rc(sbus_rx_buf[0], &rc_ctrl);

我们来看看这个sbus_to_rc函数是怎么解析数据的

void sbus_to_rc(volatile const uint8_t *sbus_buf, RC_ctrl_t *rc_ctrl)
{
    if (sbus_buf == NULL || rc_ctrl == NULL)
    {
        return;
    }

    rc_ctrl->rc.ch[0] = (sbus_buf[0] | (sbus_buf[1] << 8)) & 0x07ff;        //!< Channel 0
    rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1
    rc_ctrl->rc.ch[2] = ((sbus_buf[2] >> 6) | (sbus_buf[3] << 2) |          //!< Channel 2
                         (sbus_buf[4] << 10)) &0x07ff;
    rc_ctrl->rc.ch[3] = ((sbus_buf[4] >> 1) | (sbus_buf[5] << 7)) & 0x07ff; //!< Channel 3
    rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003);                  //!< Switch left
    rc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2;                       //!< Switch right
    rc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8);                    //!< Mouse X axis
    rc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8);                    //!< Mouse Y axis
    rc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8);                  //!< Mouse Z axis
    rc_ctrl->mouse.press_l = sbus_buf[12];                                  //!< Mouse Left Is Press ?
    rc_ctrl->mouse.press_r = sbus_buf[13];                                  //!< Mouse Right Is Press ?
    rc_ctrl->key.v = sbus_buf[14] | (sbus_buf[15] << 8);                    //!< KeyBoard value
    rc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8);                 //NULL

    rc_ctrl->rc.ch[0] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[1] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[2] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[3] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[4] -= RC_CH_VALUE_OFFSET;
}

这个函数有两个参数,其中sbus_buf就是我们要解析的缓冲区,也就是我们在当初初始化的时候设置好的两个缓冲区数组中的其中一个。

rc_ctrl是一个我们自己定义好的结构体对象。 RC_ctrl_t结构体是如是定义的

typedef  struct
{
     struct
    {
        int16_t ch[4];
        char s[2];
    }  __attribute__((__packed__)) rc;
     struct
    {
        int16_t x;
        int16_t y;
        int16_t z;
        uint8_t press_l;
        uint8_t press_r;
    }  __attribute__((__packed__))mouse;
     struct
    {
        uint16_t v;
    }  __attribute__((__packed__))key;

}  __attribute__((__packed__))RC_ctrl_t;

结构体中包含了4个通道(ch)用来存放上下左右拨杆的数据,和两个s数组用来存放左右上方的拨杆的数据

image-20220313000926746

image-20220313000944489

遥控器数据处理函数 sbus_to_rc 的功能是将通过 DMA 获取到的原始数据,按照遥控器的

数据协议拼接成完整的遥控器数据,以通道 0 的数据为例,从遥控器的用户手册中查到通道

0 的长度为 11bit,偏移为 0。

image-20220313001532094

这说明如果想要获取通道 0 的数据就需要将第一帧的 8bit 数据和第二帧数据的后三 bit 数据

拼接,如果想要获取通道 1 的数据就将第二帧数据的前 5bit 和第三帧数据的后 6bit 数据进

行拼接,不断通过拼接就可以获得所有数据帧,拼接过程的示意图如下:

image-20220313001547971

解码函数 sbus_to_rc 通过位运算的方式完成上述的数据拼接工作,十六进制数 0x07ff 的二

进制是 0b0000 0111 1111 1111,也就是 11 位的 1,和 0x07ff 进行与运算相当于截取出 11

位的数据。

通道 0 的数据获取:首先将数据帧 1 和左移 8 位的数据帧 2 进行或运算,拼接出 16 位的数

据,前 8 位为数据帧 2,后 8 位为数据帧 1,再将其和 0x07ff 相与,截取 11 位,就获得了

由数据帧 2 后 3 位和数据帧 1 拼接成的通道 0 数据。其过程示意图如下:

image-20220313001612766

我们看看代码是如何实现的

void sbus_to_rc(volatile const uint8_t *sbus_buf, RC_ctrl_t *rc_ctrl)
{
    if (sbus_buf == NULL || rc_ctrl == NULL)
    {
        return;
    }

    rc_ctrl->rc.ch[0] = (sbus_buf[0] | (sbus_buf[1] << 8)) & 0x07ff;        //!< Channel 0
    rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1
    rc_ctrl->rc.ch[2] = ((sbus_buf[2] >> 6) | (sbus_buf[3] << 2) |          //!< Channel 2
                         (sbus_buf[4] << 10)) &0x07ff;
    rc_ctrl->rc.ch[3] = ((sbus_buf[4] >> 1) | (sbus_buf[5] << 7)) & 0x07ff; //!< Channel 3
    rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003);                  //!< Switch left
    rc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2;                       //!< Switch right
    rc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8);                    //!< Mouse X axis
    rc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8);                    //!< Mouse Y axis
    rc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8);                  //!< Mouse Z axis
    rc_ctrl->mouse.press_l = sbus_buf[12];                                  //!< Mouse Left Is Press ?
    rc_ctrl->mouse.press_r = sbus_buf[13];                                  //!< Mouse Right Is Press ?
    rc_ctrl->key.v = sbus_buf[14] | (sbus_buf[15] << 8);                    //!< KeyBoard value
    rc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8);                 //NULL

    rc_ctrl->rc.ch[0] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[1] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[2] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[3] -= RC_CH_VALUE_OFFSET;
    rc_ctrl->rc.ch[4] -= RC_CH_VALUE_OFFSET;
}

可以看到这里面主要就是一些位的移动与组合,然后&07ff来取出11位赋值给对应的通道,这个通道的大小是16bit,所以是足够放的。但是有一个问题,在第8行,

sbus_buf[1] << 8

这个sbus_buf[1]的大小是8个bit,我们学c语言的时候说了,左移以后右边补0,左边的数移出去以后就没了。那这里sbus_buf[1]一共有8位,在左移8位那不一定是0么。

怀着这个疑惑,打开了clion进行debug,看看他到底是个啥值。结果如下:

image-20220313172814681

我们可以看到,虽然这个sbus_buf[1]规定的是一个8bit的大小,但是我们可以通过左移符号给它硬生生地把大小扩展成最大32bit而且数据不丢失。这个问题的根本在于:stm32的寄存器是32bit的

3. 总结

通过寄存器来分析HAL库是一件很枯燥的事情,但是这里面还是藏着许多的欣喜。当你把每一个你经常用起来习以为常的函数点开,点到最深层的寄存器层面的时候,你会发现之前你并不了解他,当然你还会发现,你再用它的时候会有更多的勇气更加地信手拈来。

在网上有人是这样说的:高手编程都是初始化借用HAL的函数,其它的直接操作寄存器。 但是可不要认为这样很装x,学长是这样说的:HAL库中一进去就是对外设状态的维护,如果说不用HAL库,那么这个状态指不定在哪就断了没人维护了,状态也就乱了。所以说能用HAL库就用HAL库,那为什么有时候需要用寄存器呢,因为有的场景HAL库没有帮你考虑到。我表示很认同。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

RM遥控器接收程序的分析 的相关文章

  • C++中的双冒号::

    在C 43 43 中 xff0c 双冒号 xff08 xff09 被用作作用域解析运算符 类作用域解析运算符 在C 43 43 中 xff0c 如果要在类的定义外部定义或实现成员函数或静态成员变量 xff0c 则必须使用双冒号运算符来引用类
  • c++中::和.区别

    在C 43 43 中 xff0c 34 34 和 34 34 都是用于访问类的成员 xff08 包括成员函数和成员变量 xff09 的运算符 xff0c 但它们有一些重要的区别 34 34 被称为作用域解析运算符 xff0c 用于访问全局作
  • Widget::Widget(QWidget *parent)为什么要传入父级指针?

    Widget Widget QWidget parent QWidget parent 列表初始化语法来初始化基类QWidget的构造函数 ui new Ui Widget 初始化指向Ui Widget类对象的指针ui 在类的声明里定义Ui
  • 拷贝构造函数为什么要传入引用

    拷贝构造函数是用于创建一个新对象并将其初始化为另一个对象的副本的特殊构造函数 在拷贝构造函数中 xff0c 如果我们将参数作为值传递 xff0c 那么会发生对象复制 xff0c 这将导致无限递归的调用 xff0c 因为拷贝构造函数的调用需要
  • 使用vscode开发apicloud

    1 第一步 xff1a VScode安装apicloud插件 xff1b 2 第二步 xff1a 配置apicloud插件里面工作区的Apicloud Subdirectories路径 路径是 xff1a VUE目录 src 3 第三步 x
  • 构造函数的调用规则

    在面向对象编程中 xff0c 构造函数是用于创建对象的特殊函数 构造函数的调用规则如下 xff1a 当创建一个对象时 xff0c 会自动调用该对象的构造函数 构造函数的名称必须与类的名称相同 构造函数可以有多个重载版本 xff0c 但是必须
  • 深拷贝与浅拷贝

    深拷贝和浅拷贝是指在计算机编程中 xff0c 当需要复制一个对象时 xff0c 复制出来的副本与原对象之间的关系的不同 浅拷贝是指在复制对象时 xff0c 只复制了对象本身的值 xff0c 而没有复制对象包含的子对象 也就是说 xff0c
  • C++三种继承方式的区别

    访问方式分为两种 xff0c 一种是类内访问 xff0c 还有一种是类外访问 xff1b 所谓类内访问 xff0c 就是类内的函数中是使用了属性 xff1b 类外访问 xff0c 就是新建一个实例对象 xff0c 并访问这个对象的属性 xf
  • python爬虫之数据解析(BeautifulSoup)

    BeautifulSoup也是python爬虫常用的一种数据解析方法 xff0c 主要就两步 1 实例化一个Beautifulsoup对象 xff0c 平且将页面源码数据加载到该对象中 2 通过调用Beautifulsoup对象中相关的属性
  • 蓝桥杯嵌入式第十四届省赛题目解析

    前几天刚刚参加完第十四届的省赛 xff0c 这届题目比我想象中的要难 xff0c 其实想一想这也是应该的 xff0c 以前的知识点都被摸透了 xff0c 也是需要加入新的知识点了 xff0c 但是我还是想说能不能别在我参加的时候加大题目难度
  • dockerfile构建

    2 简答题 编写Dockerfile制作镜像 xff0c 生成镜像名为my build Nginx2 首先创建目录dockerfile nginx2 xff0c 保存Dockerfile文件 具体要求如下 xff1a 1 基于镜像cento
  • 安装虚拟机之后怎么配置虚拟环境、深度学习、深度强化学习环境安装

    安装步骤目录 一 配置虚拟机VMware安装包 amp Ubuntu的光盘映像文件 xff1a VMware安装Ubuntu安装 二 进入虚拟机配置环境深度 xff08 强化 xff09 学习环境的配置1 得知系统所自带python版本 x
  • 力扣-刷题方法总结(测试文章)

    知乎方面收集到的资料 xff08 非原创 xff0c 题主只是对其进行统一的整理 xff0c 方便后续查看 xff09 算法训练讲究循序渐进 xff1a 1 先从简单开始 xff0c 然后过度到中等 xff0c 再过渡到困难的进程 2 如何
  • 文件分隔符 ‘/‘(斜杠) 和 ‘\‘(反斜杠) 的使用

    前言 在学习时 xff0c 总会用到 Windows 和 Linux xff0c 输入路径时 xff0c 文件路径分隔符有时用 xff08 斜杠 xff09 xff0c 有时用 xff08 反斜杠 xff09 xff0c 属实不好区分 xf
  • VMware虚拟机安装Win11教程(解决常见报错)

    前言 今天闲来无事 xff0c 就想着装一下最新版的win11玩一下 xff0c 然后来来去去还是折腾了一些时间 xff0c 有遇到一些错误不过最好都找到了解决办法 xff0c 下面我就分享一下VMware虚拟机安装win11的详细步骤 V
  • vue打包后neditor不显示了

    原因是vue和vue template compiler 1 两者的版本不一致 xff1b 2 两者的版本低了 xff1b 例如 xff1a 我出问题的版本是 34 vue 34 34 2 5 10 34 34 vue template c
  • 【Docker常用命令】

    Docker常用命令 xff08 学习笔记 xff09 一 Docker基础命令二 Docker镜像命令三 Docker容器命令3 1 运行容器3 2 退出容器3 3 查看容器进程 xff0c 日志3 4 再次进入容器3 5 容器启停3 6
  • OpenCV学习——ArUco模块

    前提介绍 xff1a ArUco模块是OpenCV的contrib拓展库中一个模块 xff0c 需要安装OpenCV的 contrib拓展库 才能正常使用 ArUco标记 xff1a ArUco 标记是由 宽黑色边框 和 确定其标识符 xf
  • 【Vue】报错:Avoid mutating a prop directly since the value will be overwritten whenever the parent

    当我们直接改变父组件的 props 值是会报以下警告 xff1a Vue warn Avoid mutating a prop directly since the value will be overwritten whenever th

随机推荐

  • 深蓝学院-机器人运动规划学习笔记-第一章

    第一课 移动机器人运动规划 Motion planning for mobile robots IntroductionCourse outlineTypical planning methods overviewMap represent
  • opencv+python实战日记 入门篇(八)色块识别

    色块识别 import cv2 import numpy as np frameWidth 61 640 frameHeight 61 480 cap 61 cv2 VideoCapture 0 获取摄像头 cap set 3 640 ca
  • highway_env中自定义环境

    前言 highway env中集成了很多强化学习或者控制算法测试的驾驶环境 xff0c 但很多时候我们需要依据需求对环境进行自定义 xff0c 这里给出了自定义环境的一些步骤 xff0c 主要是基于gym 61 61 0 26版本 创建步骤
  • 深度相机和激光雷达的融合标定(Autoware)

    深度相机和激光雷达是智能汽车上常用的传感器 但深度相机具有特征难以提取 xff0c 容易受到视角影响 激光雷达存在数据不够直观且容易被吸收 xff0c 从而丢失信息 因此在自动驾驶领域 xff0c 需要对于不同传感器做数据的融合和传感器的标
  • 基于OpenCv和ROS的图像灰度化处理

    直接调用opencv灰度化函数 xff0c 对于本地图像进行处理 实现C 43 43 代码如下 xff1a 图像灰度化 include lt iostream gt cv cvtColor头文件 include lt opencv2 img
  • IMU的轨迹解算和航迹显示

    基于ros操作系统 xff0c 调用IMU数据包 xff0c 利用数据解算小车运动的轨迹 xff0c 并在rviz中实现轨迹的可视化 其中IMU四元数对于位移速度和加速度的转换 轨迹解算和换机显示的代码 xff1a IMU航迹推算 incl
  • 对IMU数据进行卡尔曼滤波

    我们要使用IMU数据 xff0c 必须对数据进行预处理 xff0c 卡尔曼滤波就是很好的方式 1 卡尔曼滤波 卡尔曼滤波 xff08 Kalman filtering xff09 是一种利用线性系统状态方程 xff0c 通过系统输入输出观测
  • PHPExcel导出导入问题”continue” targeting switch is equivalent to “break”.Did you mean to use “continue 2”?

    在 php 7 3 的 switch 中使用 continue 会出现警告 1 2 3 最好的方式是把 PHPExcel Shared OLE php 文件中的 continue 改为 continue 2 或 break 亲测 xff0c
  • 强化学习highway_env代码解读

    写在前面 作为强化学习的新手 xff0c 写这个系列的博客主要是为了记录学习过程 xff0c 同时也与大家分享自己的所见所想 前段时间发布了人生第一篇博客 xff0c 是关于highway env的自定义环境 但博客主要是关于如何创建一个自
  • Highway_env(Intersection)修改离散动作空间

    前言 在十字路口环境中 xff0c 主车默认的动作空间是以5m s变化的加减速以及保持原速三个动作 有时候为了学习更优化的策略 xff0c 同时与自己设置的奖励函数吻合 xff0c 需要修改环境的动作空间 这里我们主要添加两个较小加速度的纵
  • 离散动作的修改(基于highway_env的Intersection环境)

    之前写的一篇博客将离散和连续的动作空间都修改了 xff0c 这里做一下更正 基于十字路口的环境 xff0c 为了添加舒适性评判指标 xff0c 需要增加动作空间 xff0c 主要添加两个不同加速度值的离散动作 需要修改以下几个地方 xff1
  • VM 导入.ova/.ovf,未通过 OVF 规范一致性或虚拟硬件合规性检查.

    今天在用虚拟机VM导入ubuntu riscv ova文件新建Ubuntu时报错 xff1a 未通过OVF规范一致性或虚拟硬件合规性检查 网上查了一下 xff0c 了解到这是因为VM内置的Ofvtool工具的版本较低导致的 xff0c 解决
  • 借助FileZilla实现Ubuntu和 Windows之间文件互传

    借助FileZilla实现Windows和 Ubuntu间文件互传 xff0c 需要使用 FTP服务 xff0c 设置方法如下 xff1a 1 Windows下安装FTP客户端 FileZilla xff08 该软件免费 xff0c 可以直
  • 使用Ubuntu系统中的gparted工具对Ubuntu磁盘扩充

    最近在使用Ubuntu时 xff0c 发现经常提示内存空间不足 就总结了扩充Ubuntu内存的主要流程 xff0c 操作步骤如下 xff1a 第一步 xff1a 在虚拟机操作界面 xff08 关闭要进行磁盘扩充的Ubuntu xff09 进
  • 通过挂载的方式,解决由于权限无法将rootfs直接拷贝到SD卡的EXT4分区的问题

    最近在使用SD卡制作Linux启动文件时 xff0c 要将自己制作的根文件系统 xff08 rootfs xff09 拷到SD卡的EXT4分区时 xff0c 发现由于权限问题无法直接拷贝 xff0c 现通过挂载的方式解决该问题 xff0c
  • RISC-V架构下,Busybox工具的安装

    今天在RISC V架构下安装Busybox工具箱时 xff0c 找了很多的资料 xff0c 但都是ARM架构下的安装教程 xff0c 尽管内核不同但有一定的参考价值 xff0c 安装完成后对RISC V下Busybox工具箱的安装过程做出了
  • 串行通信协议小结(Serial Protocols)(1)

    通信关键点 同步通信 xff08 例如SPI xff09 双方之间的数据传输使用公共时钟信号进行同步 xff0c 数据以稳定的流传输 不需要额外的比特来标记传输的每个数据块的开始 结束 xff0c 因此速度更快 异步通信 xff08 例如U
  • MATLAB课程笔记(二)——MATLAB基础知识

    MATLAB系统环境 MATLAB操作界面的组成 采用与office 2010相同风格的操作界面 gt gt 命令提示符表示MATLAB处于准备状态 xff1a 续行符 MATLAB的搜索路径 gt gt clear 清除工作区的全部变量
  • SVN的日常使用

    1 已经加入ignore的文件夹 xff0c 解除方法 xff1a 直接到被ignore的位置 xff0c 执行 xff1a svn add lt 你被ignore的文件名 gt no ignore no ignore是取消忽略 如果是ad
  • RM遥控器接收程序的分析

    由遥控器接收分析串口与DMA RM的遥控器在使用的过程中在大体上可以分成两个部分 xff1a 信息的接收 与 信息的解析 xff0c 在信息的接收中主要用到了串口的空闲中断和DMA双缓冲区接收在本篇的信息接收部分主要根据RM官方给出的代码来