DBUS

根据大疆官方的例程说明,大疆官方的遥控器通过DBUS协议与STM32单片机进行通讯。

DBUS 通讯协议和串口类似,DBUS 的传输速率为 100k bit/s,数据长度为 8 位,奇偶校验位为偶校验,结束位 1 位。需要注意的是 DBUS 使用的电平标准和串口是相反的,在 DBUS 协议中高电平表示 0,低电平表示 1,如 果使用串口进行接收需要在接收电路上添加一个反相器。-------《RoboMaster开发板C型嵌入式软件教程文档》

然而在我们所使用的 达妙 H723开发板上,我们如此配置的话是无法收到数据的。根据达妙官方的DBUS例程,我们发现要配置数据长度为9位,结束位2位才能收到数据(达妙开发板原理图标注的是SBUS接口,难道是SBUS和DBUS区别在这里吗?不太懂这个

使用开发板的UART5作为遥控器接收,因为专门预留了和遥控器接收机通信的接口。

CubeMX配置

首先记得关闭H7 的CPU DCache,否则DMA是无法接收到数据的

UART5串口配置如下图:

图片

这是达妙官方的DBUS例程,在此我们留意到UART5的发送引脚设置在了 PB13 ,该引脚后续会设置为IMU的SPI_SCK,会冲突,我们要手动指定一个不影响其他功能的引脚 PC12 。并把接收引脚 UART5_RX 设置到 PD2 引脚,其余的 Basic Parameters 和例程设置一样即可,

波特率设置为 100 000, 

字长设置为 9 位,

校验选择偶校验,

停止位选择2

图片

配置好基础设置之后我们开启UART5的全局中断和接收DMA:

图片

在DMA设置这里,例程选择的模式是 Circular,这样的话DMA会一直循环搬运串口数据,然后在代码里他自己手动实现了一个 不定长接收 ,我们后来发现可以用函数代替这一接收方式,之后会在文末提及。

图片

代码部分

这里我们先按达妙官方的例程来进行串口接收的介绍,关于遥控器数据解析部分,我们使用大疆官方例程的解析函数。

代码中他是在main函数中通过执行自己定义的函数 usart_init()来开启串口的不定长接收。

图片

达妙例程使用的是 中断结合 DMA 的方式来实现 UART 数据接收,具体逻辑如下:

初始化部分

​/**
  * @brief    空闲中断初始化函数
  * @param    huart:UART句柄指针
  * @retval    none
  */
void uart_receive_init(UART_HandleTypeDef *huart)
{
  if (huart == &huart5)
  {
    /* 清除空闲中断标志位 */
    __HAL_UART_CLEAR_IDLEFLAG(&huart5);
    /* 开启串口空闲中断 */
    __HAL_UART_ENABLE_IT(&huart5, UART_IT_IDLE);
    /* 开启DMA接收 指定接收长度和数据地址 */
    uart_receive_dma_no_it(&huart5, usart5_buf, USART5_MAX_LEN);
  }
}
/**
  * @brief      配置使能DMA接收(而不是中断接收)
  * @param[in]  huart: UART句柄指针
  * @param[in]  pData: receive buff
  * @param[in]  Size:  buff size
  * @retval     set success or fail
  */
static int uart_receive_dma_no_it(UART_HandleTypeDef *huart, uint8_t *pData, uint32_t Size)
{
  uint32_t tmp = 0;
  tmp = huart->RxState;

  /* 判断串口是否已经初始化完成 */
  if (tmp == HAL_UART_STATE_READY)
  {
    /* 检测用户输入的数据是否正确 */
    if ((pData == NULL) || (Size == 0))
      return HAL_ERROR;

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

    /* 使能DMA通道 */
    HAL_DMA_Start(huart->hdmarx, (uint32_t)&huart->Instance->RDR, (uint32_t)pData, Size);

    /* 开启DMA传输 将UART CR3 寄存器中的 DMAR位 置高 */
    SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);

    return HAL_OK;
  }
  else
    return HAL_BUSY;
}

​
  • 函数 uart_receive_init 初始化了 UART5 的接收:

    •  清除了 IDLE 中断标志 (__HAL_UART_CLEAR_IDLEFLAG)。

    •  启用了 UART 的 IDLE 中断 (__HAL_UART_ENABLE_IT)。

    • 通过 uart_receive_dma_no_it 启动了 DMA 接收,将数据存储到缓冲区 usart5_buf,接收的最大长度为 USART5_MAX_LEN

