写在前面

上一篇简要的介绍了CAN的基本知识,小米电机的通信协议,以及小米微电机驱动库的结构,本章将结合具体代码分析HAL库CAN通信流程。为什么使用F105呢?因为f105属于互联型,拥有两个CAN,分别是主CAN1和从CAN2,在使用can2时必须要开启can1的时钟,若can1和can2同时使用时,先初始化can1驱动,再初始化can2。做机器人电机比较多,双CAN板更合适一些。

前置任务:HAL库CAN通信流程

这里我们对寄存器及相关外设不做过多讨论,仅讨论函数调用和程序运行流程

一、CAN初始化

在GPIO的使用中,我们会定义一个GPIO初始化结构体,并在结构体的成员中储存GPIO的设置参数,CAN通信也是如此
不同的是,这里定义的是CAN句柄结构体,其中的成员Init是初始化结构体,有机会我们可以单开一章详细分析一下
比如这一行代码定义了结构体hcan1(一般来说单can板定义时一般写为hcan,我在这里就傻傻的踩了一个坑)

CAN_HandleTypeDef hcan1;

下面是一段初始化CAN1的代码:

void MX_CAN1_Init(void)
{
  hcan1.Instance = CAN1;
  hcan1.Init.Prescaler = 9;
  hcan1.Init.Mode = CAN_MODE_NORMAL;
  hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
  hcan1.Init.TimeSeg1 = CAN_BS1_2TQ;
  hcan1.Init.TimeSeg2 = CAN_BS2_1TQ;
  hcan1.Init.TimeTriggeredMode = DISABLE;
  hcan1.Init.AutoBusOff = DISABLE;
  hcan1.Init.AutoWakeUp = DISABLE;
  hcan1.Init.AutoRetransmission = DISABLE;
  hcan1.Init.ReceiveFifoLocked = DISABLE;
  hcan1.Init.TransmitFifoPriority = DISABLE;
  if (HAL_CAN_Init(&hcan1) != HAL_OK)
  {
    Error_Handler();
  }

}

分别对hcan1的成员Init的成员变量进行赋值,然后在传入函数HAL_CAN_Init(&hcan1)进行CAN1的初始化,
详尽分析如下:

  1. hcan1.Instance = CAN1;:将CAN1总线的实例赋值给hcan1结构体的Instance成员。

  2. hcan1.Init.Prescaler = 9;:设置CAN1总线的预分频器值为9。预分频器用于设置CAN总线的波特率。

  3. hcan1.Init.Mode = CAN_MODE_NORMAL;:设置CAN1总线的工作模式为正常模式。正常模式是指CAN总线用于数据传输而非诊断或监听模式。

  4. hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;:设置CAN1总线的同步跳转宽度为1个时间单位。

  5. hcan1.Init.TimeSeg1 = CAN_BS1_2TQ;:设置CAN1总线的时间段1长度为2个时间单位。

  6. hcan1.Init.TimeSeg2 = CAN_BS2_1TQ;:设置CAN1总线的时间段2长度为1个时间单位。

  7. hcan1.Init.TimeTriggeredMode = DISABLE;:禁用CAN1总线的时间触发模式。时间触发模式是一种特殊的传输模式,用于在特定时间触发CAN消息的发送。

  8. hcan1.Init.AutoBusOff = DISABLE;:禁用CAN1总线的自动总线关闭功能。自动总线关闭是一种保护机制,当总线错误发生时,自动关闭CAN总线。

  9. hcan1.Init.AutoWakeUp = DISABLE;:禁用CAN1总线的自动唤醒功能。自动唤醒功能是一种低功耗模式下的功能,用于在接收到CAN消息时自动唤醒系统。

  10. hcan1.Init.AutoRetransmission = DISABLE;:禁用CAN1总线的自动重传功能。自动重传功能用于在发送CAN消息时自动重传失败的消息。

  11. hcan1.Init.ReceiveFifoLocked = DISABLE;:禁用CAN1总线的接收FIFO锁定功能。接收FIFO锁定功能用于锁定接收FIFO以防止被覆盖。

  12. hcan1.Init.TransmitFifoPriority = DISABLE;:禁用CAN1总线的发送FIFO优先级功能。发送FIFO优先级功能用于设置发送FIFO中消息的优先级。

  13. if (HAL_CAN_Init(&hcan1) != HAL_OK):调用HAL库提供的函数HAL_CAN_Init对CAN1总线进行初始化。如果初始化失败,则执行Error_Handler()函数。

