CAN数据收发代码

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 0 Likes: 0 Comments: 0

183.1.6 CAN数据收发代码

/* main.c:以下是本节在 USER CODE BEGIN PV 中加入的三个全局变量。 */
uint8_t Tx_Data[]={7,8,5,24,11,2,13,9}; /* 定义实际发送的 8 字节负载;数组长度与后面填写的 DLC=8 一致。 */
uint8_t Rx_Data[8];                      /* 预留 8 字节接收缓冲区;全局变量在程序启动时会被自动清零。 */
uint32_t Tx_Mail = CAN_TX_MAILBOX0;      /* 保存 HAL 实际选中的发送邮箱位掩码;初值会被发送函数覆盖,并不能强制选择邮箱 0。 */

/* main.c:以下是本节写入 while (1) 的收发选择代码;上一课的过滤器初始化与 HAL_CAN_Start() 不在这里重复。 */
#if 1                                   /* 值为 1 时编译接收分支;改为 0 后编译 #else 中的发送分支,这是编译期选择。 */
        CAN_RxHeaderTypeDef RxMessage;  /* 定义接收帧头;只有 HAL 接收成功时,里面的 ID、DLC 等字段才是本次新报文的信息。 */

        HAL_CAN_GetRxMessage(&hcan, CAN_FILTER_FIFO0,&RxMessage, Rx_Data); /* 从 FIFO0 取一帧到 RxMessage/Rx_Data;课程代码未判断返回值。 */

        printf("recv:");                /* 先通过 USART1 打印一段提示文字;本工程的 printf 是后文定义的宏。 */
        for(int i=0;i<8;i++)            /* 固定循环 8 次;本节尚未根据 RxMessage.DLC 决定真正应打印的字节数。 */
                printf(" %u ",Rx_Data[i]); /* 依次以无符号十进制显示接收缓冲区的 8 个字节,并在数值两侧加空格。 */
        printf("\r\n");                /* 输出回车加换行,让下一次打印从串口助手的新行开始。 */
#else                                   /* 当上面的条件改为 0 时,只编译下面的发送分支。 */

        CAN_TxHeaderTypeDef TxMessage;  /* 定义发送帧头;原代码只填写四个成员,TransmitGlobalTime 仍未初始化,这是本课遗留问题。 */

        TxMessage.StdId = 0x12;         /* 使用标准标识符 0x12,也就是十进制 18,位于 11 位 ID 的合法范围内。 */
        TxMessage.IDE = CAN_ID_STD;     /* 选择 11 位标准帧;该 HAL 宏为寄存器位掩码 0,而不是随意填写的布尔值。 */
        TxMessage.DLC = 8;              /* 声明本帧数据段长度为 8 字节,对应 Tx_Data 中的 8 个元素。 */
        TxMessage.RTR  = CAN_RTR_DATA;  /* 选择数据帧;若使用 CAN_RTR_REMOTE 才表示不带数据段的远程帧。 */

        HAL_CAN_AddTxMessage(&hcan, &TxMessage,Tx_Data, &Tx_Mail); /* 把帧装入第一个可用发送邮箱,并把实际邮箱写回 Tx_Mail;原代码忽略返回值。 */
        printf("Send ok!\r\n");         /* 原样保留课程提示;它无条件执行,只表示调用已经返回,不能证明报文已获 ACK。 */
#endif                                  /* 接收/发送的条件编译代码到此结束。 */

/* usart.c:本节在 USER CODE BEGIN 0 中加入共享格式化缓冲区。 */
uint8_t u_buf[256];                     /* 为 sprintf 准备 256 字节全局缓冲区,随后由 HAL_UART_Transmit() 发出其中的字符。 */

/* usart.h:本节在用户代码区加入头文件、外部声明和 printf 宏。 */
#include <stdio.h>                      /* 声明 sprintf();这里并不是直接调用标准库 printf()。 */
extern uint8_t u_buf[256];              /* 声明 u_buf 定义在 usart.c,使包含本头文件的其他源文件可以引用同一缓冲区。 */
#define printf(...)  HAL_UART_Transmit(&huart1, (uint8_t *)u_buf, sprintf((char*)u_buf, __VA_ARGS__), 0xffff) /* 先格式化到 u_buf,再阻塞发送生成的字符数。 */

代码按本节视频结束时的状态收录,只增加注释:没有加入下一课才添加的 memset()、#include "string.h"、接收 ID/DLC 打印,也没有提前改成后续封装函数。代码中的未检查返回值、未完整初始化发送帧头等问题同样予以保留,并在正文解释风险。

一、CAN数据收发代码

上一课已经完成过滤器组 0 的全放行配置,并调用 HAL_CAN_Start() 启动 CAN。本课继续完成最小收发链路:

发送端:填写帧头 + 8 字节数据 → 放入空发送邮箱 → CAN 控制器发送
接收端:FIFO0 取出帧头 + 8 字节数据 → USART1 打印到串口助手

同一个工程使用 #if 1 / #else 在编译期选择接收端或发送端代码。它不是运行时按条件自动切换角色:修改数字后必须重新编译、分别烧录到两块板,才能得到一个发送节点和一个接收节点。

1.1 发送函数介绍

本节使用的接口声明为:

HAL_CAN_AddTxMessage(hcan, pHeader, aData, pTxMailbox)

四个参数的真实含义如下:

参数方向含义
&hcan输入指向 CAN1 的 HAL 句柄,告诉函数操作哪个 CAN 外设
&TxMessage输入指向发送帧头,提供 ID、IDE、RTR、DLC 等控制信息
Tx_Data输入指向待发送的数据数组;本课 DLC 为 8,因此 HAL 读取 8 个字节
&Tx_Mail输出HAL 把实际选中的空发送邮箱位掩码写回这里

原笔记把第一个参数写成“结构体”,第二个参数才写成“结构体指针”,容易误导:函数声明中的前两个参数实际上都是指针,只是分别指向 CAN_HandleTypeDef 和 CAN_TxHeaderTypeDef。

尤其要纠正第四个参数的解释。HAL_CAN_AddTxMessage() 会根据发送状态寄存器选择一个可用邮箱,再通过 pTxMailbox 把结果返回;调用者并不是把 CAN_TX_MAILBOX0 传进去强制选择 0 号邮箱。因此:

uint32_t Tx_Mail = CAN_TX_MAILBOX0;
...
HAL_CAN_AddTxMessage(..., &Tx_Mail);

这里的初值会被函数覆盖。调用完成后,Tx_Mail 才表示本次使用了邮箱 0、1 还是 2。三个宏是位掩码:邮箱 0、1、2 分别为 0x1、0x2、0x4。

CAN_TxHeaderTypeDef 在本项目 HAL 中实际有 6 个成员:

成员作用合法范围/本课值
StdId11 位标准标识符0~0x7FF;本课为 0x12
ExtId29 位扩展标识符0~0x1FFFFFFF;标准帧时不使用
IDE选择标准或扩展格式本课为 CAN_ID_STD
RTR选择数据帧或远程帧本课为 CAN_RTR_DATA
DLC数据段字节数Classical CAN 为 0~8;本课为 8
TransmitGlobalTime是否在数据 6、7 中发送时间触发通信时间戳本课没有赋值

原笔记中的 DRC 是识别错误,正确字段名是 DLC;“遥控帧”应为远程帧。远程帧是请求同一标识符的数据帧,不是装载遥控命令的数据包。

还要区分“CAN 帧中的逻辑位”和“HAL 宏的寄存器掩码”。帧里的 IDE 逻辑含义确实是标准格式为 0、扩展格式为 1,但 HAL 定义为:

CAN_ID_STD = 0x00000000
CAN_ID_EXT = 0x00000004

0x4 的二进制是 0100,用于直接对应发送标识符寄存器的 IDE 位(从最低位开始编号为 bit 2),并不是说 IDE 在线上传输了数值 4。课程最终选择 CAN_ID_STD 是正确的;视频中间口误说标准 ID “肯定赋成 1”与前后内容及源码都不一致。

1.2 发送函数代码

本课构造的帧可以概括为:

标准数据帧
ID   = 0x12
DLC  = 8
DATA = 07 08 05 18 0B 02 0D 09   (十六进制显示)

源码数组使用十进制初始化,所以其中 24、11、13 转成十六进制分别为 0x18、0x0B、0x0D。CAN 只传 8 位数值,不会携带“源代码当时使用十进制还是十六进制书写”这一信息。

HAL_CAN_AddTxMessage() 的工作是找到第一个可用发送邮箱、装入标识符/DLC/数据并置位发送请求。它返回 HAL_OK 只表示报文成功排入邮箱,不等于整帧已经发到总线,更不等于收到其他节点 ACK。

课程代码有三个必须知道的边界,但本节不擅自改写:

  1. TxMessage 是未清零的局部变量。虽然标准帧不使用 ExtId,但 HAL 仍会读取并检查 TransmitGlobalTime;该成员未赋值会得到不确定值,可能误启用时间戳或在启用参数断言时触发错误。
  2. HAL_CAN_AddTxMessage() 的返回值被忽略。若三个邮箱都满、CAN 尚未启动或参数不合法,函数可能返回错误,后面的打印仍会执行。
  3. printf("Send ok!") 因而只是一条无条件日志,不能作为发送完成证明。若要判断结果,还需先检查 HAL 返回值,再查询邮箱完成状态或处理发送完成回调和错误状态。

