ARTICLE DETAIL

资讯详情

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

SPI扩展CAN接口技术:基于STM32与MCP2515的多路设计

SPI扩展CAN接口技术:基于STM32与MCP2515的多路设计 很多人问我SPI扩展CAN到底靠不靠谱尤其是当MCU原生CAN接口不够用的时候。我这两年做车载网关和工业控制板经常要在单片机上堆3路甚至5路CAN最顺手的方案就是“主控MCU的SPI接口 外挂CAN控制器”其中最经典的组合是STM32F103 MCP2515 CAN收发器。这个方案听起来绕了一层但实际做起来稳定、灵活、可裁剪非常适合那些原厂不带多路CAN、或者换主控成本太高的场合。这篇文章我就从硬件选型、电路设计、CubeMX配置、软件驱动到实际踩坑把完整的SPI扩展CAN实现思路拆开讲清楚希望能帮正卡在“MCU的CAN口不够用”这个问题上的朋友打开一条路。1. 多路CAN需求与方案选型1.1 为什么需要多路CAN先说说需求从哪来。新能源汽车的整车控制器一般要同时管理动力CAN、车身CAN、充电CAN、诊断CAN动不动就是3路以上工业设备里PLC要接现场设备总线又要接上位机很多时候也需要2到3路CAN。就算是一些农业机械、工程车辆、医疗设备也经常出现“主控只有一个CAN口但实际系统需要两路甚至更多”的局面。最直接的办法是换一颗带多路CAN的MCU但现在芯片供应链波动很大很多硬件工程师一听到换主控就头疼——启动代码、外设驱动、bootloader全要返工硬件板卡也可能要大改。相比之下外挂SPI转CAN控制器只需要改很少的硬件主控MCU还是原来那颗软件上增加几个外设驱动即可成本和风险都可控这是这个方案能被大量量产项目采用的根本原因。1.2 SPI转CAN控制器的选型市面上常见的SPI转CAN控制器最经典的就是Microchip的MCP2515支持CAN 2.0B标准帧和扩展帧最高速率1Mbps内部有3个发送缓冲器和2个接收缓冲器工作电压3.3V外部需要一颗8MHz晶振然后还要外接CAN收发器。这颗芯片资料多、用的人多、成本低是目前最稳妥的选择。如果你的系统需要CAN FD可以考虑MCP2518FD或者MCP2517FD它们支持CAN FD协议数据段最高速率能到8Mbps甚至更高但SPI时钟和驱动逻辑也复杂一些寄存器结构和MCP2515不兼容。选择时主要看这几点支持的帧格式和速率、SPI时钟上限、FIFO深度、封装尺寸、供货稳定性。有些朋友可能会问STM32F103本身自带CAN外设能不能直接外挂个CAN收发器当然可以但这里讨论的是主控没有CAN外设、或者CAN外设数量不够的情况。SPI转CAN控制器的本质是把CAN协议栈放在独立芯片里主机只负责收发数据协议处理、错误检测、仲裁这些事都由MCP2515完成主控压力很小。1.3 为什么不用内置多路CAN的MCU我们列个对比看看内置多路CAN的MCU和SPI扩展方案各有什么优劣维度内置多路CAN的MCUSPI扩展CAN方案主控成本高多CAN MCU通常定位高可以使用低端主控但加上外围芯片不一定更便宜路数扩展受MCU选型限制灵活4路、8路都能加实时性高硬件自动收发受SPI通信时间影响软件工作量相对简单需要写驱动、调度资源工作量更大硬件复杂度低增加了控制器芯片、晶振、收发器芯片供货风险单芯片可将主控和CAN功能拆开备货更灵活如果每一路CAN都跑满载、而且对接收延迟极其敏感那我建议还是选内置多路CAN的MCU。但大多数应用比如网关转发、BMS数据采集、工业设备通信对延迟要求并没有苛刻到微秒级SPI扩展方案完全够用而且可维护性好、后续换主控也不至于推翻重来。2. 硬件电路设计搭建SPI转CAN的“骨架”2.1 MCU与SPI转CAN控制器的连接我以STM32F103C8T6和MCP2515为例。MCP2515的接口包括SCK、SIMOSI、SOMISO、CS、INT、RESET、TXCAN、RXCAN、OSC1/OSC2。实际接法很简单SCK接STM32的SPI1_SCK比如PA5SI接SPI1_MOSI比如PA7SO接SPI1_MISO比如PA6CS接任意GPIO比如PA4多路时每路各占一个GPIOINT接外部中断引脚比如PA0、PA1、PA2多路时各占一个RESET也建议用GPIO控制比如PB0方便软件复位TXCAN/RXCAN接CAN收发器的TXD/RXDOSC1/OSC2接8MHz晶振加上两个15pF电容多路扩展时所有MCP2515共享SCK、SI、SO这三根线CS和INT各自独立。共享总线有个好处MCU的SPI外设只需要一个成本低但也要注意MISO是三态输出某个芯片CS拉高后其MISO会释放不影响其他设备通信。如果MCU没有USB差分信号引脚那跟这个方案没有直接关系CAN是差分总线没错但MCP2515已经把CAN协议转成SPI了主控完全不必直接接触CAN的物理层差分信号。2.2 硬件片选与软件片选的选择STM32的SPI外设有NSS引脚可以做硬件片选但我强烈建议使用普通GPIO软件控制CS。原因是MCP2515要求整个SPI帧期间CS脚保持低电平而硬件NSS在某些情况下可能提前拉高或者产生不期望的时序排查起来很痛苦。用GPIO控制CS想什么时候拉低就什么时候拉低想拉多低就拉多低逻辑完全由自己掌握。多片MCP2515扩展时每个芯片单独一个CS。切换不同芯片时注意先拉高上一片CS再拉低下一片CS中间至少留几百纳秒间隔。有些芯片在CS上升沿处理数据如果间隔太短容易导致上一片还没完成内部锁存下一片已经开始访问。另外如果使用DMA传输CS必须在整个DMA传输期间保持低电平不能提前拉高。真正的做法是等DMA完成中断到来后再拉高CS而且还要确认SPI的BSY标志已经清除否则可能出现最后一次移位还没完成CS就被拉高的情况导致数据丢失。2.3 CAN收发器、隔离与终端电阻MCP2515内部是CAN控制器输出的是TXCAN和RXCAN逻辑电平还需要一颗CAN收发器转换成总线差分电平。收发器选择很多TJA1050是5V供电输出RXD高电平也是5V直接接MCP2515并不完全推荐更好的选择是3.3V供电的收发器比如TJA1051T/3、SN65HVD230或者带隔离的ISO1050、ADM3053。我实际项目中常用SN65HVD230和MCP2515能直接搭3.3V电平不需要电平转换。如果板子必须用5V供电的TJA1050遇到RXD输出电平偏高的问题最简单的处理是用电阻分压到3.3V或者用方向正的电平转换芯片。我在量产板上吃过亏当时直接拿TJA1050的RXD接MCP2515的RXCAN短距离测试没问题但布线长了之后偶尔报错后来换成3.3V收发器再没出现过。终端电阻不能忘。CAN总线规范要求两端各接一个120欧姆电阻作用是匹配阻抗、反射信号、保证差分电平正确。做板子的时候我会在CAN收发器旁边预留120欧姆的焊盘位很多场合节点间距离短、节点数量少可以先用跳线或者电阻装上现场调试再决定要不要摘掉。注意终端电阻只加在总线两端不是每个节点都加如果每个板子都带120欧整个总线等效电阻过低收发器会过载。隔离设计也很重要。工业环境里不同设备地电位差可能很大总线两端如果地不平衡CANH和CANL之间会出现电位漂移严重时直接导致通信失败。测试CAN地偏移最简单的三个步骤一是用万用表量CANH到CANL的静态电压和差分电压二是分别量CANH、CANL对本地地线的电压观察是否在收发器共模范围内三是加入隔离收发器后再测一次。如果地电位差较大就老老实实上隔离方案。2.4 SPI时钟速度与DMA设计要点MCP2515在3.3V供电下SPI时钟最高建议控制在5MHz以内5V供电可以到10MHz。STM32F103外设时钟为36MHz或72MHzSPI1挂APB2能达到18MHz。我一般使用8分频得到9MHz但为了稳妥常用16分频得到4.5MHz。这个速度下单帧20字节的报文传输时间约35微秒多路CAN完全跑得过来。为什么推荐DMA而不用中断方式收发因为多路CAN场景下主循环经常有协议解析、任务调度如果每收一帧都进中断读写几十个字节CPU占用率很高还会因为中断优先级竞争导致CAN接收FIFO溢出。DMA可以把数据从SPI直接搬到内存传输完成后才触发一次中断主控去做后续业务处理效率高很多。DMA设计有一个细节接收数据时通常先发送读命令和地址再启动DMA接收。CS保持低电平分两步时序没问题只要两次SPI访问之间CS不拉高即可。更好的办法是一次性使用HAL_SPI_TransmitReceive_DMA构造发送缓冲区[读命令, 地址, 0x00, 0x00, ...]同时接收相同长度的数据有效数据从第三个字节开始取。这种方法时序更紧凑DMA只需要一次但要注意有效数据的偏移。3. 软件驱动实现从CubeMX到多路调度3.1 CubeMX配置SPI与DMA用STM32CubeMX生成工程配置SPI1为Full-Duplex Master参数如下Master/Slave: MasterFrame Format: MotorolaData Size: 8 BitsFirst Bit: MSB FirstPrescaler: 16得到4.5MHz具体看时钟树CPOL: LowCPHA: 1 EdgeMCP2515的数据手册写明支持模式0,0也就是CPOL0、CPHA1Edge。有少数驱动用了模式1,1也能通信那是因为时序余量足够但不推荐照抄老老实实按照手册来。DMA配置两步添加SPI1_RX DMA请求Direction: PeripheralToMemoryMode: Normal添加SPI1_TX DMA请求Direction: MemoryToPeripheralMode: NormalData Width都选Byte外部中断配置好INT引脚比如PA0、PA1、PA2设为下降沿触发。CS和RESET引脚配置为GPIO Output初始电平置高。CubeMX生成的代码只负责初始化具体的MCP2515操作需要自己写驱动函数下面我把关键部分展开。3.2 MCP2515寄存器操作基础MCP2515对外提供SPI命令接口核心命令如下命令命令码作用RESET0xC0复位芯片READ0x03从指定地址读取字节WRITE0x02向指定地址写入字节RTS0x80~0x84请求发送缓冲器发送READ STATUS0xA0读取中断状态和发送状态RX STATUS0xB0读取接收过滤器信息读写寄存器最简单的实现void mcp2515_write_reg(uint8_t cs_id, uint8_t addr, uint8_t val) { uint8_t send[3] {0x02, addr, val}; cs_low(cs_id); HAL_SPI_Transmit(hspi1, send, 3, 10); cs_high(cs_id); } uint8_t mcp2515_read_reg(uint8_t cs_id, uint8_t addr) { uint8_t send[2] {0x03, addr}; uint8_t recv 0xFF; cs_low(cs_id); HAL_SPI_Transmit(hspi1, send, 2, 10); HAL_SPI_Receive(hspi1, recv, 1, 10); cs_high(cs_id); return recv; }软件复位后要等芯片稳定再初始化。MCP2515需要进入到配置模式才能修改波特率寄存器。初始化顺序是复位芯片读取CANSTAT确认进入配置模式设置CANCTRL选择正常模式、设置CNF1/CNF2/CNF3设定波特率配置收发缓冲器和中断使能设置验收滤波器和掩码最后退出配置模式。波特率由主时钟8MHz和三个配置寄存器决定。以500kbps为例设BRP0则TQ2*(BRP1)/8MHz250ns一个位时间需要8个TQ可以将CNF1的BRP设为0CNF2的PHSEG1设为2CNF3的PHSEG2设为2这样位时间为11221? 实际MCP2515的位时间结构是SyncSeg(1TQ)PropSegTQPhaseSeg1PhaseSeg2配置前要仔细对照手册。我通常直接使用官方或成熟驱动里的参数表避免手算出错。3.3 收发流程与中断处理MCP2515每路有自己的INT引脚低有效。只要芯片有接收数据、发送完成、错误等事件INT就会拉低触发MCU外部中断。接收流程进入外部中断后先判断是哪一路触发然后向对应芯片发送读取接收缓冲器命令例如读取RXB0使用0x92接着依次读取ID、控制字节、数据长度和数据字段。读完以后要把接收中断标志清掉方法是写CANINTF寄存器将RX0IF对应位置0注意读该寄存器后不能直接写回全部值否则会误清其他中断标志。发送流程更简单。应用层要发送报文时先找到空闲的发送缓冲器把ID、DLC、数据写入对应寄存器然后置位TXREQ发送请求。MCP2515有三个发送缓冲器可以同时缓存3帧适合做小队列。使用RTS命令可以直接触发指定缓冲器发送比如RTS TXB0的命令码是0x81。如果开启了发送完成中断发送完成后MCP2515会拉低INT引脚MCU在中断里读取发送中断标志更新软件队列。在低负载场景也可以不做发送完成中断只做接收中断发送流程直接阻塞等待发送完成但这种方法在多路CAN高负载时容易卡住整个SPI总线不推荐。3.4 多路CAN的资源调度与缓冲管理多路CAN共享一条SPI总线核心问题是并发访问。比如第1路CAN正在通过SPI读接收数据第2路CAN的发送请求来了不能直接抢占SPI否则两边的寄存器访问都会出错。我常用的做法是给SPI驱动加一个互斥锁用全局变量就行static volatile uint8_t spi_lock 0; int spi_acquire(void) { if (spi_lock) return -1; spi_lock 1; return 0; } void spi_release(void) { spi_lock 0; }所有对MCP2515的SPI访问都包在acquire和release之间。在实时性要求高的场景可以关中断保护临界区但关中断时间不能太长否则影响MCU其他定时任务。实测上由于每次SPI事务只有几十微秒用全局锁在多路CAN场景下完全够用。软件上要维护每路CAN的接收环形队列和发送队列。接收中断里只做SPI读数据和写环形队列协议解析在主循环完成。发送队列由应用层填入后台任务定期检查待发状态这样可以避免在中断里处理复杂的业务逻辑。我做过4路CAN网关主控是72MHz4路全部设置为500kbpsSPI通信占用率不到15%主循环还有大量时间做转存和诊断。4. 实践中的坑与排查方法4.1 SPI通信不生效怎么办这是最多人问的问题现象是MCP2515寄存器读出来全是0xFF或者写入后读不到数据。我整理一个排查顺序第一步检查芯片供电和复位。MCP2515是3.3V供电如果接了5V芯片直接烧毁或工作异常。RESET引脚不能用软件复位时至少要接一个10k上拉和一个0.1uF电容到地保证上电后能正常释放复位。第二步检查晶振是否起振。MCP2515没有时钟输出脚用示波器测量OSC1和OSC2引脚可以看到8MHz正弦波没有示波器可以量OSC2引脚对地电压通常约1.5V左右。如果晶振没起来寄存器读写必然失败。第三步检查SPI时序。用逻辑分析仪抓CS、SCK、MOSI、MISO波形确认CPOL和CPHA。常见问题是SPI模式配错MCP2515只认模式0,0。另外读寄存器时MISO数据是从第三个字节位置才开始有效如果直接用第二个字节的数据去判断容易被误导。第四步检查CS时序。我之前遇到过SPI读写函数里先调用HAL_SPI_Transmit紧接着调用HAL_SPI_Receive两者之间CS被意外拉高导致MCP2515认为这是两个独立事务。正确做法是CS在整个读时序中保持低电平。第五步检查IO复用冲突。STM32F103的PA4默认是SPI1_NSS但如果你把CS放在PA4且GPIO配置为AF开漏可能会被SPI外设抢占控制权。GPIO模式必须设置为Output Push Pull不要设置为AF。4.2 CAN总线bus-off的恢复Bus-off是CAN节点发送错误计数器超过256后进入的离线状态。进入bus-off后节点不参与总线通信直到检测到128次11个连续隐性位。实际项目中如果某一路CAN总线短路或者接错线MCP2515反复发送失败最终进入bus-off状态。MCP2515可以通过读取EFLG寄存器查看错误状态。如果判断到bus-off最简单的处理是给芯片发复位命令然后重新初始化。但不要一检测到错误就疯狂复位那会让问题更复杂。我会设计一个状态机正常状态如果连续发送失败N次进入bus-off恢复状态等待总线空闲再重新初始化并恢复发送如果恢复后仍然失败则判断是硬件故障只上报不重复重试。还要注意在某些应用中单路CAN进入bus-off不能影响到其他路。SPI互斥锁要保证复位其中一片芯片时不影响其他芯片正在进行的SPI访问。4.3 仲裁、滤波与终端电阻那些事CAN仲裁机制决定多个节点同时发送时ID小的节点赢得总线。如果你发现某个节点总是发不出去先看ID是否比总线上的其他节点ID大然后用CAN分析仪抓总线空闲率。如果总线被ID很小的节点占满那高ID节点的报文自然会延迟。MCP2515的验收滤波器也是一个容易踩坑的地方。默认情况下如果掩码为全0则所有报文都通过如果设了掩码和滤波器而配置不正确就可能收不到任何报文。调试时先把滤波器全部置为不使能确保能收到数据再逐步配置过滤。终端电阻问题前面讲过这里再说一个现象总线两端都该有120欧但如果你用万用表量CANH和CANL之间的电阻在总线上电后量到的不是60欧常见原因是有节点没有正常接入收发器或者收发器没有供电。终端电阻缺失时传输距离短、速率低可能还能通速率一旦跑到500kbps以上错误帧明显增多。4.4 多路CAN联调经验清单最后把我在多次联调中积累的经验整理成清单照着做能省很多时间上电后先读每路MCP2515的CANSTAT寄存器的OPMOD字段确认是否进入配置模式。先单路调试用USB-CAN分析仪对接确认收发正常不要一上来就把多路同时打开出了问题很难定位。在每路收发器输出端预留跳线或测试点方便断开某一节点排查短路。多路CAN共地要牢靠可以用粗铜箔或覆铜整块连接避免地回路产生压差。尽量把MCP2515的复位控制单独引出软件做统一复位时更方便。逻辑分析仪建议采样率至少10MHz能同时抓CS、SCK、MISO和INT排查时序问题最见效。发送缓冲器要监测TXREQ位防止应用层往同一个缓冲器重复写导致覆盖正在发送的数据。隔离场景下每路隔离电源的地不能共用否则隔离失去意义。多路CAN的接收中断建议设置为可抢占的阈值避免一路CAN报文多时阻塞其他路中断导致接收FIFO溢出。我自己做第一个3路CAN网关时在SPI时钟和CS时序上折腾了两天最后用逻辑分析仪抓波形才发现是CS提前拉高导致的。从那以后所有SPI外设的驱动我都会先写一个简单的寄存器回读测试函数确认最底层通信稳定后再开发业务逻辑。这个SPI扩展CAN方案说到底就是一句话用MCU的SPI接口模拟出N个CAN口把每个MCP2515当成一个独立从设备用CS和INT区分身份。只要理解了片选、SPI时序和DMA配合这三点扩展几路CAN本质上只是多画几路电路、多写几个驱动实例的问题。后续如果系统升级到CAN FD可以参考同样的设计思路把MCP2515换成MCP2518FD主控侧的SPI通信处理逻辑大方向一致只是寄存器操作细节需要重新适配。
返回列表