ST018, a coprocessor with a pre-programmed 21 MHz ARMv3 CPU (probably an ARM6)
Used by only one game, Hayazashi Nidan Morita Shougi 2
ST018 is emulated using an ARM7TDMI implementation (ARMv4T) which is almost fully backwards compatible with ARMv3. ARMv4 removed support for the legacy 26-bit addressing mode, but HNMS2 does not use that functionality.
Memory access timings are not emulated because they are not known. All memory accesses are assumed to take 1 clock cycle which is probably not realistic.
18const PROGRAM_ROM_LEN_WORDS: usize = 128 * 1024 / 4; 19const DATA_ROM_LEN: usize = 32 * 1024; 20const RAM_LEN_WORDS: usize = 16 * 1024 / 4; 21 22const TOTAL_ROM_LEN: usize = 4 * PROGRAM_ROM_LEN_WORDS + DATA_ROM_LEN; 23 24type ProgramRom = [u32; PROGRAM_ROM_LEN_WORDS]; 25type DataRom = [u8; DATA_ROM_LEN]; 26type Ram = [u32; RAM_LEN_WORDS]; 27 28#[derive(Debug, Clone, Encode, Decode)] 29struct Registers { 30 snes_to_arm_data: u8, 31 snes_to_arm_data_ready: bool, 32 arm_to_snes_data: u8, 33 arm_to_snes_data_ready: bool, 34 arm_to_snes_flag: bool, 35 arm_reset: bool, 36} 37 38impl Registers { 39 fn new() -> Self { 40 Self { 41 snes_to_arm_data: 0, 42 snes_to_arm_data_ready: false, 43 arm_to_snes_data: 0, 44 arm_to_snes_data_ready: false, 45 arm_to_snes_flag: false, 46 arm_reset: true, 47 } 48 } 49 50 fn arm_read(&mut self, address: u32) -> Option<u8> { 51 match address & 0xFF { 52 0x10 => { 53 // SNES-to-ARM data 54 self.snes_to_arm_data_ready = false; 55 Some(self.snes_to_arm_data) 56 } 57 0x20 => Some(self.read_status()), 58 _ => { 59 log::error!("Invalid ARM register read {address:08X}"); 60 None 61 } 62 } 63 } 64 65 fn arm_write(&mut self, address: u32, value: u8) { 66 log::trace!("ARM register write {address:08X} {value:02X}"); 67 68 match address & 0xFF { 69 0x00 => { 70 // ARM-to-SNES data 71 self.arm_to_snes_data = value; 72 self.arm_to_snes_data_ready = true; 73 } 74 0x10 => { 75 // ARM-to-SNES flag; writing any value sets the flag 76 self.arm_to_snes_flag = true; 77 } 78 0x20..=0x2F => { 79 // fullsnes says these are "config" registers; unclear what (if anything) they actually do 80 // HNMS2 writes to these very frequently but doesn't seem to depend on them doing anything 81 } 82 _ => { 83 log::error!("Invalid ARM register write {address:08X} {value:02X}"); 84 } 85 } 86 } 87 88 fn snes_read(&mut self, address: u32) -> Option<u8> { 89 match address & 0xFFFF { 90 0x3800 => { 91 // ARM-to-SNES data 92 self.arm_to_snes_data_ready = false; 93 Some(self.arm_to_snes_data) 94 } 95 0x3802 => { 96 // Clear ARM-to-SNES flag; read value is undefined 97 self.arm_to_snes_flag = false; 98 None 99 } 100 0x3804 => Some(self.read_status()), 101 _ => { 102 log::error!("Invalid SNES register read {address:06X}"); 103 None 104 } 105 } 106 } 107 108 fn snes_write(&mut self, address: u32, value: u8) { 109 log::trace!("SNES register write {address:06X} {value:02X}"); 110 111 match address & 0xFFFF { 112 0x3802 => { 113 // SNES-to-ARM data 114 self.snes_to_arm_data = value; 115 self.snes_to_arm_data_ready = true; 116 } 117 0x3804 => { 118 // Reset ARM CPU 119 self.arm_reset = value.bit(0); 120 } 121 _ => { 122 log::error!("Invalid SNES register write {address:06X} {value:02X}"); 123 } 124 } 125 } 126 127 fn read_status(&self) -> u8 { 128 // Reset finished (bit 7) and ??? (bit 6) hardcoded to 1 129 u8::from(self.arm_to_snes_data_ready) 130 | (u8::from(self.arm_to_snes_flag) << 2) 131 | (u8::from(self.snes_to_arm_data_ready) << 3) 132 | (1 << 6) 133 | (1 << 7) 134 } 135} 136 137#[derive(Debug, Clone, Encode, Decode)] 138struct ArmBus { 139 program_rom: Box<ProgramRom>, 140 data_rom: Box<DataRom>, 141 ram: Box<Ram>, 142 registers: Registers, 143 cycles: u64, 144 open_bus: u32, 145} 146 147macro_rules! invalid_size { 148 ($size:expr) => { 149 panic!("Invalid size, must be 0-2: {}", $size) 150 }; 151}
All reads from $60000000-$7FFFFFFF return this value according to: https://forums.bannister.org/ubbthreads.php?ubb=showflat&Number=77760&page=all Not sure if the game depends on the value read, but it does occasionally read from these addresses
156const ADDRESS_60_READS: u32 = 0x40404001;
158impl ArmBus { 159 fn read_open_bus<const SIZE: u8>(&self, address: u32) -> u32 { 160 match SIZE { 161 OpSize::BYTE => self.open_bus.to_le_bytes()[(address & 3) as usize].into(), 162 OpSize::WORD => self.open_bus, 163 _ => invalid_size!(SIZE), 164 } 165 } 166 167 fn update_open_bus<const SIZE: u8>(&mut self, value: u32) { 168 // TODO this is probably not right for 8-bit open bus 169 match SIZE { 170 OpSize::BYTE => { 171 self.open_bus = u32::from_ne_bytes([value as u8; 4]); 172 } 173 OpSize::WORD => { 174 self.open_bus = value; 175 } 176 _ => invalid_size!(SIZE), 177 } 178 } 179} 180 181impl BusInterface for ArmBus { 182 #[inline] 183 fn read<const SIZE: u8>(&mut self, address: u32, _cycle: MemoryCycle) -> u32 { 184 self.cycles += 1; 185 186 if SIZE == OpSize::HALFWORD { 187 log::error!("ST018 has an ARMv3 CPU; does not support halfword reads"); 188 return 0; 189 } 190 191 let value = match address { 192 0x00000000..=0x1FFFFFFF => { 193 // Program ROM 194 let rom_addr = ((address >> 2) as usize) & (PROGRAM_ROM_LEN_WORDS - 1); 195 let word = self.program_rom[rom_addr]; 196 match SIZE { 197 OpSize::BYTE => word.to_le_bytes()[(address & 3) as usize].into(), 198 OpSize::WORD => word, 199 _ => invalid_size!(SIZE), 200 } 201 } 202 0x40000000..=0x5FFFFFFF => { 203 // I/O registers 204 let Some(byte) = self.registers.arm_read(address) else { 205 return self.read_open_bus::<SIZE>(address); 206 }; 207 byte.into() 208 } 209 0x60000000..=0x7FFFFFFF => match SIZE { 210 OpSize::BYTE => ADDRESS_60_READS.to_le_bytes()[(address & 3) as usize].into(), 211 OpSize::WORD => ADDRESS_60_READS, 212 _ => invalid_size!(SIZE), 213 }, 214 0xA0000000..=0xBFFFFFFF => { 215 // Data ROM; only has an 8-bit data bus 216 // TODO 32-bit reads are probably not accurate; this code path is not exercised 217 let rom_addr = (address as usize) & (DATA_ROM_LEN - 1); 218 let byte = self.data_rom[rom_addr]; 219 match SIZE { 220 OpSize::BYTE => byte.into(), 221 OpSize::WORD => u32::from_ne_bytes([byte; 4]), 222 _ => invalid_size!(SIZE), 223 } 224 } 225 0xE0000000..=0xFFFFFFFF => { 226 // RAM 227 let ram_addr = ((address >> 2) as usize) & (RAM_LEN_WORDS - 1); 228 let word = self.ram[ram_addr]; 229 match SIZE { 230 OpSize::BYTE => word.to_le_bytes()[(address & 3) as usize].into(), 231 OpSize::WORD => word, 232 _ => invalid_size!(SIZE), 233 } 234 } 235 _ => return self.read_open_bus::<SIZE>(address), 236 }; 237 238 self.update_open_bus::<SIZE>(value); 239 240 value 241 } 242 243 #[inline] 244 fn write<const SIZE: u8>(&mut self, address: u32, value: u32, _cycle: MemoryCycle) { 245 self.cycles += 1; 246 247 if SIZE == OpSize::HALFWORD { 248 log::error!("ST018 has an ARMv3 CPU; does not support halfword writes"); 249 return; 250 } 251 252 self.update_open_bus::<SIZE>(value); 253 254 match address { 255 0x40000000..=0x5FFFFFFF => { 256 // I/O registers 257 self.registers.arm_write(address, value as u8); 258 } 259 0xE0000000..=0xFFFFFFFF => { 260 // RAM 261 let ram_addr = ((address >> 2) as usize) & (RAM_LEN_WORDS - 1); 262 match SIZE { 263 OpSize::BYTE => { 264 let mut bytes = self.ram[ram_addr].to_le_bytes(); 265 bytes[(address & 3) as usize] = value as u8; 266 self.ram[ram_addr] = u32::from_le_bytes(bytes); 267 } 268 OpSize::WORD => { 269 self.ram[ram_addr] = value; 270 } 271 _ => invalid_size!(SIZE), 272 } 273 } 274 _ => {} 275 } 276 } 277 278 #[inline] 279 fn irq(&self) -> bool { 280 false 281 } 282 283 #[inline] 284 fn internal_cycles(&mut self, cycles: u32) { 285 self.cycles += u64::from(cycles); 286 } 287} 288 289#[derive(Debug, Error)] 290pub enum St018LoadError { 291 #[error("Expected ROM size of {expected} bytes, was {actual} bytes")] 292 IncorrectRomSize { expected: usize, actual: usize }, 293} 294 295#[derive(Debug, Clone, Encode, Decode)] 296pub struct St018 { 297 cpu: Arm7Tdmi<ArmBus>, 298 bus: ArmBus, 299 snes_cycles: u64, 300} 301 302impl St018 {
Errors
Returns an error if the ST018 program/data ROM is invalid.
306 #[allow(clippy::missing_panics_doc)] 307 pub fn new(st018_rom: &[u8]) -> Result<Self, St018LoadError> { 308 let (program_rom, data_rom) = convert_st018_rom(st018_rom)?; 309 310 let bus = ArmBus { 311 program_rom, 312 data_rom, 313 ram: vec![0; RAM_LEN_WORDS].into_boxed_slice().try_into().unwrap(), 314 registers: Registers::new(), 315 cycles: 0, 316 open_bus: 0, 317 }; 318 319 Ok(Self { cpu: Arm7Tdmi::new(), bus, snes_cycles: 0 }) 320 }
322 pub fn tick(&mut self, snes_master_cycles: u64) { 323 // ST018 has its own 21 MHz oscillator, but it runs at almost the exact same frequency as 324 // the SNES master oscillator, so just assume they're the same speed 325 self.snes_cycles += snes_master_cycles; 326 327 if self.bus.registers.arm_reset { 328 self.bus.registers.arm_reset = false; 329 self.cpu.reset(&mut self.bus); 330 } 331 332 while self.bus.cycles < self.snes_cycles { 333 self.cpu.execute_instruction(&mut self.bus); 334 } 335 } 336 337 pub fn snes_read(&mut self, address: u32) -> Option<u8> { 338 self.bus.registers.snes_read(address) 339 } 340 341 pub fn snes_write(&mut self, address: u32, value: u8) { 342 self.bus.registers.snes_write(address, value); 343 } 344} 345 346fn convert_st018_rom(rom: &[u8]) -> Result<(Box<ProgramRom>, Box<DataRom>), St018LoadError> { 347 if rom.len() < TOTAL_ROM_LEN { 348 return Err(St018LoadError::IncorrectRomSize { 349 expected: TOTAL_ROM_LEN, 350 actual: rom.len(), 351 }); 352 } 353 354 let program_rom: Vec<_> = rom[..4 * PROGRAM_ROM_LEN_WORDS] 355 .as_chunks::<4>() 356 .0 357 .iter() 358 .map(|&chunk| u32::from_le_bytes(chunk)) 359 .collect(); 360 let program_rom: Box<ProgramRom> = program_rom.into_boxed_slice().try_into().unwrap(); 361 362 let data_rom = 363 rom[4 * PROGRAM_ROM_LEN_WORDS..4 * PROGRAM_ROM_LEN_WORDS + DATA_ROM_LEN].to_vec(); 364 let data_rom: Box<DataRom> = data_rom.into_boxed_slice().try_into().unwrap(); 365 366 Ok((program_rom, data_rom)) 367}