CAN数据中断接收

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

189.1.12 CAN数据中断接收

/* canif.c:在原过滤器初始化函数中新增 FIFO0 报文挂起通知。 */
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); /* 使能“FIFO0 中有报文等待处理”这一 CAN 中断源。 */
HAL_CAN_Start(&hcan); /* 启动 CAN,使控制器进入正常监听/通信状态。 */

/* canif.h:声明本课新增的中断接收专用封装。 */
uint32_t Can_Recv_IT_Msg(uint32_t *id, uint8_t *buf); /* 通过指针带回标准 ID 和数据,通过返回值带回 DLC。 */

/* canif.c:中断回调中使用的接收函数。 */
uint32_t Can_Recv_IT_Msg(uint32_t *id, uint8_t *buf) /* 定义不再主动轮询 FIFO 填充数量的接收封装。 */
{
	CAN_RxHeaderTypeDef RxMessage; /* 定义局部帧头,用来接收 ID、DLC 等信息。 */

//	if(HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_FILTER_FIFO0) == 0) /* 原轮询检查被课堂注释:挂起回调只在 FIFO0 有报文时进入。 */
//		return 0; /* 与上面的检查一起停用。 */

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

	*id = RxMessage.StdId; /* 把本帧标准 ID 写回共享 Recv_ID。 */

	return RxMessage.DLC; /* 把本帧 DLC 写回共享 Recv_Len。 */
}

/* main.c:中断与主循环共享的变量。 */
uint8_t Rx_Data[8]; /* 保存中断回调取出的经典 CAN 数据区。 */
uint8_t OLED_BUFF[20]; /* 保存格式化后的 OLED 显示字符串。 */
uint32_t Recv_ID = 0; /* 保存中断回调带回的标准 ID。 */
uint32_t Recv_Len = 0; /* 保存中断回调带回的 DLC。 */
uint8_t Flag=0; /* 0 表示暂无待处理帧,1 表示回调已经放入一帧数据。 */

/* stm32f1xx_it.c:CubeMX 生成的 CAN RX0 中断入口。 */
void USB_LP_CAN1_RX0_IRQHandler(void) /* STM32F103 高密度器件中,USB 低优先级与 CAN1 RX0 共用该向量名称。 */
{
	HAL_CAN_IRQHandler(&hcan); /* 交给 HAL 检查 CAN 中断标志并分派到相应回调。 */
}

/* main.c:HAL 的 FIFO0 报文挂起弱回调被用户实现覆盖。 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) /* FIFO0 至少有一帧且通知已使能时由 HAL 调用。 */
{
	Recv_Len = Can_Recv_IT_Msg(&Recv_ID, Rx_Data); /* 在中断上下文中尽快取出一帧,并保存 ID、DLC 和数据。 */
	Flag=1; /* 通知主循环处理共享缓冲区,避免在中断中打印或刷新 OLED。 */
}

/* main.c:ZET6 接收分支改为由 Flag 驱动,而不是每轮查询 FIFO。 */
if(Flag==1) /* 仅在中断回调置位后进入一次报文处理。 */
{
	printf("recv id 0x%x:\r\n", Recv_ID); /* 显示中断回调保存的标准 ID。 */
	printf("recv length %u:\r\n", Recv_Len); /* 显示中断回调保存的 DLC。 */
	printf("recv data:"); /* 输出数据行提示。 */
	for(int i=0;i<Recv_Len;i++) /* 按本帧 DLC 遍历接收缓冲区。 */
		printf(" %u ",Rx_Data[i]); /* 逐字节输出数据。 */
	printf("\r\n"); /* 一帧串口输出结束后换行。 */

	OLED_ShowString(32, 0, "Can Test",16); /* 在 OLED 首行显示标题。 */
	sprintf(OLED_BUFF,"H:%u  T:%u",Rx_Data[0],Rx_Data[2]); /* 格式化湿度和温度整数。 */
	OLED_ShowString(0, 2, OLED_BUFF,16); /* 在第二个 16 像素文本行显示测量值。 */

	Recv_Len=0; /* 处理完成后清除长度;课程也曾用它兼任“有新帧”状态。 */
	Flag=0; /* 清除待处理标志,等待下一次中断回调置位。 */
	memset(Rx_Data, 0, 8); /* 最后清空接收缓冲区。 */
}

