news 2026/10/22 5:29:31

STM32F103 CAN双机通信实战:从硬件连接到波特率计算与错误排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 CAN双机通信实战:从硬件连接到波特率计算与错误排查

1. 项目概述与需求分析

STM32F103 的 CAN 通信,对很多刚接触嵌入式通信的人来说,是既熟悉又陌生的一块。熟悉是因为它是目前工业控制、车载电子里用得最多的总线之一,任何招聘嵌入式工程师的岗位描述里几乎都会写“熟悉 CAN 总线优先”;陌生是因为大部分教程只讲了初始化寄存器、发个报文,真到了双机对接、波形异常、总线错误的时候,就完全不知道怎么下手了。

这篇实战记录要解决的,正是从“会点灯”到“CAN 通起来”之间的那道坎。我以 STM32F103C8T6 最小系统板为平台,配合两片最常见的 TJA1050 收发器,完整跑了一遍双机 CAN 通信全流程:从硬件连接、波特率计算、过滤器配置,到双机互相收发、错误处理与排查。整个过程不涉及复杂的 RTOS 或 Bootloader,就是最纯粹的双机裸机通信,但这套流程是所有 CAN 高级应用的基础。

适合看这篇文章的人,主要分三类:第一类是刚学完 STM32 基础外设、想接触 CAN 但被各种资料绕晕的新手;第二类是已经在用 CAN 做项目,但遇到总线通信不稳定、丢帧、错误帧不知道怎么排查的开发者;第三类是准备用 CAN 做多节点组网,但还没彻底搞明白过滤器怎么配的人。如果你属于其中任何一类,这篇文章应该能帮你省下不少自己踩坑的时间。

在这套项目中,我的关注重点不是“怎么把代码调通”,而是“为什么这样设计”。比如为什么外部收发器要选 TJA1050 而不是直接用芯片内置的?为什么双机通信里面,发送节点的终端电阻必不可少?为什么波特率算出来是 500kbps,但示波器量出来的实际位时间对不上?这些才是实际开发中真正卡人的地方。

2. 双机通信的整体方案设计

2.1 为什么选择 STM32F103 + TJA1050 这套组合

先聊硬件选型。STM32F103 系列的 CAN 控制器是 bxCAN,全称是 Basic Extended CAN,基本型扩展 CAN 控制器。它支持 CAN 协议 2.0A 和 2.0B,标准帧和扩展帧都能处理,内置了 3 个发送邮箱、2 个接收 FIFO(每个 FIFO 有 3 个邮箱深度),以及 28 个滤波器组。对于绝大多数中小规模的双机通信或者少节点组网,这个外设规格完全够用,而且 STM32F103 的生态资料非常丰富,哪怕是完全没有接触过 CAN 的人,从零开始也不会特别吃力。

但有一个关键点必须说明:STM32F103 内部只有 CAN 控制器,没有 CAN 收发器。这是所有嵌入式初学者的第一个认知误区——CAN 控制器负责的是协议层,包括报文封装、仲裁、错误检测、位填充这些逻辑;而真正把逻辑电平转换成差分信号、驱动总线传输的,是外部的 CAN 收发器芯片。这就好比一个人负责起草信件内容(控制器),另一个人负责把信寄出去、收回来(收发器),两者缺一不可。

TJA1050 是恩智浦(NXP)推出的高速 CAN 收发器,最高支持 1Mbps 的通信速率。它兼容 ISO 11898 标准,能与 TJA1040、PCA82C250 互相替代,引脚在大多数场景下直接兼容。我选择 TJA1050 而不是更老的 PCA82C250,主要原因是它的电磁辐射更低,而且在总线短路、热关断这些异常保护上做得更完善。实测下来,在同样 500kbps 的波特率下,TJA1050 的差分输出波形边沿更干净,这对排查总线问题是很大的优势。

2.2 硬件连接的几个关键设计点

下面列出我实际使用的连接方案,这部分是很容易出问题的地方。

CAN 控制器与收发器之间的连接相对固定:STM32F103 的 PA11 是 CAN_RX,PA12 是 CAN_TX,分别接到 TJA1050 的 RXD 和 TXD 引脚。TJA1050 的 VCC 接 3.3V 或 5V 都可以,但要注意:STM32F103 的 GPIO 是 3.3V 逻辑,而 TJA1050 的 TXD/RXD 引脚逻辑电平范围是兼容 3.3V 的,所以电源直接统一用 3.3V 就能正常工作,不需要做电平转换。很多新手在这里会画蛇添足加个电平转换电路,反而是多余的。

总线侧的连接才是真正的重点。CANH 和 CANL 之间需要接一个 120Ω 的终端电阻,而且这个电阻并不是随便接的。在双机通信的场景下,总线的两端各需要接一个终端电阻,也就是说如果你有两个节点,那么每个节点的 CANH 和 CANL 之间都要接 120Ω。如果你用的是现成的带收发器模块(比如市面上常见的 CAN 模块),有些模块上已经焊好了 120Ω 电阻,此时要确认一下自己接了几个模块,避免重复并联导致终端电阻变成 60Ω 甚至更低,那样会加大收发器负载,通信距离和稳定性都会下降。

供电方面有个容易被忽略的问题。TJA1050 的工作电流虽然不大,但在总线活动频繁时会有一定的电流波动。如果你直接用 STM32 板子上的 3.3V LDO 给收发器供电,而这时候板子还接着其他大电流外设,就有可能导致 3.3V 纹波偏大,间接影响 CAN 信号质量。我建议给收发器独立供电,或者至少确保 3.3V 电源有足够的余量和去耦电容。这个细节在短距离、低速率下可能影响不大,但在长线或者高温环境下就会被放大。

2.3 波特率计算:从时钟树到寄存器配置

CAN 波特率不是随便填一个数就能用的,它由 CAN 控制器的时钟频率、预分频器(BRP)、时间段(TSEG1、TSEG2)和同步跳转宽度(SJW)共同决定。STM32F103 的 CAN 外设挂在 APB1 总线上,而 APB1 时钟最大是 36MHz。如果你使用的是外部 8MHz 晶振,并且系统时钟设置为 72MHz,那么 APB1 分频器通常设为 2,所以 APB1 时钟就是 36MHz。但是请注意,CAN 外设自己还有一个独立的预分频器,它是在 APB1 时钟的基础上再次分频的。

标准位时间的计算公式是:

波特率 = CAN 时钟频率 / (BRP × (1 + TSEG1 + TSEG2))

其中,BRP 是波特率预分频器的分频系数,TSEG1 和 TSEG2 是时间段长度,单位都是 CAN 时钟周期。对于 500kbps 的目标波特率,36MHz 的 CAN 时钟,常见的一组参数是:BRP = 4,TSEG1 = 13,TSEG2 = 2。代入公式就是:

500000 = 36000000 / (4 × (1 + 13 + 2)) = 36000000 / (4 × 16) = 36000000 / 64 = 562500

算出来是 562.5kbps,不对。这就说明 36MHz 时钟下,BRP 和位时间的可选项无法精确得到 500kbps。所以实际应用中更常见的配置是 BRP = 9,TSEG1 = 6,TSEG2 = 1,此时:

500000 = 36000000 / (9 × (1 + 6 + 1)) = 36000000 / (9 × 8) = 36000000 / 72 = 500000

这组参数刚好是整的,所以 STM32 标准库例程里的 CAN 初始化代码,500kbps 用的就是这组数字。我建议你在做任何波特率相关的调整时,先用这个公式自己手算一遍,不要直接抄例程里的数字。因为如果你换了外部晶振(比如 12MHz 或 25MHz),或者改了系统时钟配置,同样的寄存器参数对应的实际波特率就会完全变掉。

另外还需要注意位时间的采样点位置。采样点是总线电平被真正采样的时刻,它位于位时间的末端附近。工业界常用的推荐值是 75% 到 80%,也就是让 TSEG1 占整个位时间的比例在 75% 左右。在上面的配置中,TSEG1 = 6,整个位时间为 8 个周期,采样点位置是 (1 + 6) / 8 = 87.5%。这个值偏高了,在极短距离、低负载的实验中问题不大,但在长距离、多节点场景下容易因为在位末尾采样到毛刺而出错。如果你要组一个超过 10 个节点的网络,建议把采样点调到 80% 附近,比如 BRP = 9,TSEG1 = 5,TSEG2 = 2,这样采样点位置是 (1 + 5) / 8 = 75%。

3. 核心代码逻辑与配置细节

3.1 初始化流程:GPIO 到 CAN 外设的配置顺序

CAN 的初始化顺序是有讲究的,很多人一上来就配置 CAN 外设,结果发现 TX/RX 引脚根本没使能。正确的顺序是:先开启 GPIO 和 CAN 外设的时钟,然后配置 PA11 和 PA12 的复用功能,最后再操作 CAN 控制器的寄存器。

PA11 和 PA12 默认是普通 GPIO,必须设置为复用推挽输出(AF_PP)和复用开漏输入(AF_OD),才能把它们交给 CAN 外设控制。这里有个经验:PA11 是 RX,也就是 CAN 控制器的数据输入脚,TX 引脚 PA12 设置为复用推挽输出,RX 引脚设置为浮空输入或复用开漏输入都可以,只要不配置成普通推挽输出就行,因为普通推挽输出会把引脚强制拉高或拉低,直接破坏总线上的隐性电平。

下面是一段基于标准库的初始化代码,我用的是常见的库函数风格,方便对照自己的工程:

void CAN_Init_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, 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); CAN_DeInit(CAN1); CAN_InitStructure.CAN_TTCM = DISABLE; CAN_InitStructure.CAN_ABOM = ENABLE; CAN_InitStructure.CAN_AWUM = DISABLE; CAN_InitStructure.CAN_NART = DISABLE; CAN_InitStructure.CAN_RFLM = DISABLE; CAN_InitStructure.CAN_TXFP = DISABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_6tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_1tq; CAN_InitStructure.CAN_Prescaler = 9; CAN_Init(CAN1, &CAN_InitStructure); }

这段配置里有两个选项值得单独解释。CAN_ABOM(自动离线恢复)我设置为 ENABLE,意思是当 CAN 控制器因为总线错误进入 Bus-Off 状态后,它会自动等待 128 个总线空闲信号,然后重新恢复通信。在双机通信实验中,如果对端设备断电或者拔出,总线长期处于错误状态,有 ABOM 的控制器能自动恢复,不用人工重启。而 CAN_NART(非自动重传)保持 DISABLE,意思是当发送失败时控制器会自动重传,直到成功或进入 Bus-Off。这在普通数据上报场景下非常有用,但在某些对数据时效性要求极高的控制场景下,可能需要打开 NART,避免旧数据反复占用总线。

3.2 滤波器配置:接收不迷路的关键

CAN 的滤波器是外设的一个独立硬件模块,它决定了哪些报文能够进入接收 FIFO,哪些报文会被硬件直接丢弃。这个机制能大幅减轻 CPU 的负担,也是 CAN 总线能够支撑多节点同时通信的基础之一。

在双机通信的简单场景里,最直接的方式是使用屏蔽位模式,也就是让滤波器接受所有报文,然后在中断里用软件判断帧 ID。这种方式的好处是简单,不管对端发的什么 ID 都能收到;坏处是如果总线上有很多节点在通信,CPU 会被无关的中断频繁打扰。

更规范的做法是使用标识符列表模式,在硬件层面只放行你想要的那个帧 ID。注意,STM32F103 的 bxCAN 滤波器有两种工作模式——屏蔽位模式和列表模式。屏蔽位模式可以理解为一个“通配符”,它指定哪些位必须精确匹配、哪些位无关紧要;列表模式则是精确指定一个或两个帧 ID,只有完全一致的才能通过。

以下是列表模式过滤单个 ID 的配置,过滤 ID 为 0x123:

void CAN_Filter_Config(void) { CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber = 0; CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdList; CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh = 0x123 << 5; CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x123 << 5; CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(CAN1, &CAN_FilterInitStructure); }

这里有一个很多教程没讲清楚的地方:在 32 位滤波器配置里,帧 ID 不是直接填进寄存器的,它需要根据标准帧还是扩展帧做移位。对于标准帧,帧 ID 占据寄存器的高 11 位,也就是需要左移 5 位(因为标准帧 ID 后面还有 RTR 位和 IDE 位等控制位)。这也是为什么0x123 << 5而不是直接填0x123的原因。

