sc64deployer done

This commit is contained in:
Mateusz Faderewski 2023-03-11 18:36:30 +01:00
parent 70d287c855
commit 8479c80ebe
6 changed files with 541 additions and 46 deletions

View File

@ -90,9 +90,9 @@ Second, program FPGA, microcontroller and bootloader:
4. Check in device manager which port number `COMx` is assigned to serial adapter
5. Connect SC64 board to the PC with USB-C cable (***IMPORTANT:*** connect it to the same computer as serial adapter)
6. Locate `primer.py` script in root folder
7. Make sure these files are located in the same folder as `primer.py` script: `requirements.txt`, `sc64-firmware.bin`
7. Make sure these files are located in the same folder as `primer.py` script: `requirements.txt`, `sc64-firmware-{version}.bin`
8. Run `pip3 install -r requirements.txt` to install required python packages
9. Run `python3 primer.py COMx sc64-firmware.bin` (replace `COMx` with port located in step **4**)
9. Run `python3 primer.py COMx sc64-firmware-{version}.bin` (replace `COMx` with port located in step **4**)
10. Follow the instructions on the screen
11. Wait until programming process has finished (**DO NOT STOP PROGRAMMING PROCESS OR DISCONNECT SC64 BOARD FROM PC**, doing so might irrecoverably break programming through UART header and you would need to program FPGA and/or microcontroller with separate dedicated programming interfaces through *Tag-Connect* connector on the PCB)
@ -107,4 +107,4 @@ Congratulations! Your SC64 flashcart should be ready for use!
This issue can be attributed to incorrectly programmed FT232H EEPROM in the first programming step.
Check again in `FT_PROG` application if device was configured properly.
Once FPGA and microcontroller has been programmed successfully `primer.py` script needs to be run in special mode.
Please use command `python3 primer.py COMx sc64-firmware.bin --bootloader-only` to try programming bootloader again.
Please use command `python3 primer.py COMx sc64-firmware-{version}.bin --bootloader-only` to try programming bootloader again.

322
sw/deployer/src/disk.rs Normal file
View File

@ -0,0 +1,322 @@
use crate::sc64::Error;
use std::{
collections::HashMap,
fs::File,
io::{Read, Seek, SeekFrom, Write},
};
const BLOCKS_PER_TRACK: usize = 2;
const SECTORS_PER_BLOCK: usize = 85;
const SYSTEM_SECTOR_LENGTH: usize = 232;
const BAD_TRACKS_PER_ZONE: usize = 12;
#[derive(Clone, Copy, PartialEq)]
pub enum Format {
Retail,
Development,
}
struct SystemAreaInfo<'a> {
format: Format,
sector_length: usize,
sys_lba: &'a [usize],
bad_lba: &'a [usize],
}
const SYSTEM_AREA: [SystemAreaInfo; 2] = [
SystemAreaInfo {
format: Format::Retail,
sector_length: 232,
sys_lba: &[9, 8, 1, 0],
bad_lba: &[2, 3, 10, 11, 12, 16, 17, 18, 19, 20, 21, 22, 23],
},
SystemAreaInfo {
format: Format::Development,
sector_length: 192,
sys_lba: &[11, 10, 3, 2],
bad_lba: &[0, 1, 8, 9, 16, 17, 18, 19, 20, 21, 22, 23],
},
];
const ID_LBAS: [usize; 2] = [15, 14];
struct DiskZone {
head: usize,
sector_length: usize,
tracks: usize,
track_offset: usize,
}
macro_rules! zone {
($h:expr, $l:expr, $t:expr, $o:expr) => {
DiskZone {
head: $h,
sector_length: $l,
tracks: $t,
track_offset: $o,
}
};
}
const DISK_ZONES: [DiskZone; 16] = [
zone!(0, 232, 158, 0),
zone!(0, 216, 158, 158),
zone!(0, 208, 149, 316),
zone!(0, 192, 149, 465),
zone!(0, 176, 149, 614),
zone!(0, 160, 149, 763),
zone!(0, 144, 149, 912),
zone!(0, 128, 114, 1061),
zone!(1, 216, 158, 157),
zone!(1, 208, 158, 315),
zone!(1, 192, 149, 464),
zone!(1, 176, 149, 613),
zone!(1, 160, 149, 762),
zone!(1, 144, 149, 911),
zone!(1, 128, 149, 1060),
zone!(1, 112, 114, 1174),
];
const DISK_VZONE_TO_PZONE: [[usize; 16]; 7] = [
[0, 1, 2, 9, 8, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10],
[0, 1, 2, 3, 10, 9, 8, 4, 5, 6, 7, 15, 14, 13, 12, 11],
[0, 1, 2, 3, 4, 11, 10, 9, 8, 5, 6, 7, 15, 14, 13, 12],
[0, 1, 2, 3, 4, 5, 12, 11, 10, 9, 8, 6, 7, 15, 14, 13],
[0, 1, 2, 3, 4, 5, 6, 13, 12, 11, 10, 9, 8, 7, 15, 14],
[0, 1, 2, 3, 4, 5, 6, 7, 14, 13, 12, 11, 10, 9, 8, 15],
[0, 1, 2, 3, 4, 5, 6, 7, 15, 14, 13, 12, 11, 10, 9, 8],
];
struct Mapping {
lba: usize,
offset: usize,
length: usize,
writable: bool,
}
pub struct Disk {
file: File,
format: Format,
mapping: HashMap<usize, Mapping>,
}
impl Disk {
pub fn get_format(&self) -> &Format {
&self.format
}
pub fn get_lba(&self, track: u32, head: u32, block: u32) -> Option<usize> {
if head == 0 && track < 12 {
return Some((track << 1 | block ^ (track % 2)) as usize);
}
let location = track << 2 | head << 1 | block;
self.mapping
.get(&(location as usize))
.map(|block| block.lba)
}
pub fn read_block(
&mut self,
track: u32,
head: u32,
block: u32,
) -> Result<Option<Vec<u8>>, Error> {
let location = track << 2 | head << 1 | block;
if let Some(block) = self.mapping.get(&(location as usize)) {
let mut data = vec![0u8; block.length];
self.file.seek(SeekFrom::Start(block.offset as u64))?;
self.file.read_exact(&mut data)?;
return Ok(Some(data));
}
Ok(None)
}
pub fn write_block(
&mut self,
track: u32,
head: u32,
block: u32,
data: &[u8],
) -> Result<Option<()>, Error> {
let location = track << 2 | head << 1 | block;
if let Some(block) = self.mapping.get(&(location as usize)) {
if block.length == data.len() && block.writable {
self.file.seek(SeekFrom::Start(block.offset as u64))?;
self.file.write_all(data)?;
return Ok(Some(()));
}
}
Ok(None)
}
}
pub fn open(path: &str) -> Result<Disk, Error> {
let mut file = File::options().read(true).write(true).open(path)?;
let (format, mapping) = load_ndd(&mut file)?;
Ok(Disk {
file,
format,
mapping,
})
}
pub fn open_multiple(paths: &[String]) -> Result<Vec<Disk>, Error> {
let mut disks: Vec<Disk> = Vec::new();
for path in paths {
let disk = open(path)?;
disks.push(disk);
}
if !disks.windows(2).all(|d| d[0].format == d[1].format) {
return Err(Error::new("Disk format mismatch"));
}
Ok(disks)
}
fn load_ndd(file: &mut File) -> Result<(Format, HashMap<usize, Mapping>), Error> {
let mut disk_format: Option<Format> = None;
let mut disk_type: u8 = 0;
let mut sys_data = vec![0u8; SYSTEM_SECTOR_LENGTH];
let mut bad_lba: Vec<usize> = Vec::new();
for info in SYSTEM_AREA {
bad_lba.clear();
for &lba in info.sys_lba {
let data = load_sys_lba(file, lba)?;
if verify_sys_lba(&data, info.sector_length) {
if (data[4] != 0x10) || ((data[5] & 0xF0) != 0x10) {
bad_lba.push(lba);
} else {
disk_format = Some(info.format);
disk_type = data[5] & 0x0F;
sys_data = data[0..SYSTEM_SECTOR_LENGTH].to_vec();
}
} else {
bad_lba.push(lba);
}
}
if disk_format.is_some() {
bad_lba.append(&mut info.bad_lba.to_vec());
break;
}
}
if disk_format.is_none() {
return Err(Error::new("Provided 64DD disk file is not valid"));
}
if disk_type >= DISK_VZONE_TO_PZONE.len() as u8 {
return Err(Error::new("Unknown disk type"));
}
let mut id_lba_valid = false;
for lba in ID_LBAS {
let data = load_sys_lba(file, lba)?;
let valid = verify_sys_lba(&data, SYSTEM_SECTOR_LENGTH);
if !valid {
bad_lba.push(lba);
}
id_lba_valid |= valid;
}
if !id_lba_valid {
return Err(Error::new("No valid ID LBA found"));
}
let mut zone_bad_tracks: Vec<Vec<usize>> = Vec::new();
for (zone, info) in DISK_ZONES.iter().enumerate() {
let mut bad_tracks: Vec<usize> = Vec::new();
let start = if zone == 0 { 0 } else { sys_data[0x07 + zone] };
let stop = sys_data[0x07 + zone + 1];
for offset in start..stop {
bad_tracks.push(sys_data[0x20 + offset as usize] as usize);
}
for track in 0..(BAD_TRACKS_PER_ZONE - bad_tracks.len()) {
bad_tracks.push(info.tracks - track - 1);
}
zone_bad_tracks.push(bad_tracks);
}
let mut mapping = HashMap::new();
let mut current_lba: usize = 0;
let mut starting_block: usize = 0;
let mut file_offset: usize = 0;
for zone in DISK_VZONE_TO_PZONE[disk_type as usize] {
let DiskZone {
head,
sector_length,
tracks,
track_offset,
} = DISK_ZONES[zone];
let mut track = track_offset;
for zone_track in 0..tracks {
let current_zone_track = if head == 0 {
zone_track
} else {
(tracks - 1) - zone_track
};
if zone_bad_tracks[zone].contains(&current_zone_track) {
track = if head == 0 {
track + 1
} else {
track.saturating_sub(1)
};
continue;
}
for block in 0..BLOCKS_PER_TRACK {
let location = track << 2 | head << 1 | (starting_block ^ block);
let length = sector_length * SECTORS_PER_BLOCK;
if !bad_lba.contains(&current_lba) {
mapping.insert(
location,
Mapping {
lba: current_lba,
offset: file_offset,
length,
writable: true,
},
);
}
file_offset += length;
current_lba += 1;
}
track = if head == 0 {
track + 1
} else {
track.saturating_sub(1)
};
starting_block ^= 1;
}
}
Ok((disk_format.unwrap(), mapping))
}
fn load_sys_lba(file: &mut File, lba: usize) -> Result<Vec<u8>, Error> {
let length = SYSTEM_SECTOR_LENGTH * SECTORS_PER_BLOCK;
file.seek(SeekFrom::Start((lba * length) as u64))?;
let mut data = vec![0u8; length];
file.read_exact(&mut data)?;
Ok(data)
}
fn verify_sys_lba(data: &[u8], sector_length: usize) -> bool {
let sys_data = &data[0..sector_length];
for sector in 1..SECTORS_PER_BLOCK {
let offset = sector * sector_length;
let verify_data = &data[offset..(offset + sector_length)];
if sys_data != verify_data {
return false;
}
}
true
}

