news 2026/10/22 22:19:04

STM32F105双CAN收发实战:CAN1接收CAN2发送与初始化要点

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F105双CAN收发实战:CAN1接收CAN2发送与初始化要点

简介:一套基于STM32F105的CAN总线通信例程,重点演示CAN1接收、CAN2发送的双接口独立配置方案,适合嵌入式开发者学习Cortex-M3内核下CAN协议栈的初始化、滤波器设置、中断收发与错误处理。通过标准库或HAL库的调用方式,可快速掌握双CAN通道的同步时序与邮箱管理技巧。

资源包共201个文件,压缩后2.12MB,以h头文件、c源文件为核心代码,辅以uvproj工程文件、hex/axf烧录文件以及map、lst等编译映射与列表文件,便于直接打开工程运行或分析编译结果。整体结构清晰,适合对照学习CAN模块的寄存器级操作和中断服务程序设计。

已有984人浏览学习,例程源自实际调试记录,能帮助读者少走弯路。随包附带完整工程目录与备份文件,包含CAN.c等关键源码备份和芯片启动配置,便于反复修改试验。通过对发送邮箱与接收邮箱满标志的处理,可深入理解CAN总线仲裁、重传和实时性保障机制,为工业与车联网场景下的可靠通信开发打下基础。

1. STM32f105 CAN1收 CAN2发:一颗芯片凑齐双口 CAN,转发无需再挂第二颗控制器

调试某工控数据采集板时,总线上挂着两类节点:一边是 CAN1 上的传感器,一边是 CAN2 上的执行器。手头只有一颗 STM32F105,还想让两边独立收发——这个「STM32f105 CAN1收 CAN2发」工程做的就是这件事:CAN1 负责接收总线报文,CAN2 负责把处理结果发出去,全程不用外挂第二片 CAN 控制器。反直觉的是,F105 的 CAN2 并不能独立干活,它的筛选器寄存在 CAN1 的寄存器空间里,初始化顺序不对,CAN2 连 Normal 模式都进不去。这个工程适合正在搭双 CAN 网关、做协议桥接或调试两路总线的开发者,老手能看架构选型,新手能照着把收发跑通。

2. 双 CAN 架构细节:为什么 CAN2 必须借用 CAN1 的筛选器,初始化顺序决定成败

2.1 互联型系列的 CAN 资源:一颗芯片里装了两套接口、一份筛选器池

STM32F105 属于互联型产品,比较大的卖点就是内置两个 CAN 控制器:CAN1 和 CAN2。两个控制器各自有独立的发送邮箱和接收 FIFO——CAN1 有 3 个发送邮箱、2 个接收 FIFO,CAN2 也相同,从这个角度看它们是完全对等的两套总线接口。但有一个容易忽略的共享资源:28 个筛选器 bank。所有报文的硬件过滤规则都存放在这 28 个 bank 里,而 bank 的物理寄存器挂在 CAN1 的外设地址空间上。

也就是说,不是你真的拥有 56 个筛选器可以随便分给两个 CAN,而是两个控制器共用这一份 28 组的过滤规则。你可以把其中一部分 bank 分配给 CAN1 使用,另一部分分配给 CAN2 使用,通过 CAN_FilterInit 里的 CAN_FilterFIFOAssignment 和 CAN_FilterNumber 来指定。这种设计在 F103 上没见过,因为 F103 只有一个 CAN,筛选器完全私有;换到 F105 后,很多沿用 F103 写法的人第一反应是「直接把 CAN_Init 的调用对象从 CAN1 换成 CAN2」,结果摔得头破血流。

理解这个资源模型很重要:CAN2 本身可以独立收发,但如果你想让它按 ID 过滤报文,就必须通过 CAN1 的寄存器区域去配置筛选器。所以稳妥的做法是,先初始化并使能 CAN1,拿到筛选器控制权,再去操作 CAN2。即使你的业务场景只在 CAN2 上收发数据,也得先把 CAN1 的时钟打开并完成初始化,否则 CAN2 的过滤功能就是黑匣子。这是整个工程代码里隐藏的一条硬约束。

2.2 筛选器 bank 归属:为什么先 CAN1 后 CAN2 是最稳的顺序

回到寄存器层面看,ST 的参考手册里,CAN1 的寄存器块除了包含 MCR、MSR、TSR 这些控制器寄存器之外,还包含 Filter Register 相关的一大段偏移地址,比如 FMR、FM1R、FS1R、FFA1R 等。而 CAN2 的寄存器块只覆盖控制器和邮箱部分,没有单独的筛选器寄存器组。也就是说,你在代码里调用 CAN_FilterInit 时,它实际写入的物理地址落在 CAN1 的地址范围内。

