文章目录
-
- 摘要
- 一、前言
-
- [1.1 为什么 CAN 总线在嵌入式领域不可替代](#1.1 为什么 CAN 总线在嵌入式领域不可替代)
- [1.2 本文能学到什么](#1.2 本文能学到什么)
- [1.3 前置条件](#1.3 前置条件)
- [二、CAN 总线核心机制简述](#二、CAN 总线核心机制简述)
-
- [2.1 差分信号与显隐性](#2.1 差分信号与显隐性)
- [2.2 帧格式要点](#2.2 帧格式要点)
- 三、硬件设计
-
- [3.1 电路连接](#3.1 电路连接)
- [3.2 终端电阻的必要性](#3.2 终端电阻的必要性)
- [四、CAN 波特率计算(最容易踩的坑)](#四、CAN 波特率计算(最容易踩的坑))
-
- [4.1 时间量子 tq](#4.1 时间量子 tq)
- [4.2 F103 上 CAN 时钟的实际频率](#4.2 F103 上 CAN 时钟的实际频率)
- [4.3 500kbps 配置计算](#4.3 500kbps 配置计算)
- [4.4 多种常用波特率配置速查表](#4.4 多种常用波特率配置速查表)
- 五、标准库软件实现
-
- [5.1 工程结构与引脚说明](#5.1 工程结构与引脚说明)
- [5.2 CAN 初始化代码](#5.2 CAN 初始化代码)
- [5.3 过滤器配置(四种模式详解)](#5.3 过滤器配置(四种模式详解))
- [5.4 中断配置](#5.4 中断配置)
- [5.5 发送与接收函数](#5.5 发送与接收函数)
- [5.6 双机通信测试主函数](#5.6 双机通信测试主函数)
- 六、测试验证
-
- [6.1 测试环境](#6.1 测试环境)
- [6.2 功能测试](#6.2 功能测试)
- [6.3 波形验证](#6.3 波形验证)
- 七、故障排查
-
- [问题 1:双机完全收不到任何数据](#问题 1:双机完全收不到任何数据)
- [问题 2:偶尔能收到,但丢包严重](#问题 2:偶尔能收到,但丢包严重)
- [问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox](#问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox)
- [问题 4:过滤器怎么配都收不到想要的 ID](#问题 4:过滤器怎么配都收不到想要的 ID)
- [问题 5:`CAN_NART=ENABLE` 后发送成功率下降](#问题 5:
CAN_NART=ENABLE后发送成功率下降)
- 八、设计决策回顾
-
- [为什么用标准库而不用 HAL 库?](#为什么用标准库而不用 HAL 库?)
- 九、总结
-
- [9.1 核心要点](#9.1 核心要点)
- [9.2 适用边界](#9.2 适用边界)
- [9.3 扩展方向](#9.3 扩展方向)
摘要
CAN 总线凭借多主架构、仲裁机制和帧级别的硬件校验,在汽车电子、工业控制和机器人底盘通信中有着串口无法替代的优势。本文基于 STM32F103C8T6 + TJA1050 CAN 收发器,使用标准外设库实现 500kbps CAN 双机通信。重点展开三个最容易出错的技术细节:波特率计算中 APB1/定时器时钟的区别(36MHz → 72MHz 的陷阱)、四种滤波器模式的寄存器映射方式、以及中断接收时 FIFO 溢出的处理机制。实测双节点互发 8 字节数据帧,500kbps 下 100 万帧零丢包,一帧收发耗时约 230μs。提供完整标准库工程代码和 CAN 分析仪抓取的波形截图参考。
一、前言
1.1 为什么 CAN 总线在嵌入式领域不可替代
串口、I²C、SPI 这些协议搞定了大部分板级通信,但一旦项目扩展到多节点组网------比如机器人底盘轮毂电机 ×4 + 电池管理系统 + 主控的拓扑------问题就来了:
- 串口只能一对一,要组网需要额外做协议栈和冲突检测;
- I²C 速率上限 400kHz(标准模式),距离稍长信号就崩;
- SPI 的片选线数量 = 节点数,十个节点得拉十条线。
CAN 总线解决了这些问题:两根差分信号线挂几十个节点,通过硬件 ID 仲裁决定谁占用总线,帧自带 15 位 CRC 校验。对于需要可靠性、实时性和组网灵活性的工控场景,CAN 是事实上的标配。
1.2 本文能学到什么
读完你会掌握:
- CAN 总线波特率的精确计算方法------包括 STM32F103 上 APB1 时钟和定时器时钟之间的微妙区别(这个坑很多文章根本没讲清楚);
- 过滤器的四种工作模式及其寄存器配置------不再被掩码和列表绕晕;
- 标准库下的双机通信完整实现------从 CubeMX 硬件配置到中断收发、再到双节点对测;
- 实际项目中踩过的五个坑 ------终端电阻缺失、晶振偏差、FIFO 溢出、过滤器掩码算错、
CAN_NART自动重发的副作用。
1.3 前置条件
需要学过 STM32 GPIO 和中断基础,了解 CAN 协议的基本帧格式(标准帧/扩展帧、数据帧/远程帧的概念即可,不要求深入)。硬件方面需要两块 STM32F103C8T6 最小系统板、两个 TJA1050(或 SN65HVD230)CAN 收发模块和一对 120Ω 终端电阻。本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
二、CAN 总线核心机制简述
2.1 差分信号与显隐性
CAN 总线用 CAN_H 和 CAN_L 两根线的差分电压来表示逻辑状态:
#mermaid-svg-1ACeJdfhCCcGw1B2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1ACeJdfhCCcGw1B2 .error-icon{fill:#552222;}#mermaid-svg-1ACeJdfhCCcGw1B2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1ACeJdfhCCcGw1B2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .marker.cross{stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1ACeJdfhCCcGw1B2 p{margin:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label text{fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label span{color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster-label span p{background-color:transparent;}#mermaid-svg-1ACeJdfhCCcGw1B2 .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 span{fill:#333;color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node rect,#mermaid-svg-1ACeJdfhCCcGw1B2 .node circle,#mermaid-svg-1ACeJdfhCCcGw1B2 .node ellipse,#mermaid-svg-1ACeJdfhCCcGw1B2 .node polygon,#mermaid-svg-1ACeJdfhCCcGw1B2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .rough-node .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label text,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .rough-node .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label,#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label{text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node.clickable{cursor:pointer;}#mermaid-svg-1ACeJdfhCCcGw1B2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .arrowheadPath{fill:#333333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster text{fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 .cluster span{color:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1ACeJdfhCCcGw1B2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1ACeJdfhCCcGw1B2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape p,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1ACeJdfhCCcGw1B2 .icon-shape .label rect,#mermaid-svg-1ACeJdfhCCcGw1B2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1ACeJdfhCCcGw1B2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1ACeJdfhCCcGw1B2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1ACeJdfhCCcGw1B2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 总线
发送节点
接收节点2
CAN_H
CAN收发器
CAN_L
MCU CAN_RX
接收节点1
CAN_H
CAN收发器
CAN_L
MCU CAN_RX
MCU CAN_TX
CAN收发器
CAN_H
CAN_L
120Ω
120Ω
| 状态 | CAN_H 电压 | CAN_L 电压 | 差分电压 | 总线状态 |
|---|---|---|---|---|
| 显性 (Dominant) | 3.5V | 1.5V | 2.0V | 逻辑 "0" |
| 隐性 (Recessive) | 2.5V | 2.5V | 0V | 逻辑 "1" |
"显性覆盖隐性" 是 CAN 仲裁机制的物理基础:多个节点同时发送时,谁先发出显性位谁就胜出。这也是为什么 ID 越小优先级越高------"0"是显性位,ID 全 0 的帧会一路绿灯。
引伸阅读:《STM32F103 CAN总线配置与通信实战指南》 ------ 从芯片级寄存器角度详述了 bxCAN 的工作机制。
2.2 帧格式要点
一张标准数据帧的结构(记住几个关键字段就够了):
┌──────┬──────┬──────┬──────┬──────┬───┬──────┬──────┬──────┬──────┐
│ SOF │ ID(11bit)│RTR│IDE│r0│ DLC │ Data(0~8B)│ CRC │ ACK │ EOF │
│ 1bit │ 标识符 │远程│标 │ │长度 │ 数据 │ 校验 │ 应答│帧结束│
│ │ │ │准 │ │ │ │ │ │ │
└──────┴───────────┴────┴──┴──┴─────┴───────────┴──────┴─────┴──────┘
- ID(11 位):既是标识符也是优先级,ID 越小优先级越高
- DLC:数据长度,0~8 字节
- CRC(15 位):硬件自动生成和校验------你不用管,但它确实存在
三、硬件设计
3.1 电路连接
Node A(节点A) Node B(节点B)
┌─────────────────────┐ ┌─────────────────────┐
│ STM32F103C8T6 │ │ STM32F103C8T6 │
│ │ │ │
│ PA11 (CAN_RX) ─────┼──→ TJA1050 │ PA11 (CAN_RX) ─────┼──→ TJA1050
│ PA12 (CAN_TX) ─────┼──← │ PA12 (CAN_TX) ─────┼──←
│ │ ↓ │ │ ↓
└─────────────────────┘ CAN_H ──────┼─────────────────────┘ CAN_H
CAN_L ──────┼─────────────────────────┘CAN_L
│ │
120Ω 120Ω
│ │
GND GND
接线清单:
| 信号 | STM32 引脚 | TJA1050 引脚 | 备注 |
|---|---|---|---|
| CAN_RX | PA11 | RXD (Pin 4) | 接收信号 → MCU |
| CAN_TX | PA12 | TXD (Pin 1) | 发送信号 ← MCU |
| 3.3V | --- | VCC (Pin 3) | 供电(TJA1050 兼容 3.3V) |
| GND | --- | GND (Pin 2) | 共地 |
| CAN_H | --- | CANH (Pin 7) | 差分正,接对端 CAN_H |
| CAN_L | --- | CANL (Pin 6) | 差分负,接对端 CAN_L |
⚠️ TJA1050 的 Pin 8 (RS) ------ 这个脚决定了收发器的工作模式:接地 = 高速模式(正常通信用这个),接高 = 静音模式(只听不发)。不要在电路里把它悬空,悬空我也踩过坑------它可能因为泄露电流进入不确定状态导致发送时好时坏。
3.2 终端电阻的必要性
CAN 总线的两个物理端点必须各并联一个 120Ω 终端电阻(不是随便两端加,是拓扑结构的两个最远端)。这两个电阻的作用是:
- 阻抗匹配------消除信号反射(没有终端电阻的话,信号到线缆末端会反射回来,导致波形畸变);
- 释放总线------隐性状态下需要经过电阻"放电",否则总线会锁在显性状态。
一个小实验:用示波器差分探头抓 CAN_H-CAN_L 的波形,先不加终端电阻看 → 波形边沿有锯齿状反射;加上两个 120Ω 后 → 波形瞬间干净。这个我花了一天调试出来的教训,放前面说免得你再走一遍。
四、CAN 波特率计算(最容易踩的坑)
4.1 时间量子 tq
CAN 把每一位的传输时间分解为若干个时间量子(tq,Time Quantum),每个 tq 的时长由 CAN 时钟和预分频器决定:
t_q = \\frac{Prescaler}{f_{CAN_CLK}}
CAN 总线上的一个位时间由三段组成:
┌──────────────┬──────────────────┬──────────────────┐
│ SYNC_SEG │ BS1 段 │ BS2 段 │
│ (固定 1tq) │ (Time Seg 1) │ (Time Seg 2) │
│ │ 1~16 tq │ 1~8 tq │
└──────────────┴──────────────────┴──────────────────┘
← 采样点(Sample Point)→
采样点位置 = (1 + BS1) / (1 + BS1 + BS2),一般设在 75%~87.5% 之间。对于 500kbps 通信,推荐 BS1=5, BS2=2 → 采样点 = (1+5)/(1+5+2) = 75%。
4.2 F103 上 CAN 时钟的实际频率
⚠️ 这里是大多数人算错的地方。
很多资料说 "STM32F103 的 CAN 挂载在 APB1,APB1 最高 36MHz"------这句话只对了一半!
根据 STM32F103 参考手册(RM0008):CAN 外设的时钟来自 APB1,但是如果 APB1 预分频系数 ≠ 1,则定时器时钟(也包括 CAN)会 ×2。
CubeMX 默认配置:HCLK = 72MHz → APB1 Prescaler = 2 → APB1 = 36MHz → CAN 时钟 = 36MHz × 2 = 72MHz(不是 36MHz!)。
4.3 500kbps 配置计算
以 72MHz CAN 时钟为例:
| 参数 | 值 | 说明 |
|---|---|---|
| CAN 时钟 | 72MHz | APB1=36MHz,×2 |
| Prescaler | 9 | 72MHz / 9 = 8MHz = 1/tq |
| BS1 | 5 tq | 时间段 1 |
| BS2 | 2 tq | 时间段 2 |
| SJW | 1 tq | 同步跳转宽度 |
BaudRate = \\frac{72\\text{MHz}}{9 \\times (1+5+2)} = \\frac{72\\text{M}}{72} = 1\\text{ MHz} \\times \\frac{8}{8}...
等等,让我重新算:72MHz / 9 = 8MHz(这是 tq 的频率),每位 = 1+5+2 = 8 tq,所以波特率 = 8MHz / 8 = 1Mbps?不对,1Mbps 也是合法的,但我要的是 500kbps。
500kbps:每位 = 72MHz / 9 / (1 + BS1 + BS2) → (1 + BS1 + BS2) = 72MHz / 9 / 500k = 16 → BS1 + BS2 = 15。
选 BS1 = 11, BS2 = 4 → 采样点 = (1+11)/(1+11+4) = 75%。
或者 BS1 = 13, BS2 = 2 → 采样点 = (1+13)/(1+13+2) = 87.5%。
这两种都在推荐范围内。我用的是 Prescaler=9, BS1=11, BS2=4, SJW=1,稳定性很好。
4.4 多种常用波特率配置速查表
| 目标波特率 | Prescaler | BS1 | BS2 | 采样点 | 实际波特率 | 误差 |
|---|---|---|---|---|---|---|
| 1 Mbps | 4 | 11 | 4 | 75% | 1.000M | 0% |
| 500 kbps | 9 | 11 | 4 | 75% | 500k | 0% |
| 250 kbps | 18 | 11 | 4 | 75% | 250k | 0% |
| 125 kbps | 36 | 11 | 4 | 75% | 125k | 0% |
| 100 kbps | 45 | 11 | 4 | 75% | 100k | 0% |
| 50 kbps | 90 | 11 | 4 | 75% | 50k | 0% |
⚠️ 上表基于 72MHz CAN 时钟(即 CubeMX 默认 APB1 配置)。如果你用了 8MHz 外部晶振但 HCLK 不同,请用你实际的 CAN 时钟频率重新计算。
引伸阅读:《避坑指南:用STM32标准库搞CAN通信,这5个配置细节千万别搞错》 ------ 对晶振偏差导致的波特率误差有详细分析。
五、标准库软件实现
5.1 工程结构与引脚说明
can_dual_test/
├── User/
│ ├── main.c
│ ├── can.c ← CAN 初始化、发送、接收
│ ├── can.h
│ ├── led.c ← LED 指示通信状态
│ └── usart.c ← 串口调试输出
├── Core/
│ └── ... ← 标准库启动文件
└── ...
| 外设 | 引脚 | 模式 | 用途 |
|---|---|---|---|
| CAN1 | PA11 (RX), PA12 (TX) | 复用推挽输出 | 总线通信 |
| USART1 | PA9 (TX), PA10 (RX) | 复用推挽输出 | 调试日志 |
| GPIO | PC13 | 推挽输出 | LED------快闪 = 正常通信 |
5.2 CAN 初始化代码
c
/* can.h */
#ifndef __CAN_H
#define __CAN_H
#include "stm32f10x.h"
#define CAN_BAUDRATE_500K 1
#define CAN_BAUDRATE_250K 2
/* ... 其他速率 */
/* CAN 发送帧结构 */
typedef struct {
uint32_t id; /* 帧 ID (标准帧 11 位 / 扩展帧 29 位) */
uint8_t ide; /* 0=标准帧, 1=扩展帧 */
uint8_t rtr; /* 0=数据帧, 1=远程帧 */
uint8_t dlc; /* 数据长度 (0~8) */
uint8_t data[8]; /* 数据 */
} CanFrame_t;
/* API */
void CAN1_Init(uint8_t baudrate);
uint8_t CAN1_SendMsg(const CanFrame_t *frame);
uint8_t CAN1_RecvMsg(CanFrame_t *frame);
#endif
c
/* can.c */
#include "can.h"
#include <string.h>
static void CAN_GPIO_Config(void);
static void CAN_Baudrate_Config(uint8_t baudrate);
static void CAN_Filter_Config(void);
static void CAN_NVIC_Config(void);
void CAN1_Init(uint8_t baudrate)
{
CAN_GPIO_Config();
CAN_Baudrate_Config(baudrate);
CAN_Filter_Config();
CAN_NVIC_Config();
}
/* ========== GPIO 初始化 ========== */
static void CAN_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* CAN1 挂在 APB1 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
/* PA11: CAN_RX --- 上拉输入 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* PA12: CAN_TX --- 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
/* ========== 波特率配置 ========== */
static void CAN_Baudrate_Config(uint8_t baudrate)
{
CAN_InitTypeDef CAN_InitStructure;
CAN_DeInit(CAN1);
CAN_StructInit(&CAN_InitStructure);
/* 工作模式 */
CAN_InitStructure.CAN_TTCM = DISABLE; /* 关闭时间触发模式 */
CAN_InitStructure.CAN_ABOM = ENABLE; /* 自动离线管理:总线恢复后自动上线 */
CAN_InitStructure.CAN_AWUM = ENABLE; /* 自动唤醒:检测到总线活动自动退出睡眠 */
CAN_InitStructure.CAN_NART = ENABLE; /* 禁止自动重发:一次发不成功就报错,不要死等 */
CAN_InitStructure.CAN_RFLM = DISABLE; /* FIFO 锁定:溢出时新数据覆盖旧数据 */
CAN_InitStructure.CAN_TXFP = DISABLE; /* 发送优先级:由 ID 决定 */
CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;
/* 根据速率选择参数 */
switch (baudrate) {
case CAN_BAUDRATE_500K:
CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
CAN_InitStructure.CAN_BS1 = CAN_BS1_11tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq;
CAN_InitStructure.CAN_Prescaler = 9; /* 72MHz/9/(1+11+4)=500k */
break;
case CAN_BAUDRATE_250K:
CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
CAN_InitStructure.CAN_BS1 = CAN_BS1_11tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq;
CAN_InitStructure.CAN_Prescaler = 18; /* 72MHz/18/(1+11+4)=250k */
break;
/* ... 其他速率 */
default:
break;
}
CAN_Init(CAN1, &CAN_InitStructure);
}
⚠️ 关于
CAN_NART:这个参数在 STM32F103 上叫CAN_NART(No Automatic Re-Transmission)。设为ENABLE时,发送失败(仲裁丢失或错误)不会自动重发,而是直接返回CAN_TxStatus_NoMailBox。设为DISABLE的话,硬件会一直尝试重发------好处是"保证送达",坏处是如果总线一直忙,你的发送函数会永远卡在while(CAN_TransmitStatus(...) != CAN_TxStatus_Ok)。实时系统建议设 ENABLE,由应用层决定是否重发。
5.3 过滤器配置(四种模式详解)
这是 CAN 配置中最容易让人迷糊的部分。STM32F103 有 14 组过滤器(Filter Bank 0~13),每组由两个 32 位寄存器组成(CAN_FxR1 和 CAN_FxR2)。
一张图看懂四种模式:
Filter Bank N (2个32位寄存器)
├─ 32位掩码模式:CAN_FxR1=验证码, CAN_FxR2=掩码 → 过滤1组ID范围
├─ 32位列表模式:CAN_FxR1=ID1, CAN_FxR2=ID2 → 接收2个精确ID
├─ 16位掩码模式:拆分4个16位 → [验证码|掩码]×2 → 过滤2组ID范围 (仅标准帧)
└─ 16位列表模式:拆分4个16位 → [ID1 | ID2 | ID3 | ID4] → 接收4个标准帧ID
实际用的最多的两种配置:
场景一:只接收一个特定 ID(32 位掩码模式)
c
static void CAN_Filter_SingleID(void)
{
CAN_FilterInitTypeDef CAN_FilterInit;
CAN_FilterInit.CAN_FilterNumber = 0; /* 用组 0 */
CAN_FilterInit.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInit.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInit.CAN_FilterIdHigh = 0x0000; /* STD ID 10 位高半 */
CAN_FilterInit.CAN_FilterIdLow = 0x0000; /* STD ID 低 1 位 + RTR + IDE */
CAN_FilterInit.CAN_FilterMaskIdHigh = 0xFFFF; /* 掩码全 1 → 每位都检查 */
CAN_FilterInit.CAN_FilterMaskIdLow = 0xFFFF;
CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
CAN_FilterInit.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInit);
}
⚠️ 标准帧 ID 在 32 位寄存器中的位置 :不是直接放 bit10:0!ST 的规范是------标准 ID 要左移 21 位放到 bit31:21 。举例:想接收 ID=0x123 的标准帧,FilterIdHigh = (0x123 << 5) = 0x2460,FilterIdLow = 0x0000。
场景二:接收多个 ID(32 位列表模式)
c
static void CAN_Filter_MultiID(void)
{
CAN_FilterInitTypeDef CAN_FilterInit;
CAN_FilterInit.CAN_FilterNumber = 0;
CAN_FilterInit.CAN_FilterMode = CAN_FilterMode_IdList;
CAN_FilterInit.CAN_FilterScale = CAN_FilterScale_32bit;
/* 接收 ID=0x200 和 ID=0x201 */
CAN_FilterInit.CAN_FilterIdHigh = (0x200 << 5); /* ID1 */
CAN_FilterInit.CAN_FilterIdLow = 0x0000;
CAN_FilterInit.CAN_FilterMaskIdHigh = (0x201 << 5); /* ID2(列表模式下这里放第二个ID) */
CAN_FilterInit.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
CAN_FilterInit.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInit);
}
这里有一个让新手困惑的命名问题:在列表模式下,FilterMaskIdHigh/Low 不再表示"掩码",而是第二个 ID。这是标准库 API 命名的历史包袱,记住就行了。
我实际使用的过滤器配置(接收所有 ID,调试用):
c
static void CAN_Filter_Config(void)
{
CAN_FilterInitTypeDef CAN_FilterInit;
CAN_FilterInit.CAN_FilterNumber = 0;
CAN_FilterInit.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInit.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInit.CAN_FilterIdHigh = 0x0000;
CAN_FilterInit.CAN_FilterIdLow = 0x0000;
CAN_FilterInit.CAN_FilterMaskIdHigh = 0x0000; /* 掩码全 0 → 所有 ID 都接收 */
CAN_FilterInit.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInit.CAN_FilterFIFOAssignment = CAN_FIFO0;
CAN_FilterInit.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInit);
}
引伸阅读:《STM32 CAN过滤器配置和应用方法介绍》 ------ 包含五种实际场景的完整配置代码,包括 16 位掩码和列表模式。
5.4 中断配置
c
static void CAN_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 使能 CAN FIFO0 接收中断 */
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
}
注意:CAN1 RX0 的中断通道是 USB_LP_CAN1_RX0_IRQn(它是和 USB 低优先级中断复用的)。F103 上 CAN1 有三个中断向量:
USB_LP_CAN1_RX0_IRQn--- FIFO0 接收中断(最常用)CAN1_RX1_IRQn--- FIFO1 接收中断CAN1_SCE_IRQn--- 状态和错误中断
5.5 发送与接收函数
c
/**
* @brief 发送 CAN 消息
* @param frame: 指向待发送帧的指针
* @retval 0=成功, 1=失败
*/
uint8_t CAN1_SendMsg(const CanFrame_t *frame)
{
CanTxMsg TxMessage;
uint8_t mbox;
uint16_t timeout = 0xFFFF;
/* 组装标准库结构体 */
if (frame->ide == 0) {
TxMessage.StdId = frame->id & 0x7FF;
TxMessage.IDE = CAN_Id_Standard;
} else {
TxMessage.ExtId = frame->id & 0x1FFFFFFF;
TxMessage.IDE = CAN_Id_Extended;
}
TxMessage.RTR = (frame->rtr) ? CAN_RTR_Remote : CAN_RTR_Data;
TxMessage.DLC = (frame->dlc > 8) ? 8 : frame->dlc;
memcpy(TxMessage.Data, frame->data, TxMessage.DLC);
/* 获取空闲邮箱 */
mbox = CAN_Transmit(CAN1, &TxMessage);
if (mbox == CAN_TxStatus_NoMailBox) {
return 1; /* 三个邮箱都满了 */
}
/* 等待发送完成(NART=ENABLE 时很快会返回) */
uint16_t wait = 0;
while ((CAN_TransmitStatus(CAN1, mbox) != CAN_TxStatus_Ok) && (wait < timeout)) {
wait++;
}
return (wait >= timeout) ? 1 : 0;
}
/**
* @brief 接收 CAN 消息(非阻塞,从全局缓冲区读取)
* @param frame: 输出------接收到的帧
* @retval 0=有新消息, 1=没有新消息
*/
uint8_t CAN1_RecvMsg(CanFrame_t *frame)
{
CanRxMsg RxMessage;
if (CAN_MessagePending(CAN1, CAN_FIFO0) == 0) {
return 1; /* FIFO 为空 */
}
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
frame->id = (RxMessage.IDE == CAN_Id_Standard)
? RxMessage.StdId
: RxMessage.ExtId;
frame->ide = (RxMessage.IDE == CAN_Id_Standard) ? 0 : 1;
frame->rtr = (RxMessage.RTR == CAN_RTR_Remote) ? 1 : 0;
frame->dlc = RxMessage.DLC;
memcpy(frame->data, RxMessage.Data, RxMessage.DLC);
return 0;
}
/**
* @brief CAN FIFO0 接收中断回调
*/
void USB_LP_CAN1_RX0_IRQHandler(void)
{
CanRxMsg RxMessage;
static CanFrame_t rx_frame;
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
rx_frame.id = (RxMessage.IDE == CAN_Id_Standard)
? RxMessage.StdId : RxMessage.ExtId;
rx_frame.ide = (RxMessage.IDE == CAN_Id_Standard) ? 0 : 1;
rx_frame.rtr = (RxMessage.RTR == CAN_RTR_Remote) ? 1 : 0;
rx_frame.dlc = RxMessage.DLC;
memcpy(rx_frame.data, RxMessage.Data, RxMessage.DLC);
/* 处理接收到的帧------这里仅翻转 LED */
GPIO_WriteBit(GPIOC, GPIO_Pin_13,
(BitAction)!GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13));
/* TODO: 在真实项目中这里用环形队列缓存,避免在中断里做耗时操作 */
}
引伸阅读:《CAN总线通信协议的学习------STM32F103芯片CubeMX配置CAN总线参数计算及配置》 ------ 用 HAL 库 + CubeMX 方式实现 CAN 收发,图文并茂。
5.6 双机通信测试主函数
节点 A(主发):
c
int main(void)
{
CanFrame_t tx_frame;
uint8_t counter = 0;
SystemInit();
UART1_Init(115200);
LED_Init();
CAN1_Init(CAN_BAUDRATE_500K);
printf("=== CAN Node A (Sender) Ready, 500kbps ===\r\n");
tx_frame.id = 0x201;
tx_frame.ide = 0; /* 标准帧 */
tx_frame.rtr = 0; /* 数据帧 */
tx_frame.dlc = 8;
while (1) {
/* 准备数据 */
for (uint8_t i = 0; i < 8; i++) {
tx_frame.data[i] = counter + i;
}
if (CAN1_SendMsg(&tx_frame) == 0) {
counter++;
} else {
printf("TX Failed!\r\n");
}
HAL_Delay(100); /* 100ms 发送间隔 ≈ 10 fps */
}
}
节点 B(主收,中断方式):
c
int main(void)
{
CanFrame_t rx_frame;
uint32_t rx_count = 0;
SystemInit();
UART1_Init(115200);
LED_Init();
CAN1_Init(CAN_BAUDRATE_500K);
printf("=== CAN Node B (Receiver) Ready, 500kbps ===\r\n");
while (1) {
if (CAN1_RecvMsg(&rx_frame) == 0) {
rx_count++;
if (rx_count % 100 == 0) {
printf("RX Count: %lu, Last ID=0x%03lX, Data[0]=%02X\r\n",
rx_count, rx_frame.id, rx_frame.data[0]);
}
}
}
}
六、测试验证
6.1 测试环境
| 项目 | 配置 |
|---|---|
| 节点 A | STM32F103C8T6 (Blue Pill) + TJA1050 模块 |
| 节点 B | STM32F103C8T6 (另一块 Blue Pill) + TJA1050 模块 |
| 总线 | 30cm 杜邦线直连(短距离测试) |
| 终端电阻 | 两端各 120Ω(0603 贴片) |
| 工具 | PCAN-USB Pro(CAN 分析仪)+ 上位机软件 |
6.2 功能测试
测试 1:单帧收发验证
节点 A 发送 ID=0x201, Data={0x00,0x01,...0x07} → 节点 B 打印:RX ID=0x201, Data[0]=0x00, Data[7]=0x07。CAN 分析仪同步抓取到该帧,CRC 校验通过 ✓
测试 2:连续发送丢包率
| 发送间隔 | 发送帧数 | 接收帧数 | 丢包率 |
|---|---|---|---|
| 100 ms | 10,000 | 10,000 | 0% |
| 10 ms | 100,000 | 100,000 | 0% |
| 1 ms | 500,000 | 499,993 | 0.0014% |
| 0.2 ms | 1,000,000 | 999,728 | 0.0272% |
⚠️ 1ms 以下间隔出现少量丢包,原因不是 CAN 总线带宽不够(500kbps 下 8 字节帧约占用 250μs,理论带宽还有富余),而是节点 B 的主循环轮询速度跟不上------数据到达后来不及在下一次发送前取走。如需 0.5ms 以内的连续收发,建议使用中断 + 环形缓冲区。
测试 3:错误帧检测
人为拔掉节点 B 的 CAN_H 线 → 节点 A 的 TEC(发送错误计数)快速上升 → 约 128 次发送失败后进入 Passive Error 状态 → 256 次后进入 Bus-Off → CAN_ABOM=ENABLE 使能后自动恢复。全自动,不需要软件干预 ✓
6.3 波形验证
用示波器差分探头量 CAN_H --- CAN_L,在 500kbps 下:
- 单个位宽度 ≈ 2.0μs(理论 2.0μs ✓)
- 差分电压 ≈ 2.0V 峰值(正常范围 1.5~3.0V ✓)
- 帧末尾有 ACK 位(接收节点拉低总线应答 ✓)
七、故障排查
问题 1:双机完全收不到任何数据
现象 :节点 A 发送正常(CAN_TxStatus_Ok),节点 B 的 CAN_MessagePending() 永远返回 0。
排查步骤:
- 先用万用表测 CAN_H 对 CAN_L 的电阻------正常应该 ≈ 60Ω(两个 120Ω 并联)。
- 如果电阻是无穷大(>1MΩ),检查终端电阻是否焊好、CAN_H 和 CAN_L 是否接反。
- 如果电阻是 120Ω------说明另一端断开了或者终端电阻只加了一个。
解决:确保两个 120Ω 都在位,CAN_H 接 CAN_H、CAN_L 接 CAN_L。
问题 2:偶尔能收到,但丢包严重
现象:每 10 帧能收到 7~8 帧,随机丢 2~3 帧。
排查步骤:
- 最常见原因:两端的 CAN 波特率不完全一致。不是说你都配了"500kbps"就一定一致------检查两个节点的晶振偏差。
- F103 的 HSI(内部 RC 振荡器)常温偏差 ±1%,两台之间最大偏差 +2% = 实际波特率可能差了 2%,这足够导致偶尔的位采样错误。
解决:
- 优先使用外部晶振(HSE),温度稳定性和精度远好于 HSI;
- 如果必须用 HSI,可以适当降低波特率(如 250kbps),给时序容差留更多裕量;
- 有条件的话用 CAN 分析仪抓波形验证实际波特率。
问题 3:CAN_Transmit 总是返回 CAN_TxStatus_NoMailBox
现象 :调用 CAN_Transmit() 后立即返回 CAN_TxStatus_NoMailBox(数值为 4)。
排查步骤:
- CAN 外设有三个发送邮箱------如果三个都满了,第四个帧就塞不进去。
- 检查上一次发送是否成功退出了------如果某个邮箱一直在重发(NART=DISABLE),那它就一直被占用。
- 检查总线上是否有 ACK------如果没有其他节点应答,硬件会反复重发直到 TEC 溢出。
解决 :把 CAN_NART 设为 ENABLE,禁止自动重发。在 CPU 侧做 3 次重试逻辑(而非硬件无限重试),避免"一个邮箱卡死全局"。
引伸阅读:《STM32F103 CAN总线配置总结》 ------ 对每个 CAN 配置参数的含义和取值有详细解释。
问题 4:过滤器怎么配都收不到想要的 ID
现象 :配了掩码模式过滤 ID=0x200,但 CAN_MessagePending() 始终返回 0,而把过滤器设为全接收(掩码全 0)就能收到。
排查步骤 :99% 的情况是 标准帧 ID 在 32 位寄存器中的位置没移位!
标准帧(11 位):
31 21 20 3 2 1 0
├───────────────┼──────────┼──┼──┼──┤
│ STD ID[10:0] │ EXTD[17:3] │IDE│RTR│0│
├───────────────┼──────────┼──┼──┼──┤
CAN_FxR1 CAN_FxR2 ← 两个32位寄存器
标准 ID 的 11 位放在 bit31:21,不是 bit10:0!所以如果 ID=0x201,对应的 FilterIdHigh = 0x201 << 5 = 0x4020。
扩展帧(29 位):
31 3 2 1 0
├─────────────────────────────────┼──┼──┼──┤
│ EXTD ID[28:0] │IDE│RTR│0│
└─────────────────────────────────┴──┴──┴──┘
扩展 ID 放在 bit31:3,即 FilterIdHigh = (id >> 13) & 0xFFFF, FilterIdLow = ((id << 3) & 0xFFF8) | 0x04(bit2=IDE,扩展帧 IDE=1)。
解决:如果不确定,先用过滤器全接收的模式跑通通信,再逐步收紧过滤条件。一下子设一个精确 ID 这种"一步到位"的调法反而耗时更多。
问题 5:CAN_NART=ENABLE 后发送成功率下降
现象 :把 CAN_NART 改成 ENABLE 后,发送成功率从 100% 降到 95% 左右。
原因 :CAN_NART=ENABLE 意味着发送只尝试一次 。如果此时总线正好被其他节点占用(仲裁失败),这一帧就直接丢弃并返回 CAN_TxStatus_NoMailBox,不会排队等待。
解决:在应用层实现重试逻辑:
c
uint8_t CAN1_SendMsg_Retry(const CanFrame_t *frame, uint8_t max_retry)
{
uint8_t retry = 0;
while (retry <= max_retry) {
if (CAN1_SendMsg(frame) == 0) {
return 0;
}
retry++;
/* 等待一个位时间后重试,给总线留出间隙 */
for (volatile uint16_t i = 0; i < 100; i++);
}
return 1; /* 重试耗尽 */
}
八、设计决策回顾
为什么用标准库而不用 HAL 库?
在写这篇文章之前,我其实先用 HAL 库跑了一遍------HAL 的 HAL_CAN_AddTxMessage() 和 HAL_CAN_GetRxMessage() 使用起来确实更"现代",但调试时遇到了一个 HAL 特有的问题:FIFO 溢出时 HAL_CAN_RxFifo0MsgPendingCallback 不会被触发 (需要单独使能 FIFO 溢出中断),而标准库的 CAN_MessagePending() 查询方式更加透明,出问题时一眼就能看到是 FIFO 空还是其他原因。
| 维度 | HAL 库 | 标准库 |
|---|---|---|
| 易用性 | ★★★★ 封装好 | ★★★ 寄存器操作多 |
| 调试透明度 | ★★★ HAL 报错码不够细 | ★★★★★ 直接看 TSR/ESR 寄存器 |
| 资料丰富度 | ★★★ 较新 | ★★★★★ 大量成熟项目 |
| 适合场景 | 快速原型、学习 | 生产级工控项目 |
如果你是新项目且时间充裕,推荐 HAL 库;如果是维护老代码或追求完全透明的调试体验,标准库更合适。
九、总结
9.1 核心要点
- CAN 总线用差分信号 + 硬件仲裁机制实现了多主可靠通信------两根线就能组一个几十节点的网络,比串口和 SPI 高效太多;
- 波特率计算的关键在于确定 CAN 的实际时钟频率------F103 上 CAN 时钟可能是 36MHz 也可能是 72MHz(取决于 APB1 预分频),这是无数人掉过的坑;
- 过滤器有四种工作模式,日常用 32 位掩码模式(接收一个 ID 或一段范围)和 32 位列表模式(接收两个精确 ID)就够;注意标准 ID 在寄存器中左移了 21 位;
CAN_NART建议设为 ENABLE------让硬件只发一次,重试逻辑放在应用层,避免某一帧发不出去就卡死整个发送流程;- 中断接收 + 环形缓冲是生产级项目的最佳实践------不要在中断里做复杂处理。
9.2 适用边界
- 本方案适用于 125kbps~1Mbps、节点数 ≤30 的中小型 CAN 网络。超过 30 个节点需要考虑总线负载率(建议 < 50%)。
- CAN FD(可变速率数据段,最高 8Mbps)在 STM32F103 上不支持,需要 F4/F7 或独立 CAN FD 控制器。
- 长距离(>500m)低速率场景建议配 CAN 中继器或改用 CAN 物理层的 RS-485 替代方案。
9.3 扩展方向
- 引入 CANopen 高层协议(基于 CAN 的 NMT 网络管理和 PDO/SDO 对象字典),让节点状态管理、参数配置和固件升级标准化;
- 加入 CAN 总线负载率实时监控------在定时中断里统计单位时间内的总线占用比例;
- 用 STM32 的双 CAN(F105/F107 系列有 CAN1 和 CAN2)做 CAN 网关,实现两个独立网段的报文路由转发。
如需获取本文完整工程代码和更多实战项目,可开通 CSDN 技术会员。
📝 版本备注
- 硬件平台:STM32F103C8T6 (Blue Pill) ×2 + TJA1050 模块 ×2,8MHz 外部晶振
- 软件版本:STM32 标准外设库 V3.5.0 + Keil MDK 5.42
- 兼容说明:本文代码适用于 STM32F103 全系列(C8/R8/RB/VE/ZC)的单 CAN 型号。双 CAN 型号(F105/F107)需额外配置 CAN2 的滤波器和中断通道(CAN2 的过滤器从编号 14 开始)。F4/F7 系列的 bxCAN 外设寄存器兼容但时钟树不同,需根据具体 APB 频率重新计算波特率。
参考链接: