STM32F103 CAN通信实战:协议、硬件配置与调试要点全解析
2026/9/18 18:16:16 网站建设 项目流程

简介:面向嵌入式开发者和STM32初学者的STM32F103 CAN通信完整Keil工程,覆盖汽车电子、工业自动化等场景下的控制器局域网络实现。资源以标准库工程形式提供,包含CAN控制器初始化、报文发送接收、波特率与滤波器配置、错误状态处理等核心代码,可帮助理解差分传输、多主站仲裁、标准ID与扩展ID分配等关键概念。

压缩包内共206个文件,约4.73MB,以.c/.h源码、.o/.d编译中间文件为主。同时提供.uvproj工程、.hex/.axf可执行文件及.map映射文件,可直接编译烧录验证;.lst、.crf等辅助文件也便于查看编译细节。已有1900人学习下载,适合对照学习CAN总线协议、排查通信异常,并进一步扩展多节点组网。 做嵌入式开发的人对CAN总线肯定不陌生,但真正把STM32F103的CAN通信从头到尾跑通,还是有不少细节值得记录。这篇文章就围绕STM32F103的CAN通信,从协议基础、硬件设计、软件配置到常见坑点,把我实际调试中的经验和教训完整梳理一遍,适合正在用F103做CAN节点开发、或者刚接触CAN总线想快速上手的工程师参考。

1. 项目整体思路:为什么是CAN,为什么是F103

1.1 CAN总线解决了什么问题

一个项目里一旦出现多个控制单元,比如电机的控制器、温度采集模块、IO扩展板、显示面板,它们之间怎么通信就成了大问题。如果用串口,点对点连接还可以,多节点就得靠一主多从轮询,实时性和可靠性都比较差。如果用以太网,协议栈复杂度又太高,对MCU的资源要求也大。CAN总线的定位恰好填补了这个空白,它是一根双绞线上的多主通信,节点挂上去就能收发,硬件自动处理仲裁和错误检测,实时性有保障,抗干扰能力也强,特别适合工业控制和车载环境。

STM32F103系列内置的bxCAN控制器支持CAN 2.0A和CAN 2.0B协议,也就是标准帧和扩展帧都支持,而且有3个发送邮箱、2个接收FIFO和28个过滤器。对大多数中小型项目来说,这套硬件资源非常够用,不需要外挂独立的CAN控制器芯片。而且F103的价格、供货、资料生态都非常成熟,拿它做CAN通信的入门和落地平台,几乎是最优解。

1.2 方案选型的关键考量

我见过不少人在选型时纠结要不要用带CAN FD的芯片,这里先泼一盆冷水。F103的bxCAN不支持CAN FD,只能跑经典CAN 2.0,最大8字节数据场、最高1Mbps波特率。如果你的项目确实需要更高的带宽和更大的数据段,直接换G4系列或者其他支持CAN FD的芯片更合适。但反过来,如果是常规的传感器数据上报、控制指令下发、设备状态同步,经典CAN完全够用,没必要为了一个用不上的功能增加复杂度。

还有一个容易被忽略的点:F103的CAN控制器时钟来自APB1,APB1的默认频率是36MHz,而CAN外设需要的是APB1时钟分频后的结果。很多人一上来就抄网上的例程,时钟树没配对,CAN波特率算出来是歪的,通信时断时续。后面我会专门讲波特率计算这块,这是整个CAN通信项目能不能稳定的核心。

2. CAN协议基础与bxCAN硬件结构

2.1 CAN帧结构:先把报文"翻译"成人话

CAN的报文帧分好几种,平时最常用的就两种:数据帧和远程帧,远程帧实际开发中用得很少。数据帧里又分标准帧和扩展帧,区别主要在ID长度上。标准帧的ID是11位,扩展帧是29位,扩展帧是在标准帧基础上多了18位扩展ID。我在实际项目中,如果只是板间通信,一般用标准帧就够了,ID范围0x000到0x7FF,能定义128个不同ID的报文,绰绰有余。

数据帧的结构大概是这样的:帧起始SOF、仲裁段(ID+RTR)、控制段(IDE、DLC)、数据段(0到8字节)、CRC段、ACK段、EOF。要特别注意DLC这个字段,它表示数据场实际有几个字节。发送端如果DLC填的是4,那你后面就算塞了8个字节的数据,对端也只会按4个字节去解析。这个坑我踩过一次,调试时一直觉得丢数据,最后发现是DLC没匹配。

2.2 bxCAN的邮箱、过滤器和FIFO机制

bxCAN的发送侧有3个邮箱,你调用发送函数的时候可以把报文丢到任意一个空闲邮箱里,硬件会按优先级自动发送。如果3个邮箱都满了,再调用发送函数就会返回失败。所以代码里不能只调用一个发送接口就完事了,一定要检查返回值,必要时做重发或者缓存处理。

接收侧有2个FIFO,每个FIFO能存3个完整报文。报文进来之后先经过过滤器,匹配的才进FIFO,不匹配的直接丢弃。这个机制挺重要的,尤其在多节点总线上,如果不对报文做过滤,CPU会频繁地被无关中断打扰,浪费大量时间在处理垃圾报文上。过滤器可以配置成列表模式、掩码模式,标准帧和扩展帧还可以分别过滤,用得好的话对系统性能提升非常明显。

2.3 波特率、位时间与时钟误差

CAN是异步串行通信,没有单独的时钟线,接收方要从数据流里恢复时钟。每个位的传输时间被分成四段:同步段、传播段、相位缓冲段1、相位缓冲段2。采样点就落在相位缓冲段1和2之间的边界上。波特率的计算公式是:

波特率 = 外设时钟 / (预分频值 × (1 + BS1 + BS2))

这里的BS1和BS2都以时间为单位,比如配置成了13个TQ和2个TQ,再加上同步段的1个TQ,一个位就是16个TQ。如果APB1是36MHz,预分频设4,那CAN时钟就是9MHz,每个TQ的时间是1/9MHz = 111ns,一个位16个TQ就是1.777us,算下来波特率约等于562.5kbps。想做到500kbps,可以设预分频4、BS1=13、BS2=2,或者预分频6、BS1=8、BS2=6,不同组合的采样点位置不一样,要根据总线长度和节点数量选择合适的采样点。一般的经验是采样点放在75%到85%左右,既保证余量,又能容忍一定程度的信号畸变。

这里就是"CAN时钟误差"问题的根源。CAN协议允许的位时间误差很小,在1Mbps下一般要求时钟误差不超过0.5%,所以强烈建议使用外部8MHz晶振而不是内部RC振荡器。内部RC在温度变化下的漂移可能达到1%到2%,直接导致高频通信时大量错误帧。如果项目对成本敏感、想用内部晶振,那波特率就不要超过125kbps,或者干脆换支持CAN FD容错更强的芯片。

3. 硬件设计与最小系统搭建

3.1 STM32F103最小系统的几个关键点

F103最小系统很成熟,电源(3.3V)、复位电路、8MHz晶振、BOOT引脚配置,这几个部分基本是标配。电源部分最容易被忽略,CAN收发器在工作时会向总线输出显性电平,瞬时电流比较大,如果3.3V稳压器余量不足,板子在CAN通信时会出现电压跌落,表现为偶发的总线错误。我习惯在CAN收发器的VCC脚旁边放一个100uF的电解电容再加一个0.1uF的瓷片电容,效果比只放一个0.1uF稳定得多。

晶振这块要认真对待,CAN控制器对时钟精度敏感,8MHz晶振的负载电容要按芯片手册推荐值来配,一般在20pF左右。F103的OSC_IN和OSC_OUT引脚之间如果走线太长,可能引入干扰,Layout时晶振尽量靠近MCU,地线包一圈,别偷懒。

3.2 CAN收发器选型与终端电阻

STM32F103的CAN控制器只有一个CAN_TX和CAN_RX引脚,输出的是TTL电平的逻辑信号,不能直接挂到总线上,中间必须加一个CAN收发器。最经典的收发器是TJA1050,还有PCA82C250、SN65HVD230等,功能类似。收发器的TXD引脚接MCU的CAN_TX,RXD引脚接MCU的CAN_RX,VCC接3.3V或者5V,具体看型号。SN65HVD230是3.3V供电的,和F103电源域完全一致,比较省心。TJA1050是5V供电,输出RXD的高电平大概是5V,而F103的GPIO是5V容忍的,也可以直接接,但最好串个1k电阻限流。

总线两端必须接120Ω的终端电阻。这个电阻的作用是吸收总线末端的信号反射,如果漏接了,末端信号会产生振铃,通信距离稍远或者波特率稍高就会出错。调试阶段可以在CAN_H和CAN_L之间直接焊一个120Ω贴片电阻,量产板子要考虑上拉/下拉偏置电阻和共模电感,做好EMC防护。

3.3 电平匹配、5V容忍和ESD防护

F103的GPIO手册上写着"5V容忍",意思是FT引脚可以承受5V电平输入,不用电平转换。但要注意不是所有引脚都支持,PA11和PA12默认就是CAN_RX和CAN_TX,这两个引脚是USB相关引脚,恰好是5V容忍的,所以收发器的5V输出可以直接接到PA11上。当然,为了保险起见,我会在CAN_RX线上加一个钳位电路,防止收发器在上电瞬间的毛刺电压损坏MCU引脚。

总线侧的保护也不能省,CAN收发器面对的是工业现场的长距离线缆,静电放电和浪涌随时可能打进来。我在实际产品外壳上都会在CAN_H、CAN_L对地各加一个TVS管,选型一般是SMBJ15CA之类的双向TVS,响应速度快,能钳位过压。如果环境特别恶劣,再串一个共模电感或者加个小电容滤波,可以有效提高EMC测试的通过率。

4. 软件实现:从固件库配置到收发代码

4.1 CAN外设初始化:时钟、引脚和波特率

这是整个软件部分的基础,一步错步步错。以HAL库为例,初始化顺序是:先把CAN1的时钟打开,配置PA11和PA12为复用推挽输出,然后根据APB1时钟和期望波特率设置预分频和位时间参数,最后使能CAN外设。下面是我实测可用的一段配置代码:

CAN_HandleTypeDef hcan1; void MX_CAN1_Init(void) { hcan1.Instance = CAN1; hcan1.Init.Prescaler = 4; // APB1=36MHz => 9MHz hcan1.Init.Mode = CAN_MODE_NORMAL; // 普通模式 hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; // 同步跳转宽度 hcan1.Init.TimeSeg1 = CAN_BS1_13TQ; // 采样点在87.5% hcan1.Init.TimeSeg2 = CAN_BS2_2TQ; hcan1.Init.TimeTriggeredMode = DISABLE; hcan1.Init.AutoBusOff = DISABLE; hcan1.Init.AutoWakeUp = DISABLE; hcan1.Init.AutoRetransmission = ENABLE; // 出错自动重发 hcan1.Init.ReceiveFifoLocked = DISABLE; hcan1.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan1) != HAL_OK) { Error_Handler(); } }

这段代码配置出来的波特率大约在562.5kbps,想精确到500kbps,可以把Prescaler改成5,BS1=9,BS2=6,或者Prescaler=4,BS1=13,BS2=2,两者采样点略有差异。需要注意,HAL_CAN_Init必须在时钟树配置完成之后调用,否则分频值是以默认时钟算的,出来的波特率完全不对。

4.2 发送报文:别忽略返回值

发送一个CAN报文用HAL_CAN_AddTxMessage函数,参数包括句柄、发送头结构体、数据数组和邮箱编号。这里的发送头结构体一定要认真填,IDE字段决定是标准帧还是扩展帧,RTR决定是不是远程帧,DLC是数据长度。我封装了一个发送函数,项目里所有报文都走这个口,便于统一管理优先级和重发机制:

uint8_t CAN_SendData(uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef txHeader; uint32_t mailbox = 0; uint8_t buf[8] = {0}; if (len > 8) len = 8; memcpy(buf, data, len); txHeader.StdId = id; txHeader.IDE = CAN_ID_STD; txHeader.RTR = CAN_RTR_DATA; txHeader.DLC = len; if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, buf, &mailbox) != HAL_OK) { return 1; // 发送失败,邮箱已满或总线忙 } return 0; }

实际测试下来,轮询调用这个函数,再配合HAL库内部的发送完成中断,可以做到大概每1ms发送一帧标准报文,速度完全够用。要注意的是,如果总线一直处于忙状态,AutoRetransmission使能的情况下,HAL_CAN_AddTxMessage有可能一直占用邮箱,程序里要有超时保护,不然会出现"看似发送成功、实际卡死"的情况。

4.3 接收报文:中断 + FIFO回调

接收比发送简单,因为硬件有FIFO自动缓存,我们要做的就是把FIFO里的数据及时读走,防止溢出。推荐使用接收中断,在NVIC里使能CAN1_RX0_IRQn,然后在中断处理函数里读取报文。HAL库的做法是重写这个回调函数:

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8] = {0}; uint32_t fifo = CAN_RX_FIFO0; HAL_CAN_GetRxMessage(hcan, fifo, &rxHeader, rxData); // 这里根据rxHeader.StdId分发到不同处理逻辑 ProcessCanMessage(rxHeader.StdId, rxData); }

回调函数在中断上下文里执行,不要做耗时操作,比如打印日志或者处理复杂的业务逻辑,只把收到的数据拷贝到全局环形缓冲区,再到主循环里消费。这个结构处理多路报文非常方便,PID控制、状态上报、参数读写都可以通过不同ID区分。

4.4 过滤器配置:让CPU只处理关心的报文

如果总线上有多帧不同ID的数据,但某节点只关心其中某几个ID,就用过滤器把无关报文挡在门外。bxCAN的过滤器组可以配置成掩码模式,下面这段代码配置过滤器0,只接收ID为0x111和0x222的标准帧:

CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = (0x111 << 5) & 0xFFFF; // 第一个ID filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x7FF0 | 0x0020; // 掩码 filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan1, &filter);

掩码模式的规则是:掩码位为1表示这一位必须匹配,0表示任意。如果只想接收0x111和0x222两个ID,把掩码设成0x7FF,那么所有扩展位都会被过滤掉,只保留11位标准ID中完全等于0x111或0x222的帧。过滤器配置不复杂,但经常有人因为掩码计算错误导致收不到报文,调试时用CAN分析仪抓一下总线上实际跑的帧,再反推过滤器参数,效率高很多。

5. 实测与调试:从回环模式到双机通信

5.1 回环模式自测:不接总线也能验证代码

在写正式的双机通信之前,先用回环模式自测一把很有必要。把初始化里的Mode改成CAN_MODE_LOOPBACK,然后调用发送函数,自己发的报文会直接回灌到自己的接收FIFO,不需要接任何外部设备。这个模式用来验证时钟配置、过滤器、接收中断是否正常,非常方便。

回环模式跑通之后,我一般会在接收回调里加一个计数器,每秒钟通过串口打印一次接收到的帧数。如果发送100帧,接收计数也是100,那基本可以确定软件链路是通的,可以放心进入下一步联调。

5.2 双节点通信与CAN分析仪

双节点联调时,最好有一台CAN分析仪,不管是周立功的CAN盒还是创芯科技的USB-CAN适配器,都能把总线上的报文实时抓出来看。我第一次调双节点时,程序写得明明没问题,但两个板子就是不通,用分析仪一抓,发现其中一个节点一直在发送错误帧,拿错误帧的波形去对,才发现这个板的时钟配置错了,波特率实际只有460kbps,另一个是500kbps,两边不同步,自然收不到。

还有一次遇到的问题是报文内容对不上。发送端明明发的0x01,接收端显示0x08,后来发现是CAN矩阵的字节序问题。上位机和MCU的字节序不一致,导致多字节数据在解析时高低字节颠倒了。这里我要重点强调,CAN报文是串行发送的,一个字节内的位序是从高到低,跨多个字节时没有统一规定,具体要看协议矩阵的定义,比如Motorola格式和Intel格式解析出来的结果就完全不同。调试多字节参数时,先拿一帧固定的数据去验证你的解析函数,别一上来就解析完整协议。

5.3 帧间隔与总线利用率观察

用CAN分析仪观察双节点通信时,还可以留意帧间隔和总线利用率。总线上如果同时只有两个节点各发一帧,总线利用率通常不到1%,完全没压力。但如果有多个节点频繁发送,比如说10ms发一帧,200ms发一帧,总线利用率依然很低,CAN总线的带宽余量非常充足。真正需要注意的是一帧CAN报文的最小时间间隔。以500kbps为例,一帧标准帧大概需要130us左右,满负荷时1秒能发送大约7000帧,实际项目中这个数字远达不到,因为应用层还需要处理逻辑。

6. 常见问题与排查技巧实录

6.1 波特率误差和重同步导致的偶发失败

CAN通信偶发失败的原因里,最少见但最难查的就是波特率误差。两个节点的波特率如果差得不大,CAN协议还能靠重同步机制勉强维持通信,但误差一大就会开始出现错误帧。我测试过一个板子,4个节点里3个正常,1个丢包严重,最后测了它的APB1实际时钟,发现因为晶振旁边的负载电容虚焊,频率偏了0.7%,CAN就受不了了。排查建议:用逻辑分析仪抓CAN_TX引脚的波形,数一下一个位的时间,反推实际波特率,几秒钟就能发现偏差。

重同步还有个特性是同步跳转宽度SJW,它表示接收方一次最多能调整多少个TQ来补偿相位误差。一般来说SJW设1个TQ就够了,但如果总线上节点数多、线缆长,可以试着加大到2个TQ,增加容错能力。不过SJW加大也会降低采样点的抗噪声能力,别盲目调大。

6.2 内部晶振模式下能不能跑CAN

搜资料时经常有人问F103能不能用内部晶振跑CAN,我的结论是:能跑,但只建议在低速下跑。HSI的内部RC振荡器精度在25℃时大约是1%,但温度漂移以后可能到2%以上。CAN协议规定位时间的最大误差在0.5%以内,用内部晶振很难满足。如果非要省掉外部晶振,可以把CAN波特率降到125kbps以下,然后用分析仪实测一段时间,看看错误帧计数能不能接受。很多量产项目就是这么干的,前提是波特率确实不高、节点数少。

6.3 低功耗Stop模式下的CAN唤醒问题

F103的停机模式(STOP mode)我也踩过坑。停机模式下,外设时钟全部关闭,CAN模块不工作,外部CAN总线上的活动无法直接唤醒MCU,除非开启了CAN的自动唤醒功能并且选用外部中断方式。在没有硬件唤醒信号的设计里,我一般用CAN收发器的RXD脚接一个外部中断输入,当总线有数据变化时,RXD的电平翻转触发EXTI中断,把MCU从STOP模式唤醒,再重新初始化CAN外设。这个思路简单可靠,但要记住唤醒之后必须重新配置CAN的邮箱和过滤器,否则收发功能恢复不了。

6.4 工具链兼容性和调试辅助

调试CAN项目时,上位机分析软件的选择也很影响效率。周立功的CANTest配合他家的CAN盒非常好用,但偶尔会遇到在Windows 11下驱动不兼容的问题,官方往往要更新驱动版本。创芯科技的USB-CAN设备在多数系统下免驱,上手更快。如果是在Linux环境下做自动化测试,python-can库值得试试,我经常写个几十行的Python脚本,通过USB-CAN设备回放和校验报文,比手动用分析仪点来点去高效得多。

另外提醒一句,进行总线联调时,测试代码里尽量加上错误状态寄存器(CAN_ESR)的读取,出问题第一时间看错误计数器和上次错误码,能直接定位到是位填充错误、CRC错误还是格式错误,省去大量盲猜的时间。

小结与个人体会

我在STM32F103上做CAN通信已经有好几个项目了,从最初照着网上例程改成自己的协议,到后来整个通信层完全自己设计,最大的体会是:CAN看着简单,真正稳定运行还是要把每个底层细节吃透。波特率算对了、硬件连接规范了、过滤器和FIFO用好了,协议栈反而成了最简单的部分。如果刚开始接触,建议从标准帧、500kbps、两个节点加一个CAN分析仪的组合开始,一步步把代码和工具链跑熟,再挑战高波特率、多节点、和低功耗场景。CAN总线作为一个老而弥坚的通信方案,在嵌入式领域依然值得投入精力掌握。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询