接收处理

​
/**
  * @brief    当串口发生中断的时候进此函数
  * @param    huart: UART句柄指针
  * @retval    在stm32f4xx_it.c中添加
  */
void uart_receive_handler(UART_HandleTypeDef *huart)
{
  /* __HAL_UART_GET_FLAG    检查指定的UART空闲标志位是否触发 */
  /* __HAL_UART_GET_IT_SOURCEG    检查指定的UART空闲中断是否触发 */
  if (__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) &&
      __HAL_UART_GET_IT_SOURCE(huart, UART_IT_IDLE))
  {
    /* 清除空闲中断标志位 */
    __HAL_UART_CLEAR_IDLEFLAG(huart);

    /* 关掉DMA */
    __HAL_DMA_DISABLE(huart->hdmarx);

    /* 进入空闲中断处理函数 */
    uart_rx_idle_callback(huart);

    /* 重启DMA传输 */
    __HAL_DMA_ENABLE(huart->hdmarx);
  }
}
/**
  * @brief    在接收到一帧数据之后空闲一帧数据时间之后无数据
    *                    再来则进入此回调函数,此函数会清除空闲中断标志位
  * @param    huart: UART句柄指针
  * @retval
  */
static void uart_rx_idle_callback(UART_HandleTypeDef *huart)
{
  if (huart == &huart5)
  {
    //判断数据是否为期望的长度 如不是则不进入回调函数 直接开启下一次接收
    if ((USART5_MAX_LEN - dma_current_data_counter(huart->hdmarx->Instance)) == USART5_BUFLEN)
    {
      /* 进入空闲中断回调函数 */
      usart5_callback_handler(usart5_buf);
    }

    /* 设置DMA接收数据的长度 */
    __HAL_DMA_SET_COUNTER(huart->hdmarx, USART5_MAX_LEN);
  }

}
/**
  * @brief      返回当前DMA通道中剩余的数据个数
  * @param[in]  dma_stream: DMA通道
  * @retval     DMA通道中剩余的数据个数
  */
uint16_t dma_current_data_counter(DMA_Stream_TypeDef *dma_stream)
{
  return ((uint16_t)(dma_stream->NDTR));
}
/******** 串口空闲中断处理函数 ********/
void usart5_callback_handler(uint8_t *buff)
{
    //遥控器数据解析部分
}

​
  •  当 UART 接收到数据后,如果检测到 IDLE 事件(即一段时间内没有数据传输),会触发 IDLE 中断。

  • 在 uart_receive_handler 中:

    • 检查 IDLE 标志并清除它。

    • 禁用 DMA (__HAL_DMA_DISABLE),调用 uart_rx_idle_callback 处理数据,然后重新启用 DMA (__HAL_DMA_ENABLE)。

  •  在 uart_rx_idle_callback 中:

    • 通过 dma_current_data_counter 计算接收到的数据长度。

    •  如果接收到的数据长度等于 USART5_BUFLEN,调用 usart5_callback_handler 处理数据(例如解析 SBUS 帧)。

    • 重置 DMA 的计数器 (__HAL_DMA_SET_COUNTER),准备下一次接收。

通过以上代码,我们使用单缓冲模式,仅有一个缓冲区 usart5_buf,来接收遥控器数据,并通过使用 HAL 库的回调函数 HAL_UART_RxCpltCallback,仅处理 IDLE 中断。

HAL库自带

上文的代码可以用函数 HAL_UARTEx_ReceiveToIdle_DMA()代替,不过我们似乎遇到了一些问题。

我们可以看一下该函数的定义

​
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  HAL_StatusTypeDef status;

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

    /* Set Reception type to reception till IDLE Event*/
    huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;
    huart->RxEventType = HAL_UART_RXEVENT_TC;

    status =  UART_Start_Receive_DMA(huart, pData, Size);

    /* Check Rx process has been successfully started */
    if (status == HAL_OK)
    {
      if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
      {
        __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF);
        ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
      }
      else
      {
        /* In case of errors already pending when reception is started,
           Interrupts may have already been raised and lead to reception abortion.
           (Overrun error for instance).
           In such case Reception Type has been reset to HAL_UART_RECEPTION_STANDARD. */
        status = HAL_ERROR;
      }
    }

    return status;
  }
  else
  {
    return HAL_BUSY;
  }
}

​

函数工作原理

