导言
完成本章节的学习后,CAN驱动代码已经具备实战项目的大部分功能,完全可以在工作上使用:
- CAN发送有串行与中断方式,且三个发送邮箱都使用了;
- CAN接收用FIFO1的三个邮箱与高效的数据结构ringbuffer。另外,还有接收FIFO1的溢出监控。
如上所示,本章节的目的是在《[[STM32F103_HAL库+寄存器学习笔记17 - CAN中断接收 + 接收CAN总线所有报文]]》的基础上增加强大的数据结构ringbuffer。ringbuffer是嵌入式MCU开发必须掌握的数据结构。优点如下:
- 缩短中断处理时间
在中断服务程序(ISR)中,只需要尽快把接收到的数据存入ringbuffer,然后立即退出中断。这样可以保证中断处理时间尽可能短,降低对系统其他高优先级任务的影响。短时间的中断处理对实时性要求较高的系统尤为重要。 - 解耦中断与数据处理
将数据先存入ringbuffer,可以把数据处理工作从中断上下文中剥离出来,在主循环或者专门的任务中再进行数据解析和处理。这种方式降低了中断内执行复杂计算或处理逻辑时对整个系统实时响应的负面影响。 - 缓冲突发数据
在CAN总线上,当有数据突发或短时间内大量数据到达时,ringbuffer能够临时缓存这些数据,避免中断处理时间不足导致数据丢失。即使主循环在短时段内处理不过来,ringbuffer可以起到缓冲作用,从而增强系统的鲁棒性。 - 保证数据顺序
环形缓冲区本质上是先进先出的(FIFO)结构,能够保持接收到的数据顺序一致性。这对于需要按照接收顺序依次处理的数据帧来说非常重要。 - 提高系统整体效率与可维护性
将数据接收和数据处理分离后,系统各部分的功能更加模块化,便于后续维护与扩展。中断函数只负责数据的快速存储,而数据处理模块可以采用更灵活的调度机制进行处理和响应。 - 防止中断嵌套和竞态条件
使用ringbuffer还可以降低在中断中长时间占用处理资源带来的潜在问题,如中断嵌套或与其他共享资源的竞态问题。只需注意在操作ringbuffer时进行合适的同步(如临界区保护、原子操作等),以确保数据一致性。
总之,在CAN接收中断中采用ringbuffer设计能够提高系统的实时性、稳定性以及灵活性,既减少了中断处理时间,又能有效管理和处理突发数据,从而为整个系统构建一个高效可靠的数据处理链路。
如上所示,本章节的实验结果是CAN分析仪将CAN报文发送到CAN总线上,接着,STM32F103接收到CAN总线上的CAN报文后,立即将CAN报文发送到CAN总线上。随后,CAN分析仪能收到刚才发送出去的CAN报文。
程序的效果如下:
如上所示,CAN分析仪一共发送5个CAN报文,ID从0x201~0x205。接着,STM32F103将5个CAN报文返回。
项目地址:
github:
- HAL库:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_hal_library19_Can_Rec_With_RB
- 寄存器方式:https://github.com/q164129345/MCU_Develop/tree/main/stm32f103_ll_library19_Can_Rec_With_RB
gitee(国内)
- HAL库:https://gitee.com/wallace89/MCU_Develop/tree/main/stm32f103_hal_library19_Can_Rec_With_RB
- 寄存器方式:https://gitee.com/wallace89/MCU_Develop/tree/main/stm32f103_ll_library19_Can_Rec_With_RB
一、开源ringbuffer - LwRB
1.1、简单介绍
官方:https://docs.majerle.eu/projects/lwrb/en/latest/index.html
如上所示,LwRB的功能很齐全。一般,新手应该先从怎样放入消息与怎样取出消息先开始。
二、代码(HAL库)
2.1、myCanDrive.c
如上两图所示,完成ringbuffer的初始化准备工作。另外,为什么sizeof(g_CanRxRBDataBuffer) + 1
? 不是直接sizeof(g_CanRxRBDataBuffer)
?? 参考后面的内容: [[#5.1、为什么lwbr_init()的第三个入口参数要额外+1?]]
如上所示,当接收到新的CAN报文时,需要:
- 先检查ringbuffer能不能装下最新的CAN报文(有可能主循环处理较慢,导致CAN报文“消化”比较慢);
- ringbuffer装不下最新的CAN报文的话,我一般的做法是清除全局旧的数据。或者,可以用
lwrb_read()
函数读一个包的内容出来丢掉,相当于腾出一个位置给最新的CAN报文; - 最后,将最新的CAN报文放入ringbuffer。
2.2、main.c
如上所示,每隔50ms调用函数CAN_Send_CANMsg_FromRingBuffer()
一次,将ringbuffer里的CAN报文发送出去。
2.3、编译代码
如上所示,代码编译成功。
如上所示,从.map文件看到寄存器程序占用ROM = 5.23KB,占用RAM = 2.86KB。
2.4、debug调试
如上所示,CAN分析仪发送50条CAN报文到CAN总线上,STM32F103一共收到50条CAN报文,并将50条CAN报文发回给CAN分析仪。
三、代码(寄存器方式)
3.1、myCanDrive_reg.c
如上所示,在函数CAN_Config()
里调用lwrb_init()
完成ringbuffer的初始化。
3.2、main.c
3.3、编译代码
编译成功,0错误0警告。
如上所示,从.map文件看到寄存器程序占用ROM = 3.54KB,占用RAM = 2.81KB。
对应的HAL库占用ROM = 5.23KB,RAM = 2.86KB。
3.4、debug调试
如上所示,使用CAN分析仪发送50个CAN报文到CAN总线上,CANID:0x201 ~ 0x232。从全局变量g_RxCount看到,STM32F103收到50个CAN报文,并将所有CAN报文返回给CAN分析仪。
五、细节补充
5.1、为什么lwrb_init()的第三个入口参数要额外+1?
如上所示,先来看源码lwrb.c里第66行的注释"Maximum number of bytes buffer can hold is ‘size - 1’ ",中文意思大概是ringbuffer只能存size - 1个字节,最后一个字节不能用!接下来,实际测试看看。
如上所示,将数组g_CanRxRBDataBuffer的大小从50改为3,目的是方便实验。
如上所示,ringbuffer能接收到第三条CAN报文的最后一个字节,没有丢失数据!!接下来,删除+1试试。
如上所示,ringbuffer初始化函数lwrb_init()里的第三个入口参数从sizeof(g_CanRxRBDataBuffer) + 1
改为sizeof(g_CanRxRBDataBuffer)
。试试效果怎样。
如上所示,ringbuffer丢失了第三条CAN报文的最后一个字节0x08。丢失数据了!!至于作者为什么这样设计,肯定是有原因的。
## 5.2、lwrb库ringbuffer的总缓存大小为size - 1 在设计环形缓冲区(ring buffer)时,通常会选择只使用总缓存区大小的size−1 个字节,这样的设计主要是为了避免在区分“空”和“满”状态时出现歧义。下面详细说明这种设计思路以及它的优点。
- 解决空满状态歧义
状态描述
- 空状态:当读指针(r_ptr)和写指针(w_ptr)相等时,我们希望判断缓冲区为空。
- 满状态:如果允许使用所有的 size 个字节,那么当写指针追上读指针时(即两者相等)就会有两种可能:
- 缓冲区为空(刚开始写入,读写指针都指向同一位置)。
- 缓冲区已经满了(写入的数据已填满整个缓冲区,但由于写指针环绕导致与读指针重合)。
预留一个字节的好处
通过只使用 size−1 个字节来保存数据,可以采用如下约定:
- 当 w_ptr == r_ptr 时,表示缓冲区为空。
- 当 (w_ptr + 1) % size == r_ptr 时,表示缓冲区已满。
这种做法使得区分“空”和“满”状态不再需要额外的标记或计数变量,从而简化了逻辑和实现,也更容易保证代码的线程安全性和鲁棒性。
- 简化状态计算及实现逻辑
- 简单的可用空间计算:在函数
lwrb_get_free()
中,通过计算size - 1 - (已使用字节数)来获得空闲空间。若允许使用全部size个字节,就需要额外判断是否存在一个“满”的标志,这会增加实现的复杂度。 - 无需额外状态变量:如果不预留一个字节,就需要额外的布尔标记或计数器来区分缓冲区是空还是满。预留一个字节的方式简化了代码,同时也降低了出错的风险。
- 实现上的一致性:使用固定的策略(始终预留一个字节),使得所有操作(写入、读取、跳过等)在处理指针环绕问题时的一致性更好,不容易出现边界条件问题。
总之,预留一个字节来存储 size−1 个有效数据的设计是环形缓冲区的常见实现方式,主要原因在于:
- 避免读写指针重合导致的空满状态歧义。
- 简化缓冲区状态的计算逻辑,不需要额外的标记变量。
- 提升代码的健壮性和易维护性。
这种设计在实际的嵌入式系统中尤为常见,因为它保证了在高性能、资源受限的环境下,数据管理逻辑能够保持简单而高效。