diff --git a/sw/controller/src/timer.h b/sw/controller/src/timer.h index e9f480d..916b524 100644 --- a/sw/controller/src/timer.h +++ b/sw/controller/src/timer.h @@ -6,6 +6,7 @@ typedef enum { + TIMER_ID_USB, TIMER_ID_WRITEBACK, __TIMER_ID_COUNT } timer_id_t; diff --git a/sw/controller/src/usb.c b/sw/controller/src/usb.c index e301056..6e2dd97 100644 --- a/sw/controller/src/usb.c +++ b/sw/controller/src/usb.c @@ -5,19 +5,22 @@ #include "flash.h" #include "fpga.h" #include "rtc.h" +#include "timer.h" #include "update.h" #include "usb.h" #include "version.h" #include "writeback.h" -#define BOOTLOADER_ADDRESS (0x04E00000UL) -#define BOOTLOADER_LENGTH (1920 * 1024) +#define BOOTLOADER_ADDRESS (0x04E00000UL) +#define BOOTLOADER_LENGTH (1920 * 1024) -#define MEMORY_LENGTH (0x05002980UL) +#define MEMORY_LENGTH (0x05002980UL) -#define RX_FLUSH_ADDRESS (0x07F00000UL) -#define RX_FLUSH_LENGTH (1 * 1024 * 1024) +#define RX_FLUSH_ADDRESS (0x07F00000UL) +#define RX_FLUSH_LENGTH (1 * 1024 * 1024) + +#define DEBUG_WRITE_TIMEOUT_TICKS (100) enum rx_state { @@ -49,6 +52,9 @@ struct process { uint32_t tx_token; bool tx_dma_running; + bool flush_response; + bool flush_packet; + bool response_pending; bool response_error; usb_tx_info_t response_info; @@ -161,11 +167,16 @@ static void usb_rx_process (void) { p.rx_state = RX_STATE_ARGS; p.rx_counter = 0; p.rx_dma_running = false; + p.flush_response = false; + p.flush_packet = false; p.response_error = false; p.response_info.cmd = p.rx_cmd; p.response_info.data_length = 0; p.response_info.dma_length = 0; p.response_info.done_callback = NULL; + if (p.rx_cmd == 'U') { + timer_set(TIMER_ID_USB, DEBUG_WRITE_TIMEOUT_TICKS); + } } } @@ -268,6 +279,7 @@ static void usb_rx_process (void) { if (!p.rx_dma_running) { if (usb_validate_address_length(p.rx_args[0], p.rx_args[1], true)) { p.rx_state = RX_STATE_FLUSH; + p.flush_response = true; } else { fpga_reg_set(REG_USB_DMA_ADDRESS, p.rx_args[0]); fpga_reg_set(REG_USB_DMA_LENGTH, p.rx_args[1]); @@ -284,20 +296,26 @@ static void usb_rx_process (void) { case 'U': if (p.rx_args[1] == 0) { p.rx_state = RX_STATE_IDLE; - } else if ((p.read_length > 0) && usb_dma_ready()) { - uint32_t length = (p.read_length > p.rx_args[1]) ? p.rx_args[1] : p.read_length; - if (!p.rx_dma_running) { - fpga_reg_set(REG_USB_DMA_ADDRESS, p.read_address); - fpga_reg_set(REG_USB_DMA_LENGTH, length); - fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START); - p.rx_dma_running = true; - p.read_ready = false; - } else { - p.rx_args[1] -= length; - p.rx_dma_running = false; - p.read_length -= length; - p.read_address += length; - p.read_ready = true; + } else if (usb_dma_ready()) { + if (p.read_length > 0) { + uint32_t length = (p.read_length > p.rx_args[1]) ? p.rx_args[1] : p.read_length; + if (!p.rx_dma_running) { + fpga_reg_set(REG_USB_DMA_ADDRESS, p.read_address); + fpga_reg_set(REG_USB_DMA_LENGTH, length); + fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START); + p.rx_dma_running = true; + p.read_ready = false; + } else { + p.rx_args[1] -= length; + p.rx_dma_running = false; + p.read_length -= length; + p.read_address += length; + p.read_ready = true; + timer_set(TIMER_ID_USB, DEBUG_WRITE_TIMEOUT_TICKS); + } + } else if (timer_get(TIMER_ID_USB) == 0) { + p.rx_state = RX_STATE_FLUSH; + p.flush_packet = true; } } break; @@ -390,9 +408,19 @@ static void usb_rx_process (void) { fpga_reg_set(REG_USB_DMA_SCR, DMA_SCR_DIRECTION | DMA_SCR_START); p.rx_args[1] -= length; } else { - p.rx_state = RX_STATE_IDLE; - p.response_pending = true; - p.response_error = true; + if (p.flush_response) { + p.rx_state = RX_STATE_IDLE; + p.response_pending = true; + p.response_error = true; + } else if (p.flush_packet) { + usb_tx_info_t packet_info; + usb_create_packet(&packet_info, PACKET_CMD_DATA_FLUSHED); + if (usb_enqueue_packet(&packet_info)) { + p.rx_state = RX_STATE_IDLE; + } + } else { + p.rx_state = RX_STATE_IDLE; + } } } } diff --git a/sw/controller/src/usb.h b/sw/controller/src/usb.h index 0819a46..beafc04 100644 --- a/sw/controller/src/usb.h +++ b/sw/controller/src/usb.h @@ -8,8 +8,9 @@ typedef enum packet_cmd { PACKET_CMD_BUTTON_TRIGGER = 'B', - PACKET_CMD_DD_REQUEST = 'D', + PACKET_CMD_DATA_FLUSHED = 'G', PACKET_CMD_DEBUG_OUTPUT = 'U', + PACKET_CMD_DD_REQUEST = 'D', PACKET_CMD_ISV_OUTPUT = 'I', PACKET_CMD_SAVE_WRITEBACK = 'S', PACKET_CMD_UPDATE_STATUS = 'F', diff --git a/sw/deployer/src/debug.rs b/sw/deployer/src/debug.rs index 2e440c5..3aacc91 100644 --- a/sw/deployer/src/debug.rs +++ b/sw/deployer/src/debug.rs @@ -26,6 +26,7 @@ enum DataType { RawBinary, Header, Screenshot, + Heartbeat, Unknown, } @@ -36,6 +37,7 @@ impl From for DataType { 0x02 => Self::RawBinary, 0x03 => Self::Header, 0x04 => Self::Screenshot, + 0x05 => Self::Heartbeat, _ => Self::Unknown, } } @@ -48,6 +50,7 @@ impl From for u8 { DataType::RawBinary => 0x02, DataType::Header => 0x03, DataType::Screenshot => 0x04, + DataType::Heartbeat => 0x05, DataType::Unknown => 0xFF, } } @@ -114,6 +117,26 @@ impl TryFrom> for ScreenshotMetadata { } } +struct Heartbeat { + usb_protocol: u16, + version: u16, +} + +impl TryFrom<&[u8]> for Heartbeat { + type Error = String; + fn try_from(value: &[u8]) -> Result { + if value.len() < 4 { + return Err("Invalid heartbeat data length".into()); + } + let usb_protocol = u16::from_be_bytes(value[0..2].try_into().unwrap()); + let version = u16::from_be_bytes(value[2..4].try_into().unwrap()); + Ok(Heartbeat { + usb_protocol, + version, + }) + } +} + macro_rules! success { ($($a: tt)*) => { println!("{}", format!($($a)*).bright_blue()); @@ -134,6 +157,7 @@ macro_rules! stop { } const MAX_PACKET_LENGTH: usize = 8 * 1024 * 1024; +const SUPPORTED_USB_PROTOCOL_VERSION: u16 = 2; impl Handler { pub fn set_text_encoding(&mut self, encoding: Encoding) { @@ -195,6 +219,7 @@ impl Handler { } } } + data.append(&mut b"\0".to_vec()); sc64::DebugPacket { datatype: DataType::Text.into(), data, @@ -220,6 +245,7 @@ impl Handler { DataType::RawBinary => self.handle_datatype_raw_binary(&data), DataType::Header => self.handle_datatype_header(&data), DataType::Screenshot => self.handle_datatype_screenshot(&data), + DataType::Heartbeat => self.handle_datatype_heartbeat(&data), _ => error!("Received unknown debug packet datatype: 0x{datatype:02X}"), } } @@ -252,13 +278,18 @@ impl Handler { Ok(mut file) => { if let Err(error) = file.write_all(&save_writeback.data) { error!("Couldn't write save [{filename}]: {error}"); + } else { + success!("Wrote [{}] save to [{filename}]", save_writeback.save); } - success!("Wrote [{}] save to [{filename}]", save_writeback.save); } Err(error) => error!("Couldn't create save writeback file [{filename}]: {error}"), } } + pub fn handle_data_flushed(&self) { + error!("Debug data write dropped due to timeout"); + } + fn handle_datatype_text(&self, data: &[u8]) { self.print_text(data); } @@ -269,8 +300,9 @@ impl Handler { Ok(mut file) => { if let Err(error) = file.write_all(data) { error!("Couldn't write raw binary [{filename}]: {error}"); + } else { + success!("Wrote [{}] bytes to [{filename}]", data.len()); } - success!("Wrote [{}] bytes to [{filename}]", data.len()); } Err(error) => error!("Couldn't create raw binary file [{filename}]: {error}"), } @@ -319,6 +351,23 @@ impl Handler { success!("Wrote {width}x{height} pixels to [{filename}]"); } + fn handle_datatype_heartbeat(&mut self, data: &[u8]) { + let Heartbeat { + usb_protocol, + version, + } = match data.try_into() { + Ok(heartbeat) => heartbeat, + Err(error) => return error!("Error while parsing heartbeat datatype: {error}"), + }; + if usb_protocol > SUPPORTED_USB_PROTOCOL_VERSION { + return error!("Unsupported USB protocol version: {usb_protocol}"); + } + match version { + 1 => {} + _ => return error!("Unsupported USB heartbeat version: {version}"), + } + } + fn print_text(&self, data: &[u8]) { match self.encoding { Encoding::UTF8 => print!("{}", String::from_utf8_lossy(&data)), diff --git a/sw/deployer/src/main.rs b/sw/deployer/src/main.rs index 770c7e3..138b7ef 100644 --- a/sw/deployer/src/main.rs +++ b/sw/deployer/src/main.rs @@ -48,7 +48,7 @@ enum Commands { command: DownloadCommands, }, - /// Upload ROM (and save), 64DD IPL then run disk server + /// Upload ROM (and save), 64DD IPL then run disk/debug server _64DD(_64DDArgs), /// Enter debug mode @@ -127,7 +127,7 @@ struct _64DDArgs { #[arg(short, long)] rom: Option, - /// Path to the save file + /// Path to the save file (also used by save writeback mechanism) #[arg(short, long, requires = "rom")] save: Option, @@ -398,11 +398,13 @@ fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), s let mut sc64 = init_sc64(connection, true)?; + let mut debug_handler = debug::new(); + 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() + "{}\n{}\n{}", + "========== [WARNING] ==========".bold().bright_yellow(), + "Do not use this mode when real 64DD accessory is connected to the N64".bright_yellow(), + "Doing so might permanently damage either N64, 64DD or SC64".bright_yellow() ); sc64.reset_state()?; @@ -478,10 +480,7 @@ fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), s .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, @@ -495,11 +494,22 @@ fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), s println!( "{}: {}", "[64DD]".bold(), - "Press button on the SC64 device to cycle through provided disks" + "Press button on the back of SC64 device to cycle through provided disks" .bold() .bright_green() ); + let mut selected_disk_index: usize = 0; + let mut selected_disk = Some(&mut disks[selected_disk_index]); + println!( + "{}: Disk inserted [{}]", + "[64DD]".bold(), + disk_names[selected_disk_index].bright_green() + ); + sc64.set_64dd_disk_state(sc64::DdDiskState::Inserted)?; + + sc64.set_save_writeback(true)?; + let exit = setup_exit_flag(); while !exit.load(Ordering::Relaxed) { if let Some(data_packet) = sc64.receive_data_packet()? { @@ -554,16 +564,27 @@ fn handle_64dd_command(connection: Connection, args: &_64DDArgs) -> Result<(), s selected_disk_index = 0; } selected_disk = Some(&mut disks[selected_disk_index]); - sc64.set_64dd_disk_state(sc64::DdDiskState::Inserted)?; println!( "{}: Disk inserted [{}]", "[64DD]".bold(), disk_names[selected_disk_index].bright_green() ); + sc64.set_64dd_disk_state(sc64::DdDiskState::Inserted)?; } } + sc64::DataPacket::DebugData(debug_packet) => { + debug_handler.handle_debug_packet(debug_packet); + } + sc64::DataPacket::SaveWriteback(save_writeback) => { + debug_handler.handle_save_writeback(save_writeback, &args.save); + } + sc64::DataPacket::DataFlushed => { + debug_handler.handle_data_flushed(); + } _ => {} } + } else if let Some(debug_packet) = debug_handler.process_user_input() { + sc64.send_debug_packet(debug_packet)?; } } @@ -608,6 +629,9 @@ fn handle_debug_command(connection: Connection, args: &DebugArgs) -> Result<(), sc64::DataPacket::SaveWriteback(save_writeback) => { debug_handler.handle_save_writeback(save_writeback, &args.save); } + sc64::DataPacket::DataFlushed => { + debug_handler.handle_data_flushed(); + } _ => {} } } else if let Some(debug_packet) = debug_handler.process_user_input() { diff --git a/sw/deployer/src/sc64/types.rs b/sw/deployer/src/sc64/types.rs index 3d0275d..0164d61 100644 --- a/sw/deployer/src/sc64/types.rs +++ b/sw/deployer/src/sc64/types.rs @@ -580,6 +580,7 @@ impl From for [u32; 2] { pub enum DataPacket { Button, + DataFlushed, DebugData(DebugPacket), DiskRequest(DiskPacket), IsViewer64(Vec), @@ -592,6 +593,7 @@ impl TryFrom for DataPacket { fn try_from(value: Packet) -> Result { Ok(match value.id { b'B' => Self::Button, + b'G' => Self::DataFlushed, b'U' => Self::DebugData(value.data.try_into()?), b'D' => Self::DiskRequest(value.data.try_into()?), b'I' => Self::IsViewer64(value.data),