51单片机驱动MCP2515:SPI时序与CAN通信实战
2026/9/11 21:42:23 网站建设 项目流程

简介:面向51单片机的MCP2515驱动代码包,专为需要在嵌入式系统中实现CAN总线通信的开发者设计,覆盖SPI接口配置、报文滤波、收发中断处理等关键环节,压缩包可直接导入Keil工程编译调试。包内共26个文件,既有main.c、CAN.h、SPI.h、2515.h等源码与头文件,也包含STARTUP.A51启动代码、编译产物hex、obj、lst、m51文件,以及uvproj/uvopt工程配置和bak备份文件,整体仅66KB,文件脉络清晰,便于从源码到烧录全流程追踪。目前已有204人学习下载,适合正在学习SPI协议、CAN总线协议或51单片机外设驱动的电子工程师与嵌入式爱好者。资源保留了完整工程结构和多类备份,读者可对照代码梳理MCP2515初始化、SPI读写、CAN报文收发与错误处理的具体实现,并利用hex文件直接验证效果,或基于源码快速移植到自主项目,是理解51驱动CAN控制器的实用参考。

1. 用 51 单片机驱动 MCP2515,核心不在 CAN 而在 SPI

不少人在 51 平台上第一次接触 CAN 总线时,最先看到的不是协议帧格式,而是 MCP2515 这颗独立 CAN 控制器。51 系列本身没有 CAN 外设,MCP2515 的角色是把繁琐的位定时、仲裁、错误处理全部接管,51 只需要通过 SPI 读写寄存器。于是整个“51 驱动 mcp2515”的问题,本质被拆成了两部分:先把 SPI 时序吃透,再把 MCP2515 的寄存器状态机搞清楚。这篇文章按硬件连接、驱动代码、参数调试、中断化改造的顺序展开,适合正在用 Keil5 做 STC89C52、AT89S51 等经典 51 单片机,又不想换平台、直接外挂 CAN 控制器的开发者和学生。理解了 CANCTRL、TXB0、RXB0 这几个关键寄存器,收发一帧数据就不难了。

2. 51 与 MCP2515 的硬件连接与 SPI 时序

2.1 MCP2515 的最小系统与引脚对照表

MCP2515 是一颗独立的 CAN 控制器,内部带 CAN 协议引擎、3 个发送缓冲器和 2 个接收缓冲器。它对外提供两套接口:一侧是 SPI 主机(也就是 51),另一侧是 CAN 收发器(比如 TJA1050、PCA82C250)。51 要做的只是通过 SPI 配置寄存器、填充发送缓冲、读取接收缓冲,剩下的事情由 MCP2515 自己完成。

MCP2515 引脚51 引脚说明
CSP1.0片选,低有效,每次 SPI 操作前拉低
SCKP1.1SPI 时钟,由 51 产生
SIP1.2主出从入,51 向 MCP2515 写数据
SOP1.3主入从出,MCP2515 向 51 返回数据
INTP3.2低有效中断输出,可接 51 外部中断 0
CLKOUT悬空或接示波器输出晶振二分频,调试用非常方便

这里需要特别提醒晶体电路。MCP2515 的 OSC1、OSC2 之间必须接一颗晶振,常见值 8MHz 或 16MHz,两个引脚分别对地接 15~30pF 电容。晶振频率直接决定后面波特率的 TQ 计算,如果选了一个非标频率,波特率误差会很难压下来。CLKOUT 引脚默认输出 Fosc/2,上电后可以用示波器或逻辑分析仪先看这个信号,确认 MCP2515 内部时钟已经跑起来,再去调 SPI 驱动。

电平兼容性方面,MCP2515 支持 5V 供电,SI、SO、SCK、CS 可以和 5V 的 51 直接连接。CAN 总线侧的 TXCAN、RXCAN 则需要连接 5V 供电的 CAN 收发器,收发器再接到总线。共地是必须的,否则 SPI 偶发读回 0xFF,第一个排查点就应该是地线。

2.2 51 模拟 SPI:模式 0 下的读写时序

传统 AT89S51 没有硬件 SPI,STC12、STC8 系列虽然有硬件 SPI,但不少工程为了引脚自由仍然选择 GPIO 模拟。MCP2515 支持 SPI 模式 0,0 和 1,1,最常见、也最容易写对的是模式 0。

// spi_gpio.c 51 模拟 SPI 主机,工作在 CPOL=0, CPHA=0 sbit CAN_CS = P1^0; sbit CAN_SCK = P1^1; sbit CAN_SI = P1^2; // 接 MCP2515 SI,写数据 sbit CAN_SO = P1^3; // 接 MCP2515 SO,读数据 void SPI_Init(void) { CAN_CS = 1; CAN_SCK = 0; CAN_SI = 1; } // 交换一个字节:写 8 位,同时读回 8 位 unsigned char SPI_Exchange(unsigned char dat) { unsigned char i, r = 0; for (i = 0; i < 8; i++) { CAN_SI = (dat & 0x80) ? 1 : 0; // 先放数据 CAN_SCK = 1; // 上升沿,MCP2515 锁存 SI dat <<= 1; r <<= 1; // 在 SCK 高电平期间采 SO if (CAN_SO) r |= 0x01; CAN_SCK = 0; // 拉低,准备下一位 } return r; }

模式 0 的特点是 SCK 空闲为低,主机在上升沿输出数据、下降沿采集输入。上面代码中,先把 SI 置成目标电平,再把 SCK 拉高,MCP2515 在上升沿采样;随后读取 SO,此时 MCP2515 已经在下沿之后把数据准备好,所以读到的是上一位对应的输出。这里最常犯的错是先把 SCK 拉高再改 SI,导致数据错位;另一个是读 SO 时机太早,读到的还是上一拍的旧值。

如果使用 STC 系列带硬件 SPI 的芯片,原则上也可以直接用 SPI 模块,但要注意 STC 硬件 SPI 的 CPOL、CPHA 配置位必须严格设为 0、0,否则 MCP2515 收到的是乱码。用 GPIO 模拟的优势是完全不挑引脚,缺点是在主频低的时候速度上不去,但 CAN 应用的报文频率通常不高,这个瓶颈并不明显。

2.3 复位时序与晶振选择,初始化前的硬件底气

MCP2515 的 RESET 引脚可以接一个 RC 上电复位电路,也可以干脆不接。实际驱动初始化时,会通过 SPI 发送复位指令让控制器进入复位状态,这是比引脚复位更可靠的方式,因为软件复位可以等 VDD 稳定后再执行,避免一上电就访问到未就绪的内部寄存器。

常见的坑是上电后立刻访问 MCP2515。51 的复位时间通常在几十毫秒,但 MCP2515 内部时钟起振、SPI 逻辑就绪需要额外时间。我一般会在主函数里加一个 10ms 左右的延时,再执行初始化。初始化第一步就是发 0xC0 复位指令,之后读 CANSTAT 寄存器。如果读回 0x80,说明已进入配置模式,这一步过了,SPI 通路基本就通了。

如果 CLKOUT 看不到方波,优先检查晶振两个引脚上的电容是否漏焊、晶振是否起振。CLKOUT 正常输出后,再量 CS、SCK 是否都保持高电平/低电平的合理静态值。MCP2515 的 SO 在 CS 为高时输出高阻,如果在初始化前读到 SO 一直是高,不代表异常。硬件全部就绪后,才轮到寄存器层面的驱动代码。

3. MCP2515 驱动代码:初始化、单帧发送与接收的最小实现

3.1 SPI 指令与内部寄存器映射:不背手册也能操作

MCP2515 的全部功能都通过一组 SPI 指令封装,写驱动代码前先把下面这张表放在手边。

