搞嵌入式这几年SPI、I2C、UART没少用但每次碰到CAN总线还是得翻半天寄存器手册。最近用STM32F407ZGT6做了两版带CAN通信的板子一版是双控制器CAN1CAN2同时跑另一版是和外部设备混接中间踩了不少坑也把之前零散的笔记重新捋了一遍。这篇就是我在实际调板过程中整理的CAN相关记录覆盖时钟树、波特率计算、过滤器分配、HAL库收发流程、以及总线上那些“看着玄学、其实就那几件事”的故障排查适合正在用F407调CAN的开发者特别是第一次用bxCAN外设、对邮箱和过滤器机制不太熟的。内容我会尽量按调试顺序写从为什么选F407做CAN开始一直到最后怎么用示波器查总线问题期间能贴代码的贴代码能举实测数据的给数据。1. 为什么在F407上做CAN以及它适合哪些场景1.1 F407的bxCAN外设到底强在哪儿先说说为什么我用F407而不是其他片子做CAN。STM32F407ZGT6内置两个CAN控制器代号CAN1和CAN2官方把这套外设叫basic CAN也就是常说的bxCAN。这类控制器最有价值的地方在于你不必像用51那样外挂SPI接口的独立CAN控制器芯片也不用自己软件实现CAN协议栈的底层时序把寄存器配好、报文丢进邮箱剩下的硬件仲裁、错误检测、错误状态迁移都由外设自动处理。这对于产品研发来说省掉的固件工作量是实打实的。bxCAN并不是精简到只剩收发功能的那种简陋控制器它有完整的报文存储机制3个发送邮箱2个接收FIFOFIFO0和FIFO1每个邮箱可以容纳一个完整报文还有28个可配置的过滤器组支持标准帧和扩展帧过滤。这些硬件资源在你面对双控制器通信时尤其有用因为CAN1和CAN2的过滤器组是共享的可以由软件灵活分配。这个我后面第3章会细说。从总线机制上看CAN和串口最大的区别是“多主通信报文仲裁”。串口只要两个设备同时发数据线上就乱套了CAN总线允许多个节点同时尝试发送通过报文ID的优先级在物理层做逐位仲裁低ID的报文赢输的节点自动转为接收。这套机制决定了你不需要在主从之间约定“谁先说话”非常适合传感器数据采集、设备状态上报这类天然多节点、突发上报的场景。再加上CAN_H和CAN_L两条差分线抗干扰能力比单端UART好不少在电机驱动、工业设备、车载设备附近这种电磁环境不太友好的地方通信稳定性会好很多。1.2 选F407做CAN前的三点权衡F407的CAN并不是万能的动手画板之前我先做了几点权衡不然做到一半发现需求覆盖不了会很麻烦。第一F407内置的CAN控制器不支持CAN FD。CAN FD是后续扩展的协议单帧有效载荷可以从8字节提升到64字节但bxCAN不支持这种帧格式。如果你的项目规划里明确要用CAN FD做高速升级那要么换带FDCAN外设的单片机要么在总线上加一颗CAN FD转换桥。普通CAN 250kbps-1Mbps的波特率、8字节数据帧F407完全扛得住如果只是常规控制指令和状态机上报没必要强上CAN FD。第二要提前想清楚GPIO复用。F407ZGT6的CAN1和CAN2引脚是固定的几个映射组CAN1可以用PB8/PB9或PD0/PD1CAN2可以用PB12/PB13或PB5/PB6。画PCB的时候得先查一下你的板子上这些引脚有没有被其他外设占掉比如PB5被SPI片选占着、PD0被外部中断占着那CAN就只能换引脚组甚至换芯片的封装管教这直接影响Layout周期。第三F407的CAN收发器不集成在MCU内部。也就是说MCU引脚出来的是CAN_TX和CAN_RX的TTL电平必须外接一颗CAN收发器芯片比如常规的高速CAN收发器把信号转成差分电平再接到CAN总线上。这颗收发器的选型还会影响最高速率和抗浪涌能力很多“CAN发不出去”的案子其实就是收发器那边的接线或5V/3.3V电平没处理好。2. 时钟和波特率CAN通信的基础配置2.1 波特率不是随便配的先理解位时间STM32F407系统主频168MHz时CAN外设挂在APB1总线上APB1时钟默认是42MHz。你的CAN波特率就是从这个APB1时钟分频来的跟串口计算相似但有个关键不同CAN把每个比特的时间进一步拆成了三段同步段SYNC_SEG、时间段1TSEG1、时间段2TSEG2。位时间 1 TSEG1 TSEG2单位是“时间量子tq”。所以说波特率公式写作波特率 APB1时钟 / 预分频 × 1 TSEG1 TSEG2举例主频168MHz、APB142MHz、目标是500kbps。42MHz除以500k等于84也就是说预分频和位时间总数乘起来得84。我常配的是一组分频级方案预分频41TSEG1TSEG221两者相乘正好84。然后再细化TSEG116 tqTSEG24 tq116421。为什么要拆得这么细因为CAN不是简单的电平传输它在每个位的后半段要采样总线电平。采样点的位置直接影响你在长总线、高波特率下通信的可靠性。CAN规范里推荐采样点落在75%到80%附近太早或太晚都容易把干扰电平当有效数据。我按上面那组参数算一下采样点1 TSEG1/1 TSEG1 TSEG2116/21 80.9%算是比较合理的值。再比如把预分频设为31TSEG1TSEG228想要75%左右的采样点就可以设TSEG120TSEG27采样点21/2875%。实际项目里我很少去把采样点调到极端因为还要同时兼顾总线长度和节点数量80%附近是一个稳妥的起点。2.2 用CubeMX配置CAN外设现在大部分人做STM32开发都会先用STM32CubeMX生成初始化工程我也不例外。配置CAN时重点看三个地方时钟树里的APB1频率、CAN参数结构体里的位时序、以及引脚复用。在CubeMX里打开CAN1外设会自动配套分配一组引脚F407ZGT6默认给的是PB8和PB9这两个引脚同时也能被I2C1等外设复用所以一定要在“PinoutConfiguration”界面里确认一下有没有冲突。CAN2默认给的是PB12和PB13。注意CAN2的引脚不能随意选择它没有独立映射到PD0这类位置上只能在PB12/PB13或PB5/PB6里选。参数页的“Bit Timing”部分CubeMX给了两种设置方式一是直接填“Prescaler”和“TimeQuanta”这类基础参数二是勾选“Calculated Bit Time”后填目标波特率软件自动反算分频和位时序。两种我都试过建议有经验的人直接手动填因为自动分配的采样点有时会跑到85%以上太大。手动填完之后工程生成代码你会在can.c里看到类似的初始化结构体hcan1.Instance CAN1; hcan1.Init.Prescaler 4; hcan1.Init.Mode CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_16TQ; hcan1.Init.TimeSeg2 CAN_BS2_4TQ; hcan1.Init.TimeTriggeredCommMode DISABLE; hcan1.Init.AutoBusOffManagement DISABLE; hcan1.Init.AutoWakeUp DISABLE; hcan1.Init.AutoRetransmission DISABLE; hcan1.Init.ReceiveFifoLocked DISABLE; hcan1.Init.TransmitFifoPriority DISABLE;这里SJW是同步跳转宽度一般设为1 tq就够只有总线上节点时钟误差较大时才需要加大到2或4。AutoBusOffManagement是否开启我建议前期调试不要开让它报Bus-Off错误你才能知道总线上出过问题量产固件里再决定要不要自动恢复。后续实际使用中如果发现某个节点频繁Bus-Off导致整个网络不稳定可以再开自动恢复来减少人工干预但那不是“治好”问题的手段只算缓解。3. 报文结构、过滤器的底层逻辑3.1 各种帧类型和ID到底怎么理解在写过滤器和收发程序前最好把CAN报文本身的结构过一遍。bxCAN支持两种ID格式标准帧ID是11位范围0x000-0x7FF扩展帧ID是29位范围更大。发送和接收的数据帧分数据帧和远程帧两类远程帧里没有数据字节只有IDRTR标志常用于请求远端节点发送数据。实际做控制类通信99%都用数据帧。每个报文的ID不只是标识地址它还是总线上仲裁优先级的关键ID值越小优先级越高。这一点在“多节点抢总线”的时候体现得最明显。比如ECU1发ID0x100ECU2发ID0x200这两个节点同时发送因为0x100的每一位比较下来更“强势”ECU2会在仲裁位检测到自己输了自动退出发送。所以你设计CAN通信矩阵的时候除了考虑ID好不好记还要考虑关键报警类报文是不是分配了较低的ID确保它在冲突时能先发出去。3.2 过滤器的位宽、模式和FIFO归属F407的28个过滤器组本质是28个独立的硬件比较器在数据进入FIFO前先按预设条件筛一遍不匹配的直接丢掉。这样CPU不用处理一堆跟自己无关的报文能省下不少主频资源。过滤器组每个都可以独立配置成按位宽分32位模式或16位模式按工作方式分屏蔽位模式ID Mask或列表模式ID List32位屏蔽模式最常用。屏蔽位掩码的作用像个筛选开关掩码位为1要求收到的ID对应位必须和预设值完全相等掩码位为0对应位不关心。用生活里的例子来说就像查快递单号掩码决定了“必须精确匹配哪几位”如果掩码全0等于这个过滤器放行所有报文掩码全1等于只放行指定ID。实际项目里我更多用全掩码“只收某一个或某几个ID”的方式来避免无关ID把FIFO塞满。分配FIFO这里也有讲究。F407的两个FIFO是独立的你既可以所有报文全进FIFO0也可以部分ID进FIFO0、部分ID进FIFO1。如果速率高、报文密建议把不同类别的报文分到不同FIFO然后在中断里分别处理避免所有数据挤在同一个FIFO里由于处理不过来溢出丢帧。3.3 双CAN怎么合理分配过滤器组F407的CAN1和CAN2共用一套过滤器组但硬件上有个默认规则初始化代码里hcan2会把起始过滤器组设到14。也就是说如果你不特别设置CAN1能用过滤器组0-13CAN2能用14-27一共28组刚好分开。这个细节常被忽略。有些人在配置CAN2时想用过滤器组0结果一运行发现CAN2完全收不到报文但寄存器的值检查起来又觉得是对的。原因就在这个共享逻辑上你在某个过滤器组里设置了FIFO归属和ID但是组号已经被CAN1那边用了CAN2用不了。我自己的做法是如果只用CAN1我就从0开始配如果只用CAN2就从14开始配如果双CAN都开那就0-13留给CAN114-27留给CAN2互不干涉。下面这段是典型的32位屏蔽过滤器配置让CAN1只接收标准帧ID0x0AA的报文CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank 0; sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh (0x0AA 5); sFilterConfig.FilterIdLow 0x0000; sFilterConfig.FilterMaskIdHigh (0x7FF 5); sFilterConfig.FilterMaskIdLow 0x0000; sFilterConfig.FilterFIFOAssignment CAN_RX_FIFO0; sFilterConfig.FilterActivation ENABLE;这里把标准帧ID左移5位是为了让它落在32位过滤器寄存器的高位段这个位置和CAN控制器内部报文存储的位顺序是对应的。掩码高16位设成全1表示对标准ID的11位全部精确匹配低16位设成0是因为我们没有用到扩展帧和RTR这些位不关心它们。如果你要过滤扩展帧29位ID那就需要同时操作高16位和低16位左移的方式也会不一样。可以看到HAL库把复杂的寄存器位段封装起来了但使用之前你还是得清楚这些位是往哪个字段填的否则调了半天其实该匹配的位置没填对。4. 基于HAL库的收发完整流程4.1 发送邮箱满了怎么办发送在HAL库里的入口是HAL_CAN_AddTxMessage()。它的原意是把一条待发报文装进一个空闲的发送邮箱硬件随后自动完成发送。三个发送邮箱意味着你在同一时刻最多只能压入3条待发报文。超过3条函数会忙返回典型的现象是循环发送时每次到第4条就卡住或者返回HAL_BUSY之后就没有后续了。这一步最容易被新手忽略的是第三个参数pTxMailbox。HAL库会把实际分配到的邮箱号回填到这个指针里比如0、1、2。如果你不需要关心具体是哪个邮箱可以直接传一个临时变量但如果你要做“发送完成中断里再发下一条”这类后续调度就得记录这个邮箱号然后在发送完成回调里确认是哪条报文被发出了。标准帧和扩展帧报文头需要分别配置CAN_TxHeaderTypeDef txHeader; txHeader.IDE CAN_ID_STD; txHeader.StdId 0x0AA; txHeader.RTR CAN_RTR_DATA; txHeader.DLC 8; uint8_t txData[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; uint32_t txMailbox 0; HAL_StatusTypeDef status; status HAL_CAN_AddTxMessage(hcan1, txHeader, txData, txMailbox);注意这里的数据数组不能是函数内部的临时数组因为硬件发送是异步的函数返回后DMA或外设还会去读这块内存。我遇到过一次排查了很久的问题就是在某个局部函数里定义了发送数组函数一退出发送内容就变成随机数据了。后来改成全局数组或static数组问题立刻消失。附带要讲的是如果发送失败HAL库并不会自动重发。F407虽然硬件有自动重传的功能但默认在AutoRetransmission DISABLE的情况下只发一次就不管了。如果你需要重传机制要么把初始化参数打开要么在应用层程序里判断发送状态自己决定重试次数。我习惯把AutoRetransmission设为DISABLE原因是“宁可让上层知道丢了一帧也不要让它卡在重传里把后续的指令都堵住”。4.2 接收中断回调不要干重活接收报文的第一步是让CAN外设运转起来。很多人初始化完CAN和过滤器就觉得万事大吉结果发现数据根本没进中断。原因往往是忘了调HAL_CAN_ActivateNotification()这个函数负责把FIFO接收挂起中断使能打开。顺序是初始化CAN外设和过滤器配置并启动接收中断的NVIC调用HAL_CAN_Start(hcan1)启动CAN调用HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING)使能接收回调这几步的先后顺序不用过度纠结但一定要都做。启动后一旦FIFO0收到报文硬件就会触发中断HAL库内部会清中断标志然后回调用户重定义的HAL_CAN_RxFifo0MsgPendingCallback()。在这个回调里调用HAL_CAN_GetRxMessage()取出报文头和8字节数据。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (hcan-Instance CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); // 根据 rxHeader.StdId 或扩展ID做分发把数据拷到全局缓冲区 } }在中断回调里取出报文后务必尽快拷贝数据到一个全局变量或消息队列里不要在中断里做解析协议、打印日志这些耗时的操作。之前我在一个项目里直接把报文转成字符串再从串口打印板子在报文密集时直接中断响应不过来表现为CAN总线反复出现错误帧但那其实不是总线问题是CPU在中断里卡了太久整机看门狗都要跳了。正确做法是中断里只做“拷贝置标志”主循环或RTOS任务里再做处理。4.3 完整自收发Demo的思路如果你手边没有第二个CAN设备想快速验证F407的CAN功能可以使用回环模式CAN_MODE_LOOPBACK来测试。回环模式下发送报文不会真正上总线但硬件内部会自己把发送内容反馈到接收FIFO里相当于MCU自己发、自己收。这对验证初始化、过滤器、中断链路特别有用。我在验证F407ZGT6的双CAN时是先把CAN1配成回环发一组数据看看FIFO0能不能收到确认无误后再改成正常模式把CAN1和CAN2接到同一根总线上两端各加120欧终端电阻然后让CAN1发、CAN2收两边的板子通过串口打印收到的内容。这里要注意的是正常模式下总线两端必须各接一个120欧终端电阻否则信号反射会导致大量错误帧这点我放到第5章展开。5. 总线上那些坑错误排查与调试心得5.1 终端电阻、接线顺序、波特率不一致我做CAN调试时有相当一部分“通信不上”的案例最后排查下来都是那几个基础原因这里必须单独列一下。第一终端电阻。CAN总线标准要求在物理总线两端各接一个120欧电阻注意是两端不是全部节点都接更不是不接。终端电阻的作用是吸收信号反射保证差分电平在采样点能稳定落在规定范围内。很多开发板上已经集成了这个120欧电阻但有些需要通过跳线帽开启个别模块默认不焊这就会导致单独一块板子连电脑调试时报文发出去但总线上没有ACK于是发送节点反复报ACK错误。如果你手头只有一块板子做回环测试那无所谓一旦接到总线上请先确认终端电阻状态。第二接线顺序。CAN_H对CAN_HCAN_L对CAN_L这个看着简单反了却很容易让人绕进代码里。CAN收发器输出的差分体现在CAN_H和CAN_L之间的电压差如果两条线对调所有节点看到的都是反相电平仲裁和ACK都会异常。用万用表通断档打一下物理线路能快速排除这个问题。第三波特率不一致。两个节点一个用500k一个用250k结果是两边都会疯狂报错。这里最迷惑的地方在于CRC校验和位填充机制决定了“波特率不一致”不会像串口那样偶尔解出一堆乱码而是直接产生连续错误帧表现为你发一个字节对方什么也收不到同时报错计数器不断累加。所以调试时第一步先确认总线波特率是否一致最简单的方法是用USB转CAN工具或逻辑分析仪去看实际总线的位数长度。5.2 用CAN状态寄存器定位问题F407每个CAN外设都有一个错误状态寄存器记录发送错误计数TEC和接收错误计数REC。这两个计数器的增减机制很有意思成功收发一次计数减1出错一次加8。当TEC或REC超过127节点进入被动错误状态当TEC超过255节点进入Bus-Off状态这时CAN不再参与总线通信等于掉线了。你在调试时可以通过读寄存器实时观察节点状态。HAL库也提供了接口HAL_CAN_GetError()返回一个错误码可以区分ACK错误、位填充错误、CRC错误等场景。我把常见场景整理成了一张表现象TEC/REC变化大概率原因排查方向发数据时TEC猛涨TEC 8总线上没有ACK检查终端电阻、收发器、其他节点是否在线双方波特率不同两边REC都涨位错误用工具确认实际波特率CAN_H/L接反REC涨偶发ACK错差分反相用万用表查接线干扰导致偶发错误REC偶尔上涨后回落电磁干扰检查屏蔽双绞线、接地、布线一直接收0.5秒后超时TEC突破255Bus-Off需要清错或自动恢复如果节点进了Bus-Off你可能会发现它彻底“闭嘴”了既不发也不收。如果开启自动恢复节点会在一定条件后重新参与通信但如果你不清楚背后发生了多少次错误反而会让故障隐蔽。我调试时会先关闭自动恢复让错误爆出来然后再分析根因。等真正解决了总线问题再在完整系统里评估要不要恢复自动恢复。5.3 用示波器和逻辑分析仪看差分波形软件层怎么查都查不出来的时候就得看物理波形了。CAN_H和CAN_L的典型电平和UART不同它是差分形式显性位逻辑0时CAN_H比CAN_L高2V左右隐形位逻辑1时两条线电压接近都大约在2.5V附近。用示波器两个探头分别接CAN_H和CAN_L再算出差值波形A-B模式就能看到非常清晰的位流。这一步你可以直接判断的事情很多。第一波形幅值和边沿是否正常如果差分幅度不足1.5V可能是总线过长、节点过多或终端电阻不对第二看隐性电平两条线是否接近如果CAN_H一直比CAN_L高很多说明收发器或线路有故障第三看是不是存在明显的振铃或尖峰那就去检查线缆类型和布线。不用示波器的话用逻辑分析仪采样CAN_H相对CAN_L的差分电平也可以但采样率至少要高于波特率的10倍比如500k波特率建议至少5M采样率起步否则边沿细节不准确。做了一轮完整的调试之后我对CAN的体会是大部分问题都不是“代码写不进去”而是“物理层和配置参数没对齐”。F407ZGT6的bxCAN把很多总线细节都封装好了我们真正要做的反而是把时钟、波特率、过滤器分配、FIFO归属这些基础参数吃透。把这一层搞明白不管是单CAN节点、双CAN控制器还是多设备组网配置起来都能顺很多。后续如果我再做CAN FD相关的东西还会把双层波特率、大数据帧那套逻辑继续整理出来跟大家继续交流。