【双 MCU 架构车载仪表终端(CAN 互联)】STM32H7 FDCAN classic 模式回环收发测试
1. 引言
在进行双机互发之前,必须单机本地进行回环测试,以确保发送和接收数据没有问题。
2. FDCAN CubeMX 配置
基础参数配置:

参数解释:
Basic Parameters【基础参数】
- Frame Format:帧格式。选 Classic=传统 CAN 2.0,数据场最长 8 字节;选 FD mode=数据场最长 64 字节,全帧速率一致;选 FD with BRS=数据段可切换到更高速率(通常所说的 CAN FD)。
- Mode:工作模式。选 Internal LoopBack=发送帧在芯片内部直接回送接收通路,不驱动 PB8/PB9 引脚,无需外部收发器,适合自测;选 Normal=正常收发;选 Restricted=受限模式;选 Monitor=只听模式,仅接收不发送;选 External LoopBack=经引脚和外部收发器回环。
- Auto Retransmission:自动重传。选 Disable=单次发送(one-shot),仲裁丢失或出错后不再重发,可避免阻塞总线;选 Enable=硬件自动重发直至成功(CAN 默认行为)。
- Transmit Pause:发送暂停(CCCR.TP 位)。选 Enable=连续发送的两帧之间强制插入 2 个数据位时间的停顿,防止单节点霸占总线;选 Disable=不插入停顿,连续发送。
- Protocol Exception:协议异常处理(CCCR.PXHD 位)。选 Enable=针对 CAN FD 数据段格式违规按 ISO 规范判为严重错误并进入 Bus-Off;选 Disable=不因此进入 Bus-Off;Classic 模式下影响不大。
- Message Ram Offset:本实例在 FDCAN 消息 RAM 中的起始偏移(字节,HAL 允许 0~2560)。选 0=使用默认起始偏移;选非 0=指定起始偏移,多个 FDCAN 实例同时激活时用来划分互不重叠的 RAM 区;偏移不当可能与其他实例冲突或越界。
- Std Filters Nbr:标准 ID 过滤器元素个数(0~128)。选 0=没有标准 ID 匹配入口(M_CAN 架构下报文必须命中过滤器才会进 Rx FIFO/Buffer);选大于 0=分配相应数量的标准 ID 过滤器,数量越多可配置的匹配规则越多,RAM 占用也越大。
- Ext Filters Nbr:扩展 ID(29 位)过滤器个数(0~64)。选 0=没有扩展 ID 匹配入口;选大于 0=分配相应数量的扩展 ID 过滤器,数量越多可配置的匹配规则越多,RAM 占用也越大。
- RxFifo0 Elmts Nbr:接收 FIFO0 的元素(槽位)个数(0~64)。选 0=未启用 FIFO0;选大于 0=启用并设置可缓存帧数,数值越大可缓存帧数越多,RAM 占用越大。
- RxFifo0 Elmt Size:FIFO0 每个元素的数据场长度。选 8 字节=正好容纳 Classic 帧;选更大=可容纳更长的 CAN FD 数据场,RAM 占用增加;长度不足时无法完整容纳长帧数据。
- RxFifo1 Elmts Nbr:第二个接收 FIFO 的元素个数。选 0=未启用 FIFO1;选大于 0=启用并设置可缓存帧数,数值越大缓存能力越强,RAM 占用越大。
- RxFifo1 Elmt Size:FIFO1 每个元素的数据场长度。选 8 字节=适合 Classic 帧;选更大=适合 CAN FD 长帧,RAM 占用增加。
- Rx Buffers Nbr:专用接收缓冲区个数(0~64)。选 0=未启用专用接收缓冲区;选大于 0=启用固定位置的专用接收缓冲,由过滤器指定投递目标,便于覆盖写,数量越多可分配的专用目标越多。
- Rx Buffer Size:专用接收缓冲区元素长度。选 8 字节=适合 Classic 帧;选更大=适合 CAN FD 长帧,RAM 占用增加。
- Tx Events Nbr:发送事件 FIFO 元素个数(0~32)。选 0=未启用发送事件回执;选大于 0=记录每帧发送完成的回执(ID、时间戳等),数值越大可保存的完成事件越多。
- Tx Buffers Nbr:专用发送缓冲区个数。选 0=未启用专用发送缓冲区;选大于 0=启用可单独取消/更新的发送缓冲,数量越多可管理的独立发送缓冲越多。
- Tx Fifo Queue Elmts Nbr:发送 FIFO/队列的元素个数(0~32)。选 0=未启用发送队列;选大于 0=设置发送队列深度,数值越大最多可排队的待发帧越多。
- Tx Fifo Queue Mode:发送排队策略。选 FIFO mode=先提交先发送;选 Priority Queue=按报文 ID 优先级高低发送(通常 ID 越小优先级越高)。
- Tx Elmt Size:发送元素的数据场长度。选 8 字节=足够 Classic 帧使用;选更大=可发送更长的 CAN FD 数据场,RAM 占用增加。
Clock Calibration Unit【时钟校准单元】
- Clock Calibration:时钟校准单元(CCU)。选 Enable=在 CAN FD 高速数据段测量收发位速率偏差并做时钟补偿;选 Disable=不进行补偿;Classic 模式一般用不到,保持 Disable 即可。
Bit Timings Parameters【位时序参数】
- Prescaler (for Time Quantum):预分频器(用于时间份额 tq),对 CAN 内核时钟分频,生成基础时间单位 tq。选较大值=tq 变长,波特率降低;选较小值=tq 变短,波特率提高。
- Time Quantum:时间份额(tq),CAN 位时序最小时间单位。它由内核时钟和预分频决定:预分频越大,tq 越长;预分频越小,tq 越短。
- Nominal Time Seg1:位段1的 tq 个数(BS1,包含传播段 + 相位缓冲段1),决定采样点位置。选较大值=采样点后移,单 bit 时间变长;选较小值=采样点前移,单 bit 时间变短。
- Nominal Time Seg2:位段2的 tq 个数(BS2,相位缓冲段2)。选较大值=单 bit 时间变长、采样点比例相对前移;选较小值=单 bit 时间变短、采样点比例相对后移。
- Nominal Time for one Bit:单个 bit 总时长(SyncSeg 固定 1 tq + Seg1 + Seg2)。Seg1/Seg2 增大则单 bit 时间变长、波特率降低;减小则单 bit 时间变短、波特率提高。
- Nominal Baud Rate:标称波特率。由 tq 和单 bit 总 tq 数决定:Prescaler、Seg1、Seg2 增大会降低波特率;减小会提高波特率。
- Nominal Sync Jump Width:再同步跳转宽度(SJW),总线同步时单次最多调整的 tq 数量。选较大值=允许更大的相位误差补偿;选较小值=同步更严格、补偿范围更小。
- Data Prescaler:数据段预分频器(CAN FD 数据段/BRS 高速段使用)。选较大值=数据段 tq 变长、数据段速率降低;选较小值=数据段 tq 变短、数据段速率提高;Classic 模式下不生效。
- Data Time Seg1:数据段位段1的 tq 个数。选较大值=数据段采样点后移、数据段单 bit 时间变长;选较小值=采样点前移、单 bit 时间变短;Classic 模式下不生效。
- Data Time Seg2:数据段位段2的 tq 个数。选较大值=数据段单 bit 时间变长、采样点比例相对前移;选较小值=单 bit 时间变短、采样点比例相对后移;Classic 模式下不生效。
- Data Sync Jump Width:数据段再同步跳转宽度。选较大值=数据段允许更大的再同步调整;选较小值=数据段同步更严格、调整范围更小;Classic 模式下不生效。
中断配置:

