返回

Python 实现 Flash 文件每 2048 字节加 64 字节 FF

Linux

给 Flash 文件精准“插针”:Python 实现每 2048 字节加 64 字节 FF

问题来了:Flash 文件里怎么塞 OOB 数据?

你是不是遇到过这样的事儿:手头有一个 firmware.i 这种二进制的 Flash 文件,需要把它烧录到一块 2GiB 的 NAND Flash 里去。但烧录工具或者目标系统有个怪要求,就是原始数据每隔 2048 字节,就得跟着 64 字节全是 FF 的数据块,有点像 NAND Flash 里的 OOB(Out-Of-Band)区域,虽然这里可能只是占位或者有特定格式需求。

之前可能你会用脚本从包含 OOB 的 dump 文件里把 OOB 数据删掉,但现在反过来了,需要往一个“干净”的文件里添加这种“OOB”。

具体来说,就是想把下面这样的文件:

原始文件 (input.bin):
偏移量       数据
00000000 : 12 23 4E 33 7D 66 88 XX ... (数据块 1)
...
000007F0 : 12 22 64 52 17 4E 54 98 XX XX XX XX XX XX XX XX  <-- 第 2048 字节结束

00000800 : 67 8E 43 81 09 75 23 65 ... (数据块 2)
...
00000FF0 : 76 55 55 33 22 1D XX XX XX XX XX XX XX XX XX XX  <-- 第 4096 字节结束
...

变成这样:

目标文件 (output.bin):
偏移量       数据
00000000 : 12 23 4E 33 7D 66 88 XX ... (数据块 1)
...
000007F0 : 12 22 64 52 17 4E 54 98 XX XX XX XX XX XX XX XX  <-- 第 2048 字节结束

00000800 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF  <-- 插入的 64 字节 FF
00000810 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
00000820 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
00000830 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

00000840 : 67 8E 43 81 09 75 23 65 ... (原始数据块 2)
...
00001030 : 76 55 55 33 22 1D XX XX XX XX XX XX XX XX XX XX  <-- 原始第 4096 字节结束 (现在位于 2048 + 64 + 2048 = 4160 字节处)

00001040 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF  <-- 插入的 64 字节 FF
00001050 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
00001060 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
00001070 : FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

00001080 : ... (原始数据块 3)
...

用 Python 或者其他脚本咋整呢?别急,这事儿 Python 干起来挺顺手的。

刨根问底:为啥要这么干?

在动手之前,稍微聊聊为啥会有这种需求。理解了背景,操作起来心里更有底。

  1. NAND Flash 结构: NAND Flash 存储数据的基本单位通常是“页”(Page),一个页又分为“数据区”(Data Area)和“备用区”(Spare Area),也就是常说的 OOB。数据区存实际的用户数据,OOB 区则用来存 ECC(错误校验码)、坏块标记、磨损均衡信息等管理数据。常见的页大小有 2048 字节数据 + 64 字节 OOB,或者 4096 字节数据 + 128/218 字节 OOB 等。你的需求(2048 + 64)正好是其中一种常见规格。
  2. 烧录器或文件系统要求: 有些烧录工具或者目标板上的 Bootloader、文件系统(比如 JFFS2, YAFFS)期望读写的是包含 OOB 区域布局的完整镜像文件。即使你提供的 firmware.i 只是纯数据,它们也需要你按照“数据块 + OOB占位”的格式准备好文件。这里的 64 字节 FF 可能就是为了满足这个格式要求,FF 在 NAND Flash 中通常代表“已擦除”状态,是个比较“安全”的默认值。
  3. 简化处理流程: 有时候,在 PC 端预先把文件处理成目标 Flash 的物理布局格式,可以简化嵌入式端驱动程序或者烧录逻辑的复杂度。它们可以直接按固定步长读写,不用再操心数据和 OOB 的分离合并。

说白了,即使这 64 字节 FF 在你的具体场景下没有实际的 ECC 或管理功能,它也是为了让整个烧录或运行流程能匹配 NAND Flash 的物理特性和软件系统的期望格式。

