
简介本资源是一套面向嵌入式初学者与STM32进阶开发者的CAN通信实战代码包聚焦STM32F103系列芯片的CAN总线底层驱动与应用层实现解决CAN回环测试、双节点对接通信等典型工程问题适用于工业控制、汽车电子等需要可靠串行总线通信的场景。压缩包共95个文件含57个头文件h用于接口定义与寄存器抽象31个C源文件c涵盖HAL库驱动封装、CAN初始化、消息收发及错误处理逻辑另有链接脚本.ld、启动文件.s、CubeMX配置.ioc、Makefile构建文件及PDF参考手册等关键支撑文件整体大小为1.86MB结构清晰、模块解耦。已有2691人学习下载代码严格遵循《STM32专栏》知识体系驱动层高度可移植兼容不同MCU平台配套README与文档说明详实包含CAN时序参数配置要点与实测波形分析思路显著降低CAN协议理解与调试门槛。1. 为什么要单独把 CAN 拿出来写一篇如果你跟我一样是从 51/AVR 转过来玩 STM32 的大概率一开始接触串口 USART 的时候觉得还挺友好——一个波特率寄存器一个数据寄存器中断一发完事了。但第一次调 CAN说实话我整个人是懵的。不是因为 CAN 本身玄乎而是 STM32F103 这颗芯片里的 bxCAN 外设比串口复杂得多有多个邮箱、有过滤器、有波特率分频、还能弄成环回模式调试。如果不把原理吃透代码基本都是靠抄抄完什么现象都出不来也不知道是硬件问题还是配置问题。这篇就是要解决这个问题的。我把 STM32F103 的 CAN 通信从硬件原理到代码框架全部捋了一遍最终实现的是两个开发板通过 CAN 总线互相收发报文的标准例程。这套东西用好之后你后续去理解 CANopen、J1939、甚至汽车级的 CAN FD 都会有底气得多。适合谁看正在学 STM32 基础、准备做板间通信或者小车/机械臂项目的朋友以及那种“串口用腻了想换个更稳的通信方式”的开发者。我默认你对 STM32 的基本 GPIO、时钟、串口配置有概念如果你连怎么建工程都不太熟悉建议先回头刷前几篇基础系列再来看这个。2. CAN 到底是什么和串口/I2C 有啥本质区别说起 CAN 之前得先打破一个惯性思维CAN 不是像串口那样“点对点”的通信方式。CAN 是多主总线什么意思总线上挂的设备没有“主机”和“从机”之分任何一个节点都可以随时往总线上发数据。大家靠报文 ID 来决定谁优先ID 小的优先级高硬件自动仲裁不需要软件去协调。你可以把 CAN 总线想象成一条双向单车道所有车节点都能上这条路但为了避免撞车车上装了“排队系统”——大家同时冲上去的时候ID 小数字小的车有优先通行权其余车自动退回等下一轮再发。这就是 CSMA/CA 的仲裁机制CAN 硬件层已经做好了你只用关心怎么把报文填进发送邮箱。另一个很多人忽略的点是物理层。串口用的是 TTL 电平一根 TX 一根 RX 共地就能跑CAN 不一样它是差分信号——CANH 和 CANL 两根线靠两根线的电压差来表示显性0和隐性1。所以 CAN 的抗干扰能力天然就比串口强传输距离能到几十米甚至上千米这是它在工业现场、汽车上广泛应用的根本原因。再提一下波特率的问题。CAN 的波特率不是你随便写个 9600、115200 就行它和 CAN 控制器的时钟、同步跳转宽度SJW、位段采样点都有关系。很多新手调不通 CAN八成就是波特率配错了或者采样点配得离 80% 太远。后面我给你一套直接能用的计算方式照着算就行。3. STM32F103 的 bxCAN 外设到底有哪些东西3.1 bxCAN 结构一眼看明白F103 里的 CAN 控制器叫 bxCANBasic Extended CAN 的缩写。它内部包含三块核心内容发送邮箱、接收 FIFO、过滤器。发送邮箱有 3 个编号 0/1/2你可以往任意一个邮箱里放待发报文硬件自己会找空闲的邮箱发出去。接收侧有两个 FIFO每个 FIFO 可以存 3 条报文相当于一个深度 3 的硬件队列满了之后新报文就会丢弃并且在状态寄存器里置溢出标志。过滤器是 CAN 外设里最有意思的东西。它就像门口保安——总线上所有报文都会先经过过滤器只有被放行的报文才能进 FIFO 触发接收中断。你可以配置成只收某一个 ID 的报文或者收某个范围内的 ID这样 CPU 就不会被无关报文频繁打断。我实测下来过滤器用得好CAN 通信的稳定性和实时性提升非常明显。关于报文格式F103 的 bxCAN 支持标准帧11 位 ID和扩展帧29 位 ID数据段最长 8 字节。这点和后来 CAN FD 能到 64 字节不同你要做基础通信标准帧完全够用。3.2 波特率是怎么算出来的CAN 的波特率核心公式并不复杂波特率 APB1 时钟 / (预分频值 × (同步段 传播段 相位缓冲段1 相位缓冲段2))F103 的 CAN 外设挂在 APB1 总线上一般来说 APB1 最大 36MHz。如果你用库函数默认配置SystemInit 会把 APB1 设成 36MHz所以下面我们都按 36MHz 来算。正经的 CAN 位时间被分成了 4 段同步段SYNC_SEG固定为 1 个时间量子 Tq传播段PROP_SEG补偿物理延迟相位缓冲段 1BS1相位缓冲段 2BS2采样点位置在 BS1 和 BS2 交界处。业内常规推荐采样点放在 75%~85% 之间汽车级常用 80% 左右这样采样最稳。拿最常用的 500kbps 来举例。总位时间 36MHz / 500kbps 72 Tq。如果你把预分频设成 4那么位时间里面总的 Tq 数就是 72/4 18 个 Tq。在这 18 个 Tq 里SYNC_SEG 固定占 1 个剩下 17 个分给传播段、BS1、BS2。按 75% 采样点来算采样点前有 1SYNC_SEG 传播段 BS1 13.5 个 Tq取整 13那么 BS2 就是 18-135 个 Tq。对应到库函数里就是CAN_InitStructure.CAN_BS1 CAN_BS1_12tq; // 传播段 BS1 共 121 13 个 Tq CAN_InitStructure.CAN_BS2 CAN_BS2_5tq; // 采样点 (1121)/18 77.8% CAN_InitStructure.CAN_Prescaler 4; // 预分频注意库函数里 BS1 的枚举是从 1tq 到 16tq然后另外还有一个独立的传播段寄存器没直接暴露库默认把它设成了 1tq。实际上库函数里 CAN_BS1_12tq 是“传播段BS1”的和所以总位时间 1同步段 12BS1 配置值 5BS2 18。这就是配完 500k 波特率后面再校准的由来。如果你要常用到的其他波特率推荐先把预分频和位段算好再填结构体别随手写个 2 和 8 就以为能通。3.3 环回模式为什么调试神器bxCAN 有四种工作模式正常模式Normal、静默模式Silent、环回模式Loopback、静默环回模式Silent Loopback。其中环回模式最香——它不需要外部总线连接发送的报文会直接在芯片内部被接收邮箱收回来。也就是说你只需要一块板子就能调试 CAN 发送和接收逻辑。我当时调第一版代码时就是先配成环回模式发一条报文看能不能在接收中断里抓到抓到了再改回正常模式去连真实总线。这在没有 CAN 分析仪、只有两块板子的情况下特别实用可以先把软件流程跑通再排查硬件接线问题。4. 代码框架与初始化配置拆解4.1 GPIO 初始化别把引脚复用搞错了打开 F103 的数据手册CAN 的引脚映射有两条路默认复用功能是 PA11RX和 PA12TX重映射之后是 PB8RX和 PB9TX。一般用默认的 PA11/PA12 就行。初始化 GPIO 时有个非常容易踩的坑一定要把引脚复用模式设成 AF_PP复用推挽输出。写成普通推挽输出 GPIO_Mode_Out_PPCAN 是发不出数据的。GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); 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); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure);这里 RX 引脚 PA11 要选上拉输入IPU因为 CAN 总线空闲时候是隐性电平1如果引脚悬空或者浮空输入上电瞬间可能出现不确定状态导致控制器误判总线忙。4.2 CAN 初始化最有一堆反人类的位段配置看完 GPIO接下来是 CAN 本体初始化。这一步可以拆成两个部分基础参数和过滤器配置。先看基础参数CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_TTCM DISABLE; // 时间触发通信模式不用 CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线管理建议打开 CAN_InitStructure.CAN_AWUM ENABLE; // 自动唤醒模式 CAN_InitStructure.CAN_NART DISABLE; // 非自动重传关闭即自动重传 CAN_InitStructure.CAN_RFLM DISABLE; // 接收 FIFO 锁定不锁定 CAN_InitStructure.CAN_TXFP DISABLE; // 发送优先级由报文 ID 决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_12tq; CAN_InitStructure.CAN_BS2 CAN_BS2_5tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure);这几个开关我挑重要的说一下。CAN_ABOM自动离线管理一定打开。CAN 控制器在总线错误严重时硬件会自动进入 Bus-Off离线状态退出离线需要检测到 128 次总线空闲。如果不开 ABOM软件必须手动恢复很多人的程序跑着跑着 CAN 就“死了”就是没开这个。CAN_TXFP是发送优先级策略开关。置 ENABLE 时按发送请求顺序来先请求先发置 DISABLE 时按报文 ID 大小来ID 小的优先。默认按 ID 仲裁即可符合 CAN 协议本身的机制。CAN_NART置 ENABLE 表示出错不重传发一次失败就丢置 DISABLE 表示自动重传一直发到成功。工业通信场景建议关闭 NART即开启自动重传保证报文必达。过滤器配置代码如下CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN1, CAN_FilterInitStructure);我演示的是“接收所有报文”的配置Mask 全 0 表示不检查任何位。你在实际项目里如果只想收某个 ID比如 0x123就要把 FilterIdHigh/Low 填成对应 ID 的位置Mask 全 1。这部分细节比较多我后面单独用一节讲。4.3 注意先看 CAN 初始化返回值库函数CAN_Init()是有返回值的返回CANINITOK才算成功。很多人写完初始化也不检查返回值结果 CAN 根本没初始化成功后面怎么调都不通。建议加个断言或者 while 循环等待初始化成功if (CAN_Init(CAN1, CAN_InitStructure) ! CANINITOK) { // 初始化失败建议在这里点亮一个错误指示灯或者打印错误信息 }5. 发送报文代码从结构体到邮箱5.1 CanTxMsg 结构体的每个字段都别漏用标准库做 CAN 发送核心是构造一个CanTxMsg结构体然后调用CAN_Transmit()。CanTxMsg TxMessage; TxMessage.StdId 0x123; // 标准 ID11 位 TxMessage.ExtId 0; // 扩展 ID标准帧用不到置 0 TxMessage.IDE CAN_Id_Standard; // 使用标准帧 TxMessage.RTR CAN_RTR_Data; // 数据帧不是远程帧 TxMessage.DLC 8; // 数据长度1~8 TxMessage.Data[0] 0x01; TxMessage.Data[1] 0x02; TxMessage.Data[2] 0x03; TxMessage.Data[3] 0x04; TxMessage.Data[4] 0x05; TxMessage.Data[5] 0x06; TxMessage.Data[6] 0x07; TxMessage.Data[7] 0x08; uint8_t mailbox CAN_Transmit(CAN1, TxMessage);CAN_Transmit()的返回值很关键它返回的是实际使用的邮箱号0、1、2如果返回CAN_TxStatus_NoMailBox则说明三个发送邮箱全满这一次发送没提交成功。所以在循环发送的场景里一定要判断返回值邮箱满了可以稍等再重发。顺带说一句RTR字段很少有人用远程帧因为远程帧本身只是请求数据不带数据内容。你需要知道它有这个字段但调试前期全部用数据帧就行远程帧属于协议层玩法后面做 CANopen 再研究不迟。5.2 发送完成怎么确认调CAN_Transmit()只是把报文塞进邮箱不代表已经发到总线上了。确认发送完成要用CAN_TransmitStatus()看邮箱状态uint8_t mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox ! CAN_TxStatus_NoMailBox) { while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { // 等待发送完成 } }如果你开了自动重传NART DISABLE这个 while 有可能因为总线错误一直等不到 Ok 而卡死。稳妥的做法是加个超时计数比如循环 10000 次还没 Ok 就放弃并置错误标志。由于 CAN 是从发送邮箱里取走报文后置位发送成功标志发送中断使能后会在CAN_IT_TxMailbox0/1/2中断里置位对应标志位。实际项目里用中断判断更高效但基础阶段用轮询其实够了核心是别把流程搞死。6. 接收报文代码中断方式与查询方式6.1 最稳的方式挂接收中断用 CubeMX/标准库搞 CAN 接收标准做法是打开 FIFO0 的接收中断在中断服务函数里取报文。这样总线上来了报文硬件自动收进 FIFOCPU 不用一直轮询。开启接收中断CAN_ITConfig(CAN1, CAN_IT_FIFO0_MessagePending, ENABLE);这里我特意用了CAN_IT_FIFO0_MessagePending它的含义是 FIFO0 有报文等待读取时产生中断。有的老代码喜欢用CAN_IT_FIFO0_Overrun之类但那个是溢出中断不是收报文中断别搞混。中断服务函数写法void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FIFO0_MessagePending) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 在这里处理收到的报文 // 例如把 ID 和数据保存到全局变量 g_rx_id RxMessage.StdId; memcpy(g_rx_data, RxMessage.Data, RxMessage.DLC); } }注意中断服务函数的名字USB_LP_CAN1_RX0_IRQHandler是因为在 STM32F103 的中断向量表里CAN1 的 RX0 中断和 USB 低优先级中断共用一个入口。你打开 stm32f10x_it.c 就能看到这个函数名别自己随便改。接收的中断标志清除问题也容易踩坑。CAN_GetITStatus配合CAN_ClearITPendingBit是标准清中断姿势但有时你只调 GetITStatus 不清标志中断会一直进来而清错标志位又会把该收的报文丢掉。推荐在函数末尾加CAN_ClearITPendingBit(CAN1, CAN_IT_FIFO0_MessagePending);FIFO 在读取报文时也会自动释放队列空间但不是清中断标志这两个是独立的事情同步做就行。6.2 查询方式适合什么场景查询方式就是 main 主循环里反复调用CAN_Receive()判断是否收到。它的好处是代码简单、不容易因为中断优先级设置不当导致丢报文坏处是如果主循环里还有其他耗时的任务接收实时性会受影响。CanRxMsg RxMessage; if (CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); // 处理报文 }CAN_MessagePending()返回 FIFO0 里当前待读的报文条数。如果返回值是 3说明 FIFO 已经满了新报文会被硬件丢弃。这个时候软件上要做的事情是把数据处理挪到中断里别在主循环里拖。我自己做基础通信的时候用的是“中断标志位”的方式中断里只收数据、置标志主循环里检测到标志再去做长耗时处理。这样既保证了实时性又不阻塞主循环。7. 过滤器配置的三种常用玩法7.1 玩法一全收模式适合初学和抓包// FilterIdHigh/Low 全 0Mask 全 0掩码模式下Mask 对应位为 0 表示“这一位不关心”为 1 表示“这一位必须匹配”。全 0 就是什么都不匹配所有报文都收。适合你刚开始调通信、还不想管 ID 过滤的时候。7.2 玩法二只收一个 ID适合点对点控制比如只想收 ID 为 0x123 的标准帧。这时候要填的寄存器比较多我直接给换算好的写法CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; // 标准帧 ID 是 11 位存在寄存器的高 16 位里 // 0x123 5 0x2460 CAN_FilterInitStructure.CAN_FilterIdHigh 0x2460; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; // Mask 全 1 表示必须每一位都匹配 CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFFF; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0xFFFF; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN1, CAN_FilterInitStructure);为什么是0x123 5因为标准帧 ID 是 11 位而 32 位过滤器寄存器里STID[10:0] 在 bit[31:21]后面还跟着 EXID[17:0] 等字段。库函数为了统一处理让你把 ID 左移 5 位填到高 16 位寄存器。这个细节非常容易漏网上很多代码直接填 0x123结果过滤器根本没生效。7.3 玩法三掩码匹配一个范围如果你想让节点接收一组连续 ID比如 0x100~0x107 这 8 个 IDMask 就设成保留高 8 位、低 3 位不关心。对应关系是0x100 的二进制是 001_0000_00000x107 是 001_0000_0111高 8 位都是 001_0000_0低 3 位变化。所以 Mask 的高 8 位全 1、低 3 位全 0组合出来就是 0xFFF8。// FilterIdHigh (0x100 5) 0x2000 // FilterMaskIdHigh (0xFFF8 5) 0xFFF0注意低 3 位被挪走后Mask 对应的左移 5 位会变成 0xFFF0不是 0xFFF8。这个地方我当时也换算错过你直接抄我算好的值就行原理理解了后续自己也能算。8. 硬件连接与常见坑8.1 最小系统怎么接线两块 STM32F103 最小系统板之间做 CAN 通信除了两个 CAN 控制器还需要一个CAN 收发器——这是新手最容易忽略的点。STM32 的 CAN 控制器输出的是 TX/RX 数字信号不能直接驱动差分总线必须经过收发器。市面上最常见的收发器是 TJA1050 或者 SN65HVD2303.3V 供电版本。接线如下STM32F103 (节点1) TJA1050 (节点1) PA12 (CAN_TX) ------- TXD (Pin 1) PA11 (CAN_RX) ------- RXD (Pin 4) 5V (Pin 3) ----- 接 5V 电源 GND (Pin 2) ----- 接 GND CANH (Pin 7) ----- 总线 CANH CANL (Pin 6) ----- 总线 CANL VREF / Rs 引脚通常接 5V 或通过跳线配置节点 2 的接法完全一样两个节点的 CANH 连 CANHCANL 连 CANL然后在总线的两端各跨接一个120Ω 终端电阻。120Ω 的作用是匹配传输线阻抗防止信号反射。如果只有两个节点就是两端各一个中间不需要再加电阻。很多新手只在一头加电阻或者干脆不加短距离低速比如桌面测试还能勉强工作但距离拉长、波特率提高之后就各种随机错误帧。8.2 为什么 CAN_H 和 CAN_L 量不到 3.3V用万用表量 CANH 对 GND 电压大约 2.5VCANL 对 GND 大约 2.5V两者之差接近 0V——这是正常的也就是说总线处于隐性状态空闲。只有当节点发送显性位逻辑 0的时候CANH 会被拉到约 3.5VCANL 约 1.5V差值 2V 左右。所以你拿万用表去量静态电压量到 0V 别慌不代表没电而是总线空闲。想看波形最好用示波器两个探头分别接 CANH/CANL用差分模式看。如果没有示波器可以拿另一块板子试着发报文发的时候电压会瞬间波动。8.3 共地问题CAN 是差分信号理论上不要求两个节点严格共地但收发器本身的工作电源仍然需要参考地。如果两个节点是独立供电的最好把它们的 GND 也连在一起否则收发器输入端的共模电压可能超范围导致波形畸变、丢帧。虽然很多人说 CAN 隔离可以避免共地但至少在你基础测试阶段老老实实共地。9. 从 CubeMX 配置到标准库代码的迁移思路现在很多人习惯用 STM32CubeMX 生成 HAL 库代码但市面上大量网上教程和旧项目还是标准库。我自己两边都写过这里直接给你一个快速迁移的思路不要在新老 API 之间迷失。HAL 库的 CAN 初始化核心结构体是CAN_InitTypeDef它把波特率配置拆成了这样hcan.Instance CAN1; hcan.Init.Prescaler 4; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_13TQ; hcan.Init.TimeSeg2 CAN_BS2_5TQ; hcan.Init.Mode CAN_MODE_NORMAL;注意HAL 库里TimeSeg1对应的值已经包含了传播段BS1也就是说 13TQ 直接对应标准库里的CAN_BS1_12tq。这个映射关系搞清楚CubeMX 生成的配置参数就能平移到标准库。发送部分 HAL 库用CAN_SendMessage配合CAN_TxHeaderTypeDef接收用CAN_Receive配合CAN_RxHeaderTypeDef本质上和标准库的CanTxMsg/CanRxMsg一样只是字段名变了。你先在标准库上跑通逻辑再对照手册换 HAL 库 API基本上半小时就能改完。10. 调不通我踩过的那些坑全在这里10.1 现象一CAN 完全无反应寄存器读出来全是 0先查时钟有没有打开RCC_APB1Periph_CAN1同时有没有把 GPIOA 的时钟打开CAN 外设挂在 APB1 上引脚挂 APB2两个时钟分开使能漏一个都不行。再查引脚模式TX 引脚必须是AF_PPRX 引脚必须是IPU。如果 TX 设成普通推挽输出波形出不来或者电平不对通信必失败。接着查模式有没有把 CAN 设成 Normal 模式环回模式只用于自测接真实总线必须 Normal。10.2 现象二连接正常但一直收不到数据从这两个方面排查对方有没有真的把数据发出来用示波器看 CANH/CANL 差分波形或者用 CAN 分析仪抓包。如果总线上根本没有波形问题在发送端不是接收端。波特率是否一致两端控制器设置的采样点和位段比例必须完全一致不能一端 500k 一端 250k也不能都写 500k 但 BS1/BS2 比例不同。CAN 不像串口波特率有一点点偏差都会直接影响采样乱收乱丢。10.3 现象三能发能收但偶尔出现错误帧和 Bus-Off这种基本都是物理层问题。检查 120Ω 终端电阻是否在总线两端各有一个检查 CANH/CANL 是否接反收发器上两个引脚接反时显性/隐性电平就反了通信肯定失败检查节点间地线是否可靠连接。还有一种情况是波特率过高而总线上有较大寄生电容导致边沿不够陡峭。基础阶段先跑 500k如果还是不稳定降到 250k 试试同时把线材尽量绞合、缩短。10.4 现象四程序复位或者下载之后 CAN 没反应看 CAN 有没有进入 Bus-Off 状态。用调试器读CAN_ESR寄存器的 BOFF 位如果是 1说明之前总线错误太严重控制器离线了。这时如果CAN_ABOM没打开控制器不会自动恢复必须软件复位或者重新初始化。if (CAN_GetFlagStatus(CAN1, CAN_FLAG_BOF) ! RESET) { // Bus-Off 了恢复策略重新初始化或者等待 ABOM 自动恢复 CAN_ClearFlag(CAN1, CAN_FLAG_BOF); CAN_Init(CAN1, CAN_InitStructure); }如果你确认 ABOM 已经打开总线恢复正常后它应该自动重新上线但有些老版本库对 ABOM 的实现有问题建议自己在错误中断里做一次判断必要时手动重新初始化。11. 一个能直接跑双机通信的完整示例上面知识点讲得有点散了这里我整理一份完整的示例——两块板子一个发一个收。发送节点每 500ms 发送一次 ID 为 0x123、8 字节递增数据的报文接收节点收到后把 ID 和数据通过串口打印出来。11.1 发送节点核心代码main 函数里int main(void) { // 系统时钟、GPIO、串口等初始化代码省略 CAN_Config(); uint8_t counter 0; while (1) { CanTxMsg TxMessage; TxMessage.StdId 0x123; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC 8; for (int i 0; i 8; i) { TxMessage.Data[i] counter i; } uint8_t mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox ! CAN_TxStatus_NoMailBox) { // 等待发送完成带超时保护 uint16_t timeout 10000; while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok timeout--) { } } counter; delay_ms(500); } }11.2 接收节点核心代码中断方式volatile uint8_t g_can_rx_flag 0; CanRxMsg g_rx_message; void USB_LP_CAN1_RX0_IRQHandler(void) { if (CAN_GetITStatus(CAN1, CAN_IT_FIFO0_MessagePending) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, g_rx_message); g_can_rx_flag 1; CAN_ClearITPendingBit(CAN1, CAN_IT_FIFO0_MessagePending); } } int main(void) { // 初始化代码省略 CAN_Config(); CAN_ITConfig(CAN1, CAN_IT_FIFO0_MessagePending, ENABLE); while (1) { if (g_can_rx_flag) { g_can_rx_flag 0; printf(ID: 0x%03X, Len: %d\r\n, g_rx_message.StdId, g_rx_message.DLC); for (int i 0; i g_rx_message.DLC; i) { printf(Data[%d]: 0x%02X\r\n, i, g_rx_message.Data[i]); } } } }这套代码我用 F103 最小系统板配合 TJA1050 实测过两个节点 60cm 双绞线、500k 波特率、两端各一颗 120Ω连续跑一晚上没有出现错误帧。如果你自己调的时候有问题大概率是 GPIO 复用配置或者波特率位段没算对回到前面章节慢慢对。12. CAN 调试工具怎么选你不可能永远靠两块开发板互相调工程稍微大一点必须借助工具。我用过的调试工具大致分三类。12.1 USB-CAN 分析仪推荐周立功的 USBCAN-II 或者它的国产平替。这类工具其实就是把电脑的 USB 转成一路 CAN配合上位机软件可以抓包、发报文、监控总线负载。基础调试里最有用的是“报文回放”功能——先用分析仪抓一段正常通信的报文然后回放给被测设备看它有没有正确响应。分析仪的波特率设置要和被测设备一致这个不能马虎。12.2 逻辑分析仪如果你手头预算有限或者只是临时验证波形可以整一个 24MHz 采样率的逻辑分析仪直接抓 PA11/PA12 的 TTL 信号或者抓收发器 TXD/RXD 的数字波形。CAN 协议是串行数据用逻辑分析仪也能解出 ID 和数据只是需要软件里选好协议解析插件。不过逻辑分析仪只能看数字信号看不到 CANH/CANL 的差分电平遇到物理层问题时还得示波器。12.3 示波器示波器不是必需品但看物理层波形确实最直接。把两个探头接 CANH/CANL用数学通道算差分能看到显性位和隐性位的电压切换、边沿斜率、毛刺等。如果你在整车或工控现场调试示波器几乎是必须的。不过基础学习阶段先用分析仪把协议层跑通遇到信号完整性问题再上示波器效率更高。13. 从 CAN 基础到 CAN FD多走一步的思考在你把基础 CAN 跑通之后我建议顺手了解一下 CAN FD——Controller Area Network with Flexible Data-Rate。CAN FD 和经典 CAN 最大的区别是两个数据段长度从 8 字节提升到 64 字节数据段的波特率可以比仲裁段高很多比如仲裁段 500k数据段 2M。这样在传输大数据块时效率提升非常明显。但 STM32F103 的 bxCAN 不支持 CAN FD它只支持经典 CAN 2.0B。想做 CAN FD 实验得换带 FDCAN 外设的芯片比如 STM32G4、STM32H7 系列。不过你理解了经典 CAN 的位定时、采样点、仲裁机制之后CAN FD 无非是在同一位时间框架里多了一个可变波特率段原理就像把同一个串口在收发过程中临时切换波特率一样。基础打牢了后面升级路线很平滑。最后再分享一个我自己调 CAN 多年的习惯——永远在初始化后加一个自环测试先配成 Loopback 模式发一条报文确认能收到再切回 Normal 模式接总线。这套工序看起来多花十几秒但能帮你区分“软件配置问题”和“总线物理问题”可以说是我调试 CAN 省时间最有效的一招了。本文还有配套的精品资源点击获取