写在前面
上一篇简要的介绍了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的初始化,
详尽分析如下:
-
hcan1.Instance = CAN1;
:将CAN1总线的实例赋值给hcan1
结构体的Instance
成员。 -
hcan1.Init.Prescaler = 9;
:设置CAN1总线的预分频器值为9。预分频器用于设置CAN总线的波特率。 -
hcan1.Init.Mode = CAN_MODE_NORMAL;
:设置CAN1总线的工作模式为正常模式。正常模式是指CAN总线用于数据传输而非诊断或监听模式。 -
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
:设置CAN1总线的同步跳转宽度为1个时间单位。 -
hcan1.Init.TimeSeg1 = CAN_BS1_2TQ;
:设置CAN1总线的时间段1长度为2个时间单位。 -
hcan1.Init.TimeSeg2 = CAN_BS2_1TQ;
:设置CAN1总线的时间段2长度为1个时间单位。 -
hcan1.Init.TimeTriggeredMode = DISABLE;
:禁用CAN1总线的时间触发模式。时间触发模式是一种特殊的传输模式,用于在特定时间触发CAN消息的发送。 -
hcan1.Init.AutoBusOff = DISABLE;
:禁用CAN1总线的自动总线关闭功能。自动总线关闭是一种保护机制,当总线错误发生时,自动关闭CAN总线。 -
hcan1.Init.AutoWakeUp = DISABLE;
:禁用CAN1总线的自动唤醒功能。自动唤醒功能是一种低功耗模式下的功能,用于在接收到CAN消息时自动唤醒系统。 -
hcan1.Init.AutoRetransmission = DISABLE;
:禁用CAN1总线的自动重传功能。自动重传功能用于在发送CAN消息时自动重传失败的消息。 -
hcan1.Init.ReceiveFifoLocked = DISABLE;
:禁用CAN1总线的接收FIFO锁定功能。接收FIFO锁定功能用于锁定接收FIFO以防止被覆盖。 -
hcan1.Init.TransmitFifoPriority = DISABLE;
:禁用CAN1总线的发送FIFO优先级功能。发送FIFO优先级功能用于设置发送FIFO中消息的优先级。 -
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)中被调用,并在这两种情况下被调用:
-
当USE_HAL_CAN_REGISTER_CALLBACKS宏定义为1,并且CAN外设的状态为HAL_CAN_STATE_RESET时,会先将回调函数恢复为默认的legacy函数,然后判断用户是否定义了MspInitCallback回调函数,如果没有定义,则将默认的HAL_CAN_MspInit函数赋值给hcan->MspInitCallback,最后调用hcan->MspInitCallback(hcan)来初始化CAN外设的底层硬件。
-
当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消息的传输参数,包括以下:
-
StdId:标准ID,用于指定CAN消息的标准ID。取值范围为0到0x7FF。
-
ExtId:扩展ID,用于指定CAN消息的扩展ID。取值范围为0到0x1FFFFFFF。
-
IDE:ID类型,用于指定将要传输的消息的标识符类型。可以是CAN_ID_STD(标准ID)或CAN_ID_EXT(扩展ID)。
-
RTR:帧类型,用于指定将要传输的消息的帧类型。可以是CAN_RTR_DATA(数据帧)或CAN_RTR_REMOTE(远程帧)。
-
DLC:数据长度,用于指定将要传输的消息的数据长度。取值范围为0到8。
-
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通信控制小米电机,并详细分析小米电机驱动库代码。
评论(0)
您还未登录,请登录后发表或查看评论