第1章:Lattice比特流格式深度解析

各位好,我是老张。做FPGA逆向这么多年,我始终觉得比特流格式是绕不开的硬骨头。今天咱们就来啃一啃Lattice的比特流,看看它到底长什么样。

说实话,我第一次接触Lattice的比特流时,也被那一堆二进制数据搞懵了。但后来我发现,只要掌握了几个关键点,这东西其实挺有规律的。你想想看,FPGA厂商再怎么折腾,底层逻辑逃不出那几个核心模块。

1.1 比特流文件头结构分析

每个Lattice的比特流文件,开头都有一段固定的文件头。我习惯把它叫做「身份证」——它告诉加载器:我是谁、我有多大、我该怎么用。

以Lattice ECP5系列为例,典型的文件头结构是这样的:

// 比特流文件头示例(十六进制)
0xFF 0xFF 0xFF 0xFF  // 同步头(4字节)
0xBB 0x11 0x22 0x44  // 厂商ID + 器件ID
0x00 0x00 0x00 0x10  // 配置数据长度(小端序)
0x00 0x00 0x00 0x00  // 保留字段
0x01 0x00 0x00 0x00  // 配置模式标志

这里有几个关键字段:

  • 同步头:固定为0xFFFFFFFF,用于对齐数据流。我在项目中遇到过,有些山寨加载器不认这个头,结果配置失败,折腾了两天才发现是同步头被截断了。
  • 器件ID:标识目标FPGA型号。比如ECP5-85F的ID是0x411122BB,不同封装版本会有差异。
  • 数据长度:整个配置数据的字节数,注意是小端序存储。
  • 配置模式标志:决定是主串模式、从并模式还是JTAG模式。嗯,这里要注意,有些逆向工具会忽略这个字段,导致配置时序错乱。

重要提示:文件头之后紧接着就是配置数据帧。但Lattice有个小坑——它会在文件头和数据帧之间插入一些填充字节,通常是0x00或0xFF。我曾经因为没跳过这些填充,解析出来的帧数据全乱了。

1.2 配置数据帧(Frame)的排列方式

说白了,FPGA的配置数据就是一堆帧(Frame)的集合。每个帧对应芯片内部的一行逻辑资源。Lattice的帧结构比较规整,我总结了一个规律:

帧 = 帧头 + 数据体 + 帧尾

帧头通常包含:

  • 帧类型(配置帧、初始化帧、用户帧)
  • 帧地址(行号、列号)
  • 帧长度(固定值,取决于器件密度)

数据体就是实际的LUT配置、布线开关状态、BRAM内容等。帧尾一般是一个CRC校验值。

我画了一张帧排列的示意图,帮你理解:

Lattice比特流帧排列结构 帧0:帧头(8B) + 数据体(256B) + CRC(4B) 帧1:帧头(8B) + 数据体(256B) + CRC(4B) 帧2:帧头(8B) + 数据体(256B) + CRC(4B) ...... 帧N:帧头(8B) + 数据体(256B) + CRC(4B) 帧地址连续递增,每帧对应一行逻辑资源 配置帧

你可能会问:帧地址怎么映射到物理位置?我个人的经验是,Lattice的帧地址编码方式比较直接——高位是行号,低位是列号。比如地址0x0102表示第1行第2列。但要注意,不同系列的编码方式略有差异,MachXO2和ECP5就不一样。

小技巧:如果你在逆向时发现帧地址不连续,别慌。Lattice有时会插入一些「空帧」用于时序对齐。这些空帧的数据体全是0xFF,CRC也是固定值。跳过它们就好。

1.3 CRC校验与错误检测机制

CRC校验是FPGA配置的保命符。没有它,配置数据在传输过程中被干扰了,芯片可能跑出莫名其妙的结果。我记得有一次,客户反馈说FPGA偶尔工作异常,查了三天,最后发现是配置链路上的CRC校验没通过,导致部分帧加载错误。

Lattice使用的CRC算法是CRC-32,多项式为:

// CRC-32多项式
0x04C11DB7  // 标准CRC-32

计算过程是这样的:

  1. 初始化CRC寄存器为0xFFFFFFFF
  2. 对每个字节进行逐位计算
  3. 最终结果取反,得到CRC校验值

我写了一个简单的CRC计算函数,方便你验证:

uint32_t lattice_crc32(uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 1) {
                crc = (crc >> 1) ^ 0xEDB88320;  // 反转多项式
            } else {
                crc >>= 1;
            }
        }
    }
    return ~crc;
}

Lattice的CRC检测机制分两级:

  • 帧级CRC:每个帧尾部有4字节CRC,用于检测单帧传输错误
  • 全局CRC:整个比特流末尾还有一个总CRC,用于检测整体完整性

注意:我曾经遇到过一种情况——帧级CRC全部正确,但全局CRC报错。后来发现是加载器在传输过程中多塞了一个字节。所以逆向时,两个CRC都要验证,缺一不可。

1.4 比特流压缩算法

Lattice的比特流压缩算法,说白了就是「去冗余」。FPGA配置数据中有大量重复的帧(尤其是空逻辑区域),压缩算法就是把这些重复帧用更短的标记代替。

Lattice使用的是一种基于字典的压缩方法,类似于LZ77的变体。核心思想是:

  • 如果当前帧与上一帧完全相同,用1个字节的「重复标记」代替
  • 如果当前帧与之前某帧相同,用「偏移量+长度」的标记代替
  • 如果当前帧是新数据,直接存储原始数据

压缩后的比特流格式大致如下:

// 压缩比特流示例
0x00 0x00 0x00 0x01  // 未压缩帧,长度256字节
[256字节原始数据]
0x01 0x00 0x00 0x00  // 重复上一帧标记
0x02 0x00 0x05 0x00  // 重复偏移5帧,长度1帧
0x00 0x00 0x00 0x01  // 未压缩帧
[256字节原始数据]

我个人习惯在逆向时先判断比特流是否被压缩。怎么判断?看文件头里有没有压缩标志位。Lattice通常在文件头的保留字段里放一个字节,0x00表示未压缩,0x01表示压缩。如果遇到压缩流,先解压再解析帧结构。

避坑指南:我曾经在逆向一个MachXO2的比特流时,发现帧数量比预期少了很多。后来才意识到,这个比特流用了压缩,而且压缩率高达60%。解压后帧数量才恢复正常。所以,看到帧数不对,先检查压缩标志。

解压算法其实不复杂,核心就是一个循环:读取标记,如果是重复标记,就从历史缓冲区复制数据;如果是原始数据,直接存储并加入历史缓冲区。我建议你写一个简单的解压器,用C语言或者Python都行,几百行代码就能搞定。

好了,关于Lattice比特流格式的核心内容,今天就讲到这里。记住三个关键点:文件头是身份证、帧结构是骨架、CRC是安全锁。掌握了这些,你就能在比特流的海洋里游刃有余了。

专注资料整理