二、CAN引脚初始化

void HAL_CAN_MspInit(CAN_HandleTypeDef* canHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(canHandle->Instance==CAN1)
  {
  /* USER CODE BEGIN CAN1_MspInit 0 */

  /* USER CODE END CAN1_MspInit 0 */
    /* CAN1 clock enable */
    __HAL_RCC_CAN1_CLK_ENABLE();

    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**CAN1 GPIO Configuration
    PB8     ------> CAN1_RX
    PB9     ------> CAN1_TX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_8;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    __HAL_AFIO_REMAP_CAN1_2();

    /* CAN1 interrupt Init */
    HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
  /* USER CODE BEGIN CAN1_MspInit 1 */

  /* USER CODE END CAN1_MspInit 1 */
  }
}

这个代码对CAN的引脚进行初始化,分别设置cantx与canrx的引脚模式及rx0中断。我在初学时一直没找到此函数在何处被调用,现在我终于找到了,此函数在第一步的HAL_CAN_Init(&hcan1)中被调用,并在这两种情况下被调用:

  1. 当USE_HAL_CAN_REGISTER_CALLBACKS宏定义为1,并且CAN外设的状态为HAL_CAN_STATE_RESET时,会先将回调函数恢复为默认的legacy函数,然后判断用户是否定义了MspInitCallback回调函数,如果没有定义,则将默认的HAL_CAN_MspInit函数赋值给hcan->MspInitCallback,最后调用hcan->MspInitCallback(hcan)来初始化CAN外设的底层硬件。

  2. 当USE_HAL_CAN_REGISTER_CALLBACKS宏定义为0,并且CAN外设的状态为HAL_CAN_STATE_RESET时,直接调用HAL_CAN_MspInit函数来初始化CAN外设的底层硬件。

在单片机启动运行CAN通信程序时,调用HAL_CAN_MspInit函数的方式取决于宏定义USE_HAL_CAN_REGISTER_CALLBACKS的值。(默认定义为0)

#define  USE_HAL_CAN_REGISTER_CALLBACKS         0U /* CAN register callback disabled       */

如果我们没有改动的话,会通过第二种流程调用HAL_CAN_MspInit

三、编写发送函数

HAL库是如何发送CAN通信帧的呢?

HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, const CAN_TxHeaderTypeDef *pHeader,const uint8_t aData[], uint32_t *pTxMailbox)

通过函数HAL_CAN_AddTxMessage来发送,此函数的四个参数分别代表着:CAN外设,CAN消息的传输参数,传输数据,发送邮箱
这里我们着重分析const CAN_TxHeaderTypeDef *pHeaderr

typedef struct
{
  uint32_t StdId;    /*!< Specifies the standard identifier.
                          This parameter must be a number between Min_Data = 0 and Max_Data = 0x7FF. */

  uint32_t ExtId;    /*!< Specifies the extended identifier.
                          This parameter must be a number between Min_Data = 0 and Max_Data = 0x1FFFFFFF. */

  uint32_t IDE;      /*!< Specifies the type of identifier for the message that will be transmitted.
                          This parameter can be a value of @ref CAN_identifier_type */

  uint32_t RTR;      /*!< Specifies the type of frame for the message that will be transmitted.
                          This parameter can be a value of @ref CAN_remote_transmission_request */

  uint32_t DLC;      /*!< Specifies the length of the frame that will be transmitted.
                          This parameter must be a number between Min_Data = 0 and Max_Data = 8. */

  FunctionalState TransmitGlobalTime; /*!< Specifies whether the timestamp counter value captured on start
                          of frame transmission, is sent in DATA6 and DATA7 replacing pData[6] and pData[7].
                          @note: Time Triggered Communication Mode must be enabled.
                          @note: DLC must be programmed as 8 bytes, in order these 2 bytes are sent.
                          This parameter can be set to ENABLE or DISABLE. */

} CAN_TxHeaderTypeDef;

