Python 实现 Flash 文件每 2048 字节加 64 字节 FF
2025-03-27 15:00:21
给 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 干起来挺顺手的。
刨根问底:为啥要这么干?
在动手之前,稍微聊聊为啥会有这种需求。理解了背景,操作起来心里更有底。
- NAND Flash 结构: NAND Flash 存储数据的基本单位通常是“页”(Page),一个页又分为“数据区”(Data Area)和“备用区”(Spare Area),也就是常说的 OOB。数据区存实际的用户数据,OOB 区则用来存 ECC(错误校验码)、坏块标记、磨损均衡信息等管理数据。常见的页大小有 2048 字节数据 + 64 字节 OOB,或者 4096 字节数据 + 128/218 字节 OOB 等。你的需求(2048 + 64)正好是其中一种常见规格。
- 烧录器或文件系统要求: 有些烧录工具或者目标板上的 Bootloader、文件系统(比如 JFFS2, YAFFS)期望读写的是包含 OOB 区域布局的完整镜像文件。即使你提供的
firmware.i
只是纯数据,它们也需要你按照“数据块 + OOB占位”的格式准备好文件。这里的 64 字节FF
可能就是为了满足这个格式要求,FF
在 NAND Flash 中通常代表“已擦除”状态,是个比较“安全”的默认值。 - 简化处理流程: 有时候,在 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
块。 - 确认最终文件大小是否符合预期。
- 文件开头
进阶技巧与注意事项
想让脚本更强大、更实用?可以考虑以下几点:
-
命令行参数化: 把输入文件名、输出文件名、数据块大小、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
-
错误处理: 上面的代码示例中已经包含了基本的
try...except
来捕获像FileNotFoundError
和IOError
这样的常见错误。确保脚本在出错时能给出明确提示并优雅退出。使用with open(...)
是个好习惯,它能保证无论发生什么,文件最终都会被关闭。 -
进度提示: 处理大文件时,有个进度条或者百分比提示会友好很多。上面的流式处理代码示例中增加了一个简单的进度打印。
-
确认需求细节: 和提出需求的人确认清楚:最后一个数据块如果不足 2048 字节,其后是否还需要添加 64 字节的
FF
?上面的代码是默认添加的,如果不需要,需要在while True
循环读取到data_chunk
但len(data_chunk) < chunk_size
时做判断,或者调整最后的逻辑。
选择哪种方案主要看你的文件大小。对于不确定的情况或者大文件,优先选用方案二(边读边写)。把脚本参数化后,用起来会方便很多。操作二进制文件务必小心,备份永远是好习惯!