Merge branch 'master' into 'master'
Add header parsing for Genesis, ROM size autodetection, checksum validation. See merge request InfiniteNesLives/INL-retro-progdump!23
This commit is contained in:
commit
5bd4799d2b
|
|
@ -14,6 +14,22 @@ local function unsupported(operation)
|
||||||
print("\nUNSUPPORTED OPERATION: \"" .. operation .. "\" not implemented yet for Sega Genesis.\n")
|
print("\nUNSUPPORTED OPERATION: \"" .. operation .. "\" not implemented yet for Sega Genesis.\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Compute Genesis checksum from a file, which can be compared with header value.
|
||||||
|
local function checksum_rom(filename)
|
||||||
|
local file = assert(io.open(filename, "rb"))
|
||||||
|
local sum = 0
|
||||||
|
-- Skip header
|
||||||
|
file:read(0x200)
|
||||||
|
while true do
|
||||||
|
-- Add up remaining 16-bit words
|
||||||
|
local bytes = file:read(2)
|
||||||
|
if not bytes then break end
|
||||||
|
sum = sum + string.unpack(">i2", bytes)
|
||||||
|
end
|
||||||
|
-- Only use the lower bits.
|
||||||
|
return sum & 0xFFFF
|
||||||
|
end
|
||||||
|
|
||||||
--/ROMSEL is always low for this dump
|
--/ROMSEL is always low for this dump
|
||||||
local function dump_rom( file, rom_size_KB, debug )
|
local function dump_rom( file, rom_size_KB, debug )
|
||||||
|
|
||||||
|
|
@ -30,6 +46,9 @@ local function dump_rom( file, rom_size_KB, debug )
|
||||||
|
|
||||||
-- A "large" Genesis ROM is 24 banks, many are 8 and 16 - status every 4 is reasonable.
|
-- A "large" Genesis ROM is 24 banks, many are 8 and 16 - status every 4 is reasonable.
|
||||||
-- The largest published Genesis game is Super Street Fighter 2, which is 40 banks!
|
-- The largest published Genesis game is Super Street Fighter 2, which is 40 banks!
|
||||||
|
-- TODO: Accessing banks in games that are >4MB require using a mapper.
|
||||||
|
-- See: https://plutiedev.com/beyond-4mb
|
||||||
|
|
||||||
if (read_count % 4 == 0) then
|
if (read_count % 4 == 0) then
|
||||||
print("dumping ROM bank: ", read_count, " of ", num_reads - 1)
|
print("dumping ROM bank: ", read_count, " of ", num_reads - 1)
|
||||||
end
|
end
|
||||||
|
|
@ -45,6 +64,121 @@ local function dump_rom( file, rom_size_KB, debug )
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Helper to extract fields in internal header.
|
||||||
|
local function extract_field_from_string(data, start_offset, length)
|
||||||
|
-- 1 is added to Offset to handle lua strings being 1-based.
|
||||||
|
return string.sub(data, start_offset + 1, start_offset + length)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Populates table with internal header contents from dumped data.
|
||||||
|
local function extract_header(header_data)
|
||||||
|
-- https://plutiedev.com/rom-header
|
||||||
|
-- https://en.wikibooks.org/wiki/Genesis_Programming#ROM_header
|
||||||
|
|
||||||
|
-- TODO: Decode publisher from t-series in build field
|
||||||
|
-- https://segaretro.org/Third-party_T-series_codes
|
||||||
|
|
||||||
|
local addr_console_name = 0x100
|
||||||
|
local addr_build_date = 0x110
|
||||||
|
local addr_domestic_name = 0x120
|
||||||
|
local addr_intl_name = 0x150
|
||||||
|
local addr_type_serial_version = 0x180
|
||||||
|
local addr_checksum = 0x18E
|
||||||
|
local addr_device_support = 0x190
|
||||||
|
local addr_rom_addr_range = 0x1A0
|
||||||
|
local addr_ram_addr_range = 0x1A8
|
||||||
|
local addr_sram_support = 0x1B0
|
||||||
|
local addr_modem_support = 0x1BC
|
||||||
|
local addr_region_support = 0x1F0
|
||||||
|
|
||||||
|
local len_console_name = 16
|
||||||
|
local len_build_date = 16
|
||||||
|
local len_name = 48
|
||||||
|
local len_type_serial_version = 14
|
||||||
|
local len_checksum = 2
|
||||||
|
local len_device_support = 16
|
||||||
|
local len_addr_range = 8
|
||||||
|
local len_sram_support = 12
|
||||||
|
local len_modem_support = 12
|
||||||
|
local len_region_support = 3
|
||||||
|
|
||||||
|
local header = {
|
||||||
|
console_name = extract_field_from_string(header_data, addr_console_name, len_console_name),
|
||||||
|
-- TODO: Decode T-Value and build info.
|
||||||
|
build_date = extract_field_from_string(header_data, addr_build_date, len_build_date),
|
||||||
|
domestic_name = extract_field_from_string(header_data, addr_domestic_name, len_name),
|
||||||
|
international_name = extract_field_from_string(header_data, addr_intl_name, len_name),
|
||||||
|
-- TODO: Decode Type, serial and revision.
|
||||||
|
type_serial_version = extract_field_from_string(header_data, addr_type_serial_version, len_type_serial_version),
|
||||||
|
checksum = string.unpack(">i2", extract_field_from_string(header_data, addr_checksum, len_checksum)),
|
||||||
|
-- TODO: Decode device support.
|
||||||
|
io_device_support = extract_field_from_string(header_data, addr_device_support, len_device_support),
|
||||||
|
-- TODO: Decode SRAM support.
|
||||||
|
sram_support = extract_field_from_string(header_data, addr_sram_support, len_sram_support),
|
||||||
|
-- TODO: Decode modem support.
|
||||||
|
modem_support = extract_field_from_string(header_data, addr_modem_support, len_modem_support),
|
||||||
|
-- TODO: Decode region support.
|
||||||
|
region_support = extract_field_from_string(header_data, addr_region_support, len_region_support),
|
||||||
|
}
|
||||||
|
-- ROM range can be used to autodetect the rom size.
|
||||||
|
local rom_range = extract_field_from_string(header_data, addr_rom_addr_range, len_addr_range)
|
||||||
|
local rom_start = string.unpack(">i4", string.sub(rom_range, 1, 4))
|
||||||
|
local rom_end = string.unpack(">i4", string.sub(rom_range,5, 8))
|
||||||
|
header["rom_size"] = (rom_end - rom_start + 1) / 1024
|
||||||
|
|
||||||
|
-- These should be the same in every cart according to docs, but decode in case its not. (64 Kb)
|
||||||
|
local ram_range = extract_field_from_string(header_data, addr_ram_addr_range, len_addr_range)
|
||||||
|
local ram_start = string.unpack(">i4", string.sub(ram_range, 1, 4))
|
||||||
|
local ram_end = string.unpack(">i4", string.sub(ram_range,5, 8))
|
||||||
|
header["ram_size"] = (ram_end - ram_start + 1) / 1024
|
||||||
|
|
||||||
|
return header
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Make a human-friendly text representation of ROM Size.
|
||||||
|
local function str_rom_size(rom_size_kb)
|
||||||
|
local mbit = rom_size_kb / 128
|
||||||
|
if mbit < 1 then
|
||||||
|
mbit = "<1"
|
||||||
|
end
|
||||||
|
return "" .. rom_size_kb .. " kB (".. mbit .." mbit)"
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Prints parsed header contents to stdout.
|
||||||
|
local function print_header(genesis_header)
|
||||||
|
print("Console Name: \t" .. genesis_header["console_name"])
|
||||||
|
print("Domestic Name: \t" .. genesis_header["domestic_name"])
|
||||||
|
print("Release Date: \t" .. genesis_header["build_date"])
|
||||||
|
print("Rom Size: \t" .. str_rom_size(genesis_header["rom_size"]))
|
||||||
|
print("Serial/Version: " .. genesis_header["type_serial_version"])
|
||||||
|
print("Checksum: \t" .. hexfmt(genesis_header["checksum"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reads and parses internal ROM header from first page of data.
|
||||||
|
local function read_header()
|
||||||
|
dict.sega("SET_BANK", 0)
|
||||||
|
|
||||||
|
local page0_data = ""
|
||||||
|
dump.dumptocallback(
|
||||||
|
function (data)
|
||||||
|
page0_data = page0_data .. data
|
||||||
|
end,
|
||||||
|
64, 0x0000, "GENESIS_ROM_PAGE0", false
|
||||||
|
)
|
||||||
|
local header_data = string.sub(page0_data, 1, 0x201)
|
||||||
|
local genesis_header = extract_header(header_data)
|
||||||
|
return genesis_header
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test that cartridge is readable by looking for valid entries in internal header.
|
||||||
|
local function test(genesis_header)
|
||||||
|
local valid = false
|
||||||
|
-- Trailing spaces are required! Field length is 16 characters.
|
||||||
|
if genesis_header["console_name"] == "SEGA GENESIS " then valid = true end
|
||||||
|
if genesis_header["console_name"] == "SEGA MEGA DRIVE " then valid = true end
|
||||||
|
return valid
|
||||||
|
end
|
||||||
|
|
||||||
--Cart should be in reset state upon calling this function
|
--Cart should be in reset state upon calling this function
|
||||||
--this function processes all user requests for this specific board/mapper
|
--this function processes all user requests for this specific board/mapper
|
||||||
local function process(process_opts, console_opts)
|
local function process(process_opts, console_opts)
|
||||||
|
|
@ -53,11 +187,14 @@ local function process(process_opts, console_opts)
|
||||||
-- Initialize device i/o for SEGA
|
-- Initialize device i/o for SEGA
|
||||||
dict.io("IO_RESET")
|
dict.io("IO_RESET")
|
||||||
dict.io("SEGA_INIT")
|
dict.io("SEGA_INIT")
|
||||||
|
local genesis_header = read_header()
|
||||||
|
|
||||||
|
|
||||||
-- TODO: test cart by reading manf/prod ID
|
|
||||||
if process_opts["test"] then
|
if process_opts["test"] then
|
||||||
unsupported("test")
|
-- If garbage data is in the header, it's a waste of time trying to proceed doing anything else.
|
||||||
|
local valid_header = test(genesis_header)
|
||||||
|
if valid_header ~= true then print("Unreadable cartridge - exiting! (Try cleaning cartridge connector?)") end
|
||||||
|
assert(valid_header)
|
||||||
|
print_header(genesis_header)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: dump the ram to file
|
-- TODO: dump the ram to file
|
||||||
|
|
@ -67,16 +204,30 @@ local function process(process_opts, console_opts)
|
||||||
|
|
||||||
-- Dump the cart to dumpfile.
|
-- Dump the cart to dumpfile.
|
||||||
if process_opts["read"] then
|
if process_opts["read"] then
|
||||||
|
|
||||||
|
-- If ROM size wasn't provided, attempt to use value in internal header.
|
||||||
|
local rom_size = console_opts["rom_size_kbyte"]
|
||||||
|
if rom_size == 0 then
|
||||||
|
print("ROM Size not provided, " .. str_rom_size(genesis_header["rom_size"]) .. " detected.")
|
||||||
|
rom_size = genesis_header["rom_size"]
|
||||||
|
end
|
||||||
|
|
||||||
print("\nDumping SEGA ROM...")
|
print("\nDumping SEGA ROM...")
|
||||||
|
|
||||||
file = assert(io.open(process_opts["dump_filename"], "wb"))
|
file = assert(io.open(process_opts["dump_filename"], "wb"))
|
||||||
|
|
||||||
--dump cart into file
|
--dump cart into file
|
||||||
dump_rom(file, console_opts["rom_size_kbyte"], false)
|
dump_rom(file, rom_size, false)
|
||||||
|
|
||||||
--close file
|
--close file
|
||||||
assert(file:close())
|
assert(file:close())
|
||||||
print("DONE Dumping SEGA ROM")
|
print("DONE Dumping SEGA ROM")
|
||||||
|
print("Computing checksum...")
|
||||||
|
local checksum = checksum_rom(process_opts["dump_filename"])
|
||||||
|
if checksum == genesis_header["checksum"] then
|
||||||
|
print("CHECKSUM OK! DUMP SUCCESS!")
|
||||||
|
else
|
||||||
|
print("CHECKSUM MISMATCH - BAD DUMP! (Try cleaning cartridge connector?)")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- TODO: erase the cart
|
-- TODO: erase the cart
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,6 @@ int main(int argc, char *argv[])
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((strcmp("gba", opts->console_name) == 0) ||
|
if ((strcmp("gba", opts->console_name) == 0) ||
|
||||||
(strcmp("genesis", opts->console_name) == 0) ||
|
|
||||||
(strcmp("n64", opts->console_name) == 0)) {
|
(strcmp("n64", opts->console_name) == 0)) {
|
||||||
if (opts->rom_size_kbyte <= 0) {
|
if (opts->rom_size_kbyte <= 0) {
|
||||||
printf("ROM size must be greater than 0 kilobytes.\n");
|
printf("ROM size must be greater than 0 kilobytes.\n");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue