S32K144开发笔记6 - 软件SPI驱动MCP2515的CAN总线数据收发实战

S32K144开发笔记6 - 软件SPI驱动MCP2515的CAN总线数据收发实战

1. 硬件连接与引脚配置

在开始软件SPI驱动MCP2515之前,首先要确保硬件连接正确。S32K144与MCP2515的典型连接方式如下:

  • PTB13 — INT(接收数据中断引脚)
  • PTB14 — CLK(时钟)
  • PTB15 — MISO(主设备输入从设备输出)
  • PTB16 — MOSI(主设备输出从设备输入)
  • PTB17 — CSN(片选)

在实际项目中,我遇到过因为引脚配置错误导致通信失败的情况。比如有一次把MISO和MOSI接反了,调试了半天才发现问题。所以建议大家在焊接前先仔细检查原理图,避免这种低级错误。

在S32 Design Studio中配置引脚功能时,需要特别注意以下几点:

  1. 所有SPI相关引脚都应配置为GPIO输出模式(除了MISO是输入)
  2. 中断引脚PTB13需要配置为输入模式,并启用中断功能
  3. 片选引脚PTB17初始状态应该保持高电平

这里有个小技巧:在初始化代码中,可以先把所有引脚状态设置为高,然后再进行SPI操作。这样可以避免MCP2515在上电时收到错误信号。

2. 软件SPI实现细节

2.1 GPIO模拟SPI时序

由于使用的是软件SPI,我们需要用GPIO来模拟SPI的时序。SPI的基本操作包括发送和接收一个字节:

// SPI发送一个字节 void SPI_SendByte(unsigned char dt) { unsigned char i; for(i=0;i<8;i++) { MCP2515_CLK(0); // 时钟拉低 if((dt<<i)&0x80) // 判断最高位 MCP2515_MOSI(1); else MCP2515_MOSI(0); MCP2515_CLK(1); // 时钟拉高 } MCP2515_CLK(0); // 最后保持时钟低电平 } // SPI接收一个字节 unsigned char SPI_ReadByte(void) { unsigned char i,rByte=0; MCP2515_CLK(0); // 初始时钟低电平 for(i=0;i<8;i++) { MCP2515_CLK(1); // 时钟上升沿采样 rByte<<=1; rByte|=MCP2515_MISO; MCP2515_CLK(0); // 时钟下降沿 } return rByte; }

在实际调试中,我发现时钟信号的稳定性非常重要。如果时钟速度太快(比如小于1us的延时),可能会导致MCP2515无法正确识别信号。建议在初期调试时,可以在每个时钟边沿后加入适当的延时(比如1-5us),等通信稳定后再考虑优化速度。

2.2 MCP2515基本操作函数

基于上面的SPI函数,我们可以实现MCP2515的读写操作:

// 向MCP2515写入一个字节 void MCP2515_WriteByte(unsigned char addr, unsigned char dat) { MCP2515_CS(0); // 片选拉低 SPI_SendByte(CAN_WRITE); // 发送写命令 SPI_SendByte(addr); // 发送寄存器地址 SPI_SendByte(dat); // 发送数据 MCP2515_CS(1); // 片选拉高 } // 从MCP2515读取一个字节 unsigned char MCP2515_ReadByte(unsigned char addr) { unsigned char rByte; MCP2515_CS(0); // 片选拉低 SPI_SendByte(CAN_READ); // 发送读命令 SPI_SendByte(addr); // 发送寄存器地址 rByte=SPI_ReadByte(); // 读取数据 MCP2515_CS(1); // 片选拉高 return rByte; } // MCP2515复位 void MCP2515_Reset(void) { MCP2515_CS(0); // 片选拉低 SPI_SendByte(CAN_RESET); // 发送复位命令 delay_ms(100); // 适当延时 MCP2515_CS(1); // 片选拉高 }

在调试这些基础函数时,建议先用逻辑分析仪抓取SPI波形,确认时序是否正确。我曾经遇到过因为片选信号释放太快导致写入失败的情况,后来在写操作后增加了1ms的延时解决了问题。

3. MCP2515初始化与配置

3.1 初始化流程

MCP2515的初始化主要包括以下几个步骤:

  1. 硬件复位
  2. 设置CAN总线波特率
  3. 配置接收滤波器和屏蔽寄存器
  4. 设置中断使能
  5. 切换到正常工作模式
