CAN通信

何为CAN总线#

多电控单元间交换数据使用的多主机局部网络串行通信协议

总的来说,就是一条通信线路上连接多个节点,各个节点都通过这条线路来发送信息,当一个节点发送消息时,其他节点监听;当多个节点发送消息是,按照优先级发送。

可实现 点对点,点对多点,全局广播的功能。 一般认为有两个层级: 数据链路层物理层

通信基础知识-链路#

  • 地址
  • 冲突检测与避免
  • 误码校验
  • 滤波器与掩码(此部分内容摘抄自中科大RM系列教程之CAN通信)
    • 用于配置接收设备,注意,是过滤通过,而非过滤排除机制。即符合条件的报文会接收
    • 滤波器用于过滤出想要接收的信息,掩码Mask用于匹配想要接收的信息ID
    • 如何过滤与匹配?以CAN ID作为匹配规则。标准格式中的CAN ID长度为11b,也就是CAN ID最大为0x7ff
      • 一个CAN ID为0x114,如果我要配置一个滤波器只接收这个CAN ID的帧的话,就需要配置滤波器过滤目标为0x114,掩码为0x7ff。流程就是把掩码变成二进制01串,为1的位要与过滤目标一致
      • 一段CAN_ID为0x114~0x117,如果我要配置一个滤波器只接收这一段CAN ID的帧的话,就需要配置滤波器过滤目标为0x114,掩码为0x7fc。也就是把掩码变成二进制01串,为1的位要与过滤目标一致

数据格式#

帧类型#

数据帧#

数据帧包含以下部分

  • 帧起始(SOF):单个显性位组成,总线空闲时,发送节点发送帧起始。其他节点同步于该帧起始位
  • 仲裁域:决定发送优先级
    • ID:顾名思义,所发送数据的身份标识,大小为11字节。
    • SRR:替代远程帧请求位,为显性
    • IDE:识别符扩展位,在整个数据格式中处于第14位。在标准帧格式中位于控制域,为显性;在扩展帧格式中位于仲裁域,为隐性
    • RTR:远程帧发送标识位,为显性
  • 控制域
    • IDE:已在仲裁域中解释过,不再赘述
    • r0, r1:保留位0和1,在目前的CAN总线规范中没有明确定义用途。一般用于隐性电平填充。
    • DLC:数据段长度码;BCD编码,范围0~8
  • 数据域:MSB优先传输
  • CRC域:数据检错,CRC校验值存放于CRC段
  • 应答域(ACK):帧起始到CRC段之间的内容没有错误,则在ACK段发送显性电平
    • ACK槽:发送节点发送隐性电平;接收正确的节点发送显性电平 → 总线与结果为显性电平。发送节点据此可判断发送成功
    • ACK界定符:1个隐性电平
  • 帧结尾(EOF):7个连续隐性位组成

相信上述内容有许多专有名词难以理解,这些内容将在文末进行阐释。

标准帧格式 如下

帧起始 仲裁域 控制域 数据域 CRC域 ACK域 帧结束
组成部分 SOF ID[0:10] RTR IDE r0 DLC Data:Byte0~Byte7 CRC_Value[0:14] CRC界定符 ACK槽 ACK界定符 EOF
数据大小/bit 1 11 1 1 1 4 0~64 15 1 1 1 7

扩展帧格式标准帧格式 有如下区别

... 仲裁域 控制域 ...
组成部分 ... ID[0:10] SRR IDE ID[11,28] RTR r1 r0 DLC ...
数据大小/bit ... 11 1 1 18 1 1 1 4 ...

远程帧#

与数据帧相比,远程帧无数据域,由6个部分组成,也有标准格式和扩展格式,且RTR为1(隐性电平)。

标准帧格式 如下

帧起始 仲裁域 控制域 CRC域 ACK域 帧结束
组成部分 SOF ID[0:10] RTR IDE r0 DLC CRC_Value[0:14] CRC界定符 ACK槽 ACK界定符 EOF
数据大小/bit 1 11 1 1 1 4 15 1 1 1 7

扩展帧格式标准帧格式 有如下区别