上手操作:用 Python 搞定!

Python 处理二进制文件很方便。针对这个问题,主要有两种思路,取决于你的文件大小和内存情况。

方案一:一次性读写(适合小文件)

如果你的 firmware.i 文件不是特别大(比如几百 MB 以下,具体看你机器内存),可以考虑一次性把整个文件读到内存里处理,然后再写到一个新文件。

  • 原理: 把输入文件内容全部加载进内存。然后,像切香肠一样,按 2048 字节一段切开,每切一段,就在后面拼接上 64 字节的 FF,最后把所有拼接好的片段组合起来,一次性写入输出文件。
  • 代码示例:
import sys
import os

# --- 配置参数 ---
input_file_path = 'firmware.i'  # 输入文件名
output_file_path = 'firmware_with_oob.bin' # 输出文件名
data_chunk_size = 2048          # 数据块大小 (字节)
oob_size = 64                   # 要插入的 FF 字节数
oob_byte = 0xFF                 # 要插入的字节值 (十进制 255)
# --- ---

def add_oob_at_once(in_path, out_path, chunk_size, ff_size, ff_byte):
    """
    一次性读写方式添加 OOB。
    适合文件不大,内存足够的情况。
    """
    print(f"开始处理文件: {in_path}")
    print(f"数据块大小: {chunk_size} 字节, 插入大小: {ff_size} 字节")

    oob_data = bytes([ff_byte] * ff_size) # 生成 64 字节的 FF

    try:
        with open(in_path, 'rb') as infile:
            file_content = infile.read()
            print(f"成功读取输入文件,大小: {len(file_content)} 字节")

        processed_data = bytearray() # 用 bytearray 方便拼接
        total_bytes_read = 0

        while total_bytes_read < len(file_content):
            data_chunk = file_content[total_bytes_read : total_bytes_read + chunk_size]
            processed_data.extend(data_chunk) # 添加数据块
            processed_data.extend(oob_data)   # 添加 FF 块
            total_bytes_read += len(data_chunk) # 移动指针

            # 处理最后一个块不足 chunk_size 的情况
            # 题目要求是每 2048 字节后加,即使最后不足 2048 也要加
            # 如果是要求仅在完整的 2048 字节后加,这里的逻辑需要调整
            # 目前逻辑是:只要读到数据(哪怕不满2048),就加 OOB

            if len(data_chunk) < chunk_size:
                 print(f"处理到文件末尾,最后一个数据块大小: {len(data_chunk)} 字节")
                 # 根据具体需求,如果最后不满一个 chunk 就不需要加 OOB,可以在这里加判断
                 # if len(data_chunk) == chunk_size:
                 #    processed_data.extend(oob_data)
                 # else:
                 #    # 不足一个 chunk,可以选择不加 OOB,或者按现有逻辑加
                 #    pass # 现有逻辑是在上面 extend(oob_data) 已经加了
                 break # 已经读到最后,跳出循环


        with open(out_path, 'wb') as outfile:
            outfile.write(processed_data)
            print(f"成功写入输出文件: {out_path}")
            print(f"输出文件大小: {len(processed_data)} 字节")

    except FileNotFoundError:
        print(f"错误: 输入文件 '{in_path}' 未找到!")
        sys.exit(1)
    except IOError as e:
        print(f"错误: 文件读写失败 - {e}")
        sys.exit(1)
    except Exception as e:
        print(f"发生未知错误: {e}")
        sys.exit(1)

