【CAN总线】STM32 的 CAN 总线通信开发笔记(基于 HAL)
目录
- CAN 总线简介
- 波特率计算与误差分析(含 9.735K 与 10K 差异)
- 初始化配置详解
- CAN 模式说明
- CAN 滤波器原理与配置
- 消息发送流程
- 接收机制(FIFO 与中断)
- 源代码与调试技巧
1. ✅ CAN 总线简介
🔸 CAN 是什么?
- CAN(Controller Area Network) 是一种 多主机、面向消息 的通信协议,最早由德国 Bosch 公司为汽车电子开发,现广泛应用于工业自动化、电梯、医疗、机器人等领域。
🔸 CAN 报文结构:
每条 CAN 消息包含以下主要字段:
字段 | 含义 |
---|---|
ID | 报文标识符:决定优先级(ID 越小优先级越高) |
DLC | Data Length Code:表示数据区长度(0~8 字节) |
DATA | 有效负载:最多 8 字节的数据 |
🔸 差分通信原理(CAN_H / CAN_L):
CAN 使用 差分信号 进行通信,有两根线:
线名 | 正常值 | 主动发送“显性”时 | 主动发送“隐性”时 |
---|---|---|---|
CAN_H | ~2.5V | 拉高到 ~3.5V | 保持 2.5V |
CAN_L | ~2.5V | 拉低到 ~1.5V | 保持 2.5V |
✅ 特性:
- 抗干扰能力强:差分信号可抵消共模干扰
- 无需接地基准:因为只看两线间电压差
- 远距离通信稳定:典型可达 40 米(1 Mbps)~ 1 km(50 kbps)
显性(Dominant) vs 隐性(Recessive):
状态 | CAN_H – CAN_L 差值 | 作用 |
---|---|---|
显性位(Dominant) | ~2V | 用于强制总线为 0(主导) |
隐性位(Recessive) | 0V | 表示总线为 1(弱化) |
如果两个节点同时发送:一个发“0”(显性),一个发“1”(隐性),最终总线呈现“0” → 实现自动仲裁!
🔸 总线拓扑与终端电阻
-
CAN 总线为典型的 双绞线总线拓扑
-
两端各有一个 120Ω终端电阻,用于:
- 匹配阻抗,防止反射
- 稳定电平,增强抗干扰
-
终端电阻 必须存在,不能省略
2. 📐 波特率计算与误差分析
波特率计算公式:
CAN 波特率=fAPB1Prescaler×(1+BS1+BS2) \text{CAN 波特率} = \frac{f_{\text{APB1}}}{\text{Prescaler} \times (\text{1} + \text{BS1} + \text{BS2})} CAN 波特率=Prescaler×(1+BS1+BS2)fAPB1
示例:
APB1 = 48 MHz
Prescaler = 3
BS1 = 12TQ
,BS2 = 2TQ
波特率=48MHz3×(1+12+2)=48MHz45≈1.0667Mbps \text{波特率} = \frac{48 \text{MHz}}{3 \times (1 + 12 + 2)} = \frac{48 \text{MHz}}{45} ≈ 1.0667 \text{Mbps} 波特率=3×(1+12+2)48MHz=4548MHz≈1.0667Mbps
可以用此公式计算 10Kbps、9.735Kbps 等配置。
📌 重要:波特率偏差与通信不对称问题
实测现象解释:
发送端 | 接收端 | 能否通信 | 原因 |
---|---|---|---|
9.735K | 10K | ✅ | 接收端采样点早,容忍慢速发 |
10K | 9.735K | ❌ | 接收端采样点晚,采不到位变 |
- CAN 控制器能容忍一定的波特率误差(一般 ±1.5%~3%)
- 误差在发送快、接收慢时更容易出错
- 实际项目中应确保所有节点波特率一致!
3. 🧱 初始化配置详解
初始化在 MX_CAN1_Init()
中完成,关键结构体为:
hcan1.Init.Prescaler = 3;
hcan1.Init.Mode = CAN_MODE_NORMAL;
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_12TQ;
hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;
参数含义:
参数名 | 说明 |
---|---|
Prescaler | 分频器,影响总线波特率 |
Mode | 工作模式(NORMAL / LOOPBACK / SILENT) |
SyncJumpWidth (SJW) | 同步跳转宽度,建议设置为 1 |
TimeSeg1 | 比特前段采样点(数据采样位于 Bit 的后1/2~3/4) |
TimeSeg2 | 比特后段,用于同步 |
建议:保持 (1 + BS1 + BS2)
在 8~25TQ 之间,系统更稳定。
4. ⚙️ 模式(CAN_Mode
)—— CAN的运行方式
模式 | 枚举值 | 描述 | 是否发送 | 是否接收 | 是否驱动CAN总线 | 典型用途 |
---|---|---|---|---|---|---|
正常模式 | CAN_MODE_NORMAL | 标准工作模式,支持发送与接收 | ✅ 是 | ✅ 是 | ✅ 是 | 实际通信使用 |
回环模式 | CAN_MODE_LOOPBACK | 自发自收,控制器将发送数据直接回收到自身的接收缓冲区,不发送到总线 | ✅ 是 | ✅ 是 | ❌ 否 | 单机调试、发送逻辑测试 |
静默模式 | CAN_MODE_SILENT | 只监听总线上的数据,不主动发送或响应,且不会干扰总线 | ❌ 否 | ✅ 是 | ❌ 否 | 被动监听,调试总线或与未知设备共存 |
静默+回环模式 | CAN_MODE_SILENT_LOOPBACK | 回环 + 静默,完全断开物理总线,仅内部测试 | ✅ 是 | ✅ 是 | ❌ 否 | 回环测试,不接触外部,适合自动化测试 |
📝 模式使用建议:
- 调试阶段:可使用
LOOPBACK
模式验证发送与接收逻辑,确认程序框架正确; - 监听设备:例如日志记录器/监控工具,可用
SILENT
模式无干扰地接入总线; - 真实通信:正式运行时必须使用
NORMAL
模式才能与其他节点通信; - 复杂测试:若需断开物理总线测试通信流程,用
SILENT_LOOPBACK
模式最安全。
❗ 注意事项:
LOOPBACK
和SILENT_LOOPBACK
模式下 不会产生物理电平变化,示波器上看不到 CAN 波形;SILENT
模式下设备不可主动发送,即使调用发送函数也不会生效;- 切换为
NORMAL
模式前,必须确保波特率、滤波器、邮箱、FIFO 等配置无误。
5. 🧊 CAN 滤波器(Filter)
📌 目的:
CAN 总线是多主广播机制,所有节点都能看到所有消息。滤波器的作用是:
- 筛选出感兴趣的消息(指定 ID);
- 屏蔽无关的数据帧,减轻 MCU 中断负担和内存开销;
- 支持多个滤波器组(F1xx 系列通常为 14 个);
🧱 配置结构体(CAN_FilterTypeDef
)
成员 | 含义 | 说明 |
---|---|---|
FilterBank | 滤波器编号 | 取值范围 0 ~ N(最多 28 个,取决于芯片) |
FilterMode | 模式 | 掩码模式(IDMASK)或列表模式(IDLIST) |
FilterScale | 尺寸 | 16位模式 or 32位模式 |
FilterIdHigh/Low | 滤波 ID | 高/低 16 位;取决于扩展/标准帧 |
FilterMaskIdHigh/Low | 滤波掩码 | 与 FilterId 比较的屏蔽位 |
FilterFIFOAssignment | 分配到哪个 FIFO | FIFO0 或 FIFO1 |
FilterActivation | 激活开关 | 启用或禁用该滤波器 |
SlaveStartFilterBank | 用于双 CAN 的分界线 | F4/H7 系列特有 |
🧭 两种模式详解
1️⃣ 掩码模式(CAN_FILTERMODE_IDMASK
)
作用:按照 ID 的某些位进行匹配(常用于 ID 分段匹配)
-
匹配规则:
接收ID & Mask == FilterID & Mask
-
掩码为
0
:表示该位“忽略”; -
掩码为
1
:表示该位“必须精确匹配”; -
适合批量匹配某个范围的 ID
✅ 示例:接收所有帧(“全开”滤波器)
FilterIdHigh = 0x0000;
FilterMaskIdHigh = 0x0000; // 所有位都“忽略” → 接收所有帧
✅ 示例:只接收 ID 为 0x321 的标准帧
FilterIdHigh = 0x321 << 5; // 标准 ID 左移 5 位
FilterMaskIdHigh = 0xFFE0; // 只比较前 11 位
2️⃣ 列表模式(CAN_FILTERMODE_IDLIST
)
作用:只接收 特定 的几个 ID,不考虑掩码
- 结构体中 ID 和 Mask 字段被看作 4 个完整 ID 的列表(如果是 16-bit 模式就是 4 个,32-bit 模式就是 2 个);
- 适合精确接收多个确定 ID(非范围)
✅ 示例:只接收 0x100 和 0x200
FilterMode = IDLIST;
FilterIdHigh = 0x100 << 5;
FilterMaskIdHigh = 0x200 << 5;
📏 滤波器位移说明(标准帧 vs 扩展帧)
- 标准帧(11位 ID):要左移 5 位后写入
FilterIdHigh
- 扩展帧(29位 ID):要左移 3 位后拆分成高 16/低 16,写入
FilterIdHigh/Low
🧪 滤波器配置完整示例(接收所有 ID)
CAN_FilterTypeDef canFilter;
canFilter.FilterBank = 0;
canFilter.FilterMode = CAN_FILTERMODE_IDMASK;
canFilter.FilterScale = CAN_FILTERSCALE_32BIT;
canFilter.FilterIdHigh = 0x0000;
canFilter.FilterIdLow = 0x0000;
canFilter.FilterMaskIdHigh = 0x0000;
canFilter.FilterMaskIdLow = 0x0000;
canFilter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
canFilter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan1, &canFilter);
📎 FIFO 分配与使用
-
每个滤波器必须明确分配到
CAN_FILTER_FIFO0
或FIFO1
-
接收中断也分 FIFO 注册(
HAL_CAN_ActivateNotification()
)HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
🚦 注意事项
- 滤波器必须在
HAL_CAN_Start()
之前配置; - 若接收不到消息,优先检查滤波器是否太严;
- 同时开启多个滤波器时,
FilterBank
编号不可重复; - 多路接收时可以将不同的 ID 分配给 FIFO0 / FIFO1,便于分类处理。
6. 📤 消息发送流程
CAN_TxHeaderTypeDef txHeader;
HAL_CAN_AddTxMessage(&hcan1, &txHeader, data, &txMailbox);
- 使用三个邮箱(Tx Mailbox)
- 可用
HAL_CAN_GetTxMailboxesFreeLevel()
查看空闲邮箱数 DLC
为实际发送数据长度
7. 📥 接收逻辑详解(FIFO + 中断)
🔸 什么是 FIFO?
-
FIFO:First In First Out(先进先出)缓冲区
-
STM32 的 CAN 控制器内置两个硬件 FIFO:
FIFO0
(接收队列0)FIFO1
(接收队列1)
-
每个 FIFO 最多缓存 3 个完整 CAN 帧(包括 ID、DLC、数据等)
-
当接收到的帧匹配某个滤波器,就被分配到相应 FIFO 中。
🔸 FIFO0 与 FIFO1 的区别?
特性 | FIFO0 | FIFO1 |
---|---|---|
可使用中断 | ✅ CAN_IT_RX_FIFO0_MSG_PENDING | ✅ CAN_IT_RX_FIFO1_MSG_PENDING |
可使用回调函数 | ✅ HAL_CAN_RxFifo0MsgPendingCallback | ✅ HAL_CAN_RxFifo1MsgPendingCallback |
默认选择 | 默认使用 FIFO0(除非滤波器指定使用 FIFO1) | |
目的 | 可以按需分组消息来源、优先级或协议功能,分别放入 FIFO0/1 |
✅ FIFO0 与 FIFO1 可以同时使用!
可以设置不同的滤波器,将不同 ID 的报文路由到不同的 FIFO,提升任务并行性与可维护性。
🔸 使用中断接收的推荐方式:
-
启用中断:
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
-
实现回调函数(FIFO0 示例):
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {CAN_RxHeaderTypeDef rxHeader;uint8_t rxData[8];HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);// 处理数据printf("Recv ID=0x%X DLC=%d Data=...", rxHeader.StdId, rxHeader.DLC); }
-
好处:
- 不需要轮询接收(节省 CPU 资源)
- 多个帧自动缓存到 FIFO,防止漏帧
- 响应及时可靠
🔸 所有 CAN 中断说明:
STM32 中与 CAN 相关的 IRQ 一共四种,使用时建议根据用途精细启用:
中断向量名称 | 描述 | 用途 | 是否必须 |
---|---|---|---|
CAN1_TX_IRQn | 发送邮箱中断 | 发送完成 / 可用时触发 | 可选,若需处理发送确认 |
CAN1_RX0_IRQn | FIFO0 接收中断 | 接收到帧(匹配滤波器并进入 FIFO0) | ✅ 推荐开启 |
CAN1_RX1_IRQn | FIFO1 接收中断 | 同上,针对 FIFO1 | 如使用 FIFO1,则必须 |
CAN1_SCE_IRQn | 状态变化与错误中断(SCE) | 错误帧、仲裁失败、总线关闭等 | 可选,适合诊断问题 |
若只用 FIFO0 接收,可以仅启用
CAN1_RX0_IRQn
和CAN_IT_RX_FIFO0_MSG_PENDING
。其余中断可用HAL_NVIC_DisableIRQ(...)
关闭,以节省资源。
🔸 HAL 中断控制常用函数:
函数 | 功能 |
---|---|
HAL_NVIC_EnableIRQ(IRQn_Type IRQn) | 启用指定中断向量 |
HAL_NVIC_DisableIRQ(IRQn_Type IRQn) | 禁用指定中断 |
__HAL_CAN_ENABLE_IT() | 启用 CAN 中的特定中断标志位(如 RX、TX) |
__HAL_CAN_CLEAR_FLAG() | 清除中断标志 |
✅ 示例配置(启用 FIFO0 接收中断):
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);
如果想使用 FIFO1 接收,记得:
- 将对应滤波器的
FilterFIFOAssignment
设置为CAN_FILTER_FIFO1
- 同时使能
CAN_IT_RX_FIFO1_MSG_PENDING
- 实现
HAL_CAN_RxFifo1MsgPendingCallback(...)
8. 🛠️ 源码与调试技巧
can.c:
/* USER CODE BEGIN Header */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "can.h"/* USER CODE BEGIN 0 */
#include "stdio.h" // printf用
void CAN1_Start(void);
void CAN1_ConfigFilter(void);
/* USER CODE END 0 */CAN_HandleTypeDef hcan1;/* CAN1 init function */
void MX_CAN1_Init(void)
{/* USER CODE BEGIN CAN1_Init 0 *//* USER CODE END CAN1_Init 0 *//* USER CODE BEGIN CAN1_Init 1 *//* USER CODE END CAN1_Init 1 */hcan1.Instance = CAN1;hcan1.Init.Prescaler = 3;hcan1.Init.Mode = CAN_MODE_NORMAL;hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;hcan1.Init.TimeSeg1 = CAN_BS1_12TQ;hcan1.Init.TimeSeg2 = CAN_BS2_2TQ;hcan1.Init.TimeTriggeredMode = DISABLE;hcan1.Init.AutoBusOff = ENABLE;hcan1.Init.AutoWakeUp = DISABLE;hcan1.Init.AutoRetransmission = ENABLE;hcan1.Init.ReceiveFifoLocked = DISABLE;hcan1.Init.TransmitFifoPriority = DISABLE;if (HAL_CAN_Init(&hcan1) != HAL_OK){Error_Handler();}/* USER CODE BEGIN CAN1_Init 2 */CAN1_ConfigFilter();CAN1_Start();/* USER CODE END CAN1_Init 2 */}void HAL_CAN_MspInit(CAN_HandleTypeDef* canHandle)
{GPIO_InitTypeDef GPIO_InitStruct = {0};if(canHandle->Instance==CAN1){/* USER CODE BEGIN CAN1_MspInit 0 *//* USER CODE END CAN1_MspInit 0 *//* CAN1 clock enable */__HAL_RCC_CAN1_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();/**CAN1 GPIO ConfigurationPA11 ------> CAN1_RXPA12 ------> CAN1_TX*/GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_12;GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;GPIO_InitStruct.Alternate = GPIO_AF9_CAN1;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);/* CAN1 interrupt Init */HAL_NVIC_SetPriority(CAN1_TX_IRQn, 5, 0);HAL_NVIC_EnableIRQ(CAN1_TX_IRQn);HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 5, 0);HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn);HAL_NVIC_SetPriority(CAN1_RX1_IRQn, 5, 0);HAL_NVIC_EnableIRQ(CAN1_RX1_IRQn);HAL_NVIC_SetPriority(CAN1_SCE_IRQn, 5, 0);HAL_NVIC_EnableIRQ(CAN1_SCE_IRQn);/* USER CODE BEGIN CAN1_MspInit 1 *//* USER CODE END CAN1_MspInit 1 */}
}void HAL_CAN_MspDeInit(CAN_HandleTypeDef* canHandle)
{if(canHandle->Instance==CAN1){/* USER CODE BEGIN CAN1_MspDeInit 0 *//* USER CODE END CAN1_MspDeInit 0 *//* Peripheral clock disable */__HAL_RCC_CAN1_CLK_DISABLE();/**CAN1 GPIO ConfigurationPA11 ------> CAN1_RXPA12 ------> CAN1_TX*/HAL_GPIO_DeInit(GPIOA, GPIO_PIN_11|GPIO_PIN_12);/* CAN1 interrupt Deinit */HAL_NVIC_DisableIRQ(CAN1_TX_IRQn);HAL_NVIC_DisableIRQ(CAN1_RX0_IRQn);HAL_NVIC_DisableIRQ(CAN1_RX1_IRQn);HAL_NVIC_DisableIRQ(CAN1_SCE_IRQn);/* USER CODE BEGIN CAN1_MspDeInit 1 *//* USER CODE END CAN1_MspDeInit 1 */}
}/* USER CODE BEGIN 1 *//*** @brief 配置 CAN1 滤波器(接收所有标准帧)*/
void CAN1_ConfigFilter(void)
{CAN_FilterTypeDef canFilter;canFilter.FilterBank = 0;canFilter.FilterMode = CAN_FILTERMODE_IDMASK;canFilter.FilterScale = CAN_FILTERSCALE_32BIT;canFilter.FilterIdHigh = 0x0000;canFilter.FilterIdLow = 0x0000;canFilter.FilterMaskIdHigh = 0x0000;canFilter.FilterMaskIdLow = 0x0000;canFilter.FilterFIFOAssignment = CAN_RX_FIFO0;canFilter.FilterActivation = ENABLE;canFilter.SlaveStartFilterBank = 14;if (HAL_CAN_ConfigFilter(&hcan1, &canFilter) != HAL_OK){Error_Handler();}
}
// /**
// * @brief 配置 CAN1 滤波器(ID范围0x100~0x10F)
// */
// void CAN1_ConfigFilter(void)
// {
// CAN_FilterTypeDef canFilter = {0};// canFilter.FilterBank = 0;
// canFilter.FilterMode = CAN_FILTERMODE_IDMASK;
// canFilter.FilterScale = CAN_FILTERSCALE_32BIT;// // 过滤ID范围0x100~0x10F,掩码屏蔽最低4位
// uint32_t filter_id = 0x100 << 21;
// uint32_t filter_mask = 0x7F0 << 21; // 掩码最低4位不比较,其他位比较// canFilter.FilterIdHigh = (filter_id >> 16) & 0xFFFF;
// canFilter.FilterIdLow = filter_id & 0xFFFF;
// canFilter.FilterMaskIdHigh = (filter_mask >> 16) & 0xFFFF;
// canFilter.FilterMaskIdLow = filter_mask & 0xFFFF;// canFilter.FilterFIFOAssignment = CAN_RX_FIFO0;
// canFilter.FilterActivation = ENABLE;
// canFilter.SlaveStartFilterBank = 14;// if (HAL_CAN_ConfigFilter(&hcan1, &canFilter) != HAL_OK)
// {
// printf("Filter config failed!\r\n");
// Error_Handler();
// }
// }/*** @brief 启动 CAN1 并启用接收中断*/
void CAN1_Start(void)
{if (HAL_CAN_Start(&hcan1) != HAL_OK){printf("CAN Start Failed!\r\n");Error_Handler();}else{printf("CAN Started Successfully.\r\n");}if (HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK){printf("Enable RX Notification Failed!\r\n");Error_Handler();}else{printf("RX Notification Enabled.\r\n");}
}/*** @brief CAN1 发送一帧标准数据帧* @param StdId 标准帧ID* @param data 数据指针(最多8字节)* @param len 数据长度(0~8)*/
HAL_StatusTypeDef CAN1_SendMessage(uint32_t StdId, uint8_t *data, uint8_t len)
{CAN_TxHeaderTypeDef txHeader;uint32_t txMailbox;txHeader.StdId = StdId;txHeader.ExtId = 0;txHeader.IDE = CAN_ID_STD;txHeader.RTR = CAN_RTR_DATA;txHeader.DLC = len;txHeader.TransmitGlobalTime = DISABLE;if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, data, &txMailbox) != HAL_OK){return HAL_ERROR;}return HAL_OK;
}/*** @brief 接收中断回调函数(FIFO0)*/
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{if (hcan->Instance == CAN1){CAN_RxHeaderTypeDef rxHeader;uint8_t rxData[8];if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK){printf("CAN1 RX: ID=0x%03X DLC=%d Data=", rxHeader.StdId, rxHeader.DLC);for (int i = 0; i < rxHeader.DLC; i++){printf("%02X ", rxData[i]);}printf("\r\n");}}
}/*** @brief 示例函数:仅发送一帧*/
void Example_CAN_Work(void)
{uint8_t exampleData[8] = {0x19, 0x92, 0x93, 0x74, 0x55, 0x36, 0x77, 0x00};if (HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) > 0){if (CAN1_SendMessage(0x123, exampleData, 8) == HAL_OK){printf("CAN message sent.\r\n");}else{printf("CAN send error.\r\n");}}else{printf("No free CAN Tx mailbox!\r\n");}
}/* USER CODE END 1 */
避免问题:
- 确认终端电阻(120Ω × 2)
- 检查波特率一致性
- 使用 CAN分析仪 做抓包验证
- 若收不到数据,先试试是否 开启中断 + 滤波器未屏蔽
✅ 代码启动顺序建议
MX_CAN1_Init(); // 初始化
CAN1_ConfigFilter(); // 设置滤波器(可接收哪些帧)
CAN1_Start(); // 启动 + 启用接收中断
✅ 示例发送(发送一帧):
uint8_t data[8] = {0x01, 0x02, 0x03};
CAN1_SendMessage(0x123, data, 3);
✅ 示例接收(中断自动触发)
void HAL_CAN_RxFifo0MsgPendingCallback(...)
{HAL_CAN_GetRxMessage(...);// 打印数据
}
🔚 总结
CAN 的收发流程并不复杂,难点在于波特率配置、滤波器使用、同步机制的理解。
亲测的 9.735kbps 与 10kbps 通信失败,正体现了接收采样点延迟带来的不对称性问题,是工程上值得注意的重要细节。