View File

@ -1,13 +1,13 @@
mod debug;
mod disk;
mod n64;
pub mod sc64;
mod sc64;
use chrono::Local;
use clap::{Args, Parser, Subcommand, ValueEnum};
use clap_num::maybe_hex_range;
use colored::Colorize;
use panic_message::panic_message;
use sc64::ServerEvent;
use std::{
fs::File,
io::{stdin, stdout, Read, Write},
@ -27,7 +27,7 @@ struct Cli {
command: Commands,
/// Connect to SC64 device on provided serial port
#[arg(short, long, conflicts_with = "remote")]
#[arg(short, long)]
port: Option<String>,
/// Connect to SC64 device on provided remote address
@ -49,7 +49,7 @@ enum Commands {
command: DownloadCommands,
},
/// Upload ROM, 64DD IPL and run disk server
/// Upload ROM (and save), 64DD IPL then run disk server
_64DD(_64DDArgs),
/// Enter debug mode
@ -58,7 +58,7 @@ enum Commands {
/// Dump data from arbitrary location in SC64 memory space
Dump(DumpArgs),
/// Print information about SC64 device
/// Print information about connected SC64 device
Info,
/// Update persistent settings on SC64 device
@ -98,8 +98,8 @@ struct UploadArgs {
#[arg(short, long)]
no_shadow: bool,
/// Force TV type (ignored when used in conjunction with direct boot mode)
#[arg(long)]
/// Force TV type
#[arg(long, conflicts_with = "direct")]
tv: Option<TvType>,
}
@ -117,19 +117,32 @@ struct DownloadArgs {
#[derive(Args)]
struct _64DDArgs {
/// Path to the ROM file
#[arg(short, long)]
rom: Option<PathBuf>,
/// Path to the 64DD IPL file
ddipl: PathBuf,
/// Path to the 64DD disk file (.ndd format, can be specified multiple times)
#[arg(required = true)]
disk: Vec<PathBuf>,
/// Path to the ROM file
#[arg(short, long)]
rom: Option<PathBuf>,
/// Path to the save file
#[arg(short, long, requires = "rom")]
save: Option<PathBuf>,
/// Override autodetected save type
#[arg(short = 't', long, requires = "rom")]
save_type: Option<SaveType>,
/// Use direct boot mode (skip bootloader)
#[arg(short, long)]
direct: bool,
/// Force TV type
#[arg(long, conflicts_with = "direct")]
tv: Option<TvType>,
}
#[derive(Args)]
@ -308,10 +321,10 @@ fn handle_list_command() -> Result<(), sc64::Error> {
fn handle_upload_command(connection: Connection, args: &UploadArgs) -> Result<(), sc64::Error> {
let mut sc64 = init_sc64(connection, true)?;
let (mut rom_file, rom_name, rom_length) = open_file(&args.rom)?;
sc64.reset_state()?;
let (mut rom_file, rom_name, rom_length) = open_file(&args.rom)?;
log_wait(format!("Uploading ROM [{rom_name}]"), || {
sc64.upload_rom(&mut rom_file, rom_length, args.no_shadow)
})?;
@ -325,7 +338,6 @@ fn handle_upload_command(connection: Connection, args: &UploadArgs) -> Result<()
};
save_type.into()
};
let save_type: sc64::SaveType = save.into();
println!("Save type set to [{save_type}]");
sc64.set_save_type(save_type)?;
@ -347,14 +359,10 @@ fn handle_upload_command(connection: Connection, args: &UploadArgs) -> Result<()
sc64.set_boot_mode(boot_mode)?;
if let Some(tv) = args.tv.clone() {
if args.direct {
println!("TV type ignored due to direct boot mode being enabled");
} else {
let tv_type: sc64::TvType = tv.into();
println!("TV type set to [{tv_type}]");
sc64.set_tv_type(tv_type)?;
}
}
sc64.calculate_cic_parameters()?;
@ -381,26 +389,179 @@ fn handle_download_command(
}
fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), sc64::Error> {
let _ = (connection, args);
const MAX_ROM_LENGTH: usize = 32 * 1024 * 1024;
// TODO: handle 64DD stuff
let mut sc64 = init_sc64(connection, true)?;
// TODO: print BIG warning to not use this mode together with real 64DD
println!(
"{} {} {}",
"[WARNING]:".bold().bright_yellow(),
"Do not use this mode when real 64DD accessory is connected to the N64.".bright_yellow(),
"This might permanently damage either 64DD or SC64.".bright_yellow()
);
println!("{}", "Sorry nothing".yellow());
println!("{}", "64DD emulation not implemented yet".red());
sc64.reset_state()?;
if let Some(rom) = &args.rom {
let (mut rom_file, rom_name, rom_length) = open_file(rom)?;
if rom_length > MAX_ROM_LENGTH {
return Err(sc64::Error::new("ROM file size too big for 64DD mode"));
}
log_wait(format!("Uploading ROM [{rom_name}]"), || {
sc64.upload_rom(&mut rom_file, rom_length, false)
})?;
let save: SaveType = if let Some(save_type) = args.save_type.clone() {
save_type
} else {
let (save_type, title) = n64::guess_save_type(&mut rom_file)?;
if let Some(title) = title {
println!("ROM title: {title}");
};
save_type.into()
};
let save_type: sc64::SaveType = save.into();
println!("Save type set to [{save_type}]");
sc64.set_save_type(save_type)?;
if args.save.is_some() {
let (mut save_file, save_name, save_length) = open_file(&args.save.as_ref().unwrap())?;
log_wait(format!("Uploading save [{save_name}]"), || {
sc64.upload_save(&mut save_file, save_length)
})?;
}
}
let (mut ddipl_file, ddipl_name, ddipl_length) = open_file(&args.ddipl)?;
log_wait(format!("Uploading DDIPL [{ddipl_name}]"), || {
sc64.upload_ddipl(&mut ddipl_file, ddipl_length)
})?;
let boot_mode = if args.rom.is_some() {
if args.direct {
sc64::BootMode::DirectRom
} else {
sc64::BootMode::Rom
}
} else {
if args.direct {
sc64::BootMode::DirectDdIpl
} else {
sc64::BootMode::DdIpl
}
};
println!("Boot mode set to [{boot_mode}]");
sc64.set_boot_mode(boot_mode)?;
if let Some(tv) = args.tv.clone() {
let tv_type: sc64::TvType = tv.into();
println!("TV type set to [{tv_type}]");
sc64.set_tv_type(tv_type)?;
}
sc64.calculate_cic_parameters()?;
let disk_paths: Vec<String> = args
.disk
.iter()
.map(|path| path.to_string_lossy().to_string())
.collect();
let disk_names: Vec<String> = args
.disk
.iter()
.map(|path| path.file_name().unwrap().to_string_lossy().to_string())
.collect();
let mut disks = disk::open_multiple(&disk_paths)?;
let mut selected_disk_index: usize = 0;
let mut selected_disk: Option<&mut disk::Disk> = None;
let drive_type = match disks[0].get_format() {
disk::Format::Retail => sc64::DdDriveType::Retail,
disk::Format::Development => sc64::DdDriveType::Development,
};
sc64.configure_64dd(sc64::DdMode::Full, drive_type)?;
println!(
"{}",
"Press button on the SC64 device to cycle through provided disks".bold()
);
let exit = setup_exit_flag();
while !exit.load(Ordering::Relaxed) {
if let Some(data_packet) = sc64.receive_data_packet()? {
match data_packet {
sc64::DataPacket::Disk(mut packet) => {
let track = packet.info.track;
let head = packet.info.head;
let block = packet.info.block;
if let Some(ref mut disk) = selected_disk {
let reply_packet = match packet.kind {
sc64::DiskPacketKind::Read => {
print!("[R]");
disk.read_block(track, head, block)?.map(|data| {
packet.info.set_data(&data);
packet
})
}
sc64::DiskPacketKind::Write => {
print!("[W]");
let data = &packet.info.data;
disk.write_block(track, head, block, data)?.map(|_| packet)
}
};
let lba = if let Some(lba) = disk.get_lba(track, head, block) {
format!("{lba}")
} else {
"Invalid".to_string()
};
let message = format!(" {track:4}:{head}:{block} / LBA: {lba}");
if reply_packet.is_some() {
println!("{}", message.green());
} else {
println!("{}", message.red());
}
sc64.reply_disk_packet(reply_packet)?;
} else {
sc64.reply_disk_packet(None)?;
}
}
sc64::DataPacket::Button => {
if selected_disk.is_some() {
sc64.set_64dd_disk_state(sc64::DdDiskState::Ejected)?;
selected_disk = None;
println!("64DD disk ejected [{}]", disk_names[selected_disk_index]);
} else {
selected_disk_index += 1;
if selected_disk_index >= disks.len() {
selected_disk_index = 0;
}
selected_disk = Some(&mut disks[selected_disk_index]);
sc64.set_64dd_disk_state(sc64::DdDiskState::Inserted)?;
println!("64DD disk inserted [{}]", disk_names[selected_disk_index]);
}
}
_ => {}
}
} else {
thread::sleep(Duration::from_millis(1));
}
}
Ok(())
}
fn handle_debug_command(connection: Connection, args: &DebugArgs) -> Result<(), sc64::Error> {
let mut sc64 = init_sc64(connection, true)?;
let mut debug_handler = debug::new(args.gdb)?;
if let Some(port) = args.gdb {
println!("GDB TCP socket listening at [0.0.0.0:{port}]");
}
let mut sc64 = init_sc64(connection, true)?;
if args.isv.is_some() {
sc64.configure_is_viewer_64(args.isv)?;
println!(
@ -600,16 +761,16 @@ fn handle_server_command(connection: Connection, args: &ServerArgs) -> Result<()
};
sc64::run_server(port, args.address.clone(), |event| match event {
ServerEvent::Listening(address) => {
sc64::ServerEvent::Listening(address) => {
println!("{}: Listening on address [{}]", "[Server]".bold(), address)
}
ServerEvent::Connection(peer) => {
sc64::ServerEvent::Connection(peer) => {
println!("{}: New connection from [{}]", "[Server]".bold(), peer);
}
ServerEvent::Disconnected(peer) => {
sc64::ServerEvent::Disconnected(peer) => {
println!("{}: Client disconnected [{}]", "[Server]".bold(), peer);
}
ServerEvent::Err(error) => {
sc64::ServerEvent::Err(error) => {
println!(
"{}: Client disconnected with error: {}",
"[Server]".bold(),

View File

@ -10,7 +10,8 @@ pub use self::{
link::{list_local_devices, ServerEvent},
types::{
BootMode, ButtonMode, ButtonState, CicSeed, DataPacket, DdDiskState, DdDriveType, DdMode,
DebugPacket, DiskPacket, FpgaDebugData, McuStackUsage, SaveType, Switch, TvType,
DebugPacket, DiskPacket, DiskPacketKind, FpgaDebugData, McuStackUsage, SaveType, Switch,
TvType,
},
};
@ -550,6 +551,7 @@ impl SC64 {
self.command_config_set(Config::DdSdEnable(Switch::Off))?;
self.command_config_set(Config::DdDriveType(drive_type))?;
self.command_config_set(Config::DdDiskState(DdDiskState::Ejected))?;
self.command_config_set(Config::ButtonMode(ButtonMode::UsbPacket))?;
Ok(())
}
@ -587,11 +589,11 @@ impl SC64 {
pub fn reply_disk_packet(&mut self, disk_packet: Option<DiskPacket>) -> Result<(), Error> {
if let Some(packet) = disk_packet {
match packet {
DiskPacket::ReadBlock(disk_block) => {
self.command_memory_write(disk_block.address, &disk_block.data)?;
match packet.kind {
DiskPacketKind::Read => {
self.command_memory_write(packet.info.address, &packet.info.data)?;
}
DiskPacket::WriteBlock(_) => {}
DiskPacketKind::Write => {}
}
self.command_dd_set_block_ready(false)?;
} else {

View File

@ -628,9 +628,14 @@ impl TryFrom<Vec<u8>> for DebugPacket {
}
}
pub enum DiskPacket {
ReadBlock(DiskBlock),
WriteBlock(DiskBlock),
pub enum DiskPacketKind {
Read,
Write,
}
pub struct DiskPacket {
pub kind: DiskPacketKind,
pub info: DiskBlock,
}
impl TryFrom<Vec<u8>> for DiskPacket {
@ -650,8 +655,14 @@ impl TryFrom<Vec<u8>> for DiskPacket {
data: value[12..].to_vec(),
};
Ok(match command {
1 => Self::ReadBlock(disk_block),
2 => Self::WriteBlock(disk_block),
1 => DiskPacket {
kind: DiskPacketKind::Read,
info: disk_block,
},
2 => DiskPacket {
kind: DiskPacketKind::Write,
info: disk_block,
},
_ => return Err(Error::new("Unknown disk packet command code")),
})
}

1
sw/tools/.gitignore vendored
View File

@ -1 +0,0 @@
*.bin