FEATURED · 精选文章

STM32F103 CAN通信开发实战:从初始化到双机联调

发布时间 / 2026/9/1 22:34:01
来源 / 创域科博编辑部
栏目 / 资讯中心
STM32F103 CAN通信开发实战:从初始化到双机联调 简介本资源是一套面向嵌入式初学者与STM32进阶开发者的CAN通信实战代码包聚焦STM32F103系列芯片的CAN总线驱动实现与协议理解解决CAN回环测试、双节点对接等典型应用场景下的配置难点与调试痛点。压缩包共95个文件涵盖57个头文件含寄存器定义、接口声明与配置宏、31个C源文件含CubeMX生成的底层初始化及可移植的CAN驱动逻辑、1个启动汇编、1个链接脚本、1个Makefile构建文件及PDF原理图等整体大小1.86MB结构清晰分为Drivers、Core、Config等模块便于工程迁移与二次开发。已有2691人学习下载配套《STM32专栏》知识点提供完整时序分析、错误处理机制与实测验证逻辑驱动层代码高度解耦可直接复用于其他ARM Cortex-M平台显著降低CAN通信功能集成门槛。 写《STM32基础》这个系列的时候我一直没急着碰CAN。原因是它比串口和SPI多了一层协议门槛——代码本身不难难的是把总线上的电气特性、仲裁机制、位时序这些概念和代码对应上。这篇是系列的第五篇我把STM32F103的CAN通信从初始化到收发、从回环自测到双机联调完整跑了一遍整理出一份可以直接落地的工程代码同时也把几个容易踩的坑单独拿出来讲清楚。如果你之前只玩过UART和SPI这篇文章会帮你把CAN的整个链路打通为什么需要终端电阻、波特率怎么算、滤波器怎么配、报文怎么发怎么收。不管是准备做CAN节点设备、调试工控板还是纯粹想把STM32的外设学全这篇都适用。1. CAN总线与STM32F103先搞清楚它在项目里的位置1.1 什么场景下你会真正需要CAN很多初学者对CAN的第一反应是我串口都够用了为什么还要学CAN。这个想法不能说错但取决于项目规模。CAN最典型的应用场景是汽车电子和工业控制比如一条总线上挂了十几个传感器和执行器它们之间还要互相通信。如果用UART做多点互联要么主从轮询、要么一组线对应一个设备物理接线和协议处理都很痛苦。CAN总线最大的特点是多主架构也就是说总线上任何一个节点都能主动发起通信不需要主机来仲裁。再加上差分信号传输抗干扰能力比单端信号强不少在电机、变频器这种噪声环境里很稳。我实际测试过CAN在1Mbps下跑十几米线缆比同等条件下的RS485省心很多。从嵌入式学习路线来看STM32F103内置的CAN外设是经典中的经典网上方案多、参考代码多、踩坑记录也全非常适合作为学习CAN协议的入门硬件。你手上的F103开发板几乎都带CAN收发器好好利用它。1.2 STM32F103的CAN外设资源到底能做什么STM32F103的CAN外设叫BXCAN是Basic Extended CAN的缩写也就是基本扩展CAN实现了CAN 2.0A和CAN 2.0B协议。它支持标准帧11位ID和扩展帧29位ID数据段最长8字节。具体资源上F103的CAN控制器包含3个发送邮箱、2个接收FIFO每个FIFO可以缓存3个报文、14个滤波器组。发送邮箱解决的是想发的时候没有缓存的问题3个邮箱意味着软件可以同时提交最多3帧硬件会自动按优先级排队发送。2个接收FIFO配合14个滤波器组可以用来做报文过滤比如整车CAN总线上有好多ID你可以只接收自己关心的那几类帧其余的直接丢弃这能大幅降低MCU的处理负担。有一点要提醒普通STM32F103比如C8T6、ZET6只有CAN1没有CAN2。只有STM32F105/107这种互联型产品才带双CAN。另外F103的CAN不支持CAN FDCAN FD是高波特率、可变数据长度的扩展协议如果你要用CAN FD就得换支持FDCAN外设的芯片比如STM32G4系列。这个点很多教程不会说但选型时很重要。2. CAN协议核心概念不搞懂这些代码就是抄了个寂寞2.1 帧结构标准帧和扩展帧的位域拆解CAN总线上传输的基本单位是帧。一个完整的数据帧由起始位、仲裁场、控制场、数据场、CRC场、应答场和帧结束构成。仲裁场里的ID决定了这个帧的优先级ID数值越小优先级越高这是CAN总线调度的核心。标准帧的ID是11位扩展帧是29位。在STM32的代码里标准帧和扩展帧的区别主要在两个地方一个是发送结构体里IDE字段是CAN_Id_Standard还是CAN_Id_Extended另一个是ID要放在StdId还是ExtId里。很多初学者直接把一个32位ID填进StdId发出去之后对方解析出来的ID对不上就是因为没搞清楚11位和29位的差别。还要知道的就是RTR位用来区分数据帧和远程帧。远程帧不携带数据它请求对端节点发送对应ID的数据帧。实际项目里远程帧用得不多但你至少要认识它因为调试工具抓包时看到没有数据段的帧别一脸懵。数据场最多8字节这是CAN 2.0的限制。不要超过8发送函数里我会做长度保护但你自己写代码时也要有这个意识。2.2 总线仲裁为什么多个节点能同时发CAN是总线型拓扑所有节点共享一对差分线。如果两个节点同时发送不同ID的数据帧总线上一旦出现冲突就要有一个机制决定谁能占用总线这就是仲裁。CAN的仲裁机制和以太网的冲突检测完全不同它是在发送过程中逐位进行的。显性位逻辑0能够覆盖隐性位逻辑1所以当一个节点发送隐性位、另一个节点发送显性位时发送隐性位的节点会检测到总线电平与自己发送的位不一致立即停止发送转为接收状态。发送显性位的节点继续发送直到整帧发完。因为仲裁是按ID从高位到低位逐位进行的所以ID数值越小在仲裁中获胜的概率越大。这个机制带来的好处是没有额外的仲裁帧开销发送者在比特级就完成了冲突裁决利用率非常高。在代码层面你不需要管仲裁逻辑硬件会自己处理但理解这一点有助于你理解为什么ID分配很重要以及为什么CAN总线在多节点并发时不会像串口那样数据乱掉。2.3 波特率与采样点为什么波特率算对了还是不通CAN的波特率不是随便填一个数就能用的它由位时序决定。一个位时间分成四段同步段SYNC_SEG、传播段、相位缓冲段1BS1和相位缓冲段2BS2。其中同步段固定为1个时间量子TQBS1和BS2可以配置。波特率公式是波特率 CAN时钟频率 / (预分频值 × (1 BS1 BS2))采样点位置是采样点 (1 BS1) / (1 BS1 BS2) × 100%F103的CAN挂载在APB1总线上。默认情况下F103的系统时钟是72MHzAPB1分频2所以APB1的实际时钟是36MHzCAN外设的时钟源也是36MHz。如果你用的是自己写的时钟初始化或者换了个晶振这个数值必须跟着变否则波特率全错。我常用的配置是500Kbps参数如下波特率预分频值BS1BS2实际波特率采样点1Mbps38tq3tq36M / (3×12) 1M75%500Kbps95tq2tq36M / (9×8) 500K75%250Kbps185tq2tq36M / (18×8) 250K75%125Kbps365tq2tq36M / (36×8) 125K75%100Kbps455tq2tq36M / (45×8) 100K75%采样点建议设置到70%到80%之间我一般习惯75%。采样点太靠前或太靠后在总线较长或者多个节点时钟略有偏差时容易出现偶发通信错误。3. 核心代码实现一个完整的CAN收发例程3.1 引脚、时钟与GPIO初始化STM32F103的CAN1引脚映射到PA11CAN_RX和PA12CAN_TX。这里经常有人搞混把PA9和PA10拿来当CAN收发引脚。PA9和PA10是USART1的TX和RX不是CAN这一点在接线前先确认清楚。GPIO初始化需要注意CAN_RXPA11配置为上拉输入CAN_TXPA12配置为复用推挽输出速度建议50MHz。同时要打开GPIOA和AFIO的时钟还要打开CAN1的时钟。CAN1挂在APB1上对应的时钟使能函数是RCC_APB1PeriphClockCmd。GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);如果你用的是带CAN收发器的开发板PA11和PA12已经通过板上收发器接出来了直接找CANH和CANL接线就行。如果是自己画的板子记得在PA12和PA11到收发器TXD、RXD之间不要加太大的串联电阻否则会影响信号质量。3.2 CAN初始化模式、波特率、采样点配置CAN外设初始化用CAN_InitTypeDef结构体里面字段比较多每个字段背后都是一组寄存器位。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; // 接收FIFO锁定模式 CAN_InitStructure.CAN_TXFP DISABLE; // 发送优先级由ID决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; // 同步跳转宽度1tq CAN_InitStructure.CAN_BS1 CAN_BS1_5tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 9; // 36MHz / (9×8) 500Kbps CAN_Init(CAN1, CAN_InitStructure);几个字段的作用说一下。CAN_ABOM是自动离线管理如果CAN节点出错次数太多进入了bus-off状态硬件会自动恢复。这个建议打开否则节点一旦离线软件不干预就永远无法通信了。CAN_NART如果设置成DISABLE表示自动重传发送失败时会自动重发对提高可靠性有帮助。CAN_TXFP决定发送邮件的优先级规则DISABLE表示按报文ID优先级发送ID越小越优先。CAN_SJW是重同步跳转宽度它不参与波特率计算但会影响节点在总线时钟偏差时的调整能力一般设为1tq即可。要特别注意这里的BS1、BS2和预分频值必须和你计算波特率用的参数一致否则采样点和波特率会出现偏差。CAN_StructInit的作用是把结构体里所有字段填成默认值然后你再改自己要改的部分。初学者容易漏掉调用的就是它漏了之后有些字段是随机值初始化可能失败。3.3 发送函数邮箱机制与代码逻辑发送一帧数据的流程分三步填充CanTxMsg结构体、调用CAN_Transmit申请邮箱、等待发送完成。标准库的CAN_Transmit返回值不是简单的成功失败而是返回被分配到的邮箱号0、1、2。只有在3个邮箱都用完时才返回CAN_TxStatus_NoMailBox0xFF。所以这里的判断一定要用CAN_TxStatus_NoMailBox而不是想当然地去判断0。uint8_t CAN_SendData(uint32_t stdId, uint8_t *data, uint8_t len) { CanTxMsg TxMsg; uint8_t mailbox; uint16_t timeout 0xFFFF; uint8_t i; if (len 8) { len 8; } TxMsg.StdId stdId; TxMsg.IDE CAN_Id_Standard; TxMsg.RTR CAN_RTR_Data; TxMsg.DLC len; for (i 0; i 8; i) { TxMsg.Data[i] (i len) ? data[i] : 0; } mailbox CAN_Transmit(CAN1, TxMsg); if (mailbox CAN_TxStatus_NoMailBox) { return 0; } while (CAN_TransmitStatus(CAN1, mailbox) ! CAN_TxStatus_Ok) { if (--timeout 0) { return 0; } } return 1; }这里我做了一个细节处理把TxMsg.Data的8个字节全部清零后再填充有效数据。虽然DLC指定了长度硬件只发送DLC范围内的字节但清一下可以避免调试时栈里残留的脏数据干扰分析。发送等待这里加了一个超时保护因为如果总线上没有其他节点应答或者终端电阻没接发送可能会卡住。如果想要周期性地发某一帧直接在主循环里调用这个函数就行。比如uint8_t data[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; CAN_SendData(0x123, data, 8);3.4 接收代码滤波器配置与中断处理接收部分我建议用中断方式效率高不占用主循环。接收前要配置滤波器和中断。滤波器的作用是决定哪些报文能进入接收FIFO。初期调试时最简单的方式是配置成接收所有报文也就是32位掩码模式、掩码全0。掩码全0意味着不关心ID的每一位任何进来的帧都接收。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_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure);初始化完滤波器后使能FIFO0的报文挂起中断并配置NVIC。F103的CAN1_RX0中断向量对应标准库的中断函数名是USB_LP_CAN1_RX0_IRQHandler这个名称和启动文件里的向量表必须一致。CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); 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_Receive从FIFO0读出一帧。void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMsg; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) ! RESET) { CAN_Receive(CAN1, CAN_FIFO0, RxMsg); if (RxMsg.IDE CAN_Id_Standard) { // 标准帧ID在RxMsg.StdId } else { // 扩展帧ID在RxMsg.ExtId } // 数据处理比如存到全局变量里或者加一个标志位 CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }有个容易忽略的点CAN_Receive读走一帧后FIFO的报文计数会自动减一但CAN_GetITStatus的中断标志位通常还会保持所以中断函数里调完CAN_Receive后最好显式清一次挂起位。有些工程师不写这行也偶尔能跑但一旦连续进中断就可能丢帧。如果你不想用中断也可以在主循环里轮询if (CAN_MessagePending(CAN1, CAN_FIFO0) 0) { CAN_Receive(CAN1, CAN_FIFO0, RxMsg); // 处理 }两种方式各有用处中断适合强实时场景轮询适合主循环本来就很空闲的程序。我建议初学阶段先用中断因为后续你要做协议栈时中断方式更容易扩展。4. 调试实战从回环自测到双机互联4.1 第一步回环模式自测调CAN代码我强烈建议第一个阶段不要接任何外部设备直接用回环模式CAN_Mode_LoopBack。在这个模式下CAN控制器的发送输出会在芯片内部直接连接到接收输入不需要外部收发器不需要总线接线你发送的数据帧会自动进入自己的接收FIFO。回环模式的配置很简单把3.2节里的CAN_Mode改成CAN_Mode_LoopBack其他不变。然后在主循环里调用发送函数同时在接收中断里把收下来的ID和数据通过串口打印出来。如果串口能看到自己发的那一帧说明GPIO配置、CAN初始化、波特率计算、中断配置都是正确的。这一步能帮你把问题和硬件环境隔离。很多人在这一步都没跑通就开始怀疑收发器、怀疑线缆、怀疑对端设备结果查了一圈发现是初始化代码里某个字段配错了。先跑回环能省去一大半没必要的排查。回环测试通过后再把模式改回CAN_Mode_Normal。注意回环模式下即使不接负载也能正常发送但正常模式下必须有总线上的ACK应答才能发完一帧。如果你总线另一端什么都没有发送会一直报错这是正常现象。4.2 第二步双机/连接CAN分析仪的调试回环通了之后找一个USB-CAN分析仪比如周立功的USBCAN系列或者常见的创芯CAN分析仪把开发板的CANH接到分析仪的CANHCANL接到CANL两端各接一个120欧姆终端电阻。在PC端打开调试软件设置和开发板相同的波特率比如500Kbps然后点击启动。开发板往总线上发数据你就能在软件里看到一帧一帧的报文包括ID、DLC、数据字节、帧类型。同时你也可以用分析仪往总线上发数据看开发板的接收中断能不能收到。这里我建议不只看还要学会对报文。比如程序里发送的是CAN_SendData(0x123, data, 8)如果你在分析仪里看到ID为0x123、数据和你填的一样那就完全正常。如果ID对不上多半是发送结构体里StdId和IDE配置不一致。如果是两块开发板互相通信注意两个节点必须共地也就是说两个板的GND要连在一起。CAN的差分信号虽然不需要像数字信号那样严格共地但完全不共地时收发器可能会工作异常。4.3 终端电阻为什么是120欧姆终端电阻是CAN调试里最容易被忽略又最容易踩坑的点。CAN总线规范要求在总线最远的两端各接一个120欧姆终端电阻作用是把总线上传输到两端时产生的反射降低保证信号质量。需要特别注意的是是两端各一个不是每个节点都接。在只有两个节点的简单场景里就是两个节点各接一个120欧姆电阻。如果一条总线上挂了5个节点也不是每个节点都加而是在物理上处于总线最远端的两个节点上加。你可以用万用表在断电状态下量CANH和CANL之间的电阻。如果总线两端都接了120欧姆测出来应该是60欧姆左右两个120并联。如果测出来是120欧姆说明只有一端接了电阻如果接近0或者无穷大说明接法有问题。这个测量口诀特别实用我从一开始做CAN就一直在用。还有一种情况是用USB-CAN分析仪调试时分析仪本身已经内置了终端电阻你再接一个120欧姆就等于两个电阻并联最后总线等效电阻小于120欧信号质量反而会变差。所以接设备前先查一下你的工具是否带终端电阻可以用万用表测一下分析仪的CANH和CANL如果已经60欧姆左右说明它内部已经并了两个电阻外部就不需要再接了。5. 常见问题与排查思路整理5.1 发送失败TME位一直不置位的几种可能发送失败最常见的表现是调用发送函数后超时返回或者CAN_TransmitStatus一直停在CAN_TxStatus_Pending。这个现象背后通常是总线错误导致报文无法发送成功。我的排查顺序是先看回环模式能不能通过如果回环都发不出来问题在MCU配置本身去查GPIO和CAN初始化。如果回环正常但正常模式发不出问题在硬件链路用万用表量CANH和CANL之间有没有60欧姆左右的总线电阻。然后把收发器TXD、RXD两根线确认一遍尤其是自制的板子PA12应该接收发器TXDPA11接RXD接反了也会发不出。还有一种情况是芯片进入了bus-off状态。读一下CAN_ESR寄存器的LEC位可以看到错误类型。如果反复出现显性位错误或者位填充错误先把CAN_ABOM打开让硬件自动本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