前言

CAN 通信原理、协议格式等内容网络资料丰富,本文几乎不会提及,重心将放在 CUBEMX 配置和 Keil 代码实现上。目的是提供一个通用模版,让读者能够根据数据手册,手写任意产品的 CAN 数据收发代码。

本文将默认读者已经粗略了解 CAN 总线相关硬件知识,若文中有不明白的名词,劳烦自行搜索。

本文 CUBEMX 配置基于 STM32H7 系列 FDCAN。

报文核心格式

CAN 报文有“帧(Frame)”的概念,一份报文分为数据帧、遥控帧、错误帧、过载帧、帧间空间。而在软件应用中,我们通常只需要关注 数据帧(Data frame):用于发送单元向接收单元传送数据的帧。

数据帧由 7 个段构成,如下图,分别为:帧起始、仲裁段、控制段、数据段、CRC段、ACK段、帧结束。

CAN数据帧

帧起始

表示一个数据帧的开始,1 个显性位。

仲裁段

写明报文 ID,确定发送的帧类型(数据帧 or 遥控帧)。

在标准格式帧和扩展格式帧中,仲裁段的长度不同:

  • 标准格式:11 位 ID
  • 扩展格式:29 位 ID

对于前 11 位 ID 相同的标准帧和扩展帧,标准帧优先级更高。

对于两种格式的帧,在应用中我们只需要记住他们的 ID 长度不同即可,其余内容几乎完全一致。

控制段

指示了数据段的长度。

数据段

实际要传输的数据,0~8 字节,从 MSB 至 LSB。

CRC段

校验段,检测数据在传输过程中是否出错。

ACK段

接收单元通过 ACK 段通知发送单元数据是否正确接收。

帧结束

表示一个数据帧的结束,7 个隐性位。

CUBEMX配置

起手丝滑小连招,新建工程、选芯片、配时钟树等常规流程,此处跳过。

Connectivity 中选择 FDCAN1,选中 Activated,进入配置界面。(FDCAN 包含了普通 CAN,看懂 FDCAN 就两个都懂了)

FDCAN参数详解

FDCAN参数

注:以下参数若标明 根据计算得出,则先不必纠结具体数值,后文会讲解如何计算。

  • Frame Format:Classic mode,经典 CAN 模式(CAN2.0)。

  • Mode:Normal。

  • Auto Retransmission(自动重发):Disable。根据需求,多个节点发生阻塞情况可能需要开启。

  • Transmit Pause(发送延时):Disable。FDCAN 特有的选项,通常用不到。

  • Protocol Exception(协议异常处理):Disable。常规应用一般用不到,Enable 也可。

  • Nominal Sync Jump Width(仲裁段同步跳转宽度):根据计算得出。用于调整时钟同步的容差范围。

  • Data Prescaler(数据段预分频器):根据计算得出。时钟分频。

  • Data Sync Jump Width(数据段同步跳转宽度):根据计算得出

  • Data Time Seg1、Data Time Seg2(数据段时间段):根据计算得出。数据段采样点位置。

  • Message Ram Offset(消息 RAM 地址偏移):根据计算得出。此 RAM 是所有 FDCAN 共用的,如果配置了多路 FDCAN,强烈建议设置此偏移参数。

  • Std Filters Nbr(标准格式过滤器数量):标准格式滤波器数量,用于在仲裁段过滤(11 位)ID。如果外设为标准格式 CAN,此参数至少为 1。

  • Ext Filters Nbr(扩展格式过滤器数量):扩展格式滤波器数量,用于在仲裁段过滤(29 位)ID。如果外设为扩展格式 CAN,此参数至少为 1。

  • Rx Fifo0 Elmts Nbr、Rx Fifo1 Elmts Nbr(接收 FIFO 的数量):这两个 FIFO 相互独立,如果只用一个,另一个可以直接填 0。具体需要的数量受外设数量、外设发送频率影响,可以尽量开大些。

  • Rx Fifo0 Elmt Size、Rx Fifo1 Elmt Size(每个 FIFO 的大小):通常 8 bytes data field 即可,一个数据段最大也就 8 字节。

    注:Fifo Elmts Nbr 和 Fifo Elmt Size 两个参数的严格计算方式笔者理解不深,但通常不影响应用,这里贴上一篇笔者读过的文章《STM32H7系列FDCAN配置成经典CAN的经验教程和注意事项》,供参考。

  • Rx Buffers Nbr、Rx Buffer Size(接收缓冲区数量、大小):0。如果用 FIFO 就不需要缓冲区,填 0 即可。

  • Tx Events Nbr、Tx Buffers Nbr(发送事件数量、发送缓冲区数量):0。如果不使用 Events 和 Buffers 发送,填 0 即可。

  • Tx Fifo Queue Elmts Nbr(发送 FIFO 队列数量):和接收的类似。

  • Tx Fifo Queue Mode(发送 FIFO 队列模式):FIFO mode。使用 FIFO 模式。

  • Tx Elmt Size(每个发送区的大小):通常 8 bytes data field 即可。

  • Bit Timings Parameters(仲裁段时间参数):根据计算得出。和前面数据段类似,经典 CAN 要求仲裁段和数据段波特率一致,填写一样的参数即可。

