From b8d39e3e3c74be4980220a70d22aa09bca7a8588 Mon Sep 17 00:00:00 2001 From: beyondcoast Date: Sat, 16 Mar 2019 17:20:26 -0500 Subject: [PATCH 1/2] Add header parsing for Genesis, ROM size autodetection, checksum validation. --- host/scripts/sega/genesis_v1.lua | 163 +++++++++++++++++++++++++++++-- 1 file changed, 157 insertions(+), 6 deletions(-) diff --git a/host/scripts/sega/genesis_v1.lua b/host/scripts/sega/genesis_v1.lua index da6c59b..fba9429 100644 --- a/host/scripts/sega/genesis_v1.lua +++ b/host/scripts/sega/genesis_v1.lua @@ -14,6 +14,22 @@ local function unsupported(operation) print("\nUNSUPPORTED OPERATION: \"" .. operation .. "\" not implemented yet for Sega Genesis.\n") 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 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. -- 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 print("dumping ROM bank: ", read_count, " of ", num_reads - 1) end @@ -45,6 +64,121 @@ local function dump_rom( file, rom_size_KB, debug ) 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 --this function processes all user requests for this specific board/mapper local function process(process_opts, console_opts) @@ -53,11 +187,14 @@ local function process(process_opts, console_opts) -- Initialize device i/o for SEGA dict.io("IO_RESET") dict.io("SEGA_INIT") + local genesis_header = read_header() - - -- TODO: test cart by reading manf/prod ID 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 -- TODO: dump the ram to file @@ -67,16 +204,30 @@ local function process(process_opts, console_opts) -- Dump the cart to dumpfile. 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...") - file = assert(io.open(process_opts["dump_filename"], "wb")) - + --dump cart into file - dump_rom(file, console_opts["rom_size_kbyte"], false) + dump_rom(file, rom_size, false) --close file assert(file:close()) 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 -- TODO: erase the cart From 6bf1d82c4ad89c0cac11dd3eb03b7bf2e621bcc7 Mon Sep 17 00:00:00 2001 From: beyondcoast Date: Sat, 16 Mar 2019 17:22:42 -0500 Subject: [PATCH 2/2] Remove size checking in C for Sega because autodetection was implemented. --- host/source/inlprog.c | 1 - 1 file changed, 1 deletion(-) diff --git a/host/source/inlprog.c b/host/source/inlprog.c index 097d39f..d758a22 100644 --- a/host/source/inlprog.c +++ b/host/source/inlprog.c @@ -330,7 +330,6 @@ int main(int argc, char *argv[]) } if ((strcmp("gba", opts->console_name) == 0) || - (strcmp("genesis", opts->console_name) == 0) || (strcmp("n64", opts->console_name) == 0)) { if (opts->rom_size_kbyte <= 0) { printf("ROM size must be greater than 0 kilobytes.\n");