指令字节作用
RESET0xC0复位芯片,回到配置模式
READ0x03读指定寄存器的值
WRITE0x02写指定寄存器
BIT_MODIFY0x05对寄存器做位修改,适合改控制位
READ_STATUS0xA0一次读出多个状态位
RTS0x80请求发送,可带缓冲器编号

代码里最常见的寄存器是 CANSTAT(0x0E)和 CANCTRL(0x0F),初始化模式切换靠这两个;发送相关的 TXB0CTRL(0x30)、TXB0SIDH(0x31)、TXB0DLC(0x35);接收相关的 RXB0CTRL(0x60)、RXB0SIDH(0x61)、RXB0DLC(0x65)。数据段从 TXB0DATA(0x36)和 RXB0DATA(0x66)开始,各占 8 字节。

所有指令的时序都一样:CS 拉低,发送指令字节,再发送地址或数据,最后 CS 拉高。多个连续字节可以在一次 CS 低电平期间完成。写 READ 指令时会同时收到 SO 上的字节,因此读寄存器实际上也是先写再读的“交换”过程。

3.2 初始化流程:配置模式里完成波特率与滤波器

初始化是所有问题的高发区,顺序没走对,后面一切都是白搭。MCP2515 上电后默认进入配置模式,但经过软件复位之后,必须再次确认当前模式,再修改 CNF1、CNF2、CNF3 这些波特率寄存器。配置模式之外写入这些寄存器是不生效的。

#define CANCTRL 0x0F #define CANSTAT 0x0E #define CNF1 0x2A #define CNF2 0x29 #define CNF3 0x28 #define CANINTE 0x2B #define CANINTF 0x2C #define RXB0CTRL 0x60 // 发送一个 SPI 复位指令 void MCP2515_Reset_Soft(void) { CAN_CS = 0; SPI_Exchange(0xC0); CAN_CS = 1; } void MCP2515_ModifyByte(unsigned char addr, unsigned char mask, unsigned char data) { CAN_CS = 0; SPI_Exchange(0x05); // BIT_MODIFY SPI_Exchange(addr); SPI_Exchange(mask); // 只修改 mask 对应的位 SPI_Exchange(data); CAN_CS = 1; } unsigned char MCP2515_ReadReg(unsigned char addr) { unsigned char val; CAN_CS = 0; SPI_Exchange(0x03); // READ SPI_Exchange(addr); val = SPI_Exchange(0xFF); CAN_CS = 1; return val; } void MCP2515_Init(void) { unsigned char wait = 0; MCP2515_Reset_Soft(); delay_ms(5); // 请求进入配置模式:CANCTRL 高三位 REQOP = 100 MCP2515_ModifyByte(CANCTRL, 0xE0, 0x80); // 等待状态切换完成,防止在非配置模式下写 CNF 寄存器 while (((MCP2515_ReadReg(CANSTAT)) & 0xE0) != 0x80) { if (++wait > 200) break; } // 波特率相关,取值见第 4 章计算 MCP2515_WriteReg(CNF1, 0x03); // BRP=3, SJW=1TQ MCP2515_WriteReg(CNF2, 0xB9); // PHSEG1=3TQ, PROPSEG=2TQ MCP2515_WriteReg(CNF3, 0x81); // PHSEG2=2TQ // 关闭验收滤波,接收所有报文,bit6:5 = 11 MCP2515_WriteReg(RXB0CTRL, 0x60); // 使能 RX0 中断,发送中断先不开 MCP2515_WriteReg(CANINTE, 0x01); // 退出配置模式:REQOP = 000,进入正常工作模式 MCP2515_ModifyByte(CANCTRL, 0xE0, 0x00); }

这里 wait 变量防止极端的硬件故障导致初始化死循环,实际开发里如果停在 while 里出不来,多半是 SPI 通路没通,优先查时钟和接线。RXB0CTRL 写 0x60 是把两个验收滤波位 RXM 都置 1,表示不过滤,适合初期联调。后面要收特定 ID 时再改这一处。

3.3 发送一帧:从 TXREQ 到中断标志

发送一个标准帧,核心是往 TXB0 缓冲器填 ID、长度和数据,然后置 TXREQ 位请求发送。发送完成的标志是 TXB0CTRL 的 TXREQ 由硬件自动清零。

#define TXB0CTRL 0x30 #define TXB0SIDH 0x31 #define TXB0SIDL 0x32 #define TXB0DLC 0x35 #define TXB0DATA 0x36 unsigned char MCP2515_SendFrame(unsigned int id, unsigned char len, unsigned char *buf) { unsigned char i, st; if (len > 8) len = 8; // CAN 单帧最多 8 字节 st = MCP2515_ReadReg(TXB0CTRL); if (st & 0x08) return 0; // 上一帧还没发完,不能覆盖 // 标准帧 11 位 ID:高 8 位放 SIDH,低 3 位放 SIDL 的高 3 位 MCP2515_WriteReg(TXB0SIDH, (id >> 3) & 0xFF); MCP2515_WriteReg(TXB0SIDL, (id & 0x07) << 5); MCP2515_WriteReg(TXB0DLC, len & 0x0F); for (i = 0; i < len; i++) { MCP2515_WriteReg(TXB0DATA + i, buf[i]); } // 置 TXREQ = 1,请求发送 MCP2515_ModifyByte(TXB0CTRL, 0x08, 0x08); // 等待发送完成,硬件清 TXREQ while (MCP2515_ReadReg(TXB0CTRL) & 0x08); // 清发送完成中断标志 TX0IF MCP2515_ModifyByte(CANINTF, 0x04, 0x00); return 1; }

上面的代码只用了 TXB0,实际使用中往往三个发送缓冲器轮换,避免连续发送时互相等待。发送前检查 TXREQ 而不是检查中断标志,是为了避免在上一帧还挂在总线上的时候覆盖缓冲。如果系统要求非阻塞发送,可以把 while 改成查询次数限制,超时返回未发送完成,这样就不会卡住主循环。TX0IF 清标志用的是 0x04,因为 CANINTF 寄存器的 bit2 对应 TX0 中断,这个位一定要用 BIT_MODIFY 单独清,不能直接对整个寄存器赋值,否则会误清掉接收中断。

3.4 接收一帧:轮询 RX0IF 还是中断

接收有两种实现,最简单的是在主循环里轮询 CANINTF 的 RX0IF 位。收到帧后,从 RXB0SIDH、RXB0SIDL 拼出 ID,从 RXB0DLC 取长度,再读 8 字节数据,最后清 RX0IF。

#define RXB0SIDH 0x61 #define RXB0SIDL 0x62 #define RXB0DLC 0x65 #define RXB0DATA 0x66 // 返回 1 表示收到新帧,0 表示无新帧 unsigned char MCP2515_ReceiveFrame(unsigned int *id, unsigned char *buf, unsigned char *len) { unsigned char i; unsigned char st = MCP2515_ReadReg(CANINTF); if (!(st & 0x01)) return 0; // RX0IF 未置位 *id = MCP2515_ReadReg(RXB0SIDH) << 3; *id |= MCP2515_ReadReg(RXB0SIDL) >> 5; *len = MCP2515_ReadReg(RXB0DLC) & 0x0F; for (i = 0; i < *len; i++) { buf[i] = MCP2515_ReadReg(RXB0DATA + i); } // 清 RX0IF MCP2515_ModifyByte(CANINTF, 0x01, 0x00); return 1; }

读 RXB0SIDL 时只取了高 3 位,这是因为标准帧 ID 的低 3 位存放在这个寄存器的 bit7:5,bit4 是扩展帧标志位。如果以后要支持扩展帧,这里要按位判断 EXIDE,再决定是否读 EID 字段。轮询方式在报文不多的时候完全够用,但主循环里如果有长耗时操作,比如按键消抖、显示刷新,就可能丢帧。解决方式是改用外部中断,第 5 章会专门展开。

4. 实战调试:波特率计算、验收滤波与信号定位

4.1 波特率的三段公式:把 TQ 分配写进代码

MCP2515 的波特率不是直接写一个数值,而是由 BRP、PRSEG、PHSEG1、PHSEG2 共同决定。先算出一个 TQ 的时间,再算一个位占多少 TQ。

TQ 时间公式:TQ = 2 × (BRP + 1) / Fosc

位时间由四段组成:同步段固定 1 TQ,传播段为 PRSEG 寄存器值加 1,相位段 1 和相位段 2 同理。所以总位时间 TQ 数为:

位时间 TQ 数 = 1 + (PRSEG + 1) + (PHSEG1 + 1) + (PHSEG2 + 1)

以 16MHz 晶振、500kbps 为例:位时间 2us,TQ 需要 16 个,TQ 就是 0.125us。代入公式得到 BRP=0。四段分配取同步 1、传播 5、相位段 1 为 5、相位段 2 为 5,就是 PRSEG=4、PHSEG1=4、PHSEG2=4。采样点落在 (1 + 5 + 5) / 16 = 68.75%。

另一个常见组合是 125kbps,这种速率总线上能挂更多节点。位时间 8us,如果选 BRP=3,TQ 就是 1us,总 TQ 数为 8。分配为同步 1、传播 3、相位段 1 为 2、相位段 2 为 2,采样点 75%。

目标波特率FoscBRPPRSEGPHSEG1PHSEG2采样点
500kbps16MHz044468.75%
125kbps16MHz321175%
250kbps8MHz121175%

调试时最怕的是两个节点各自都能跑,一接总线就报错。用示波器量 CAN_H、CAN_L 的差分波形,常规配置下静止电平约 2.5V,显性位约 2.0V 压差。如果波形有回沟、边沿圆滑,优先把 BRP 加大、总 TQ 数提高,让每一位更“宽”,牺牲一点波特率换取稳定性。

4.2 收发双方都“通”但收不到:先查验收滤波器

初始化时 RXB0CTRL 写 0x60,已经把 RXM 置为 11,关闭了验收滤波,这是最宽松的模式。如果改成自定义过滤,就要注意:MCP2515 的接收缓冲器每个都绑定一组验收滤波寄存器和一组掩码寄存器。RXB0 只能使用 RXF0、RXF1,默认情况下 RXB1 使用 RXF2 到 RXF5。

寄存器地址作用
RXF0SIDH0x00验收滤波 0 的 ID 高 8 位
RXF0SIDL0x01验收滤波 0 的 ID 低 3 位
RXM0SIDH0x20验收掩码 0 高 8 位
RXM0SIDL0x21验收掩码 0 低 3 位

掩码位为 0 表示不关心这一位,为 1 表示必须与滤波寄存器一致。最常见的坑是:掩码设置成 0x00,期望“全部接收”,但 RXM 位却是 00 或 01,导致总线上的广播帧被内部过滤掉。正确做法是:要么 RXM=11 关掉滤波,要么把 RXM 设为 00、掩码全 0、滤波 ID 也全 0,两者效果一致。

联调时还有一个容易忽略的点:MCP2515 的接收缓冲器写满后,如果 RXB0 有帧且 RXB1 也满,新帧会被丢弃。轮询模式下要保证 RX0IF 清得足够快;中断模式下则要保证中断服务程序里尽快搬走数据,否则高速状态下会丢帧。

4.3 静默故障:从 CLKOUT 到逻辑分析仪的定位顺序

CAN 通信完全静默时,按下面的顺序定位,不要一开始就去改波特率。首先量 CLKOUT,确认内部时钟;然后读 CANSTAT,确认处于运行模式;接着用 SPI 读任何已知寄存器,比如 CANCTRL,验证 SPI 读写通路。

