STM32标准库CAN环回例程详解:从GPIO配置到采样点设置
简介基于STM32F103ZET6的CAN总线开发资料包适合嵌入式入门及进阶开发者快速掌握CAN外设的配置流程覆盖从GPIO引脚映射、控制器初始化、报文过滤到中断收发与错误处理的常见操作可用于工业控制、车载网络等分布式应用场景。包内提供完整工程模板和示例代码可直接在Keil等IDE中打开并参考其中对工作模式、位速率、标准帧/扩展帧等关键参数的设定。压缩包共176个文件体积仅1.32MB以C源文件、头文件、编译目标文件、链接脚本及工程配置文件为主同时附有hex固件与调试辅助文件便于对照源码和构建产物排查问题。环回示例采用发送数据直接返回接收端的验证方式无需外部CAN节点即可测试通信链路、过滤器配置和收发逻辑的正确性。已有293人学习下载对希望基于STM32F103系列实际开发CAN通信功能的读者这是一份便于练习和拓展的实用参考。1. 拿到的是一份标准外设库的CAN环回例程一个带28375编号的CAN例程包文件名是CAN总线环回.axf、CAN.uvproj.bak这种Keil老工程的样子里外都透露出这是标准外设库时代写的实验代码不是CubeMX生成、也不是HAL库包装过的工程。这样一份资源的价值在于把bxCAN的寄存器操作摊开了不依赖自动生成代码环回模式下甚至不需要CAN收发器和第二块板子芯片内部就把TX和RX连在一起适合在写应用层之前先验证波特率、过滤器、发送邮箱和接收中断这些底层逻辑到底通不通。这正是排查CAN总线上速率对不上、报文进不来、一上总线就bus off问题的前置练习。后面从GPIO复用一路拆到采样点设置把这套工程的每一处配置讲透。2. 先拆外设GPIO复用、波特率位时序与环回模式2.1 GPIO不是随便配的TX推挽、RX上拉与重映射CAN外设的引脚配置在F103上分两层先选引脚再定时钟。CAN1的默认引脚是PA11RX和PA12TX完全重映射后可在PB8/PB9上工作。PA11/PA12也是USB的DP/DM引脚如果板子上同时引出了USB建议直接重映射到PB8/PB9否则USB接入时CAN会被D上的上拉电阻干扰。void CAN_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap1_CAN1, DISABLE); /* 0: PA11/PA12, 1: PB8/PB9 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; /* CAN1_TX */ GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; /* 复用推挽输出 */ GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; /* CAN1_RX */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; /* 输入上拉 */ GPIO_Init(GPIOA, GPIO_InitStructure); }GPIO的8种工作模式里CAN只用两种TX用复用推挽输出AF_PPRX用上拉输入IPU。TX引脚不能配成开漏开漏输出没有主动驱动显性电平的能力会导致发送时总线上看不到正确的显性位。RX配成浮空输入也能跑但引脚悬空时可能收到随机显性位触发错误帧上拉输入相对稳。GPIO_Speed这里选50MHz不是越快越好而是CAN位时间在1Mbps时翻转周期1us2MHz输出模式驱动能力不足波形边沿变缓采样点附近电平不确定到500kHz速率时2MHz勉强能用但既然外设支持50MHz直接按数据手册推荐值配。重映射时AFIO时钟最容易漏。GPIO_PinRemapConfig操作的是AFIO_MAPR寄存器这个寄存器挂在APB2上不使能RCC_APB2Periph_AFIO时写操作会被忽略程序照常运行但引脚没换过去。判断方法是回读AFIO_MAPR对应位确认或者用示波器量引脚有没有波形。另一个坑是PB8/PB9所在端口与JTAG引脚相邻虽然PB8/PB9本身不与JTAG冲突但若同时用到PA15/PB3/PB4要记得在最早期调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)释放这些引脚。2.2 500kbps波特率是怎么从36MHz分出来的CAN没有独立的波特率寄存器它通过APB1时钟、预分频器和位时间段的组合算出每一位的时长。STM32F103ZET6默认SystemInit后APB1输出36MHzCAN1挂在APB1上。先分频得到时间量子tq再按同步段固定1tq、BS1、BS2三段拼成1个位时间。采样点落在同步段加BS1的边界上也就是在BS2之前采样。这个位置直接决定总线在干扰下的容错能力。波特率公式实际波特率 APB1时钟 / (CAN_Prescaler × (1 CAN_BS1 CAN_BS2))500kbps时预分频与段长度的合法组合不止一种下面四组都能得到500kbps但采样点不同CAN_PrescalerCAN_BS1CAN_BS2CAN_SJW位时间采样点适用场景49tq8tq1tq18 tq55.6%短总线、节点少、实验板66tq5tq1tq12 tq58.3%板级通信95tq2tq1tq8 tq75.0%一般工业总线推荐124tq1tq1tq6 tq83.3%波特率较高时工程里如果照示例抄了第一组环回测试能跑通但放到多节点总线上采样点55.6%意味着采样时刻接近位时间的后沿线缆分布电容、收发器传播延迟稍微大一点采到的电平就可能已经进入下一位。实际配置时按第三组CAN_Prescaler9、BS15tq、BS22tq来写采样率75%是多数工业CAN应用的经验值。CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_5tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 9;参数对应APB136MHz时的500kbps。需要注意APB1是不是36MHz取决于外部晶振和SystemInit的实际行为。用8MHz晶振时系统时钟72MHz、APB136MHz没问题如果板子上是25MHz晶振且没改PLL配置APB1是36.25MHz左右这套参数实际波特率会偏到502kbps左右和其他节点500kbps对不上总线上一帧都发不出去。调这类问题时先确认RCC_GetClocksFreq读出的PCLK1值再动预分频。2.3 环回模式比正常模式多做了什么事环回模式是F103 bxCAN自带的测试模式CAN_Mode_LoopBack会把内部发送路径直接回连到接收路径发出去的帧不经外部总线就进到接收FIFO。关键点是环回时TX引脚仍然有波形输出但RX引脚被内部接管外部收发器即使接了数据也进不来。所以环回测试通过只能证明CAN内核、过滤器、收发中断这条链路是通的不能证明外部的CAN_H/CAN_L差分信号正确。从环回切到正常模式后装上总线收发器、终端电阻、采样点的问题才会暴露。环回还有另一个变体叫静默环回CAN_Mode_Silent_LoopBack它不驱动TX引脚只做内部回环适合需要完全不影响总线的前提下反复测试发送逻辑。这两个模式切换只需改CAN_Mode字段不影响过滤器和其他初始化参数。3. 从初始化到收发标准库CAN驱动的完整调用链3.1 CAN_Init返回失败时先查时钟和初始化模式CAN_Init不是简单往寄存器里写参数它内部会先置INRQ位让控制器进入初始化模式配置完位时序后再清INRQ退出。任何一步不满足函数返回CAN_InitStatus_Failed。最常见的原因是RCC_APB1PeriphClockCmd里没开CAN1时钟或者GPIO时钟没开导致CAN外设复位后一直处于冻结状态。初始化失败后后面所有CAN_Transmit调用都会返回CAN_TxStatus_Failed接收中断也永远不会触发。uint8_t CAN1_Init_With_Retry(uint8_t* err_cnt) { 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 DISABLE; CAN_InitStructure.CAN_NART DISABLE; /* 自动重传开启 */ CAN_InitStructure.CAN_RFLM DISABLE; /* 接收FIFO不锁定 */ CAN_InitStructure.CAN_TXFP DISABLE; /* 按ID优先级发送 */ CAN_InitStructure.CAN_Mode CAN_Mode_LoopBack; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_5tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 9; while (CAN_Init(CAN1, CAN_InitStructure) ! CAN_InitStatus_Success) { (*err_cnt); if (*err_cnt 10) return 0; } return 1; }CAN_StructInit先把结构体每个字段填成复位值再按需覆盖避免漏配字段。CAN_ABOM设ENABLE的意义在于控制器进入bus off状态后自动等待128个11位隐性位并恢复否则bus off之后必须软件干预。CAN_NART置DISABLE是让控制器在发送失败时自动重传这符合CAN协议对可靠性的设计NART置ENABLE则只发一次常用于测试错误帧场景。字段里没有采样点这个选项采样点是BS1和BS2的比值算出来的改任何一段都会改变采样点位置。3.2 过滤器只让0x123进FIFOF103的bxCAN把过滤器组挂在CAN1和CAN2之间ZET6这类大容量型号的过滤器组数量足够覆盖多路CAN应用。每帧CAN报文进入接收FIFO前会先经过过滤器匹配匹配不上的直接丢弃。环回测试里最容易出现的问题是发送成功但接收中断不触发十有八九是过滤器掩码配错把原本要匹配的ID段忽略了。CAN_FilterInitTypeDef CAN_FilterInitStructure; CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; /* 掩码模式 */ CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; /* 32位过滤器 */ CAN_FilterInitStructure.CAN_FilterIdHigh (0x123 5); /* STID[10:0] 放入 bit15:5 */ CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; /* 标准帧IDE/RTR 段置 0 */ CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFFF; /* 高16位全匹配 */ CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; /* 低16位不关心 */ CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);过滤器寄存器里标准帧ID在bit15:5即STID左移5位。0x123左移5位得到0x2460写进CAN_FilterIdHigh。掩码寄存器对应位置1表示该位必须匹配置0表示忽略所以MaskIdHigh0xFFFF要求STID的11位全部相等。MaskIdLow0则对RTR位、IDE位不设限也就是说无论数据帧还是远程帧都能收。想只收数据帧把MaskIdLow调成0x0002可强制RTR0。过滤器组0在掩码模式下只占一组列表模式则能精确匹配两个固定ID但需要占用两个过滤器组。调掩码不是看报文字节而是要知道ID存在寄存器哪个bit区间对照参考手册的CAN_FxR1寄存器布局逐位核对。3.3 发送邮箱申请与CAN_Transmit返回值bxCAN有3个发送邮箱CAN_Transmit做的事是把待发报文装入策略选择的一个邮箱然后请求发送。返回值不是简单的成功或失败而是申请到的邮箱号0、1或2当三个邮箱都被占用时返回CAN_TxStatus_NoMailBox。从函数原型看返回值是uint8_t按邮箱号判断更可靠。发送状态用CAN_TransmitStatus单独查询。CanTxMsg TxMessage; uint8_t mailbox; TxMessage.StdId 0x123; /* 标准ID */ TxMessage.IDE CAN_Id_Standard; /* 11位ID */ TxMessage.RTR CAN_RTR_Data; /* 数据帧 */ TxMessage.DLC 8; TxMessage.Data[0] 0xA5; TxMessage.Data[1] (uint8_t)(tick 8); TxMessage.Data[2] (uint8_t)(tick 0xFF); mailbox CAN_Transmit(CAN1, TxMessage); if (mailbox 2) { while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { /* 等待发送完成实际项目里建议加超时退出 */ } }CAN_Transmit返回值与CAN_TransmitStatus返回值很容易混。前者返回邮箱号后者返回该邮箱的发送状态可能值是CAN_TxStatus_Ok、CAN_TxStatus_Pending和CAN_TxStatus_Failed。等待循环里长时间收不到CAN_TxStatus_Ok常见原因总线没有其他节点应答导致应答错误或已进入bus off状态。环回模式下自身应答不会出现应答错误如果环回测试发送总是卡在Pending优先检查CAN_ESR寄存器里的LEC错误码和BOFF位。三个邮箱都用完时没及时读取接收邮箱或发送完成中断没清标志都会让后续报文排队体现为周期性丢帧。多节点同时抢总线时ID数值小的帧在仲裁场逐位比较中获胜这就是CAN_TXFPDISABLE时按ID优先级发送的实际含义。3.4 接收FIFO中断与NVIC向量名接收路径的配置比发送多两步使能CAN的接收FIFO消息挂起中断再在NVIC打开对应中断通道。F103ZET6的CAN1_RX0中断向量名是USB_LP_CAN1_RX0_IRQn这个命名容易让不熟悉的人觉得是USB中断但它的确是CAN1的FIFO0接收中断。用错向量名会编译报错或用成USB的同一向量导致两个中断互相干扰。NVIC_InitTypeDef NVIC_InitStructure; 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);void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0)) { CAN_Receive(CAN1, CAN_FIFO0, RxMessage); if (RxMessage.StdId 0x123) { /* 业务数据从这里取出DLC 决定有效字节数 */ } } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); }CAN_IT_FMP0是FIFO0消息挂起中断只要有1帧数据进入FIFO0就触发。CAN_Receive从FIFO0读出最早的一帧并同时释放FIFO空间。中断服务函数里建议只做拷贝和数据标记实际解析放到主循环避免长时间占中断。如果同时使能FIFO1中断和FIFO0中断注意区分CAN_IT_FMP1和CAN_GetITStatus的FIFO参数两个FIFO独立计数。接收邮箱溢出时CAN_GetFlagStatus(CAN1, CAN_FLAG_FOV0)会置位这个标志在调试接收丢帧时非常有用很多项目丢帧查不到原因最后发现是FIFO溢出。4. 环回实测用发送返回值、ESR寄存器和报文数据定位问题4.1 环回自测四步判定法环回测试在无硬件条件下就能跑但它的判断顺序有讲究。不是发一帧然后看接收中断就行要按照初始化 → 发送申请 → 发送完成 → 接收触发 → 数据比对五步来看。每一步卡住都有对应排查方向能快速缩小问题范围。配置完调用CAN_Init确认返回CAN_InitStatus_Success后读取CAN1-MSR的INAK位该位置1表示已在初始化模式清0表示已退出并进入正常模式。调用CAN_Transmit返回值必须是0、1或2中的一个。返回0xFF说明三个邮箱全满需检查是否有未完成发送或中断里没清发送完成标志。轮询CAN_TransmitStatus直到返回CAN_TxStatus_Ok。等待CAN_GetITStatus(CAN1, CAN_IT_FMP0)为SET进入接收中断。在接收中断里比对收到的数据与发送的TxMessage.Data是否一致同时比对IDE、RTR、DLC字段。第二步就走不过基本是外设时钟或GPIO配置问题第三步卡住先读CAN1-ESR的LEC位第四、五步不过几乎都是过滤器掩码配置把ID过滤掉了。把问题拆到每一步比逐行读初始化代码高效。4.2 读CAN_ESR而不是猜错误CAN控制器自己会报告错误状态比外部示波器更早暴露问题。ESR寄存器里的LEC字段bit4:6在每次错误事件后更新标准库虽然封装了CAN_GetLastError但直接读寄存器在调试时能一起看到BOFF、EPVF、EWGF这些状态位。void CAN_Print_ErrorState(CAN_TypeDef* CANx) { uint32_t esr CANx-ESR; uint8_t lec (esr 4) 0x07; if (esr (1u 2)) /* BOFF */ { /* bus off需等待协议规定的恢复时间或手动恢复 */ } if (esr (1u 1)) /* EPVF */ { /* 被动错误 */ } if (esr (1u 0)) /* EWGF */ { /* 主动错误警告 */ } switch (lec) { case 0: /* 无错误 */ case 1: /* 位填充错误 */ case 2: /* 格式错误 */ case 3: /* 应答错误 */ case 4: /* 隐性位错误 */ case 5: /* 显性位错误 */ case 6: /* CRC错误 */ default: break; } }环回模式下如果出现应答错误几乎可以断定不是环回模式CAN_Mode被改成了正常模式。出现显性位错误或隐性位错误方向是检查收发器电路或总线上是否有短路。位填充错误和CRC错误在总线干扰大、采样点偏移严重时出现表现为偶发错误帧。bus off后如果CAN_ABOM没使能控制器不会自动恢复后续所有发送全部失败且CAN_TransmitStatus返回一直接近Failed。ABOM使能时恢复过程要在总线上检测到128次11个连续隐性位时间约为100ms级别500kbps期间任何发送请求都会被丢弃业务层要做好重试。4.3 改到正常模式接外部节点断开环回才能测出差分信号环回测通后把CAN_Mode改成CAN_Mode_Normal接上TJA1050这类收发器连到另一块板子或CAN分析仪。此时环回模式的内部回连被解除TX波形通过收发器转换成CAN_H/CAN_L差分信号外部节点发来的帧经过收发器恢复成单端电平进入RX引脚。这步经常出现环回好好的一上总线就废的情况原因顺序排查收发器供电、CAN_H/CAN_L是否接反、两端是否有终端电阻、总线波特率是否有偏差。用示波器量CAN_H和CAN_L相对GND的电平发送显性位时CAN_H约3.5V、CAN_L约1.5V差分约2V隐性位时两个都在2.5V附近。量到恒定的2.5V说明总线上没有节点在发送。如果CAN_H或CAN_L被拉到0V查收发器是否损坏或总线有短路。用CAN分析仪抓帧时核对三件事ID是否与StdId配置一致、DLC是否是8、Data字节序是否按大端传输。CAN本身没有字节序概念但很多上位机工具按大端显示多字节数据字段要确认端序后再判断高低字节是否颠倒。另一个常用验证是把发送程序改成周期发送然后在分析仪上观察报文周期是否稳定周期抖动异常往往意味着发送邮箱竞争或CAN_TransmitStatus轮询超时。5. 采样点、终端电阻与CAN FD环回例程之后要补的三课5.1 采样点75%与SJW的重同步能力第2章推荐过9分频、BS15tq、BS22tq的配置采样点落在75%位置这个位置也决定SJW的实际作用边界。SJW定义的是重同步时最多能挪动多少个tq如果SJW1tq时钟偏差引起的相位误差超过1tq就无法一次纠正到位。CAN标准要求SJW至少1tq上限不超过4tq但还要小于等于BS2。500kbps、9分频时1tq约等于250ns若两个节点的时钟误差累计超过这个值就会出现偶发位错误。多节点总线或使用无源晶振时把CAN_SJW从CAN_SJW_1tq调成CAN_SJW_2tq重同步范围变宽采样点位置不变。5.2 终端电阻与电平测量这个例程包不包含硬件电路但接外部节点时最容易忽略终端电阻。CAN规范要求总线两端各接一个120Ω电阻不是每个节点都接。两个节点都在板子上焊了120Ω总线等效电阻约60Ω用万用表在断电状态量CAN_H与CAN_L之间就能判断60Ω左右说明两端已接120Ω说明只有一端接了开路说明两端都没有。终端电阻缺失时信号在总线末端反射表现为波形过冲和振铃高速率时直接导致位错误。5.3 向CAN FD迁移要留意的接口差异F103的bxCAN只支持CAN 2.0B不支持CAN FD。换到带FDCAN的芯片时寄存器布局和邮箱结构完全变了环回工程的CanTxMsg结构体里的DLC字段最大8字节CAN FD扩展到64字节且DLC编码从线性值改成编码表9到15分别映射到12、16、20、24、32、48、64字节。数据结构不兼容意味着不能把标准库的驱动代码直接拷过去。但采样点、SJW、过滤器掩码这些概念完全通用先把环回工程里的报文收发模型吃透迁移CAN FD时只需要换掉寄存器访问层上层协议和滤波器设计思路直接带走。本文还有配套的精品资源点击获取