
做嵌入式这几年CAN总线是我用过最耐造的工业通信方式而STM32F103这颗片上集成的bxCAN外设又是看起来简单、实际踩坑最多的地方。标题里的几个关键词——STM32F103、CAN通信、标准库配置——其实已经把关键矛盾全暴露出来了网上随手一搜就是标准库例程但照抄进工程后经常出现波特率不对、收不到数据、上电卡死这类问题。这篇文章不堆大而全的理论直接从标准库配置到收发测试的完整流程走一遍把验证过的代码、调过的坑和排查思路都写出来适合正在调CAN通信、刚把F103最小系统跑起来准备接CAN收发器的朋友。1. 动手之前的硬件准备CAN总线不是“接上就能通”1.1 为什么选STM32F103的bxCAN做工业通信STM32F103这个系列虽然老但在工业控制、车载诊断、传感器采集这些场景里依然是常青树。原因很简单它片上自带的bxCAN模块支持CAN 2.0A和CAN 2.0B协议标准帧、扩展帧都能收发而且三个发送邮箱、两个接收FIFO、六个过滤器对中小项目完全够用。相比外挂SPI接口的MCP2515方案内部的bxCAN不需要额外通信开销中断响应也更直接代码写起来干净得多。很多人纠结为什么不用HAL库或者干脆换成F105/F107的双CAN我的看法是工具没有绝对好坏关键是项目规模和环境。标准库虽然官方不再更新但稳定、代码透明、网上资料量大特别适合把CAN收发时序彻底看明白后做移植。HAL库代码量更“政治正确”但封装的层次一多底层细节反而容易被遮住。这篇文章以标准库为主线末尾也会提一嘴HAL库对应函数的写法方便两边工程对照。1.2 最小系统之外的CAN硬件电路引脚、收发器、终端电阻STM32F103C8T6最小系统板上通常已经把晶振、复位、串口下载电路都做好了但CAN通信还需要自己外接收发器。F103的CAN1引脚默认映射在PA11RX和PA12TX这两个引脚同时也是USB的D-和D所以一旦板子上有USB电路就要特别小心冲突。如果PA11/PA12被USB占用、或者布线不理想F103还支持把CAN1重映射到PB8/PB9使用标准库的AFIO重映射功能即可。收发器我常用TJA1050或SN65HVD230。TJA1050供电是5V输出电平与3.3V单片机互连时要注意TXD/RXD的逻辑电平匹配不少模块内部已经做了电平转换但如果你是自己画板子最好查一下芯片数据手册的电平阈值。SN65HVD230是3.3V收发器用在F103上可以直接连接适合做低功耗或3.3V系统。基本接线并不复杂单片机的CAN_TXPA12接收发器TXDCAN_RXPA11接收发器RXD收发器CANH/CANL分别接总线上的两个线然后在总线两端各放一个120Ω终端电阻。注意如果总线上已经有两个节点各放了一个120Ω电阻就不要在第三个节点上再加否则等效电阻变小信号反射会变严重。1.3 关于CAN模块供电与接线常见电源误区的补充网上经常有人问“CAN通信模块芯片能否给板子供电”答案是不能。CAN收发器是一个电平转换器件不是电源管理芯片它需要外部供电才能工作不可能通过CANH/CANL给单片机供电。我那会儿见过一个同学把CAN收发器模块的CANH接到开发板3.3V上结果模块直接冒烟。这里明确两点模块的VCC和GND必须单独接对应的电源CANH/CANL只是差分信号线上面有隐性电平但绝对没有带载能力。还有一个容易被忽略的坑是共地。CAN虽然是差分信号但收发器的工作电源一定要和单片机共地否则共模电压超出收发器承受范围总线容易出现大量错误帧。我在实验桌上短距离测试时有时图省事只接CANH/CANL不接GND结果偶尔能通、偶尔完全静默后来老老实实把GND接上问题立刻消失。2. 标准库CAN初始化配置顺序与关键参数逐项拆解2.1 开启时钟与配置GPIO的顺序不能乱很多初学者把CAN不通归结为硬件问题实际上很大一部分原因是软件初始化顺序不对。标准库初始化CAN第一步必须是先使能时钟再配置GPIO最后初始化CAN外设。这里面总线归属要理清楚GPIOA和AFIO挂在APB2上CAN1挂在APB1上这两个时钟使能缺一不可。如果漏了AFIO时钟重映射操作会静默失败CAN功能根本无法工作。以默认引脚PA11/PA12为例GPIO配置代码可以这样写void CAN_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); /* PA11: CAN1_RX建议用上拉输入避免总线空闲时电平抖动 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); /* PA12: CAN1_TX复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); }如果要把CAN1重映射到PB8/PB9则在使能AFIO时钟后加一句GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);然后GPIO操作对象改成GPIOB的Pin8和Pin9。需要注意的是PA11/PA12在物理上不能同时再作为普通IO使用否则外设之间互相干扰这也是PA11处容易出“灵异事件”的原因后面会专门展开。2.2 CAN_Init结构体各字段的含义与推荐取值标准库的CAN初始化结构体字段很多第一次看容易发懵但其实每个字段都对应bxCAN控制寄存器里的一个位功能。我平时的推荐配置是关闭时间触发通信TTCM、开启自动总线恢复ABOM、开启自动唤醒AWUM、关闭非自动重传NART、关闭接收FIFO锁定RFLM、关闭发送优先级由FIFO决定TXFP。换句话说让外设尽量工作在自动管理的常规模式。对应的初始化代码void CAN_Module_Init(void) { 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; CAN_InitStructure.CAN_TXFP DISABLE; CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_13tq; CAN_InitStructure.CAN_BS2 CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler 4; if (CAN_Init(CAN1, CAN_InitStructure) ! CANINITOK) { /* 初始化失败这里建议打印或保存错误标志 */ } }逐项解释几个重点ABOM会在总线离线bus-off后自动恢复发送接收不开启的话一旦总线上错误率超标控制器就会一直处于离线状态表现就是“板子偶尔能收到数据但一发就死”。AWUM配合自动唤醒适合低功耗唤醒场景。NART设为DISABLE表示发送失败后硬件自动重传典型应用就是CAN重传保证可靠性如果改成ENABLE则发送不成功直接丢弃适合需要自己控制重发逻辑的场景。CAN_Mode有三个常用模式正常模式Normal、回环模式LoopBack和静默模式Silent。调试初期建议先用LoopBack模式不接外部设备也能自测代码里只需要把CAN_Mode改成CAN_Mode_LoopBack后面联调再切回Normal。2.3 过滤器配置屏蔽位模式一遍讲清CAN过滤器是很多人看不懂又绕不开的部分。F103的bxCAN有六个过滤器每个过滤器可以用32位屏蔽位模式或16位列表模式。最简单的办法是让所有报文都进FIFO0适合刚开始调通的阶段。32位屏蔽位模式下把筛选ID和掩码ID都设为0等价于接收所有IDvoid CAN_Filter_Init(void) { CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure); }掩码为0表示不关心任何位掩码为1表示必须匹配对应位。比如只想接收标准ID为0x123的报文就把FilterIdHigh/FilterIdLow填成0x123对应的格式MaskIdHigh/MaskIdLow填成全1。实际开发中我建议先把过滤器关掉或设为全通过等正常通信后再根据项目需求加过滤规则否则第一步就卡在“为什么我发了却收不到”上很难定位是过滤器问题还是物理链路问题。配置完过滤器后还需要使能接收中断。FIFO0对应中断入口是USB_LP_CAN1_RX0_IRQHandler需要在NVIC里使能这个通道同时调用CAN_ITConfig使能FMP0中断否则只能靠轮询CAN_Receive或检查FIFO计数实时性差很多。2.4 波特率计算为什么你的CAN速率总是不对CAN波特率计算的公式可以简化成一句话最终波特率等于APB1外设时钟除以分频器预分频值再除以一个完整位时间包含的时间量子数tq。F103在系统时钟72MHz下APB1最高是36MHz。一个位时间通常由同步段1tq、传播段相位缓冲段1BS1、相位缓冲段2BS2组成换算关系就是波特率 APB1时钟 / (CAN_Prescaler × (1 BS1 BS2))举个例子APB136MHzCAN_Prescaler4BS113tqBS24tq则位时间113418tq时间量子36MHz/49MHz最终波特率9MHz/18500kbps。这个500k是工业CAN最常用的速率之一。我经常看到有人用默认的BS19tq、BS28tq算出来是562.5kbps却以为自己配置的是500k结果双方速率不匹配总线上全是错误帧。不同波特率对应的推荐参数可以按这张表快速套用前提是APB1保持36MHz目标波特率预分频PrescalerBS1BS2位时间tq采样点1 Mbps21341877.8%500 kbps41341877.8%250 kbps81341877.8%125 kbps161341877.8%采样点一般建议落在75%到85%之间77.8%适合多数总线长度。如果总线上有波特率不同的节点必须把所有节点配置成完全一致的预分频、BS1、BS2才能保证采样点一致。很多总线偶发错误帧并不是硬件问题而是不同板卡的采样点偏差太大导致的。3. 收发代码与实测从LoopBack到USB转CAN联调3.1 发送函数设计邮箱等待与超时处理bxCAN发送有三组发送邮箱CAN_Transmit函数会自动选择一个空邮箱返回邮箱编号如果当前三个邮箱都满了会返回CAN_TxStatus_NoMailBox。发送完成还要通过CAN_TransmitStatus查询邮箱状态确认邮箱从Pending变成Ok而不能发完就不管否则在总线拥堵时可能存在丢帧和错误状态残留。一个带超时处理的发送函数可以这样写uint8_t CAN1_Send_Msg(uint32_t stdId, uint8_t *data, uint8_t len) { CanTxMsg txMsg; uint8_t mailbox; uint16_t timeout 0; if (len 8) len 8; txMsg.StdId stdId; txMsg.ExtId 0; txMsg.IDE CAN_Id_Standard; txMsg.RTR CAN_RTR_Data; txMsg.DLC len; for (uint8_t i 0; i len; i) { txMsg.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, txMsg); if (mailbox CAN_TxStatus_NoMailBox) { return 1; } while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { timeout; if (timeout 500) { CAN_CancelTransmit(CAN1, mailbox); return 2; } } return 0; }发送函数里的超时循环不能省。实际项目中如果总线上只有两个节点一个发一个收发送基本不会失败但一旦多个节点竞争总线优先级低的报文可能要等很久才能进邮箱如果没有超时机制CPU会被卡死在发送状态整个系统的实时性就崩了。合理的做法是设置一个用滴答定时器计算的超时阈值或直接用简单的软件计数确保异常时能退出并做错误记录。3.2 接收中断处理FIFO读取与数据校验接收部分我建议优先用中断而不是轮询。轮询CAN_Receive看起来简单但主循环里一旦有耗时操作比如驱动屏幕、执行Flash擦写就可能漏掉FIFO溢出导致丢帧。F103的CAN1_RX0中断入口是USB_LP_CAN1_RX0_IRQHandler这个函数名看着有点奇怪因为USB和CAN的Rx0在F103上共用了同一个中断向量。中断处理代码void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg rxMsg; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, rxMsg); /* 这里把接收到的数据拷贝到全局缓冲区并置标志位 */ g_can_rx_id rxMsg.StdId; g_can_rx_len rxMsg.DLC; memcpy(g_can_rx_buf, rxMsg.Data, rxMsg.DLC); g_can_rx_flag 1; } }在中断函数里尽量不要做复杂处理最稳妥的做法是把数据拷贝到全局变量置位标志让主循环去消费。如果需要根据ID立即响应可以在中断里做简单的状态机跳转但绝对不要调用延时函数或阻塞式串口打印否则中断嵌套和长时间占用会导致FIFO溢出。接收中断初始化别忘了使能NVICNVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);如果工程里还跑了FreeRTOSNVIC分组不要改成和RTOS内核冲突的方式。FreeRTOS一般要求NVIC_PriorityGroup_4也就是所有优先级位都作为抢占优先级你在这种工程里配置CAN中断时最好也保持一致并把中断优先级设为适合的数值避免在临界区中触发调度造成系统崩溃。3.3 回环模式自测不接外部设备也能验证在没有USB转CAN工具、没有第二个节点的情况下最快验证初始化是否正确的方法是使用LoopBack模式。LoopBack模式下bxCAN在内部把TX数据回环给RX路径不需要外部收发器和总线信号参与PA11/PA12上甚至不会产生有效差分管脚电平因此你可以用普通杜邦线把板子摆桌上直接测。测试思路是初始化时把CAN_Mode设成CAN_Mode_LoopBack然后在主循环里周期调用发送函数同时打开接收中断。如果在LoopBack模式下能收到自己发的ID和数据说明时钟、GPIO、过滤器、中断、发送邮箱全链路都通了硬件电路和外部总线才是下一步要排查的对象。我经常在项目调试的第一步先跑这个自测确认软件无误后再把模式切回Normal接外部设备。否则直接上总线调试一旦不通很难分清是软件问题还是收发器接线问题排查成本会成倍增加。3.4 用USB转CAN进行整机联调实测过滤与数据记录软件自测通过后就可以把USB转CAN工具和F103板卡接到同一对CANH/CANL上。联调时先在电脑端软件里把波特率设为500kbps然后让F103周期发送数据帧。如果电脑端能稳定收到说明发送链路没问题。接着用电脑端软件主动发一帧F103的中断收到后通过串口1打印出来。这里串口1的调试信息格式很重要建议把ID、DLC、数据字节都打出来方便对照printf(CAN RX: ID0x%03X DLC%d Data, g_can_rx_id, g_can_rx_len); for (uint8_t i 0; i g_can_rx_len; i) { printf(%02X , g_can_rx_buf[i]); } printf(\r\n);实际联调中我发现一个高频问题USB转CAN工具和板卡都不发数据时总线上的隐性电平稳定在2.5V左右一旦把终端电阻漏了用示波器能看到波形振铃明显报文出错率上升。还有一个很容易被忽视的点是很多USB转CAN工具的报文发送间隔和帧类型可以配置如果你发的是远程帧RTR而单片机只处理数据帧那也会出现“明明收到了却没有任何打印”的情况。所以联调前先确认工具端发的确实是标准数据帧。4. 踩坑实录从“上电卡死”到“丢帧”的排查手册4.1 初始化失败与上电卡死的三个常见根源CAN初始化的坑多数集中在时钟和GPIO配置。第一个坑是APB1时钟忘了开或开错总线代码执行到CAN_Init时直接卡死在等待初始化完成的循环里。标准库的CAN_Init有超时等待机制如果硬件时钟没起来函数返回CANINITFAILED但很多人不看返回值导致程序继续往下跑看起来就像“跑飞了”。建议在初始化后立刻判断返回值并利用串口或LED做状态提示。第二个坑是PA11/PA12被复用成其他功能。有人用CubeMX生成代码时把PA11配置成了普通输入或USB功能然后又手动加了CAN初始化两条外设配置互相覆盖最后CAN毫无反应。标准库的坑在于它不像HAL库那样有清晰的Pinout界面所有配置都在GPIO_Init里一旦后面再有代码把PA11改配置前面的初始化就白做了。第三个坑是重映射配置遗漏。用PB8/PB9做CAN时AFIO时钟和GPIO_PinRemapConfig缺一不可很多人只开了GPIOB时钟结果PB8/PB9始终是普通IOCAN当然不通。4.2 PA11/PA12引脚冲突与隐藏的USB占用问题STM32F103的PA11和PA12天生就是USB的D-和D它们和CAN1_RX/CAN1_TX共用引脚。官方手册明确警告USB和CAN不能同时在同一组引脚上工作。实际项目里最小系统板上的USB转串口芯片一般只占用PA9/PA10不会影响PA11/PA12但如果你自己画的板子上有USB座、或者别的外设把PA11拉低CAN接收就会一直处于错误状态。我遇到过一种典型的“PA11 bug”现象程序在最小系统板上收发一切正常挪到自研板卡上就完全收不到把逻辑分析仪挂上去才发现PA11电平始终被钳在低电平。原因是板子上USB的DM引脚带了下拉电阻恰好占用了PA11。解决方法是把CAN1重映射到PB8/PB9或者把USB功能彻底断开并检查PA11外部是否有下拉器件。这类问题最难查因为它不是软件错误而是原理图层面的隐性冲突。4.3 收不到数据过滤器、中断优先级、波特率三连排查CAN收不到数据按出现频率排序我基本都是按这个顺序排查的。第一是过滤器很多例程默认接收所有报文但你自己写了过滤器又想收0x123结果发的是0x124自然进不来。最快的验证办法是先把过滤器掩码全清零确认链路通后再精调过滤规则。第二是波特率USB转CAN工具和板卡只要相差哪怕1%总线上就会持续产生错误帧双方都收不到有效数据。除了代码里的参数还要确认APB1时钟源是否真的是36MHz有些工程改了系统时钟分频但CAN参数没跟着改。第三是中断和NVIC。F103的CAN接收中断和USB共用一个向量初始化时如果NVIC里的通道没开或者ISR里没调用CAN_ClearITPendingBit中断标志一直挂着CPU就会反复进入中断却不处理数据表现出来是“程序卡死”或“偶发丢帧”。建议同时在CAN_GetITStatus判断后调用CAN_Receive然后在函数末尾加CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0)兜底虽然标准库的CAN_Receive会释放FIFO但手动清一次不影响功能还能避免一些边界状况。4.4 发送失败、bus-off恢复与总线仲裁问题发送失败最常见的表现是发送函数一直等待最终返回超时。这时候先看是否是总线错误导致总线离线。如果总线上只有F103一个节点并且没有接任何其他CAN节点或USB转CAN工具正常模式下发送会一直尝试并重传因为总线没有应答ACK发送状态永远无法变成Ok。这是很多新手第一次测试时最纳闷的问题明明回环模式能收到切到正常模式就发送超时。解决办法很简单正常模式测试时必须保证总线上至少有两个节点或者接一个USB转CAN工具因为CAN协议规定发送节点必须收到至少一个其他节点的ACK才算发送成功。另一个问题是总线离线后怎么办。开启ABOM后硬件会自动等待128个总线空闲信号然后恢复通信。我调试高波特率或长线传输时偶尔会遇到总线错误率飙升导致bus-off开启ABOM能明显减少人工重启次数但要注意恢复需要时间实时性要求极高的系统要另做总线状态监控。4.5 标准库、HAL库与RTOS工程的几个易混点现在很多新人从CubeMX起步生成的是HAL库工程回头再看标准库代码容易混淆。HAL库的CAN初始化主函数是HAL_CAN_Init需要先用MX_CAN1_Init填好参数再调用HAL_CAN_Start紧接着用HAL_CAN_ActivateNotification使能接收回调。发送变成了HAL_CAN_AddTxMessage接收回调在HAL_CAN_RxFifo0MsgPendingCallback里实现。步骤比标准库多但功能和标准库一一对应理解标准库之后看HAL库会轻松很多。霍尔库和标准库同时在工程里使用是很多人容易踩的雷。虽然有些编译环境能把标准库文件单独塞进CubeMX工程但不建议强行混用因为外设寄存器的定义可能冲突。如果手里有旧标准库代码最快的迁移办法是直接对照参考手册重写一遍CAN相关部分而不是把两套库硬凑到一起。如果工程里已经移植了FreeRTOS还要注意CAN接收中断的优先级不要设置的数值过高避免在RTOS内核临界区里频繁抢占。合理的做法是让CAN中断活跃但不要打断SysTick和PendSV的关键操作同时主循环里消费CAN数据的任务单独做一个队列不要在中断里调用任何RTOS阻塞接口。这样多线程调CAN无论是标准库还是HAL库都能保持稳定。最后再分享一个我自己的调试习惯每次拿到新的CAN板卡我都会用回环模式跑一遍自检再写一个简单的“周期发送串口打印接收”测试程序验证完基础通信后才进入业务逻辑开发。这个习惯帮我挡掉了很多硬件和底层配置的问题。调试CAN没有太多捷径但只要把引脚映射、时钟、波特率、过滤器和中断这几关都过一遍绝大多数问题都能在一小时内定位到具体环节。