嵌入式CAN通信学习记录(万字解析):从STM32到Linux双机联调实战
1. 引言
CAN(Controller 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(&hcan1, CAN_RX_FIFO0, &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
done
5.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(&hcan1, CAN_RX_FIFO0, &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 < 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 读取后归零导致打印判断永不成立,通过将打印逻辑紧接接收赋值代码之后解决。
欢迎大家在评论区留言讨论!
更多推荐



所有评论(0)