FDCAN参数计算

数据段/仲裁段波特率

包含 Data Prescaler、Data Sync Jump Width、Data Time Seg1、Data Time Seg2、Norminal Prescaler、Nominal Time Seg1、Nominal Time Seg2 参数。

先放一个公式:

BaudRate=FDCANFrequency÷Prescaler1+TimeSeg1+TimeSeg2BaudRate = \frac{FDCANFrequency \div Prescaler}{1 + TimeSeg1 + TimeSeg2}

这里了解即可,具体计算推荐使用 CAN 波特率计算器。

  • FDCANFrequency:FDCAN 外设时钟频率,单位 Hz。从时钟树中可以找到。

    CAN时钟频率

  • Prescaler:预分频器,分频后得到一个更小的频率。

  • TimeSeg1、TimeSeg2:采样点位置。

推荐一个好用的 CAN 波特率计算器:ZCANPRO 致远电子

zcanpro

下载后,打开 ZBaudcalTool.exe。(整个文件夹包含的工具很多,所以比较大,要是能把 ZBaudcalTool 单独拎出来就好了)

zbaudcaltool

  1. 设备类型选择 CANFDNET_TCP 或 CANFDNET_UDP

cancal1

  1. 填写时钟频率(上文的 FDCANFrequency)、想要的波特率、允许误差。

cancal2

  1. 选择一组合适的参数,其中:
  • BPR 应尽量小,以保证 Tq 尽可能小,减少误差;
  • 采样点推荐范围:75%~87.5%;
  • 波特率 <= 500 kbps时,推荐采样点87.5%;
  • 波特率 > 500 kbps时,推荐采样点80%;
  • 波特率 > 800 kbps时,推荐采样点75%。
  • 注意 CUBEMX 中参数的可填写范围

这里我们选择了一组合适的参数,然后修改同步跳转宽度(Sync Jump Width),应尽量大且尽量与 TSEG2 一致。

cancal3

cancal4

然后就可以将这组参数填入 CUBEMX 中,填好后如下图。

1 2

消息RAM地址偏移(Message Ram Offset)

STM32H7 中,多路 CAN 是共用一个 RAM 的,如果配置不当,设备一多很容易出现丢包、无法通信的情况。

canram

上图出自 ST 的《AN5348 Introduction to FDCAN peripherals for STM32 MCUs》

一路 CAN可以分配最大 2560 words 的 RAM 空间,当我们使用多路 CAN 时,应该给除第一路 CAN 外的其他 CAN 配置 RAM 偏移地址。

比如使用 3 路 CAN,最简单的方式就是:FDCAN1 偏移填 0,FDCAN2 偏移填 256013=8532560 \cdot \frac{1}{3} = 853,FDCAN3 偏移填 256023=17062560 \cdot \frac{2}{3} = 1706。填写时注意进制。

开启中断

进入 NVIC Settings,打开 FDCAN 中断

canirq

生成代码

配置好工程其他设置,就可以生成代码了。

Keil代码

定义CAN过滤器初始化函数

过滤器是 CAN 通信中非常重要的一部分,即使不需要过滤任何数据,也 必须 配置过滤器,不然无法正常通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
void fdcan_filter_init(FDCAN_HandleTypeDef *hfdcan){
FDCAN_FilterTypeDef fdcan_filter;

fdcan_filter.IdType = FDCAN_STANDARD_ID;
fdcan_filter.FilterIndex = 0;
fdcan_filter.FilterType = FDCAN_FILTER_MASK;
fdcan_filter.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;// FIFO0
fdcan_filter.FilterID1 = 0x00;
fdcan_filter.FilterID2 = 0x00;
HAL_FDCAN_ConfigFilter(hfdcan,&fdcan_filter);
HAL_FDCAN_ConfigGlobalFilter(hfdcan,FDCAN_REJECT,FDCAN_REJECT,FDCAN_REJECT_REMOTE,FDCAN_REJECT_REMOTE);
HAL_FDCAN_ConfigFifoWatermark(hfdcan, FDCAN_CFG_RX_FIFO0, 1);
}

