Skip to content

Commit

Permalink
3.21
Browse files Browse the repository at this point in the history
  • Loading branch information
lesserkuma committed Jan 16, 2023
1 parent 3b360b2 commit 288dbf8
Show file tree
Hide file tree
Showing 35 changed files with 681 additions and 112 deletions.
Binary file modified .github/01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 15 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Release notes
### v3.21 (released 2023-01-16)
- Bundles GBxCart RW v1.4/v1.4a firmware version R39+L8 (adds support for insideGadgets WonderSwan and Game Gear flash carts)
- Added support for SD007_48BALL_SOP28 with M29W320ET *(thanks DevDavisNunez)*
- Added support for the BennVenn MBC3000 RTC cart *(thanks LucentW)*
- Added support for Flash2Advance Ultra 256M with 8× 3204C3B100 *(thanks djeddit)*
- Added support for SD007_T40_64BALL_SOJ28 with 29LV016T *(thanks Stitch)*
- Confirmed support for SD007_T40_64BALL_S71_TV_TS28 with TC58FVB016FT-85 *(thanks edo999)*
- Added support for F864-3 with M36L0R7050B *(thanks s1cp)*
- Allowed for switching between different Write Enable pins during chip erase and sector erase command sequences via a third parameter (“WR” or “AUDIO”) *(thanks ALXCO-Hardware for the suggestion)*
- Added support for the Squareboi 4 MB (2× 2 MB) cart *(thanks ALXCO-Hardware)*
- Added an option to limit the baud rate for GBxCart RW v1.4/v1.4a
- Minor bug fixes and improvements *(thanks gboh, Grender and orangeglo)*

### v3.20 (released 2022-11-30)
- Bundles GBxCart RW v1.4/v1.4a firmware version R38+L8
- Bundles GBxCart RW v1.4/v1.4a firmware version R38+L8 (minor improvements)
- Will now retry failed flash sector writes a few times before stopping the process (requires firmware version L1+)
- Added delta ROM writing (only write the difference between two ROMs); requires both the original <name>.<ext> ROM file and the changed <name>.delta.<ext> ROM file in the same directory (requires firmware version L1+) *(thanks djeddit for the suggestion)*
- Fixed support for Flash2Advance Ultra 64M with 2× 28F320C3B
Expand Down Expand Up @@ -234,7 +247,7 @@
- Added support for SD007_TSOP_48BALL_V10 with M29W320DT *(thanks Jayro)*
- Fixed a problem of reading from a certain type of cartridge that uses the GL256S flash chip *(thanks marv17)*
- Added support for B11 with 26L6420MC-90 *(thanks dyf2007)*
- Added support for DIY carts with MBC3 and MX29LV640 *(thanks eveningmoose)*
- Added support for DIY carts with MX29LV640 *(thanks eveningmoose)*

### v2.1 (released 2021-05-05)
- Fixed support for SD007_TSOP_29LV017D with L017D70VC *(thanks marv17 and 90sFlav)*
Expand Down
19 changes: 17 additions & 2 deletions FlashGBX/DataTransfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Author: Lesserkuma (github.com/lesserkuma)

import traceback
from serial import SerialException
from . import pyside as PySide2

class DataTransfer(PySide2.QtCore.QThread):
Expand All @@ -25,6 +26,8 @@ def isRunning(self):
return not self.FINISHED

def run(self):
tb = ""
error = None
try:
if self.CONFIG == None:
pass
Expand All @@ -34,7 +37,19 @@ def run(self):
self.CONFIG['port'].TransferData(self.CONFIG, self.updateProgress)
self.FINISHED = True

except SerialException as e:
if "GetOverlappedResult failed" in e.args[0]:
self.updateProgress.emit({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"The USB connection was lost during a transfer. Try different USB cables, reconnect the device, restart the software and try again.", "abortable":False})
self.FINISHED = True
return
tb = traceback.format_exc()
error = e

except Exception as e:
traceback.print_exc()
self.updateProgress.emit({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"An unresolvable error has occured. See console output for more information. Reconnect the device, restart the software and try again.\n\n{:s}: {:s}".format(type(e).__name__, str(e)), "abortable":False})
tb = traceback.format_exc()
error = e

if error is not None:
print(tb)
self.updateProgress.emit({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"An unresolvable error has occured. See console output for more information. Reconnect the device, restart the software and try again.\n\n{:s}: {:s}".format(type(error).__name__, str(error)), "abortable":False})
self.FINISHED = True
3 changes: 2 additions & 1 deletion FlashGBX/FlashGBX_CLI.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ def FinishOperation(self):
self.CONN.INFO["last_action"] = 0