用逻辑分析仪抓 SPI 三根线,能直观看到 CS 有没有拉低、SCK 有没有时钟、SI 上的命令字节对不对。如果 SI 波形和代码预期完全一致但 SO 没数据,问题大概率在 MCP2515 侧,重点查晶振和复位。如果 SO 有数据但读回全 0xFF,通常是因为 CS 在字节间隙被拉高,或者 SCK 极性不对。

一个有效的自测方法:把两个 51 + MCP2515 节点用双绞线对接,两端都接 120 欧姆终端电阻,A 节点循环发一帧固定 ID,B 节点收到后回一帧。谁收不到,问题就缩小到单侧。对比例是 Linux 平台上的 RK3128 等处理器,MCP2515 的配置写在设备树 dts 里,比如 spi-max-frequency、oscillator-frequency 这些属性;而 51 平台没有设备树,所有参数都是一行行寄存器赋值,反而更容易追踪。

5. 用 INT0 把接收改成中断驱动,再加一段 16 字节环形队列

轮询接收的瓶颈在“主循环什么时候有空去看标志位”。把 MCP2515 的 INT 引脚接到 51 的外部中断 0,帧到来时硬件自动触发中断,实时性和可靠性都会好很多。这里给出一个可直接改造的中断接收方案。

#define QUEUE_LEN 16 unsigned int can_rx_id[QUEUE_LEN]; unsigned char can_rx_len[QUEUE_LEN]; unsigned char can_rx_buf[QUEUE_LEN][8]; unsigned char rx_head = 0, rx_tail = 0; void CAN_INT0_ISR(void) interrupt 0 { unsigned char cur, i, len; unsigned int id; if (MCP2515_ReadReg(CANINTF) & 0x01) { cur = rx_head; if (!MCP2515_ReceiveFrame(&id, can_rx_buf[cur], &len)) { return; // 异常情况,直接退出 } can_rx_id[cur] = id; can_rx_len[cur] = len; rx_head = (rx_head + 1) % QUEUE_LEN; } MCP2515_ModifyByte(CANINTF, 0x01, 0x00); }

外部中断配置为下降沿触发。MCP2515 的 INT 引脚是低有效开漏输出,帧接收完成会拉低,51 检测到下降沿进入中断,读走数据后清 RX0IF,INT 恢复高电平。这里要严格保证 MCP2515_ReceiveFrame 内部读取的 RXB0 和中断标志一致,所以它在函数开头读 CANINTF,结尾清标志,整个操作在中断里完成才安全。

环形队列的长度 16 意味着在极端情况下,主循环可以延迟处理最多 16 帧而不丢。队列满的时候,新帧会覆盖 rx_head 当前指向的位置,这个策略按业务需求自行调整,一般控制类应用宁可丢新帧也不覆盖还没处理的旧帧。

中断服务程序里做 SPI 操作,要小心主循环也同时在访问 SPI。51 没有多核,但主循环里调用 MCP2515_ReadReg 时,如果恰好被外部中断打断,两次 SPI 事务就会交错。最简单可靠的做法是:SPI 相关的 MCP2515 操作只在主循环执行,或只在中断里执行,不要交叉。如果两边都必须访问,就给每个 SPI 函数加一个“正在传输”标志,或者在进出临界区时关 EA、开 EA。

验证中断接收是否正常,可以定时在主循环里向串口打印队列长度,或者用一个 GPIO 翻转电平,用示波器看中断服务程序占用时间。CAN 帧到达频率高时,MCU 停留在中断里的时间会明显变长,如果发现主循环任务被饿死,就说明队列长度不够,或者需要把 SPI 时钟提上来。51 的 GPIO 模拟 SPI 速度有限,这正是中断方式比轮询方式更容易暴露的边界。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询