ARTICLE DETAIL

资讯详情

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

STM32F103通过SPI驱动MCP2515实现CAN总线通信的完整指南

STM32F103通过SPI驱动MCP2515实现CAN总线通信的完整指南 简介面向嵌入式开发者的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实际使用中我固定按模式0CPOL0CPHA1处理这是绝大多数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; // 写IDSIDH存高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与500kbpsCAN波特率由CNF1、CNF2、CNF3三个寄存器共同决定。TQ是最小时间单元计算公式是TQ 2 × (BRP1) / fosc一个位时间段由同步段、传播段、相位缓冲段1、相位缓冲段2组成。配置时先确定位时间总TQ数再反推寄存器值。目标波特率BRP同步段传播段相位缓冲段1相位缓冲段2CNF1CNF2CNF3250kbps011770x000x300x06500kbps011330x000x180x021Mbps011110x000x080x00以250kbps那行为例8MHz晶振BRP0时TQ0.25us同步段1TQ、传播段1TQ、PHSEG1取7TQ、PHSEG2取7TQ加起来16TQ位时间正好4us对应250kHz。采样点位置在(117)/1656.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对应RXF0RXM0RXB1对应RXF1RXM1。屏蔽寄存器决定哪些位必须匹配验收码哪些位可以忽略。收所有帧的配置是屏蔽寄存器写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代表逐位严格匹配。如果想要收到ID0x120到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.5VCANL从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超时。如果一切正常这套驱动就可以进入业务逻辑开发阶段。本文还有配套的精品资源点击获取
返回列表