... 仲裁域 控制域 ...
组成部分 ... ID[0:10] SRR IDE ID[11,28] RTR r1 r0 DLC ...
数据大小/bit ... 11 1 1 18 1 1 1 4 ...

数据帧与远程帧的区别#

比较内容 数据帧 远程帧
ID 发送节点的ID 被请求发送节点的ID
SRR 0(显性电平) 1(隐性电平)
RTR 0(显性电平) 1(隐性电平)
DLC 发送数据长度 请求的数据长度
有无数据段
CRC校验范围 帧起始+仲裁域+控制域+数据域 帧起始+仲裁域+控制域

错误帧#

CAN-bus 错误类型

  • CRC错误:发送节点计算得到的CRC值与接收到的CRC值不匹配
  • 格式错误:传输的数据帧格式与任何一种合法帧格式不符
  • 应答错误:发送节点在ACK阶段没有接收到应答信号
  • 位发送错误:发送节点在发送时发现总线电平与发送电平不相同
  • 位填充错误:传输信号违反“位填充”规则

当出现以上5种错误类型之一时,发送或接收节点将发送错误帧,结构为 错误标识 + 错误界定符

主动错误标识:由处于主动错误状态的节点发送 ↓

6个连续显性电平位 8个连续隐性电平位
0 0 0 0 0 0 1 1 1 1 1 1 1 1

被动错误标识:由处于被动错误状态的节点发送 ↓

6个连续隐性电平位 8个连续隐性电平位
1 1 1 1 1 1 1 1 1 1 1 1 1 1

为防止无法正常接收的节点一直发送错误帧干扰通信,CAN-bus规定了节点的三种状态及行为

  • REC:接收错误计数器

  • TEC:发送错误计数器

复位时二者都被清零

CAN-bus_node_states

过载帧#

当某接收节点没有做好接受下一帧数据准备时,将发送过载帧来通知发送节点

过载帧结构 过载标志 + 过载帧界定符

6个连续显性电平位(过载标志) 8个连续隐性电平位(过载帧界定符)
0 0 0 0 0 0 1 1 1 1 1 1 1 1

由于存在多个节点同时过载且过载帧发送有时间差,可能出现叠加超6个位的情况

Can-bus_overload

帧间隔#

用于将数据帧或远程帧和他们之前的帧分开,但过载帧和错误帧前不会插入帧间隔

... 其他帧 帧间隔 数据帧或远程帧 ... 其他帧 数据帧或远程帧 ...
  • 帧间隔过后,若无节点发送帧则总线进入空闲
... 帧间隔 0~∞ ...
  • 帧间隔过后,如果被动错误节点要发送帧,则先发送8个隐性电平的传输延迟,再发送帧
... 帧间隔 传输延迟(8个隐性位) 被动错误节点发送帧 ...

[!warning]

保证主动错误节点优先发送,避免被动错误节点因硬件故障干扰整个网络

收发模式#

  • 常规模式

    • 向总线发送

    • 从总线接收

  • 回环模式

    • 向总线和本机发送

    • 不从总线接收仅从本机接收

  • 静默模式

    • 不向总线发送仅向本机发送

    • 从总线和本机接收

  • 回环静默模式

    • 不向总线发送

    • 不从总线接收

    • 自收自发

一些疑惑及解答#

何为显性电平?何为隐性电平2#