分析这个结构体的定义:CAN_TxHeaderTypeDef结构体用于存储CAN消息的传输参数,包括以下:

  1. StdId:标准ID,用于指定CAN消息的标准ID。取值范围为0到0x7FF。

  2. ExtId:扩展ID,用于指定CAN消息的扩展ID。取值范围为0到0x1FFFFFFF。

  3. IDE:ID类型,用于指定将要传输的消息的标识符类型。可以是CAN_ID_STD(标准ID)或CAN_ID_EXT(扩展ID)。

  4. RTR:帧类型,用于指定将要传输的消息的帧类型。可以是CAN_RTR_DATA(数据帧)或CAN_RTR_REMOTE(远程帧)。

  5. DLC:数据长度,用于指定将要传输的消息的数据长度。取值范围为0到8。

  6. TransmitGlobalTime:传输全局时间,指定是否将在帧传输开始时捕获的时间戳计数器值发送到DATA6和DATA7,替代pData[6]和pData[7]。需要启用时间触发通信模式,并且DLC必须设置为8字节,以便发送这2个字节。可以设置为ENABLE或DISABLE。

所以这个结构体控制着我们发送的帧类型,帧种类,帧ID内容(上一篇所说控制电机的重中之重),帧数据长度
这里另附一张数据帧结构图


再来看函数HAL_CAN_AddTxMessage()

HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, const CAN_TxHeaderTypeDef *pHeader,
                                       const uint8_t aData[], uint32_t *pTxMailbox)
{
  uint32_t transmitmailbox;
  HAL_CAN_StateTypeDef state = hcan->State;
  uint32_t tsr = READ_REG(hcan->Instance->TSR);

  /* Check the parameters */
  assert_param(IS_CAN_IDTYPE(pHeader->IDE));
  assert_param(IS_CAN_RTR(pHeader->RTR));
  assert_param(IS_CAN_DLC(pHeader->DLC));
  if (pHeader->IDE == CAN_ID_STD)
  {
    assert_param(IS_CAN_STDID(pHeader->StdId));
  }
  else
  {
    assert_param(IS_CAN_EXTID(pHeader->ExtId));
  }
  assert_param(IS_FUNCTIONAL_STATE(pHeader->TransmitGlobalTime));

  if ((state == HAL_CAN_STATE_READY) ||
      (state == HAL_CAN_STATE_LISTENING))
  {
    /* Check that all the Tx mailboxes are not full */
    if (((tsr & CAN_TSR_TME0) != 0U) ||
        ((tsr & CAN_TSR_TME1) != 0U) ||
        ((tsr & CAN_TSR_TME2) != 0U))
    {
      /* Select an empty transmit mailbox */
      transmitmailbox = (tsr & CAN_TSR_CODE) >> CAN_TSR_CODE_Pos;

      /* Store the Tx mailbox */
      *pTxMailbox = (uint32_t)1 << transmitmailbox;

      /* Set up the Id */
      if (pHeader->IDE == CAN_ID_STD)
      {
        hcan->Instance->sTxMailBox[transmitmailbox].TIR = ((pHeader->StdId << CAN_TI0R_STID_Pos) |
                                                           pHeader->RTR);
      }
      else
      {
        hcan->Instance->sTxMailBox[transmitmailbox].TIR = ((pHeader->ExtId << CAN_TI0R_EXID_Pos) |
                                                           pHeader->IDE |
                                                           pHeader->RTR);
      }

      /* Set up the DLC */
      hcan->Instance->sTxMailBox[transmitmailbox].TDTR = (pHeader->DLC);

      /* Set up the Transmit Global Time mode */
      if (pHeader->TransmitGlobalTime == ENABLE)
      {
        SET_BIT(hcan->Instance->sTxMailBox[transmitmailbox].TDTR, CAN_TDT0R_TGT);
      }

      /* Set up the data field */
      WRITE_REG(hcan->Instance->sTxMailBox[transmitmailbox].TDHR,
                ((uint32_t)aData[7] << CAN_TDH0R_DATA7_Pos) |
                ((uint32_t)aData[6] << CAN_TDH0R_DATA6_Pos) |
                ((uint32_t)aData[5] << CAN_TDH0R_DATA5_Pos) |
                ((uint32_t)aData[4] << CAN_TDH0R_DATA4_Pos));
      WRITE_REG(hcan->Instance->sTxMailBox[transmitmailbox].TDLR,
                ((uint32_t)aData[3] << CAN_TDL0R_DATA3_Pos) |
                ((uint32_t)aData[2] << CAN_TDL0R_DATA2_Pos) |
                ((uint32_t)aData[1] << CAN_TDL0R_DATA1_Pos) |
                ((uint32_t)aData[0] << CAN_TDL0R_DATA0_Pos));

      /* Request transmission */
      SET_BIT(hcan->Instance->sTxMailBox[transmitmailbox].TIR, CAN_TI0R_TXRQ);

      /* Return function status */
      return HAL_OK;
    }
    else
    {
      /* Update error code */
      hcan->ErrorCode |= HAL_CAN_ERROR_PARAM;

      return HAL_ERROR;
    }
  }
  else
  {
    /* Update error code */
    hcan->ErrorCode |= HAL_CAN_ERROR_NOT_INITIALIZED;

    return HAL_ERROR;
  }
}

以上是HAL_CAN_AddTxMessage函数的代码实现。这个函数用于向CAN总线发送CAN消息。

函数首先检查传入的CAN_HandleTypeDef结构体中的CAN外设状态和TSR寄存器的值,确认CAN外设处于正确的状态并且至少有一个空闲的发送邮箱。

然后,函数会根据传入的CAN_TxHeaderTypeDef结构体中的参数配置选中的发送邮箱:

  • 设置TIxR寄存器,即设置CAN消息的标识符。根据pHeader的IDE、StdId/ExtId和RTR来设置TIR寄存器的对应位。
  • 设置TDTxR寄存器,即设置CAN消息的数据长度。
  • 如果pHeader的TransmitGlobalTime为使能状态,则设置TDT0R寄存器的TGT位。
  • 将CAN消息的数据写入到选中的发送邮箱的TDHR和TDLR寄存器中。

最后,函数请求发送CAN消息,通过设置TIR寄存器的TXRQ位向CAN外设请求发送CAN消息。

函数根据发送结果返回相应的函数执行状态:

  • 如果发送成功,则返回HAL_OK。
  • 如果发送失败(如发送邮箱已满),则返回HAL_ERROR,并更新CAN外设的ErrorCode为HAL_CAN_ERROR_PARAM。
  • 如果CAN外设未初始化,则返回HAL_ERROR,并更新CAN外设的ErrorCode为HAL_CAN_ERROR_NOT_INITIALIZED。
    CAN寄存器参考表:

    理解了这些,我们来编写发送函数

    void can_send_message(uint32_t id, uint8_t *buf, uint8_t len)
    {
      uint32_t tx_mail = CAN_TX_MAILBOX0;
    
      g_can1_txheader.ExtId = id;
      g_can1_txheader.DLC = len;
      g_can1_txheader.IDE = CAN_ID_EXT;
      g_can1_txheader.RTR = CAN_RTR_DATA;
    
      HAL_CAN_AddTxMessage(&hcan1, &g_can1_txheader, buf, &tx_mail);
    
      while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) != 3);
    }
    

    这个函数有三个参数,ID、数据、数据长度,g_can1_txheader结构体是上文所述CAN_TxHeaderTypeDef类型结构体,需要在程序中定义为全局变量,上述代码段中未体现。
    函数首先定义了一个变量tx_mail,用于存储选择的发送邮箱。

然后,函数通过设置全局变量g_can1_txheader的各个字段来配置CAN消息的参数:

  • 设置g_can1_txheader的ExtId字段为传入的id,即CAN消息的扩展标识符。
  • 设置g_can1_txheader的DLC字段为传入的len,即CAN消息的数据长度。
  • 设置g_can1_txheader的IDE字段为CAN_ID_EXT,表示使用扩展标识符。
  • 设置g_can1_txheader的RTR字段为CAN_RTR_DATA,表示数据帧。

之后函数会调用HAL_CAN_AddTxMessage函数来发送CAN消息,传入的参数为CAN外设的句柄hcan1、g_can1_txheader结构体、消息数据buf和tx_mail变量的地址。
完成这些工作后函数会等待直到CAN外设的发送邮箱全部空闲(即可用数量为3),然后才会退出循环。

结语

在了解了HAL库CAN通信的基本流程与原理后,下一篇我们开始基于实例来使用CAN通信控制小米电机,并详细分析小米电机驱动库代码。