CAN收发代码优化(2)

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

186.1.9 CAN收发代码优化(2)

/* canif.c:本课完善后的发送函数。 */
uint32_t Can_Send_Msg(uint32_t id, uint32_t len,uint8_t *buf) /* 传入标准 ID、DLC 和待发送数据首地址,返回课堂约定的状态码。 */
{
	CAN_TxHeaderTypeDef TxMessage; /* 定义局部发送帧头;课堂源码没有把整个结构体清零。 */
	uint32_t Tx_Mail = CAN_TX_MAILBOX0; /* 定义邮箱输出变量;HAL 会把实际选中的邮箱位掩码写回此处。 */
	TxMessage.StdId = id; /* 把调用者传入的 11 位标准标识符写入帧头。 */
	TxMessage.IDE = CAN_ID_STD; /* 固定使用标准帧格式。 */
	TxMessage.DLC = len; /* 把调用者传入的长度写入 DLC;经典 CAN 范围为 0~8。 */
	TxMessage.RTR  = CAN_RTR_DATA; /* 固定发送数据帧,而不是远程帧。 */

	if(HAL_CAN_AddTxMessage(&hcan, &TxMessage,buf, &Tx_Mail) != HAL_OK) /* 请求把报文加入空闲发送邮箱,并检查是否成功入队。 */
		return 1; /* HAL 未能接受发送请求时,立即向 main.c 返回 1。 */

	while(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) !=3); /* 忙等到三个发送邮箱都空闲;分号表示循环体为空。 */
	return 0; /* 课堂约定走到这里返回 0,main.c 将其显示为发送成功。 */
}

/* canif.c:本课完善后的轮询接收函数。 */
uint32_t Can_Recv_Msg(uint32_t *id, uint8_t *buf) /* 通过指针带回标准 ID 和数据,用返回值带回 DLC。 */
{
	CAN_RxHeaderTypeDef RxMessage; /* 定义局部接收帧头,用来保存刚取出报文的属性。 */

	if(HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_FILTER_FIFO0) == 0) /* 查询 FIFO0 中等待读取的报文数是否为 0。 */
		return 0; /* FIFO0 为空时不再调用取帧函数,以 0 表示当前没有数据。 */

	HAL_CAN_GetRxMessage(&hcan, CAN_FILTER_FIFO0,&RxMessage, buf); /* 从 FIFO0 取出一帧,把数据写入调用者缓冲区。 */

	*id = RxMessage.StdId; /* 把帧头中的标准 ID 写回 main.c 的 Recv_ID。 */

	return RxMessage.DLC; /* 把收到的 DLC 返回给 main.c。 */
}

/* main.c:发送分支改为判断 Can_Send_Msg() 的返回值。 */
uint32_t ret = Can_Send_Msg(0x12, 8,Tx_Data); /* 请求发送 ID 0x12、长度 8 和 Tx_Data 中的数据,并保存状态码。 */
if(ret == 0) /* 课堂接口把 0 约定为发送流程成功。 */
{
	printf("send ok\r\n"); /* 返回 0 时输出发送成功提示。 */
}else{ /* 返回值不是 0 时进入错误分支。 */
	printf("send eorro\r\n"); /* 原源码把 error 拼成 eorro,这里依照源码保留。 */
}

/* main.c:接收分支继续使用返回的 DLC 判断是否处理数据。 */
uint32_t Recv_ID = 0; /* 保存 Can_Recv_Msg() 通过指针带回的标准 ID。 */
uint32_t Recv_Len = Can_Recv_Msg(&Recv_ID, Rx_Data); /* 轮询 FIFO0,并保存收到的 DLC 或“无数据”状态 0。 */
if(Recv_Len>0) /* DLC 大于 0 时打印这一帧。 */
{
	printf("recv id 0x%x:\r\n", Recv_ID); /* 以十六进制显示标准 ID。 */
	printf("recv length %u:\r\n", Recv_Len); /* 显示报文 DLC。 */
	printf("recv:"); /* 输出数据行提示。 */
	for(int i=0;i<Recv_Len;i++) /* 按 DLC 遍历有效数据字节。 */
		printf(" %u ",Rx_Data[i]); /* 逐字节以无符号十进制显示数据。 */
	printf("\r\n"); /* 一帧显示结束后换行。 */

	memset(Rx_Data, 0, 8); /* 处理后清空 8 字节接收缓冲区。 */
}else{ /* 当前 FIFO 中没有非零长度报文时进入此分支。 */
	printf("recv eorro\r\n"); /* 原源码拼写保留;轮询速度很快时会连续输出此提示。 */
}

