CAN
这次粗线条的聊一聊 CAN 通讯,因为想要详细的讲通它,实在是太长了!
# 硬件与协议
# 接线
CAN 通讯只使用了两根线:CAN_H
与 CAN_L
,通过差分信号的方式区分显性与隐形电平,从而表达逻辑0与逻辑1
# 仲裁
由于 CAN 通常是以“一主多从”的形式连接的,所以通常需要对来自不同设备的 CAN 信号进行优先级的仲裁。
而CAN 的仲裁机制正好利用了差分信号的特性,即显性电平覆盖隐形电平的特性,如果出现多个设备同时发送的情况,则先输出隐形电平的设备会失去对总线的占有权。下图中 D 为显性电平,R 为隐形电平,通过该图可以很容易地理解 CAN 的仲裁机制:
# 波特率
对于波特率的计算,目前不需要知道详细的原理,只需要知道怎么计算即可,例如对于下图, APB1 总线上的时钟频率是
波特率就是:
在 CubeMX 中的配置计算方法如下,对于更一般的,请参考这篇文章 (opens new window)。
# 代码层面
# 滤波器
CAN 总线上的信息由不同的设备发出,如果不进行任何手段的过滤、判别信号是否为我们需要的则将会接到很多的混杂的,所以有必要设置对信息的过滤规则以接收到我们需要的信息
这里依然引用一下这篇文章 (opens new window)的对于滤波器的形象解释:
重点介绍一下掩码模式的原理:掩码模式的思路很容易理解,举个例子,某所学校的学号构成方式为 [4 位 10 进制 入学年份]+[4 位 10 进制 学生序号],比如一个 2016 年入学的学生,其学号可以是 20161234,那么假如要开一个 2016 年毕业生的庆祝会,会场门口要检查每一个人的学号,只有 2016 级的才可以进入,这里应该使用什么样的判断方法呢?
首先,我们需要设置屏蔽码,屏蔽掉后四位的学生序号,因为他们和本次检测无关,反而增大了计算量。
然后设置检验法 2016,如果屏蔽后的结果等于 2016,则可以放行。
如下表所示,第一行为原码,第二行为掩码,将第一行表格中的数与掩码相乘,即得到第三行的屏蔽码,最后一行是验证码,屏蔽码和验证码比较确定一致后,就接收该学号:
2 0 1 6 1 2 3 4 1 1 1 1 0 0 0 0 2 0 1 6 0 0 0 0 2 0 1 6 0 0 0 0
对于 RM 官方的电调,我们对滤波器的设置与使能如下:
filter1.FilterIdHigh = 0x0000; // 设置验证码高低各16位
filter1.FilterIdLow = 0x0000;
filter1.FilterMaskIdHigh = 0x0000; // 设置屏蔽码高低各16位
filter1.FilterMaskIdLow = 0x0000;
filter1.FilterFIFOAssignment = CAN_FILTER_FIFO0; // 通过CAN的信息放入0号FIFO
filter1.FilterBank = 0;
filter1.SlaveStartFilterBank=14;
filter1.FilterMode = CAN_FILTERMODE_IDMASK; // 使用掩码模式
filter1.FilterScale = CAN_FILTERSCALE_32BIT; // 设置32位宽
filter1.FilterActivation = ENABLE; // 使能滤波器
HAL_CAN_ConfigFilter(&hcan, &filter1);
2
3
4
5
6
7
8
9
10
11
12
以上代码对于滤波器的使能就是:
- 使用屏蔽码
0x00000000
将所有的信号均变为0x00000000
- 随后使用验证码
0x00000000
检测经屏蔽码过滤后的信号是否与此符合
所以,就是全部都接受,因为对于这个 CAN,其实只挂载了电调,而电调上的电机信号我们一定是要接收的。所以就产生了这个看似怪异的滤波器
# 接收信号
我们通过 CAN 的接收中断来接收来自 CAN 的信号,对于接收消息,我们需要额外使用 CAN_RxHeaderTypeDef
来定义一个消息接受句柄。对于消息接收句柄,我们只需要定义它而无需配置相关参数即可
首先我们需要开启 CAN 的接收中断:
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)
滤波器使能等等不在此处阐述,随后就是接受中断函数。在中断接收函数中,由于电机电调发送来的报文格式是拆为高8位与低8位。我们主要对数据进行拼接转换为我们需要的格式:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *Target_hcan)
{
s_CAN_Message can1_rx_message;
CAN_RxHeaderTypeDef Can1RxHeader;
if(Target_hcan -> Instance == CAN1)
{
HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &Can1RxHeader, can1_rx_message.Data);
switch (Can1RxHeader.StdId)
{
case 0x201:
{
printf("CAN receives successfully.\n\r");
motor.esc_back_position = can1_rx_message.Data[0]<<8 | can1_rx_message.Data[1];
motor.esc_back_speed = can1_rx_message.Data[2]<<8 | can1_rx_message.Data[3];
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
关于 RoboMaster 使用的电调格式等,依然请参考上面那篇文章。
# 发送信号
发送信号时,依然需要自己定义一个 CAN_TxHeaderTypeDef
类型的消息发送句柄,不同的是,消息发送句柄需要我们设置一些参数以表明 CAN 信号中的标识符,让别人正确的识别到这是从我们这里发出的信息。
void CANTx_SendCurrent(CAN_HandleTypeDef *Target_hcan, uint32_t id, int16_t current1, int16_t current2, int16_t current3, int16_t current4)
{
s_CAN_Message tx_message;
CAN_TxHeaderTypeDef CanTxHeader;
uint32_t TX_MailBOX = CAN_TX_MAILBOX0;
// 设置发送消息句柄
CanTxHeader.StdId = id;
CanTxHeader.IDE = CAN_ID_STD;
CanTxHeader.RTR = CAN_RTR_DATA;
CanTxHeader.DLC = 0x08;
tx_message.Data[0] = (unsigned char)(current1>>8);
tx_message.Data[1] = (unsigned char)current1;
tx_message.Data[2] = (unsigned char)(current2>>8);
tx_message.Data[3] = (unsigned char)current2;
tx_message.Data[4] = (unsigned char)(current3>>8);
tx_message.Data[5] = (unsigned char)current3;
tx_message.Data[6] = (unsigned char)(current4>>8);
tx_message.Data[7] = (unsigned char)current4;
if(HAL_CAN_AddTxMessage(Target_hcan, &CanTxHeader, tx_message.Data, &TX_MailBOX) != HAL_OK)
{
printf("CAN send failed!\r\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
我们发送信息主要使用了 HAL 库函数 HAL_CAN_AddTxMessage
,他还有一个参数是发送信箱的地址,也需要自行定义一下
最简单的方式就是这样,如需参考稍微高级一点的还请参考: