1. 项目概述:当树莓派的SPI0口被占用时
玩树莓派的朋友,尤其是做物联网、机器人或者汽车电子相关项目的,肯定对MCP2515这颗CAN总线控制器芯片不陌生。它价格便宜,驱动成熟,是让树莓派接入CAN网络最经典、最稳定的方案。通常的教程都会告诉你,把MCP2515模块接到树莓派的SPI0接口(也就是那组标着MOSI, MISO, SCK, CE0, CE1的引脚)上,然后加载spidev和mcp251x驱动,一切就搞定了。
但现实情况往往更复杂。你的树莓派上可能已经接了一个SPI屏幕、一个SPI接口的ADC芯片,或者像我之前做的一个项目,SPI0被一个高精度的惯性测量单元(IMU)给占用了。这时候,你看着手头另一个急需CAN通信功能的项目,难道要再买一块树莓派?当然不。树莓派从3B+开始,其实就隐藏了第二组甚至第三组SPI硬件接口,也就是SPI1和SPI2。它们没有像SPI0那样有现成的、物理引脚直接引出,但通过配置设备树(Device Tree),我们可以将这些接口“映射”到指定的GPIO引脚上,从而让MCP2515“移居”到新的SPI总线。
这个项目要解决的,就是这个“移居”问题。核心目标很明确:在不干扰原有SPI0设备工作的前提下,启用树莓派的SPI1或SPI2接口,并成功驱动MCP2515 CAN控制器,使其稳定工作。整个过程会深入到Linux的设备树配置、驱动加载和引脚复用,是深入理解树莓派底层硬件和Linux驱动模型的一个绝佳实践。无论你是想扩展树莓派的接口能力,还是单纯对设备树感到好奇,这篇从踩坑到填坑的完整记录,都能给你一份可以直接“抄作业”的指南。
2. 硬件与原理深度解析
2.1 树莓派的SPI硬件资源盘点
很多人以为树莓派只有一个SPI,其实不然。以最常见的树莓派3B+/4B/5为例,其博通BCM283x系列SoC内部通常包含多个SPI控制器:
- SPI0 (Primary SPI): 这是主SPI接口,对应物理引脚GPIO 9 (MISO), 10 (MOSI), 11 (SCLK),以及片选CE0 (GPIO 8)和CE1 (GPIO 7)。它被最广泛地支持,默认在系统配置中启用。
- SPI1 (Auxiliary SPI): 这是一个辅助SPI接口。在树莓派上,它通常没有预定义的标准引脚映射,需要用户手动配置。其硬件控制器是独立的,可以与SPI0并行工作。
- SPI2 (Secondary SPI): 在某些型号(如CM4)或特定配置下可用,是另一个独立的SPI控制器。
为什么默认只用SPI0?主要是为了兼容性和简化用户入门。把一组功能固定、文档齐全的引脚留给最常用的外设,可以避免新手混淆。SPI1和SPI2则留给了有更高级定制需求、或者系统设计者(比如使用树莓派计算模块做载板)去自由发挥。
选择SPI1还是SPI2?对于大多数让MCP2515“移居”的场景,SPI1是更常见、更稳妥的选择。原因有三:首先,其驱动支持相对更完善;其次,可用的GPIO引脚选择更多,更容易找到一组未被其他功能占用的“干净”引脚;最后,社区实践案例更丰富,遇到问题更容易找到参考。SPI2通常在一些更专业的扩展板或计算模块的特定引脚上才有引出,通用性稍弱。因此,除非你的硬件设计明确要求使用SPI2,否则建议优先考虑SPI1。
2.2 MCP2515与SPI通信的关键细节
MCP2515是一颗独立的CAN控制器,它通过SPI接口接受主控制器(这里是树莓派)的指令,并独立处理CAN协议帧的收发、滤波、缓冲等复杂任务。这意味着树莓派CPU的负载很低,只需要通过SPI读写几个寄存器即可。
通信的关键参数必须匹配:
- SPI模式: MCP2515支持SPI模式0,0和1,1。绝大多数情况下,使用Mode 0(CPOL=0, CPHA=0) 即可。这是最常用的SPI模式,时钟空闲时为低电平,数据在时钟上升沿采样。
- 时钟频率: MCP2515的最高SPI时钟频率典型值为10 MHz。树莓派的SPI控制器可以轻松输出这个频率。在实际配置中,我们通常会设置一个低于上限的值以确保稳定,比如8 MHz或5 MHz。
- 片选 (Chip Select, CS): 这是SPI通信中用于选择从设备的关键信号。每个SPI控制器可以管理多个片选线。我们需要为MCP2515分配一个独立的片选引脚。
硬件连接示意(以使用SPI1为例): 假设我们选择GPIO 16作为SPI1的片选(CE0),那么连接如下:
| 树莓派 GPIO 引脚 (BCM编号) | 引脚功能 (SPI1) | 连接至 MCP2515 模块引脚 |
|---|---|---|
| GPIO 16 (Pin 36) | SPI1_CE0_N (片选0) | CS (Chip Select) |
| GPIO 19 (Pin 35) | SPI1_MISO (主入从出) | SO (Serial Output) |
| GPIO 20 (Pin 38) | SPI1_MOSI (主出从入) | SI (Serial Input) |
| GPIO 21 (Pin 40) | SPI1_SCLK (时钟) | SCK (Clock) |
| 任意 3.3V 引脚 (如 Pin 1) | 3.3V Power | VCC |
| 任意 GND 引脚 (如 Pin 39) | Ground | GND |
注意:务必确认你的MCP2515模块逻辑电平是3.3V。树莓派的GPIO是3.3V电平,如果模块是5V的,需要加电平转换电路,否则可能损坏树莓派。
2.3 设备树(Device Tree)的核心作用
设备树是Linux内核用于描述硬件配置的一种数据结构。它就像一个“菜单”,告诉内核:“这块板子上,在什么地方(哪个GPIO),有什么样的设备(SPI控制器),以及这个设备上又挂了什么子设备(MCP2515)”。
在树莓派上,默认的bcm2711-rpi-4-b.dtb(对于Pi 4)等设备树二进制文件只启用了SPI0。我们要使用SPI1,就需要修改这个“菜单”,增加关于SPI1控制器及其引脚配置的条目,并在它下面挂载一个mcp2515子节点。
修改方式不是直接编辑二进制文件,而是编写一个“覆盖层”(Device Tree Overlay)。这是一个小型的、只描述我们想要修改或新增部分的设备树片段。系统启动时,它会自动与基础设备树合并,从而实现动态的硬件配置,无需重新编译整个内核。这是我们本次项目的技术核心。
3. 软件环境准备与配置
3.1 系统与内核头文件
首先,确保你的树莓派系统是最新的,并且安装了编译设备树覆盖层所必需的内核头文件。我使用的是树莓派官方64位Bullseye系统,但方法适用于多数基于Debian的版本。
# 更新软件包列表并升级系统 sudo apt update sudo apt full-upgrade -y # 安装设备树编译器(dtc)和内核头文件 sudo apt install -y device-tree-compiler raspberrypi-kernel-headers # 重启以确保使用新内核 sudo reboot安装内核头文件非常关键。因为编译覆盖层时,需要引用内核源代码中的头文件来获取正确的引脚定义和驱动绑定信息。如果缺少头文件,编译可能会失败,或者生成错误的覆盖层。
3.2 确认SPI驱动状态
在开始之前,先检查一下当前SPI接口的状态。
# 查看已加载的SPI相关内核模块 lsmod | grep spi # 查看系统识别到的SPI控制器 ls /dev/spidev*正常情况下,如果你从未启用过SPI,/dev/spidev*可能不存在。如果你启用了SPI0(通过raspi-config或/boot/config.txt),你可能会看到/dev/spidev0.0和/dev/spidev0.1。我们的目标是让系统在配置后出现/dev/spidev1.0(对应SPI1, CE0)。
同时,检查MCP2515的驱动是否可用:
# 查找MCP2515相关的内核模块 find /lib/modules/$(uname -r) -name "*mcp251*" -type f你应该能看到类似mcp251x.ko的文件。如果没有,可能需要安装或编译内核模块,但树莓派官方内核通常已包含。
4. 创建并编译SPI1设备树覆盖层
这是整个过程中最核心的一步。我们将创建一个.dts(设备树源文件),描述如何启用SPI1并将其引脚映射到我们想要的GPIO上。
4.1 编写设备树覆盖层源文件
创建一个新文件,例如spi1-mcp2515.dts:
sudo nano /boot/overlays/spi1-mcp2515.dts将以下内容粘贴进去。请仔细阅读注释,理解每一部分的作用:
/dts-v1/; /plugin/; / { compatible = "brcm,bcm2835"; // 与树莓派SoC兼容 fragment@0 { target = <&gpio>; __overlay__ { // 定义SPI1引脚的状态,使用pinctrl子系统 spi1_pins: spi1_pins { brcm,pins = <19 20 21>; // GPIO 19 (MISO), 20 (MOSI), 21 (SCLK) brcm,function = <3>; // 功能码3代表ALT4,即SPI1功能 brcm,pull = <0>; // 上下拉电阻:0=关闭,1=下拉,2=上拉 }; spi1_cs_pins: spi1_cs_pins { brcm,pins = <16>; // GPIO 16 作为片选CE0 brcm,function = <1>; // 功能码1代表输出模式 // 注意:片选引脚通常由驱动控制,初始化为输出高电平(不选中) }; }; }; fragment@1 { target = <&spi1>; // 目标是SPI1控制器节点 __overlay__ { #address-cells = <1>; #size-cells = <0>; status = "okay"; // 启用SPI1控制器 pinctrl-names = "default"; pinctrl-0 = <&spi1_pins &spi1_cs_pins>; // 应用上面定义的引脚配置 // 定义SPI1总线上的从设备:MCP2515 mcp2515@0 { compatible = "microchip,mcp2515"; // 驱动匹配的关键字 reg = <0>; // 片选编号0,对应我们定义的spi1_cs_pins中的GPIO 16 spi-max-frequency = <10000000>; // SPI时钟频率,10MHz interrupt-parent = <&gpio>; // 中断引脚连接到GPIO interrupts = <26 8>; // 使用GPIO 26,高电平触发 (8 = IRQ_TYPE_LEVEL_HIGH) clocks = <&mcp2515_osc>; // 引用下面的振荡器定义 // 定义MCP2515外部的振荡器 mcp2515_osc: oscillator { compatible = "fixed-clock"; #clock-cells = <0>; clock-frequency = <16000000>; // MCP2515模块常见的16MHz晶振频率 }; }; }; }; fragment@2 { target-path = "/"; __overlay__ { // 这个片段可以留空,或者用于其他全局定义 // 例如,如果需要定义一个稳压器,可以放在这里 }; }; };关键参数解析与避坑指南:
brcm,function: 这是最易出错的地方。树莓派每个GPIO有多个复用功能(ALT0, ALT1, ALT2, ALT3, ALT4, ALT5)。对于SPI1:- GPIO 19 (MISO): 功能应为ALT4
- GPIO 20 (MOSI): 功能应为ALT4
- GPIO 21 (SCLK): 功能应为ALT4
- GPIO 16 (CE0): 作为GPIO控制的片选,设置为普通输出模式(
<1>)。也可以设置为ALT4让硬件控制,但软件控制更灵活。
interrupts = <26 8>: 这一行定义了MCP2515的中断引脚。MCP2515的INT引脚需要连接到一个树莓派的GPIO(这里以GPIO 26为例),并在收到CAN消息时产生中断。<26 8>表示使用GPIO 26,中断触发类型为“高电平有效”(8)。你必须将MCP2515模块的INT引脚实际连接到树莓派的GPIO 26(或其他你指定的引脚),并在此处修改对应的GPIO编号。如果不需要中断功能(轮询模式),可以删除interrupt-parent和interrupts这两行,但强烈建议使用中断以提高效率。clock-frequency = <16000000>: 这个值必须与你MCP2515模块上焊接的晶振频率一致!常见的有8MHz, 16MHz。用错了会导致CAN通信波特率计算错误,无法通信。拆开模块看一下晶振上印的数字是最保险的。spi-max-frequency: 设置为10,000,000 (10MHz)。这是MCP2515的安全上限。如果你的布线较长或有干扰,可以降低到5MHz或8MHz以提高稳定性。
4.2 编译与部署覆盖层
保存并退出编辑器(在nano中按Ctrl+X,然后按Y,再按Enter)。
接下来,使用设备树编译器(dtc)将这个.dts源文件编译成.dtbo二进制覆盖层文件:
# 编译设备树覆盖层 sudo dtc -@ -I dts -O dtb -o /boot/overlays/spi1-mcp2515.dtbo /boot/overlays/spi1-mcp2515.dts参数解释:
-@: 允许使用符号引用,对于覆盖层是必须的。-I dts: 输入格式为dts。-O dtb: 输出格式为dtb(设备树二进制)。-o: 指定输出文件路径。
编译成功后,/boot/overlays/目录下会生成spi1-mcp2515.dtbo文件。
4.3 配置系统加载覆盖层
现在需要告诉系统在启动时加载这个覆盖层。编辑树莓派的config.txt文件:
sudo nano /boot/config.txt在文件末尾添加以下行:
# 启用SPI1接口并加载MCP2515配置 dtoverlay=spi1-mcp2515如果你想同时启用SPI0,确保文件中没有dtparam=spi=off,或者有dtparam=spi=on。默认情况下SPI0是关闭的,如果需要可以加上dtparam=spi=on。
保存并退出。
5. 系统重启与驱动加载验证
5.1 重启并检查内核日志
执行重启命令:
sudo reboot系统重启后,首先查看内核启动日志,确认覆盖层是否加载成功,以及设备是否被识别:
# 查看内核启动信息,过滤出SPI和MCP2515相关日志 sudo dmesg | grep -E "(spi|mcp2515|CAN)"你应该能看到类似下面的输出,这表明SPI1控制器和MCP2515设备已被成功识别和注册:
[ 5.123456] spi-bcm2835 3f804000.spi: SPI controller at 0x3f804000 (irq 93) [ 5.234567] mcp251x spi1.0: MCP2515 successfully initialized. [ 5.234568] mcp251x spi1.0: CAN device registered (irq=123)如果看到错误,比如“failed to claim CS pin”或“probe failed”,请返回检查设备树覆盖层中的引脚定义是否正确,是否有冲突,以及硬件连接是否牢固。
5.2 验证设备节点
检查SPI设备节点和CAN网络接口是否创建:
# 查看SPI设备,现在应该能看到spidev1.0 ls -l /dev/spidev* # 查看CAN网络接口,应该能看到can0 ip link show如果一切正常,ip link show命令的输出中会有一个can0接口,状态是DOWN(还未启动)。
5.3 配置并启动CAN接口
CAN接口需要配置波特率后才能启动。使用ip命令和can-utils工具包(需要安装:sudo apt install can-utils):
# 设置CAN0的波特率为500kbps,这是汽车CAN网络最常用的速率之一 sudo ip link set can0 type can bitrate 500000 # 启动CAN0接口 sudo ip link set up can0 # 再次查看接口状态,现在应该是UP状态了 ip link show can0输出应显示state UP。你也可以使用ifconfig can0来查看。
6. 功能测试与数据收发
6.1 基础回环测试
首先,进行本地回环测试,验证驱动和接口基本功能是否正常:
# 设置CAN0为回环模式(自己发,自己收) sudo ip link set can0 type can bitrate 500000 loopback on sudo ip link set up can0 # 在一个终端发送数据 candump can0 & # 在另一个终端(或新开一个SSH窗口)发送一帧数据 cansend can0 123#1122334455667788如果回环测试成功,你会在candump的终端看到接收到的帧,ID是123,数据是11 22 33 44 55 66 77 88。
6.2 连接真实CAN网络测试
关闭回环模式,连接到真实的CAN总线(例如,连接一辆汽车的OBD-II接口,或另一个CAN节点):
# 关闭接口,取消回环模式 sudo ip link set down can0 sudo ip link set can0 type can bitrate 500000 loopback off # 重新启动接口 sudo ip link set up can0现在,使用candump can0可以监听总线上的所有CAN帧。使用cansend can0 <can_id>#<data>可以向总线发送帧。例如,读取车辆发动机转速(标准OBD-II PID 0x0C)的请求帧可能是:
cansend can0 7DF#02010C0000000000如果车辆支持并响应,你会在candump中看到回复帧。
6.3 使用SocketCAN高级工具
can-utils提供了更多强大工具:
cangen: 自动生成并发送随机或规律的CAN帧,用于压力测试。canbusload: 计算并显示CAN总线的负载率。canplayer: 播放之前录制的CAN日志文件(.log格式)。
例如,进行简单的总线负载测试:
# 在后台以100Hz频率发送随机ID和数据的帧 cangen can0 -g 10 -i -I 7FF -L 8 & # 监控总线负载 canbusload can0 5000007. 故障排查与经验实录
即使按照步骤操作,也可能会遇到问题。以下是我在多次实践中总结的常见问题及解决方法。
7.1 设备树覆盖层编译或加载失败
问题现象:dmesg中无相关日志,或提示Failed to load overlay 'spi1-mcp2515'。
排查步骤:
- 检查语法:确保
.dts文件没有拼写错误,特别是分号、括号是否匹配。可以使用dtc -I dts -O dtb -o /dev/null your_file.dts 2>&1来检查语法,不输出文件只检查错误。 - 检查路径和权限:确保
.dtbo文件在/boot/overlays/目录下,并且config.txt中的dtoverlay=行拼写正确,没有多余空格。 - 引脚冲突:这是最常见的原因。你选择的GPIO(如16, 19, 20, 21, 26)可能被系统其他功能占用(比如UART, I2C,或者被用户空间程序设置为输出)。使用
raspi-gpio get命令查看这些引脚当前的功能状态。确保它们在加载覆盖层前是INPUT状态且未被占用。有时需要禁用其他可能冲突的接口,比如在config.txt中添加enable_uart=0或dtparam=i2c_arm=off(如果它们和你的SPI1引脚冲突)。 - 内核头文件不匹配:如果系统刚升级过内核但未重启,或者
raspberrypi-kernel-headers版本与运行的内核版本不一致,会导致编译的覆盖层无法加载。确保uname -r显示的内核版本与已安装的头文件版本一致。
7.2 CAN接口无法启动(状态一直为DOWN)
问题现象:ip link show can0显示state DOWN,执行sudo ip link set up can0后报错,或dmesg中有MCP2515相关的错误(如“transmit error”、“bus-off”)。
排查步骤:
- 检查中断引脚:如果设备树中配置了中断(
interrupts属性),但硬件上没有连接,或者连接错误(比如接到了3.3V或GND),会导致驱动初始化失败或工作异常。确认MCP2515的INT引脚正确连接到指定的GPIO,并且该GPIO在设备树中配置正确。 - 检查晶振频率:这是导致CAN波特率不准、进而无法通信或频繁“bus-off”的元凶。百分之百确认
clock-frequency的值与你模块上晶振的标称值完全一致。用放大镜看。 - 检查物理连接和电源:
- 确保SPI四根线(MISO, MOSI, SCLK, CS)连接正确,没有接反。
- 用万用表测量MCP2515模块的VCC电压,确保是稳定的3.3V。电压不足会导致芯片工作不稳定。
- 检查CANH和CANL之间的终端电阻。高速CAN总线需要在两端各接一个120欧姆的终端电阻。如果只是点对点测试,在两个节点上各接一个,或者直接在总线两端接上。
- 降低SPI速率:在设备树覆盖层中将
spi-max-frequency从10000000改为5000000(5MHz),重新编译加载测试。长导线或干扰环境可能导致高速SPI通信出错。 - 检查CAN总线电平:使用示波器或CAN总线分析仪查看CANH和CANL的波形。隐性电平(逻辑1)时,两者电压应接近2.5V且相等。显性电平(逻辑0)时,CANH应约3.5V,CANL应约1.5V。如果电平异常,检查MCP2515的CAN收发器(通常是TJA1050或SN65HVD230)是否损坏,或者VCC电压是否正常。
7.3 能收到数据但发送失败
问题现象:candump能收到其他节点的数据,但cansend发送后,自己收不到(非回环模式),对方也收不到。
排查步骤:
- 检查总线终端电阻:缺少终端电阻会导致信号反射,严重时可能无法通信。确保总线上至少有两个120欧姆电阻。
- 检查波特率一致性:总线上所有节点的波特率必须完全相同。哪怕有1%的误差,长期通信也可能出问题。使用
ip -details link show can0确认设置的波特率。如果可能,用分析仪确认总线实际波特率。 - MCP2515发送缓冲区错误:查看内核日志
dmesg | grep mcp2515,是否有“TX buffer full”或类似错误。这可能是软件发送过快,而硬件来不及处理。可以尝试在发送命令间增加微小延迟。
7.4 性能优化与稳定性建议
- 中断与轮询:设备树中配置中断是最佳实践。中断方式可以极大降低CPU占用率,提高实时性。如果确实不想用中断,驱动也支持轮询模式,但需要在加载内核模块时传递参数
oscillator=16000000(根据你的晶振修改),并确保设备树中不定义interrupts属性。轮询模式下,需要用一个内核线程不断检查芯片状态,对简单应用可以,但对负载重的总线不推荐。 - 提高接收缓冲区:默认的SocketCAN接收缓冲区可能较小,在高负载下会丢帧。可以增加缓冲区大小:
可以将这些命令添加到sudo sysctl -w net.core.rmem_max=262144 sudo sysctl -w net.core.rmem_default=262144/etc/sysctl.conf中使其永久生效。 - 使用更高效的发送方式:对于需要高速、连续发送的应用(如模拟ECU数据),使用
cangen或自己编写C程序使用SocketCAN接口,比循环调用cansend脚本效率高得多,因为后者每次都要建立和销毁Socket连接。
经过以上步骤,你应该已经成功将MCP2515挂载到了树莓派的SPI1接口上,并建立了稳定的CAN通信。这个过程虽然涉及到底层的设备树配置,但每一步都有其明确的目的。掌握这个方法后,你就能让树莓派灵活地适配各种硬件连接需求,不再受限于默认的引脚定义。下次当SPI0被占用时,你完全可以自信地说:“没关系,我们还有SPI1。”