发送分支位于无限循环中且没有延时,会尽可能快地重复请求发送并打印。这适合作为短时实验压力测试,却可能迅速占满发送邮箱、占用总线和阻塞串口;不能不加评估地照搬到正式业务循环。

1.3 接收函数介绍

接收接口为:

HAL_CAN_GetRxMessage(hcan, RxFifo, pHeader, aData)
参数方向含义
&hcan输入CAN1 句柄地址
FIFO 选择输入指定从 FIFO0 或 FIFO1 读取;本课过滤器把报文分配给 FIFO0
&RxMessage输出接收成功后写入 ID、IDE、RTR、DLC、时间戳和过滤器匹配序号等帧头信息
Rx_Data输出接收成功后写入数据段,最多 8 字节

原样代码给第二个参数传入 CAN_FILTER_FIFO0。该宏用于“过滤器分配到 FIFO0”,而接收 API 语义上应使用 CAN_RX_FIFO0;本 HAL 版本中二者的数值恰好同为 0,所以代码能够工作,但读代码时不应把两个概念混为一谈。

接收成功后,HAL 会读取 FIFO0 的队首帧并释放输出邮箱,让下一帧能够向前移动。但本课代码没有先调用 HAL_CAN_GetRxFifoFillLevel(),也没有检查 HAL_CAN_GetRxMessage() 的返回值:

  • FIFO0 有新帧时,RxMessage 与 Rx_Data 会被更新;
  • FIFO0 为空时,HAL 返回错误,缓冲区不会自动代表“一次新的空接收”;
  • 程序仍继续打印,所以同一组旧数据可能被反复显示,不能把每行 recv: 都理解为收到了一帧新报文。

本课固定循环 8 次打印 Rx_Data,因为发送端 DLC 设置为 8。更通用的接收程序应在确认接收成功后按照 RxMessage.DLC 处理有效字节,但这项修改没有在本课源码中出现,因此这里只说明,不改写代码。

#if 1、#else、#endif 是预处理指令。接收分支为 1 时,发送分支在编译前就被删掉;改成 0 时结果相反。一次构建只检查被选中的分支,因此学习时最好分别用 1 和 0 各编译一次,避免隐藏分支中有语法错误却没被发现。

串口输出的 \r\n 由两个控制字符组成:\r 把光标移回行首,\n 移到下一行。串口助手通常用这一组合正确换行,不是原笔记所说的只加 \r。

1.4 接收函数代码

原笔记中的文件名 urt.c 应为 usart.c,头文件 stdlo.h 应为标准库头文件 stdio.h。本课使用的“串口重定向”并不是常见的重写 fputc(),而是直接定义了一个同名宏:

printf(格式, 参数...)
        ↓ 宏展开
sprintf() 把文本写进 u_buf
        ↓
HAL_UART_Transmit() 经 USART1 阻塞发送

三个组成部分各有明确作用:

  • uint8_t u_buf[256]; 在 usart.c 中分配真实存储空间;
  • extern uint8_t u_buf[256]; 在 usart.h 中只是声明“这个数组定义在别处”,不会再次分配一份数组;
  • #include <stdio.h> 为 sprintf() 提供函数声明;
  • #define printf(...) ... 让包含 usart.h 的源文件把后续 printf 调用替换成格式化并串口发送的表达式。

这些代码应放在 CubeMX 的 USER CODE BEGIN/END 区域,否则重新生成工程时可能被覆盖。当前工程的 main.c 已经包含 usart.h,所以其中的 printf() 能看到这条宏定义。

这份教学宏简单直观,但有以下限制:

  • sprintf() 不知道 u_buf 只有 256 字节,格式化结果过长会越界;
  • 所有调用共享同一个全局缓冲区,不可重入,不适合在主循环和中断中同时调用;
  • HAL_UART_Transmit() 是阻塞发送,超时参数 0xFFFF 很大,频繁打印会明显占用 CPU;
  • 宏没有检查串口发送返回值,且会覆盖标准库同名 printf。

本课只完成编译,没有进行实际双节点验证。硬件连接、分别烧录发送/接收版本、串口观察,以及下一步暴露出的缓冲区旧数据问题,都属于下一课内容,不能提前写成本课已经验证成功。

参考资料

  • 课程配套 STM32中文参考手册:核对发送邮箱结构、发送调度、接收 FIFO 和报文存储。
  • 本课配套 main.c、usart.c 与 usart.h:核对收发数组、帧头、HAL 调用、条件编译和串口打印宏;main.c 中下一课追加的代码未提前收录。
  • 配套 HAL stm32f1xx_hal_can.h 与 stm32f1xx_hal_can.c:核对帧头成员、ID/RTR/邮箱宏、函数参数方向、空邮箱选择和返回条件。
  • ST 官方 RM0008 Rev 21:核对当前版 bxCAN 发送邮箱、接收 FIFO 与报文存储机制。