private static void CheckRAMAdapter(IFamicomDumperConnectionExt dumper) { // Just simple test that RAM adapter is connected bool ramAdapterPresent = true; dumper.WriteCpu(0x4023, 0x01); // enable disk registers dumper.WriteCpu(0x4026, 0x00); dumper.WriteCpu(0x4025, 0b00100110); // reset dumper.WriteCpu(0x0000, 0xFF); // to prevent open bus read var ext = dumper.ReadCpu(0x4033); if (ext != 0x00) { ramAdapterPresent = false; } dumper.WriteCpu(0x4026, 0xFF); dumper.WriteCpu(0x0000, 0x00); // to prevent open bus read ext = dumper.ReadCpu(0x4033); if ((ext & 0x7F) != 0x7F) { ramAdapterPresent = false; } if (!ramAdapterPresent) { throw new IOException("RAM adapter IO error, is it connected?"); } }
public static void PrintFlashInfo(IFamicomDumperConnectionExt dumper) { Program.Reset(dumper); dumper.WriteCpu(0x5007, 0x04); // enable PRG write dumper.WriteCpu(0x5002, 0xFE); // mask = 32K var cfi = FlashHelper.GetCFIInfo(dumper); FlashHelper.PrintCFIInfo(cfi); FlashHelper.LockBitsCheckPrint(dumper); FlashHelper.PPBLockBitCheckPrint(dumper); }
public static void PPBClear(IFamicomDumperConnectionExt dumper) { // enable PRG write dumper.WriteCpu(0x5007, 0x04); // mask = 32K dumper.WriteCpu(0x5002, 0xFE); // Sector 0 dumper.WriteCpu(0x5000, 0); dumper.WriteCpu(0x5001, 0); FlashHelper.PPBClear(dumper); }
public static void DumpFDS(IFamicomDumperConnectionExt dumper, string fileName, byte sides = 1, bool dumpHiddenFiles = true, bool useHeader = true) { if (dumper.ProtocolVersion < 3) { throw new NotSupportedException("Dumper firmware version is too old, update it to read/write FDS cards"); } CheckRAMAdapter(dumper); var sideImages = new List <FdsDiskSide>(); for (int side = 1; side <= sides; side++) { var driveStatus = dumper.ReadCpu(0x4032); if ((driveStatus & 1) != 0) { Console.Write($"Please set disk card, side #{side}... "); while ((driveStatus & 1) != 0) { Thread.Sleep(100); driveStatus = dumper.ReadCpu(0x4032); } Console.WriteLine("OK"); } var sideImage = DumpFDSSide(dumper, dumpHiddenFiles, printDiskInfo: true); sideImages.Add(sideImage); if (side < sides) { driveStatus = dumper.ReadCpu(0x4032); if ((driveStatus & 1) == 0) { Console.Write($"Please remove disk card... "); while ((driveStatus & 1) == 0) { Thread.Sleep(100); driveStatus = dumper.ReadCpu(0x4032); } Console.WriteLine("OK"); } } } Console.Write($"Saving to {fileName}... "); var fdsImage = new FdsFile(sideImages); fdsImage.Save(fileName, useHeader); Console.WriteLine("OK"); }
public static void PPBClear(IFamicomDumperConnectionExt dumper, ushort coolboyReg) { // Sector 0 int bank = 0; byte r0 = (byte)(((bank >> 3) & 0x07) // 5, 4, 3 bits | (((bank >> 9) & 0x03) << 4) // 10, 9 bits | (1 << 6)); // resets 4th mask bit byte r1 = (byte)((((bank >> 7) & 0x03) << 2) // 8, 7 | (((bank >> 6) & 1) << 4) // 6 | (1 << 7)); // resets 5th mask bit byte r2 = 0; byte r3 = (byte)((1 << 4) // NROM mode | ((bank & 7) << 1)); // 2, 1, 0 bits dumper.WriteCpu(coolboyReg, r0, r1, r2, r3); FlashHelper.PPBClear(dumper); }
public static void StartServer(IFamicomDumperConnectionExt dumper, string url) { FamicomDumperService.dumper = dumper; Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(webBuilder => { #if !DEBUG webBuilder.ConfigureLogging((context, logging) => logging.ClearProviders()); #endif webBuilder.UseUrls(url); webBuilder.ConfigureKestrel((options) => { options.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http2); }); webBuilder.UseStartup <GrpcStartup>(); }) .Build() .Run(); }
public static byte DetectVersion(IFamicomDumperConnectionExt dumper) { byte version; Console.Write("Detecting COOLBOY version... "); // 0th CHR bank using both methods dumper.WriteCpu(0x5000, 0, 0, 0, 0x10); dumper.WriteCpu(0x6000, 0, 0, 0, 0x10); // Writing 0 dumper.WritePpu(0x0000, 0); // First CHR bank using both methods dumper.WriteCpu(0x5000, 0, 0, 1, 0x10); dumper.WriteCpu(0x6000, 0, 0, 1, 0x10); // Writing 1 dumper.WritePpu(0x0000, 1); // 0th bank using first method dumper.WriteCpu(0x6000, 0, 0, 0, 0x10); byte v6000 = dumper.ReadPpu(0x0000); // return dumper.WriteCpu(0x6000, 0, 0, 1, 0x10); // 0th bank using second method dumper.WriteCpu(0x5000, 0, 0, 0, 0x10); byte v5000 = dumper.ReadPpu(0x0000); if (v6000 == 0 && v5000 == 1) { version = 1; } else if (v6000 == 1 && v5000 == 0) { version = 2; } else { throw new IOException("Can't detect COOLBOY version"); } Console.WriteLine($"Version: {version}"); return(version); }
public static void PrintFlashInfo(IFamicomDumperConnectionExt dumper) { Program.Reset(dumper); var version = DetectVersion(dumper); var CoolboyReg = (UInt16)(version == 2 ? 0x5000 : 0x6000); int bank = 0; byte r0 = (byte)(((bank >> 3) & 0x07) // 5, 4, 3 bits | (((bank >> 9) & 0x03) << 4) // 10, 9 bits | (1 << 6)); // resets 4th mask bit byte r1 = (byte)((((bank >> 7) & 0x03) << 2) // 8, 7 | (((bank >> 6) & 1) << 4) // 6 | (1 << 7)); // resets 5th mask bit byte r2 = 0; byte r3 = (byte)((1 << 4) // NROM mode | ((bank & 7) << 1)); // 2, 1, 0 bits dumper.WriteCpu(CoolboyReg, r0, r1, r2, r3); var cfi = FlashHelper.GetCFIInfo(dumper); FlashHelper.PrintCFIInfo(cfi); FlashHelper.LockBitsCheckPrint(dumper); FlashHelper.PPBLockBitCheckPrint(dumper); }
private static FdsDiskSide DumpFDSSide(IFamicomDumperConnectionExt dumper, bool dumpHiddenFiles = true, bool printDiskInfo = false) { if (dumper.ProtocolVersion < 3) { throw new NotSupportedException("Dumper firmware version is too old, update it to read/write FDS cards"); } var oldTimeout = dumper.Timeout; try { dumper.Timeout = 30000; CheckRAMAdapter(dumper); IEnumerable <IFdsBlock> blocks; if (dumper.MaxReadPacketSize != ushort.MaxValue) { // Reading block by block blocks = DumpSlow(dumper, dumpHiddenFiles, printDiskInfo); } else { // Reading the whole disk at once blocks = DumpFast(dumper, dumpHiddenFiles, printDiskInfo); } var sideImage = new FdsDiskSide(blocks); if (sideImage.Files.Count < sideImage.FileAmount) { throw new IOException($"Invalid file count: {sideImage.Files.Count} < {sideImage.FileAmount}"); } return(sideImage); } finally { dumper.Timeout = oldTimeout; } }
public static void Write(IFamicomDumperConnectionExt dumper, string fileName, IEnumerable <int> badSectors, bool silent, bool needCheck = false, bool writePBBs = false, bool ignoreBadSectors = false) { byte[] PRG; if (Path.GetExtension(fileName).ToLower() == ".bin") { PRG = File.ReadAllBytes(fileName); } else { try { var nesFile = new NesFile(fileName); PRG = nesFile.PRG.ToArray(); } catch { var nesFile = new UnifFile(fileName); PRG = nesFile.Fields["PRG0"]; } } int banks = PRG.Length / BANK_SIZE; Program.Reset(dumper); var version = DetectVersion(dumper); var coolboyReg = (ushort)(version == 2 ? 0x5000 : 0x6000); FlashHelper.ResetFlash(dumper); var cfi = FlashHelper.GetCFIInfo(dumper); Console.WriteLine($"Device size: {cfi.DeviceSize / 1024 / 1024} MByte / {cfi.DeviceSize / 1024 / 1024 * 8} Mbit"); Console.WriteLine($"Maximum number of bytes in multi-byte program: {cfi.MaximumNumberOfBytesInMultiProgram}"); if (dumper.ProtocolVersion >= 3) { dumper.SetMaximumNumberOfBytesInMultiProgram(cfi.MaximumNumberOfBytesInMultiProgram); } FlashHelper.LockBitsCheckPrint(dumper); if (PRG.Length > cfi.DeviceSize) { throw new ArgumentOutOfRangeException("PRG.Length", "This ROM is too big for this cartridge"); } try { PPBClear(dumper, coolboyReg); } catch (Exception ex) { if (!silent) { Program.PlayErrorSound(); } Console.WriteLine($"ERROR! {ex.Message}. Lets continue anyway."); } var writeStartTime = DateTime.Now; var lastSectorTime = DateTime.Now; var timeEstimated = new TimeSpan(); int totalErrorCount = 0; int currentErrorCount = 0; var newBadSectorsList = new List <int>(badSectors); for (int bank = 0; bank < banks; bank++) { while (badSectors.Contains(bank / 8) || newBadSectorsList.Contains(bank / 8)) { bank += 8; // bad sector :( } try { byte r0 = (byte)(((bank >> 3) & 0x07) // 5, 4, 3 bits | (((bank >> 9) & 0x03) << 4) // 10, 9 bits | (1 << 6)); // resets 4th mask bit byte r1 = (byte)((((bank >> 7) & 0x03) << 2) // 8, 7 | (((bank >> 6) & 1) << 4) // 6 | (1 << 7)); // resets 5th mask bit byte r2 = 0; byte r3 = (byte)((1 << 4) // NROM mode | ((bank & 7) << 1)); // 2, 1, 0 bits dumper.WriteCpu(coolboyReg, r0, r1, r2, r3); var data = new byte[BANK_SIZE]; int pos = bank * BANK_SIZE; if (pos % (128 * 1024) == 0) { timeEstimated = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (banks - bank) / 8); timeEstimated = timeEstimated.Add(DateTime.Now - writeStartTime); lastSectorTime = DateTime.Now; Console.Write($"Erasing sector #{bank / 8}... "); dumper.EraseFlashSector(); Console.WriteLine("OK"); } Array.Copy(PRG, pos, data, 0, data.Length); var timePassed = DateTime.Now - writeStartTime; Console.Write($"Writing bank #{bank}/{banks} ({100 * bank / banks}%, {timePassed.Hours:D2}:{timePassed.Minutes:D2}:{timePassed.Seconds:D2}/{timeEstimated.Hours:D2}:{timeEstimated.Minutes:D2}:{timeEstimated.Seconds:D2})... "); dumper.WriteFlash(0x0000, data); Console.WriteLine("OK"); if ((bank % 8 == 7) || (bank == banks - 1)) // After last bank in sector { if (writePBBs) { FlashHelper.PPBSet(dumper); } currentErrorCount = 0; } } catch (Exception ex) { totalErrorCount++; currentErrorCount++; Console.WriteLine($"Error {ex.GetType()}: {ex.Message}"); if (!silent) { Program.PlayErrorSound(); } if (currentErrorCount >= 5) { if (!ignoreBadSectors) { throw; } else { newBadSectorsList.Add(bank / 8); currentErrorCount = 0; Console.WriteLine($"Lets skip sector #{bank / 8}"); } } else { Console.WriteLine("Lets try again"); } bank = (bank & ~7) - 1; Program.Reset(dumper); FlashHelper.ResetFlash(dumper); continue; } } if (totalErrorCount > 0) { Console.WriteLine($"Write error count: {totalErrorCount}"); } if (newBadSectorsList.Any()) { Console.WriteLine($"Can't write sectors: {string.Join(", ", newBadSectorsList.OrderBy(s => s))}"); } var wrongCrcSectorsList = new List <int>(); if (needCheck) { Console.WriteLine("Starting verification process"); Program.Reset(dumper); var readStartTime = DateTime.Now; lastSectorTime = DateTime.Now; timeEstimated = new TimeSpan(); for (int bank = 0; bank < banks; bank++) { while (badSectors.Contains(bank / 8)) { bank += 8; // bad sector :( } byte r0 = (byte)(((bank >> 3) & 0x07) // 5, 4, 3 bits | (((bank >> 9) & 0x03) << 4) // 10, 9 bits | (1 << 6)); // resets 4th mask bit byte r1 = (byte)((((bank >> 7) & 0x03) << 2) // 8, 7 | (((bank >> 6) & 1) << 4) // 6 | (1 << 7)); // resets 5th mask bit byte r2 = 0; byte r3 = (byte)((1 << 4) // NROM mode | ((bank & 7) << 1)); // 2, 1, 0 bits dumper.WriteCpu(coolboyReg, r0, r1, r2, r3); int pos = bank * BANK_SIZE; if (pos % (128 * 1024) == 0) { timeEstimated = new TimeSpan((DateTime.Now - lastSectorTime).Ticks * (banks - bank) / 8); timeEstimated = timeEstimated.Add(DateTime.Now - readStartTime); lastSectorTime = DateTime.Now; } ushort crc = Crc16Calculator.CalculateCRC16(PRG, pos, BANK_SIZE); var timePassed = DateTime.Now - readStartTime; Console.Write($"Reading CRC of bank #{bank}/{banks} ({100 * bank / banks}%, {timePassed.Hours:D2}:{timePassed.Minutes:D2}:{timePassed.Seconds:D2}/{timeEstimated.Hours:D2}:{timeEstimated.Minutes:D2}:{timeEstimated.Seconds:D2})... "); var crcr = dumper.ReadCpuCrc(0x8000, BANK_SIZE); if (crcr != crc) { Console.WriteLine($"Verification failed: {crcr:X4} != {crc:X4}"); if (!silent) { Program.PlayErrorSound(); } wrongCrcSectorsList.Add(bank / 8); } else { Console.WriteLine($"OK (CRC = {crcr:X4})"); } } if (totalErrorCount > 0) { Console.WriteLine($"Write error count: {totalErrorCount}"); } if (newBadSectorsList.Any()) { Console.WriteLine($"Can't write sectors: {string.Join(", ", newBadSectorsList.OrderBy(s => s))}"); } if (wrongCrcSectorsList.Any()) { Console.WriteLine($"Sectors with wrong CRC: {string.Join(", ", wrongCrcSectorsList.Distinct().OrderBy(s => s))}"); } } if (newBadSectorsList.Any() || wrongCrcSectorsList.Any()) { throw new IOException("Cartridge is not writed correctly"); } }
private static IFdsBlock[] DumpFast(IFamicomDumperConnectionExt dumper, bool dumpHiddenFiles = false, bool printDiskInfo = false) { Console.Write($"Reading disk... "); var blocks = dumper.ReadFdsBlocks().ToArray(); Console.WriteLine($"Done"); List <IFdsBlock> result = new(); if (blocks.Length == 0) { throw new IOException("Invalid disk info block"); } if (!blocks[0].CrcOk) { throw new IOException($"Invalid CRC on disk info block"); } var fdsBlockDiskInfo = FdsBlockDiskInfo.FromBytes(blocks[0].Data); if (!fdsBlockDiskInfo.IsValid) { throw new IOException($"Invalid disk info block type"); } if (printDiskInfo) { PrintDiskHeaderInfo(fdsBlockDiskInfo); } result.Add(fdsBlockDiskInfo); if (blocks.Length == 1) { throw new IOException("Invalid file amount block"); } if (!blocks[1].CrcOk) { throw new IOException($"Invalid CRC on file amount block"); } var fdsBlockFileAmount = FdsBlockFileAmount.FromBytes(blocks[1].Data); if (!fdsBlockFileAmount.IsValid) { throw new IOException($"Invalid file amount block type"); } var fileAmount = fdsBlockFileAmount.FileAmount; byte visibleBlockAmount = (byte)(2 + fileAmount * 2); if (printDiskInfo) { Console.WriteLine($"Number of non-hidden files: {fileAmount}"); } result.Add(fdsBlockFileAmount); // Check files and print info for (int blockNumber = 2; blockNumber < (dumpHiddenFiles ? blocks.Length : visibleBlockAmount); blockNumber++) { if (!blocks[blockNumber].CrcOk) { if (blocks.Length < visibleBlockAmount) { throw new IOException($"Invalid CRC on block #{blockNumber} (file #{(blockNumber - 2) / 2})"); } else { if (printDiskInfo) { Console.WriteLine($"Invalid CRC on block #{blockNumber}, it's not hidden file, aboritng"); } break; } } IFdsBlock block = (blockNumber % 2 == 0) ? FdsBlockFileHeader.FromBytes(blocks[blockNumber].Data) : FdsBlockFileData.FromBytes(blocks[blockNumber].Data); if (!block.IsValid) { if (blocks.Length < visibleBlockAmount) { throw new IOException($"Invalid block #{blockNumber} (file #{(blockNumber - 2) / 2}) type"); } else { break; } } if (block is FdsBlockFileHeader && printDiskInfo) { Console.WriteLine($"File #{(blockNumber - 2) / 2}/{fileAmount}:"); PrintFileHeaderInfo(block as FdsBlockFileHeader); } result.Add(block); } if (blocks.Length < visibleBlockAmount) { throw new IOException($"Only {(blocks.Length - 2) / 2} of {fileAmount} valid files received"); } if (dumpHiddenFiles && printDiskInfo) { Console.WriteLine($"Number of hidden files: {(blocks.Length - 2) / 2 - fileAmount}"); } return(result.ToArray()); }
private static IEnumerable <IFdsBlock> DumpSlow(IFamicomDumperConnectionExt dumper, bool dumpHiddenFiles = false, bool printDiskInfo = false) { var result = new List <IFdsBlock>(); byte blockNumber = 0; byte fileAmount = 0; byte visibleBlockAmount = 0; while (true) { switch (blockNumber) { case 0: Console.Write("Reading block #0 (disk info block)... "); break; case 1: Console.Write("Reading block #1 (file amount block)... "); break; default: if ((blockNumber % 2) == 0) { Console.Write($"Reading block #{blockNumber} (file #{(blockNumber - 2) / 2}/{(result[1] as FdsBlockFileAmount).FileAmount} header block)... "); } else { Console.Write($"Reading block #{blockNumber} (file #{(blockNumber - 2) / 2}/{(result[1] as FdsBlockFileAmount).FileAmount} data block)... "); } break; } var fdsData = dumper.ReadFdsBlocks(blockNumber, 1); if (fdsData.Length == 0) { throw new IOException($"Can't read block #{blockNumber}"); } if (!fdsData[0].CrcOk) { switch (blockNumber) { case 0: throw new IOException($"Invalid CRC on disk info block"); case 1: throw new IOException($"Invalid CRC on file amount block"); } // Fatal error if bad CRC on non-hidden file if (result.Count < visibleBlockAmount) { throw new IOException($"Invalid CRC on block"); } else { break; } } IFdsBlock block; switch (blockNumber) { case 0: block = FdsBlockDiskInfo.FromBytes(fdsData[0].Data); break; case 1: block = FdsBlockFileAmount.FromBytes(fdsData[0].Data); fileAmount = (block as FdsBlockFileAmount).FileAmount; visibleBlockAmount = (byte)(2 + fileAmount * 2); break; default: if ((blockNumber % 2) == 0) { block = FdsBlockFileHeader.FromBytes(fdsData[0].Data); } else { block = FdsBlockFileData.FromBytes(fdsData[0].Data); } break; } if (!block.IsValid) { switch (block.GetType().Name) { case nameof(FdsBlockDiskInfo): throw new IOException($"Invalid disk info block"); case nameof(FdsBlockFileAmount): throw new IOException($"Invalid file amount block"); } // Fatal error if bad block ID on non-hidden file if (result.Count < visibleBlockAmount) { throw new IOException($"Invalid block"); } else { break; } } result.Add(block); Console.WriteLine($"OK"); // Some info if (printDiskInfo) { switch (blockNumber) { case 0: PrintDiskHeaderInfo(block as FdsBlockDiskInfo); break; case 1: Console.WriteLine($"Number of non-hidden files: {fileAmount}"); break; default: if ((blockNumber % 2) == 0) { Console.WriteLine($"File #{(blockNumber - 2) / 2}:"); PrintFileHeaderInfo(block as FdsBlockFileHeader); } break; } } // Abort if last file dumped if (!dumpHiddenFiles && (result.Count >= 2) && (result.Count >= visibleBlockAmount)) { break; } blockNumber++; } if (dumpHiddenFiles) { Console.WriteLine($"Number of hidden files: {(result.Count - 2) / 2 - (result[1] as FdsBlockFileAmount).FileAmount}"); } return(result); }
public static void WriteFDS(IFamicomDumperConnectionExt dumper, string fileName, bool needCheck = false) { if (dumper.ProtocolVersion < 3) { throw new NotSupportedException("Dumper firmware version is too old, update it to read/write FDS cards"); } var oldTimeout = dumper.Timeout; try { dumper.Timeout = 30000; //CheckRAMAdapter(dumper); var rom = new FdsFile(fileName); for (int sideNumber = 0; sideNumber < rom.Sides.Count; sideNumber++) { var driveStatus = dumper.ReadCpu(0x4032); if ((driveStatus & 1) != 0) { Console.Write($"Please set disk card, side #{sideNumber + 1}... "); while ((driveStatus & 1) != 0) { Thread.Sleep(100); driveStatus = dumper.ReadCpu(0x4032); } Console.WriteLine("OK"); } PrintDiskHeaderInfo(rom.Sides[sideNumber].DiskInfoBlock); Console.WriteLine($"Number of non-hidden files: {rom.Sides[sideNumber].FileAmount}"); Console.WriteLine($"Number of hidden files: {rom.Sides[sideNumber].Files.Count - rom.Sides[sideNumber].FileAmount}"); var blocks = rom.Sides[sideNumber].GetBlocks().ToArray(); Console.WriteLine($"Total blocks to write: {blocks.Length}"); byte blocksWrited = 0; while (blocksWrited < blocks.Length) { uint totalSize = 1; var blockIDs = new List <byte>(); var blocksToWrite = new List <IFdsBlock>(); for (byte i = blocksWrited; i < blocks.Length; i++) { if (totalSize + blocks[i].Length + 3 <= dumper.MaxWritePacketSize) { blocksToWrite.Add(blocks[i]); blockIDs.Add(i); totalSize += blocks[i].Length + 3; } else { break; } } if (!blocksToWrite.Any()) { throw new OutOfMemoryException("Dumper has not enoght memory to write such big block"); } Console.Write($"Writing block(s): {string.Join(", ", blockIDs)}... "); dumper.WriteFdsBlocks(blockIDs.ToArray(), blocksToWrite.Select(b => b.ToBytes()).ToArray()); Console.WriteLine("OK"); blocksWrited += (byte)blocksToWrite.Count; } if (needCheck) { Console.WriteLine("Starting verification process"); var hiddenFiles = rom.Sides[sideNumber].Files.Count > rom.Sides[sideNumber].FileAmount; var sideImage = DumpFDSSide(dumper, dumpHiddenFiles: hiddenFiles, printDiskInfo: false); if (!sideImage.DiskInfoBlock.Equals(rom.Sides[sideNumber].DiskInfoBlock)) { throw new IOException("Disk info block verification failed"); } if (!sideImage.FileAmount.Equals(rom.Sides[sideNumber].FileAmount)) { throw new IOException("File amount block verification failed"); } if (sideImage.Files.Count < rom.Sides[sideNumber].Files.Count) { throw new IOException($"Invalid file count: {sideImage.Files.Count} < {rom.Sides[sideNumber].Files.Count}"); } for (int f = 0; f < rom.Sides[sideNumber].Files.Count; f++) { if (!sideImage.Files[f].HeaderBlock.Equals(rom.Sides[sideNumber].Files[f].HeaderBlock)) { throw new IOException($"File #{f} header block verification failed"); } if (!sideImage.Files[f].DataBlock.Equals(rom.Sides[sideNumber].Files[f].DataBlock)) { throw new IOException($"File #{f} data block verification failed"); } } Console.WriteLine("Verification successful."); } if (sideNumber + 1 < rom.Sides.Count) { driveStatus = dumper.ReadCpu(0x4032); if ((driveStatus & 1) == 0) { Console.Write($"Please remove disk card... "); while ((driveStatus & 1) == 0) { Thread.Sleep(100); driveStatus = dumper.ReadCpu(0x4032); } Console.WriteLine("OK"); } } } } finally { dumper.Timeout = oldTimeout; } }