RT-Thread下STM32F103 CAN总线驱动配置与工业应用实践

📅 2026/8/19 15:06:21
RT-Thread下STM32F103 CAN总线驱动配置与工业应用实践
1. 项目缘起为什么要在RT-Thread上折腾STM32F103的CAN最近在做一个工业数据采集节点的项目主控芯片选用了经典的STM32F103ZET6通信接口上除了常规的串口还必须支持CAN总线。原因很简单现场环境里PLC、变频器、传感器这些设备很多都靠CAN总线互联它抗干扰能力强传输距离远多主仲裁的机制也适合分布式控制。一开始我打算用裸机开发自己写一套CAN驱动和简单的任务调度。但转念一想设备后期功能肯定会增加比如远程固件升级、文件系统记录运行日志、更复杂的网络协议栈等裸机那套状态机轮询的架构后期维护会是个噩梦。这时候RT-Thread就进入了我的视线。它是一个国产的、开源且组件丰富的实时操作系统内核小巧但生态完整。最关键的是它的BSP板级支持包对STM32系列的支持非常成熟特别是F1系列这种“老兵”驱动完善社区资料也多。在RT-Thread上使用CAN意味着我可以直接调用官方或社区维护好的、经过验证的CAN设备驱动框架不用再从寄存器层面去配置波特率、过滤器这些底层细节能把精力集中在应用逻辑上。而且RT-Thread的ulog日志组件可以轻松对接文件系统把CAN通信的收发状态、错误帧记录下来这对于现场调试和故障排查简直是神器。基于这些考虑我决定将项目迁移到RT-Thread平台核心任务就是搞定STM32F103ZET6的CAN通信。这个组合——RT-Thread STM32F103 CAN——看似平凡却是很多嵌入式工程师从单片机裸机开发转向RTOS实战的经典跳板。它涉及RTOS的基本使用、设备驱动框架的理解、以及工业现场总线通信的实践是一个综合性很强的练手项目。下面我就把从环境搭建、驱动配置、到应用编程、调试排坑的完整过程梳理一遍希望能给正在类似道路上摸索的朋友一份详细的参考。2. 环境搭建与工程创建从零开始的正确姿势在开始敲代码之前一个稳定、高效的开发环境是基石。对于RT-Thread项目目前最主流的方式是使用RT-Thread Studio这个官方IDE或者使用Env工具配合MDK-Keil或IAR。我个人更倾向于RT-Thread Studio因为它集成了RT-Thread的配置、构建和包管理功能对新手更友好。2.1 硬件准备与原理图核对我的核心板是STM32F103ZET6这是一款基于ARM Cortex-M3内核的MCU拥有512KB Flash和64KB RAM对于运行RT-Thread和常规应用绰绰有余。在动手写软件之前必须反复确认硬件连接。STM32F103ZET6有两个CAN控制器CAN1和CAN2。它们通常与特定的GPIO引脚复用。CAN1: 默认的RX是PA11TX是PA12。这是最常用的引脚。CAN2: 它的RX和TX需要重映射通常使用PB5和PB6或者PB12和PB13具体取决于芯片的引脚复用功能。我的电路板上CAN收发器我用的是TJA1050连接到了MCU的PA11和PA12这意味着我将使用CAN1。这里第一个坑务必确认你的CAN收发器与MCU之间的接线正确并且终端电阻通常是120Ω是否已经连接在CAN_H和CAN_L之间。对于总线两端必须各接一个120Ω电阻如果只有你一个节点在调试至少要在板子上把这个电阻焊上否则通信极不稳定波形会严重畸变。2.2 使用RT-Thread Studio创建项目打开RT-Thread Studio选择“文件 - 新建 - RT-Thread项目”。基于开发板在“选择厂商与开发板”页面搜索“STM32F103ZET6”。RT-Thread Studio通常已经提供了许多标准开发板的BSP。如果你的板子恰好是某款热门开发板如正点原子、野火可以直接选择。如果找不到完全一致的可以选择一个引脚兼容的F103ZE系列开发板作为基础后续再修改引脚定义。设置项目名称和路径给项目起个名字比如rtthread_can_demo。选择RT-Thread版本建议选择最新的LTS长期支持版本稳定性有保障。Finish点击完成后Studio会自动为你生成一个完整的、基于所选BSP的工程框架。这个框架包含了RT-Thread内核、该BSP对应的所有驱动包括CAN、以及根目录下的main.c。创建完成后工程目录结构非常清晰/board存放板级相关的代码如board.c系统时钟、内存初始化等和drv_can.cCAN驱动实现。/driversRT-Thread的通用设备驱动框架。/applications用户应用代码main.c就在这里。/rt-threadRT-Thread内核源码。/packages用于存放通过包管理器添加的软件包。RT-Thread Settings这是一个图形化的配置工具是工程配置的核心。2.3 关键配置通过RT-Thread Settings使能CAN双击工程根目录下的RT-Thread Settings文件会打开一个可视化配置界面。这里是我们“武装”系统的武器库。硬件配置在“硬件”栏目下找到“On-chip Peripheral”或“设备驱动程序”。展开“CAN”选项你会看到“Enable CAN1 BUS”和“Enable CAN2 BUS”。因为我们使用PA11/PA12所以勾选“Enable CAN1 BUS”。勾选后通常会自动配置好引脚。但你必须点开右侧的配置详情或去检查代码确认引脚是否正确。对于F103CAN1的RX/TX引脚配置通常在board/CubeMX_Config/下的MX_GPIO_Init函数中或者在board/drv_can.c的开头以宏定义形式出现。确保它们是#define CAN1_RX_PIN GET_PIN(A, 11) #define CAN1_TX_PIN GET_PIN(A, 12)波特率配置这是第二个容易踩坑的地方。配置界面可能有一个默认波特率如500kbps。你需要根据你的总线网络实际需求来设置。CAN波特率计算涉及波特率分频器、时间段1、时间段2和重同步跳转宽度。对于初学者可以先用一个经典配置比如1Mbps。在drv_can.c的初始化函数里你会找到类似hcan1.Init.Prescaler xx;的配置。一个常见的1Mbps配置系统时钟72MHz可能是hcan.Init.Prescaler 9; // 分频系数 hcan.Init.TimeSeg1 CAN_BS1_4TQ; // 时间段1为4个时间单元 hcan.Init.TimeSeg2 CAN_BS2_3TQ; // 时间段2为3个时间单元 hcan.Init.SJW CAN_SJW_1TQ; // 同步跳转宽度为1个时间单元 // 计算(1/(72MHz / ((143) * 9))) ≈ 1 Mbps如果不确定可以先使用一个保守的值如125kbps或250kbps确保通信稳定后再提高。软件包与组件配置ulog日志在“软件包”或“组件”中心搜索并启用ulog。这是RT-Thread的日志组件。我们需要将它配置为“异步日志”并“启用文件系统后端”。这样日志不仅能打印到串口还能自动写入文件比如SD卡。这对于记录CAN通信的长期运行状态至关重要。文件系统如果要用ulog写文件必须启用文件系统。通常选择FATFS这个软件包并配置好对应的存储设备如SPI Flash或SD卡的驱动。FinSH控制台强烈建议启用。它是一个命令行组件可以通过串口输入命令来查看线程状态、内存使用、甚至动态调用CAN的发送接收函数是调试的利器。配置完成后点击保存。RT-Thread Studio会自动执行scons --targetmdk5如果你用Keil或类似的命令更新工程文件将你选择的配置如CAN驱动、ulog的源代码链接到项目中。3. CAN驱动框架解析与应用层编程实战环境配好了接下来就是理解RT-Thread如何操作CAN设备并编写我们的应用代码。RT-Thread使用一套名为“设备驱动框架”的模型来统一管理硬件外设CAN也不例外。3.1 RT-Thread的设备驱动模型一切皆文件在RT-Thread中所有的硬件设备如UART、SPI、I2C、CAN在系统初始化后都会注册为一个“设备对象”并挂载到设备框架中。应用程序通过标准的“打开-读写-控制-关闭”接口来操作设备类似于Linux下的文件操作。对于CAN设备这套接口被封装得更加符合CAN通信的特点。首先在main.c或你的应用线程中你需要找到并打开CAN设备#include rtdevice.h // 必须包含这个头文件 static rt_device_t can_dev RT_NULL; // 定义设备句柄 void can_thread_entry(void *parameter) { /* 1. 查找设备 */ can_dev rt_device_find(can1); // “can1”就是在BSP驱动中注册的设备名称 if (can_dev RT_NULL) { rt_kprintf(Error: Find CAN1 device failed!\n); return; } /* 2. 以中断接收模式打开设备 */ if (rt_device_open(can_dev, RT_DEVICE_FLAG_INT_RX) ! RT_EOK) { rt_kprintf(Error: Open CAN1 device failed!\n); return; } rt_kprintf(CAN1 device opened successfully.\n); /* 3. 设置接收回调函数 */ rt_device_set_rx_indicate(can_dev, can_rx_callback); /* 4. 设置发送完成回调函数可选 */ rt_device_set_tx_complete(can_dev, can_tx_done_callback); // ... 后续的发送和接收逻辑 }关键点解析rt_device_find(can1)这里的字符串can1必须与BSP驱动中注册的名字一致。通常可以在drv_can.c里找到rt_hw_can_init()函数里面有rt_device_register(can_dev, can1, ...)的调用。RT_DEVICE_FLAG_INT_RX这是打开模式。这里指定了“中断接收模式”意味着当CAN控制器收到报文时会产生中断驱动层会自动将数据存入缓冲区并调用我们设置的回调函数。这是最常用、最高效的方式。也可以使用轮询模式但不推荐。can_rx_callback这是你定义的回调函数。当有数据到达时系统会在中断上下文或中断下半部调用它。注意回调函数里不能做耗时操作通常只用于释放一个信号量或发送一个消息通知应用线程来处理数据。3.2 定义数据结构与回调函数CAN报文有标准格式。RT-Thread在rtdevice.h中定义了struct rt_can_msg结构体我们需要熟悉它struct rt_can_msg { rt_uint32_t id; /* 报文ID */ rt_uint32_t ide; /* 扩展帧标识RT_CAN_STDID 或 RT_CAN_EXTID */ rt_uint32_t rtr; /* 远程帧标识RT_CAN_DTR 或 RT_CAN_RTR */ rt_uint32_t len; /* 数据长度 (0-8) */ rt_uint8_t data[8]; /* 数据 */ };接下来实现回调函数。一个典型的模式是使用rt_sem信号量进行线程同步static rt_sem_t rx_sem RT_NULL; // 接收信号量 static struct rt_can_msg rx_msg; // 用于存放接收到的报文 /* 接收回调函数 */ static rt_err_t can_rx_callback(rt_device_t dev, rt_size_t size) { /* 当驱动收到数据中断服务程序会调用此函数 */ if (size 0) { rt_sem_release(rx_sem); // 释放信号量通知处理线程 } return RT_EOK; } /* 发送完成回调用于非阻塞发送 */ static rt_err_t can_tx_done_callback(rt_device_t dev, void *buffer) { rt_kprintf(CAN message sent.\n); return RT_EOK; }在应用线程初始化时创建这个信号量rx_sem rt_sem_create(can_rx, 0, RT_IPC_FLAG_FIFO);。3.3 应用线程发送与接收的完整循环现在我们可以在应用线程里编写主循环等待信号量并处理数据同时也可以主动发送数据。void can_thread_entry(void *parameter) { // ... 前面的设备查找、打开、回调设置代码 struct rt_can_msg tx_msg; rt_size_t send_size; while (1) { /* 等待CAN接收信号量超时时间设为RT_WAITING_FOREVER或一个具体值 */ if (rt_sem_take(rx_sem, RT_WAITING_FOREVER) RT_EOK) { /* 信号量到来说明有数据可读 */ rt_size_t recv_size rt_device_read(can_dev, 0, rx_msg, sizeof(rx_msg)); if (recv_size 0) { // 处理接收到的报文 rx_msg rt_kprintf([CAN RX] ID:0x%08X, Len:%d, Data:, rx_msg.id, rx_msg.len); for (int i 0; i rx_msg.len; i) { rt_kprintf(%02X , rx_msg.data[i]); } rt_kprintf(\n); // 示例简单回环将收到的数据原样发回ID加1以示区别 tx_msg.id rx_msg.id 1; tx_msg.ide rx_msg.ide; tx_msg.rtr RT_CAN_DTR; tx_msg.len rx_msg.len; rt_memcpy(tx_msg.data, rx_msg.data, rx_msg.len); send_size rt_device_write(can_dev, 0, tx_msg, sizeof(tx_msg)); if (send_size 0) { // rt_kprintf(Loopback message sent.\n); } } } /* 也可以主动定时发送一些数据 */ // static rt_tick_t last_send_tick 0; // if (rt_tick_get() - last_send_tick 1000) // 约1秒 // { // tx_msg.id 0x123; // tx_msg.ide RT_CAN_STDID; // tx_msg.len 4; // tx_msg.data[0] 0xAA; tx_msg.data[1] 0xBB; tx_msg.data[2] 0xCC; tx_msg.data[3] 0xDD; // rt_device_write(can_dev, 0, tx_msg, sizeof(tx_msg)); // last_send_tick rt_tick_get(); // } } }关键操作解析rt_device_read从CAN设备读取数据。第二个参数0是读取的起始位置对于CAN设备通常为0。它会从驱动的接收缓冲区中取出一帧报文。rt_device_write向CAN设备写入发送数据。同样第二个参数0对于CAN设备是固定的。这是一个阻塞调用直到报文被放入CAN控制器的发送邮箱或发送FIFO才会返回。如果邮箱已满线程会阻塞等待。如果需要非阻塞发送可以使用rt_device_write配合RT_DEVICE_FLAG_NONBLOCK标志打开设备并依赖can_tx_done_callback来确认发送完成。数据打印这里使用了rt_kprintf它会输出到控制台如串口。在实际项目中更推荐使用ulog来记录格式更规范还能存文件。4. 进阶配置与深度排坑指南把基本的收发跑通只是第一步。在真实的工业或车载应用中CAN的配置要复杂和严谨得多。下面分享几个进阶主题和踩过的坑。4.1 CAN过滤器配置精准接收的关键STM32的CAN控制器有强大的硬件过滤器组可以只接收特定ID范围的报文极大地减轻CPU负担。在RT-Thread的驱动框架中通常通过rt_device_control函数来配置过滤器。假设我们只接收标准ID为0x100到0x1FF的报文可以这样配置#include rtdevice.h struct rt_can_filter_item filter_item; struct rt_can_filter_config filter_config; /* 配置一个过滤器项 */ filter_item.id 0x100; // 要过滤的ID filter_item.mask 0x700; // 掩码0x700 二进制 0111 0000 0000表示我们关心高3位即0x1XX低8位不关心。 filter_item.mode RT_CAN_FILTER_MODE_ID_MASK; // 标识符掩码模式 filter_item.hdr -1; // -1表示由驱动自动分配一个可用的过滤器组 /* 将过滤器项放入配置结构 */ filter_config.count 1; // 只有一个过滤器项 filter_config.items filter_item; /* 使用控制命令配置过滤器 */ if (rt_device_control(can_dev, RT_CAN_CMD_SET_FILTER, filter_config) ! RT_EOK) { rt_kprintf(Error: Set CAN filter failed!\n); } else { rt_kprintf(CAN filter configured. Will only accept ID 0x100~0x1FF.\n); }掩码计算解释掩码的每一位为1表示必须匹配ID对应的位为0表示不关心。0x700的二进制是0111 0000 0000这意味着ID的bit10~bit8即从高位开始的第11~9位对于标准ID是29-18位中的一部分这里简化理解必须是001即0x1而低8位可以是任意值。所以它能过滤出0x100到0x1FF的所有ID。踩坑记录1过滤器数量有限。STM32F103的CAN1和CAN2共享最多14个过滤器组具体数量查数据手册。每个组可以配置为一个32位过滤器或两个16位过滤器。如果你的过滤规则很多需要精心设计合理使用掩码模式或列表模式避免过滤器组不够用。在RT-Thread中如果hdr设为-1驱动会尝试自动分配一个空闲组如果分配失败rt_device_control会返回错误。4.2 错误处理与状态监控让系统更健壮CAN总线在恶劣环境下可能出现各种错误如总线离线、错误警告、被动错误等。一个好的驱动应该能报告这些状态。RT-Thread的CAN驱动框架提供了获取错误状态的接口。rt_err_t result; struct rt_can_status status; result rt_device_control(can_dev, RT_CAN_CMD_GET_STATUS, status); if (result RT_EOK) { rt_kprintf(CAN Status:\n); rt_kprintf( Error Code: %d\n, status.rcverrcnt); // 接收错误计数器 rt_kprintf( Error Code: %d\n, status.snderrcnt); // 发送错误计数器 rt_kprintf( LEC: %d (Last Error Code)\n, status.lec); // LEC: 0无错误, 1填充错误, 2格式错误, 3应答错误, 4隐性位错误, 5显性位错误, 6CRC错误, 7未用 if (status.status RT_CAN_STATUS_BUS_OFF) { rt_kprintf( ** BUS OFF State! **\n); // 总线离线需要软件干预恢复。通常需要调用 rt_device_control(can_dev, RT_CAN_CMD_SET_MODE, RT_CAN_MODE_INIT) 进入初始化模式再重新配置。 } }建议在应用线程中定期比如每10秒查询一次CAN状态并通过ulog记录到文件。当错误计数器持续增加或进入总线离线状态时可以触发报警或自动恢复流程。4.3 使用ulog记录CAN通信日志前面提到了ulog这里详细说一下如何集成。首先确保在RT-Thread Settings中使能了ulog和文件系统并配置了存储设备。在代码中使用起来非常简单#include ulog.h // 在文件开头定义日志标签 #define LOG_TAG CAN void can_thread_entry(void *parameter) { // ... 初始化代码 while(1) { if (rt_sem_take(rx_sem, RT_WAITING_FOREVER) RT_EOK) { rt_device_read(can_dev, 0, rx_msg, sizeof(rx_msg)); // 使用ulog的不同级别记录日志 LOG_D([CAN RX] ID:0x%X, DLC:%d, rx_msg.id, rx_msg.len); // DEBUG级别调试信息 LOG_I(Received data: %02X %02X %02X %02X, rx_msg.data[0], rx_msg.data[1], rx_msg.data[2], rx_msg.data[3]); // INFO级别 // 如果发现错误帧 if (rx_msg.rtr RT_CAN_RTR) // 假设远程帧我们不处理记录警告 { LOG_W(Received RTR frame with ID 0x%X, ignored., rx_msg.id); } } // 定期记录状态 static rt_tick_t last_log_tick 0; if (rt_tick_get() - last_log_tick 10000) // 10秒 { struct rt_can_status status; if (rt_device_control(can_dev, RT_CAN_CMD_GET_STATUS, status) RT_EOK) { LOG_I(CAN Health: RecvErr%d, SendErr%d, status.rcverrcnt, status.snderrcnt); } last_log_tick rt_tick_get(); } } }ulog会自动将日志输出到控制台如果使能了后端同时也会按照配置写入到文件系统中例如/sd/can_log_20240515.log。在RT-Thread的mshFinSH中你还可以动态调整日志级别ulog_lvl CAN I将CAN标签的日志级别设置为INFO只显示INFO及以上级别的日志非常方便。4.4 性能优化与注意事项接收缓冲区大小在drv_can.c中驱动会为每个CAN通道定义一个接收缓冲区通常是环形队列。如果总线数据量很大需要适当调大这个缓冲区的大小防止数据丢失。找到struct rt_can_device实例化时的rxbuffer大小参数进行修改。中断优先级CAN接收中断的优先级需要合理设置。如果中断优先级过低可能被其他高优先级中断打断导致数据接收不及时。如果优先级过高又可能影响系统实时性。通常设置为中等偏上的优先级。在CubeMX生成的代码或drv_can.c的HAL_CAN_ConfigFilter和HAL_CAN_ActivateNotification相关部分可以找到中断配置。发送超时处理rt_device_write是阻塞的。如果总线负载很高发送邮箱长时间满线程会一直阻塞。可以考虑使用rt_device_write的非阻塞模式或者在一个独立的发送线程中操作并设置一个合理的超时等待时间超时后记录错误并丢弃旧报文避免整个线程卡死。多线程访问CAN设备句柄can_dev是一个共享资源。如果多个线程都需要发送CAN报文必须做好互斥保护使用rt_mutex互斥锁确保同一时间只有一个线程在操作发送。接收回调函数由于在中断上下文被调用其内部释放信号量等操作必须是线程安全的RT-Thread的内核对象操作通常是安全的。5. 调试技巧与常见问题排查即使按照步骤一步步来第一次调通CAN也难免遇到问题。下面是我总结的一套排查流程。现象根本收不到任何数据也发不出去。检查硬件连接万用表测量CAN_H和CAN_L对地电压。静默时CAN_H约2.5VCAN_L约2.5V。差分电压为0V。如果电压异常比如接近0或VCC检查收发器供电、引脚是否虚焊、终端电阻是否接上。用示波器看波形是最直接的。在发送时应该能看到清晰的差分信号。标准CAN波形在显性位逻辑0时CAN_H约3.5VCAN_L约1.5V差分电压约2V。检查软件配置波特率这是头号杀手。确保发送和接收节点的波特率设置完全一致包括分频、时间段1、时间段2和SJW。最好用计算工具算好直接写死参数。工作模式确认CAN控制器是否已正确进入正常工作模式Normal Mode而不是初始化模式或回环模式。在drv_can.c的初始化函数末尾应有调用HAL_CAN_Start或类似函数。过滤器如果你配置了过滤器但过滤条件设置错误可能会屏蔽掉所有报文。调试初期可以先将过滤器设置为“接收所有报文”模式。在RT-Thread中可以通过RT_CAN_CMD_SET_FILTER命令传入一个空的filter_configcount0来禁用所有过滤器或者设置一个全通过滤器掩码为0。中断是否开启检查驱动中是否使能了CAN接收中断HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING)。利用RT-Thread工具诊断list_device在FinSH控制台输入list_device查看是否注册了名为can1的设备以及其状态。ps输入ps查看你的CAN应用线程是否在正常运行状态是否为ready或running。日志确保ulog控制台后端已打开查看启动日志和运行日志看是否有CAN初始化失败的错误信息。现象能发送但收不到或者能收到自己的回环数据但收不到其他节点的。回环测试在驱动初始化时将CAN模式设置为回环模式RT_CAN_MODE_LOOPBACK。在这个模式下控制器自己发送的报文会被自己接收不经过外部总线。用这个模式可以快速验证MCU的CAN控制器和驱动代码是否正常。如果回环模式能自发自收说明软件和芯片本身没问题问题出在外部总线或另一个节点上。检查另一个节点确认另一个节点是否正常工作、波特率是否匹配、终端电阻是否已接。检查硬件差异有些CAN收发器如TJA1050有静默模式引脚STB。确保它被正确拉高或拉低使收发器处于正常工作状态。现象通信不稳定偶尔丢帧或出现错误帧。总线负载用CAN分析仪监控总线负载率。如果负载率长期超过70%-80%可能会因为仲裁和延迟导致丢帧。需要优化通信协议减少不必要的报文或提高波特率。布线干扰CAN总线对布线很敏感。应使用双绞线并确保屏蔽层良好接地。避免与电源线、电机驱动线等强干扰源平行走线。地线问题所有CAN节点必须有良好的共地。地线环路或地电位差会引入巨大噪声。查看错误计数器如4.2节所述定期读取错误计数器。如果接收错误计数器增长很快可能是总线上的显性/隐性位识别有问题检查硬件连接和终端电阻。如果发送错误计数器增长可能是发送节点无法获得总线仲裁ID优先级低或没有收到应答总线上无其他正常节点。通过以上步骤你应该能解决RT-Thread下STM32F103ZET6 CAN通信的大部分问题。这个组合的稳定性已经经过大量项目验证一旦调通在RT-Thread丰富的组件生态支持下你可以轻松地在此基础上扩展出更复杂的应用比如通过CAN总线实现固件升级IAP、接入物联网网关等。