ARTICLE DETAIL

资讯详情

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

STM32F407 CAN总线从零调试:位时序、过滤器与回环模式

STM32F407 CAN总线从零调试:位时序、过滤器与回环模式 简介一份面向嵌入式开发者和STM32初学者的CAN总线通信Demo资源基于STM32F407芯片演示CAN1/CAN2控制器初始化、滤波器配置、消息发送与接收的完整流程适用于汽车电子、工业控制等需要可靠实时通信的场合。压缩包共522个文件以C源码.c/.h、Keil工程文件.uvprojx、编译产物.axf/.o/.hex及说明文档为主整体约23MB便于直接导入Keil查看和调试。资源已有199人学习内含基础工程模板、硬件底层库HARDWARE/FWLIB/CORE/SYSTEM以及可运行的发送接收示例开发者可借助这份代码理解CAN2.0B协议的应用方法并快速迁移到自己的项目中去。1. stm32f407 CAN 总线 DEMO 到底要跑通什么点灯是很多人拿到 stm32f407 之后做的第一件事但点完灯真正开始调设备通信第一关往往是 CAN。CAN 总线上任何一端不上拉、波特率差一个百分点、过滤器多拦了一类帧现象都是“发不出去”或者“收不到”对着示波器看 CAN_H 和 CAN_L 却始终抓不到一个完整的显性位这才是这个 DEMO 真正要解决的问题。所谓 stm32f407 CAN 总线 DEMO不是给你一个点灯的框架而是把 F407 的两个 bxCAN 控制器从初始化到收发打通的最小链路跑起来GPIO 复用、位时序、过滤器、发送邮箱、接收 FIFO每一项都要能讲清楚寄存器里发生了什么。这篇文章面向正在调第一块 F407 板子的人也面向被 CAN2 调不通、波特率怎么都对不上折磨过的熟手把这个标题里缺省的那些参数和坑一次补全。2. CAN 总线 DEMO 开跑前必须搞清的协议与外设分工2.1 仲裁、ID 与过滤器为什么 DEMO 要先定好 IDCAN 报文里最容易被误解的是 ID。ID 不携带用户数据它只做两件事决定总线上的优先级以及作为接收端过滤的索引。总线上两个节点同时发送时从 SOF 之后逐位仲裁显性位 0 覆盖隐性位 1所以 ID 数值越小优先级越高。标准帧 ID 是 11 位范围 0x0000x7FF扩展帧是 29 位。DEMO 阶段不必把协议栈做完整但至少要固定一组 ID 规划否则后面两台设备联调时你会分不清帧是谁发的。我一般会在 DEMO 里约定两组 ID主控下发用 0x123设备上报用 0x456。标准帧数据帧各占 8 字节以内ID 不参与 CRC 校验之外的数据处理接收端匹配 ID 用的是过滤器而非软件判断。理解这一步对后面配置 CAN_FilterInitStructure 很有用过滤器是在硬件里按 ID 快速筛帧的不是靠 CPU 一条条比较。/* 发送端标准帧 0x1238 字节数据 */ CanTxMsg TxMessage; TxMessage.StdId 0x123; TxMessage.ExtId 0; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC 8; TxMessage.Data[0] 0xAA;这里 IDE 决定是标准帧还是扩展帧RTR 决定是数据帧还是远程请求帧。DEMO 阶段统一用数据帧远程帧放到后面再碰因为它会牵涉到 DLC 的语义变化容易把初学的人绕进去。2.2 bxCAN 的邮箱、FIFO 和过滤器结构stm32f407 有两个 CAN 控制器CAN1 和 CAN2协议内核是 ST 说的 Basic Extended CANbxCAN。每个控制器有 3 个发送邮箱、2 个接收 FIFOFIFO0 和 FIFO1每个 FIFO 里能缓存 3 帧。发送时你把报文填进任意一个空邮箱硬件自动按优先级和 TSS 时间戳调度接收时硬件按过滤器把命中的帧放进 FIFO再用 FIFO 的“有消息”中断通知 CPU。过滤器是 F407 比较特殊的部分28 个过滤器分布在 CAN1 和 CAN2 之间可以配置成分配给哪个控制器。每个过滤器有 2 个 32 位寄存器支持屏蔽模式和列表模式。屏蔽模式下只有 mask 位为 1 的位参与比较mask 为 0 的位不关心列表模式则要求 ID 必须完全一致。DEMO 阶段最省事的做法是先屏蔽全 0让所有帧都进 FIFO0通过软件读每条帧的 StdId 来区分。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表示任何 ID 都能进 FIFO0。参数说明FilterNumber 选择第几个硬件过滤器FIFOAssignment 决定帧进哪条 FIFO。这样配置之后你在中断回调里会同时收到 0x123 和 0x456这对联调阶段观察总线上的所有流量很有帮助。2.3 位时序公式与 500k 采样点计算CAN 的波特率不是直接给一个 500k 就完了它由 APB1 外设时钟经过预分频后再按位时间三段的 tq 数量来切分。stm32f407 的 CAN1 和 CAN2 挂在 APB1 上默认系统时钟跑 168MHz 时 APB1 是 42MHz。位时间 同步段固定 1 tq BS1 BS2而波特率 F_APB1 / Prescaler / (1 BS1 BS2)。以 42MHz 的 APB1 为例要得到 500kbps可以取 Prescaler 6得到 TQ 时钟 7MHz再除以 14 就是 500k。14 个 tq 分配成 BS1 11、BS2 2采样点落在 (1 11) / 14 85.7%。这个采样点位置对大多数收发器都足够下面这张表可以直接抄目标波特率CAN_PrescalerCAN_BS1CAN_BS2实际误差125 kbps2411 tq2 tq0250 kbps1211 tq2 tq0500 kbps611 tq2 tq01 Mbps311 tq2 tq0补充一点标准外设库的 CAN_BS1_11tq 和 CAN_BS2_2tq 对应硬件寄存器里的值会做减一处理因此代码里写 11 和 2 时实际硬件是 11 和 2。不要在不理解寄存器位域的情况下随意调 BS1否则采样点会偏到很诡异的位置近节点能通、拉一根 10 米线就疯狂报错。3. 从零搭建 stm32f407 CAN 发送与接收的最小 DEMO3.1 硬件接线F407 引脚、收发器、终端电阻F407 的 CAN 控制器引脚是 TTL 电平不能直接怼到总线上去必须经过 CAN 收发器。CAN1 的 TX 是 PA12、RX 是 PA11CAN2 的 TX 是 PB13、RX 是 PB12。常见的板载收发器型号是 TJA1050 或 SN65HVD230如果是自己飞线收发器的 TXD 接 PA12、RXD 接 PA11收发器的 CANH 和 CANL 之间跨接 120 欧终端电阻注意总线两端各有一个终端电阻不是每个节点都跨。低速短距离调试时只在一端接终端电阻很多时候也能通但不要把这个当生产接法。还需要注意共地。两台通过 CAN 通信的板子如果各自用独立电源GND 必须连起来否则差分信号虽然不依赖 GND但收发器内部电平比较仍需要参考地。常见现象是单独每块板子都用 USB 供电两根板子互相发数据偶尔通偶尔不通用万用表量 CANH 和 CANL 电压又没有明显异常十有八九是地没连好。RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; 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(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource11, GPIO_AF_CAN1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource12, GPIO_AF_CAN1);这段代码的作用是打开 CAN1 外设时钟和 GPIOA 时钟再把 PA11、PA12 从普通 IO 切换成复用功能并连接到 CAN1 外设。GPIO_PinAFConfig 是 F4 系列才有的步骤F1 系列没有这一步。如果漏掉 PinAFConfigGPIO 会保持普通输入输出模式CAN 控制器发出的电平根本出不到引脚上。3.2 CAN 控制器初始化模式、波特率、中断接下来是 CAN 的初始化。注意顺序先调用 CAN_DeInit 复位外设再设置 CAN_InitStructure然后用 CAN_Init 写入之后才能使能中断。中断配置需要放在 CAN_Init 之后因为 CAN_ITConfig 依赖外设已经进入初始化状态。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_11tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 6; CAN_Init(CAN1, CAN_InitStructure);几个关键参数的默认值先说清楚。CAN_ABOM 开成 ENABLE 后控制器进入 Bus Off 状态时能自动恢复不需要程序介入这对无人值守的设备很重要CAN_NART 保持 DISABLE意思是发送失败会自动重传如果遇到总线忙碌硬件会等到总线空闲再次发送CAN_TXFP 为 DISABLE 时邮箱发送顺序由报文 ID 优先级决定而非写入先后顺序这一点在压力测试时会看到帧顺序和调用顺序不一致不是 bug。硬件自动恢复这件事是 CAN 总线 DEMO 与普通 UART 教学 DEMO 的最大区别UART 发坏一帧就丢了CAN 会替你重试直到成功或总线报错。3.3 发送邮箱写法带超时的 CAN_Transmit 循环发送函数不要直接调 CAN_Transmit 就完事它返回的是一个邮箱编号真正发送完成需要查询状态。常见误用是只检查返回值是否等于 CAN_TxStatus_Failed忽略还有 Pending 状态于是连续快速发送时邮箱还没空就把新报文又塞进来最后丢帧。我一般会把发送封装成带超时的循环uint8_t CAN_SendFrame(uint32_t id, uint8_t *data, uint8_t len) { CanTxMsg TxMessage; uint8_t mailbox; uint32_t timeout 1000; TxMessage.StdId id; TxMessage.IDE CAN_Id_Standard; TxMessage.RTR CAN_RTR_Data; TxMessage.DLC len; for (uint8_t i 0; i len; i) { TxMessage.Data[i] data[i]; } mailbox CAN_Transmit(CAN1, TxMessage); while (CAN_TransmitStatus(CAN1, mailbox) CAN_TxStatus_Pending) { if (--timeout 0) { return 1; } } if (CAN_TransmitStatus(CAN1, mailbox) CAN_TxStatus_Failed) { return 2; } return 0; }CAN_Transmit 成功返回邮箱号 0、1 或 2如果三个邮箱全满则返回 CAN_TxStatus_Failed。之后 CAN_TransmitStatus 有三种状态Pending正在发送、Ok成功、Failed由于仲裁丢失或总线错误未发出。超时时间根据波特率调整500kbps 下一帧大约 0.2ms1000 次循环对 while 自旋来说足够宽裕。Failed 状态一定要单独处理它往往意味着 CAN_H/CAN_L 接反、没有终端电阻或者波特率与对端不一致。3.4 过滤器先全开保证每帧都进 FIFO0过滤器配置放在发送封装之后因为一个能收到数据的 DEMO 才算完整。上文已经给了一段全屏蔽配置这里补充它的实际效果CAN_FilterMaskIdHigh 和 CAN_FilterMaskIdLow 都为 0表示屏蔽所有位任何节点发的任何 ID 都会进入 FIFO0。出于调试目的这是合理的但生产环境不要这么做否则总线负载稍高FIFO0 会被无关帧灌满真正关心的数据反而因溢出被丢弃。开启 FIFO0 消息挂号中断的方法如下CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);FMP0 是 FIFO0 的 pending message 中断只有 FIFO0 里有帧时才进中断帧读完自动清掉。这样比 CAN_IT_FMP0 之外的状态中断干净很多不会因为错误状态频繁进中断。3.5 中断接收与回调处理中断服务函数里要做的就是把 FIFO0 里的帧搬出来放到一个环形缓冲区或直接交给协议状态机void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; CAN_Receive(CAN1, CAN_FIFO0, RxMessage); if (RxMessage.IDE CAN_Id_Standard RxMessage.StdId 0x123) { /* 处理下发的控制指令 */ } }CAN_Receive 一次只取 FIFO0 里最早的一帧FIFO 里有几帧就进几次中断吗不是FMP0 中断只会在 FIFO 从空变非空时触发一次所以在中断里读完后要主动检查 CAN_MessagePending(CAN1, CAN_FIFO0)如果还有剩余帧需要继续读。这个细节很容易被忽略一次中断只读一帧后续帧会留在 FIFO 里直到下一次新帧到来低流量时无害但总线上持续有流量时FIFO 溢出会悄悄发生。void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; while (CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); HandleCanFrame(RxMessage); } }用 while 把 FIFO0 里积压的帧一次清空是通用做法。HandleCanFrame 里按 ID 分发到应用层保留原始 ID 和 DLC 便于打印。4. CAN 总线 DEMO 最容易翻车的 5 个参数与排查手段4.1 APB1 时钟源改了波特率表全部失效stm32f407 的默认配置是外部 8MHz 晶振经 PLL 倍频到 168MHzAPB1 二分频得到 42MHz。但如果你的板子用的是 25MHz 晶振或者有人把 APB1 分频改成了 4CAN 外设的输入时钟就不再是 42MHz2.3 节那张表直接作废。这里不是计算错了而是时钟预处理和初始化代码里的 SystemInit 不一致。排查时不要只在 CAN_InitStructure 里找问题先检查 RCC_GetClocksFreq 读出来的 PCLK1 是多少。RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(clocks); printf(PCLK1: %d\n, clocks.PCLK1_Frequency);如果 PCLK1 不是 42MHz按 42MHz 配置的 Prescaler 和 BS1/BS2 组合产出的波特率会偏差百分之十几而 CAN 收发双方的位时间容错只有约 0.5%表现就是一台能发、另一台完全收不到或者能收到但偶发错误帧。修正方法是按实际 PCLK1 反推 Prescaler而不是照抄例程。4.2 回环模式与正常模式的接线差异调试初期我强烈建议先把 CAN_Mode 设成 CAN_Mode_LoopBack这种模式下控制器发出的帧不经过引脚直接在内部被接收不需要收发器也能验证代码。回环模式通过通常只说明初始化、发送、接收这几段代码逻辑是对的并不代表外部总线能通。从回环切到正常模式才第一次真正依赖硬件接线、收发器和终端电阻。切换时唯一要改的只有CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack;同一套发送接收代码不用动这是 LoopBack 模式最大的价值。如果回环通、正常模式不通问题集中在外部电路CANH 和 CANL 是否接反、120 欧电阻是否跨接、收发器供电是否为 3.3V 或 5V。用示波器探头点在 CANH 对 GND 上正常通信时能看到约 2.5V 中间电平和约 1V 摆幅的波形如果完全没有电平变化多半是引脚复用未生效。4.3 过滤器把扩展帧或 RTR 帧拦干净了32 位屏蔽模式过滤时ID 寄存器里不仅包含 ID还包含 IDE 和 RTR 位。很多人配置 CAN_FilterIdHigh 时只填 StdIdmask 用 0xFFFF以为只要 ID 对上就放行实际却把扩展帧或远程帧全部屏蔽了。因为硬件比较的是完整仲裁字段标准帧和扩展帧的 IDE 位不同两个 11 位 ID 相同的帧一个是标准帧、一个是扩展帧在过滤器看来是不同的帧。解决方法是配置过滤器时把 mask 的 IDE 位也置 1例如只关心标准帧数据帧时mask 低 16 位设为 0x1FFF其中 bit 13 是 RTRbit 14 是 IDE这样扩展帧和远程帧不会进入 FIFO。DEMO 阶段干脆把 mask 设为 0x0000 的全局放行方案可以避免这个坑。4.4 8 字节数据与 DLC 的语义边界CAN 2.0 的数据帧长度由 DLC 字段决定最大 8 字节。DLC 小于 8 时剩下的数据字节内容不确定不应假设为 0。很多 DEMO 代码发送时用 memset 把 TxMessage.Data 清零但接收端不能依赖这个行为。DLC 本身只有 4 位值大于 8 时在 CAN 2.0 里是保留位不同控制器的表现不一样。另一个容易踩的点是 RTR 帧的 DLC 表示请求的数据长度不是实际数据长度收到 RTR 帧时 Data 区无意义。DEMO 阶段只处理 DLC 在 1 到 8 之间的数据帧遇到 RTR 直接丢弃即可。if (RxMessage.RTR CAN_RTR_Data RxMessage.DLC 0 RxMessage.DLC 8) { // 合法数据帧 }4.5 从 CAN_ESR 的 LEC 字段判断总线错误总线出问题不要靠猜。bxCAN 的 CAN_ESR 寄存器里有一个 3 位的 LECLast Error Code它记录最后一次错误类型0 表示正常1 是位填充错误2 是格式错误3 是 ACK 错误4 是隐性位错误5 是显性位错误6 是 CRC 错误。ACK 错误最常见帧明明发出去了总线上却一个接收节点都没有发送端在 ACK 槽采样不到显性位于是报 3。uint8_t lec (CAN1-ESR CAN_ESR_LEC) CAN_ESR_LEC_Pos; if (lec 3) { // 总线上没有其他节点在 ACK }还有一种情况是 CAN_ESR 里 TEC 和 REC 计数持续上升。只发不收的设备 TEC 涨到 127 就进入 Error Passive再涨到 255 进入 Bus Off。如果 ABOM 没开Bus Off 后控制器直接躺平必须软复位。看到这种趋势说明总线上只有你一个人在说话或者收发器根本没有把数据回环到 RX 引脚。下面这张表总结了 DEMO 阶段最常见的问题现象根因定位手段回环通外接不通接线、收发器示波器看 CANH/CANL能发不能收过滤器、波特率全屏蔽 检查 PCLK1报 ACK 错误总线只有一个节点接第二块板或回环FIFO 溢出无关帧太多缩小 mask 范围偶发 CRC 错误总线过长、采样点不对降波特率、调 BS15. 用回环模式给 stm32f407 CAN 总线 DEMO 做一次性自测5.1 回环模式下的最小验证路径把 CAN_Mode 改成 CAN_Mode_LoopBack 后不需要接任何外部电路只需要板子和一根 USB 线。初始化流程保持不变GPIO 配置、过滤器、中断都可以留作正常模式下的状态但外部引脚没有波形输出所以调试串口打印是主要观察手段。回环模式验证的接受准则有三条调用 CAN_SendFrame 返回 0等待不超过 10ms 后中断里收到同一帧收到的 StdId、DLC、前两个字节与发送值一致。5.2 自测逻辑发送规约、接收判据、状态码我给 DEMO 写过一个简单的自测函数每次上电自动发一组递增数据收到的帧和发送的帧做字节对比如下uint8_t demo_data[4] {0x11, 0x22, 0x33, 0x44}; uint8_t self_test_once(void) { if (CAN_SendFrame(0x123, demo_data, 4) ! 0) return 1; for (int i 0; i 1000; i) { if (received_flag) { if (received_id 0x123 received_dlc 4 received_data[0] 0x11 received_data[1] 0x22) { return 0; } return 2; } } return 3; }received_flag 在 CAN1_RX0_IRQHandler 里置位这是回环模式自测的关键发送的同时中断里就能收到自己发出的帧完全不需要第二块板。返回 3 说明中断没触发多半是 NVIC 或过滤器没配好返回 2 说明帧回来了但数据不对重点检查 DLC 和字节顺序。5.3 用逻辑分析仪验证回环外的真实波形回环模式验证的是控制器内部逻辑切换到正常模式后还需要验证外部物理链路。用逻辑分析仪抓 CAN_RX 引脚能看到完整的帧波形而抓 CAN_TX 引脚看到的是经过收发器之前的 TTL 信号。判断标准是显性位对应低电平隐性位对应高电平SOF 始终是一个显性位。如果 CAN_H 和 CAN_L 之间有 2V 以上差分电压且波形稳定说明收发器工作正常。另一个实用技巧是把波特率临时降到 125k波形会宽 4 倍逻辑分析仪更容易捕获到完整帧等确认帧格式正确后再改回 500k这样能把物理层问题和位时序问题分开定位。自测完成后把 CAN_Mode 恢复为 Normal再插上外部收发器和第二块板把同样的 DEMO 代码烧到另一块板上将 0x123 改成 0x456两块板就能互发了。这个自测流程我建议保留在工程里后面改过滤器和波特率时写一个条件编译开关随时切回 LoopBack 回归一遍比每次接线查波形快得多。本文还有配套的精品资源点击获取
返回列表