# --- 执行 ---
if __name__ == "__main__":
    # 简单检查下输入文件是否存在
    if not os.path.exists(input_file_path):
        print(f"错误: 输入文件 '{input_file_path}' 不存在或无法访问。")
    else:
        add_oob_at_once(input_file_path, output_file_path, data_chunk_size, oob_size, oob_byte)
  • 优缺点:
    • 优点:逻辑相对简单直接。对于小文件,处理速度可能很快。
    • 缺点:内存消耗巨大!如果你的 firmware.i 文件有几个 GB,那需要同样甚至更多的内存来暂存 processed_data,很容易把内存撑爆。
  • 安全建议:
    • 备份!备份!备份! 操作前一定把原始的 firmware.i 文件备份好,防止脚本出错或结果不符合预期导致原文件损坏。
    • 检查输出: 处理完成后,检查 firmware_with_oob.bin 文件的大小是否符合预期 ( 原始大小 + (原始大小 / 2048) * 64,注意整除和余数处理)。可以用 ls -l 查看文件大小,或者用十六进制编辑器(如 hexedit, HxD, 010 Editor 等)打开看看开头几个块的结构对不对。

方案二:边读边写(处理大文件的推荐姿势)

对于大文件,特别是像 2GiB 这种级别的,边读边写(流式处理)是更稳妥的选择。这种方法内存占用小,几乎不受文件大小限制。

  • 原理: 打开输入和输出两个文件。循环地从输入文件读取固定大小(2048 字节)的数据块。每读一块,就立刻把它写入输出文件,紧接着再把 64 字节的 FF 写入输出文件。一直重复这个过程,直到输入文件读完为止。
  • 代码示例:
import sys
import os

# --- 配置参数 ---
input_file_path = 'firmware.i'  # 输入文件名
output_file_path = 'firmware_with_oob_streamed.bin' # 输出文件名
data_chunk_size = 2048          # 数据块大小 (字节)
oob_size = 64                   # 要插入的 FF 字节数
oob_byte = 0xFF                 # 要插入的字节值 (十进制 255)
# --- ---

def add_oob_streamed(in_path, out_path, chunk_size, ff_size, ff_byte):
    """
    边读边写的方式添加 OOB。
    内存占用小,适合处理大文件。
    """
    print(f"开始流式处理文件: {in_path}")
    print(f"数据块大小: {chunk_size} 字节, 插入大小: {ff_size} 字节")

    oob_data = bytes([ff_byte] * ff_size) # 生成 64 字节的 FF
    total_bytes_written = 0
    input_file_size = os.path.getsize(in_path) # 获取输入文件总大小,用于计算进度

    try:
        # 使用 'with' 语句确保文件会被自动关闭,即使发生错误
        with open(in_path, 'rb') as infile, open(out_path, 'wb') as outfile:
            while True:
                # 从输入文件读取一个数据块
                data_chunk = infile.read(chunk_size)

                if not data_chunk:
                    # 如果读不到数据了,说明文件已经读完,跳出循环
                    print("\n文件处理完成。")
                    break

                # 将读取到的数据块写入输出文件
                outfile.write(data_chunk)
                # 紧接着写入 OOB 数据
                outfile.write(oob_data)

                total_bytes_written += len(data_chunk) + len(oob_data)

                # 打印进度 (可选,对大文件友好)
                percent_done = (infile.tell() / input_file_size) * 100
                print(f"\r处理进度: {percent_done:.2f}%", end="")

            print(f"\n成功写入输出文件: {out_path}")
            print(f"输出文件大小: {total_bytes_written} 字节")

            # 验证一下理论大小
            expected_chunks = (input_file_size + chunk_size - 1) // chunk_size # 计算有多少个块(包括不足一块的)
            expected_size = input_file_size + expected_chunks * ff_size
            if total_bytes_written == expected_size:
                print("文件大小符合预期。")
            else:
                print(f"警告: 文件大小与预期不符! 预期: {expected_size}, 实际: {total_bytes_written}")


    except FileNotFoundError:
        print(f"错误: 输入文件 '{in_path}' 未找到!")
        sys.exit(1)
    except IOError as e:
        print(f"错误: 文件读写失败 - {e}")
        sys.exit(1)
    except Exception as e:
        print(f"发生未知错误: {e}")
        sys.exit(1)