源码范围说明:上面只收录第 186 课相对上一课真正补充的邮箱/FIFO 判断、明确返回值和 main.c 状态处理。代码按课程配套 02Can_Test_Ok 保留,没有擅自把忙等改成超时,也没有修正 eorro、FIFO 宏或发送帧头初始化;这些限制在正文中说明。

一、CAN收发代码优化

本课承接第 185 课。上一课已经把 CAN 操作拆进 canif.c/.h,但发送函数没有有效返回值,接收函数在 FIFO 为空时仍会直接调用 HAL_CAN_GetRxMessage()。本课用两个 HAL 查询接口补上最基本的状态判断:

发送:构造帧 → 请求加入邮箱 → 等待三个邮箱重新空闲 → 返回状态
接收:检查 FIFO0 数量 → 有报文才取帧 → 返回 DLC;无报文返回 0

这里的“邮箱”和“FIFO”都是 bxCAN 外设内部的硬件缓冲结构,不是操作系统在线程之间传消息的邮箱/队列:

  • 三个发送邮箱暂存软件提交、等待控制器发送的 CAN 帧;
  • FIFO0、FIFO1 保存通过硬件过滤器的接收帧,每个 FIFO 有自己的挂起数量和溢出状态;
  • CPU 通过寄存器或 HAL 接口查询这些硬件状态。

1.1 邮箱和 FIFO

课程在 stm32f1xx_hal_can.h 的控制函数区域找到:

接口返回值的准确意义
HAL_CAN_GetTxMailboxesFreeLevel(&hcan)当前三个发送邮箱中有多少个空闲,范围为 0~3,不是简单的布尔值
HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0)FIFO0 当前存有多少帧等待读取;0 表示空

原笔记中的 tx_mail_books_free_ve、rx_level 都是语音识别错误,实际函数名分别是 HAL_CAN_GetTxMailboxesFreeLevel() 和 HAL_CAN_GetRxFifoFillLevel()。

1.2 发送函数接口

发送函数现在有两级判断:

  1. HAL_CAN_AddTxMessage() 返回值:判断 HAL 是否找到空邮箱、写好帧头和数据并置位发送请求。返回 HAL_OK 只代表请求被控制器接受,不等于总线发送已经成功。
  2. HAL_CAN_GetTxMailboxesFreeLevel():查看目前有几个空邮箱。代码等待返回值等于 3,即三个邮箱全部空闲。

while (条件); 末尾的分号非常关键:它构成一个空循环体,CPU 会反复调用查询函数,直到条件为假才执行后面的 return 0。这属于忙等待,期间当前执行流不能做其他工作。

课程把等待语句放在 HAL_CAN_AddTxMessage() 后面,是想表达“先提交,再等邮箱释放”;若放在发送前面,则表达“等全部邮箱空了才允许提交”。这两种位置不只是习惯不同:

  • 发送前等待 3 个全空会人为放弃 bxCAN 三邮箱带来的排队能力;通常只要至少有一个空邮箱即可提交。
  • 发送后等到 3 个全空会把异步发送变成阻塞流程,并等待其他邮箱中的所有请求也结束,而不只是本次 Tx_Mail 对应的邮箱。
1.2.1 函数实体

课程约定 Can_Send_Msg() 返回 0 表示成功、1 表示失败,并在 main.c 中据此打印 send ok 或 send eorro。但要准确区分三个阶段:

阶段当前代码能否确认
请求是否成功放入邮箱能,检查了 HAL_CAN_AddTxMessage()
邮箱是否最终重新空闲能,忙等到空闲数为 3
本帧是否成功获得仲裁并被其他节点 ACK不能仅由“邮箱空闲”推出