FDCAN_FilterTypeDef 结构体的每个参数,在 stm32h7xx_hal_fdcan.h 文件中都有很详细地说明,在这里简单概括下。

  • IdType:帧格式。标准格式(FDCAN_STANDARD_ID)或扩展格式(FDCAN_EXTENDED_ID)。
  • FilterIndex:过滤器编号,从 0 开始。标准帧最多 128 个,扩展帧最多 64 个。
  • FilterTypeFilterID1FilterID2:第一个参数是过滤器类型,后两个参数与 FilterType 配合。类型共 4 种,FDCAN_FILTER_RANGE(接收 ID 在 ID1~ID2 的消息,包含 ID1、ID2)、FDCAN_FILTER_DUAL(只接收 ID 等于 ID1 或 ID2 的消息)、FDCAN_FILTER_MASK(ID2 作为掩码,对比消息 ID 和 ID1,只接收掩码位相等的消息)、FDCAN_FILTER_RANGE_NO_EIDM(接收 ID 在 ID1~ID2 的消息,包含 ID1、ID2,但不应用 EIDM mask)。
    关于掩码模式,如果 ID2 设为 0,那么就没有掩码位,也就不会过滤任何消息。所以无需过滤消息的场景中,通常都会配置成 掩码模式ID1=0ID2=0

HAL_FDCAN_ConfigFilter(hfdcan,&fdcan_filter);:调用函数,应用过滤器配置。

HAL_FDCAN_ConfigGlobalFilter(FDCAN_HandleTypeDef *hfdcan, uint32_t NonMatchingStd, uint32_t NonMatchingExt, uint32_t RejectRemoteStd, uint32_t RejectRemoteExt):决定如何处理未与过滤器匹配的数据帧,以及全部遥控帧。

  • NonMatchingStd:如何处理未与过滤器匹配的标准帧。
  • NonMatchingExt:如何处理未与过滤器匹配的扩展帧。
    上述两个参数的可选项有 FDCAN_ACCEPT_IN_RX_FIFO0FDCAN_ACCEPT_IN_RX_FIFO1FDCAN_REJECT,分别表示接收至 FIFO0、FIFO1 和拒绝。
  • RejectRemoteStd:是否拒绝标准格式的遥控帧。
  • RejectRemoteExt:是否拒绝扩展格式的遥控帧。
    上述两个参数的可选项有 FDCAN_FILTER_REMOTEFDCAN_REJECT_REMOTE,分别表示放入过滤器或拒绝。

HAL_FDCAN_ConfigFifoWatermark(FDCAN_HandleTypeDef *hfdcan, uint32_t FIFO, uint32_t Watermark):配置 FIFO 的水位线,当 FIFO 中消息量达到水位线(阈值)时,触发中断。

CAN 初始化(main.c)

1
2
3
4
5
6
/* USER CODE BEGIN 2 */
fdcan_filter_init(&hfdcan1); // 初始化过滤器
HAL_FDCAN_Start(&hfdcan1); // 启动!
HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_RX_FIFO0_WATERMARK, 0); // 开启 FIFO0 水位线中断
HAL_FDCAN_ActivateNotification(hfdcan, FDCAN_IT_BUS_OFF, 0); // 开启总线离线中断,可选
/* USER CODE END 2 */

消息接收

重写中断回调函数

接收的消息触发水位线中断时,会进入此函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs){
if (hfdcan == &hfdcan1)
{
FDCAN1RCV(&hfdcan1);
}
if (hfdcan == &hfdcan2)
{
// ...
}
if (hfdcan == &hfdcan3)
{
// ...
}
}

定义消息处理函数

这里提供两种形式,都可以使用。

1
2
3
4
5
6
7
8
void FDCAN1RCV(FDCAN_HandleTypeDef *hfdcan){
FDCAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8] = {0};
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &rx_header, rx_data) == HAL_OK) {
uint8_t id = rx_header.Identifier & 0xFF; // 根据需求修改
// ... rx_data数据处理
}
}

rx_data 这个数组就是数据段(Data Field)中的数据。

1
2
3
4
5
6
void FDCAN1RCV(FDCAN_HandleTypeDef *hfdcan){
uint16_t id;
uint8_t rx_data[8] = {0};
fdcanx_receive(&hfdcan2, &id, rx_data);
// ... rx_data数据处理
}

消息发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FDCAN_TxHeaderTypeDef fdcan_tx_header;
uint8_t fdcan_tx_data[8] = {0};

void FDCANSend(FDCAN_HandleTypeDef *hfdcan, uint16_t id, uint8_t* tx_data){
fdcan_tx_header.Identifier = id; // 报文ID
fdcan_tx_header.IdType = FDCAN_STANDARD_ID; // 帧格式
fdcan_tx_header.TxFrameType = FDCAN_DATA_FRAME; // 帧类型
fdcan_tx_header.DataLength = FDCAN_DLC_BYTES_8; // 数据长度

fdcan_tx_data[0] = tx_data[0];
fdcan_tx_data[1] = tx_data[1];
// ...fdcan_tx_data消息发送数组赋值

HAL_FDCAN_AddMessageToTxFifoQ(hfdcan, &fdcan_tx_header, fdcan_tx_data);
}

发送函数的传入参数根据实际情况修改,定义之后在需要发送消息时调用即可。

结语

至此,配置、应用 CAN 通信的流程就全部结束了,望本篇文章能让你有所收获,若有表述错误,恳请指正。