简介:面向嵌入式开发者的STM32F与MCP2515 SPI转CAN通讯完整工程资源包,解决MCU通过SPI接口扩展CAN总线通信的典型需求,适用于工业控制、车载电子等场景,适合具备一定STM32基础并希望掌握CAN通信的开发者。压缩包内含161个文件,以头文件(h)、C源码(c)、编译生成的AXF/OBJ等为主,并附带Keil工程配置与调试辅助文件,整体压缩后仅3.32MB,便于快速部署与二次开发。目前已有2801人学习下载,实践参考价值较高。内容覆盖STM32F的SPI主模式配置、MCP2515寄存器初始化、CAN2.0A/B帧收发、中断与DMA处理以及常见错误标志检测等关键环节,工程内代码结构清晰,可直接对照CubeMX配置和固件逻辑进行移植调试验证,能帮助开发者大幅度缩短SPI转CAN通讯方案的开发周期。
1. 为什么STM32F外挂MCP2515而不是直接用片上CAN外设
很多开发者在选型阶段会优先看STM32F103的片上bxCAN,但做实际项目时会发现三个现实问题:F103C8T6这类高性价比型号只有单路CAN,一套设备里要隔离驱动总线和调试总线就不够用;换上国产GD32或同时挂以太网时,片上外设数量再次成为瓶颈;CAN的位定时同步和错误重发逻辑放在MCU侧调试成本很高。MCP2515把CAN协议栈从物理层到数据链路层全部处理完,STM32F只需要通过SPI读写寄存器就能得到一个完整CAN节点。这篇文章以STM32F103加8MHz晶振的MCP2515模块为例,覆盖SPI主模式配置、寄存器初始化、中断收发、波特率匹配和故障定位,代码可以直接放进Keil工程验证。
2. MCP2515寄存器地图与STM32F的SPI主模式配置
2.1 SPI指令帧结构:先发指令再传地址
MCP2515在SPI总线上不是普通存储器,它定义了一套指令集。所有访问都要先拉低CS,发出一个指令字节,后续字节按指令类型决定是地址还是数据。最常用的五条指令列在下面:
| 指令字节 | 助记符 | 说明 |
|---|---|---|
| 0x02 | 写寄存器 | 地址 + 数据 |
| 0x03 | 读寄存器 | 地址 + 读回数据 |
| 0x05 | 位修改 | 地址 + 屏蔽字节 + 数据字节 |
| 0x40 | 读状态 | 返回一个状态字节 |
| 0x80/0xA0/0xC0 | 发送TXB0/B1/B2 | 无后续字节 |
0x05这条位修改指令在做中断标志清除和模式切换时非常有用,它在芯片内部完成读-改-写,避免主控侧先读再改再写造成的窗口期。SPI时序上MCP2515要求数据在SCK上升沿移入,时钟空闲电平支持模式0或者模式3,实际使用中我固定按模式0(CPOL=0,CPHA=1)处理,这是绝大多数MCP2515驱动库的默认配置。
对应的读写函数写成这样:
static uint8_t mcp2515_xfer(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); } uint8_t mcp2515_read_reg(uint8_t addr) { uint8_t val; mcp2515_cs_low(); mcp2515_xfer(0x03); mcp2515_xfer(addr); val = mcp2515_xfer(0xFF); mcp2515_cs_high(); return val; }第三个字节传0xFF的作用是持续输出SCK时钟,主机的移位寄存器在每个时钟沿从MISO线上采样,最终得到目标寄存器的值。这里必须等待RXNE标志置位后再取数据,否则连续读多个寄存器时容易拿到上一次的残留值。我调试SPI通信时遇到过的数据错位问题,大多是漏了这句等待直接连读导致的。
2.2 SPI速率、引脚复用与软件片选
STM32F103的SPI1挂在APB2总线上,时钟72MHz。MCP2515规格书上写的最高SCK是10MHz,但实际模块上如果用杜邦线连接,超过4.5MHz后MISO信号沿变缓,偶发性读错位开始出现。稳妥的做法是预分频器设成8,得到9MHz时钟,再不行就降到16。
void spi1_init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_4; // CS使用普通GPIO gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4); spi.SPI_Direction = SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode = SPI_Mode_Master; spi.SPI_DataSize = SPI_DataSize_8b; spi.SPI_CPOL = SPI_CPOL_Low; spi.SPI_CPHA = SPI_CPHA_2Edge; spi.SPI_NSS = SPI_NSS_Soft; spi.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; spi.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &spi); SPI_Cmd(SPI1, ENABLE); }PA5、PA6、PA7分别对应SCK、MISO、MOSI,必须配置成复用推挽输出,PA4作为软件片选用普通推挽输出。SPI_NSS设为Soft的含义是硬件NSS引脚不参与控制,片选完全交给软件拉电平。硬件片选模式在总线竞争时虽然省一条GPIO,但对MCP2515这种交互型从设备并不合适,因为一次访问内部可能拉低片选几百微秒,软片选可以随时释放总线给其他SPI设备。
2.3 复位指令与配置模式切换
MCP2515上电后的默认状态是配置模式,但晶振起振时间和内部上电复位时间不完全确定,所以驱动初始化第一步要显式发复位指令0xC0。复位后需要等待至少1ms,让内部逻辑稳定,然后再写寄存器。
void mcp2515_reset(void) { mcp2515_cs_low(); mcp2515_xfer(0xC0); mcp2515_cs_high(); delay_ms(1); } void mcp2515_set_mode(uint8_t mode) { uint8_t ctrl = mcp2515_read_reg(0x0F); // CANCTRL ctrl &= 0xE0; ctrl |= mode; mcp2515_write_reg(0x0F, ctrl); }CANCTRL寄存器的最低5位是REQOP位段,0x80对应配置模式,0x00对应正常模式。波特率寄存器和验收滤波器只能在配置模式下修改,如果跳过模式切换直接写,寄存器值不会改变,而且不会报任何错误。切换模式后建议回读CANCTRL的OPMOD位段确认目标模式已经生效,因为模式切换本身需要几个时钟周期,立刻读可能返回旧值。
3. 中断驱动的CAN帧收发链路
3.1 三个发送缓冲区的选择与优先级
TXB0、TXB1、TXB2的寄存器基地址分别是0x30、0x40、0x50,每个缓冲区都有独立的发送优先级控制。TXBnCTRL上的TXP位段越高,仲裁优先级越高,但这里的优先级只在芯片内部多个TXB同时等待发送时起作用,并不影响CAN总线上的仲裁结果。
要发送标准帧时,需要依次写入ID、DLC和数据字节,最后置位TXREQ请求发送。标准ID为11位,分布在SIDH和SIDL两个寄存器中,写的时候要做移位处理:
void mcp2515_send_std(uint8_t txb, uint16_t id, uint8_t dlc, uint8_t *data) { uint8_t base = (txb == 0) ? 0x30 : (txb == 1) ? 0x40 : 0x50; // 写ID:SIDH存高8位,SIDL存低3位(对齐到bit7:5) mcp2515_write_reg(base + 1, (uint8_t)(id >> 3)); mcp2515_write_reg(base + 2, (uint8_t)(id << 5)); // DLC低4位是数据长度,标准帧扩展位用0填充 mcp2515_write_reg(base + 5, dlc & 0x0F); for (uint8_t i = 0; i < dlc; i++) mcp2515_write_reg(base + 6 + i, data[i]); // TXREQ置1,开始发送 mcp2515_bit_modify(base, 0x08, 0x08); }TXBnSIDL的bit7到bit5放ID低3位,所以id << 5之后刚好落在高位。如果直接给SIDH和SIDL各填一个字节再用整型拼接ID,发送到总线上的帧ID就会错位。写DLC时,RTR位在第6位,标准数据帧这一位必须是0,期望数据长度超过8字节时按8字节处理,CAN协议里DLC最大就是8。
发送完成后TXREQ自动清零,同时CANINTF寄存器对应TXBnIF位置1。主控可以轮询这两个标志判断发送状态。我在项目里一般优先使用TXB0,只有在需要连续快速发送大量报文时才启动TXB1和TXB2的轮换机制,让三个缓冲区交替发送。
3.2 外部中断引脚与主循环取值分离
MCP2515的INT引脚是低有效开漏输出,任何接收或错误中断都会把它拉低。把它接在STM32F的PA0上并配置为下降沿外部中断是常见做法。
void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) != RESET) { g_can_irq_flag = 1; EXTI_ClearITPendingBit(EXTI_Line0); } }中断服务函数里只置标志位,真正的SPI读取放到主循环。这样设计的理由很简单:SPI读取一个接收帧需要拉低片选、发多个字节、再拉高片选,整个过程大约几十微秒。如果这段代码放在中断上下文里,一旦此时有一个更高优先级的中断插入,SPI时序会被拉长,而MCP2515的接收缓冲寄存器在片选拉高之前不会锁存新数据,极端情况下会造成读出的帧内容和DLC校验不一致。
主循环中检测到标志后,先读CANINTF确认中断来源,再执行读取。这样设计之后,CAN报文接收的实时性由硬件中断保证,SPI访问的完整性由主循环保证,两者互不干扰。对于1Mbps波特率下报文间隔只有几微秒的极端场景,这个方案还是会有丢帧风险,那时候需要改用DMA配合SPI连续读取。
3.3 接收缓冲区读取与验收标志清除
读取接收帧时,指令0x60可以返回RXB0和RXB1的状态,以及帧格式信息。但更通用的方式是指定基地址0x60或0x70,配合连续读指令把数据一次性取出。
uint8_t mcp2515_read_rx(uint8_t rxb, can_msg_t *msg) { uint8_t base = (rxb == 0) ? 0x60 : 0x70; uint8_t dlc; msg->id = (mcp2515_read_reg(base + 1) << 3) | (mcp2515_read_reg(base + 2) >> 5); dlc = mcp2515_read_reg(base + 5) & 0x0F; msg->dlc = (dlc > 8) ? 8 : dlc; for (uint8_t i = 0; i < msg->dlc; i++) { msg->data[i] = mcp2515_read_reg(base + 6 + i); } // 清除对应接收中断标志 mcp2515_bit_modify(0x2C, (rxb == 0) ? 0x01 : 0x02, 0x00); return 1; }RXB0和RXB1读取完成之后,必须清除CANINTF寄存器里的RX0IF或RX1IF,下次中断才能正常触发。如果忘记清除,INT引脚会一直保持低电平,外部中断不再产生新的边沿,整条接收链路直接失效。这也是MCP2515调试中最常遇到的“收不到后续帧”的原因。
4. 波特率计算、验收滤波与总线故障定位
4.1 用8MHz晶振配出250kbps与500kbps
CAN波特率由CNF1、CNF2、CNF3三个寄存器共同决定。TQ是最小时间单元,计算公式是TQ = 2 × (BRP+1) / fosc,一个位时间段由同步段、传播段、相位缓冲段1、相位缓冲段2组成。配置时先确定位时间总TQ数,再反推寄存器值。
| 目标波特率 | BRP | 同步段 | 传播段 | 相位缓冲段1 | 相位缓冲段2 | CNF1 | CNF2 | CNF3 |
|---|---|---|---|---|---|---|---|---|
| 250kbps | 0 | 1 | 1 | 7 | 7 | 0x00 | 0x30 | 0x06 |
| 500kbps | 0 | 1 | 1 | 3 | 3 | 0x00 | 0x18 | 0x02 |
| 1Mbps | 0 | 1 | 1 | 1 | 1 | 0x00 | 0x08 | 0x00 |
以250kbps那行为例:8MHz晶振,BRP=0时TQ=0.25us,同步段1TQ、传播段1TQ、PHSEG1取7TQ、PHSEG2取7TQ,加起来16TQ,位时间正好4us,对应250kHz。采样点位置在(1+1+7)/16=56.25%,落在位周期中间偏前,适合大多数工业总线场景。
mcp2515_set_mode(0x80); // 配置模式 mcp2515_write_reg(0x28, 0x00); // CNF1 mcp2515_write_reg(0x29, 0x30); // CNF2 mcp2515_write_reg(0x2A, 0x06); // CNF3 mcp2515_set_mode(0x00); // 回正常模式收到“波特率配置不生效”的问题时,先检查当前是否在配置模式。另一个常见的坑是总线上两端的TQ分配不一致——波特率一样但采样点不同,长线传输时会在位边沿出现毛刺,造成偶发错误帧。调试时尽量让所有节点使用完全相同的CNF1、CNF2、CNF3值,不要只核对最终波特率数字。
4.2 验收屏蔽寄存器的实际配置
MCP2515的验收滤波分为验收屏蔽和验收码两组,RXB0对应RXF0+RXM0,RXB1对应RXF1+RXM1。屏蔽寄存器决定哪些位必须匹配验收码,哪些位可以忽略。收所有帧的配置是屏蔽寄存器写0。
// 只接收ID为0x123的帧 mcp2515_write_reg(0x00, 0x12); // RXF0SIDH: 高8位 mcp2515_write_reg(0x01, 0x30); // RXF0SIDL: 低3位(<<5) mcp2515_write_reg(0x04, 0x12); // RXF1SIDH mcp2515_write_reg(0x05, 0x30); // RXF1SIDL // 验收屏蔽:所有位都需要匹配 mcp2515_write_reg(0x20, 0xFF); // RXM0SIDH mcp2515_write_reg(0x21, 0xFF); // RXM0SIDL mcp2515_write_reg(0x24, 0xFF); // RXM1SIDH mcp2515_write_reg(0x25, 0xFF); // RXM1SIDL验收码寄存器写0x123的高8位0x12和低3位0x30,屏蔽寄存器全写0xFF代表逐位严格匹配。如果想要收到ID=0x120到0x13F范围内的帧,只需要把验收码SIDL写0x20,屏蔽写入0xE0,忽略SIDL低2位即可。
这里要注意RXB0和RXB1的滤波逻辑不完全相同。RXB0有两条滤波链可以用,RXB1只有一条。当RXB0被占满而又有新帧到达时,新帧会落到RXB1,这时RXB1的滤波器决定它是否接收。如果没有配置RXB1的相关寄存器,默认值全0,会把所有帧都收进来。我的习惯是RXB0设窄滤波处理关键报文,RXB1关掉滤波做调试兜底。
4.3 状态寄存器区分总线故障类型
MCP2515的CANINTF和EFLG寄存器组合起来能定位大部分总线故障。EFLG里比较重要的位有:
- TXBO:总线关闭
- TXWAR:发送警告
- RXWAR:接收警告
- TXEP/RXEP:错误被动状态
TEC或REC超过127会进入错误被动状态,超过255则触发总线关闭。排查顺序是先读EFLG,再读CANINTF,把两个值拼起来判断。
uint8_t err = mcp2515_read_reg(0x2D); // EFLG uint8_t irq = mcp2515_read_reg(0x2C); // CANINTF if (err & 0x80) { // 总线关闭,需要重新初始化控制器恢复正常模式 mcp2515_set_mode(0x80); mcp2515_write_reg(0x2D, 0x00); // 清除错误标志 mcp2515_set_mode(0x00); }总线关闭后MCP2515不会自动恢复,必须软件重置模式。最常见的原因是总线只有单节点在发送,没有ACK应答,MCP2515会不断重发并累计发送错误计数,直到超过255进入总线关闭。遇到这个问题时先确认总线上是否接了终端电阻和至少第二个节点,而不是急着改软件。
5. 用回环模式验证时序并抓取波形
5.1 配置回环模式的步骤
调试时不要把两个模块插在总线上直接猜问题。MCP2515支持自检用的回环模式,CANCTRL的REQOP段设为0x40即可进入回环模式。这种模式下芯片的发送逻辑和接收逻辑在内部直接连通,发出的帧会原样返回给自己,不需要外部节点。
void mcp2515_loopback_init(void) { mcp2515_reset(); mcp2515_clear_ints(); mcp2515_set_mode(0x80); // 配置模式 mcp2515_write_reg(0x28, 0x00); // 250kbps配CNF1 mcp2515_write_reg(0x29, 0x30); // CNF2 mcp2515_write_reg(0x2A, 0x06); // CNF3 mcp2515_bit_modify(0x2C, 0x07, 0x00); // 清中断 mcp2515_set_mode(0x40); // 回环模式 }然后在主循环中先发一帧0x55,再等INT引脚拉低,读取RXB0,检查数据是否与发送内容一致。能回读就说明SPI通路、寄存器位段和中断线路都没有问题。回环模式下不需要终端电阻,也不需要CAN收发器,只要SPI接线正确就能跑起来。
如果回环模式数据能收但双节点通不了,下一步用示波器或逻辑分析仪观察CANH和CANL之间的差分信号。正常发送一帧时CANH从2.5V跳到3.5V,CANL从2.5V跳到1.5V,幅值大约2V。如果只能看到共模电平变化而没有差分跳变,说明是TJA1050或SN65HVD230这类收发芯片的供电或终端电阻问题。
5.2 用片选毛刺判断SPI时序异常
抓SPI波形时重点关注CS拉低到第一个SCK上升沿之间的建立时间,以及SCK频率是否稳定。MCP2515要求CS拉低后至少等待一个SPI时钟周期再发数据,如果这个时间太短,指令字节可能未被正确锁存。表现是读寄存器返回0xFF或0x00,而且逻辑分析仪上MISO完全没有有效数据。
我一般会在每个SPI事务结束后主动拉高CS并延时几个微秒再开始下一笔。MCP2515在一次读操作结束后,内部的寄存器输出已经锁存,如果CS刚拉高又立刻拉低开始新的访问,个别位段可能出现毛刺。延时函数用空循环或定时器都行,关键是保证片选信号干净复位。
双节点验证通过后,把初始化中的模式从0x40改回0x00,再在总线上用CAN分析仪确认报文ID和DLC,整个SPI转CAN链路就完成了。最后一件事是跑一小时的稳定性测试,在总线上周期发送随机ID的帧,观察MCP2515的EFLG寄存器是否出现警告位,同时检查STM32F侧是否有SPI超时。如果一切正常,这套驱动就可以进入业务逻辑开发阶段。
本文还有配套的精品资源,点击获取