void MCP2515_Init(void) { unsigned char temp=0; MCP2515_Reset(); // 发送复位指令 delay_ms(1); // 短暂延时 // 设置波特率为500Kbps MCP2515_WriteByte(CNF1, CAN_500Kbps); MCP2515_WriteByte(CNF2, 0x80|PHSEG1_3TQ|PRSEG_1TQ); MCP2515_WriteByte(CNF3, PHSEG2_3TQ); // 清空接收缓冲区 MCP2515_WriteByte(RXB0SIDH, 0x00); MCP2515_WriteByte(RXB0SIDL, 0x00); MCP2515_WriteByte(RXB0CTRL, 0x20); // 仅接收标准帧 MCP2515_WriteByte(RXB0DLC, DLC_8); // 接收数据长度为8字节 // 配置滤波器和屏蔽寄存器(这里设置为接收所有报文) MCP2515_WriteByte(RXF0SIDH, 0x00); MCP2515_WriteByte(RXF0SIDL, 0x00); MCP2515_WriteByte(RXM0SIDH, 0x00); MCP2515_WriteByte(RXM0SIDL, 0x00); // 清空中断标志,使能接收中断 MCP2515_WriteByte(CANINTF, 0x00); MCP2515_WriteByte(CANINTE, 0x01); // 使能RX0中断 // 切换到正常模式 MCP2515_WriteByte(CANCTRL, REQOP_NORMAL|CLKOUT_ENABLED); // 确认是否进入正常模式 temp=MCP2515_ReadByte(CANSTAT); if(OPMODE_NORMAL!=(temp&&0xE0)) { MCP2515_WriteByte(CANCTRL, REQOP_NORMAL|CLKOUT_ENABLED); } }

3.2 波特率配置技巧

MCP2515的波特率配置相对复杂,需要设置三个寄存器(CNF1、CNF2、CNF3)。这里我分享一个实际项目中的经验:在车载应用中,500Kbps是最常用的波特率,但在工业控制中可能会用到125Kbps或250Kbps。

波特率计算公式为: CAN波特率 = Fosc / (2 × (BRP + 1) × TQ总数)

其中:

  • Fosc是MCP2515的时钟频率(通常为8MHz或16MHz)
  • BRP是CNF1中的波特率预分频值
  • TQ总数 = (同步段 + 传播段 + 相位缓冲段1 + 相位缓冲段2)

在我的一个项目中,使用8MHz晶振配置500Kbps时,CNF1=0x00,CNF2=0x90,CNF3=0x02。这个配置经过实际测试非常稳定。

4. CAN数据收发实现

4.1 发送数据

CAN数据发送需要考虑以下几点:

  1. 等待发送缓冲区就绪
  2. 设置报文ID(标准帧或扩展帧)
  3. 填充数据
  4. 触发发送
void CANSendData(unsigned int id, unsigned char *CAN_TX_Buf, unsigned char len) { unsigned char j,dly,count=0; while(count<len) { dly=0; // 等待发送缓冲区就绪 while((MCP2515_ReadByte(TXB0CTRL)&0x08) && (dly<50)) { delay_ms(1); dly++; } // 设置标准帧ID unsigned char DH = (unsigned char)(id >> 3); unsigned char DL = (unsigned char)((id & 0x07) << 5); MCP2515_WriteByte(TXB0SIDH, DH); MCP2515_WriteByte(TXB0SIDL, DL); // 填充数据 for(j=0;j<8;) { MCP2515_WriteByte(TXB0D0+j, CAN_TX_Buf[count++]); j++; if(count>=len) break; } // 设置数据长度并触发发送 MCP2515_WriteByte(TXB0DLC, j); MCP2515_CS(0); MCP2515_WriteByte(TXB0CTRL, 0x08); // 请求发送 MCP2515_CS(1); } }

在实际应用中,我发现发送超时检测非常重要。上面的代码中设置了50ms的超时等待,避免因为硬件故障导致程序死等。此外,对于关键数据,建议实现重发机制,比如连续发送3次,确保数据可靠传输。

4.2 接收数据(中断方式)

为了提高系统效率,我们采用中断方式接收CAN数据:

// 中断服务函数 void MCP2515_RECEIVE_IRQ(void) { uint32_t receive = PINS_DRV_GetPortIntFlag(PORTB) & (1 << 13); if(receive != 0) { if(((receive >> 13) & 0x01) == 1) { debug_flag = 1; // 设置接收标志 } } PINS_DRV_ClearPortIntFlagCmd(PORTB); // 清除中断标志 } // 主循环中处理接收 unsigned char CANReceiveBuffer(unsigned int *id, unsigned char *CAN_RX_Buf) { unsigned char i=0,len=0,temp=0; temp = MCP2515_ReadByte(CANINTF); if(temp & 0x01) { // 检查RX0IF标志 *id = get_standar_id(); // 获取报文ID len=MCP2515_ReadByte(RXB0DLC); // 获取数据长度 while(i<len) { CAN_RX_Buf[i]=MCP2515_ReadByte(RXB0D0+i); // 读取数据 i++; } } MCP2515_WriteByte(CANINTF,0); // 清除中断标志 return len; }

在调试中断接收时,有几点需要注意:

  1. 确保中断引脚配置正确(下降沿或低电平触发)
  2. 中断服务函数要尽可能简短,避免影响其他中断
  3. 在主循环中处理接收数据时,要考虑数据缓冲区的大小限制
  4. 记得清除中断标志,否则会一直触发中断

我曾经遇到过一个棘手的问题:中断频繁触发但实际没有新数据。后来发现是忘记清除中断标志了。所以现在每次写中断服务函数,我都会特别注意标志位的清除。

5. 调试技巧与常见问题

5.1 调试工具推荐

在开发CAN通信时,以下工具非常有用:

  1. 逻辑分析仪:用于观察SPI信号时序
  2. CAN分析仪(如PCAN、周立功CAN卡):用于监控CAN总线数据
  3. 示波器:检查信号质量和干扰
  4. 万用表:检查电源和接地

5.2 常见问题及解决方案

  1. SPI通信失败

    • 检查硬件连接是否正确
    • 确认时钟极性(CPOL)和相位(CPHA)设置
    • 降低SPI时钟速度测试
    • 用逻辑分析仪抓取波形分析
  2. CAN报文发送不成功

    • 检查MCP2515是否进入正常模式
    • 确认波特率设置与总线其他节点一致
    • 检查终端电阻(通常需要120Ω)
  3. 接收不到数据

    • 确认滤波器和屏蔽寄存器配置
    • 检查中断引脚连接和配置
    • 用CAN分析仪确认总线上确实有数据
  4. 通信不稳定

    • 检查电源是否干净(建议增加滤波电容)
    • 检查PCB布局,避免高频干扰
    • 适当降低通信速率测试

在最近的一个车载项目中,我们遇到了CAN通信偶尔丢帧的问题。经过排查发现是电源纹波太大导致的。后来在MCP2515的电源引脚增加了10μF和0.1μF的去耦电容,问题得到了解决。这个经验告诉我们,硬件设计同样重要,不能只关注软件。