如果你觉得手动移位容易出错,还有一个更省事的办法:使用标准外设库提供的CAN_FilterInit配合结构体时,很多版本的库函数已经帮你处理好了,但在寄存器级别操作时一定要搞清楚这一点。我的建议是,写完后打印一下寄存器值,确认 FilterIdHigh 里存的是不是0x123 << 5的结果,这一步能避免大量排查时间的浪费。

3.3 发送与接收中断:两种常用做法对比

发送部分的代码有两种主流写法。第一种是轮询方式,也就是把报文放入发送邮箱后,不断查询发送状态寄存器,直到确认发送完成。第二种是中断方式,发送完成后触发 TX 中断,在中断回调里做后续处理。双机通信实验中,我推荐先用轮询方式,逻辑链路简单,容易确认是否真的发出去了;等到你确实需要非阻塞发送时,再切换成中断不迟。

发送一封标准帧的核心代码如下:

uint8_t CAN_SendFrame(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; TxMessage.StdId = id; TxMessage.ExtId = 0; TxMessage.IDE = CAN_Id_Standard; TxMessage.RTR = CAN_RTR_Data; TxMessage.DLC = len; for (uint8_t i = 0; i < len; i++) { TxMessage.Data[i] = data[i]; } uint8_t mailbox = CAN_Transmit(CAN1, &TxMessage); uint8_t timeout = 0; while (CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok && timeout < 200) { timeout++; } if (CAN_TransmitStatus(CAN1, mailbox) == CAN_TxStatus_Ok) { return 1; } else { return 0; } }

接收我建议直接用中断方式。因为一个健壮的通信节点不能一直轮询接收,否则会错过总线上的突发报文。CAN 接收中断的配置思路是:在 CAN 初始化完成后,配置 NVIC 的 CAN1_RX0_IRQn 通道并设置优先级,然后在中断服务函数里调用库提供的接收函数把 FIFO 里的报文取出来。

void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); // 处理收到的报文 process_can_message(&RxMessage); CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }

一个细节:FMP0 是 FIFO0 的“报文挂起”中断标志,它表示 FIFO0 里至少有一封报文等待读取。读出来之后标志不会自动清除,需要手动调用CAN_ClearITPendingBit,否则即使 FIFO 已经空了,中断标志仍然置位,会反复进入中断。这个坑我遇到过不止一次。

4. 双机联调的完整实操过程

4.1 实验平台与环境准备

这次双机通信实验,我用了两块 STM32F103C8T6 最小系统板,每块板上外接一个独立的 TJA1050 CAN 收发器模块。两块板子的 CAN_TX 和 CAN_RX 分别接到各自收发器的 TXD 和 RXD,然后两块收发器的 CANH 接 CANH、CANL 接 CANL,接线颜色尽量统一,避免长距离飞线导致信号串扰。

实验中的通信速率为 500kbps,一帧标准数据帧的负载是 8 字节。A 机(节点 A)作为主发节点,周期性地向 B 机(节点 B)发送一帧 ID 为 0x11 的报文,内容是 0 到 7 递增的字节序列;B 机收到报文后,解析数据并将接收到的字节数回传给 A 机,ID 设为 0x22,实现一个最简单的“请求—应答”握手。

我用逻辑分析仪同时抓取了 CAN_TX 引脚上的 TTL 信号和 CANH/CANL 之间的差分电压波形。因为逻辑分析仪不能直接解析差分总线信号,我选择观察收发器输入侧的 TXD 信号,这是一个与 CAN 控制器侧电平完全一致的 TTL 信号,能直观看出报文的帧起始、仲裁字段、数据字段和 CRC 字段。

4.2 上电后的联调步骤

整个联调过程我按照下面的顺序推进,每一步都确认无误后再进下一步。

先做静态检查。用万用表测量每个节点 CANH 和 CANL 之间的电阻,确认是 120Ω。如果测出来是 60Ω,说明两个节点各接了 120Ω 电阻并联了;如果是接近 0Ω,说明 CANH 和 CANL 之间有短路。这一步在系统上电前一定要做,能避免上电后总线驱动过流。

