ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32用SPI扩展多路CAN:MCP2515驱动与调试实战

STM32用SPI扩展多路CAN:MCP2515驱动与调试实战 做嵌入式这些年但凡听到“板子上的CAN口不够用”我第一反应基本都是同一个方向用SPI去外扩CAN控制器。前两天还有个朋友问我手头MCU只剩一个SPI和几个空闲GPIO却要接两路CAN问能不能搞。我说太能了这不只是能搞搞完还能留下余量后面想加到四路、六路也只是换板子加片选的事。今天就把这套方案从头拆一遍从硬件选型到CubeMX配置再到怎么把SPI上的寄存器操作封装成好用的驱动最后把我在调试中踩过的坑一起摆出来。如果你正在搞STM32F103这类只有单路CAN的MCU或者想在一块主控下挂多路CAN节点这篇应该能帮你少走不少弯路。1. 为什么我推荐用SPI扩展CAN选题逻辑和方案对比1.1 什么场景下会用到多路CAN多路CAN不是没事找事。工控设备里常见的一种场景是运动控制器一个主控既要跟伺服驱动器走CANopen又要跟IO扩展模块走私有CAN协议两边波特率还不一样一路CAN根本忙不过来。车载测试设备也是重灾区一个测试盒子往往同时挂在整车的动力CAN、车身CAN和诊断CAN上物理上就是三路独立总线不可能共用一条。还有一种更隐蔽的需求是“协议隔离”。比如设备本身是CANopen主站但还需要从外部采集一些非标准报文。如果把所有节点扔到一条总线上不同协议之间互相干扰排查起来特别痛苦。用多路CAN把不同通信域隔开各走各的调试时一抓一个准。这类需求落到MCU选型上就很尴尬。主流MCU普遍只带1到2路CAN控制器带3路以上CAN的芯片要么贵要么采购周期长要么你手头已经在用的平台根本没那么高配的型号。这时候SPI扩CAN就成了最现实的选择。1.2 几种扩CAN方案的横向对比先说结论没有最优方案只有最合适的方案。我用了几年以后把常见做法拉了一张对比表。方案实现成本通道扩展能力实时性灵活性适合场景用MCU内置多路CAN硬件简单芯片选型受限固定一般最多2路最好差新项目选型阶段就可以规划SPI外扩独立CAN控制器需要增加芯片和外围电路一条SPI总线挂多个扩展很灵活受SPI速率影响但够用好现有板卡改版、多协议隔离、快速出样外接USB转CAN模块成本低接线简单受USB口数量限制通常1-2路依赖USB协议栈延迟高一般上位机调试、PC端采集不适合MCU嵌入式用FPGA实现CAN控制器灵活但开发量巨大很强极好极强定制化量产、特殊波特率、极多通道我大部分项目都落在第二行。原因有三个STM32F103的SPI接口是现成的CAN控制器芯片如MCP2515成本不高而且驱动逻辑可以完全由自己做主。第三行看着省事但让MCU挂USB转CAN模块属于给自己挖坑USB Host栈跑到单片机里协议栈开销和稳定性都是一言难尽。1.3 SPI方案的优势在哪里又有什么限制SPI扩CAN最大的好处是“数量自由”。一条SPI总线上可以挂多个MCP2515每个用一根片选线CS区分。MCU的GPIO一般管够扩四路CAN一共也就占用SPI的SCK、MOSI、MISO三根线加四路片选。如果后面发现四路还不够板子上预留几个电阻位把新芯片的CS并到新的GPIO上驱动的改动量很小。代价也有最明显的是带宽。SPI外扩CAN的总吞吐量受SPI时钟和协议开销限制。MCP2515的SPI接口最高能跑到10MHz左右一次报文操作大约要读写好几个帧的寄存器数据。假设波特率是500kbps一条CAN总线每秒最多约几千帧报文SPI通信通常在报文层面完全够用。但如果你跑CAN FD数据段速率高达5Mbps想靠MCP2515这种老芯片去顶那确实不现实。这种场景要么换MCP2517FD/2518FD这类支持CAN FD的SPI控制器要么干脆上更高性能的多CAN芯片。还有一个限制是延迟。SPI读写本身是主从同步通信MCU需要主动去轮询或者靠中断信号触发读取相比硬件CAN控制器通过FIFO和DMA自动收包CPU占用率会高一些。所以SPI方案适合“多路但并不极端高速”的场景如果每个通道都是满负载的CAN FD高波特率通信建议重新考虑架构。2. 系统架构和底层原理搞懂SPI和CAN怎么握手2.1 整体硬件组网方式先画一遍硬件结构。主控MCU的SPI接口分别连接CAN控制器MCP2515的SCK、SDI、SDO引脚再用一个GPIO连它的CS片选。MCP2515后面再接一颗CAN收发器比如MCP2562、TJA1050或者SN65HVD230收发器再把CAN_H和CAN_L引到接线端子。多路扩展就是同一组SPI信号线并联挂多个MCP2515每一个芯片的CS独立接到MCU的不同GPIO。因为SPI从机的SCK、SDI、SDO在没有被片选选中时可以看成高阻状态所以并联不会互相干扰。收发的数据隔离靠CS完成这是SPI协议天然支持多从机的原因。MCP2515还需要一个外部晶振或者时钟源常见的是20MHz晶振。有些同学图省事想用MCU的MCO引脚直接输出时钟给它也能工作但要注意MCU时钟配置不能随意改改完总线波特率会跟着跑偏。独立晶振反而是最稳的。中断线值得一提。MCP2515有个INT引脚当收到报文或者发生错误时会拉低可以接到MCU的EXTI外部中断引脚这样就不需要MCU不停轮询。多路CAN时每片MCP2515可以各自配一个中断引脚也可以把多个INT引脚直接并到一起因为中断是电平触发MCU检测到低电平后读每一路的中断状态寄存器就知道是谁触发的。2.2 SPI时序、片选策略与DMA配合MCP2515的SPI接口支持标准SPI模式我常用的是模式0,0也就是CPOL0、CPHA0。数据在SCK上升沿采样MSB先发送。这个时序在STM32F103的SPI外设里很好配置CubeMX里选好模式波特率分频根据实际时钟算一下就行。片选使用上一直有硬件片选和软件片选两种做法。硬件NSS由SPI外设自动控制看起来省心但在多从机和DMA场景下容易踩坑。NSS信号在不同芯片上触发时机不完全一致当你用DMA连续搬运数据时硬件NSS的拉高拉低时机做到精确控制很麻烦。软件片选就是用普通GPIO手动控制CS传输前拉低传输结束后拉高跟SPI外设完全解耦。对比项硬件NSS软件GPIO片选配置复杂度低复用SPI外设引脚略高需要手动操作GPIO多从机支持麻烦通常只适合单从机非常容易每个从机一根线DMA配合容易出半包、竞争问题可控性强按字节流控制调试友好度波形上看不到片选切换细节逻辑分析仪上一目了然我的建议简单单从机场景多路CAN、DMA传输一律用软件片选DMA方面读取CAN控制器内部寄存器这种短数据操作用轮询就好没必要上DMA。但对于批量读取接收缓冲区、连续写入发送缓冲区这类超过几十字节的传输DMA能明显降低CPU占用。STM32F103的SPI配合DMA收发比较常见CubeMX里把SPI的发送和接收DMA请求都打开代码里分别准备两个数组用HAL_SPI_TransmitReceive_DMA触发全双工传输。2.3 CAN控制器和收发器的分工很多初学者会把CAN控制器和CAN收发器搞混。CAN控制器负责协议层面的事比如组帧、解析、CRC校验、仲裁、错误状态管理。CAN收发器负责物理层面的事把控制器输出的逻辑电平转换成CAN_H和CAN_CAN_L上的差分电平信号。MCP2515内置了三个发送缓冲区和两个接收缓冲区还有自己的报文过滤和屏蔽逻辑。这意味着MCU不需要每帧报文都参与处理符合条件的报文会自动进入接收缓冲区并通过INT引脚通知MCU来取。发送时MCU把完整报文写到发送缓冲区然后触发请求发送命令MCP2515会自动进行位填充、CRC计算、总线仲裁。CAN收发器选择的坑主要在电压域和速率上。如果是3.3V系统选SN65HVD230这种3.3V收发器配合3.3V的MCP2515刚好。如果是5V系统TJA1050这类经典收发器很皮实。千万别让5V收发器直接接3.3V控制器电平不兼容会导致通信异常甚至烧引脚。2.4 终端电阻与地偏移的基本功CAN总线两端各需要一颗120欧姆终端电阻。注意是“两端各一颗”不是每个节点一颗。很多人图省事在每个节点都放一个可跳线的120欧姆电阻结果多条CAN设备并联在一起时终端电阻并联后等效值远低于60欧姆信号反射严重通信距离一大就丢帧。地偏移是最容易被忽视的问题。CAN总线虽然靠差分信号抗干扰但每个节点的CAN收发器共模输入范围是有限的。当两个节点之间地电位差过大差分信号整体被抬高或拉低收发器就无法正确采样。我见过不少现场问题总线波特率没问题终端电阻没问题但就是偶发错误帧最后发现是节点间地线接触不良地偏移超过几伏。测量地偏移并不复杂。设备都供电但不通信的情况下用万用表交流档和直流档分别测两个节点地线之间的电压正常应接近0V如果直流偏移超过1V或者有较大的交流分量就需要检查地线连接和隔离方案。条件允许的话在CAN收发器侧用DC-DC隔离电源加数字隔离器把节点地彻底隔开几乎能解决绝大多数地环路问题。3. 基于CubeMX和STM32F103的完整实现过程3.1 CubeMX里的SPI和DMA配置这里以STM32F103C8T6为例配合一个MCP2515模块。先打开CubeMX选择芯片型号在Pinout视图里把SPI1的SCK、MOSI、MISO引脚映射出来。PA5是SCKPA7是MOSIPA6是MISO片选我用PA4中断用PA3都设为GPIO_Output和GPIO_EXTI。SPI1的参数配置如下Mode选Full-Duplex MasterHardware NSS DisableClock Speed设成系统时钟APB2的二分频或四分频保证实际SCK在5MHz以内比较稳妥。CPOL和CPHA都选Low和1 Edge也就是模式0,0。Data Size选8bitFirst Bit选MSB First。DMA配置在DMA Settings选项卡里把SPI1_TX和SPI1_RX分别添加方向分别是MemoryToPeripheral和PeripheralToMemoryMode选Normal。这是比较关键的一步如果选成Circular模式DMA会在传输结束后反复传输导致SPI总线被反复触发。生成工程前记得在GPIO设置里把PA3的模式设为External Interrupt Mode with Falling edge trigger detectionPA4设为Output Push Pull初始电平设High因为MCP2515的CS是低电平有效。初始化代码建议保留在CubeMX自动生成的MX_GPIO_Init和MX_SPI1_Init中后续不要手动删改避免重新生成工程时冲突。3.2 驱动封装用SPI寄存器操作MCP2515MCP2515的寄存器操作完全靠SPI命令完成。常用命令有RESET0xC0、READ0x03、WRITE0x02、RTS0x80缓冲区编号、READ_STATUS0xA0。底层驱动说穿了就是封装好CS拉低、SPI收发一个字节、CS拉高这三件事。// 底层字节收发单个字节轮询方式适合寄存器读写 uint8_t mcp_spi_transfer(uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, byte, rx, 1, 100); return rx; } // 读寄存器 uint8_t mcp_read_reg(uint8_t reg) { uint8_t val 0; MCP_CS_LOW(); mcp_spi_transfer(0x03); // READ 命令 mcp_spi_transfer(reg); // 寄存器地址 val mcp_spi_transfer(0x00); // 读回数据 MCP_CS_HIGH(); return val; } // 写寄存器 void mcp_write_reg(uint8_t reg, uint8_t val) { MCP_CS_LOW(); mcp_spi_transfer(0x02); // WRITE 命令 mcp_spi_transfer(reg); mcp_spi_transfer(val); MCP_CS_HIGH(); }上面这段是最初级的版本适合刚起步时验证SPI通路。实际项目里我会在这之上包一层带片选参数和多实例支持的函数比如把“当前操作的是第几路CAN”作为参数传入。C语言里用结构体保存每个MCP2515的片选GPIO和中断状态代码结构清晰很多。初始化流程一般是上电延时几毫秒发RESET命令然后配置CANCTRL寄存器进入配置模式再写波特率相关的CNF1、CNF2、CNF3寄存器配置验收过滤寄存器最后把CANCTRL切回Normal模式。每一步都可以通过读取状态寄存器来确认配置是否生效。3.3 发送、接收和中断处理MCP2515发送一帧报文本质上是把报文的ID、DLC、数据字节以及帧格式信息写入发送缓冲区然后发RTS命令请求发送。下面这段代码演示了标准数据帧的发送流程void mcp_send_std_frame(uint8_t buf_idx, uint16_t id, uint8_t *data, uint8_t len) { uint8_t buf_reg TXB0CTRL (buf_idx * 0x10); MCP_CS_LOW(); mcp_spi_transfer(0x40 (buf_idx * 0x10)); // LOAD TXB0/B1/B2 mcp_spi_transfer((uint8_t)((id 3) 0xFF)); // TXB0SIDH mcp_spi_transfer((uint8_t)((id 0x07) 5)); // TXB0SIDL mcp_spi_transfer(0x00); // TXB0EID8 mcp_spi_transfer(0x00); // TXB0EID0 mcp_spi_transfer(0x00); // DLC: 标准帧 数据长度 for (int i 0; i len; i) { mcp_spi_transfer(data[i]); // 数据段 } MCP_CS_HIGH(); // 请求发送指定缓冲区 MCP_CS_LOW(); mcp_spi_transfer(0x80 (1 buf_idx)); // RTS 命令 MCP_CS_HIGH(); }接收那边我推荐用中断加标志位的方式。INT引脚触发下降沿外部中断后MCU调用中断服务函数读取MCP2515的接收状态寄存器判断哪个接收缓冲区有帧然后按地址读出ID和数据置一个“有CAN数据待处理”的软件标志。主循环里看到标志再去解析避免在中断里做太多耗时操作。中断服务函数里还有一个容易忽略的动作读取接收缓冲区之后要发送一个清除接收标志的命令或者读取对应寄存器否则中断会一直被触发MCU卡在中断里出不来。第一次调的时候要是发现程序动不动死循环先怀疑这个。3.4 从一路CAN扩展到多路CAN上面所有底层函数都基于MCP2515本身和“是第几路”没有关系。扩展到多路只需要在代码里加入一个索引参数操作不同的片选引脚。typedef struct { GPIO_TypeDef *cs_port; uint16_t cs_pin; GPIO_TypeDef *int_port; uint16_t int_pin; } mcp_instance_t; // 示例定义两路CAN mcp_instance_t can_instances[2] { {GPIOA, GPIO_PIN_4, GPIOA, GPIO_PIN_3}, {GPIOB, GPIO_PIN_0, GPIOB, GPIO_PIN_1}, }; void mcp_select_instance(uint8_t idx) { // 先把所有片选拉高 for (int i 0; i 2; i) { HAL_GPIO_WritePin(can_instances[i].cs_port, can_instances[i].cs_pin, GPIO_PIN_SET); } // 再选中目标 HAL_GPIO_WritePin(can_instances[idx].cs_port, can_instances[idx].cs_pin, GPIO_PIN_RESET); }所有寄存器操作函数前面先调用mcp_select_instance就能做到一套驱动函数管多路CAN。中断那边同理可以在每个INT引脚的EXTI回调里设置对应的通道标志主循环按优先级处理。这里有个细节SPI总线上只允许一个从机被选中所以在写多路驱动时务必保证所有SPI事务都按照“选中某一路、发完整命令、取消选中”的原子方式执行。如果代码里出现一个函数还没执行完就去切换片选总线协议会被打乱。我自己写过一版中断和主循环共用一个SPI的代码结果两边同时抢总线后来加了互斥标志才稳定。4. 调试过程中遇到的典型问题和排查方法4.1 SPI通信不生效多半不是协议问题很多人遇到SPI读写MCP2515没有任何反应第一反应是SPI协议配置错了。实际调试下来我发现“读寄存器一直返回0xFF或者0x00”这类问题根因经常在初始化或者接线层面。先用逻辑分析仪拉一下CS、SCK、MOSI、MISO四根线看看SPI命令有没有完整发出。只要波形正常问题基本就锁定在MCP2515没工作。MCP2515没工作的常见原因包括供电引脚接错晶振没起振。用示波器量一下MCP2515的CLKOUT引脚如果能正常输出时钟说明芯片内部工作正常。CLKOUT没有输出或者频率不对大概率晶振问题检查晶振负载电容是否匹配以及焊盘有没有虚焊。还有一种情况是软件片选时序不对。MCP2515要求CS低电平期间完成完整的命令帧命令结束后CS拉高。有些人用HAL库时HAL_SPI_TransmitReceive返回之前CS已经拉高导致命令被截断。解决办法是先拉低CS完成所有字节传输确认SPI状态机空闲后再拉高CS。4.2 CAN节点进bus-off怎么恢复才安全bus-off是CAN控制器在发送错误计数器TEC超过255后进入的状态。进入bus-off后控制器会主动断开总线这时候这个节点既不发送也不接收整条总线如果只剩这个节点线上直接处于空闲状态其他设备可能还会报错。想强制恢复bus-off可以软件给MCP2515发RESET命令或者通过配置CANCTRL寄存器让芯片重新初始化。但要注意如果导致bus-off的根因还在比如终端电阻接触不良、波特率不匹配恢复之后很快就会再次进入bus-off。恢复之前先排查物理层不是无脑复位。还有一种常见的误杀操作多个节点同时上电此时总线上还没有稳定的同步有的控制器会误判错误并进入bus-off。解决办法是软件初始化CAN控制器后延时几十毫秒再开始发送报文给总线上其他节点足够的同步时间。4.3 地偏移测试怎么做才可靠前面提到地偏移问题这里给一个可复现的测试步骤。先把所有节点正常供电但不启动CAN通信。用万用表直流电压档测量节点A的地和节点B的地之间的电压记为Vdc。再用交流毫伏档测量同一个位置记为Vac。如果Vdc绝对值大于1V说明两个节点之间地线压差已经很大传输距离稍长就会出现收发器共模超限。如果Vac有明显幅值说明地环路中串入了高频干扰最常见的是接入了电机驱动、开关电源这类设备。最直接的解决办法是用隔离CAN收发器比如ISO1050或者带隔离电源的CAN收发模块。隔离之后两边地完全断开地偏移问题从物理上消失。现场急修时也可以临时用共地线把各个节点地连起来但这不是长久之计共地线路径长一点又会引入新的干扰。4.4 仲裁导致丢帧的误判CAN总线仲裁机制是按照报文ID的优先级的。多个节点同时发送时ID小显性位优先级高的报文会赢得仲裁ID大的节点会自动转为接收状态下一轮再重发。这个机制本身不会丢帧但如果你在上位机或者MCU端没有做发送完成确认很容易误以为帧丢了。我第一次做多路CAN时用SPI扩展的两路CAN同时发数据一路ID是0x100另一路ID是0x200逻辑上应该都发出去。调试时发现ID小的那一路总是先出现ID大的经常“丢”。后来读MCP2515的发送错误计数器和状态寄存器才发现ID大的那路一直在等待重发并不是丢了只是被仲裁延后了。排查仲裁问题的最佳工具是CAN分析仪。抓一段总线波形把报文ID和时间戳展开能看到仲裁和重发的全过程。如果只是简单统计收到的帧数很容易把仲裁延时当成故障。4.5 更多排查技巧整理现象优先排查方向可能原因所有CAN节点都不通总线上是否有终端电阻两端120欧姆未接或断线单帧偶发错误地线、屏蔽层地偏移、干扰耦合某个节点发送失败该节点的供电和晶振晶振频率偏差、供电纹波大程序卡死在中断中断标志未清除读接收缓冲区后未清标志SPI读写部分寄存器有问题片选时序CS拉高过早或过晚在多路CAN中某一路不稳定该路CS引脚复用冲突引脚被其他外设占用5. 最后再分享几个我后来觉得特别有用的经验第一条是关于寄存器的初始化备份。MCP2515的寄存器配置项比较多移植时会反复改波特率和过滤配置。建议把每个通道的CNF1、CNF2、CNF3和验收过滤寄存器做一份配置结构体程序启动时统一写入。这样换板子现场调参数时只需要改一个配置数组不需要翻代码里散落的魔数。第二条是DMA和半双工SPI的配合方式。如果你用的是STM32的SPI半双工模式接收和发送共用一根数据线DMA的触发逻辑会和全双工不一样编程复杂度会上升。我这几年做过的SPI扩展CAN方案里绝大多数都建议用全双工模式哪怕只用到其中一路收发。半双工节省一个引脚但对驱动开发要求更高不建议没有特别理由时去省这个引脚。最后一条是关于MCP2517FD这类CAN FD控制器的提醒。CAN FD的数据段波特率远高于标称波特率对SPI读写速度和DMA时序要求更高不能直接套用MCP2515的老驱动。我自己的习惯是只要项目里明确要求CAN FD选型时直接从支持CAN FD的芯片开始不回头考虑老芯片如果只是普通CANMCP2515经过多年大规模验证依然是很稳的选择。回到开头那个问题一片只有单路CAN的STM32F103通过SPI挂两片MCP2515实际跑起来收发都很稳定。硬件上多花几块钱软件上多写一点驱动却换来了后续扩展的可能性。这套方案适合快速出样也适合中低速率的多路CAN隔离场景。如果你正在纠结板子CAN口不够用不妨按这个思路搭一版试试调通之后你会觉得原来多路CAN也没那么复杂。
返回列表