1. RK3399Pro 挂 MCP2515 收发丢包,问题到底出在哪
MCP2515 是一颗 SPI 转 CAN 的控制器,RK3399Pro 通过 SPI 总线把它挂上去,Linux 侧就会多出一个 can0/can1 网络设备。这套组合在飞控、工控、车载测试里很常见,成本低、驱动成熟,但真正跑长时间收发时,很多人会碰到一个很烦的现象:CAN 负载明明不到 30%,波特率 500K,跑几个小时却零星丢帧,丢包率大概 0.01% 这个量级。
0.01% 听起来很小,可对连续通信来说就是不可接受。125 万帧丢 130 帧,测试 1343 秒,平均十几秒就掉一帧。这种丢包不是"总线拥塞"造成的,因为负载很低;也不是"线缆接触不良"那种偶发,因为它稳定复现。它更像是中断响应、SPI 时钟、缓冲配置三者里某一环在长时间运行下被拖慢了。
这篇就按我实际排查的顺序走一遍:先确认硬件与设备树,再给一份可复制的 config.toml 骨架和 SPI/CAN 参数,然后用 candump 抓包对比收发计数,把丢包定位到具体环节,最后列出几个高频踩坑点。适合正在用 RK3399Pro + MCP2515 做双路 CAN、并且被长时间丢包困扰的开发者。
需要说明的是,MCP2515 驱动本身是内核里比较老的一批代码,很多平台直接沿用官方版本。丢包时第一反应不该是"驱动有 bug",而是先看中断有没有被及时处理、SPI 单次传输有没有超时、发送队列有没有堆积。下面逐层拆。
2. 前置准备:TaoToken 与调试环境怎么搭
排查这类问题,除了板子和示波器,我习惯把模型对话和接入文档放在手边,遇到寄存器含义、驱动参数不确定时直接查,比翻数据手册快。TaoToken 这边可以用模型对话快速确认 MCP2515 寄存器位定义、SPI 模式要求,接入文档里有完整的 API 说明,排障阶段很省事。
如果你只是临时查几个寄存器或驱动行为,用模型对话就够了;如果要把这套排查流程做成脚本、长期跑在 CI 或 Agent 里,那更适合 Coding Plan,能持续调用不中断。下面给的是接入侧要用的地址,按需取用。
- 模型对话(查寄存器、问驱动逻辑):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model
- Coding Plan(长期编码/Agent 场景):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan
- 接入文档(API 参数、错误码):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc
- API Keys 管理(生成/轮换 Key):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys
- 控制台(看调用量、排查额度):https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console
API 基础地址统一用 https://taotoken.net/api ,注意这个不带 UTM 参数,配置里直接写死即可。Key 的生成在 API Keys 页面完成,拿到后不要硬编码进仓库,用环境变量注入。
注意:接入文档里的模型名和参数会随版本更新,配置前先扫一眼当前文档,避免用到已下线的模型名。
3. 可复制配置:config.toml 骨架与 SPI/CAN 参数
先把配置层的东西固定下来。MCP2515 的丢包,很多时候根源在设备树里 SPI 频率给太高、或者中断触发方式配错。下面这份 config.toml 是我在 RK3399Pro 上用的骨架,把 SPI、CAN、中断三块参数集中管理,方便对照修改。
# config.toml - RK3399Pro + MCP2515 双路 CAN 配置骨架 [spi] # MCP2515 支持最高 10MHz,但长走线/多设备时建议降到 8MHz 以内 bus = "/dev/spidev1.0" speed_hz = 8000000 mode = 0 # MCP2515 要求 SPI mode 0 (CPOL=0, CPHA=0) bits = 8 delay_usec = 0 [can0] bitrate = 500000 sample_point = 0.875 # 500K 下建议 87.5% restart_ms = 100 # 总线关闭后自动恢复时间 tx_queue_len = 1000 # 发送队列长度,太小会 NETDEV_TX_BUSY [can1] bitrate = 500000 sample_point = 0.875 restart_ms = 100 tx_queue_len = 1000 [interrupt] # 中断触发方式,MCP2515 INT 引脚为低有效,下降沿触发 trigger = "falling" threaded = true # 使用线程化中断,底半部处理 oneshot = true # IRQF_ONESHOT,处理完才收下一次 [debug] log_level = "warn" # 排查阶段可临时调 info,稳定后调回 dump_regs = false # 打开会周期性打印寄存器,影响实时性设备树里对应的 SPI 节点要保证spi-max-frequency和上面speed_hz一致,中断引脚用interrupts = <GPIOx IRQ_TYPE_EDGE_FALLING>。如果设备树里频率写成 10MHz 而 config 写 8MHz,实际以设备树为准,两边必须对齐。
CAN 参数用 iproute2 落地,命令如下:
# 关闭接口后重新配置,避免参数不生效 sudo ip link set can0 down sudo ip link set can0 type can bitrate 500000 sample-point 0.875 restart-ms 100 sudo ip link set can0 txqueuelen 1000 sudo ip link set can0 up # can1 同理 sudo ip link set can1 down sudo ip link set can1 type can bitrate 500000 sample-point 0.875 restart-ms 100 sudo ip link set can1 txqueuelen 1000 sudo ip link set can1 up # 确认参数已生效 ip -details link show can0ip -details输出里重点看bitrate、sample-point、restart-ms三项是否和预期一致。如果sample-point显示 0.875 但实际采样点偏移,收发双方对不齐,长时间跑就会零星丢帧。
4. 验证请求:candump 抓包对比收发计数
配置好之后,不能只看"能不能通",要看"长时间跑丢不丢"。我的做法是让发送端持续发已知序列号,接收端用 candump 落盘,最后对比计数。
发送端用 cansend 循环发带递增 ID 的帧:
# 发送端:发 1250000 帧,ID 从 0x100 递增,数据段带序号 for i in $(seq 1 1250000); do cansend can0 100#$(printf '%08X' $i) done接收端用 candump 记录时间戳和帧内容:
# 接收端:抓包落盘,带时间戳 candump -t a -l can0 # 默认写入 candump-<日期>.log,也可指定文件 candump -t a can0 > recv.log跑完后统计收发数量:
# 发送端统计实际发出的帧数 grep -c '^' send.log # 接收端统计收到的帧数 grep -c 'can0' recv.log # 找出丢失的序号(假设数据段就是序号) awk -F'#' '{print strtonum("0x"$2)}' recv.log | sort -n > recv_seq.txt seq 1 1250000 | sort -n > send_seq.txt comm -23 send_seq.txt recv_seq.txt | head -20comm -23会列出"发送了但没收到"的序号,前 20 个足够看出丢包是集中在某段时间还是均匀分布。如果丢包集中在某个时间窗口,多半是那段时间系统里有高优先级任务抢占;如果均匀分布,更可能是 SPI 传输或中断处理本身有瓶颈。
同时用ip -s link show can0看内核统计:
ip -s link show can0输出里的RX: errors、TX: errors、dropped是关键。如果dropped在涨,说明内核队列满了,问题在发送侧;如果errors在涨而dropped不动,问题更可能在 SPI 或中断。
5. 本篇常见错排查
5.1 中断被日志拖慢
这是我在实际项目里踩过最深的坑。系统里有个模块每隔 60 秒用pr_err等级打印一大段多行日志,pr_err会直接输出到串口控制台,而串口在 115200 波特率下打印几十行要几百毫秒。这几百毫秒里,如果 MCP2515 的接收中断没被及时处理,MCP2515 内部只有两个接收缓冲,第三帧就会溢出丢失。
排查方法:把printk控制台日志等级调高,屏蔽掉非关键日志。
# 临时把控制台日志等级调到 4,只显示 KERN_ERR 以上 echo 4 > /proc/sys/kernel/printk # 或者直接关掉串口控制台输出 dmesg -n 1改完后重跑长时间测试,如果丢包消失,基本可以确认是日志抢占导致的。长期方案是把那个模块的pr_err改成pr_debug,或者用printk_ratelimit限流。
5.2 SPI 频率过高导致传输超时
MCP2515 数据手册标称支持 10MHz SPI,但 RK3399Pro 的 SPI 控制器在长走线、多从设备场景下,8MHz 以上容易出现位错误。表现是dmesg里偶发mcp251x spi1.0: SPI transfer failed或timeout。
处理方式:把设备树spi-max-frequency降到 8MHz 甚至 6MHz,同时确认 config.toml 里speed_hz一致。降频后如果丢包明显减少,说明是 SPI 时序余量不够。
# 查看当前 SPI 实际频率 cat /sys/kernel/debug/spi/spi1.0/speed_hz 2>/dev/null || \ dmesg | grep -i 'spi.*freq'5.3 发送队列溢出
tx_queue_len默认可能只有 10,CAN 帧发得快时队列瞬间满,ndo_start_xmit返回NETDEV_TX_BUSY,上层就丢帧。把txqueuelen调到 1000 能缓解,但根本方案是确认发送线程没有被阻塞。
# 查看队列溢出计数 ip -s link show can0 | grep -A2 TX # dropped 列在涨就是队列溢出5.4 中断线程化后优先级不够
驱动用request_threaded_irq加IRQF_ONESHOT,底半部跑在内核线程里。如果这个线程优先级低于其他实时任务,长时间运行时会被抢占,导致中断处理延迟。可以用chrt把相关内核线程优先级提上去,或者检查CONFIG_PREEMPT配置。
# 找到 mcp251x 相关内核线程 ps -eLo pid,tid,cls,rtprio,comm | grep -i mcp # 提升优先级(示例,按实际 tid 调整) chrt -f -p 80 <tid>5.5 采样点与对端不一致
500K 波特率下,采样点 87.5% 是常见推荐值,但如果对端设备用的是 75%,两边采样时刻错开,长线传输时边沿抖动就会导致偶发错误帧。用示波器量一下 CAN_H/CAN_L 的位时间,或者用ip -details确认本端采样点,再和对端手册核对。
6. 把排查链路固定成流程
这套问题排查下来,我的经验是:先别动驱动代码,先看系统层。MCP2515 驱动本身经过大量平台验证,真正导致 0.01% 丢包的,往往是日志抢占、SPI 频率、队列长度这些外围配置。把printk等级调高、SPI 降到 8MHz、txqueuelen调到 1000 这三步做完,大部分长时间丢包都能压下去。
如果三步做完还在丢,再回到中断和采样点,用 candump 的序号对比法定位是接收侧还是发送侧。接收侧丢包看中断延迟,发送侧丢包看队列和 SPI 传输耗时。整个链路里,config.toml 负责把参数集中管理,candump 负责给出可量化的证据,两者配合才能把"偶发丢包"变成"可定位问题"。
需要查 MCP2515 寄存器位定义或驱动参数时,用模型对话直接问最快;要把这套抓包对比脚本做成长期跑的自动化任务,走 Coding Plan 更合适。接入地址和文档都在上面第 2 节,按场景取用即可。