答:显性电平在逻辑层面表现为0,隐性电平在逻辑层面表现为1;显性电平覆盖隐性电平;均为隐性电平时总线上才为隐性电平。CAN采用差分电压发送电平( VdiffV_diff = CAN_H - CAN_L

CAN总线为“隐性”(逻辑1)时,CAN_H和CAN_L的电平为2.5V(电位差V_diff为0V)

为什么要有120Ω的电阻?

CAN_H与CAN_L通过120Ohm电阻连接在一起,防止总线上传输的高频信号存在反射振铃效应导致信号不稳定、寄生电容导致信号响应速度变慢等情况

图解

CAN_Bus_Line
CAN_electrical_level

CAN通信采用双绞线传输的优点?#

答:使外部干扰在两根导线上产生的噪声相同,以便后续的差分电路提取出有用信号

仲裁域如何实现仲裁功能?#

答:CAN控制器在发送数据的同时监测数据线的电平是否与发送数据对应电平相同,如果不同则停止发送并做其他处理 ↓

  1. 如果该位处于仲裁域,则退出总线竞争
  2. 如果处于其他域,则产生错误时间(ACK或被动错误标志传输期间除外)

简而言之,显性电平覆盖隐性电平。同一位电平不相同时,显性电平继续发送,隐性电平的进入只听模式

数据帧与遥控帧的优先级#

答:数据帧优先级>遥控帧优先级,因为数据帧仲裁段RTR为显性

标准格式与扩展格式的优先级

答:标准格式优先级>扩展格式优先级,因为标准格式IDE位为显性电平,扩展格式IDE位为隐性电平。

[!warning]

对于前11位ID相同的标准格式和扩展格式才适用于此规则,前11位ID不相同的适用于仲裁规则

r0, r1作用?什么是隐性电平填充 3#

答:r0与r1为保留位,在当前的CAN标准中,这些位的具体用途并未明确定义。之所以用于隐性电平填充是因为 数字越大优先级越小,若为1(即隐性电平)则永远不会用得到它。

BCD编码 [4]?#

答:BCD码(Binary-Coded Decimal),用4位二进制数来表示1位十进制中的0~9这10个数码,是一种二进制的数字编码形式,用二进制编码的十进制代码。

十进制 BCD表达格式
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001

此处仅列出上文中所需要的知识,具体请查阅参考资料 [4]

什么是MSB数据?#

答:最高有效位,二进制中代表最高值的比特位,这一位对数值的影响最大。

以数字9为例子,其二进制为1001,若MSB发生错误,即最高位1变为0,则整个数字变为了1,误差为8。

此处仅列出上文中所需要的知识,具体请查阅参考资料 5

CRC如何计算 6 7#

答:CRC全称为 循环冗余校验。其根本思想是在要发送的帧后面附加一个数,作为数据校验码,并生成一个新帧发给接收端,用于验证数据是否损坏。

从数学上讲,CRC是数据的模二多项式 (polynomial) 除法的余数。因此,CRC可以写成 CRC = data % 2 polynomial

计算过程#

  1. 选定CRC生成多项式G(x)作为除数。G(x)决定了校验码的生成方式,常用的CRC多项式包括CRC-16(即除数共17位)、CRC-32。注意,G(x)的最高位和最低位必须为1。 以4位CRC(二进制共5位)作为示例 11001 表示的就是 \(G(x)=x^4+x^3+1\)

    [!warning]

    由于选择不同的多项式将导致相同数据产生不同的CRC,所以发送方和接收方需要使用共同的多项式。 对于CRC来说,根据具体的使用场景,有些多项式比其他多项式更适合使用。例如,某些多项式可能更容易检测出连续位错误或交替位错误。

  2. 看所选定的除数二进制位数(假设为k位),然后在要发送的数据帧(假设为m位)后面加上k-1位“0”,然后以这个加了k-1个“0“的新帧(一共是m+k-1位)以“模2除法”方式除以上面这个除数,所得到的余数(也是二进制的比特串)就是该帧的CRC校验码,也称之为FCS(帧校验序列)。

    [!warning]

    余数的位数一定要是比除数位数只能少一位,哪怕前面位是0,甚至是全为0(附带好整除时)也都不能省略。

  3. 再把这个校验码附加在原数据帧(就是m位的帧,注意不是在后面形成的m+k-1位的帧)后面,构建一个新帧发送到接收端

  4. 最后在接收端再把这个新帧以“模2除法”方式除以前面选择的除数,如果没有余数,则表明该帧在传输过程中没出错,否则出现了差错。

什么是模2除法#

模2减法

答:模2运算是对二进制数逐位进行的运算,不存在进位或借位,每个位被视为独立的数。(其实就是C语言中的异或运算)

下表为模2减法的数据表,其公式为 \(bit1-bit2=XOR\)

bit1 bit2 XOR
0 0 0
0 1 1
1 0 1
1 1 0

模2除法

模二除法的执行类似于算术除法,唯一的区别是我们使用模二减法(XOR)而不是算术减法来计算每一步的余数。

假设被除数为 100100 ,除数为 1101

  1. 计算除数长度为 L ,在示例中为 4

  2. 在被除数后面加上 L-1 位,这里为 3 (即CRC长度)

  3. 用二进制模2进行计算,得到的余数即为CRC结果:001

    mod2operation

关于 模2运算 的更多知识,请参阅参考资料 8

何为主动错误状态?何为被动错误状态?[9]

答:主动错误(Active Error)是指节点在发送数据帧时发现总线上存在错误,并且主动向其他节点通知该错误。 主动错误通常是由于发送节点检测到仲裁场(Arbitration Field)或控制位(Control Field)上的错误而产生的。

被动错误(Passive Error)是指节点在接收数据帧时检测到总线上存在错误,但不会主动通知其他节点。被动错误通常是由于接收节点检测到数据位(Data Field)或CRC(Cyclic Redundancy Check)校验错误而产生的。接收节点会将错误信息记录下来,但不会对其他节点进行任何主动干预。被动错误表示总线上存在一些问题,但并不影响其他节点的正常通信。

主动错误和过载帧格式一致,如何区分[10]?

答:过载帧与主动错误帧具有相同的格式。但是,过载帧只能在帧间间隔产生,因此可通过这种方式区分过载帧和错误帧(错误帧是在帧传输时发出的)。

CSMA/CD协议

此部分内容较长,单开一节讲起

CAN在stm32 HAL库中的应用#

硬件实现#

  • STM32自带CAN控制器,箭头表示收发关系,直线表示我们自行接到外部的总线
  • 发送方式:把发送的数据加到CAN收发器中,由它自主控制发送
  • 接收方式:从接收队列(FIFO队列)选择接收
STM32_CAN_hardware

软件配置#

  • 查看原理图,明确元件与引脚的对应关系
    • 在本实验中,明确LED和CAN即可
  • 配置STM32CubeMX
    • Serial Wire、外部时钟、时钟树
    • 配置LED引脚
      • 初始状态为高电平表示灯灭
      • 上拉电阻,尽量别浮空
    • 配置CAN参数
      • Prescaler=9 即从APB1中分频出来
      • Time Quanta in Bit Segment 1&2 均为2,采样在两者间
      • Operating Mode=Loopback 使用回环模式(自收自发)
      • 使能中断 Rx Tx

代码配置#

必要的自定义宏定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//滤波器编号
//将滤波器数值写入对应位
# define CAN_FILTER(x) ((x) << 3)

//接收队列
# define CAN_FIFO_0 (0 << 2)
# define CAN_FIFO_1 (1 << 2)

//标准帧或扩展帧
# define CAN_STDIO (0 << 1)
# define CAN_EXTID (1 << 1)

//数据帧或遥控帧
# define CAN_DATA_TYPE (0 << 0)
# define CAN_REMOTE_TYPE (1 << 0)

初始化CAN总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/**
* @brief 初始化CAN总线
*
* @param hcan CAN编号
* @param Callback_Function 处理回调函数
*/
void CAN_Init(CAN_HandleTypeDef *hcan)
{
HAL_CAN_Start(hcan);//打开CAN
//使能两个中断
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO1_MSG_PENDING);
}

