1. 项目概述
在嵌入式系统开发中,CAN(Controller Area Network)总线因其高可靠性、多主架构和强大的抗干扰能力,已成为工业控制、汽车电子等领域的标准通信协议。本文将基于STM32F407芯片,通过CubeMX和Keil工具链,详细讲解CAN通信的完整实现过程。
不同于简单的代码示例,本教程将从硬件选型、电路设计到软件配置,全方位解析CAN通信的每个关键环节。特别针对初学者容易忽视的终端电阻配置、筛选器设置等难点,提供经过实际验证的解决方案。
2. 硬件设计与选型
2.1 CAN通信核心组件
一个完整的CAN节点包含四个关键部分:
- CAN控制器:STM32芯片内置,负责协议处理
- CAN收发器:完成TTL电平与差分信号的转换
- 物理总线:双绞线连接各节点
- 终端电阻:匹配阻抗,消除信号反射
2.1.1 STM32 CAN控制器特性
不同STM32系列的CAN控制器存在差异:
- F103系列:1组CAN2.0控制器
- F407系列:2组独立CAN2.0控制器(CAN1+CAN2)
- H750系列:支持CAN FD协议
引脚复用规则需特别注意:
- CAN1默认使用PA11(RX)/PA12(TX)
- 可重映射至PB8(RX)/PB9(TX)
- CAN2固定使用PB5(RX)/PB6(TX)
实际项目中,当PA11/PA12被USB占用时,建议使用PB8/PB9作为CAN引脚,避免资源冲突。
2.1.2 收发器选型指南
常用收发器型号对比:
| 型号 | 协议支持 | 最高速率 | 工作电压 | 特点 |
|---|---|---|---|---|
| TJA1050 | CAN2.0 | 1Mbps | 4.5-5.5V | 性价比高,市场存量最大 |
| TJA1042 | CAN2.0 | 1Mbps | 4.5-5.5V | 抗干扰能力更强 |
| TJA1051 | CAN FD | 5Mbps | 4.5-5.5V | 支持新一代CAN协议 |
注意:STM32F4系列不支持CAN FD,使用TJA1051时无法发挥其高速特性。
2.2 物理层设计要点
2.2.1 总线拓扑规范
-
线材选择:
- 测试环境:普通杜邦线即可
- 工业现场:屏蔽双绞线(推荐AWG22)
- 长距离传输:线径与距离匹配(100米内≥0.75mm²)
-
终端电阻配置:
- 必须在总线两端各接一个120Ω电阻
- 中间节点不应接入终端电阻
- 实测H-L间电阻应为≈60Ω(两个120Ω并联)
-
接地策略:
- 短距离同电源:可不共地
- 远距离/多电源:必须单点共地
- 屏蔽层处理:单端接地(干扰源端)
2.2.2 典型接线方案
场景1:核心板+外接收发器
mermaid复制graph LR
STM32-->|PB8/PB9|TJA1050
TJA1050-->|H/L|Bus
Bus-->|H/L|Analyzer
场景2:开发板(内置收发器)
mermaid复制graph LR
STM32-->|板载线路|TJA1050
TJA1050-->|端子台|Bus
注意:收发器需要5V供电,直接接3.3V会导致工作异常。
3. 软件配置详解
3.1 CubeMX基础工程创建
3.1.1 关键配置步骤
-
时钟树配置:
- 确保APB1时钟正确(F407为42MHz)
- 系统时钟建议设置为168MHz(F407)
-
CAN外设使能:
- Connectivity → CAN1 → Activated
- 根据硬件连接选择引脚(PB8/PB9或PA11/PA12)
-
波特率计算:
公式:波特率 = APB1时钟/(Prescaler*(BS1+BS2+1))常用配置(F407@42MHz APB1):
目标波特率 Prescaler BS1 BS2 实际波特率 1Mbps 3 5 1 1.00Mbps 500Kbps 6 5 1 500Kbps 250Kbps 12 5 1 250Kbps -
中断配置:
- 启用RX FIFO0中断
- 优先级建议设置为中等(如4)
3.1.2 代码生成注意事项
- 生成MDK-ARM工程
- 检查生成的初始化代码:
c复制hcan1.Instance = CAN1; hcan1.Init.Prescaler = 6; hcan1.Init.Mode = CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan1.Init.TimeSeg1 = CAN_BS1_5TQ; hcan1.Init.TimeSeg2 = CAN_BS2_1TQ; hcan1.Init.TimeTriggeredMode = DISABLE; // 其他参数保持默认
3.2 用户代码架构设计
推荐的文件结构:
code复制Project/
├── Bsp/
│ └── CAN/
│ ├── bsp_CAN.c
│ └── bsp_CAN.h
├── Core/
└── Drivers/
bsp_CAN.h 基础内容:
c复制#ifndef __BSP_CAN_H
#define __BSP_CAN_H
#include "stm32f4xx_hal.h"
/* 函数声明 */
uint8_t CAN1_SendData(uint32_t type, uint32_t ID, uint8_t *msgData, uint8_t num);
void CAN1_FilterInit(void);
#endif
4. 核心功能实现
4.1 CAN初始化流程
完整的初始化序列:
- 硬件初始化(CubeMX生成)
- 筛选器配置
- 启动CAN外设
- 使能接收中断
关键代码实现:
c复制// main.c中调用
MX_CAN1_Init();
CAN1_FilterInit();
if(HAL_CAN_Start(&hcan1) != HAL_OK) {
printf("CAN启动失败!\r\n");
Error_Handler();
}
4.2 数据发送实现
4.2.1 发送状态机
CAN发送包含三个阶段:
- 配置帧参数(ID、格式、长度等)
- 等待发送邮箱空闲(3个邮箱轮询)
- 提交发送请求
增强版发送函数:
c复制uint8_t CAN1_SendData(uint32_t type, uint32_t ID, uint8_t *msgData, uint8_t num)
{
CAN_TxHeaderTypeDef TxHeader;
uint32_t txMailbox;
uint8_t timeout = 0;
/* 参数校验 */
if(num > 8) num = 8;
/* 帧头配置 */
TxHeader.RTR = CAN_RTR_DATA;
TxHeader.DLC = num;
TxHeader.TransmitGlobalTime = DISABLE;
if(type == CAN_ID_EXT) {
TxHeader.ExtId = ID;
TxHeader.IDE = CAN_ID_EXT;
} else {
TxHeader.StdId = ID;
TxHeader.IDE = CAN_ID_STD;
}
/* 等待邮箱空闲 */
while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan1) == 0) {
if(++timeout > 10) {
printf("发送超时!\r\n");
return HAL_ERROR;
}
HAL_Delay(1);
}
/* 启动发送 */
return HAL_CAN_AddTxMessage(&hcan1, &TxHeader, msgData, &txMailbox);
}
4.3 数据接收实现
4.3.1 筛选器配置原理
STM32提供28个可编程筛选器(CAN1:0-13, CAN2:14-27),支持两种工作模式:
-
掩码模式(CAN_FILTERMODE_IDMASK):
- 类似子网掩码概念
- 掩码位为1表示必须匹配
- 掩码位为0表示不关心
-
列表模式(CAN_FILTERMODE_IDLIST):
- 精确匹配特定ID
- 每个筛选器可存1个32位ID或2个16位ID
接收所有报文的配置示例:
c复制void CAN1_FilterInit(void)
{
CAN_FilterTypeDef filter;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = 0x0000;
filter.FilterIdLow = 0x0000;
filter.FilterMaskIdHigh = 0x0000;
filter.FilterMaskIdLow = 0x0000;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
filter.SlaveStartFilterBank = 14;
HAL_CAN_ConfigFilter(&hcan1, &filter);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
}
4.3.2 中断回调实现
基础版本(调试用):
c复制void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData);
printf("ID:0x%X DLC:%d Data:",
RxHeader.IDE? RxHeader.ExtId : RxHeader.StdId,
RxHeader.DLC);
for(uint8_t i=0; i<RxHeader.DLC; i++)
printf("%02X ", RxData[i]);
printf("\r\n");
}
生产环境优化版:
c复制// bsp_CAN.c中定义
typedef struct {
uint32_t id;
uint8_t data[8];
uint8_t length;
uint8_t isNew;
} CAN_Message;
static CAN_Message canRxMsg;
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef header;
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, canRxMsg.data);
canRxMsg.id = header.IDE ? header.ExtId : header.StdId;
canRxMsg.length = header.DLC;
canRxMsg.isNew = 1;
}
uint8_t CAN_GetMessage(CAN_Message *msg)
{
if(canRxMsg.isNew) {
memcpy(msg, &canRxMsg, sizeof(CAN_Message));
canRxMsg.isNew = 0;
return 1;
}
return 0;
}
5. 高级配置与优化
5.1 精准ID过滤实现
5.1.1 列表模式精确匹配
配置只接收ID为0x123的标准帧:
c复制filter.FilterMode = CAN_FILTERMODE_IDLIST;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = (0x123 << 5) >> 16; // STDID[10:0]左移5位
filter.FilterIdLow = (0x123 << 5) | CAN_ID_STD;
5.1.2 掩码模式范围匹配
接收ID 0x100-0x10F的标准帧:
c复制filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = (0x100 << 5) >> 16;
filter.FilterIdLow = (0x100 << 5) | CAN_ID_STD;
filter.FilterMaskIdHigh = 0xFFE0 >> 16; // 高5位必须匹配
filter.FilterMaskIdLow = 0xFFFF; // 所有控制位必须匹配
5.2 错误处理与监控
5.2.1 错误状态获取
c复制uint32_t errCode = HAL_CAN_GetError(&hcan1);
if(errCode != HAL_CAN_ERROR_NONE) {
printf("CAN错误:0x%lX\r\n", errCode);
}
5.2.2 常见错误代码
| 错误代码 | 含义 | 处理建议 |
|---|---|---|
| HAL_CAN_ERROR_EWG | 协议警告 | 检查总线终端电阻 |
| HAL_CAN_ERROR_EPV | 错误被动状态 | 检查波特率设置 |
| HAL_CAN_ERROR_BOF | 总线关闭状态 | 重启CAN控制器 |
| HAL_CAN_ERROR_STF | 填充错误 | 检查总线连接 |
| HAL_CAN_ERROR_FOR | 格式错误 | 检查其他节点配置 |
5.3 性能优化技巧
-
DMA传输:
- 对高负载场景,配置CAN RX使用DMA
- 减少CPU中断开销
-
双FIFO利用:
- 将高优先级报文分配到FIFO0
- 普通报文分配到FIFO1
-
时间触发模式:
- 对实时性要求高的应用
- 启用TimeTriggeredMode
6. 实战调试技巧
6.1 常见问题排查
6.1.1 无法发送数据
- 检查CAN控制器是否成功启动(HAL_CAN_Start返回值)
- 测量CANH-CANL电压(隐性时应≈2.5V)
- 确认终端电阻配置(总阻值≈60Ω)
6.1.2 无法接收数据
- 验证筛选器配置(最简单方法是先配置为接收所有报文)
- 检查中断是否使能(NVIC配置)
- 确认回调函数是否正确定义
6.1.3 通信不稳定
- 降低波特率测试(如从1Mbps降到500Kbps)
- 检查电源质量(示波器观察5V电源纹波)
- 缩短总线长度或改用屏蔽双绞线
6.2 调试工具推荐
-
CAN分析仪:
- PCAN-USB Pro(高端)
- USB-CAN Analyzer(经济型)
-
波形观测:
- 示波器测量CANH-CANL差分信号
- 正常显性电平≈2V,隐性≈0V
-
电阻检测:
- 断电状态下测量H-L间电阻
- 正常值应在50-70Ω之间
7. 工程源码解析
完整工程包含以下关键文件:
-
bsp_CAN.c/h:
- 发送/接收接口封装
- 筛选器配置
- 中断处理
-
main.c:
- 硬件初始化
- 主循环处理
- 测试用例
-
CubeMX配置:
- CAN参数设置
- 引脚分配
- 时钟树配置
提示:实际项目中建议将CAN处理封装为独立模块,通过接口函数与主程序交互,提高代码复用性。
8. 扩展应用
8.1 多节点组网建议
-
ID分配规划:
- 系统消息:0x000-0x100
- 节点1:0x101-0x1FF
- 节点2:0x201-0x2FF
- 广播消息:0x7FF
-
优先级设计:
- 低ID值具有高优先级
- 关键消息使用小ID
8.2 协议层实现
基于CAN的基础通信协议示例:
c复制typedef struct {
uint32_t id;
uint8_t cmd;
uint8_t data[6];
uint8_t crc;
} CAN_Protocol;
void CAN_SendCommand(uint8_t cmd, uint8_t *params)
{
CAN_Protocol frame;
frame.id = 0x100 + cmd;
frame.cmd = cmd;
memcpy(frame.data, params, 6);
frame.crc = Calculate_CRC(&frame, 8);
CAN1_SendData(CAN_ID_STD, frame.id, (uint8_t*)&frame, 8);
}
9. 总结与进阶
通过本教程,我们系统性地实现了STM32 CAN通信的全套解决方案。关键要点包括:
- 硬件设计必须包含终端电阻
- 筛选器配置是接收数据的必要条件
- 中断处理应遵循"快进快出"原则
- 生产环境需要完善的错误处理机制
对于需要更高性能的场景,建议考虑以下进阶方向:
-
CAN FD升级:
- 选用STM32H7系列
- 搭配TJA1051收发器
- 最高支持5Mbps速率
-
RTOS集成:
- 使用消息队列处理CAN数据
- 创建专用CAN处理任务
-
协议栈开发:
- 实现CANopen或J1939协议
- 提供标准化接口
在实际项目开发中,CAN通信的稳定性往往取决于细节处理。建议在原型阶段充分测试各种异常场景,包括总线短路、节点热插拔、强干扰环境等,确保系统鲁棒性。