然后是单节点回环测试。所谓回环就是让 A 机自己发自己收,用来验证控制器和收发器本身是否工作正常。方法很简单:把两个节点的总线断开,只保留 A 机自己的收发器,并在软件里把 A 机的接收滤波器设为接受所有帧,然后周期发送报文。如果 A 机能收到自己发的报文,说明控制器、收发器、滤波器、中断这一整条链路都没问题。这一步能帮你快速把“硬件接线问题”和“软件逻辑问题”切分开。

双机对接时,先上电 A 机,观察它的 CAN 控制器是否进入正常状态;再上电 B 机,通过逻辑分析仪抓取总线空闲时的隐性电平。正常情况下 CANH 对地约为 2.5V,CANL 对地约为 2.5V,CANH 和 CANL 之间的差分电压接近 0V,此时总线处于隐性状态。当 A 机开始发送报文,CANH 会被拉到约 3.5V,CANL 被拉到约 1.5V,差分约为 2V,这是显性状态。抓到的波形里,如果这些特征出现,说明物理层已经通了一半。

4.3 通信验证与现象观测

当 A 机周期发送、B 机中断接收回传时,我在“PC 端串口调试助手”里实时打印了两个节点的收发计数。实测结果如下表:

节点发送帧数接收帧数错误帧数状态
A 机100010000正常
B 机100010000正常

我连续跑了 30 分钟,收发计数完全一致,说明硬件连接、波特率配置、过滤器设置、收发逻辑这几大块都没问题。此时再观察逻辑分析仪抓到的完整报文,标准帧的波形结构很清晰,SOF 位(帧起始)的下降沿之后是 12 位的仲裁字段,然后依次是控制字段、数据字段、CRC 字段、ACK 槽和 EOF。把逻辑分析仪解析出的位流和代码里填写的 ID、数据对比,完全吻合。

这里有个值得记录的细节:在双机正常通信时,ACK 槽在发送端的波形上会出现一个“毛刺”或者短暂的电平变化。这是因为 CAN 是“多主”总线,发送节点在发出 ACK 槽后会释放总线,而接收节点会在 ACK 槽期间拉低总线表示确认。如果你在逻辑分析仪上看到自己的发送帧里 ACK 位一直是隐性电平(没有拉低),说明总线上根本没有其他节点在正常接收你的报文。这个细节在排查“为什么发出去没收不到”时非常有用。

5. 实际踩坑与问题排查实录

5.1 常见问题速查表

下面把我在实验过程中遇到的最典型的问题列成表格,供你对照排查。

现象可能原因排查方案
发送超时,CAN_TxStatus 一直不是 Ok总线上没有其他节点,或终端电阻缺失先做回环测试确认控制器链路;检查总线是否接了第二个节点或电阻
能收到自己的报文,但收不到对端报文滤波器配置错误,把对端 ID 过滤掉了暂时关闭滤波器或改为接受所有帧,确认对端报文能否进入 FIFO
波特率不对,双方都收不到晶振频率和初始化参数不匹配用示波器测量 TXD 引脚的实际位宽度,反推实际波特率
报文偶尔丢失,错误计数器递增总线没有终端电阻,或者线缆过长确认两端各接一个 120Ω 电阻;检查布线是否形成环路
接收中断标志无法清除,反复进中断没有调用 CAN_ClearITPendingBit,或 FIFO 没有读空在中断末尾手动清除挂起位,并确认每次中断都实际读出了报文
通信时出现连续错误帧,随后控制器进入 Bus-Off波特率不匹配或者 CANH/CANL 接反检查波特率计算值;确认所有节点 CANH 接 CANH、CANL 接 CANL

5.2 一个典型的自收发误判

我在双机通信的一开始犯了这样一个错误:A 机在没有任何对端节点的情况下,用回环模式发了一帧数据,逻辑分析仪确实抓到了波形,A 机自己也通过中断收到了这帧数据。我当时以为一切正常,就把 B 机接上,结果 A 机发出去的报文 B 机死活收不到。

排查过程是这样的:我先确认 B 机的中断有没有触发,结果没有。然后我把 B 机的滤波器改成接受所有帧,还是收不到。最后我用逻辑分析仪直接抓总线上的差分波形,发现 A 机发送时总线上根本没有显性电平的变化。原因很快就水落石出了——A 机在回环模式下测试后,代码里忘了把 CAN 模式从回环改回正常模式。回环模式下,控制器发出的报文只是在芯片内部绕了一圈,收发器根本没有驱动总线。