/* USER CODE END 0 */

调用 CAN_Init 进行初始化

1
2
3
/* USER CODE BEGIN 2 */
CAN_Init(&hcan1);
/* USER CODE END 2 */

定义发送报文函数

(关于 CAN_TxHeaderTypeDefHAL_CAN_AddTxMessage() 相关定义可自行在keil编译后使用 F12 进行查看)

1
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
27
28
29
30
31
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/**
* @brief 发送数据帧
*
* @param hcan CAN编号
* @param ID CAN_ID
* @param Data 被发送的数据指针
* @param Length 长度
* @return uint8_t 执行状态
*/
uint8_t CAN_Send_Data(CAN_HandleTypeDef *hcan, uint16_t ID, uint8_t *Data, uint16_t Length)
{
CAN_TxHeaderTypeDef tx_header;//声明发送数据头
uint32_t used_mailbox;//使用的“邮箱”

// 检测关键传参
assert_param(hcan != NULL);

//写入数据
tx_header.StdId = ID;
tx_header.ExtId = 0;
tx_header.IDE = 0;
tx_header.RTR = 0;
tx_header.DLC = Length;

return (HAL_CAN_AddTxMessage(hcan, &tx_header, Data, &used_mailbox));
}

/* USER CODE END 0 */

调用发送报文函数