本工程 can.c 中 AutoRetransmission = DISABLE。若某次发送发生错误且请求结束,邮箱同样可能重新变为空闲;当前函数仍会返回 0。严谨判断应针对 Tx_Mail 检查发送完成及 TXOK/发送错误状态,或使用发送完成与错误回调,而不是把“空闲邮箱数等于 3”直接等同于总线发送成功。

当前忙等也没有超时。总线异常、状态机异常或配置改变时,函数可能长时间卡在循环中。课堂源码必须原样保留,所以代码块没有添加超时;实际工程应设置超时并报告 CAN 错误状态。

发送帧头仍有上一课指出的问题:局部 CAN_TxHeaderTypeDef TxMessage 没有整体初始化,源码没有给 TransmitGlobalTime 赋值。本项目 HAL 会读取此成员。该问题不是本课修改内容,因此只提示、不擅自补写。

1.3 接收函数接口

接收函数先调用 HAL_CAN_GetRxFifoFillLevel():

  • 返回 0:FIFO0 为空,函数直接 return 0;
  • 返回非 0:FIFO0 至少有一帧,再调用 HAL_CAN_GetRxMessage() 取出队首报文;
  • 取帧后通过 *id 带回标准 ID,通过 buf 写回数据,通过普通返回值带回 DLC。

这解决了第 185 课在 FIFO 为空时读取未初始化 RxMessage 的主要问题。不过仍有三点需要知道:

  1. HAL_CAN_GetRxMessage() 自身也有 HAL_StatusTypeDef 返回值,课堂代码没有检查它;稳健代码仍应只在返回 HAL_OK 时使用帧头。
  2. 接收 API 的 FIFO 参数按语义应使用 CAN_RX_FIFO0。课堂写的是 CAN_FILTER_FIFO0,它用于过滤器的 FIFO 分配字段;本 HAL 中两者数值恰好都是 0,程序能运行,但名称表达的概念不同。
  3. 函数把返回值 0 同时当成“FIFO 无帧”,而合法经典 CAN 帧的 DLC 也可以为 0,因此这个接口无法区分“无帧”和“收到零长度帧”。本实验只发送 DLC=8,不会触发该歧义。

原笔记所说“返回值小于 0 表示失败”不成立,因为 Recv_Len 和函数返回类型都是无符号 uint32_t。if (Recv_Len > 0) ... else ... 的 else 实际只覆盖 0。

1.4 验证优化后的代码

课程继续使用两块板验证:ZET6 编译接收分支,C8T6 编译发送分支。操作顺序为:

  1. 把条件编译值设为 1,重新 Build,并把接收固件烧入 ZET6;
  2. 断电或谨慎核对接口后,把 ST-Link 接到 C8T6;
  3. 把条件编译值设为 0,重新 Build,把发送固件烧入 C8T6;
  4. 打开两个串口会话,分别选择两块板对应的 COM 口和 115200-8-N-1;
  5. 发送端应出现 send ok,接收端应看到 ID 0x12、长度 8 和上一课的 8 字节数据。

修改条件编译值之后必须重新编译再烧写;只修改编辑器中的数字而不 Build,下载的仍可能是旧固件。ST-Link 四线顺序也不能只凭线色判断,应逐针核对目标板的 SWDIO、SWCLK、GND 和参考电压。

本课的实测证明了封装后仍能完成当前实验,但它没有消除轮询空转:接收主循环在绝大多数“暂时没有帧”的时刻仍会打印 recv eorro。第 189 课会改为 FIFO0 报文挂起中断,让程序只在新报文到达时开始接收处理。

本笔记的接口语义以项目内 STM32F1 HAL 源码为准:该源码明确把 HAL_CAN_GetTxMailboxesFreeLevel() 定义为“空闲邮箱数量”,把 HAL_CAN_GetRxFifoFillLevel() 定义为“FIFO 中可用报文数量”。CAN 外设结构可继续对照 ST 官方 RM0008 参考手册。