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

参数解释:
- Bit Timings Parameters【位时序参数】
- Prescaler (for Time Quantum):预分频器(用于时间份额 tq),对 CAN 内核时钟分频,生成基础时间单位 tq(CAN 时钟源为 APB1 外设时钟 36MHz)。选较大值=tq 变长、波特率降低;选较小值=tq 变短、波特率提高。
- Time Quantum:时间份额 (tq),CAN 位时序最小时间单位。它由内核时钟和预分频决定:预分频越大,tq 越长;预分频越小,tq 越短。
- Time Quanta in Bit Segment 1:位段 1 的 tq 个数(BS1,包含传播段 + 相位缓冲段 1,决定采样点位置)。选较大值=采样点后移,单 bit 时间变长;选较小值=采样点前移,单 bit 时间变短。
- Time Quanta in Bit Segment 2:位段 2 的 tq 个数(BS2,相位缓冲段 2)。选较大值=单 bit 时间变长、采样点比例相对前移;选较小值=单 bit 时间变短、采样点比例相对后移。
- Time for one Bit:单个 bit 总时长(SyncSeg 固定 1 tq + BS1 + BS2)。BS1/BS2 增大则单 bit 时间变长、波特率降低;减小则单 bit 时间变短、波特率提高。
- Baud Rate:波特率。由 tq 和单 bit 总 tq 数决定:Prescaler、BS1、BS2 增大会降低波特率;减小会提高波特率。
- ReSynchronization Jump Width:再同步跳转宽度 (SJW),总线同步时单次最多调整的 tq 数量。选较大值=允许更大的相位误差补偿;选较小值=同步更严格、补偿范围更小。
- Basic Parameters【基础参数】
- Time Triggered Communication Mode:时间触发通信模式(TTCAN,基于时间调度发送报文,汽车 AUTOSAR 常用)。选 Enable=启用时间触发模式,基于内部时间基准调度发送并记录时间戳;选 Disable=使用标准的基于事件触发的 CAN 通信模式。
- Automatic Bus-Off Management:自动 Bus-Off 管理。选 Enable=出错进入 Bus-Off 后硬件自动恢复,不需要软件干预;选 Disable=进入 Bus-Off 后需要软件手动清除标志并恢复。
- Automatic Wake-Up Mode:自动唤醒模式。选 Enable=休眠时检测到总线活动自动唤醒外设;选 Disable=不自动唤醒,需要软件介入或外部触发。
- Automatic Retransmission:自动重传。选 Enable=仲裁丢失/发送出错时硬件自动重试直至成功(会一直占用某个邮箱进行重发,不会自动释放);选 Disable=只发送一次,然后释放邮箱(单发模式/one-shot),失败不重发,避免阻塞总线。
- Receive Fifo Locked Mode:接收 Fifo 锁定模式。选 Enable=FIFO 满时锁定,不再覆盖旧报文(保护旧数据);选 Disable=覆盖最早报文(始终接收最新数据)。
- Transmit Fifo Priority:发送 Fifo 优先级。选 Enable=按 FIFO 入队顺序发送;选 Disable=按 CAN ID 仲裁优先级发送(通常 ID 越小优先级越高)。
- Advanced Parameters【高级参数】
- Operating Mode:工作模式。选 Normal=正常收发;选 Loopback=环回自测模式(发送帧在内部回送,不驱动引脚);选 Silent=静默模式(只听,不发送,不干扰总线);选 Silent_Loopback=静默+回环自测模式(环回且不影响总线)。
中断配置:

