嵌入式CAN通信学习记录(万字解析):从STM32到Linux双机联调实战

发布时间:2026/8/26 17:57:42
嵌入式CAN通信学习记录(万字解析):从STM32到Linux双机联调实战 1. 引言CANController Area Network总线是嵌入式系统中广泛使用的一种高可靠性、多主机的串行通信协议尤其在汽车电子、工业控制等领域应用广泛。本文旨在记录我学习嵌入式CAN通信的完整过程涵盖从STM32开发板的环回/静默模式测试到Linux开发板上的环回/静默模式测试最终实现双机联调收发测试的实践步骤与关键代码。希望通过这篇记录为同样在学习CAN通信的开发者提供一份清晰的参考。2. 环境与工具准备在开始测试前需要准备好硬件和软件环境。2.1 硬件清单STM32开发板我使用的是正点原子STM32F407VGT6-DISCOVERY开发板STM32F407VGT6核心其内置了bxCAN控制器。Linux开发板我使用的是正点原子i.MX6UL-MINI开发板EMMC版通过MCP2515 CAN总线模块扩展CAN接口。CAN收发器SN65HVD230用于连接STM32/Linux开发板与物理CAN总线。连接线杜邦线、CAN总线双绞线、120Ω终端电阻。2.2 软件与驱动STM32开发环境STM32CubeIDE使用HAL库进行开发。Linux开发环境正点原子提供的i.MX6UL系统镜像已安装can-utils工具包sudo apt install can-utils。驱动确保Linux内核已启用CAN及MCP2515 SPI驱动。串口助手波特律动串口助手学习参考STM32环回/静默模式测试部分参考了B站视频教程视频链接024_CAN通讯案例一_环回静默模式测试_HAL库方式_工程配置_哔哩哔哩_bilibili编译环境GCC影响printf重定向到串口打印3. STM32开发板环回与静默模式测试首先在STM32上验证CAN控制器的基础功能使用环回静默Loopback_Silent模式进行自测试。环回模式下CAN控制器将发送的数据直接回收到自己的接收缓冲区无需外部硬件连接适合快速验证软件配置。对外不发不收且自发自收。实验设计开启can的环回静默模式编写好过滤器配置、发送、接收函数在主函数中定义发送的信息调用过滤器配置、发送、接收函数打开USART2串口使用串口打印信息。创建MX工程保存并生成代码打开usart.h引入stdio.h打开usart.c写入以下代码完成printf重定向如有问题可查看本人上篇串口文章解决。/* USER CODE BEGIN 1 */ int __io_putchar(int ch) { HAL_UART_Transmit(huart2, (uint8_t *)ch, 1, HAL_MAX_DELAY); return ch; } /* USER CODE END 1 */打开can.h修改代码定义发送和接收的结构体定义配置过滤器、发送、接收函数/* USER CODE BEGIN Private defines */ typedef struct { uint32_t StdId; uint32_t DLC; uint8_t Data[8]; } CAN_TxMsgTypeDef; typedef struct { uint32_t StdId; uint32_t DLC; uint8_t Data[8]; } CAN_RxMsgTypeDef; /* USER CODE END Private defines */ void MX_CAN1_Init(void); /* USER CODE BEGIN Prototypes */ void CAN_Filter_Config(void); void CAN_Send_Msg(CAN_TxMsgTypeDef *TxMessage); void CAN_Receive_Msg(CAN_RxMsgTypeDef RxMessage); / USER CODE END Prototypes */打开can.c文件编写过滤器、发送、接收函数的具体实现逻辑/* USER CODE BEGIN 1 */ //配置过滤器 void CAN_Filter_Config(void) { CAN_FilterTypeDef canfilterconfig; //过滤器组编号 canfilterconfig.FilterBank 0; //关联FIFO0 canfilterconfig.FilterFIFOAssignment CAN_FILTER_FIFO0; //配置ID寄存器FR1 canfilterconfig.FilterIdHigh 0x0000; canfilterconfig.FilterIdLow 0x0000; //配置掩码寄存器FR2 canfilterconfig.FilterMaskIdHigh 0x0000; canfilterconfig.FilterMaskIdLow 0x0000; //工作模式屏蔽位模式 canfilterconfig.FilterMode CAN_FILTERMODE_IDMASK; //位宽32位 canfilterconfig.FilterScale CAN_FILTERSCALE_32BIT; //激活过滤器组 canfilterconfig.FilterActivation ENABLE; if (HAL_CAN_ConfigFilter(hcan1, canfilterconfig) ! HAL_OK) { Error_Handler(); } } //发送报文 void CAN_Send_Msg(CAN_TxMsgTypeDef *TxMessage) { //检测是否有空邮箱 while (HAL_CAN_GetTxMailboxesFreeLevel(hcan1) 0) { // 等待空邮箱 } //配置数据帧信息帧头 CAN_TxHeaderTypeDef TxHeader; TxHeader.StdId TxMessage-StdId; TxHeader.RTR CAN_RTR_DATA;//数据帧 TxHeader.IDE CAN_ID_STD; //标准帧 TxHeader.DLC TxMessage-DLC; TxHeader.TransmitGlobalTime DISABLE; uint32_t TxMailbox; if (HAL_CAN_AddTxMessage(hcan1, TxHeader, TxMessage-Data, TxMailbox) ! HAL_OK) { while (HAL_CAN_IsTxMessagePending(hcan1, TxMailbox) HAL_OK); printf(CAN message sent successfully.\n); } else { printf(CAN message send error.\n); } } void CAN_Receive_Msg(CAN_RxMsgTypeDef *RxMessage) { //检测FIFO0中接收到的报文数量 uint32_t RxFifoFillLevel HAL_CAN_GetRxFifoFillLevel(hcan1, CAN_RX_FIFO0); //遍历接收所有报文 for (uint8_t i 0; i RxFifoFillLevel; i) { CAN_RxHeaderTypeDef RxHeader; //从FIFO0中读取报文 HAL_CAN_GetRxMessage(amp;hcan1, CAN_RX_FIFO0, amp;RxHeader, RxMessage[i].Data); //将接收到的报文信息存储到结构体中 RxMessage[i].StdId RxHeader.StdId; RxMessage[i].DLC RxHeader.DLC; } } /* USER CODE END 1 */打开main.c文件编写主逻辑/* USER CODE BEGIN 2 */ CAN_Filter_Config(); HAL_CAN_Start(hcan1); printf(CAN initialization complete.\r\n); CAN_TxMsgTypeDef TxMessage; TxMessage.StdId 0x123; TxMessage.DLC 8; for (int i 0; i 8; i) { TxMessage.Data[i] i; } CAN_Send_Msg(TxMessage); printf(CAN message sent.\r\n); /* USER CODE END 2 */ /* Infinite loop / / USER CODE BEGIN WHILE / while (1) { / USER CODE END WHILE / for (int i 0; i 8; i) { TxMessage.Data[i] (TxMessage.Data[i] 1) % 256; // Increment data and wrap around at 256 } CAN_Send_Msg(TxMessage); HAL_Delay(1000); // Delay for 1 second // 发送之后立刻检查 FIFO0 if (HAL_CAN_GetRxFifoFillLevel(hcan1, CAN_RX_FIFO0) 0) { CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; HAL_CAN_GetRxMessage(hcan1, CAN_RX_FIFO0, RxHeader, RxData); // 把收到的数据用串口打印出来 printf(\r\nReceived CAN message with StdId: 0x%lX, DLC: %ld\r\n, RxHeader.StdId, RxHeader.DLC); for (int i 0; i 8; i) { printf(Data[%d]: 0x%X\r\n, i, RxData[i]); } } / USER CODE BEGIN 3 / } / USER CODE END 3 */ }点击编译无报错无警告烧录试运行结果输出乱码等待过后也无其他输出点击板子上的复位按键再次弹出乱码问题比较大错误分析当前工程得到的APB1是36M串口波特率使用的115200有输出说明硬件接线没问题但是输出乱码正常情况下不应该出现先解决乱码问题时钟配置修改成42M的APB1经过测试发现还是会乱码让我们使用上一篇博客的重定向的工程实验一下发现仅开启外部时钟源不使用不会影响修改频率为42M也无影响但是一旦改成外部时钟源HSE就开始乱码。经过查询并实验后得到问题原因外部晶振起振失败时钟回退到HSI所以才会乱码经过检查开发板上晶振丝印发现板载晶振是8M的然而时钟配置的外部晶振是25M不匹配所以没奏效因而只要修改为8M就能正常使用回到本项目中修改后验证得到确实如此现在已经有了打印结果。但是结果只有过滤器初始化成功和can初始化完成的打印还是有问题在可以在报错回调函数中加上报错打印看看是不是进入了报错回调。/** * brief This function is executed in case of error occurrence. * retval None */ void Error_Handler(void) { /* USER CODE BEGIN Error_Handler_Debug */ /* User can add his own implementation to report the HAL error return state */ __disable_irq(); while (1) { printf(Error occurred! Please check the system.\r\n); } /* USER CODE END Error_Handler_Debug */ }测试结果并无报错打印按下复位键也没有出现可以考虑多加一些日志打印看看问题出在哪查看代码发现流程卡在了can初始化完成与发送完成之间大概率是在将信息写入结构体时出现问题在循环中加上printf打印日志看看结果发现已经往结构体中写入了数据那错误就锁定在发送函数里了现在为发送信息函数内添加更详细的日志测试并查看结果发现发送函数一直卡在等待空邮箱中所以才没有报错也没有继续继续检查代码问题等待邮箱的函数逻辑还未有修改仅修改了添加消息至邮箱部分代码就能跑通了可能是之前此处的代码对邮箱有什么影响不能使用之类。uint32_t TxMailbox; if (HAL_CAN_AddTxMessage(hcan1, TxHeader, TxMessage-Data, TxMailbox) ! HAL_OK) { printf(CAN message send error.\r\n); Error_Handler(); } else { printf(CAN message send error.\n); }但是显示还是有点问题发送错误但又接收成功逻辑有点混乱上面代码中两个判断都是写的error需要修改一个修改完else里的输出后就正常了4. Linux开发板环回与静默模式测试在Linux系统下CAN接口通常被抽象为网络设备使用SocketCAN框架进行操作。下面以树莓派连接MCP2515为例。4.1 环回模式测试首先加载虚拟CAN设备或配置物理CAN接口为环回模式。在i.MX6UL-MINI开发板上需要先确保CAN接口驱动已正确加载。使用USB_TTL线确保插在板子上的USB_TTL口上不能插错了打开波特律动 串口助手 - 直接在浏览器使用的串口调试工具连接好串口上电发现串口助手正在打印linux初始化的一些代码等串口输出了如图示结果并稳定后再输入测试代码测试代码见下注意一条一条执行非常重要的一点输入框复制了代码后还需另外加一个回车不然串口助手只相当于发了一串字符串并未执行带上回车才算执行# 1. 设置为回环模式 sudo ip link set can0 type can bitrate 500000 loopback on 2. 启用接口 sudo ip link set can0 up 3. 确认状态看有没有 loopback on 和 state UP ip -details link show can0 4. 后台监听 candump can0 5. 等1秒让 candump 准备好 sleep 1 6. 发送测试帧 cansend can0 123#112233每步执行结果可参考以下几张图片测试结果符合预期5. 双机联调收发测试在分别完成单板测试后将STM32开发板与Linux开发板通过CAN总线连接进行双向通信测试。5.1 硬件连接双机联调时两块开发板各自通过一块独立的 SN65HVD230 CAN 收发器模块接入总线具体连接方式如下将 STM32F407VGT6-DISCOVERY 的 CAN_TX、CAN_RX 引脚分别连接到 SN65HVD230 模块对应的的 TXD、RXD 引脚并接好 3.3V 电源与 GND。将 i.MX6UL-MINI 开发板通过 MCP2515 模块的 CAN_H、CAN_L 引脚分别连接到SN65HVD230 模块的 CAN_H、CAN_L 引脚并接好 3.3V 电源与 GND。确保两块开发板与收发器模块共地良好用一根杜邦线单独连接两板子的GND。5.2 STM32端配置与代码重新创建一个工程开启外部时钟源修改时钟配置如图修改CAN配置如图开启PD13为output模式修改标签为LED用于错误提示打开串口2保持默认配置在项目配置中勾选为每个外设生成对应的,c.h文件保存并生成代码移植前面的can代码到此工程中在主函数中的错误回调中打印错误日志不要加Delay容易卡死在主函数中添加以下代码逻辑/* USER CODE BEGIN 2 */ CAN_Filter_Config(); if (HAL_CAN_Start(hcan1) ! HAL_OK) { /* Start Error */ Error_Handler(); } printf(CAN initialization complete.\r\n); CAN_TxMsgTypeDef TxMessage; CAN_RxMsgTypeDef RxMessage; TxMessage.StdId 0x321; TxMessage.DLC 8; uint8_t txData[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; for (int i 0; i 8; i) { TxMessage.Data[i] txData[i]; } printf(CAN message prepared for transmission.\r\n); /* USER CODE END 2 */ /* Infinite loop / / USER CODE BEGIN WHILE / while (1) { HAL_Delay(1000); // Delay for 1 second for (int i 0; i 8; i) { TxMessage.Data[i] (TxMessage.Data[i] 1) % 256; // Increment data and wrap around at 256 } CAN_Send_Msg(TxMessage); printf(CAN message sent successfully.\r\n); HAL_Delay(1000); // Delay for 1 second CAN_Receive_Msg(RxMessage); printf(CAN message received successfully.\r\n); //接收成功循环遍历输出can接收到的数据 while (HAL_CAN_GetRxFifoFillLevel(hcan1, CAN_RX_FIFO0) 0) { CAN_Receive_Msg(RxMessage); // 把收到的数据用串口打印出来 printf(\r\nReceived CAN message with StdId: 0x%lX, DLC: %ld\r\n, RxMessage.StdId, RxMessage.DLC); for (int i 0; i 8; i) { printf(Data[%d]: 0x%X\r\n, i, RxMessage.Data[i]); } } / USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ }点击编译无警告无报错为stm开发板烧录代码5.3 Linux端配置与测试在Linux端设置can0为正常模式并启动监听和发送。# 设置can0波特率500k关闭环回和静默 sudo ip link set can0 type can bitrate 500000 sudo ip link set up can0 终端1持续监听来自STM32的数据 candump can0 终端2周期性向STM32发送数据 while true; do cansend can0 100#AABBCCDDEEFF0011 sleep 2 done5.4 测试验证STM32发Linux收在Linux的candump终端应能看到can帧一秒一次发送结果如图Linux发STM32收在STM32的接收回调中应能正确接收到can帧并打印出来结果如图有接收成功的日志但无can帧打印大概率是stm端主循环出问题了问题应聚焦在can帧打印逻辑上初步分析可能是数据没能正常从结构体中取出可以将逻辑放到循环外只执行一次多加几处日志打印寻找问题修改后可以看到通信确实是卡在打印那步了所以问题极大概率是因为接收时没写入接收的结构体可以在接收帧信息函数中打印一次试试看。在发送函数里添加打印逻辑也没打印出来在遍历接收报文的循环中加上日志打印看是否进入接收循环。发现根本就没进入接收循环意味着FIFO中根本就没有数据进来经过检查后得知是linux端和stm端采样点不匹配linux采样点大约在86.7%而stm端在75%修改can配置如图fifo即可接收到帧再次测试得到如图结果但仍然无帧打印出来只是接收到了帧现在代码中有一处疑点HAL_CAN_GetRxFifoFillLevel函数会随读取fifo而变化读取完这个值就归零但代码中打印两处都用这个做判断判断永远不会成立所以打印的代码应该紧接着接收并赋值了的代码之后修复发送代码如下void CAN_Receive_Msg(CAN_RxMsgTypeDef *RxMessage) { //检测FIFO0中接收到的报文数量 uint32_t RxFifoFillLevel HAL_CAN_GetRxFifoFillLevel(hcan1, CAN_RX_FIFO0); if (RxFifoFillLevel 0) { printf(No CAN message received.\r\n); return; } //遍历接收所有报文 for (uint8_t i 0; i RxFifoFillLevel; i) { CAN_RxHeaderTypeDef RxHeader; //从FIFO0中读取报文 HAL_CAN_GetRxMessage(amp;hcan1, CAN_RX_FIFO0, amp;RxHeader, RxMessage[i].Data); //将接收到的报文信息存储到结构体中 RxMessage[i].StdId RxHeader.StdId; RxMessage[i].DLC RxHeader.DLC; printf(Received CAN message with StdId: 0x%lX, DLC: %ld\r\n, RxMessage[i].StdId, RxMessage[i].DLC); for (int j 0; j lt; 8; j) { printf(Data[%d]: 0x%X\r\n, j, RxMessage[i].Data[j]); } } }编译运行得到如图结果顺利接收到can帧。6. 常见问题与调试技巧无法通信检查接线CAN_H/CAN_L是否接反、终端电阻、共地以及两端波特率、采样点是否严格一致。大量错误帧可能是总线阻抗不匹配、干扰过大或波特率计算错误。STM32发送失败检查CAN控制器是否已启动HAL_CAN_Start发送邮箱是否空闲。Linux端无法设置can0检查MCP2515驱动是否加载SPI通信是否正常。7. 总结本文完整记录了从零开始学习嵌入式 CAN 通信的实践过程覆盖了单板自测与双机联调两个阶段主要收获如下STM32 环回静默模式测试通过 STM32CubeIDE 配置工程编写过滤器、发送、接收函数验证了 CAN 控制器自发自收的基础功能。期间解决了两个关键问题一是板载晶振为 8M 而工程配置为 25M 导致外部晶振起振失败、串口输出乱码二是发送函数卡在等待空邮箱通过调整添加消息至邮箱的代码逻辑解决。Linux 环回静默模式测试使用 SocketCAN 框架通过 ip 命令将 can0 配置为环回模式配合 candump 监听与 cansend 发送验证了 Linux 端 CAN 接口的自发自收功能。双机联调收发测试两块开发板通过独立的 SN65HVD230 收发器模块接入总线实现双向通信。期间解决了两个关键问题一是两端采样点不匹配Linux 约 86.7%、STM32 为 75%导致 FIFO 收不到数据通过修改 CAN 配置解决二是 HAL_CAN_GetRxFifoFillLevel 读取后归零导致打印判断永不成立通过将打印逻辑紧接接收赋值代码之后解决。欢迎大家在评论区留言讨论