rose/tools/build_sd_image.py
2026-09-11 10:47:51 -04:00

106 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Build the minimal FAT16 SD card used by the Renode V5 model.
The firmware's stdlib loader reaches the SDHCI block-device path, while the
mainloop looks for assets.bin on the same removable filesystem. Keep both
vendor inputs in an ordinary FAT root directory so the guest owns all file
lookup and reads.
"""
import argparse
import math
import os
import struct
import sys
SECTOR = 512
SECTORS = 65536 # 32 MiB
SECTORS_PER_CLUSTER = 8 # 4 KiB clusters
RESERVED = 1
FATS = 2
ROOT_ENTRIES = 512
FAT_SECTORS = 32
ROOT_SECTORS = (ROOT_ENTRIES * 32 + SECTOR - 1) // SECTOR
DATA_START = RESERVED + FATS * FAT_SECTORS + ROOT_SECTORS
CLUSTERS = (SECTORS - DATA_START) // SECTORS_PER_CLUSTER
def fat_name(name):
stem, ext = name.upper().rsplit('.', 1)
return stem.encode('ascii').ljust(8) + ext.encode('ascii').ljust(3)
def root_entry(name, first_cluster, size):
entry = bytearray(32)
entry[0:11] = fat_name(name)
entry[11] = 0x20
struct.pack_into('<H', entry, 26, first_cluster)
struct.pack_into('<I', entry, 28, size)
return entry
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--out', default='.build/emu/sdcard_mbr.img')
ap.add_argument('--vexos-dir', default='.build/vexos-0/VEXOS_V5_1_1_5_0')
ap.add_argument('--stdlib', default='.build/emu/assets/fs/stdlib_0.bin')
args = ap.parse_args()
assets = os.path.join(args.vexos_dir, 'assets.bin')
inputs = [('ASSETS.BIN', assets), ('STDLIB_0.BIN', args.stdlib)]
for _, path in inputs:
if not os.path.isfile(path):
print('missing %s (run: make assets)' % path, file=sys.stderr)
return 2
files = [(name, open(path, 'rb').read()) for name, path in inputs]
needed = sum((len(data) + SECTOR * SECTORS_PER_CLUSTER - 1) //
(SECTOR * SECTORS_PER_CLUSTER) for _, data in files)
if needed > CLUSTERS:
raise ValueError('SD image too small for %d clusters' % needed)
image = bytearray(SECTORS * SECTOR)
boot = memoryview(image)[:SECTOR]
boot[0:3] = b'\xeb\x3c\x90'
boot[3:11] = b'ROSESD '
struct.pack_into('<H', boot, 11, SECTOR)
boot[13] = SECTORS_PER_CLUSTER
struct.pack_into('<H', boot, 14, RESERVED)
boot[16] = FATS
struct.pack_into('<H', boot, 17, ROOT_ENTRIES)
# TotalSectors16 uses zero when the volume needs the 32-bit field.
struct.pack_into('<H', boot, 19, 0)
boot[21] = 0xF8
struct.pack_into('<H', boot, 22, FAT_SECTORS)
struct.pack_into('<H', boot, 24, 32)
struct.pack_into('<H', boot, 26, 64)
struct.pack_into('<I', boot, 32, SECTORS)
boot[510:512] = b'\x55\xaa'
fat = bytearray(FAT_SECTORS * SECTOR)
struct.pack_into('<H', fat, 0, 0xFFF8)
struct.pack_into('<H', fat, 2, 0xFFFF)
root = memoryview(image)[(RESERVED + FATS * FAT_SECTORS) * SECTOR:
DATA_START * SECTOR]
cluster = 2
for index, (name, data) in enumerate(files):
count = max(1, math.ceil(len(data) / (SECTOR * SECTORS_PER_CLUSTER)))
for n in range(count):
value = 0xFFFF if n + 1 == count else cluster + n + 1
struct.pack_into('<H', fat, 2 * (cluster + n), value)
root[index * 32:(index + 1) * 32] = root_entry(name, cluster, len(data))
start = (DATA_START + (cluster - 2) * SECTORS_PER_CLUSTER) * SECTOR
image[start:start + len(data)] = data
cluster += count
for fat_index in range(FATS):
start = (RESERVED + fat_index * FAT_SECTORS) * SECTOR
image[start:start + len(fat)] = fat
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
open(args.out, 'wb').write(image)
print('wrote %s: FAT16, %d MiB, %s' %
(args.out, len(image) // (1024 * 1024), ', '.join(name for name, _ in files)))
if __name__ == '__main__':
sys.exit(main())