源码范围说明:上面收录第 189 课实际新增的 CAN 通知、IRQ 调用链、回调、共享变量、标志位和中断专用接收函数。DHT11、OLED 与发送函数沿用前课,不重复抄录。代码依照配套 05Can_DHT11_OLED_IT 保留,未擅自增加 volatile、临界区、队列或 HAL 错误检查;这些工程风险在正文中展开。

一、CAN数据中断接收

本课把 ZET6 从“主循环不停询问 FIFO0 是否有帧”改成“FIFO0 有帧时由硬件触发中断”。完整调用链是:

CAN 帧通过过滤器进入 FIFO0
        ↓
FIFO0 message pending 条件成立
        ↓
NVIC 进入 USB_LP_CAN1_RX0_IRQHandler()
        ↓
HAL_CAN_IRQHandler(&hcan)
        ↓
HAL_CAN_RxFifo0MsgPendingCallback()
        ↓
Can_Recv_IT_Msg() 取帧,Flag = 1
        ↓
主循环打印并刷新 OLED,随后 Flag = 0

中断模式减少了无意义的 FIFO 查询和错误日志,但不是“完全不占 CPU”:每帧到达仍会进入 ISR,HAL 分派和取帧也有开销。报文很密集或每帧处理很轻时,轮询、批量读取、DMA/RTOS 等方案各有取舍;本课低频 DHT11 报文适合用中断演示。

1.1 课前回顾

前一工程的接收端每次循环都调用 Can_Recv_Msg()。发送端读取 DHT11 有通信耗时,本课开始前又在发送分支加入 HAL_Delay(100),所以两帧之间存在明显间隔;接收端在这段间隔里多次得到 0,并连续打印 recv eorro。

这不是 CAN 总线持续出错,而是应用把“当前无帧”误命名为“接收错误”。中断模式让主循环只在新帧事件发生后处理,因此不再需要无数据时的 else 错误打印。

课程还给发送端增加数据打印:循环输出 Data[0..SEND_LEN-1],便于同时比较发送端 DHT11 数据和接收端 CAN 数据。该改动已经体现在配套源码,但属于前课后的辅助观测代码,不是 CAN 中断机制本身。

需要保留一个严谨提醒:DHT11 的允许采样周期应以具体器件规格书为准,100 ms 只是课程用来缓解连续读取现象的延时,不应直接视为满足所有 DHT11 版本的推荐周期。

1.2 新建工程

课程从 04Can_DHT11_OLED 复制出 05Can_DHT11_OLED_IT。IT 在 STM32 HAL 命名中常表示 interrupt(中断)模式。

随后在 CubeMX 的 CAN → NVIC Settings 中勾选 CAN RX0 中断,并把抢占优先级设为 1、子优先级为 0。因为过滤器一直把通过的报文分配到 FIFO0,所以选择 RX0;若过滤器分配到 FIFO1,则应配置 RX1 并使用对应回调,不能随意交叉。

生成代码后可在 .ioc 中看到 USB_LP_CAN1_RX0_IRQn 已启用,在 stm32f1xx_it.c 中看到同名 IRQHandler 调用 HAL_CAN_IRQHandler(&hcan)。

1.3 bxCAN简介

bxCAN 有两个独立接收 FIFO:FIFO0 和 FIFO1。过滤器不仅决定帧能否通过,还决定通过后进入哪一个 FIFO。本项目的过滤器配置为:

Can_FilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;

因此本课使用 CAN_IT_RX_FIFO0_MSG_PENDING 和 HAL_CAN_RxFifo0MsgPendingCallback()。如果只开启 NVIC 而不使能 CAN 外设内部的消息挂起通知,CPU 仍不会因该事件进入期望回调。

1.3.1 bxCAN 接收中断模式

原笔记的“bxCAN 处于调试模式”是误识别。本节讲的是 中断模式,不是 CAN 的 Debug Freeze、静默模式或回环调试模式。

中断生效需要三层都连通:

  1. 过滤器把帧送入 FIFO0;
  2. HAL_CAN_ActivateNotification() 打开 FIFO0 message pending 中断源;
  3. NVIC 使能 USB_LP_CAN1_RX0_IRQn,其 ISR 再调用 HAL 处理函数。

只完成其中一层都不够。课程第一次烧写后全是错误,原因正是生成了 NVIC 入口和用户回调,却尚未调用 HAL_CAN_ActivateNotification()。此外,示例没有检查该函数的返回值;正式代码应在启动阶段确认它返回 HAL_OK,否则中断链路可能根本没有启用。

