简介本资源是一套基于STM32F103与MCP2515协同实现CAN总线通信的完整嵌入式开发工程面向嵌入式初学者、CAN协议实践者及工业通信项目开发者解决STM32通过SPI驱动MCP2515进行稳定收发的核心技术难点。压缩包共193个文件含30个C源文件实现SPI初始化、MCP2515寄存器配置、CAN帧发送/接收、中断处理等核心逻辑、33个头文件定义寄存器映射、CAN结构体及API接口、31个编译中间文件.o/.d/.crf及Keil MDK工程文件.uvprojx/.axf/.hex等完整保留可直接编译运行的调试环境包体大小为4.95MB。已有1765人学习下载工程结构清晰包含标准外设库驱动如stm32f10x_rcc.c、stm32f10x_spi.c与MCP2515专用驱动层附带波特率配置、验收滤波器设置、错误状态监控等关键实现可快速移植至同类CAN应用场景。 自己做嵌入式这几年经常遇到一种情况项目需要挂CAN总线但手上这颗STM32没有CAN外设或者一路CAN根本不够用。这个时候外挂一颗MCP2515就是最省心的方案。最近我刚好完成了一个基于STM32和MCP2515的CAN收发数据程序全程用C/C实现从寄存器配置到发送接收再到扩展帧滤波都调通了。这篇文章把整个实现过程复盘一遍重点讲MCP2515的初始化、收发函数怎么写、波特率怎么配、扩展帧滤波怎么设以及实际调试中容易踩到的一堆坑。计划接触CAN通信的嵌入式开发者尤其是那些想用STM32加外置CAN控制器做数据收发的朋友可以直接拿这份代码作为参考。1. 整体设计与方案选型1.1 为什么用MCP2515而不是STM32内置CAN很多人第一反应是STM32本身自带CAN控制器为什么还要外挂MCP2515这种想法本身没错但实际项目里会遇到几个情况。第一选型时为了成本或供货选了不带CAN外设的型号比如STM32F103系列中的某些小封装型号或者从F0/F1换到更低端的资源时CAN直接被砍掉了。第二一个节点要同时控制多个CAN网络比如整车上的动力CAN和车身CAN是分开的单路CAN控制器就不够用。第三有些用户模块已经内置了MCP2515比如各种CAN扩展板、USB-CAN适配器MCU只需要通过SPI操作它逻辑反而简单。MCP2515本身是一个独立的CAN控制器支持标准帧、扩展帧最大速率1Mbps集成了发送缓冲、接收缓冲、验收滤波、错误检测等完整CAN协议功能。MCU只需要通过SPI接口跟它通信把要发送的数据写到它内部的缓冲区剩下的CAN协议处理全部由MCP2515完成。对STM32来说相当于多扩展出了一路CAN接口。我用STM32的原因也纯粹是手边开发板多SPI外设也方便其实只要带SPI的单片机都能这么干。整体软件架构上底层是SPI驱动中间是MCP2515寄存器读写上层是CAN报文收发接口。如果要做产品化还可以用C再包一层把每个CAN节点当成一个类来管理。1.2 硬件连接与SPI接线MCP2515通过SPI接收指令所以硬件连接其实很固定。以STM32F103为例我用的是SPI1引脚分配如下SPI1_SCKPA5接MCP2515的SCKSPI1_MOSIPA7接MCP2515的SISPI1_MISOPA6接MCP2515的SOSPI1_CSPA4接MCP2515的CSINTPB0接MCP2515的INT理论上可以省但建议保留MCP2515还需要外接晶振一般都是8MHz这很关键后面波特率配置直接受晶振频率影响。供电方面MCP2515可以3.3V也可以5V但MCU和MCP2515之间要注意电平匹配。STM32F103如果工作在3.3VMCP2515也选3.3V供电两边电平就一致。如果MCP2515用5V供电SPI引脚再接回3.3V的STM32就需要电平转换否则存在烧引脚的风险。还有一个容易被忽略的点CAN总线两端要各接一个120Ω终端电阻。对调试来说如果只是两块板对接至少要在其中一块板子的CANH和CANL之间接上这个电阻否则CAN信号反射严重经常出现时好时坏的现象。我一开始偷懒没接结果收发成功率只有一半后来接上就好了这个坑必须写出来。1.3 软件框架选择标准库、HAL库还是CMCP2515驱动本身不依赖STM32库只要你SPI收发函数能正常跑就行。我这次用了STM32CubeMX生成的HAL库原因很简单生成工程快SPI初始化、GPIO配置都省事。如果你是标准库党原理完全一样只要把HAL_SPI_Transmit和HAL_SPI_TransmitReceive替换成标准库里的SPI_I2S_SendData和SPI_I2S_ReceiveData就行。关于C/CMCP2515的驱动代码我所有底层函数都用C写没有做C类和虚函数主要是为了保持代码体积小、可移植性好。但上层业务逻辑用了C风格比如把CAN报文抽象成一个结构体用类似类的静态方法组织接口这样在工程里既能直接调用也不会被编译器找麻烦。如果你的工具链支持C可以把MCP2515封装成一个类把初始化、发送、接收都变成成员函数管理多个CAN节点会方便很多。2. MCP2515核心机制与关键寄存器解析2.1 内部结构、缓冲器与报文存储MCP2515内部结构可以理解成一个独立的CAN协议芯片加上SPI从站接口。MCU通过SPI往它内部寄存器写数据它自动把数据打包成CAN帧发到总线上收到总线上的报文后它会把数据存进接收缓冲器再通过中断引脚或状态寄存器通知MCU来取。它内部有3个发送缓冲器分别叫TXB0、TXB1、TXB2每个缓冲器都有独立的控制寄存器、ID寄存器、数据长度寄存器和8字节数据空间。好处是可以提前把多帧报文填进去再分别请求发送适合做批量发送。接收侧有2个接收缓冲器RXB0和RXB1。RXB0优先级高RXB1可以配置成当RXB0满时自动接收溢出的报文两个缓冲器共同配合可以减少丢帧。理解缓冲器结构很重要。发送的流程不是直接把数据发给MCP2515就完事而是先写进TXBn的8字节区域再置位发送请求位MCP2515才会把整帧数据发出去。接收也一样收到报文后数据不会直接出现在MCU面前而是存进RXBnMCU要主动去读。MCP2515还有一个错误状态寄存器可以读取发送错误计数和接收错误计数对排查总线故障非常有用。实际调试时我经常先读这个寄存器如果错误计数一直在涨基本就是硬件连接或波特率配置有问题而不是代码逻辑问题。2.2 SPI指令集与基础读写流程MCP2515的SPI指令集并不复杂常用的就几个0x02写寄存器后面跟寄存器地址和要写入的数据0x03读寄存器后面跟寄存器地址再发一个哑字节读出数据0x05读状态寄存器用于快速查询发送请求位、接收缓冲满标志等0x81请求发送TXB0缓冲器0xC0复位芯片0xB0对寄存器做位修改传入地址、掩码、新值所以底层的核心就是三个函数读寄存器、写寄存器、位修改。以HAL库为例读寄存器可以这样写uint8_t mcp2515_read_reg(uint8_t addr) { uint8_t tx[2] {0x03, addr}; uint8_t rx[2] {0x00, 0x00}; MCP2515_CS_Low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); MCP2515_CS_High(); return rx[1]; }这里的关键在于SPI的特性发送读指令和地址之后主机需要再发送一个哑字节才能把MCP2515返回的数据读出来。HAL_SPI_TransmitReceive在第二字节发送哑字节的同时会把MISO线上的数据存到rx[1]所以最后返回的是rx[1]。写寄存器简单一些void mcp2515_write_reg(uint8_t addr, uint8_t val) { uint8_t tx[2] {0x02, addr}; MCP2515_CS_Low(); HAL_SPI_Transmit(hspi1, tx, 2, 10); HAL_SPI_Transmit(hspi1, val, 1, 10); MCP2515_CS_High(); }在写任何寄存器之前MCP2515的CS引脚必须拉低一个完整的SPI事务结束后再拉高。CS时序如果不对数据会随机错位我遇到过几次都是因为CS在操作中途被其他中断打断导致多了一个时钟周期。位修改函数也很有用尤其适合修改CANINTF、CANCTRL这种状态位可以做到只改某个bit而不影响其他bitvoid mcp2515_modify_bits(uint8_t addr, uint8_t mask, uint8_t val) { uint8_t tx[4] {0xB0, addr, mask, val}; MCP2515_CS_Low(); HAL_SPI_Transmit(hspi1, tx, 4, 10); MCP2515_CS_High(); }2.3 波特率配置与计算过程MCP2515的波特率不是像STM32内部CAN那样直接用寄存器算预分频而是依靠三个寄存器来配置CNF1、CNF2、CNF3。这三个寄存器组合决定了一个CAN位的长度、采样点位置以及波特率。关于计算原理MCP2515内部有一个位时序发生器每个位时间被分成多个时间份额TQ。TQ的大小由晶振频率和BRP预分频决定公式可以简单理解为TQ 2 * (BRP 1) / Fosc比如MCP2515外接8MHz晶振BRP设为0那么TQ就是2us? 这里要注意不是2us是0.25us。2 / 8MHz 0.25us也就是250ns。如果目标波特率是500kbps每个CAN位时间是2us那么一个位周期需要8个TQ。接下来的任务就是把8个TQ分配到同步段、传播段、相位缓冲段1和相位缓冲段2上。实际项目中我一般不会手动去推导每一个寄存器值因为MCP2515的数据手册里给了很多常用配置表。我把自己实测过的几组配置贴出来晶振为8MHz时目标波特率CNF1CNF2CNF3实测误差500kbps0x000x900x82正常250kbps0x010x900x82正常125kbps0x030x900x82正常100kbps0x040x900x82正常这段配置适合大部分常规应用。你拿到代码后如果板子上的晶振是16MHz就需要换一组值。千万不要拿8MHz的配置直接去跑16MHz的板子否则波特率会差一倍总线怎么都通不了。初始化流程中一定要先把MCP2515切换到配置模式然后才能修改CNF1、CNF2、CNF3。在正常模式下写这三个寄存器是无效的甚至有可能会触发配置错误。切换模式的代码在后面会一起给出。2.4 滤波与掩码机制MCP2515的滤波机制很多人搞不懂我尽量用大白话解释。MCP2515有2个掩码寄存器RXM0和RXM1分别作用于RXB0和RXB1。掩码决定你要检查报文的哪些位。掩码某一位为1表示这一位必须匹配为0表示这一位不关心。此外还有6个滤波器RXF0到RXF5RXB0可以关联RXF0和RXF1RXB1可以关联RXF2到RXF5。滤波器写出你想接收的ID值。举个例子我想接收一个CAN ID为0x123的报文掩码设置为0xFFFFFFFF所有位都要匹配滤波器设置为0x00000123那么只有ID为0x123的帧才能进接收缓冲器。如果我想同时接收0x123和0x124可以把掩码设为0xFFFFFFFE后一位不关心滤波器设为0x00000122这样0x122和0x123都会被接收。注意这里的ID和掩码都是29位还是11位取决于你接收的是扩展帧还是标准帧。滤波配置其实是在配置模式做的事情之一。实际项目中如果只做收发测试完全可以不启用滤波让所有报文都进缓冲器。需要过滤大量报文时再认真配置掩码不然一个不精确的过滤器会吞掉你真正想收的帧而且非常难查出来。3. C/C收发程序设计与实现3.1 SPI初始化与引脚配置我用STM32CubeMX生成工程SPI1配置为全双工主机模式CPOL设为LowCPHA设为1Edge也就是SPI模式0。MCP2515对SPI模式有严格要求必须是模式0如果配置成模式3芯片能读到数据但经常会出现错位。这个我踩过坑第一条就要确认。SPI时钟频率我设的是4MHz保守一点。MCP2515最大支持10MHz SPI时钟但STM32的SPI分频后不一定刚好是整数而且线路稍长时高频容易出错。对500kbps的CAN通信来说SPI 4MHz完全够用。CS引脚和INT引脚单独配置成普通GPIOCS配置为推挽输出INT配置为输入。如果要用中断方式接收INT引脚要选择外部中断EXTI模式。我先用轮询方式把逻辑跑通再本文还有配套的精品资源点击获取