这个错误的典型性在于:回环模式是 CAN 控制器内部把发送数据直接送回接收路径,不经过收发器外部引脚。所以回环测试只能证明“控制器逻辑没问题”,不能证明“物理层没问题”。你在自己做联调时,一定要在进入双机对接之前检查当前使用的是 CAN_Mode_LoopBack 还是 CAN_Mode_Normal,这个标志位藏在初始化结构体的 CAN_Mode 字段里,不留意就会漏掉。

5.3 波特率微调的一个实战思路

如果你在做一套需要兼容多个节点的系统,而且总线上不同节点的晶振精度不一致,波特率会产生微小偏差,长时间运行后错误帧会逐渐增多。这时候不要盲目改采样点,先量一下每个节点实际发出的位时间宽度。

用逻辑分析仪或示波器,抓取节点在发送报文时的 SOF 位到第二位之间的宽度,这个宽度就是一个位时间。比如 500kbps 的波特率,位时间理论上是 2μs。如果量出来是 1.98μs 或 2.02μs,说明实际波特率有 ±1% 的偏差,这在短报文下问题不大,但如果报文长度达到 8 字节、而且总线很长,误差累积后就会导致采样点错误。

此时优先调整的是 SJW(同步跳转宽度)。SJW 的作用是允许控制器在总线边沿处调整位时间,以补偿节点之间的时钟偏差。把 SJW 从 1tq 调整为 2tq 或 3tq,能显著提升系统抵抗时钟偏移的能力。代价是位同步的灵活性增加,但在稳定总线上不会造成其他负面影响。

6. 进阶方向与扩展思路

6.1 从双机到多节点组网

双机通信跑通之后,很多人会自然想到一个问题:如果总线上接 5 个、10 个节点,代码还能这样写吗?答案是可以,但要注意两个变化。

第一个变化是仲裁机制。CAN 总线允许多个节点同时发送报文,通过帧 ID 的位仲裁决定优先级。ID 数值越小,优先级越高。在多节点系统里,你需要为每个节点分配独立的帧 ID 范围,并设计好优先级策略。比如紧急控制帧用低 ID(如 0x001),普通状态帧用高 ID(如 0x500),这样即使总线上有冲突,紧急帧也能优先通过。

第二个变化是过滤器的作用会变得更重要。双机通信时你只需要过滤一个 ID,多节点时你可能需要过滤多个 ID,或者用屏蔽位模式把一组相关 ID 全部放行。比如只接收 ID 在 0x100 到 0x1FF 范围内的报文,可以用一个屏蔽位模式,让高 7 位精确匹配0x100这段范围的公共前缀,低 4 位忽略。这个设计能大幅减少中断次数,而单片机只需要处理与自己相关的报文。

6.2 用 CAN 实现简单的传感器数据采集应用

如果你手头有一块带 CAN 接口的传感器模块(比如 CAN 接口的温湿度传感器或气压传感器),完全可以基于现有的双机通信框架,把它改造成一个传感器数据采集系统。传感器作为从节点,周期性上报数据;主节点接收数据,解析出温度、湿度、气压等物理量,并通过串口打印到调试助手。

这种场景下,最需要关注的是数据长度和数据类型转换。CAN 一帧最多只能携带 8 字节数据,如果你要上报的物理量超过 8 字节,比如一组 3 轴加速度数据加上时间戳,就需要拆分成多帧发送,并在每一帧里面加上帧序号,接收端再按照序号拼接。这是从双机通信迈向实际项目时绕不开的一个细节:CAN 是面向帧的协议,不是面向流的协议——它没有像 UART 那样的字节流连续性,每一帧都是独立的,所以应用层要自己处理好分包和重组逻辑。

6.3 结合 DMA 的批量发送思路

STM32 的 CAN 外设是没有 DMA 通道直接支持的,很多人被这一点卡住。但实际上,如果你的 CPU 还有富余,可以自己管理一个发送缓冲区,把要发的报文排成队列,在定时器中断里逐个喂给 CAN 发送邮箱。这种软件队列的方式,效果上能近似实现非阻塞发送,同时避开 CAN 外设没有 DMA 的硬件限制。