1.3.2 例题 1:bxCAN 接收数据与中断处理

HAL_CAN_IRQHandler() 会读取 CAN 中断使能与状态。当 FIFO0 message pending 已使能且 FIFO0 仍有消息时,它调用用户覆盖的 HAL_CAN_RxFifo0MsgPendingCallback()。

回调函数参数 hcan 指向触发中断的 CAN 句柄。课堂代码没有使用它,因为工程只有 CAN1;可扩展工程通常应确认 hcan->Instance,避免多个 CAN 实例共用回调时混淆。

局部变量只能在其函数作用域内访问,所以原先定义在 while(1) 内的 Recv_ID、Recv_Len 必须移到文件作用域,才能同时被回调和主循环使用。Rx_Data 本来就是全局数组。

第一次改造只用 Recv_Len > 0 兼任“长度”和“新数据标志”。回调将它设为 5 后,主循环会持续进入处理块;第一次末尾又把 Rx_Data 清零,于是屏幕和日志很快反复显示 0。课程随后在处理末尾加入 Recv_Len=0,确保每次回调只触发一次主循环处理。

进一步又加入独立 Flag:

  • 回调取帧后 Flag=1;
  • 主循环只在 Flag==1 时处理;
  • 处理完将 Flag=0。

这样 Recv_Len 只表达 DLC,Flag 表达“有一帧待处理”,职责更清楚。

1.4 bxCAN工作模式

课程将处理拆成类似“中断上半部/下半部”的结构:

位置做什么为什么
ISR/回调从 FIFO0 取帧,保存 ID、DLC、数据,置位标志尽快释放 FIFO 并退出中断
主循环printf、sprintf、OLED I²C 刷新、业务判断这些操作耗时,放在中断里会延长中断占用

“上半部/下半部”是对设计思想的类比;裸机 STM32 中这里没有操作系统真正调度的软中断下半部,主循环只是通过共享标志完成延后处理。

Can_Recv_IT_Msg() 从轮询版复制而来,并去掉 FIFO 填充数量检查,因为 message pending 回调本身就是由 FIFO 有帧触发。它仍有两个课堂遗留问题:

  • HAL_CAN_GetRxMessage() 返回值没有检查,回调无条件把 Flag 置 1;
  • 第二个参数仍写成语义不匹配的 CAN_FILTER_FIFO0,严谨名称应为 CAN_RX_FIFO0,虽然本 HAL 中二者数值都是 0。

中断与主循环共享变量还需要理解并发风险。课堂的 Recv_ID、Recv_Len、Flag 没有声明为 volatile,编译器并不知道它们会被异步 ISR 修改;在优化构建中,这可能导致主循环观察不到及时变化。加 volatile 只解决“每次都重新读写”的可见性,不会自动解决下列竞争:

  1. 主循环打印/刷新 OLED 时,下一帧中断可能覆盖同一个 Rx_Data;
  2. 新中断可能在主循环 Flag=0 或 memset() 前后到达,造成新帧标志丢失或数据被清零;
  3. 单一 Flag 最多表达“至少一帧”,无法累计多帧,帧率升高时会丢事件。

本实验约百毫秒级一帧,而串口和 OLED 处理通常能在下一帧前完成,所以现象可用;可靠系统应采用临界区保护、双缓冲或环形队列,并统计 FIFO 溢出。

1.5 课程总结

完成 CAN FIFO0 中断接收需要记住四个对象,而不是只背一个回调函数:

  1. CubeMX/NVIC:使能 USB_LP_CAN1_RX0_IRQn;
  2. CAN 通知:调用 HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
  3. 中断入口:USB_LP_CAN1_RX0_IRQHandler() 调用 HAL_CAN_IRQHandler();
  4. 用户回调:HAL_CAN_RxFifo0MsgPendingCallback() 取帧并通知主循环。

处理多个 CAN 报文时,可以在主循环根据 Recv_ID 使用 switch 分派不同业务:每个 ID 对应一种报文定义、长度和解析函数。但“能接收多个 ID”首先受硬件过滤器配置影响,也受单缓冲/队列容量影响;仅增加 switch 不会解决高并发覆盖问题。

本节中断路径及 API 语义可对照项目内 stm32f1xx_hal_can.c/.h 与 ST 官方 RM0008 bxCAN 章节。