- USB high priority or CAN TX interrupts:CAN 发送报文完成/中止
- USB low priority or CAN RX0 interrupts:CAN FIFO0 接收报文
- CAN RX1 interrupt:CAN FIFO1 接收报文
- CAN SCE interrupt:状态变化与错误:Bus-Off、错误被动、错误计数超限、唤醒/睡眠事件
3. 代码编写
CubeMX 生成代码后,CAN 的过滤器和中断需要手动开启。
3.1 配置并使能过滤器
在 CAN 初始化后的 /* USER CODE BEGIN CAN_Init 2 */ 进行配置和使能 CAN 的过滤器。
/* CAN init function */
void MX_CAN_Init(void)
{
/* USER CODE BEGIN CAN_Init 0 */
/* USER CODE END CAN_Init 0 */
/* USER CODE BEGIN CAN_Init 1 */
/* USER CODE END CAN_Init 1 */
hcan.Instance = CAN1;
hcan.Init.Prescaler = 3;
hcan.Init.Mode = CAN_MODE_LOOPBACK;
hcan.Init.SyncJumpWidth = CAN_SJW_2TQ;
hcan.Init.TimeSeg1 = CAN_BS1_8TQ;
hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = ENABLE;
hcan.Init.AutoWakeUp = ENABLE;
hcan.Init.AutoRetransmission = ENABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN CAN_Init 2 */
CAN_FilterTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.FilterBank = 0;
/**
* @brief 四种过滤器
* 1. 16 位列表模式:四个参数分别存入一组 ID 即可
* 2. 16 位掩码模式:IdHigh 存入第一组 ID,MaskIdHigh 存入对应掩码;IdLow 存入第二组 ID,MaskIdLow 存入对应掩码
* 3. 32 位列表模式:IdHigh 和 IdLow 组合到一起,存入第一组 32 位 ID;MaskIdHigh 和 MaskIdLow 组合到一起,存入第二组 32 位 ID
* 4. 32 位掩码模式:IdHigh 和 IdLow 组合到一起,存入第一组 32 位 ID;MaskIdHigh 和 MaskIdLow 组合到一起,存入 对应掩码位
* 5. 全通模式:IdHigh 和 IdLow 都存入 0x0000,MaskIdHigh 和 MaskIdLow 都存入 0xFFFF,前提是 32 位掩码模式
*/
CAN_FilterInitStruct.FilterIdHigh = 0x0000;
CAN_FilterInitStruct.FilterIdLow = 0x0000;
CAN_FilterInitStruct.FilterMaskIdHigh = 0x0000;
CAN_FilterInitStruct.FilterMaskIdLow = 0x0000;
CAN_FilterInitStruct.FilterScale = CAN_FILTERSCALE_32BIT;
CAN_FilterInitStruct.FilterMode = CAN_FILTERMODE_IDMASK;
CAN_FilterInitStruct.FilterFIFOAssignment = CAN_FILTER_FIFO0;
CAN_FilterInitStruct.FilterActivation = ENABLE;
if (HAL_CAN_ConfigFilter(&hcan, &CAN_FilterInitStruct) != HAL_OK)
{
LOG_ERROR("CAN", "ConfigFilter failed");
Error_Handler();
}
/* USER CODE END CAN_Init 2 */
}
3.1 使能中断
使能过滤器后加上 CAN FIFO0 接收中断的使能:
/* 使能接收 FIFO 0 中断 */
if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
Error_Handler();
}
3.2 启动 CAN
中断使能后才能启动 CAN:
/* 启动 CAN 模块 */
if (HAL_CAN_Start(&hcan) != HAL_OK)
{
LOG_ERROR("CAN", "Start failed");
Error_Handler();
}
3.3 发送与接收代码编写
新建 app_can.c/h 文件后进行编写,这里接收部分代码使用了环形缓冲区的方法,后续会进行讲解。
#include "app_can.h"
#include "mylog.h"
#include "can.h"
#include <string.h>
#define CAN_RX_BUF_SIZE 16 // 软件接收缓冲区深度(帧),16 对单帧间隔收发足够
typedef struct
{
uint32_t ID; // 报文 ID(标准或扩展)
uint8_t Length; // 数据长度
uint8_t Data[8]; // 数据域
} CAN_FrameTypeDef;
static CAN_FrameTypeDef CAN_RxBuf[CAN_RX_BUF_SIZE]; // 环形缓冲区
static volatile uint8_t CAN_RxHead = 0; // 写指针,仅中断回调修改
static volatile uint8_t CAN_RxTail = 0; // 读指针,仅主循环修改
/**
* @brief 发送 CAN 消息
* @param ID 标准 ID 号
* @param Length 数据长度
* @param TxData 数据指针
*/
void CAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *TxData)
{
CAN_TxHeaderTypeDef TxMessage; // 发送消息结构体
uint32_t TxMailbox; // 接收返回存储 Tx 消息的邮箱号
/* 填充帧头信息 */
TxMessage.StdId = ID; // 标准 ID 号
TxMessage.ExtId = ID; // 扩展 ID 号
TxMessage.IDE = CAN_ID_STD; // 扩展 ID 标志位,选择标准 ID 类型
TxMessage.RTR = CAN_RTR_DATA; // 数据帧 & 遥控帧
TxMessage.DLC = Length; // 数据长度
/* 打印当前空闲邮箱数量 */
LOG_INFO("CAN", "Free Tx mailbox count: %lu", HAL_CAN_GetTxMailboxesFreeLevel(&hcan));
/* 将发送消息添加到发送邮箱队列并激活相应的传输请求 */
uint8_t result = HAL_CAN_AddTxMessage(&hcan, &TxMessage, TxData, &TxMailbox);
if (result != HAL_OK)
{
LOG_ERROR("CAN", "Add TxMessage to Mailbox or Activate transmission request Failed, return");
return;
}
LOG_INFO("CAN", "Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x%02X", TxMailbox);
}
/**
* @brief 检查软件接收缓冲区是否为空
* @param None
* @return uint8_t 1: 为空, 0: 不为空
*/
uint8_t CAN_RxFIFO_isEmpty(void)
{
if (CAN_RxHead == CAN_RxTail) // 软件缓冲区为空
{
LOG_WARN("CAN", "CAN RxFIFO is empty");
return 1;
}
LOG_INFO("CAN", "CAN RxFIFO not empty");
return 0;
}
/**
* @brief 接收 CAN 消息
* @param ID 标准 ID 号指针
* @param Length 数据长度指针
* @param RxData 数据指针
*/
void CAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *RxData)
{
CAN_FrameTypeDef *frame = &CAN_RxBuf[CAN_RxTail];
*ID = frame->ID;
*Length = frame->Length;
memcpy(RxData, frame->Data, frame->Length);
CAN_RxTail = (CAN_RxTail + 1) % CAN_RX_BUF_SIZE;
}
/**
* @brief FIFO0 收到报文的中断回调(中断上下文,禁止调用 mylog)
*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcanPtr)
{
CAN_RxHeaderTypeDef RxMessage;
CAN_FrameTypeDef frame;
/* 从 FIFO FIFO0 读取消息 */
if (HAL_CAN_GetRxMessage(hcanPtr, CAN_RX_FIFO0, &RxMessage, frame.Data) == HAL_OK)
{
frame.ID = (RxMessage.IDE == CAN_ID_STD) ? RxMessage.StdId : RxMessage.ExtId; // 读出标准 ID 号或扩展 ID 号
frame.Length = RxMessage.DLC; // 数据长度
uint8_t next = (CAN_RxHead + 1) % CAN_RX_BUF_SIZE;
if (next != CAN_RxTail) // 缓冲区未满才入队,满了丢弃 newest
{
CAN_RxBuf[CAN_RxHead] = frame;
CAN_RxHead = next;
}
}
}
3.4 环形缓冲区入队原理
基本设定:
生产者 = CAN 接收中断,只推进 H(写指针) ← 调用入队代码
消费者 = 主循环 CAN_Receive(),只推进 T(读指针) ← 调用出队代码
一格 = 一条完整 CAN 帧(不是 1 字节)
· = 空格子 F1、F2… = 已存入的帧 F1* = 已被取走、逻辑作废
流程图:
入队代码就三步,后面每张图都对应它:
next = (H + 1) % 8; // ① 探路:算出 H 的下一格(绕回就是它实现的)
if (next != T) { // ② 判满:下一格没顶到 T 才能写
buf[H] = frame; // ③a 先写数据
H = next; // ③b 后推指针(宣告这格就绪)
}
第 0 步:开机,缓冲区空
0 1 2 3 4 5 6 7
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ · │ · │ · │ · │ · │ · │ · │ · │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲
T=H=0 ← H == T,判定"空"
第 1 步:中断收到 F1(走一遍入队三步)
next = (0+1)%8 = 1 1 ≠ T(0) → 未满
写入 buf[0] → H=1
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F1 │ · │ · │ · │ · │ · │ · │ · │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
T=0 H=1
第 2 步:又连收 F2、F3(每次重复三步,H 每次走一格)
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F1 │ F2 │ F3 │ · │ · │ · │ · │ · │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
T=0 H=3
缓冲区现有帧数 = 从 T 数到 H 的前一格 = 3 帧
第 3 步:主循环取走 F1(消费者的动作,和入队互为镜像)
读 buf[0] 得到 F1 → T=1
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F1* │ F2 │ F3 │ · │ · │ · │ · │ · │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
T=1 H=3
F1 的字节还躺在内存里,但逻辑上已作废,[0] 号格可在下次写入时直接覆盖
第 4 步:中断连续收 F4~F8,写到末尾后绕回
依次写 buf[3]=F4 … buf[7]=F8,写完 buf[7] 时:
next = (7+1)%8 = 0 ← % 运算把指针接回数组开头,"环形"生效 → H=0
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F1* │ F2 │ F3 │ F4 │ F5 │ F6 │ F7 │ F8 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
H=0 T=1
此时存了 7 帧 = 8−1,已达上限。检查:next=(0+1)%8=1 == T(1) → 满。
第 5 步:再来 F9 —— 撞上满条件,丢弃
next = (0+1)%8 = 1 == T(1) → if 不成立 → F9 整帧丢弃
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F1* │ F2 │ F3 │ F4 │ F5 │ F6 │ F7 │ F8 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
H=0 T=1 ← H、T、格子内容全部原封不动
旧帧 F2~F8 全部保住,被扔掉的是最新的 F9。
第 6 步:主循环取走 F2,空位出现,写入恢复
读 buf[1] 得到 F2 → T=2
此时 next=(0+1)%8=1 ≠ T(2) → 未满 → F10 可以入队:写 buf[0],H=1
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ F10 │ F2* │ F3 │ F4 │ F5 │ F6 │ F7 │ F8 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲ ▲
H=1 T=2
注意 F10 恰好覆盖了最早作废的 F1* 那一格——空位就是这么复用的。
3.5 主函数调用
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_CAN_Init();
/* USER CODE BEGIN 2 */
LOG_INFO("APP", "APP Boot ============================================================================");
LOG_INFO("SYSTEM", "main() user init OK");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(1000);
LOG_INFO("SYSTEM", "running");
CAN_Transmit(0x123, 8, (uint8_t *)"Hello!");
if (!CAN_RxFIFO_isEmpty())
{
uint32_t ID = 0;
uint8_t Length = 0;
uint8_t RxData[8] = {0};
CAN_Receive(&ID, &Length, RxData);
LOG_INFO("CAN", "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]);
}
CAN_ProcessEvents();
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
4. 串口打印消息
[I][APP] APP Boot ============================================================================
[I][SYSTEM] main() user init OK
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
...
博客导航
本文来自博客园,作者:膝盖中箭卫兵,转载请注明原文链接:https://www.cnblogs.com/hai-feng-dev/p/23509638

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