简介:基于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的默认引脚是PA11(RX)和PA12(TX),完全重映射后可在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_PP),RX用上拉输入(IPU)。TX引脚不能配成开漏,开漏输出没有主动驱动显性电平的能力,会导致发送时总线上看不到正确的显性位。RX配成浮空输入也能跑,但引脚悬空时可能收到随机显性位触发错误帧,上拉输入相对稳。GPIO_Speed这里选50MHz,不是越快越好,而是CAN位时间在1Mbps时翻转周期1us,2MHz输出模式驱动能力不足,波形边沿变缓,采样点附近电平不确定;到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输出36MHz,CAN1挂在APB1上。先分频得到时间量子tq,再按同步段(固定1tq)、BS1、BS2三段拼成1个位时间。采样点落在同步段加BS1的边界上,也就是在BS2之前采样。这个位置直接决定总线在干扰下的容错能力。
波特率公式:实际波特率 = APB1时钟 / (CAN_Prescaler × (1 + CAN_BS1 + CAN_BS2))
500kbps时,预分频与段长度的合法组合不止一种,下面四组都能得到500kbps,但采样点不同:
| CAN_Prescaler | CAN_BS1 | CAN_BS2 | CAN_SJW | 位时间 | 采样点 | 适用场景 |
|---|---|---|---|---|---|---|
| 4 | 9tq | 8tq | 1tq | 18 tq | 55.6% | 短总线、节点少、实验板 |
| 6 | 6tq | 5tq | 1tq | 12 tq | 58.3% | 板级通信 |
| 9 | 5tq | 2tq | 1tq | 8 tq | 75.0% | 一般工业总线推荐 |
| 12 | 4tq | 1tq | 1tq | 6 tq | 83.3% | 波特率较高时 |
工程里如果照示例抄了第一组,环回测试能跑通,但放到多节点总线上,采样点55.6%意味着采样时刻接近位时间的后沿,线缆分布电容、收发器传播延迟稍微大一点,采到的电平就可能已经进入下一位。实际配置时按第三组CAN_Prescaler=9、BS1=5tq、BS2=2tq来写,采样率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;参数对应APB1=36MHz时的500kbps。需要注意APB1是不是36MHz取决于外部晶振和SystemInit的实际行为。用8MHz晶振时系统时钟72MHz、APB1=36MHz没问题;如果板子上是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进FIFO
F103的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表示忽略,所以MaskIdHigh=0xFFFF要求STID的11位全部相等。MaskIdLow=0则对RTR位、IDE位不设限,也就是说无论数据帧还是远程帧都能收。想只收数据帧,把MaskIdLow调成0x0002可强制RTR=0。过滤器组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_TXFP=DISABLE时"按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分频、BS1=5tq、BS2=2tq的配置,采样点落在75%位置,这个位置也决定SJW的实际作用边界。SJW定义的是重同步时最多能挪动多少个tq,如果SJW=1tq,时钟偏差引起的相位误差超过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时只需要换掉寄存器访问层,上层协议和滤波器设计思路直接带走。
本文还有配套的精品资源,点击获取