51单片机驱动MCP2515实现CAN总线:SPI、波特率与收发验证
2026/9/23 19:07:02 网站建设 项目流程

简介:基于51单片机的MCP2515驱动代码包,面向车载电子、工业现场总线等嵌入式开发者,解决通过SPI接口扩展CAN总线收发能力的实际需求。资源包共26个文件、约66KB,包含C源码(main.c、CAN.h、SPI.h等)、Keil工程文件、HEX固件、列表/目标文件及备份文件,源码注释清晰,层次分明,便于直接编译与烧录验证。已有204人学习下载,代码覆盖MCP2515的初始化流程、工作模式切换、接收滤波器配置、中断服务程序以及报文收发函数,便于对照调试。通过阅读和运行代码,可快速掌握51单片机与MCP2515协同工作的完整链路,从底层寄存器配置到应用层收发函数,均可移植到自主项目中,有效降低CAN通信开发门槛。

1. 51单片机上驱动 MCP2515:为什么 CAN 控制器要先于波特率被想明白

给 51 单片机加 CAN 总线,跟加串口完全是两个路子。串口靠定时器就能把电平拼出来,CAN 不行;它有时序、仲裁、错误重发和帧格式,51 的算力不适合在中断里实时拼位流。MCP2515 就是用来干这件事的外置 CAN 控制器,通过 SPI 接收 51 的指令,自己完成成帧、发送、错误检测和接收过滤。驱动它的代码写得好不好,直接决定节点在总线上是否“讲规矩”。这篇就按 MCP2515 的数据手册思路,从 SPI 底层、寄存器初始化、收发帧到回环验证,把 51 上最容易出问题的地方过一遍。新手可以照抄,老手可以重点看位时序和错误标志那几处。

2. 先打通 SPI:用 GPIO 模拟 SPI 读写 MCP2515 寄存器

2.1 MCP2515 引脚与 51 的最小连线

MCP2515 有 18 脚 DIP,也有 SOIC。51 端最少只需要 4 根线:CS、SCK、SI、SO,另外把 INT 接到 51 的外部中断引脚上,便于做接收中断。晶振直接接在 MCP2515 的 OSC1/OSC2,推荐 16MHz,后面的波特率寄存器配置都按 16MHz 算。

MCP2515 引脚方向51 单片机引脚说明
CS输入P2.0SPI 片选,低有效
SCK输入P2.1SPI 时钟
SI输入P2.2主出从入(MOSI)
SO输出P2.3主入从出(MISO)
INT输出P3.2(INT0)中断输出,低有效
RESET输入P2.4也可以接 RC 上电复位

选择普通 IO 模拟 SPI 有两个原因:一是 89C51/STC89C52 这类芯片不一定有硬件 SPI;二是用 GPIO 模拟时,时钟极性和相位可以完全由代码控制,调试时更容易用逻辑分析仪定位问题。如果用的是 STC12 或 STC15,硬件 SPI 可以顶上来,但寄存器读写函数接口不必改。

2.2 用 GPIO 模拟 SPI 模式 0,先写两个底层函数

MCP2515 支持 SPI 模式 0,0 和 1,1。这里统一用模式 0,也就是说 SCK 空闲为低,数据在上升沿被锁存。对应的 C51 代码如下。

sbit MCP_CS = P2^0; sbit MCP_SCK = P2^1; sbit MCP_SI = P2^2; sbit MCP_SO = P2^3; void SPI_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { MCP_SCK = 0; MCP_SI = (dat & 0x80) ? 1 : 0; // 先送出最高位 dat <<= 1; MCP_SCK = 1; // 上升沿,MCP2515 锁存 SI } MCP_SCK = 0; } unsigned char SPI_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { MCP_SCK = 0; dat <<= 1; if (MCP_SO) dat |= 0x01; // SCK 为低时读取 SO MCP_SCK = 1; } MCP_SCK = 0; return dat; }

模式 0 下,SCK 的低电平阶段数据已经稳定,所以在拉高 SCK 之前读取 SO 是安全的。写时序则是先把数据放到 SI,再用上升沿让 MCP2515 采进去。如果板子上 SI/SO 接反,这个函数不会报错,只会读回来全 0 或固定值,这是接线检查要做的第一件事。

2.3 用 READ 命令确认 SPI 链路通没通

MCP2515 的 SPI 指令不多,先记住最常用的 6 个。

指令字节值功能后续数据
RESET0xC0复位所有寄存器
READ0x03从指定地址开始读地址,然后是读出的数据
WRITE0x02从指定地址开始写地址,然后是写入的数据
BIT MODIFY0x05对指定寄存器做位修改地址、掩码、数据
RTS0x80~0x84请求发送 TXB0/TXB1/TXB2
READ STATUS0xB0快速读取收发状态

基于这 6 条指令,先写三个最常用的封装:复位、读寄存器、写寄存器。

void MCP2515_Reset(void) { MCP_CS = 0; SPI_WriteByte(0xC0); // RESET 指令 MCP_CS = 1; } unsigned char MCP2515_ReadReg(unsigned char addr) { unsigned char dat; MCP_CS = 0; SPI_WriteByte(0x03); // READ SPI_WriteByte(addr); dat = SPI_ReadByte(); // 读回一个字节 MCP_CS = 1; return dat; } void MCP2515_WriteReg(unsigned char addr, unsigned char val) { MCP_CS = 0; SPI_WriteByte(0x02); // WRITE SPI_WriteByte(addr); SPI_WriteByte(val); MCP_CS = 1; }

上电后第一个动作是复位。复位后 MCP2515 会进入配置模式,CANSTAT 寄存器地址 0x0E 的高 3 位变成 0b100,也就是 0x80。所以用下面的代码等链路就绪。

MCP2515_Reset(); while ((MCP2515_ReadReg(0x0E) & 0xE0) != 0x80);

如果仿真或真机卡死在这个循环里,基本不是时序问题就是 CS/SCK 接错。可以先用万用表确认 SI/SO 没有短路,再用逻辑分析仪抓 SCK 和 SI 上有没有 0xC0 这个字节。链路通了,下一步才谈得上配置 MCP2515。

3. 初始化 MCP2515:模式切换、波特率计算和验收滤波器

3.1 复位后先在配置模式里操作,不要一上来写 Tx/Rx

MCP2515 的寄存器分成几组,其中 CNF1、CNF2、CNF3、RXB0CTRL、TXB0CTRL 这些寄存器只有在配置模式下才能写入。复位后芯片默认就处于配置模式,所以上面复位后等待 CANSTAT 等于 0x80 的做法,恰好保证了后续写波特率寄存器是安全的。

实际开发里常见错误是:写 MCP2515_Init 时不进配置模式,直接往 TXB0CTRL 里写数据,结果发送邮箱永远不工作。正确的顺序是:复位、确认配置模式、写中断控制、写波特率、写接收模式、回到 Normal 模式。中断标志 CANINTF 在配置模式下也要先清一次,避免上电残留的 RX0IF 直接触发一次接收中断。

3.2 用 CNF1/CNF2/CNF3 设置波特率,先搞懂 TQ

MCP2515 的波特率不是“9600”那种概念,而是把一个位时间切成若干份 TQ(Time Quantum)。计算公式是:

TQ = 2 × (BRP + 1) / F_OSC 位时间 = (1 + PROPSEG + PHSEG1 + PHSEG2) × TQ

其中同步段固定为 1 个 TQ,其余三段分别由 CNF2、CNF3 控制。16MHz 晶振下,如果 BRP=0,那么 TQ=2/16MHz=125ns;要得到 500kbps,位时间是 2µs,等于 16 个 TQ。我习惯用下面这组配置,采样点靠后,抗干扰能力在中短距离总线上表现不错。

波特率BRP 写入值CNF1CNF2CNF3
500kbps00x000xB10x05
250kbps10x010xB10x05
125kbps30x030xB10x05

CNF2 里的 0xB1 对应 BTLMODE=1、PHSEG1=6、PROPSEG=1;CNF3 里的 0x05 表示 PHSEG2=5。把寄存器值加 1 才是实际 TQ 数,所以位时间 TQ 数是 1 + 2 + 7 + 6 = 16。500k、250k、125k 只是通过 BRP 改变 TQ 长度,段比例保持不变,总线上的采样点位置也就保持一致。

初始化时把这些值写进对应地址即可:

#define MCP_CNF1 0x28 #define MCP_CNF2 0x27 #define MCP_CNF3 0x26 #define MCP_CANCTRL 0x0F #define MCP_CANSTAT 0x0E MCP2515_WriteReg(MCP_CNF1, 0x00); // BRP=0, SJW=1TQ MCP2515_WriteReg(MCP_CNF2, 0xB1); // PHSEG1=7TQ, PROPSEG=2TQ MCP2515_WriteReg(MCP_CNF3, 0x05); // PHSEG2=6TQ

如果总线上有多个节点,波特率寄存器必须一致,尤其是 BRP 和段长度不能只对波特率相等就完事。两个节点都显示 500k,但一个采样点在 60%,一个在 85%,长总线奇数次重发就会出现。

3.3 验收滤波器先全放开,RTS 前少踩一个坑

MCP2515 默认配了 6 个验收滤波器和 2 个屏蔽寄存器,刚开始驱动时不要急着配 ID 过滤,先用“接收所有报文”验证链路。方法是在 RXB0CTRL 和 RXB1CTRL 里把 RXM0 和 RXM1 都写成 1。

#define MCP_RXB0CTRL 0x60 #define MCP_RXB1CTRL 0x70 MCP2515_WriteReg(MCP_RXB0CTRL, 0x60); // RXM1:0 = 1:1,接收所有 MCP2515_WriteReg(MCP_RXB1CTRL, 0x60);

0x60 这个值常见,但要注意它同时也打开了 RXRTR 等位。数据手册里位 7 是 RXM1,位 6 是 RXM0,两个都是 1 表示不对验收滤波器和屏蔽位做匹配。这样任何 ID 的报文都会进入接收缓冲器,调试时不需要考虑 ID 算错的问题。

3.4 完整初始化函数,最后一定要回到 Normal 模式

把前面所有步骤合并成一个初始化函数。这里同样按 16MHz 晶振,默认 500kbps 处理。

void MCP2515_Init(void) { MCP2515_Reset(); while ((MCP2515_ReadReg(MCP_CANSTAT) & 0xE0) != 0x80); MCP2515_WriteReg(0x2B, 0x00); // CANINTE: 先关中断 MCP2515_WriteReg(0x2C, 0x00); // CANINTF: 清中断标志 // 波特率:500kbps,16MHz 晶振 MCP2515_WriteReg(MCP_CNF1, 0x00); MCP2515_WriteReg(MCP_CNF2, 0xB1); MCP2515_WriteReg(MCP_CNF3, 0x05); // 接收全部报文 MCP2515_WriteReg(MCP_RXB0CTRL, 0x60); MCP2515_WriteReg(MCP_RXB1CTRL, 0x60); // 开接收中断 MCP2515_WriteReg(0x2B, 0x03); // RX0IE + RX1IE // 切换到 Normal 模式 MCP2515_WriteReg(MCP_CANCTRL, 0x00); while ((MCP2515_ReadReg(MCP_CANSTAT) & 0xE0) != 0x00); }

地址 0x2B 是 CANINTE,位 0 和位 1 分别对应 RX0IE、RX1IE。开中断前先清 CANINTF 是个好习惯,不然复位后残留的过载标志会立刻把 51 的 INT0 拉低。回 Normal 模式的等待循环也不能少,否则后面紧接着发送数据,芯片可能还停留在配置模式,寄存器写入会被硬件忽略。

4. 发送与接收 CAN 报文:把帧装进 TXB/RXB 邮箱

4.1 标准帧 ID 在 TXB0SIDH/TXB0SIDL 里的排列

MCP2515 每个发送邮箱有独立的 SIDH、SIDL、DLC 和 8 字节数据区。标准帧的 11 位 ID 放在两个寄存器里:TXB0SIDH 存 ID 的低 8 位,TXB0SIDL 的高 3 位存 ID 的最高 3 位。这里的对应关系容易搞反,很多人直接写id >> 3到 SIDH,结果总线上的 ID 和预期完全不是一个值。

接收侧重建 ID 时用下面公式:

id = ((unsigned int)(SIDL & 0xE0) << 3) | SIDH;

发送侧写入时反向操作:

MCP2515_WriteReg(MCP_TXB0SIDH, (unsigned char)(id & 0xFF)); MCP2515_WriteReg(MCP_TXB0SIDL, (unsigned char)(((id >> 8) & 0x07) << 5));

扩展帧还会用到 EID8 和 EID0,并且要在 SIDL 的 bit3 写 1。建议第一次调驱动只做标准帧,把邮箱布局吃透后再处理扩展帧。51 的课程设计大多不要求扩展帧,标准帧够用。

4.2 发送:先查 TXREQ,再用 RTS 命令

写发送函数时,第一件事不是填 SIDH,而是检查 TXB0CTRL 的 bit3,也就是 TXREQ。如果上一帧还没发完,直接覆盖会破坏正在发送的报文。下面是完整的发送函数。

bit MCP2515_Send0_Std(unsigned int id, unsigned char *buf, unsigned char len) { unsigned char i, ctrl; for (i = 0; i < 250; i++) { ctrl = MCP2515_ReadReg(MCP_TXB0CTRL); if ((ctrl & 0x08) == 0) break; // TXREQ=0,邮箱空闲 delay_10us(); } if (ctrl & 0x08) return 0; // 超时,邮箱仍占用 MCP2515_WriteReg(MCP_TXB0SIDH, (unsigned char)(id & 0xFF)); MCP2515_WriteReg(MCP_TXB0SIDL, (unsigned char)(((id >> 8) & 0x07) << 5)); MCP2515_WriteReg(MCP_TXB0DLC, len & 0x0F); for (i = 0; i < len; i++) MCP2515_WriteReg(MCP_TXB0D0 + i, buf[i]); // RTS 指令 0x81:请求发送 TXB0 MCP_CS = 0; SPI_WriteByte(0x81); MCP_CS = 1; return 1; }

delay_10us 可以用 51 定时器做一个最小延时,循环 250 次大约 2.5ms。如果总线上有多个节点连着发送,稍微等一下再发,比不加判断直接覆盖更稳。RTS 指令没有地址参数,CS 拉低后只发一个字节 0x81,MCP2515 就开始按 CAN 时序处理 TXB0 里的帧。

4.3 接收:轮询 CANINTF,别只盯一个缓冲器

接收路径先用轮询把 RXB0 跑通。CANINTF 的位 0 是 RX0IF,位 1 是 RX1IF。收到一帧后,从 RXB0SIDH、RXB0SIDL、RXB0DLC、RXB0D0 依次读出。读完再通过位修改指令清标志。

bit MCP2515_Rx0_Std(unsigned int *id, unsigned char *buf, unsigned char *len) { unsigned char i, dlc, sidl; if ((MCP2515_ReadReg(MCP_CANINTF) & 0x01) == 0) return 0; // RX0IF 未置位 sidl = MCP2515_ReadReg(MCP_RXB0SIDL); *id = (((unsigned int)(sidl & 0xE0)) << 3) | MCP2515_ReadReg(MCP_RXB0SIDH); dlc = MCP2515_ReadReg(MCP_RXB0DLC) & 0x0F; if (dlc > 8) dlc = 8; *len = dlc; for (i = 0; i < dlc; i++) buf[i] = MCP2515_ReadReg(MCP_RXB0D0 + i); // 清 RX0IF,保留其它中断标志 MCP2515_WriteReg(MCP_CANINTF, MCP2515_ReadReg(MCP_CANINTF) & ~0x01); return 1; }

清标志有两种写法:直接写 CANINTF 地址,或者用 BIT MODIFY 只清相应位。上面代码读回再求反写回,简单但存在风险,如果两次 SPI 操作之间又有新报文进 RXB1,第二次写回可能把硬件置上去的新标志清掉。更稳妥的是用 BIT MODIFY:

MCP_CS = 0; SPI_WriteByte(0x05); // BIT MODIFY SPI_WriteByte(MCP_CANINTF); SPI_WriteByte(0x01); // 掩码 SPI_WriteByte(0x00); // 清零 MCP_CS = 1;

如果走中断方式,把 INT 接到 P3.2,在中断服务函数里读取 CANINTF,再调用同一个处理逻辑。中断里不要做长延时,收到数据后置一个标志,回主循环再解析。51 单片机课程设计的 CAN 节点,用轮询和中断的实时性差别不大,选好用的方式就行。

5. 驱动写完了怎么验证:回环模式、错误寄存器和 DTS 移植提示

5.1 回环模式只是把 CANCTRL 写成 0x40

回环模式是 MCP2515 自检的一把钥匙。在这个模式下,数据不经过 CAN 收发器,直接在芯片内部从发送缓冲器回到接收缓冲器。所以不需要在总线上挂第二个节点,只要 51 和 MCP2515 之间 SPI 正常,就能确认初始化、发送、接收、中断标志这四条链路有没有问题。

进入回环模式的办法是写 CANCTRL 的 REQOP 位为 010,也就是 0x40,然后等待 CANSTAT 高 3 位也变成 0x40。测试流程如下。

MCP2515_WriteReg(MCP_CANCTRL, 0x40); while ((MCP2515_ReadReg(MCP_CANSTAT) & 0xE0) != 0x40); // 发送一帧标准帧 MCP2515_Send0_Std(0x123, data, 1); // 等待 RX0IF 置位 for (i = 0; i < 200; i++) { if (MCP2515_ReadReg(MCP_CANINTF) & 0x01) { // 接收并比对 ID break; } delay_10us(); }

回环失败时,不要急着改寄存器配置,先查硬件。MCP2515 的 RESET 引脚如果悬空,芯片可能一直被复位,CANINTF 永远读不到标志。真机上还有一个容易忽略的点:CANH/CANL 必须接收发器,比如 TJA1050,但回环模式下收发器不参与数据路径,所以即使收发器没装,回环也应该能过。Proteus 仿真 51 单片机连同 MCP2515 时,同样先跑这个流程,能过再上真实总线。

5.2 总线报错先看 EFLG、TEC、REC

上线后如果发现发不出去,或者发送状态一直占用,不要先怀疑 SPI。MCP2515 有个错误标志寄存器 EFLG,地址 0x2D,配合 TEC 和 REC 两个 8 位错误计数器,能很快定位问题。

EFLG 位标志含义
bit7RX1OVRRXB1 溢出
bit6RX0OVRRXB0 溢出
bit5TXBO总线关闭
bit4TXEP发送错误被动
bit3RXEP接收错误被动
bit2TXWAR发送错误警告
bit1RXWAR接收错误警告
bit0EWARN错误警告

最常见的是 TXBO,也就是总线关闭。此时芯片已经不能再发数据,必须重新初始化或者等恢复机制退出。通常原因是 CANH/CANL 接反、波特率不匹配、或者总线没有接 120Ω 终端电阻。调试时把这个值映射到 51 的串口打印出来,比在 Keil5 里单步看寄存器快得多。

5.3 从 51 移植到 RK3128 DTS 时,哪些寄存器逻辑不变

如果哪天项目从 51 挪到 ARM,比如 RK3128 上接 MCP2515,驱动代码会完全换成 Linux 的 mcp251x 内核驱动,但波特率寄存器 CNF1/CNF2/CNF3 的计算逻辑不会变。Linux 设备树主要解决的是 SPI 资源描述问题。

&spi1 { status = "okay"; mcp2515: can@0 { compatible = "microchip,mcp2515"; reg = <0>; status = "okay"; spi-max-frequency = <10000000>; clocks = <&xtal16m>; interrupt-parent = <&gpio1>; interrupts = <RK_PB2 IRQ_TYPE_LEVEL_LOW>; }; };

这里 interrupt-parent 和 interrupts 对应 51 侧的 INT 引脚,clocks 里的 16MHz 晶振对应 51 方案里的外部晶振。在 51 里你手写 SPI 时序去访问寄存器,在 Linux 里这些工作由 spi 子系统和 mcp251x 驱动完成,但 CANCTRL 里的模式切换顺序,以及波特率分段设置,仍然是当年那套逻辑。所以 51 上调试踩过的坑,换平台后依然有参考价值。

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

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

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

立即咨询