HuC-3 mapper, used by Robopon and a few Japan-only games
References: https://gbdev.io/pandocs/HuC3.html https://gbdev.gg8.se/forums/viewtopic.php?id=744
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 15enum RamMapping { 16 // $0 17 #[default] 18 RamReadOnly, 19 // $A 20 RamReadWrite, 21 // $B 22 RtcSendCommand, 23 // $C 24 RtcReceiveResponse, 25 // $D 26 RtcSemaphore, 27 // $E 28 Ir, 29 // Other values 30 OpenBus, 31} 32 33impl RamMapping { 34 fn from_byte(byte: u8) -> Self { 35 match byte & 0xF { 36 0x0 => Self::RamReadOnly, 37 0xA => Self::RamReadWrite, 38 0xB => Self::RtcSendCommand, 39 0xC => Self::RtcReceiveResponse, 40 0xD => Self::RtcSemaphore, 41 0xE => Self::Ir, 42 _ => Self::OpenBus, 43 } 44 } 45} 46 47#[derive(Debug, Clone, Encode, Decode)] 48pub struct Huc3 { 49 rom_bank: u8, 50 rom_addr_mask: u32, 51 ram_bank: u8, 52 ram_addr_mask: u32, 53 ram_mapping: RamMapping, 54 rtc: Huc3Rtc, 55} 56 57impl Huc3 { 58 pub fn new(rom_len: u32, ram_len: u32, rtc: Option<Huc3Rtc>) -> Self { 59 Self { 60 rom_bank: 0, 61 rom_addr_mask: rom_len - 1, 62 ram_bank: 0, 63 ram_addr_mask: ram_len - 1, 64 ram_mapping: RamMapping::default(), 65 rtc: rtc.unwrap_or_else(Huc3Rtc::new), 66 } 67 } 68 69 pub fn map_rom_address(&self, address: u16) -> u32 { 70 basic_map_rom_address(address, self.rom_bank.into(), true, self.rom_addr_mask) 71 } 72 73 pub fn write_rom_address(&mut self, address: u16, value: u8) { 74 match address { 75 0x0000..=0x1FFF => { 76 // RAM mode select 77 self.ram_mapping = RamMapping::from_byte(value); 78 log::trace!("HuC-3 $A000-$BFFF mapping: {:?}", self.ram_mapping); 79 } 80 0x2000..=0x3FFF => { 81 // ROM bank 82 self.rom_bank = value; 83 } 84 0x4000..=0x5FFF => { 85 // RAM bank 86 self.ram_bank = value; 87 } 88 0x6000..=0x7FFF => { 89 // Unknown functionality; ignore 90 } 91 0x8000..=0xFFFF => panic!("Invalid ROM address: {address:04X}"), 92 } 93 } 94 95 pub fn read_ram(&self, address: u16, sram: &[u8]) -> u8 { 96 match self.ram_mapping { 97 RamMapping::RamReadOnly | RamMapping::RamReadWrite => { 98 basic_map_ram_address(true, address, self.ram_bank.into(), self.ram_addr_mask) 99 .map_or(0xFF, |ram_addr| sram[ram_addr as usize]) 100 } 101 RamMapping::RtcReceiveResponse => { 102 let response = 0x80 | (self.rtc.command << 4) | self.rtc.response; 103 log::trace!( 104 "RTC response received ({response:02X}): command {:X} response {:X}", 105 self.rtc.command, 106 self.rtc.response 107 ); 108 response 109 } 110 RamMapping::RtcSemaphore => 0xFE | u8::from(self.rtc.busy_cycles_remaining == 0), 111 RamMapping::Ir => { 112 // Normally $C1 means "saw light" and $C0 means "didn't see light"; always return $C0 113 0xC0 114 } 115 RamMapping::RtcSendCommand | RamMapping::OpenBus => 0xFF, 116 } 117 } 118 119 pub fn write_ram(&mut self, address: u16, value: u8, sram: &mut [u8]) { 120 match self.ram_mapping { 121 RamMapping::RamReadWrite => { 122 if let Some(ram_addr) = 123 basic_map_ram_address(true, address, self.ram_bank.into(), self.ram_addr_mask) 124 { 125 sram[ram_addr as usize] = value; 126 } 127 } 128 RamMapping::RtcSendCommand => { 129 self.rtc.command = (value >> 4) & 0x07; 130 self.rtc.argument = value & 0x0F; 131 log::trace!( 132 "RTC command sent ({value:02X}): command {:X} argument {:X}", 133 self.rtc.command, 134 self.rtc.argument 135 ); 136 } 137 RamMapping::RtcSemaphore => { 138 if !value.bit(0) { 139 self.rtc.execute_command(); 140 log::trace!("Executing RTC command"); 141 } 142 } 143 RamMapping::RamReadOnly 144 | RamMapping::RtcReceiveResponse 145 | RamMapping::Ir 146 | RamMapping::OpenBus => {} 147 } 148 } 149 150 pub fn tick_cpu(&mut self) { 151 self.rtc.busy_cycles_remaining = self.rtc.busy_cycles_remaining.saturating_sub(1); 152 } 153 154 pub fn update_rtc_time(&mut self) { 155 self.rtc.update_time(); 156 } 157 158 pub fn save_rtc_state<S: SaveWriter>(&self, save_writer: &mut S) -> Result<(), S::Err> { 159 save_writer.persist_serialized("rtc", &self.rtc) 160 } 161} 162 163const NANOS_PER_MINUTE: u128 = 60 * 1_000_000_000; 164const MINUTES_PER_DAY: u64 = 1440; 165 166const RTC_RAM_LEN: usize = 256;
Arbitrary value, HuC-3 games supposedly expect a delay instead of immediate command execution
169const RTC_BUSY_CYCLES: u16 = 1000;
171#[derive(Debug, Clone, Encode, Decode)] 172pub struct Huc3Rtc { 173 memory: Box<[u8; RTC_RAM_LEN]>, 174 memory_address: u8, 175 last_update_time_nanos: u128, 176 nanos_of_minute: u128, 177 minutes_of_day: u16, 178 day: u16, 179 command: u8, 180 argument: u8, 181 response: u8, 182 busy_cycles_remaining: u16, 183} 184 185impl Huc3Rtc { 186 fn new() -> Self { 187 let memory = 188 iter::repeat_with(|| rand::random::<u8>() & 0x0F).take(RTC_RAM_LEN).collect::<Vec<_>>(); 189 190 Self { 191 memory: memory.into_boxed_slice().try_into().unwrap(), 192 memory_address: 0, 193 last_update_time_nanos: timeutils::current_time_nanos(), 194 nanos_of_minute: 0, 195 minutes_of_day: rand::random(), 196 day: rand::random(), 197 command: 0, 198 argument: 0, 199 response: 0, 200 busy_cycles_remaining: 0, 201 } 202 } 203 204 fn update_time(&mut self) { 205 let now_nanos = timeutils::current_time_nanos(); 206 let elapsed_nanos = now_nanos.saturating_sub(self.last_update_time_nanos); 207 self.last_update_time_nanos = now_nanos; 208 209 self.nanos_of_minute += elapsed_nanos; 210 211 let elapsed_minutes = self.nanos_of_minute / NANOS_PER_MINUTE; 212 self.nanos_of_minute %= NANOS_PER_MINUTE; 213 214 let new_minutes_of_day = u64::from(self.minutes_of_day) + elapsed_minutes as u64; 215 let elapsed_days = new_minutes_of_day / MINUTES_PER_DAY; 216 self.minutes_of_day = (new_minutes_of_day % MINUTES_PER_DAY) as u16; 217 218 self.day = self.day.wrapping_add(elapsed_days as u16); 219 220 // These memory addresses seem to hold the current time always? 221 self.memory[0x10..0x13].copy_from_slice(&to_nibbles(self.minutes_of_day)); 222 self.memory[0x13..0x16].copy_from_slice(&to_nibbles(self.day)); 223 } 224 225 fn execute_command(&mut self) { 226 self.busy_cycles_remaining = RTC_BUSY_CYCLES; 227 228 match self.command { 229 0x1 => { 230 // Read value and increment address 231 self.response = self.memory[self.memory_address as usize] & 0x0F; 232 self.memory_address = self.memory_address.wrapping_add(1); 233 } 234 0x3 => { 235 // Write value and increment address 236 self.memory[self.memory_address as usize] = self.argument; 237 self.memory_address = self.memory_address.wrapping_add(1); 238 } 239 0x4 => { 240 // Update address low nibble 241 self.memory_address = (self.memory_address & 0xF0) | self.argument; 242 } 243 0x5 => { 244 // Update address high nibble 245 self.memory_address = (self.memory_address & 0x0F) | (self.argument << 4); 246 } 247 0x6 => match self.argument { 248 0x0 => { 249 // Copy current time to $00-$06 in RTC memory 250 self.memory[0..3].copy_from_slice(&to_nibbles(self.minutes_of_day)); 251 self.memory[3..6].copy_from_slice(&to_nibbles(self.day)); 252 } 253 0x1 => { 254 // Copy $00-$06 in RTC memory to current time 255 self.minutes_of_day = from_nibbles(&self.memory[0..3]); 256 self.day = from_nibbles(&self.memory[3..6]); 257 258 log::trace!( 259 "Updated RTC time: minutes={}, days={}", 260 self.minutes_of_day, 261 self.day 262 ); 263 264 // TODO event time? 265 } 266 0x2 => { 267 // Some sort of status command? Games expect it to always respond with 1 268 self.response = 0x01; 269 } 270 0xE => { 271 log::warn!("HuC-3 tone generator is not emulated"); 272 } 273 _ => { 274 log::warn!("Unexpected HuC-3 extended command: {:X}", self.argument); 275 } 276 }, 277 _ => { 278 log::warn!( 279 "Unexpected HuC-3 command: command {:X} argument {:X}", 280 self.command, 281 self.argument 282 ); 283 } 284 } 285 } 286} 287 288fn to_nibbles(value: u16) -> [u8; 3] { 289 [(value & 0x0F) as u8, ((value >> 4) & 0x0F) as u8, ((value >> 8) & 0x0F) as u8] 290} 291 292fn from_nibbles(nibbles: &[u8]) -> u16 { 293 u16::from(nibbles[0]) | (u16::from(nibbles[1]) << 4) | (u16::from(nibbles[2]) << 8) 294}