# --- 执行 ---
if __name__ == "__main__":
    if not os.path.exists(input_file_path):
        print(f"错误: 输入文件 '{input_file_path}' 不存在或无法访问。")
    else:
        add_oob_streamed(input_file_path, output_file_path, data_chunk_size, oob_size, oob_byte)

  • 优缺点:
    • 优点:内存占用极小 ,无论文件多大,内存消耗都稳定在一个很低的水平(主要就是缓冲区的大小)。这是处理大文件的标准做法。
    • 缺点:相较于内存足够时的一次性读写,可能会因为频繁的磁盘 I/O 稍微慢一点点,但对于 GB 级文件,避免内存耗尽才是关键,这点性能差异通常可以接受。
  • 安全建议:
    • 同样,备份原始文件 是第一要务!
    • 仔细检查输出: 对于大文件,完整比对不现实。建议用十六进制编辑器打开输出文件,抽查 几个关键位置,比如:
      • 文件开头 0x0000 处是否是原始数据。
      • 第一个 OOB 块的位置 0x0800 (2048) 处是否是 64 字节的 FF
      • 第一个 OOB 块之后 0x0840 (2048+64) 处是否是原始文件的第 2049 字节数据。
      • 第二个 OOB 块的位置 0x1040 (2048+64+2048) 处是否是 FF
      • 检查文件末尾部分,看最后一个数据块(可能不足 2048 字节)后面是否也正确添加了 FF 块。
      • 确认最终文件大小是否符合预期。

进阶技巧与注意事项

想让脚本更强大、更实用?可以考虑以下几点:

  1. 命令行参数化: 把输入文件名、输出文件名、数据块大小、OOB 大小等做成命令行参数,这样用起来更灵活,不用每次都改代码。Python 的 argparse 模块就是干这个的。

    import argparse
    # ... (前面的 add_oob_streamed 函数不变) ...
    
    if __name__ == "__main__":
        parser = argparse.ArgumentParser(description='向二进制文件的数据块后添加 OOB 数据。')
        parser.add_argument('-i', '--input', required=True, help='输入文件路径')
        parser.add_argument('-o', '--output', required=True, help='输出文件路径')
        parser.add_argument('-c', '--chunksize', type=int, default=2048, help='数据块大小 (字节), 默认 2048')
        parser.add_argument('-s', '--oobsize', type=int, default=64, help='要插入的 OOB 大小 (字节), 默认 64')
        parser.add_argument('-b', '--byte', type=lambda x: int(x, 0), default=0xFF,
                            help='要插入的字节值 (可以是十进制、十六进制如 0xFF), 默认 0xFF')
    
        args = parser.parse_args()
    
        if not os.path.exists(args.input):
            print(f"错误: 输入文件 '{args.input}' 不存在或无法访问。")
            sys.exit(1)
    
        # 检查 OOB 字节值是否在 0-255 范围内
        if not (0 <= args.byte <= 255):
            print(f"错误: 插入的字节值 '{args.byte}' 无效,必须在 0 到 255 之间。")
            sys.exit(1)
    
        add_oob_streamed(args.input, args.output, args.chunksize, args.oobsize, args.byte)
    
    

    这样,你就可以这样运行脚本了:
    python your_script_name.py -i firmware.i -o processed_firmware.bin -c 2048 -s 64 -b 0xFF

  2. 错误处理: 上面的代码示例中已经包含了基本的 try...except 来捕获像 FileNotFoundErrorIOError 这样的常见错误。确保脚本在出错时能给出明确提示并优雅退出。使用 with open(...) 是个好习惯,它能保证无论发生什么,文件最终都会被关闭。

  3. 进度提示: 处理大文件时,有个进度条或者百分比提示会友好很多。上面的流式处理代码示例中增加了一个简单的进度打印。

  4. 确认需求细节: 和提出需求的人确认清楚:最后一个数据块如果不足 2048 字节,其后是否还需要添加 64 字节的 FF?上面的代码是默认添加的,如果不需要,需要在 while True 循环读取到 data_chunklen(data_chunk) < chunk_size 时做判断,或者调整最后的逻辑。

选择哪种方案主要看你的文件大小。对于不确定的情况或者大文件,优先选用方案二(边读边写)。把脚本参数化后,用起来会方便很多。操作二进制文件务必小心,备份永远是好习惯!