def FindDevices(self, port=None):
# pylint: disable=global-variable-not-assigned
global hw_devices
for hw_device in hw_devices:
dev = hw_device.GbxDevice()
Expand Down Expand Up @@ -607,7 +608,7 @@ def ReadCartridge(self, data):
Util.AGB_Global_CRC32 = 0
db_agb_entry = None
if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)):
with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f:
with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH), encoding="UTF-8") as f:
db_agb = f.read()
db_agb = json.loads(db_agb)
if data["header_sha1"] in db_agb.keys():
Expand Down
40 changes: 32 additions & 8 deletions FlashGBX/FlashGBX_GUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ def __init__(self, args):
self.mnuConfig.addAction("Prefer full &chip erase over sector erase when both available", lambda: self.SETTINGS.setValue("PreferChipErase", str(self.mnuConfig.actions()[2].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("&Verify data after writing", lambda: self.SETTINGS.setValue("VerifyWrittenData", str(self.mnuConfig.actions()[3].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("&Limit voltage to 3.3V when detecting Game Boy flash cartridges", lambda: self.SETTINGS.setValue("AutoDetectLimitVoltage", str(self.mnuConfig.actions()[4].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("Always &generate ROM dump reports", lambda: self.SETTINGS.setValue("GenerateDumpReports", str(self.mnuConfig.actions()[5].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addAction("Limit &baud rate to 1Mbps for GBxCart RW v1.4 devices", lambda: [ self.SETTINGS.setValue("LimitBaudRate", str(self.mnuConfig.actions()[5].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")), self.SetLimitBaudRate() ])
self.mnuConfig.addAction("Always &generate ROM dump reports", lambda: self.SETTINGS.setValue("GenerateDumpReports", str(self.mnuConfig.actions()[6].isChecked()).lower().replace("true", "enabled").replace("false", "disabled")))
self.mnuConfig.addSeparator()
self.mnuConfig.addAction("Re-&enable suppressed messages", self.ReEnableMessages)
self.mnuConfig.addSeparator()
Expand All @@ -211,12 +212,14 @@ def __init__(self, args):
self.mnuConfig.actions()[3].setCheckable(True)
self.mnuConfig.actions()[4].setCheckable(True)
self.mnuConfig.actions()[5].setCheckable(True)
self.mnuConfig.actions()[6].setCheckable(True)
self.mnuConfig.actions()[0].setChecked(self.SETTINGS.value("UpdateCheck") == "enabled")
self.mnuConfig.actions()[1].setChecked(self.SETTINGS.value("SaveFileNameAddDateTime", default="disabled") == "enabled")
self.mnuConfig.actions()[2].setChecked(self.SETTINGS.value("PreferChipErase", default="disabled") == "enabled")
self.mnuConfig.actions()[3].setChecked(self.SETTINGS.value("VerifyWrittenData", default="enabled") == "enabled")
self.mnuConfig.actions()[4].setChecked(self.SETTINGS.value("AutoDetectLimitVoltage", default="disabled") == "enabled")
self.mnuConfig.actions()[5].setChecked(self.SETTINGS.value("GenerateDumpReports", default="disabled") == "enabled")
self.mnuConfig.actions()[5].setChecked(self.SETTINGS.value("LimitBaudRate", default="disabled") == "enabled")
self.mnuConfig.actions()[6].setChecked(self.SETTINGS.value("GenerateDumpReports", default="disabled") == "enabled")

self.btnConfig.setMenu(self.mnuConfig)

Expand Down Expand Up @@ -264,7 +267,7 @@ def __init__(self, args):

def GuiCreateGroupBoxDMGCartInfo(self):
self.grpDMGCartridgeInfo = QtWidgets.QGroupBox("Game Boy Cartridge Information")
self.grpDMGCartridgeInfo.setMinimumWidth(352)
self.grpDMGCartridgeInfo.setMinimumWidth(364)
group_layout = QtWidgets.QVBoxLayout()
group_layout.setContentsMargins(-1, 5, -1, -1)

Expand Down Expand Up @@ -361,7 +364,7 @@ def GuiCreateGroupBoxDMGCartInfo(self):

def GuiCreateGroupBoxAGBCartInfo(self):
self.grpAGBCartridgeInfo = QtWidgets.QGroupBox("Game Boy Advance Cartridge Information")
self.grpAGBCartridgeInfo.setMinimumWidth(352)
self.grpAGBCartridgeInfo.setMinimumWidth(364)
group_layout = QtWidgets.QVBoxLayout()
group_layout.setContentsMargins(-1, 5, -1, -1)

Expand Down Expand Up @@ -459,6 +462,17 @@ def GuiCreateGroupBoxAGBCartInfo(self):
self.grpAGBCartridgeInfo.setLayout(group_layout)
return self.grpAGBCartridgeInfo

def SetLimitBaudRate(self):
if not self.CheckDeviceAlive(): return
mode = self.CONN.GetMode()
limit_baudrate = self.SETTINGS.value("LimitBaudRate")
if limit_baudrate == "enabled":
self.CONN.ChangeBaudRate(baudrate=1000000)
else:
self.CONN.ChangeBaudRate(baudrate=1700000)
self.DisconnectDevice()
self.FindDevices(connectToFirst=True, mode=mode)

def UpdateCheck(self):
update_check = self.SETTINGS.value("UpdateCheck")
if update_check is None:
Expand Down Expand Up @@ -614,7 +628,6 @@ def ConnectDevice(self):
self.CONN = None
if self.cmbDevice.count() == 0: self.lblDevice.setText("No connection.")
return False

elif isinstance(ret, list):
for i in range(0, len(ret)):
status = ret[i][0]
Expand All @@ -637,6 +650,7 @@ def ConnectDevice(self):
return False

if dev.IsConnected():
dev.SetWriteDelay(enable=str(self.SETTINGS.value("WriteDelay", default="disabled")).lower() == "enabled")
qt_app.processEvents()
self.CONN = dev
self.optDMG.setAutoExclusive(False)
Expand Down Expand Up @@ -722,6 +736,8 @@ def FindDevices(self, connectToFirst=False, port=None, mode=None):

messages = []
last_msg = ""

# pylint: disable=global-variable-not-assigned
global hw_devices
for hw_device in hw_devices:
dev = hw_device.GbxDevice()
Expand Down Expand Up @@ -1178,6 +1194,9 @@ def FlashROM(self, dpath=""):

if not just_erase:
self.SETTINGS.setValue(setting_name, os.path.dirname(path))
if os.path.getsize(path) == 0:
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The selected ROM file is empty.", QtWidgets.QMessageBox.Ok)
return
if os.path.getsize(path) > 0x10000000: # reject too large files to avoid exploding RAM
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "ROM files bigger than 256 MB are not supported.", QtWidgets.QMessageBox.Ok)
return
Expand Down Expand Up @@ -1234,7 +1253,7 @@ def FlashROM(self, dpath=""):
mbc2 = Util.get_mbc_name(hdr["mapper_raw"])
compatible_mbc = [ "None", "MBC2", "MBC3", "MBC5", "MBC7", "GBD", "G-MMC1", "HuC-1", "HuC-3" ]
if mbc2 == "None":
mbc = 0x19 # MBC5
pass
elif mbc2 != "None" and not (mbc1 in compatible_mbc and mbc2 in compatible_mbc):
if "mbc" in carts[cart_type] and carts[cart_type]["mbc"] == "manual":
msg_text = "The ROM file you selected uses a different mapper type than your current selection. What mapper should be used when writing the ROM?\n\nSelected mapper type: {:s}\nROM mapper type: {:s}".format(mbc1, mbc2)
Expand All @@ -1251,6 +1270,7 @@ def FlashROM(self, dpath=""):
elif msgbox.clickedButton() == button_2:
mbc = hdr["mapper_raw"]
else:
if mbc1 == "None": mbc1 = "None/Unknown"
msg_text = "Warning: The ROM file you selected uses a different mapper type than your cartridge type. The ROM file may be incompatible with your cartridge.\n\nCartridge mapper type: {:s}\nROM mapper type: {:s}".format(mbc1, mbc2)
answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), msg_text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if answer == QtWidgets.QMessageBox.Cancel: return
Expand Down Expand Up @@ -1416,7 +1436,7 @@ def WriteRAM(self, dpath="", erase=False):

if not erase:
filesize = os.path.getsize(path)
if filesize > 0x200000: # reject too large files to avoid exploding RAM
if filesize == 0 or filesize > 0x200000: # reject too large files to avoid exploding RAM
QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "The size of this file is not supported.", QtWidgets.QMessageBox.Ok)
return

Expand Down Expand Up @@ -1795,7 +1815,7 @@ def ReadCartridge(self, resetStatus=True):

db_agb_entry = None
if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)):
with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f:
with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH), encoding="UTF-8") as f:
db_agb = f.read()
db_agb = json.loads(db_agb)
if data["header_sha1"] in db_agb.keys():
Expand Down Expand Up @@ -2413,5 +2433,9 @@ def run(self):
else:
qt_app.exec_() # PySide2

os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1"
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
qt_app = QApplication(sys.argv)
qt_app.setApplicationName(APPNAME)
75 changes: 67 additions & 8 deletions FlashGBX/Flashcart.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,33 @@ class Flashcart:
CART_READ_FNCPTR = None
CART_POWERCYCLE_FNCPTR = None
PROGRESS_FNCPTR = None
SET_WE_PIN_WR = None
SET_WE_PIN_AUDIO = None
DEFAULT_WE = None
SECTOR_COUNT = 0
SECTOR_POS = 0
SECTOR_MAP = None
CFI = None
LAST_SR = 0x00

def __init__(self, config=None, cart_write_fncptr=None, cart_write_fast_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, progress_fncptr=None):
def __init__(self, config=None, cart_write_fncptr=None, cart_write_fast_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, progress_fncptr=None, set_we_pin_wr=None, set_we_pin_audio=None):
if config is None: config = {}
self.CART_WRITE_FNCPTR = cart_write_fncptr
self.CART_WRITE_FAST_FNCPTR = cart_write_fast_fncptr
self.CART_READ_FNCPTR = cart_read_fncptr
self.CART_POWERCYCLE_FNCPTR = cart_powercycle_fncptr
self.PROGRESS_FNCPTR = progress_fncptr
self.SET_WE_PIN_WR = set_we_pin_wr
self.SET_WE_PIN_AUDIO = set_we_pin_audio
self.CONFIG = config
if "command_set" in config:
self.CONFIG["_command_set"] = config["command_set"]
elif "read_identifier" in config and config["read_identifier"][0][1] == 0x90:
self.CONFIG["_command_set"] = "INTEL"
else:
self.CONFIG["_command_set"] = ""
if "write_pin" in config:
self.DEFAULT_WE = config["write_pin"]

def CartRead(self, address, length=0):
if length == 0:
Expand All @@ -42,16 +49,16 @@ def CartRead(self, address, length=0):
length = 1
return self.CART_READ_FNCPTR(address, length)

def CartWrite(self, commands, flashcart=True, sram=False):
if "command_set" in self.CONFIG and self.CONFIG["command_set"] in ("GBMEMORY", "DMG-MBC5-32M-FLASH"): flashcart = False
dprint(commands, flashcart, sram)
if flashcart and not sram:
def CartWrite(self, commands, fast_write=True, sram=False):
if "command_set" in self.CONFIG and self.CONFIG["command_set"] in ("GBMEMORY", "DMG-MBC5-32M-FLASH"): fast_write = False
dprint(commands, fast_write, sram)
if fast_write and not sram:
self.CART_WRITE_FAST_FNCPTR(commands, flashcart=True)
else:
for command in commands:
address = command[0]
value = command[1]
self.CART_WRITE_FNCPTR(address, value, flashcart=flashcart, sram=sram)
self.CART_WRITE_FNCPTR(address, value, flashcart=fast_write, sram=sram)

def GetCommandSetType(self):
return self.CONFIG["_command_set"].upper()
Expand Down Expand Up @@ -158,7 +165,7 @@ def Unlock(self):
dprint("Reading 0x{:X} bytes from cartridge at 0x{:X} = {:s}".format(command[1], command[0], str(temp)))
time.sleep(0.001)
if "unlock" in self.CONFIG["commands"]:
self.CartWrite(self.CONFIG["commands"]["unlock"])
self.CartWrite(self.CONFIG["commands"]["unlock"], fast_write=False)
time.sleep(0.001)

def Reset(self, full_reset=False, max_address=0x2000000):
Expand All @@ -172,7 +179,7 @@ def Reset(self, full_reset=False, max_address=0x2000000):
if j >= max_address: break
dprint("reset_every @ 0x{:X}".format(j))
for command in self.CONFIG["commands"]["reset"]:
self.CartWrite([[j, command[1]]])
self.CartWrite([[j + command[0], command[1]]])
time.sleep(0.01)
elif "reset" in self.CONFIG["commands"]:
self.CartWrite(self.CONFIG["commands"]["reset"])
Expand Down Expand Up @@ -285,8 +292,23 @@ def ChipErase(self):
for i in range(0, len(self.CONFIG["commands"]["chip_erase"])):
addr = self.CONFIG["commands"]["chip_erase"][i][0]
data = self.CONFIG["commands"]["chip_erase"][i][1]
if len(self.CONFIG["commands"]["chip_erase"][i]) > 2:
we = self.CONFIG["commands"]["chip_erase"][i][2]
else:
we = None

if not addr == None:
if we == "WR":
self.SET_WE_PIN_WR()
elif we == "AUDIO":
self.SET_WE_PIN_AUDIO()
self.CartWrite([[addr, data]])
if we is not None:
if self.DEFAULT_WE == "WR":
self.SET_WE_PIN_WR()
elif self.DEFAULT_WE == "AUDIO":
self.SET_WE_PIN_AUDIO()

time.sleep(0.1)
if self.CONFIG["commands"]["chip_erase_wait_for"][i][0] != None:
addr = self.CONFIG["commands"]["chip_erase_wait_for"][i][0]
Expand All @@ -298,7 +320,18 @@ def ChipErase(self):
for j in range(0, len(self.CONFIG["commands"]["read_status_register"])):
#sr_addr = self.CONFIG["commands"]["read_status_register"][j][0]
sr_data = self.CONFIG["commands"]["read_status_register"][j][1]

if we == "WR":
self.SET_WE_PIN_WR()
elif we == "AUDIO":
self.SET_WE_PIN_AUDIO()
self.CartWrite([[addr, sr_data]])
if we is not None:
if self.DEFAULT_WE == "WR":
self.SET_WE_PIN_WR()
elif self.DEFAULT_WE == "AUDIO":
self.SET_WE_PIN_AUDIO()

self.CartRead(addr, 2) # dummy read (fixes some bootlegs)
wait_for = struct.unpack("<H", self.CartRead(addr, 2))[0]
self.LAST_SR = wait_for
Expand All @@ -320,13 +353,28 @@ def SectorErase(self, pos=0, buffer_pos=0):
for i in range(0, len(self.CONFIG["commands"]["sector_erase"])):
addr = self.CONFIG["commands"]["sector_erase"][i][0]
data = self.CONFIG["commands"]["sector_erase"][i][1]
if len(self.CONFIG["commands"]["sector_erase"][i]) > 2:
we = self.CONFIG["commands"]["sector_erase"][i][2]
else:
we = None

if addr == "SA": addr = pos
if addr == "SA+1": addr = pos + 1
if addr == "SA+2": addr = pos + 2
if addr == "SA+0x4000": addr = pos + 0x4000
if addr == "SA+0x7000": addr = pos + 0x7000
if not addr == None:
if we == "WR":
self.SET_WE_PIN_WR()
elif we == "AUDIO":
self.SET_WE_PIN_AUDIO()
self.CartWrite([[addr, data]])
if we is not None:
if self.DEFAULT_WE == "WR":
self.SET_WE_PIN_WR()
elif self.DEFAULT_WE == "AUDIO":
self.SET_WE_PIN_AUDIO()

if self.CONFIG["commands"]["sector_erase_wait_for"][i][0] != None:
addr = self.CONFIG["commands"]["sector_erase_wait_for"][i][0]
data = self.CONFIG["commands"]["sector_erase_wait_for"][i][1]
Expand All @@ -342,7 +390,18 @@ def SectorErase(self, pos=0, buffer_pos=0):
for j in range(0, len(self.CONFIG["commands"]["read_status_register"])):
sr_addr = self.CONFIG["commands"]["read_status_register"][j][0]
sr_data = self.CONFIG["commands"]["read_status_register"][j][1]

if we == "WR":
self.SET_WE_PIN_WR()
elif we == "AUDIO":
self.SET_WE_PIN_AUDIO()
self.CartWrite([[sr_addr, sr_data]])
if we is not None:
if self.DEFAULT_WE == "WR":
self.SET_WE_PIN_WR()
elif self.DEFAULT_WE == "AUDIO":
self.SET_WE_PIN_AUDIO()

self.CartRead(addr, 2) # dummy read (fixes some bootlegs)
temp = self.CartRead(addr, 2)
if len(temp) != 2:
Expand Down
Loading

0 comments on commit 288dbf8

Please sign in to comment.