前言
在前几天写完【芯片前端】保持代码手感——异步FIFO全解析之后自我感觉非常良好,觉得异步fifo的问题我已经全部拿捏了,没想到今天突然想到一个我自己代码里的bug。
代码中的BUG
事情的源头在同步fifo里,深度任意可配的同步fifo里使用了两个非饱和的cnt计数器记录读写地址指针,waddr和raddr均比实际地址多一位,最高位用来指示套圈情况。当waddr和raddr的最高位相同时,fifo_cnt = waddr-raddr;当waddr和raddr的最高位相反时,fifo_cnt = DEPTH + waddr[ADDR_WIDTH-1:0] - raddr[ADDR_WIDTH-1:0]。最高位作为标志位,当低位计数到DEPTH-1时,高位翻转,代码大意如下:
assign waddr_d_h = (waddr[DP_WD-1:0] == DEPTH-1) ? ~waddr[DP_WD] : waddr[DP_WD];
assign waddr_d_l = (waddr[DP_WD-1:0] == DEPTH-1) ? 0 : waddr[DP_WD-1:0] + 1;
always @(posedge clk or negedge rst_n)begin
if(~rst_n) waddr <= 0;
else if(wenc) waddr <= {waddr_d_h, waddr_d_l};
end
假设fifo深度设为10,那么waddr的跳转为:
高比特 | 低比特 | 指针数值 |
0 | 0000 | 0 |
0 | 0001 | 1 |
... | ... | ... |
0 | 1001 | 9 |
1 | 0000 | (套圈)0 |
1 | 0001 | (套圈)1 |
... | ... | |
1 | 1001 | (套圈)9 |
0 | 0000 | 0 |
在同步fifo里这样做是没有问题的,但是问题出现在,把这个代码代入到异步fifo中的时候。
异步fifo中使用格雷码对addr进行跨异步,关键处理是二进制 - 格雷码 - 跨异步 - 二进制处理路径 。而之所以能够使用格雷码跨异步是因为格雷码相邻的两个数只相差1bit,而深度为10的异步fifo,存在'b01001('d9) -> 'b10000('d16)的跳转,这个就不是相邻跳转了,相应的格雷码变化也不是单比特的跳转:01101 -> 11000,显然的这会导致空满判断出现逻辑错误。
BUG修复
这个问题的关键就在于不饱和计数在边界的跳变导致的格雷码不连续,所以先观察格雷码。十进制0~15对应的二进制和格雷码如下表:
十进制 | 二进制 | 格雷码 |
0 | 0000 | 0000 |
1 | 0001 | 0001 |
2 | 0010 | 0011 |
3 | 0011 | 0010 |
4 | 0100 | 0110 |
5 | 0101 | 0111 |
6 | 0110 | 0101 |
7 | 0111 | 0100 |
8 | 1000 | 1100 |
9 | 1001 | 1101 |
10 | 1010 | 1111 |
11 | 1011 | 1110 |
12 | 1100 | 1010 |
13 | 1101 | 1011 |
14 | 1110 | 1001 |
15 | 1111 | 1000 |
显而易见的一条规律是,相邻两个数只有1bit不一样。进一步可以发现,3->0,7->0,15->0的编码也是“连续的”,即只有1bit不同。为了更好地观察规律,我们做出格雷码的码盘来观察,码盘如下图,红色为1白色为0,同心圆内为高比特,外部为低比特,如十进制2的格雷码为0011,对应的图就是“白白红红”。
盯一会码盘就会发现码盘的“对称性”和“连续性”,比如说1和14、2和13、6和9、7和8在码盘上处于轴对称的位置,而格雷码编码值恰好相差1bit。
那么也就是说,如果当前的值是5,那么下一个数想要保持格雷码连续性,就只能跳4、6或者10:
十进制5 = 格雷码:0111 | 十进制4 = 格雷码:0110 |
十进制6 = 格雷码:0101 | |
十进制10 = 格雷码:1111 |
显然,此时只能跳10才能满足最高位作为标志位,当低位计数到DEPTH-1时,高位翻转的条件,也就是说我们实际要由5('b0101) -> 8('b1000)变成了由5('b0101) -> 10('b1010),所以说实际多条了两个数,因此实际cnt的值是10 - 偏移量,此时的偏移量是2,fifo的深度为6(所以低位计数计到5)。
那么继续推导,当深度配置为5,那么低位满4(格雷码:0110)跳转为11(格雷码:1110),实际的cnt值是11 - 3 = 8('d1000),偏移量为3。进一步的我就不推了,根据码盘也可以发现,偏移量 = 2^N - 配置深度,低位计满跳转值 = 2^N + 偏移量。用代码表示这几个参数:
parameter DEPTH = 11;
parameter WIDTH = $clog2(DEPTH);//4
parameter DEPTH_TO2 = 2 ^ WIDTH;//16
parameter SHIFT = DEPTH_TO2 - DEPTH;//16-11=5
还是以深度11为例,看一下这种策略下的指针跳转:
高比特 | 低比特 | 数值(二进制) | 数值(格雷码) | 实际指针数值 |
0 | 0000 | 0 | 0_0000 | 0 |
0 | 0001 | 1 | 0_0001 | 1 |
... | ... | ... | ... | ... |
0 | 1010 | 10 | 0_1111 | 10 |
1 | 0101 | 21 | 1_1111 | 1_0101 - 101('d5) = 1_0000,0 |
1 | 0110 | 22 | 1_1101 | 1_0110 - 101 = 1_0001,1 |
... | ... | ... | ... | ... |
1 | 1111 | 31 | 1_0000 | 1_1111 - 101 = 1_1010,10 |
0 | 0000 | 0 | 0_0000 | 0 |
OK,策略上没有问题,进一步的异步FIFO代码组织放到下次,今天太晚了。