实现思路如下:定义一份全局环形缓冲区,每个元素是一组 CanTxMsg 结构体;上层业务只负责往缓冲区里写,底层定时器中断负责从缓冲区读出来并调用 CAN_Transmit。只要缓冲区设计得合理,你就能在业务代码里告别轮询等待,把 CAN 发送变成一个“提交即返回”的操作。在我的一个轻量级传感器上报项目里,我用这个方法做到了 500kbps 速率下接近满负载的连续发送,CPU 占用率依然很低。

结尾:一点个人经验

在反复折腾这套双机通信之后,我最深的一个体会是:CAN 相比 UART、SPI 这些简单总线,最需要建立的是“总线思维”。UART 是点对点的,发出去就完了;SPI 是主从的,主机说了算;而 CAN 是真正的多主总线,所有节点共享一对信号线,因此你要时刻关心总线上其他节点在干什么——它们会不会同时发?会不会总线错误?自己的过滤器会不会把别人的帧也收进来?这些才是 CAN 开发里真正花时间的地方。

最后分享一个我在实际调试中反复用到的技巧:无论你的代码逻辑多复杂,先把波特率、终端电阻和总线波形这三大基础验证扎实,再谈业务逻辑。很多让人抓狂的“偶发丢帧”“时好时坏”问题,根源往往不在代码,而在物理层或配置层。先把波形抓干净,后面所有问题都会变得简单。这套流程跑通之后,你再去看那些复杂的 CANopen、J1939 协议栈,也会觉得它们不过是建筑在这套基础通信机制上的规则约定而已。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 22:10:03

Python实现带优先级解析的计算器:从原理到实践

1. 项目概述&#xff1a;为什么需要自制计算器&#xff1f;在Python中实现一个简易计算器&#xff0c;看似是个基础练习&#xff0c;但背后蕴含着编程入门的核心价值。我十年前刚学Python时&#xff0c;第一个完整项目就是计算器&#xff0c;它帮我理解了函数封装、异常处理和用…

作者头像 李华
网站建设 2026/10/22 13:25:04

状态空间模型参数估计实战:从物理建模到嵌入式部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 14:07:40

Linux数据流处理:从I/O重定向到命令链的底层思维

1. 这不是“背命令”&#xff0c;而是构建Linux数据流的底层思维很多人学Linux命令行I/O重定向&#xff0c;第一反应是“又要记一堆符号&#xff1a;>、>>、2>、|、<……太乱了”。我带过不少刚转运维或DevOps的新人&#xff0c;他们花三天背完所有重定向操作符…

作者头像 李华
网站建设 2026/10/22 22:09:26

Linux驱动学习(1)

Linux 驱动学习&#xff1a;基于 Platform 架构编写驱动之前编写 LED 字符设备驱动时&#xff0c;需要自己申请设备号、创建设备节点&#xff0c;并且将寄存器地址直接写在驱动代码中。这种方式虽然可以实现功能&#xff0c;但是不方便管理和移植。为了让驱动结构更加清晰&…

作者头像 李华
网站建设 2026/10/17 6:55:39

实测 5 款 2026 人才测评工具,横评踩坑后锁定靠谱平台

一、为什么你买的测评工具总是“用不上”“招进来的人聊得挺好&#xff0c;干了三个月发现不合适。”这句话在HR圈子里出现的频率太高了。问题不在人&#xff0c;在选人时缺了一条数据链路。据市场研究机构统计&#xff0c;2026年全球人才评估服务市场规模预计达到99.2亿美元&a…

作者头像 李华
网站建设 2026/10/20 14:38:20

开源ERP二次开发:中小企业低成本打造可定制系统的务实路径

开发ERP软件这件事&#xff0c;我从几个角度都碰过&#xff1a;自己从零写过、用低代码平台搭过、也在开源系统基础上做过二次开发。最近有个做零配件加工的朋友来问我&#xff0c;公司想上ERP&#xff0c;到底该走哪条路&#xff1f;这问题我特别有发言权。我的结论可能会让一…

作者头像 李华