|原创作者| Q哥
先问大家一个隐私习惯,吃茶叶蛋的时候,你会先磕破鸡蛋比较小的那一端,还是比较大的那一端?
这个看似无厘头的问题,曾经引发了两个小国家持续不断的战争,好奇的读者可以自行查阅《格列佛游记》。这部小说也是big endian(大端)和little endian(小端)两个词汇的来源。
数据在memory中存储,以及在总线传输的时候,同样也会面临大小端问题。这个蛋疼的问题之所以存在,就好比各个国家的插座不兼容一样,都是历史遗留问题。
对于验证人员来说,要实现memory的后门读写,编写RAL寄存器模型的adapter,调试串口,这些都需要对大小端有所了解。
数据在memory中存储的时候,低地址存放低Byte,高地址存放高byte, 称为Little Endian存储。反之,低地址存放高Byte,高地址存放低Byte,称为Big Endian存储。
1.1 大端存储
例如,对于”learn verification with jerry_ic”这个字符串,对于64位大端存储,byte的排布如下:
地址 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
数据 | 6c | 65 | 61 | 72 | 6e | 20 | 76 | 65 |
字符 | l | e | a | r | n | 空格 | v | e |
地址 | F | E | D | C | B | A | 9 | 8 |
数据 | 72 | 69 | 66 | 69 | 63 | 61 | 74 | 69 |
字符 | r | i | f | i | c | a | t | i |
地址 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 |
数据 | 6c | 65 | 61 | 72 | 6e | 20 | 76 | 65 |
字符 | o | n | 空格 | w | i | t | h | 空格 |
地址 | 1F | 1E | 1D | 1C | 1B | 1A | 19 | 18 |
数据 | 72 | 69 | 66 | 69 | 63 | 61 | 74 | 69 |
字符 | j | e | r | r | i | _ | i | c |
因为64位相当于8个字符,所以memory第一行就可以存储”learn空格ve”这8个字符。
按照高地址先存低byte,低地址存高byte这个大端规则:第一个byte,专业术语叫LSB(least significant byte),也就是字符l(字母L小写)的ASCII码值6c,存入地址7;而这8个字符里最后一个byte,专业术语叫MSB(most significant byte),即字符e的ASCII码值65,存入地址0。
从memory中读出第一行数据到logic[63:0] BigEndianData64这个变量,则BigEndianData64 == 64'h6c_65_61_72_6e_20_76_65。
这一行的8个byte,每个byte所在位置称为一个byte lane。有些memory,可以按照各个byte lane 是否enable决定只写某几个byte。这时候也要根据大端原则来给对应的byte lane enable信号。
紧接着,memory的第二行可以存储rificati这8个字符。
以此类推......
这里之所以强调行号,因为在验证环境里面,对memory进行后门读写的时候,通常需要直接用行号进行索引。
好奇的读者可能要问了,如果最后一行凑不够8个byte怎么办?
好问题!依然是先写高地址,再写低地址。所以如果只有一个byte ’hff, 会存入高地址,那么这一行的数据就是64'hff_xx_xx _xx _xx _xx _xx _xx。
1.2 小端存储
对于“learn verification with jerry_ic”这个字符串,在64位小端存储的内存里是这样的:
地址 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
数据 | 65 | 76 | 20 | 6e | 72 | 61 | 65 | 6c |
字符 | e | v | 空格 | n | r | a | e | l |
地址 | F | E | D | C | B | A | 9 | 8 |
数据 | 69 | 74 | 61 | 63 | 69 | 66 | 69 | 72 |
字符 | i | t | a | c | i | f | i | r |
地址 | 17 | 16 | 15 | 14 | 13 | 12 | 11 | 10 |
数据 | 65 | 76 | 20 | 6e | 72 | 61 | 65 | 6c |
字符 | 空格 | h | t | i | w | 空格 | n | o |
地址 | 1F | 1E | 1D | 1C | 1B | 1A | 19 | 18 |
数据 | 69 | 74 | 61 | 63 | 69 | 66 | 69 | 72 |
字符 | c | i | _ | i | r | r | e | j |
跟64位大端存储对比,每一行的存储数据是一样的,只不过这一行里每个byte的排布是完全反的。
代码片段1
按照低地址先存低byte,高地址存高byte这个小端规则:第一个byte,专业术语叫LSB,也就是字符l(字母L小写)的ASCII码值6c,存入地址0;而这8个字符里最后一个byte,专业术语叫MSB,即字符e的ASCII码值65,存入地址7. 从memory中读出第一行数据到logic[63:0] LittleEndianData64这个变量,则LittleEndianData64时,LittleEndianData64 == 64'h65_76_20_6e_72_61_65_6c。
紧接着,memory的第二行可以存储rificati这8个字符。
以此类推......
最后一行如果只有一个byte ’hff, 会存入低地址,这一行的数据是64'hxx_xx_xx _xx _xx _xx _xx _ff。
对于32位大端存储,byte的排布如下:
地址 | 3 | 2 | 1 | 0 |
---|---|---|---|---|
数据 | 6c | 65 | 61 | 72 |
字符 | l | e | a | r |
地址 | 7 | 6 | 5 | 4 |
数据 | 6e | 20 | 76 | 65 |
字符 | n | 空格 | v | e |
地址 | B | A | 9 | 8 |
数据 | 72 | 69 | 66 | 69 |
字符 | r | i | f | i |
地址 | F | E | D | C |
数据 | 63 | 61 | 74 | 69 |
字符 | c | a | t | i |
地址 | 13 | 12 | 11 | 10 |
数据 | 6c | 65 | 61 | 72 |
字符 | o | n | 空格 | w |
地址 | 17 | 16 | 15 | 14 |
数据 | 6e | 20 | 76 | 65 |
字符 | i | t | h | 空格 |
地址 | 1B | 1A | 19 | 18 |
数据 | 72 | 69 | 66 | 69 |
字符 | j | e | r | r |
地址 | 1F | 1E | 1D | 1C |
数据 | 63 | 61 | 74 | 69 |
字符 | i | _ | i | c |
这里需要注意下,对于数据位宽是32位的大端存储,相当于把上面64位大端memory的一行,拆成了两行进行存储。只不过这两行存储的时候,即先存储地址范围高的那4个byte,再存储地址范围低的那4个byte 。
代码片段2
在32位小端存储的内存里是这样的:
地址 | 3 | 2 | 1 | 0 |
---|---|---|---|---|
数据 | 72 | 61 | 65 | 6c |
字符 | r | a | e | l |
地址 | 7 | 6 | 5 | 4 |
数据 | 65 | 76 | 20 | 6e |
字符 | e | v | 空格 | n |
地址 | B | A | 9 | 8 |
数据 | 69 | 66 | 69 | 72 |
字符 | i | f | i | r |
地址 | F | E | D | C |
数据 | 69 | 74 | 61 | 63 |
字符 | i | t | a | c |
地址 | 13 | 12 | 11 | 10 |
数据 | 72 | 61 | 65 | 6c |
字符 | w | 空格 | n | o |
地址 | 17 | 16 | 15 | 14 |
数据 | 65 | 76 | 20 | 6e |
字符 | 空格 | h | t | i |
地址 | 1B | 1A | 19 | 18 |
数据 | 69 | 66 | 69 | 72 |
字符 | r | r | e | j |
地址 | 1F | 1E | 1D | 1C |
数据 | 69 | 74 | 61 | 63 |
字符 | c | i | _ | i |
对于数据位宽是32位的小端存储,相当于把上图64位小端memory的一行,拆成了两行进行存储;先存低byte,再存高byte。
代码片段3
32位小端相比于32位大端就比较清晰简单了,只是把byte的顺序颠倒了一下。
代码片段4
总线传输的时候,同样有大小端问题。这里按照总线是并口还是串口,分别说明。
2.1 并口总线
对于并口总线,MSB传输低地址数据,LSB传输高地址数据,即为大端传输。反之, LSB传输低地址数据,MSB传输高地址数据,即为小端传输。
这里抛开具体协议,举个例子:对于数据位宽是32位的总线,即xfer_data[31:0],有4个byte lane,其中xfer_data[7:0]称为LSB,xfer_data[31:24]称为MSB。这时候要传输地址0到3存储的4个byte。
如果xfer_data[7:0]对应地址0,xfer_data[15:8]对应地址1,xfer_data[23:16]对应地址2,xfer_data[31:24]对应地址3,这样的总线就是小端传输。常见的有AHB/AXI等。
反之,如果xfer_data[7:0]对应地址3,xfer_data[15:8]对应地址2,xfer_data[23:16]对应地址1,xfer_data[31:24]对应地址0,这样的总线就是大端传输。
这里同样存在凑不够32bit的问题。但是因为每一byte数据都有对应的地址,只使用该地址对应的byte lane就好了。
那么问题来了?同样使用小端总线,传输的时候不用考虑memory是大端还是小端吗?
总线协议千差万别,Q哥没法给大家肯定的答复。只能说,常见的总线都是byte-invariant的,也就是说,不用管它是大端memory还是小端memory,地址0对应的byte一定是放到小端总线的xfer_data[7:0] 进行传输的,以此类推。
对于总线传输数据位宽与memory存储数据位宽相等的系统,直接整行进行读写以及整行传输就好了。
对于总线位宽与memory存储位宽不匹配的系统,就需要考虑转接适配了。通常系统里面总线位宽和存储位宽是整数倍关系,只需要计算好每次传输和memory读写的地址关系就可以了。
总线位宽大于存储位宽,相当于总线上一拍数据传输,需要读写N次memory。总线位宽小于存储位宽, 相当于读写一次memory,需要总线上N拍数据传输才能完成。按照上面64位转32位的对应关系就可以了。
总线协议虽然千差万别,但是大家只要弄清楚每一拍数据对应的地址是怎么变化的,就可以游刃有余,以不变应万变。请持续关注【杰瑞IC验证】,后面会为大家带来AXI等常见总线的协议讲解,敬请期待!
2.2 串口总线
对于串口总线,就比较复杂了。不单单要考虑byte的大小端,甚至要考虑bit的大小端了。也就是说,对于一拍传输一个bit的串口总线,传输16’h1234有4种组合:
先发MS Byte,再发LS Byte(ByteBigEndian);并且,每个byte先发MS bit, 再发LS bit(Bit BigEndian)
先发MS Byte,再发LS Byte(ByteBigEndian);并且,每个byte先发LS bit, 再发MS bit(Bit LittleEndian)
先发LS Byte,再发MS Byte(Byte LittleEndian);并且,每个byte先发MS bit, 再发LS bit(Bit BigEndian)
先发LS Byte,再发MS Byte(Byte LittleEndian);并且,每个byte先发LS bit, 再发MS bit (Bit LittleEndian)
这里看似复杂,但实际上对照波形仔细检查,很容易核对。
当你搭好验证环境,开始调试的时候,发现灌到RTL上的激励或者抓到的输出结果完全对不上的时候,不要慌,有可能只是大小端搞错了。
某些项目可能因为传承原因,参考模型所提供的参考数据跟实际需要的大小端不一致。这时候只需要按照上面代码片段修改一下数据的大小端排布就好了。
另外,为了快速调试大小端问题,可以把数据设置为’h12345678这样子递增的模式。这样查看仿真log 或者波形,都是一目了然的。
VIM或者UltraEdit等文本编辑工具,都可以查看二进制文件的byte内容,如下图所示:
这里简单解释下:第1列是每行数据的起始地址,第2到第9列是每个地址存储的byte内容(地址从低到高增加),最右边是第2到第9列对应的字符。这个图相当于是一个128位(每行16byte)的小端存储器显示。
Q哥今天给大家讲述了数据存储和总线传输的大小端问题。大家在集成RAL模型的时候,需要注意RAL adapter是否需要修改地址和数据匹配的代码。后门读写memory、解析仿真模型提供的参考数据时,都要注意大小端。具体的案例,先卖个关子,且听Q哥下回分解。
——The End——