这个函数的核心功能是通过 DMA 接收 UART 数据,并在检测到线路空闲(IDLE)事件时停止接收。以下是其具体步骤:

1. 检查接收状态
if (huart->RxState == HAL_UART_STATE_READY)
  •  函数首先检查 UART 的接收状态是否为 HAL_UART_STATE_READY

  •  如果是,说明当前没有正在进行的接收操作,可以继续执行。

  •  如果不是(例如 UART 正在接收数据),函数返回 HAL_BUSY,表示 UART 忙碌,无法启动新的接收操作。

2. 参数校验
if ((pData == NULL) || (Size == 0U)) {   return HAL_ERROR; }
  •  检查传入的缓冲区指针 pData 是否为空(NULL),或者缓冲区大小 Size 是否为 0。

  •  如果任一条件成立,函数返回 HAL_ERROR,因为没有有效的缓冲区或大小来存储接收数据。

3. 设置接收类型
huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE; huart->RxEventType = HAL_UART_RXEVENT_TC;
  •  将 ReceptionType 设置为 HAL_UART_RECEPTION_TOIDLE,表示接收模式为“接收直到 IDLE 事件”。

  •  将 RxEventType 设置为 HAL_UART_RXEVENT_TC,表示传输完成(Transfer Complete),为后续的事件处理做准备。

4. 启动 DMA 接收
status = UART_Start_Receive_DMA(huart, pData, Size);
  •  调用内部函数 UART_Start_Receive_DMA 来启动 DMA 接收进程。

  •  该函数会配置 DMA 将 UART 接收到的数据传输到 pData 指向的缓冲区中,传输的最大字节数由 Size 指定。

  •  返回的状态码存储在 status 变量中,用于后续检查。

5. 检查接收是否成功启动
if (status == HAL_OK) 
{   if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)   
    {     __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF);     
            ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);   
    }   
    else   
    {     
        status = HAL_ERROR;   
    } 
}
  •  成功情况

    •  如果 UART_Start_Receive_DMA 返回 HAL_OK,说明 DMA 接收已成功启动。

    •  进一步检查 ReceptionType 是否仍为 HAL_UART_RECEPTION_TOIDLE

      •  如果是,执行以下操作:

        •  __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF):清除 UART 的 IDLE 标志位,以确保之前的空闲状态不会干扰当前操作。

        •  ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE):启用 IDLE 中断,当 UART 检测到线路空闲时会触发中断。

  • • 失败情况

    •  如果 ReceptionType 不再是 HAL_UART_RECEPTION_TOIDLE,说明在启动 DMA 接收时发生了错误(例如溢出错误),导致接收类型被重置为标准模式。

    •  在这种情况下,函数将 status 设置为 HAL_ERROR

6. 返回状态
return status;
  • 函数最后返回 status,表示整个操作的结果。

使用方式

我们直接使用HAL_UARTEx_ReceiveToIdle_DMA()代替原先的所有函数即可,之后在回调函数HAL_UARTEx_RxEventCallback()中启用遥控器数据解析,并再次启动串口空闲接收。

哦对,记得把DMA设置那里的Circular改为Normal,即不循环接收,由我们手动开启下一次的接收。另外,缓冲区半满也会进入回调函数,所以我们可以关闭半满的中断

​
void remoter_start(void)
{
    HAL_UARTEx_ReceiveToIdle_DMA(&huart5,usart5_buf,USART5_BUFLEN);
    __HAL_DMA_DISABLE_IT(&hdma_uart5_rx, DMA_IT_HT);
}

​

之后在其回调函数里进行遥控器数据的解析和重启串口空闲中断接收即可。

​
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if(huart->Instance == UART5)
    {
        //遥控器数据解析
        Dbus_to_rc(usart5_buf,&remoter);

        remoter_start();
    }
}

​
遥控器数据解析函数
​
void Dbus_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;
}

​

不过,根据我们的测试,直接这样代替 '似乎' 会导致某些情况下无法接收到数据。

在我们的测试下,程序刚烧录完成时(启用了Reset and Run),或者在Keil的Debug模式下,打开全速运行时, '有 概 率' 会无法收到信息,缓冲区都是空的,不过这种情况,可以在Debug模式下多按几次 Reset 来解决,多Reset几次之后就莫名能收到数据了....

由于本人能力有限,目前仍未理解该问题发生的原因,也希望大佬能在评论区解惑,谢谢大家。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