这意味着两件事。第一,访问这些筛选器寄存器之前,必须保证 CAN1 的外设时钟处于使能状态,否则读操作可能返回全 0,写操作被忽略或产生总线错误。第二,CAN2 的初始化顺序要放在 CAN1 之后,至少要在筛选器配置之前完成 CAN1 的 CAN_Init。很多人的现象是 CAN2 初始化也能成功,但一旦程序里调用了 CAN_FilterInit 指定给 CAN2 的 bank,数据就进不来或者进错 FIFO,问题往往就出在顺序上。

我习惯用一张表把资源关系列在工程注释里,防止后来接手的人改乱:

资源项CAN1CAN2
发送邮箱3 个3 个
接收 FIFOFIFO0、FIFO1FIFO0、FIFO1
筛选器 bank0-27 中已分配的 bank0-27 中已分配的 bank
筛选器寄存器物理位置CAN1 寄存器空间与 CAN1 相同
初始化依赖独立依赖 CAN1 时钟与筛选器控制权

在这个工程里,我的分工是:bank 0 分配给 CAN1 的 FIFO0 用于接收所有报文,CAN2 不启用额外筛选,直接靠邮箱发送。这样最简,也最适合先把链路跑通。等以后要上协议过滤,再在 2.1 节的框架里扩展不迟。

2.3 时钟与初始化顺序:先让 CAN1 活过来,再碰 CAN2

代码层面对上面这一点体现得很直接。首先是时钟使能,CAN1 和 CAN2 都在 APB1 总线上,但必须同时把两组外设时钟打开,不能只开 CAN2:

RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1 | RCC_APB1Periph_CAN2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);

APB1 这一句如果只写 CAN2,后面 CAN_FilterInit 操作的是 CAN1 的寄存器,外设时钟没开,结果就是筛选器配置无效。GPIO 的时钟挂在 APB2 上,PA 和 PB 都要开,因为 CAN1 的引脚在 PA,CAN2 的引脚在 PB,缺一个都起不来。这里我用标准外设库写法演示,HAL 库同理,换成 __HAL_RCC_CAN1_CLK_ENABLE() 和 __HAL_RCC_GPIOA_CLK_ENABLE() 就行,关键是顺序别乱。

初始化顺序我固定写成三段式:先 CAN1_Init,再 CAN2_Init,最后统一配置筛选器。这样 CAN2 依赖的筛选器资源一定处于可用状态。后面第 3 章会给完整代码,这里先记住结论:双 CAN 工程的第一步不是写逻辑,是把外设的先后关系写对。

3. 工程实现拆解:CAN1 接收中断 + CAN2 发送轮询的完整链路

3.1 GPIO 端口映射:PA11/PA12 给 CAN1,PB12/PB13 给 CAN2

STM32F105 的 CAN 引脚默认不是重映射,所以直接配置 GPIO 即可。CAN1 的 RX 在 PA11,TX 在 PA12;CAN2 的 RX 在 PB12,TX 在 PB13。不要自作聪明去重映射到其他引脚,除非你明确看过数据手册的 AF 映射表,否则默认引脚最省事。

GPIO 配置有一个常见误区:把 RX 和 TX 都设成复用推挽。TX 确实要设成 AF_PP,但 RX 是输入引脚,应该用上拉输入 IPU,这样在总线空闲时保持确定电平,避免误触发。下面这段配置就按这个原则来写:

GPIO_InitTypeDef GPIO_InitStructure; /* CAN1_TX: PA12 复用推挽 */ 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); /* CAN1_RX: PA11 上拉输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStructure); /* CAN2_TX: PB13 复用推挽 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); /* CAN2_RX: PB12 上拉输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOB, &GPIO_InitStructure);

这里 GPIO_Speed 设成 50MHz 是给推挽输出用的,输入模式不关心速度。四个引脚分开初始化,不要图省事把 PA11、PA12 塞进同一个 GPIO_Init 结构体,因为模式不同,合并后会互相覆盖。

3.2 波特率与位时序:500K 采样点 77.8% 的寄存器换算

CAN 波特率由 APB1 时钟、预分频器和位时间共同决定。STM32F105 的 APB1 最大 36MHz,很多 F103 移植过来的例程直接沿用 APB1=72MHz 的分频参数,结果总线波特率变成预期的一半。这个工程里我按 36MHz 计算,跑标准 500Kbps,位时序配置如下:

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; CAN_InitStructure.CAN_TXFP = ENABLE; CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler = 4; CAN_Init(CAN1, &CAN_InitStructure); CAN_Init(CAN2, &CAN_InitStructure);

计算逻辑是:位时间 Tbit = 1 + BS1 + BS2 = 1 + 13 + 4 = 18 tq,tq 时钟 = APB1 / Prescaler = 36MHz / 4 = 9MHz,波特率 = 9MHz / 18 = 500Kbps。采样点出现在同步段之后、BS1 结束的位置,也就是 (1 + 13) / 18 = 77.8%,这个采样点比较靠近常规 CAN 的推荐值。

CAN_ABOM 我打开,这样总线出错导致 Bus Off 后,控制器会自动尝试恢复,省得应用层天天盯着错误状态。CAN_TXFP 打开表示发送优先级由请求先后顺序决定,而不是靠 ID 大小,这和我们后面简单轮询发送的行为一致。

3.3 筛选器与接收中断:CAN1 接收 FIFO0 的进入路径

筛选器这一块,本阶段保持最简思路:接收所有报文,不按 ID 过滤,先证明物理链路通畅。这样配置:

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_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStructure);

Mask 全部为 0,代表掩码不关心任何位,所有帧都被接受。如果后面要过滤某个标准 ID,需要自己计算 ID 在寄存器里的位位置:标准 ID 是 11 位,在 32 位筛选寄存器中左移 21 位放置。我第一次调的时候直接填 0x123 进去,发现一个帧都收不到,血的教训就是别跳过这一步。

接着配置中断优先级使能 FIFO0 消息挂起中断:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);

CAN_FilterFIFOAssignment 填 CAN_FIFO0,那么匹配的帧会进入 FIFO0,中断源 CAN_IT_FMP0 正是 FIFO0 的消息挂起中断。如果填了 FIFO1,中断源就要改成 CAN_IT_FMP1,二者必须对齐,否则中断永远不触发。

3.4 CAN2 发送数据:mailbox 轮询和失败判定

CAN2 的发送功能封装成一个独立函数,这样主循环或者后续逻辑可以直接调用。函数里需要构造 CanTxMsg 结构体,并处理标准和扩展 ID 的区分:

uint8_t CAN2_Send_Frame(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; uint16_t timeout = 0; TxMessage.DLC = len; TxMessage.RTR = CAN_RTR_Data; TxMessage.Data[0] = data[0]; TxMessage.Data[1] = data[1]; TxMessage.Data[2] = data[2]; TxMessage.Data[3] = data[3]; TxMessage.Data[4] = data[4]; TxMessage.Data[5] = data[5]; TxMessage.Data[6] = data[6]; TxMessage.Data[7] = data[7]; if (id & 0x80000000) { TxMessage.IDE = CAN_Id_Extended; TxMessage.ExtId = id; TxMessage.StdId = 0; } else { TxMessage.IDE = CAN_Id_Standard; TxMessage.StdId = id; TxMessage.ExtId = 0; } mailbox = CAN_Transmit(CAN2, &TxMessage); if (mailbox == 0xFF) { return 0; } while (CAN_TransmitStatus(CAN2, mailbox) == CAN_TxStatus_Pending) { if (++timeout > 1000) { return 0; } } return 1; }

CAN_Transmit 返回的 mailbox 是 0、1、2 中的某个编号,代表这个帧占用了哪个发送邮箱。如果三个邮箱都满,返回 0xFF。返回后还要用 CAN_TransmitStatus 查询该 mailbox 的状态,Pending 表示正在等待总线空闲或仲裁,转发完成才变 Successful。这里加了超时上限,防止总线挂死时程序无限等待。

3.5 接收中断与应用层衔接:把收到的数据喂给 CAN2

接收中断里不做任何耗时操作,只调用 CAN_Receive 把帧从 FIFO0 取出,然后拷贝到全局变量:

CanRxMsg RxMessage; volatile uint32_t rx_count = 0; volatile uint32_t last_id = 0; volatile uint8_t last_data[8]; void CAN1_RX0_IRQHandler(void) { if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); last_id = RxMessage.StdId; for (uint8_t i = 0; i < RxMessage.DLC; i++) { last_data[i] = RxMessage.Data[i]; } rx_count++; CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }

主循环里就可以周期性地把接收计数通过 CAN2 发出去,形成完整的「CAN1 收、CAN2 发」闭环:

while (1) { uint8_t buf[8]; buf[0] = (uint8_t)(rx_count); buf[1] = (uint8_t)(rx_count >> 8); buf[2] = (uint8_t)(last_id); buf[3] = 0; buf[4] = last_data[0]; buf[5] = last_data[1]; buf[6] = last_data[2]; buf[7] = last_data[3]; CAN2_Send_Frame(0x200, buf, 8); delay_ms(100); }

这里只是演示数据怎么接起来,真正做项目时建议用队列,而不是直接在中断里覆盖全局变量。第 6 章会给出改进方案。

4. 避坑现场:双 CAN 调试最容易翻车的五个细节

4.1 现象:CAN2 初始化返回 ERROR,状态卡在 Init 模式

我第一次在 F105 上写双 CAN 程序时,把 CAN_Init(CAN2, ...) 放在 CAN1 之前,结果 HAL_Init 返回 HAL_ERROR,打死进不了 Normal。查寄存器发现 CAN2 的 MCR 一直没跳转。

原因:CAN2 的筛选器和状态机复位逻辑依赖 CAN1 的外设时钟。单独开 CAN2 时钟时,CAN2 控制器本身能起来,但库函数里某些初始化步骤会访问共享的筛选器寄存器,那边没有时钟,硬件行为就不确定。

解决:先 CAN_Init(CAN1),再 CAN_Init(CAN2),中间不要间隔太耗时的操作,确保 CAN1 的 MCR 已经有 INRQ 位操作完成。从那以后我固定这个顺序,再也没有在初始化阶段遇见过卡死。

4.2 现象:CAN1 能进中断但收到的全是垃圾帧

有一次总线上数据全是 0xFFFFFFF,看起来像没同步。排查发现 GPIO 配置有问题,把 PA11 也配成了 AF_PP,RX 引脚输出模式在电气上会不断改变电平,接收器采样到的完全不是总线信号。

原因:RX 引脚没有正确配置为输入。在标准外设库下,默认复用推挽会把 RX 变成输出,CAN 外设根本采不到有效位流。

解决:RX 设成 GPIO_Mode_IPU 或 GPIO_Mode_IN_FLOATING。用 IPU 更稳,总线空闲是高电平,不会误判起始位。这个错误在中文社区里很常见,因为很多例程偷懒把四个引脚写在一个结构体里,RX、TX 全用 AF_PP,短距离自测能通,接长线就翻车。

4.3 现象:设成 500K 波特率,逻辑分析仪抓出来是 250K

把 F103 的 CAN 初始化结构体平移到 F105,其他不动,结果实测波特率不对。F103 默认 APB1 是 72MHz,F105 是 36MHz,分频系数没按 36MHz 重新算,位时间就长了一倍。

原因:预分频和位时序的乘积没变,但输入时钟变了。比如原来用 PCLK1=72M,Prescaler=4,BS1=8,BS2=7,位时间 16 tq,波特率就是 72M / (4 * 16) = 1.125M。同样参数放到 36MHz 上只有一半。

解决:先确认 PCLK1 实际频率。用公式直接算,不要凭经验搬参数。我上面给的 500K 配置是 Prescaler=4、BS1=13、BS2=4、SJW=1,对应 36MHz 主频,采码率算完 77.8%,在标准分析仪上直接就能看到正确采样点位。

4.4 现象:发送函数偶尔死循环,总线负载一高就丢帧

CAN2 发送函数在没有超时判断时,如果三个 mailbox 都被占满,CAN_Transmit 返回 0xFF,我的旧代码没判断就直接等状态,结果在负载大的时候卡死在 while 里。

原因:CAN 发送邮箱资源不足,且发送失败没有处理分支。总线忙时邮箱会一直 pending,轮询等待没有上限,程序就挂住。

解决:先判断 CAN_Transmit 返回值是否为 0xFF,之后再轮询,并且给轮询加计数器,超时返回失败。应用层收到失败后可以选择丢弃、重试或把这帧记录到错误日志。我的习惯是发送函数永远不阻塞超过 1ms,宁可让上层知道丢帧,也不能让主循环卡顿。

4.5 现象:接收中断里直接调用 CAN2 发送,跑一会儿死机

想着省事,在 CAN1_RX0_IRQHandler 里收到帧就马上调 CAN2_Send_Frame。单独测没问题,一旦两边同时来数据,程序就进入 HardFault。

原因:CAN_Transmit 过程涉及操作寄存器组并等待邮箱状态,这段代码本身比较长,在中断里执行时如果同优先级的 CAN2 事件也触发中断,造成重入,或者栈被压爆。

解决:接收中断只做数据拷贝和置标志,把发送动作挪到主循环或低优先级任务里。如果非要中断里发,需要把发送函数改写成非阻塞式,并保证临界区保护。工程阶段我直接选定「中断收、主循环发」的结构,稳定且好维护。

5. 跑通与验证:自回环、双板互联、抓包波形怎么对应代码

5.1 先做自回环:把 CAN1 配成回环模式,验证中断链路

拿到工程别急着接总线,先在单独一块板上验证底层能通。把 CAN_InitStructure.CAN_Mode 改成 CAN_Mode_Silent_LoopBack,其他不动。这个模式下 CAN1 的内部发送路径直接连到接收路径,不需要外部收发器,引脚上也不输出数据。

改好后,在主循环里直接调用 CAN1 的发送函数发一帧,然后看 CAN1_RX0_IRQHandler 有没有被触发。可以用一个 LED 翻转来标志接收成功。这一步通过,说明时钟、GPIO、筛选器、NVIC、中断回调这条链路都是通的。注意回环模式不占用总线仲裁,所以发送时 mailbox 状态会很快回到 Successful。

如果回环都收不到,先查 GPIO 配置和筛选器 mask 是否全零,这两处是自检验里最容易写错的地方。回环通过后再把 CAN_Mode 改回 Normal,进入双板互联测试。

5.2 双板互联测试:一块板发、一块板收,CAN2 出、CAN1 进

准备两块 STM32F105,板 A 负责用 CAN2 发数据,板 B 用 CAN1 收数据。接线方法是两块板的 CAN 收发器 TR 输出并联到同一条总线,CANH 接 CANH,CANL 接 CANL,共地必须接好。我用的都是标准 TJA1050 模块,120 欧终端电阻在总线两端各加一个,中间不加。

测试流程是这样:板 A 上电后用一个按键触发 CAN2_Send_Frame(0x123, test_buf, 8),test_buf 里填 0xAA 0x55 0x01 0x02 0x03 0x04 0x05 0x06。板 B 的 CAN1 收到后,将 last_id 和 last_data 通过串口打印,或者在调试器里看变量。我实际跑的时候,板 B 在中断里把接收到的数据再转发到板 A 的另一个通道,这样能同时验证双向通信,但第一次跑建议只做单向,减少干扰。

如果板 B 收不到,优先测波特率。用板 A 发送,另一块板收不到时,把板 B 初始化里的 Prescaler 从 4 改到 5?不,那不会好。正确做法是用分析仪看总线波形,直接测波特率是否真的是 500K。很多所谓收不到的问题,本质上都是两边位时序没对齐。

5.3 CAN 分析仪抓包验证:从 ID、DLC 到数据段逐字段对应程序

用 USB-CAN 分析仪挂在总线上,是最直观的验证方式。分析仪波特率设为 500K,然后板 A 按键发送。分析工具里会列出报文,我一般这样逐字段核对程序里的配置:

抓包字段程序预期实际结果核对点
ID0x1230x123CAN2_Send_Frame 里 StdId=0x123
帧类型标准数据帧标准数据帧IDE=CAN_Id_Standard,RTR=CAN_RTR_Data
DLC88函数入参 len=8
Data[0]0xAA0xAAtest_buf 首字节
Data[1]0x550x55test_buf 第二字节
波特率500K500K分析仪波特率设置以及位时间抓取

如果 Data 错位,多半是发送缓冲区的数据没对齐 DLC;如果 DLC 显示 0,说明 TxMessage.DLC 赋值被后来代码覆盖。把分析仪作为唯一事实来源,程序里再纠结都不如抓包一锤定音。整个链路验证通过后,这份资源就可以作为后续改动的基础工程,随手拿一块芯片就能继续扩展。

6. 二次开发实战:从「单向转发」升级成「双通道桥接器」的改造路线

6.1 给收-发之间加一张规则表,让转发可控

现在工程是 CAN1 收啥就发啥,但要上生产就得按 ID 做规则。我建议加一张转发规则表,每个条目包含接收 ID、要转发的目标 ID 和使能标志。中断里收到帧后,主循环查表,命中才调 CAN2_Send_Frame。这样既保留了接收中断的短小,又让转发策略变得可配置。

typedef struct { uint32_t rx_id; uint32_t tx_id; uint8_t enable; } ForwardRule; const ForwardRule rule_table[] = { {0x101, 0x201, 1}, {0x102, 0x202, 1}, };

查表逻辑写在主循环里,最好别放中断。表是 const,后期可以通过上位机改写 Flash 里的副本,达到运行时更新规则的效果,但这是后话了。

6.2 把接收中断改造成生产者-消费者式队列

单包收发没问题后,下一步就是防丢帧。用环形缓冲区做收包队列,中断只负责把数据压入队列,主循环从队列弹出并处理。这样即便 CAN2 忙,CAN1 也不会因为处理不及时而丢帧。

#define RX_QUEUE_SIZE 32 typedef struct { uint32_t id; uint8_t data[8]; uint8_t dlc; } RxPacket; RxPacket rx_queue[RX_QUEUE_SIZE]; volatile uint16_t rx_head = 0; volatile uint16_t rx_tail = 0;

中断里只做入队,主循环里出队并调用发送函数。队列长度根据实际负载调整,32 条对大多数工控场景够用。改完之后,中断函数执行时间从几十微秒降到几个微秒,主循环也不容易卡死了。

6.3 总线错误恢复与状态上报

最后加一道保险:打开 CAN_ABOM 后,总线关闭会自动恢复,但应用层最好知道发生过什么。用 D 类的错误统计变量,在中断里累积发送失败和接收错误计数,通过 CAN2 周期上报给上位机。这样出问题时不是玄学,能直接看到错误次数在涨。

从那以后我每次调双 CAN,都会先默写一遍「CAN1 先启、筛选器后配、RX 用上拉、发送看 mailbox」这四步,确认没问题再碰总线,总能把飘忽不定的问题压成确定性的逻辑错误。希望帮到你。

本文还有配套的精品资源,点击获取

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

Zun容器网络绑定失败:vif_type不支持的根因与修复

1. 问题本质与典型发生场景还原 “zun docker vif_type(binding_failed) is not supported. A binding script for this type cant be found”——这行报错不是偶然闪现的调试日志&#xff0c;而是某次容器化边缘计算任务部署失败后&#xff0c;控制台里反复刷屏的红色警报。它…

作者头像 李华
网站建设 2026/10/21 19:05:59

DGI数据治理框架落地指南:拆解概念、决策权与问责制度

简介&#xff1a;这是基于 DGI 数据治理框架的中文译版 PDF 文档&#xff0c;适合数据治理从业者、企业数据管理人员以及刚接触治理体系的学习者&#xff0c;用于快速理解数据治理的目标、原则与落地路径。资源共 1 个 PDF 文件&#xff0c;整体大小约 607KB&#xff0c;为纯文…

作者头像 李华
网站建设 2026/10/21 19:05:39

零基础免费建站指南:从域名到静态托管手把手实操

1. 从零建站这件事&#xff0c;到底值不值得自己动手很多人第一次冒出“自己搭一个网站”的念头&#xff0c;往往是被两件事逼出来的&#xff1a;一是想有个地方放自己的作品、笔记或者小生意介绍&#xff0c;二是问了一圈外包报价&#xff0c;发现动辄几千上万&#xff0c;心里…

作者头像 李华
网站建设 2026/10/21 19:05:19

智能体降临:2026年AI应用从模型中心到任务完成的范式转移

2026年&#xff0c;大模型退场&#xff0c;智能体降临。这句话刚说出来的时候&#xff0c;很多人的第一反应是“模型要没了”&#xff0c;第二反应是“又在贩卖焦虑”。但如果你真的在AI产品一线待过几年&#xff0c;你会发现这句标题描述的其实不是模型的死亡&#xff0c;而是…

作者头像 李华
网站建设 2026/10/22 20:18:22

多移动机器人领航追随编队控制Matlab仿真全流程解析

做多移动机器人编队仿真这件事&#xff0c;卡住大多数人的往往不是控制算法本身&#xff0c;而是第一步&#xff1a;面对一堆公式&#xff0c;不知道从哪开始写代码&#xff1b;写完代码&#xff0c;又不知道参数怎么调。领航追随法算是最容易上手的编队方案了&#xff0c;核心…

作者头像 李华
网站建设 2026/10/22 22:17:48

架构图设计指南:从分层到版本管理,让图真正被看懂

最近在梳理一个遗留系统的重构方案&#xff0c;评审会上有人把新架构图投到屏幕上&#xff0c;会议室里先是安静了三秒&#xff0c;然后就开始吵。有人指着某个方框问"这是进程还是容器"&#xff0c;有人问"这两个箭头是调用还是数据流"&#xff0c;还有人…

作者头像 李华