第3章 FIFO基本概念:数据结构、读写指针、空/满标志逻辑
搞CAN FD控制器,FIFO是绕不开的硬骨头。很多人一上来就盯着协议细节,结果被溢出折腾得死去活来。我个人习惯,先扎扎实实把FIFO本身搞透。这一章就聊三个最核心的东西:数据结构到底怎么摆、读写指针怎么动、空和满怎么判断。
3.1 数据结构:环形缓冲区
FIFO在硬件里最常用的实现是环形缓冲区。说白了,就是一个固定大小的数组,配上两个指针。数组的每个格子放一份报文数据——CAN FD的报文体量比传统CAN大不少,所以每个格子通常64字节打底。你想想看,如果不用环形,每次读写都搬数据,那延迟和面积谁也扛不住。
环形怎么理解?数组首尾相接。写指针走到尾部后,下一个地址自动绕回开头。读指针也一样。整个结构就是个圆环,数据在里面转圈圈。
我在项目中遇到过一个问题:有人把环形缓冲区实现成链表,觉得灵活。结果因为硬件里链表节点需要额外存储指针,面积飙上去,时序还跑不过。后来全改成数组+环形,问题就没了。所以硬件FIFO,老老实实用数组。
环形缓冲区核心参数:
- 深度:能存多少帧。CAN FD控制器常见深度8、16、32、64。
- 宽度:每帧数据字段长度 + 控制位 + 时间戳。至少64字节 + 若干bit。
- 基地址:数组起始地址,硬件中就是一组寄存器或SRAM空间。
3.2 读写指针
两个指针分别是写指针(写索引、写偏移)和读指针(读索引、读偏移)。写指针指向下一个要写入的空闲位置,读指针指向下一个要读出的已存数据位置。
每次写入一帧后,写指针递增;每次读取一帧后,读指针递增。注意,递增不是简单的加1,而是加1后对FIFO深度取模,实现环绕效果。嗯,这里要注意:取模操作在硬件里可以用位与实现——前提是深度是2的幂。这是我踩过的坑,有一次选了深度12,结果硬件里取模得用除法器,延迟和面积都爆炸。后来所有设计强制深度为2的幂。
硬件设计小技巧:深度选择2N,这样指针递增后只需要与深度减1进行按位与,就能自动环绕。比如深度16(0~15),指针加1后 & 0x0F即可。
3.3 空/满标志逻辑
这个才是FIFO的核心难点。环形缓冲区里,空和满看起来都是读写指针相等,那怎么区分?
三种常见做法:
- 比较指针值——最朴素。写指针追上读指针就是满,读指针追上写指针就是空。但两者相等时到底空还是满?必须靠额外逻辑。比如加一个“满标志”寄存器,当写操作刚好使指针等于读指针时置位,读操作清除。这个方法容易因毛刺误判。
- 用额外标志位——给每个指针加1个额外bit。比如深度16,指针实际用5位,最高位作为翻转标志。写指针溢出一轮后最高位翻转,读指针同理。这样空满判定变成:完全相等为同时翻转则为空?嗯,其实更常见的是用格雷码指针,跨时钟域同步时特别好用。
- 计数器方式——用一个计数器记录当前FIFO内数据帧数。写入加1,读出减1。计数器为0时空,等于深度时满。我最喜欢这种方法,简单可靠,还能顺便提供“半满”“大约半满”等水位信息。在CAN FD控制器里,水位信号可以用来触发中断,提前预警资源紧张。
我曾经在项目里用过第一种(比较指针),结果在异步跨时钟域时,指针同步延迟导致空满判断瞬间出错,溢出没被检测到,丢了好几帧报文。后来换成计数器+格雷码同步,就再没出过类似问题。如果你在做高速CAN FD(8Mbps以上),时钟域跨来跨去,我建议你直接上计数器方式。
避坑指南:千万不要以为空满逻辑只是个小判断。我见过一个案例,设计者用组合逻辑直接比较读写指针的实时值,结果因为门延迟,在指针跳变沿产生毛刺,误判为满,把数据卡死在外面。一定要加同步器和多级打拍。特别是异步FIFO,空满信号必须用格雷码跨时钟域。
3.4 FIFO操作流程
| 操作 | 条件 | 动作 | 指针变化 |
|---|---|---|---|
| 写请求 | 写使能 & 不满 | 将报文写入写指针指向的位置 | 写指针加1取模 |
| 读请求 | 读使能 & 不空 | 从读指针指向的位置读取报文 | 读指针加1取模 |
| 溢出检测 | 写请求 & 满 | 丢弃数据,置位溢出标志 | 指针不变 |
| 空读检测 | 读请求 & 空 | 返回无效数据,置位空读错误 | 指针不变 |
上面这个表,你写代码、配寄存器的时候最好对着看。CAN FD控制器里,溢出标志通常是一个可读可写清零的寄存器位,触发了就引起中断。如果你不处理中断,后面来的报文全被扔了——这就是最常见的溢出问题。
3.5 环形缓冲区结构图
下面这张图把环形缓冲区、读写指针、空满标志的关系画清楚了。你多看两眼,后面定位溢出时就靠它。
我的习惯:在CAN FD控制器设计初期,我会把FIFO的读写指针和空满标志单独引出来,放在仿真波形的最前几行。这样一旦溢出发生,一看波形就能秒懂是什么时候满的、哪个指针没跟上。省去很多排查时间。