☰
STM32F103ZET6 CAN总线通信例程详解:从初始化到回环测试
2026/10/20 12:33:23 网站建设 项目流程

简介:STM32F103ZET6单片机CAN总线通信程序例程,面向单片机开发者与嵌入式学习者,解决CAN协议在STM32平台上的工程实现与调试问题。程序基于官方固件库编写,已在开发板上实际调试通过,较寄存器版本更易理解和移植,适合作为入门CAN通信的学习模板或项目基础。压缩包内含70个文件,以30个头文件与26个C源文件为核心,涵盖固件库配置、外设驱动、主程序及中断服务等模块,另含启动文件、工程配置和备份文件,便于还原完整开发环境;包体仅188KB,轻量且可直接打开工程对照学习。目前已有2542人学习下载,实用性经过验证。读者可获得一套可运行的CAN通信工程模板,参照其中初始化流程、收发函数封装和中断处理逻辑,快速应用到自己的单片机项目中,有效节省协议栈搭建与调试时间。

1. 这个例程解决什么问题:从焊好板子点不亮CAN到3分钟跑通回环

前阵子帮朋友调一块ZET6控制板,CAN收发器焊好了,示波器量TX脚却死活没有波形。查了一圈发现不是硬件问题,是初始化代码里波特率分频和APB1时钟没对齐。这种翻车在CAN调试里太常见了。这个STM32F103ZET6单片机CAN总线通信程序例程,就是把初始化、发送、接收、中断处理这四块最基础也最容易错的部分打包好,让拿到板子的人能直接跑回环测试,验证控制器和收发器是否正常。适合刚接触CAN的嵌入式新手,也适合需要快速验证硬件或抓协议的老手。例程里没有花哨的协议栈,只有一针见血的底层通信逻辑,改起来不用拆东墙补西墙。

2. 硬件准备与技术选型:ZET6的CAN外设接线、时钟树和波特率

2.1 接线:CAN_TX和CAN_RX不是随便接的

F103ZET6这个型号自带两个CAN控制器,例程默认使用CAN1。CAN1发出的数据走PA12,接收数据走PA11。很多人拿到板子第一件事是质疑为什么发送引脚没有输出,结果量的是PA11。开发板上的CAN收发器一般已经和单片机引脚连好,你只需要确认两点:收发器供电是3.3V还是5V,以及总线两端有没有接入120欧终端电阻。终端电阻对于低速短距离测试可以省,但连线超过一米或者要接多个节点时,必须加上。

如果用外部USB-CAN调试盒连接板子,除了CANH接CANH、CANL接CANL,还一定注意两个设备要共地。曾经遇到过调试盒和板子各自用隔离电源,结果报文时好时坏,加了一根GND线之后问题彻底消失。下面是例程对外部接线的最小要求:

信号单片机引脚收发器对应引脚说明
CAN_TXPA12CAN收发器TXD例程内部复用为AF功能
CAN_RXPA11CAN收发器RXD需要配置为上拉输入
GND板子GND调试盒GND多节点必须共地

提示:不要试图用普通GPIO口直接接总线,CAN是差分信号,必须有收发器。

2.2 选型:为什么例程用标准外设库而不是HAL

现在很多新项目开始用HAL库,但这个例程坚持用标准外设库是有原因的。标准库的CAN接口非常直白,CAN_Init、CAN_Transmit、CAN_Receive三个API就能覆盖全部收发逻辑,你不需要理解回调函数注册那一套间接层,拿过来改参数就能用。对于只想要一个能跑通的CAN基础代码的人,标准库其实是更快的路径。

如果你的项目已经在HAL库体系里,也不用推翻重写。HAL的HAL_CAN_Start、HAL_CAN_AddTxMessage和标准库的调用一一对应,中断回调即HAL_CAN_RxFifo0MsgPendingCallback,把本例程里的处理逻辑搬过去即可。例程代码本身是可移植的,核心处理逻辑不依赖库函数的特殊魔法。

2.3 时钟树:APB1的36MHz就是CAN的计时基准

ZET6上电后默认使用内部HSI时钟,但例程在SystemInit里已经把时钟切到外部晶振并倍频到72MHz主频,APB1分频为2,所以APB1外设时钟是36MHz。CAN控制器挂在APB1总线上,它计算波特率用的基准就是这36MHz,不是72MHz。如果你在别的例程里看到同样的CAN配置但通讯不上,先检查APB1分频系数。

CAN波特率公式为 BaudRate = APB1Clock / (Prescaler * (1 + BS1 + BS2)),这里SJW不参与分频计算。例程设置的BS1为4个时间单元,BS2为4个时间单元,加起来一个位时间正好是9个tq(同步段占1个tq),预分频取4,得到36M / (4 * 9) = 1Mbps。这是高速测试的常用配置。如果需要500kbps,保持BS1、BS2不变,把预分频改成8即可。下表列出几种常用参数:

目标波特率预分频PrescalerBS1BS2实际波特率
1Mbps44tq4tq1Mbps
500kbps84tq4tq500kbps
250kbps164tq4tq250kbps
125kbps324tq4tq125kbps

注意:这个计算的前提是APB1为36MHz。如果换用低主频的F103C8,APB1也是36MHz,但若使用RCC配置不同,需要重新计算。在调试时推荐先用低波特率如125kbps验证链路,因为125k对线材和终端电阻的要求低很多。

2.4 普通模式与回环模式:例程默认的回环测试逻辑

例程在回环测试时会把CAN_Mode配置为CAN_Mode_LoopBack,这时CAN控制器的发送数据直接在内部被接收,不需要外部总线。这个模式用来验证单片机和外设配置的正确性。当你接上真实收发器并连到总线时,再把模式改成CAN_Mode_Normal。很多初学的人忽略这个开关,拿着回环模式的固件去点对点通信,当然收不到对方的数据。

普通模式下,发送一个报文必须有至少一个其他节点回ACK,否则发送会一直等总线响应。回环模式下不需要ACK,因为数据根本没到总线。这两种模式的作用和出发点是不同的。例程把模式定义放在can_init.c顶部,改成CAN_Mode_LoopBack还是CAN_Mode_Normal只动一行宏。这样设计是为了让你先确认芯片自身没问题,再挂到真正总线上,少一层排查成本。

3. 工程结构拆解:从启动文件到main.c各管哪一块

3.1 目录结构

先看例程的顶层目录,理清每个文件夹的功能,才能知道改代码该去哪个文件里找。一个典型的ZET6工程大致是这个样子:

CAN_Test/ ├── Core/ │ ├── startup_stm32f10x_hd.s │ ├── system_stm32f10x.c │ └── stm32f10x.h ├── StdPeriph_Library/ │ ├── inc/ │ │ ├── stm32f10x_can.h │ │ ├── stm32f10x_gpio.h │ │ └── stm32f10x_rcc.h │ └── src/ │ ├── stm32f10x_can.c │ ├── stm32f10x_gpio.c │ └── stm32f10x_rcc.c ├── User/ │ ├── main.c │ ├── can_init.c │ ├── can_init.h │ └── stm32f10x_it.c └── project.uvprojx

注意:Core目录里的启动文件是hd后缀,对应高密度Flash芯片。ZET6的Flash是512KB,属于高密度类型,如果你把这个例程往C8T6这类中容量芯片上烧,启动文件就选错了,代码根本跑不起来。StdPeriph_Library就是标准外设库源码,与你业务无关,不要改它。实际需要动手的只有User目录下的文件。

3.2 核心文件职责

文件职责改动频率
main.c主循环,调用发送测试函数和空闲处理高
can_init.c / can_init.hCAN外设初始化、GPIO配置、滤波器配置高
stm32f10x_it.c中断服务函数,接收CAN报文中

main.c里只做三件事:初始化时钟和外设、调用CAN发送测试、在循环里打印状态。can_init.c是真正的核心,波特率、模式、滤波器都在这里。stm32f10x_it.c里的接收中断函数会把报文从FIFO搬到一个全局数组,方便主循环使用。如果以后要加接收缓存,直接改这个中断函数就行。

3.3 从main()到CAN外设的初始化顺序

很多人拿到代码直接编译下载,结果CAN不工作,然后怀疑例程有问题,其实是初始化顺序被打乱了。标准外设库对初始化顺序有严格要求:先使能GPIO和CAN外设的时钟,再配置引脚复用功能,最后初始化CAN控制器和滤波器。如果中间漏掉AFIO时钟的使能,引脚复用配置会被忽略,TX/RX引脚就还是普通GPIO模式。

正确的调用顺序可以总结成下面这段逻辑:

int main(void) { SystemInit(); /* 配置系统时钟到72MHz */ GPIO_CAN_Config(); /* 使能时钟并配置PA11/PA12 */ CAN_Config(CAN_Mode_LoopBack);/* 初始化CAN控制器与滤波器 */ CAN_SendTestFrame(); /* 发送一帧测试数据 */ while (1) { /* 轮询接收标志并处理 */ } }

这里每个函数放到后面再讲,重点是你不要自己去调整顺序。CAN外设和GPIO复用关系紧密,不像UART那样随便开个时钟就行。一旦发现初始化后TX没有波形,不要怀疑芯片坏了,回去检查是不是在GPIO配置前先调了CAN_Init。

4. 核心模块代码解析:一个CAN帧从发送到中断返回的完整链路

4.1 初始化:GPIO复用、CAN模式和滤波器

CAN初始化是所有工作的地基。例程把GPIO和CAN控制器拆成了两个函数,GPIO部分负责把PA11、PA12复用成CAN功能。关键配置如下:

void GPIO_CAN_Config(void) { 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_12; // CAN_TX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11; // CAN_RX GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); }

逻辑说明:CAN1的时钟在APB1总线上,所以使用RCC_APB1PeriphClockCmd;而GPIOA和AFIO在APB2总线上,要用RCC_APB2PeriphClockCmd。TX配置成复用推挽,RX配置成上拉输入,这是标准做法。参数方面,GPIO_Speed取50MHz即可,不必更高。

接着是CAN控制器的模式参数:

void CAN_Config(uint8_t mode) { 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 = mode; // 回环或普通模式 CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_4tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_4tq; CAN_InitStructure.CAN_Prescaler = 4; // 1Mbps CAN_Init(CAN1, &CAN_InitStructure); 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); }

逻辑说明:CAN_DeInit先让外设恢复默认,CAN_StructInit填充非法默认值,避免结构体里残留垃圾数据。ABOM打开很重要,CAN控制器因为总线错误进入离线状态后,可以自动恢复。NART关闭,让发送失败时自动重传。滤波器这里用的是IdMask模式,掩码全0表示接受所有报文,方便测试。如果你只想收某个ID的报文,把掩码改成0x7FF就能精确过滤。

4.2 发送:选择邮箱与状态轮询

发送函数是例程里最直观的部分。它构造一帧标准数据帧,放到发送邮箱,然后等待发送完成:

void CAN_SendTestFrame(void) { CanTxMsg TxMessage; uint8_t mailbox = 0; TxMessage.StdId = 0x001; // 标准帧ID TxMessage.IDE = CAN_Id_Standard; // 标准帧 TxMessage.RTR = CAN_RTR_Data; // 数据帧 TxMessage.DLC = 8; // 数据长度8字节 TxMessage.Data[0] = 0x55; // 填充测试数据 TxMessage.Data[1] = 0xAA; TxMessage.Data[2] = 0x01; TxMessage.Data[3] = 0x02; TxMessage.Data[4] = 0x03; TxMessage.Data[5] = 0x04; TxMessage.Data[6] = 0x05; TxMessage.Data[7] = 0x06; mailbox = CAN_Transmit(CAN1, &TxMessage); while (CAN_TransmitStatus(CAN1, mailbox) != CAN_TxStatus_Ok) { } }

逻辑说明:CAN_Transmit返回的mailbox是几个发送邮箱的编号,值可以是0、1、2,也可能返回错误。用while等待发送完成,在回环模式下很快就能通过,在普通模式下如果总线上没有其他节点,这个循环会一直卡住,因为一直等不到ACK。所以联调时注意是否真的接入了另一个节点。

参数说明:StdId最大支持0x7FF标准帧ID,发扩展帧时换成ExtId并设置IDE=CAN_Id_Extended。DLC可以改成0到8,超过8字节CAN协议不支持。回环测试时,这一帧数据会进到接收FIFO,触发下面的中断。

4.3 接收:中断服务函数与FIFO读取

例程的接收用了中断方式,这样主循环不需要时刻查询是否收到数据,配置比较简单。首先要在CAN_Config里使能FIFO0消息接收中断:

CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);

中断服务函数放在stm32f10x_it.c里:

void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET) { CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); /* 把接收到的数据存到全局变量,供主循环使用 */ g_rx_len = RxMessage.DLC; g_rx_id = RxMessage.StdId; for (uint8_t i = 0; i < RxMessage.DLC; i++) { g_rx_data[i] = RxMessage.Data[i]; } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }

逻辑说明:进入中断后先判断是不是FIFO0消息挂起,然后调用CAN_Receive把数据移到CanRxMsg结构体。注意CAN_ClearITPendingBit必须调用,否则中断标志一直置位,会不断进入服务函数。这里用全局变量g_rx_len、g_rx_data接收数据,主循环直接读就行,没有用环形缓存,比较适合例程的场景。

参数说明:CAN_Receive的第二个参数是FIFO号码,这里用CAN_FIFO0。CanRxMsg里的StdId或ExtId在赋给全局变量时不用再改字节序,芯片已经帮你处理过了。DLC表示实际接收到的数据长度,可能和发送端设置的不一样,始终按它为准。

5. 避坑记录:CAN例程最常见的五个翻车现场与排查

5.1 初始化阶段的坑:不起振、进中断、配置被覆盖

现象一:代码编译下载后,用示波器量不到TX脚波形,CAN总线也无电压变化。原因八成是时钟配置没生效或者启动文件选错。解决:先确认芯片型号是HD高密度,查SystemInit里是否已经把PLL倍频到72M,再量PA12引脚是否复用成CAN功能。复位后先看RCC寄存器是否使能了AFIO时钟。

现象二:回环模式下能收到自己的报文,但一改成普通模式就发送超时。原因:普通模式需要总线上至少一个节点发送ACK,如果只有单板测试,发送会一直等待。解决:接上USB-CAN调试盒,并确认调试盒波特率与单片机一致,还要检查CANH、CANL是否接反。接反时总线没有差分电压,但示波器单端看可能感觉有信号。

现象三:中断服务函数里调用了延时或者复杂函数,导致接收卡死。原因:中断内执行流程过长,嵌套中断或主循环抢占了FIFO。解决:中断里只做CAN_Receive和变量拷贝,数据处理全部放到主循环。如果必须做复杂协议解析,用一个标志位通知主循环处理。

5.2 收发链路的坑:波特率、滤波器和错误计数器

现象四:波特率明明配置对了,但用调试盒收不到数据,或者数据乱码。原因:APB1时钟不是36MHz,很多移植者直接照抄分频参数,没算自己板子的时钟树。解决:用示波器量MCO引脚或调试器看RCC_GetClocksFreq返回的实际时钟值,拿到实际APB1后按公式重新计算预分频。

现象五:报文能够正常进入接收中断,但ID过滤后不该收到的报文也进来了。原因:滤波器掩码设置错误,把MaskIdLow和MaskIdHigh都设成0x0000,这表示掩码全为零,任何报文都能通过,但很多人以为这是不过滤ID的意思,实际它就是不过滤。解决:如果只想收StdId为0x001的报文,将MaskIdLow的低16位设置成0x01FF即可,例如FilterIdLow=0x0001,FilterMaskIdLow=0x01FF。

现象六:发送几次失败后,错误计数一直增长,最终进入BusOff状态。原因:总线上存在短路或波特率不匹配,CAN控制器自动重传累积错误。解决:开启ABOM,然后读取CAN_GetErrorCode排查错误类型,同时检查总线是否有终端电阻。如果只是联调,先降低波特率到125k再试。

6. 二次开发:把例程改造成自定义应用层协议

拿到这个例程,不要急着改成很复杂的CANopen,先定一个小目标:把固定ID和固定数据改成可配置的自定义协议帧。第一步,在can_init.h里增加宏定义,把节点ID、目标ID、数据长度做成可配置项。第二步,在发送函数里,把写死的StdId和DLC改成参数传入,做一个通用的CAN_SendData(uint16_t id, uint8_t *data, uint8_t len)。第三步,把接收中断里的全局变量数组换成带环形缓冲的队列,避免主循环处理慢时丢帧。

改造时有一点必须注意:如果同时使用CAN1和CAN2,CAN2的滤波器初始化有点特殊,它必须复用CAN1的时钟使能,并且滤波器起始号和CAN1有重叠,需要设置CAN_FilterInitStructure.CAN_FilterActivation时区分。ZET6有两个CAN控制器,很多二次开发会想让CAN1发控制指令,CAN2接收状态反馈,这种场景下用CAN2时记得关掉CAN1的FIFO0中断,防止两个控制器都进同一个中断服务函数。

验证方法比代码本身更重要。每次改完波特率或滤波器,先做回环测试,再做单点测试,最后才上多节点。我习惯把测试固件分成两个版本:一个版本定死ID和内容用于自发自收,另一个版本正常发送期待远端节点回应。这样出了问题能迅速分清是物理层、数据链路层还是应用层的问题。

从那以后我每次动CAN相关代码,都会强制走一遍「看时钟树、查波特率公式、回环验证」三步,改完就踏实。这套流程也推荐给你,这个例程只是一个起点,把底层收发跑通了,上面的协议设计才有意义。希望帮到你。

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

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

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

立即咨询