
1. CAN通信基础与STM32F4硬件准备第一次接触CAN总线时我完全被那些专业术语吓到了。但实际用起来才发现STM32的库函数已经帮我们封装了大部分底层细节就像开车不需要懂发动机原理一样。CAN总线在工业控制、汽车电子领域应用广泛它的最大特点是抗干扰能力强最多可挂载110个节点最远传输距离能达到10公里波特率5kbps时。STM32F4系列通常自带1-2个CAN控制器我们以常见的STM32F407为例它的CAN1和CAN2都挂在APB1总线上。硬件连接只需要四根线CAN_H、CAN_L、VCC和GND。实际项目中我习惯用TJA1050这类CAN收发器芯片把控制器的逻辑电平转换成差分信号。这里有个容易踩坑的地方记得在CAN_H和CAN_L之间接120Ω终端电阻否则通信会不稳定。开发环境准备硬件STM32F407开发板我用的是正点原子探索者软件Keil MDK或STM32CubeIDE调试工具USB转CAN模块如ZLG的USBCAN-II库函数建议使用标准外设库或HAL库2. 从零搭建CAN通信工程2.1 引脚配置与时钟使能新建工程时我习惯先规划好引脚分配。以CAN1为例默认使用PA11(CTX)和PA12(CRX)但这两个引脚和USB冲突所以我改用了PD0(CRX)和PD1(CTX)。配置步骤如下// 使能时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); // 配置复用功能 GPIO_PinAFConfig(GPIOD, GPIO_PinSource0, GPIO_AF_CAN1); GPIO_PinAFConfig(GPIOD, GPIO_PinSource1, GPIO_AF_CAN1); // TX引脚配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOD, GPIO_InitStructure); // RX引脚配置 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_Init(GPIOD, GPIO_InitStructure);2.2 CAN控制器初始化波特率设置是第一个难点。CAN总线使用位定时机制计算公式为 波特率 APB1时钟 / (Prescaler * (BS1 BS2 1))假设APB1时钟为42MHz目标500kbps时CAN_InitStructure.CAN_BS1 CAN_BS1_4tq; // 时间段1 CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; // 时间段2 CAN_InitStructure.CAN_Prescaler 12; // 分频系数 // 计算42MHz / (12*(421)) 500kbps完整初始化代码CAN_InitTypeDef CAN_InitStructure; CAN_DeInit(CAN1); CAN_StructInit(CAN_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; // 时间触发模式 CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线管理 CAN_InitStructure.CAN_AWUM ENABLE; // 自动唤醒 CAN_InitStructure.CAN_NART DISABLE; // 禁止自动重传 CAN_InitStructure.CAN_RFLM DISABLE; // FIFO不锁定 CAN_InitStructure.CAN_TXFP DISABLE; // 发送优先级由ID决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_Init(CAN1, CAN_InitStructure);3. 过滤器配置技巧过滤器是CAN最复杂的部分之一但也是保证通信效率的关键。STM32F4提供28个过滤器组每个组可以配置为标识符列表模式精确匹配掩码模式模糊匹配)我常用的是32位掩码模式下面这个配置允许接收0x100-0x1FF的标准帧CAN_FilterInitTypeDef filter; filter.CAN_FilterNumber 0; filter.CAN_FilterMode CAN_FilterMode_IdMask; filter.CAN_FilterScale CAN_FilterScale_32bit; filter.CAN_FilterIdHigh 0x100 5; // STDID[10:0]左移5位 filter.CAN_FilterIdLow 0; filter.CAN_FilterMaskIdHigh 0x1FF 5; filter.CAN_FilterMaskIdLow 0; filter.CAN_FilterFIFOAssignment CAN_FilterFIFO0; filter.CAN_FilterActivation ENABLE; CAN_FilterInit(filter);实际项目中遇到过过滤器配置不当导致丢帧的问题后来发现要注意确保过滤器组号不冲突掩码位为1表示必须匹配为0表示不关心扩展帧ID要拆分成高16位和低16位分别设置4. 数据收发实战4.1 发送数据封装发送函数要考虑邮箱状态检查我习惯封装成带超时检测的版本uint8_t CAN_SendMsg(uint32_t id, uint8_t* data, uint8_t len, uint8_t ext) { CanTxMsg txMsg; uint32_t timeout 0; uint8_t mailbox; txMsg.IDE ext ? CAN_Id_Extended : CAN_Id_Standard; txMsg.RTR CAN_RTR_Data; txMsg.DLC len 8 ? 8 : len; if(ext) txMsg.ExtId id; else txMsg.StdId id; memcpy(txMsg.Data, data, len); mailbox CAN_Transmit(CAN1, txMsg); if(mailbox CAN_TxStatus_NoMailBox) return 1; while((CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) (timeout 0xFFF)) timeout; return timeout 0xFFF ? 2 : 0; }4.2 接收方案选择接收有两种常用方式各有利弊轮询方式适合简单应用if(CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, rxMsg); printf(Received ID:0x%X Data:, rxMsg.StdId); for(int i0; irxMsg.DLC; i) printf(%02X , rxMsg.Data[i]); printf(\n); }中断方式实时性更好void CAN1_RX0_IRQHandler(void) { if(CAN_GetITStatus(CAN1, CAN_IT_FMP0)) { CAN_Receive(CAN1, CAN_FIFO0, rxMsg); // 处理数据... CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }我在电机控制项目中实测发现500kbps波特率下中断方式比轮询的延迟低80%以上。但要注意中断服务函数要尽量精简避免嵌套中断。5. 调试技巧与常见问题调试CAN通信时这几个工具帮了大忙USB转CAN分析仪直接监控总线原始数据逻辑分析仪检查TX/RX引脚信号串口打印在关键节点输出状态信息常见问题排查清单通信失败检查终端电阻、波特率设置、滤波器配置只能发不能收确认收发器方向控制引脚有的芯片需要数据错乱检查ID冲突或过滤器掩码设置过宽偶尔丢帧增加重发机制或降低波特率有个特别隐蔽的坑STM32的CAN发送邮箱只有3个如果连续快速发送4帧以上数据必须检查邮箱状态否则会丢帧。我的解决方案是加入发送队列#define CAN_TX_QUEUE_SIZE 16 typedef struct { uint32_t id; uint8_t data[8]; uint8_t len; uint8_t ext; } CanTxMsgType; CanTxMsgType txQueue[CAN_TX_QUEUE_SIZE]; uint8_t txHead 0, txTail 0; void CAN_TxProcess(void) { if(txHead ! txTail CAN_TransmitStatus(CAN1, 0) CAN_TxStatus_Ok) { CAN_SendMsg(txQueue[txTail].id, txQueue[txTail].data, txQueue[txTail].len, txQueue[txTail].ext); txTail (txTail 1) % CAN_TX_QUEUE_SIZE; } }6. 完整项目示例结合温度传感器和CAN总线的数据采集节点// main.c int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); CanTransPort_Init(); ADC_Init(); uint16_t temp; uint8_t canData[8]; while(1) { temp Get_Temperature(); // 获取温度值 canData[0] temp 8; canData[1] temp 0xFF; CAN_SendMsg(0x123, canData, 2, 0); // 发送标准帧 if(CAN_MessagePending(CAN1, CAN_FIFO0)) { CanRxMsg rxMsg; CAN_Receive(CAN1, CAN_FIFO0, rxMsg); if(rxMsg.StdId 0x456) { // 接收到控制指令 Set_Fan_Speed(rxMsg.Data[0]); } } Delay_ms(1000); } }这个项目里我遇到了电磁干扰导致温度数据跳变的问题后来通过以下措施解决CAN总线增加磁环传感器信号线使用双绞线在代码中加入软件滤波算法移植到其他STM32F4芯片时主要需要修改引脚复用配置时钟树配置可能调整波特率参数