1
2
3
4
5
6
//先定义
uint8_t Send_Data = 0;

//以下添加在while(1)里
Send_Data++;
CAN_Send_Data(&hcan1, 0x114, &Send_Data, 1);

定义接收报文滤波器函数(规则按照上述讲解进行)

(关于 CAN_FilterTypeDefHAL_CAN_ConfigFilter(); 相关定义可自行在keil编译后使用 F12 进行查看)

1
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//配置滤波器

/**
* @brief 配置CAN的滤波器
*
* @param hcan CAN编号
* @param Object_Para 编号 | FIFOx | ID类型 | 帧类型
* @param ID ID
* @param Mask_ID 屏蔽位(0x3ff, 0x1fffffff)
*/
void CAN_Filter_Mask_Config(CAN_HandleTypeDef *hcan, uint8_t Object_Para, uint32_t ID, uint32_t Mask_ID)
{
CAN_FilterTypeDef can_filter_init_structure;

// 检测关键传参
assert_param(hcan != NULL);

if ((Object_Para & 0x02)) //判定标准帧或扩展帧
{
// 标准帧
// 掩码后ID的高16bit
can_filter_init_structure.FilterIdHigh = ID << 3 >> 16;
// 掩码后ID的低16bit
can_filter_init_structure.FilterIdLow = ID << 3 | ((Object_Para & 0x03) << 1);
// ID掩码值高16bit
can_filter_init_structure.FilterMaskIdHigh = Mask_ID << 3 << 16;
// ID掩码值低16bit
can_filter_init_structure.FilterMaskIdLow = Mask_ID << 3 | ((Object_Para & 0x03) << 1);
}
else
{
// 扩展帧
// 掩码后ID的高16bit
can_filter_init_structure.FilterIdHigh = ID << 5;
// 掩码后ID的低16bit
can_filter_init_structure.FilterIdLow = ((Object_Para & 0x03) << 1);
// ID掩码值高16bit
can_filter_init_structure.FilterMaskIdHigh = Mask_ID << 5;
// ID掩码值低16bit
can_filter_init_structure.FilterMaskIdLow = ((Object_Para & 0x03) << 1);
}

// 滤波器序号, 0-27, 共28个滤波器, can1是0~13, can2是14~27
can_filter_init_structure.FilterBank = Object_Para >> 3;
// 滤波器绑定FIFOx, 只能绑定一个
can_filter_init_structure.FilterFIFOAssignment = (Object_Para >> 2) & 0x01;
// 使能滤波器
can_filter_init_structure.FilterActivation = ENABLE;
// 滤波器模式, 设置ID掩码模式
can_filter_init_structure.FilterMode = CAN_FILTERMODE_IDMASK;
// 32位滤波
can_filter_init_structure.FilterScale = CAN_FILTERSCALE_32BIT;
//从机模式选择开始单元
can_filter_init_structure.SlaveStartFilterBank = 14;

HAL_CAN_ConfigFilter(hcan, &can_filter_init_structure);
}

调用接收报文滤波器

1
CAN_Filter_Mask_Config(&hcan1, CAN_FILTER(13) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE, 0x114, 0x7ff);

定义接收报文中断回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief HAL库CAN接收FIFO1中断
*
* @param hcan CAN编号
*/
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef header;
uint8_t data;

HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO1, &header, &data);

LED_Control(data);//点灯
}

点灯函数

1
2
3
4
5
6
7
8
9
10
11
void LED_Control(uint8_t data)
{
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_1, ((data & 1) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_2, ((data & 2) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_3, ((data & 4) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_4, ((data & 8) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, ((data & 16) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_6, ((data & 32) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_7, ((data & 64) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_8, ((data & 128) == 0) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}

参考资料#

[4]: BCD码是什么 - 知乎 (zhihu.com)

[9]: CAN通讯中 主动错误和被动错误的区别? - CSDN文库

[10]: CAN总线过载帧 - alifpga - 博客园 (cnblogs.com)