- FDCAN1 interrupt 0:中断线 0。M_CAN 只有 2 根中断输出线,内部几十个事件源(Rx FIFO0/1 非空、发送完成、错误、总线状态切换等)通过中断线选择寄存器分组路由到这两根线(HAL 中用 HAL_FDCAN_ConfigInterruptLines)
- FDCAN1 interrupt 1:中断线 1,承接第二组事件,与线 0 配合实现收/发或高/低优先级事件分离
- FDCAN calibration unit interrupt:时钟校准单元(CCU)的中断,仅在 Clock Calibration 启用时有意义
3. 代码编写
CubeMX 生成代码后,CAN 的过滤器(特别注意,不使能相当于全阻)、中断以及 FDCAN 需要手动使能。
3.1 FDCAN 过滤器配置与使能
在 FDCAN 初始化后的 /* USER CODE BEGIN FDCAN1_Init 2 */ 进行配置和使能 FDCAN 的过滤器。
/* FDCAN1 init function */
void MX_FDCAN1_Init(void)
{
/* USER CODE BEGIN FDCAN1_Init 0 */
/* USER CODE END FDCAN1_Init 0 */
/* USER CODE BEGIN FDCAN1_Init 1 */
/* USER CODE END FDCAN1_Init 1 */
hfdcan1.Instance = FDCAN1;
hfdcan1.Init.FrameFormat = FDCAN_FRAME_CLASSIC;
hfdcan1.Init.Mode = FDCAN_MODE_NORMAL;
hfdcan1.Init.AutoRetransmission = ENABLE;
hfdcan1.Init.TransmitPause = DISABLE;
hfdcan1.Init.ProtocolException = ENABLE;
hfdcan1.Init.NominalPrescaler = 10;
hfdcan1.Init.NominalSyncJumpWidth = 2;
hfdcan1.Init.NominalTimeSeg1 = 8;
hfdcan1.Init.NominalTimeSeg2 = 3;
hfdcan1.Init.DataPrescaler = 1;
hfdcan1.Init.DataSyncJumpWidth = 1;
hfdcan1.Init.DataTimeSeg1 = 1;
hfdcan1.Init.DataTimeSeg2 = 1;
hfdcan1.Init.MessageRAMOffset = 0;
hfdcan1.Init.StdFiltersNbr = 0;
hfdcan1.Init.ExtFiltersNbr = 0;
hfdcan1.Init.RxFifo0ElmtsNbr = 8;
hfdcan1.Init.RxFifo0ElmtSize = FDCAN_DATA_BYTES_8;
hfdcan1.Init.RxFifo1ElmtsNbr = 0;
hfdcan1.Init.RxFifo1ElmtSize = FDCAN_DATA_BYTES_8;
hfdcan1.Init.RxBuffersNbr = 0;
hfdcan1.Init.RxBufferSize = FDCAN_DATA_BYTES_8;
hfdcan1.Init.TxEventsNbr = 0;
hfdcan1.Init.TxBuffersNbr = 1;
hfdcan1.Init.TxFifoQueueElmtsNbr = 0;
hfdcan1.Init.TxFifoQueueMode = FDCAN_TX_QUEUE_OPERATION;
hfdcan1.Init.TxElmtSize = FDCAN_DATA_BYTES_8;
if (HAL_FDCAN_Init(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN FDCAN1_Init 2 */
/* 全局过滤器:非匹配标准/扩展帧收进 RX FIFO0,远程帧拒收 */
if (HAL_FDCAN_ConfigGlobalFilter(&hfdcan1,
FDCAN_ACCEPT_IN_RX_FIFO0,
FDCAN_ACCEPT_IN_RX_FIFO0,
FDCAN_REJECT,
FDCAN_REJECT) != HAL_OK)
{
Error_Handler();
}
/* USER CODE END FDCAN1_Init 2 */
}
3.2 使能中断
使能过滤器后加上 FDCAN FIFO0 接收新消息中断的使能:
if (HAL_FDCAN_ActivateNotification(&hfdcan1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK)
{
Error_Handler();
}
3.3 使能 FDCAN 外设
使能中断后使能 FDCAN 外设,启动 FDCAN:
if (HAL_FDCAN_Start(&hfdcan1) != HAL_OK)
{
Error_Handler();
}
3.4 发送与接收代码编写
新建 app_can.c/h 文件后进行编写,这里接收部分代码使用了环形缓冲区的方法,详细原理可见:
【双 MCU 架构车载仪表终端(CAN 互联)】STM32F1 CAN 回环收发测试
#include "app_can.h"
#include "mylog.h"
#include "fdcan.h"
#include "FreeRTOS.h"
#include "task.h"
#include "cmsis_os2.h"
#include <string.h>
#define FDCAN_RX_BUF_SIZE 16 // 软件接收缓冲区深度(帧),16 对单帧间隔收发足够
typedef struct
{
uint32_t ID; // 报文 ID(标准或扩展)
uint8_t Length; // 数据长度
uint8_t Data[8]; // 数据域
} CAN_FrameTypeDef;
static CAN_FrameTypeDef FDCAN_RxBuf[FDCAN_RX_BUF_SIZE]; // 环形缓冲区
static volatile uint8_t FDCAN_RxHead = 0; // 写指针,仅中断回调修改
static volatile uint8_t FDCAN_RxTail = 0; // 读指针,仅主循环修改
static void FDCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *TxData)
{
FDCAN_TxHeaderTypeDef TxMassage;
TxMassage.BitRateSwitch = FDCAN_BRS_OFF;
TxMassage.DataLength = Length;
TxMassage.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
TxMassage.FDFormat = FDCAN_CLASSIC_CAN;
TxMassage.Identifier = ID;
TxMassage.IdType = FDCAN_STANDARD_ID;
TxMassage.MessageMarker = 0xAA;
TxMassage.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
TxMassage.TxFrameType = FDCAN_DATA_FRAME;
/* 将消息添加到专用Tx缓冲区 */
if (HAL_FDCAN_AddMessageToTxBuffer(&hfdcan1, &TxMassage, TxData, FDCAN_TX_BUFFER0) != HAL_OK)
{
/* 发送失败:多半是 Tx FIFO 满(8 元素),直接返回 */
LOG_ERROR("FDCAN", "AddMessageToTxBuffer failed");
return;
}
LOG_INFO("FDCAN", "AddMessageToTxBuffer success");
/* 启用传输请求 */
if (HAL_FDCAN_EnableTxBufferRequest(&hfdcan1, FDCAN_TX_BUFFER0) != HAL_OK)
{
LOG_ERROR("FDCAN", "EnableTxBufferRequest failed");
return;
}
LOG_INFO("FDCAN", "EnableTxBufferRequest success");
}
/**
* @brief 检查软件接收缓冲区是否为空
* @return uint8_t 1: 为空, 0: 不为空
*/
static uint8_t FDCAN_FIFO_isEmpty(void)
{
if (FDCAN_RxHead == FDCAN_RxTail) // 软件缓冲区为空
{
LOG_WARN("FDCAN", "FIFO is empty");
return 1;
}
LOG_INFO("FDCAN", "FIFO is not empty");
return 0;
}
/**
* @brief 接收 CAN 消息
* @param ID 标准 ID 号指针
* @param Length 数据长度指针
* @param RxData 数据指针
*/
static void FDCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *RxData)
{
CAN_FrameTypeDef *frame = &FDCAN_RxBuf[FDCAN_RxTail];
*ID = frame->ID;
*Length = frame->Length;
memcpy(RxData, frame->Data, frame->Length);
FDCAN_RxTail = (FDCAN_RxTail + 1) % FDCAN_RX_BUF_SIZE;
}
/**
* @brief FIFO0 收到报文的中断回调
*/
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs)
{
FDCAN_RxHeaderTypeDef RxMessage;
CAN_FrameTypeDef frame;
if (hfdcan->Instance == FDCAN1)
{
switch (RxFifo0ITs)
{
case FDCAN_IT_RX_FIFO0_MESSAGE_LOST: // 报文丢失事件
break;
case FDCAN_IT_RX_FIFO0_FULL: // FIFO 满事件
break;
case FDCAN_IT_RX_FIFO0_WATERMARK: // 水位事件
break;
case FDCAN_IT_RX_FIFO0_NEW_MESSAGE: // 新报文事件
/* 从 Rx FIFO 0 读取消息 */
if (HAL_FDCAN_GetRxMessage(hfdcan, FDCAN_RX_FIFO0, &RxMessage, frame.Data) == HAL_OK)
{
frame.ID = RxMessage.Identifier; // ID 已是纯数值,不用再按 IDE 挑 StdId/ExtId
frame.Length = RxMessage.DataLength; // 经典帧 0~8 字节时 DLC 码即字节数
uint8_t next = (FDCAN_RxHead + 1) % FDCAN_RX_BUF_SIZE;
if (next != FDCAN_RxTail) // 缓冲区未满才入队,满了丢弃 newest
{
FDCAN_RxBuf[FDCAN_RxHead] = frame;
FDCAN_RxHead = next;
}
}
break;
default:
break;
}
}
}
3.5 RTOS 回环收发任务创建
创建 FDCAN 任务,以测试 FDCAN 的回环收发:
/**
* @brief CAN 任务线程
* @param pvParameters 任务参数指针,本例中未使用
*/
static void FDCAN_Thread(void *pvParameters)
{
for (;;)
{
FDCAN_Transmit(0x123, 8, (uint8_t *)"Hello!");
osDelay(1000);
if (!FDCAN_FIFO_isEmpty())
{
uint32_t ID = 0;
uint8_t Length = 0;
uint8_t RxData[8] = {0};
FDCAN_Receive(&ID, &Length, RxData);
LOG_INFO("FDCAN", "ID: %d, Length: %d, Data: %02X %02X %02X %02X %02X %02X %02X %02X",
ID, Length,
RxData[0],
RxData[1],
RxData[2],
RxData[3],
RxData[4],
RxData[5],
RxData[6],
RxData[7]);
}
}
}
/**
* @brief 启动 CAN 任务线程
*/
void FDCAN_Thread_Start(void)
{
BaseType_t xReturn = pdPASS;
xReturn = xTaskCreate((TaskFunction_t)FDCAN_Thread,
(char *)"FDCAN_Thread",
(uint16_t)1024,
(void *)NULL,
(uint8_t)5,
(TaskHandle_t *)NULL);
if (xReturn != pdPASS)
{
LOG_ERROR("FDCAN", "task create failed");
}
}
4. 串口打印消息
[I][FDCAN] AddMessageToTxBuffer success
[I][FDCAN] EnableTxBufferRequest success
[I][FDCAN] FIFO is not empty
[I][FDCAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][FDCAN] AddMessageToTxBuffer success
[I][FDCAN] EnableTxBufferRequest success
[I][FDCAN] FIFO is not empty
[I][FDCAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][FDCAN] AddMessageToTxBuffer success
[I][FDCAN] EnableTxBufferRequest success
[I][FDCAN] FIFO is not empty
[I][FDCAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/hai-feng-dev/p/23517866

浙公网安备 33010602011771号
https://orcid.org/0000-0001-5102-772X