diff --git a/README.md b/README.md index bce7572..3bc8fc3 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ To enable the experimental FIFO renderer (as opposed to the scanline renderer), - SRAM - EEPROM - Timers run effeciently on the scheduler + - Real-time clock support ### Remaining Work - GB / GBC @@ -108,6 +109,7 @@ A special thanks goes out to those in the emudev community who are always helpfu - https://github.com/DenSinH - https://github.com/fleroviux - https://github.com/destoer +- https://github.com/GhostRain0 ## Contributing diff --git a/src/crab/gba/bus.cr b/src/crab/gba/bus.cr index 0e8a207..6cfbdeb 100644 --- a/src/crab/gba/bus.cr +++ b/src/crab/gba/bus.cr @@ -1,3 +1,5 @@ +require "./gpio" + module GBA class Bus # Timings for rom are estimated for game compatibility. @@ -11,8 +13,11 @@ module GBA getter wram_board = Bytes.new 0x40000 getter wram_chip = Bytes.new 0x08000 + @gpio : GPIO + def initialize(@gba : GBA, bios_path : String) File.open(bios_path) { |file| file.read @bios } + @gpio = GPIO.new(@gba) end def [](index : Int) : Byte @@ -87,11 +92,10 @@ module GBA address -= 0x8000 if address > 0x17FFF @gba.ppu.vram[address] when 0x7 then @gba.ppu.oam[index & 0x3FF] - when 0x8, 0x9, - 0xA, 0xB, - 0xC then @gba.cartridge.rom[index & 0x01FFFFFF] - when 0xD - if @gba.storage.eeprom? index + when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD + if @gpio.address?(index) && @gpio.allow_reads + @gpio[index] + elsif @gba.storage.eeprom?(index) @gba.storage[index] else @gba.cartridge.rom[index & 0x01FFFFFF] @@ -116,11 +120,10 @@ module GBA address -= 0x8000 if address > 0x17FFF (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value - when 0x8, 0x9, - 0xA, 0xB, - 0xC then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value - when 0xD - if @gba.storage.eeprom? index + when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD + if @gpio.address?(index) && @gpio.allow_reads + @gpio[index].to_u16! + elsif @gba.storage.eeprom?(index) @gba.storage[index].to_u16! else (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value @@ -145,11 +148,10 @@ module GBA address -= 0x8000 if address > 0x17FFF (@gba.ppu.vram.to_unsafe + address).as(Word*).value when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value - when 0x8, 0x9, - 0xA, 0xB, - 0xC then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value - when 0xD - if @gba.storage.eeprom? index + when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD + if @gpio.address?(index) && @gpio.allow_reads + @gpio[index].to_u32! + elsif @gba.storage.eeprom?(index) @gba.storage[index].to_u32! else (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value @@ -173,7 +175,13 @@ module GBA address = 0x1FFFE_u32 & index # (u8 write only) halfword-aligned address -= 0x8000 if address > 0x17FFF # todo: determine if this happens before or after the limit check (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = 0x0101_u16 * value if address <= limit - when 0xD then @gba.storage[index] = value if @gba.storage.eeprom? index + when 0x7 # can't write bytes to oam + when 0x8, 0xD # all address between aren't writable + if @gpio.address? index + @gpio[index] = value + elsif @gba.storage.eeprom? index + @gba.storage[index] + end when 0xE, 0xF then @gba.storage[index] = value else log "Unmapped write: #{hex_str index.to_u32}" end @@ -193,8 +201,13 @@ module GBA address = 0x1FFFF_u32 & index address -= 0x8000 if address > 0x17FFF (@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value - when 0xD then @gba.storage[index] = value.to_u8! if @gba.storage.eeprom? index + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value = value + when 0x8, 0xD # all address between aren't writable + if @gpio.address? index + @gpio[index] = value.to_u8! + elsif @gba.storage.eeprom? index + @gba.storage[index] = value.to_u8! + end when 0xE, 0xF then write_half_internal_slow(index, value) else log "Unmapped write: #{hex_str index.to_u32}" end @@ -214,8 +227,13 @@ module GBA address = 0x1FFFF_u32 & index address -= 0x8000 if address > 0x17FFF (@gba.ppu.vram.to_unsafe + address).as(Word*).value = value - when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value = value - when 0xD then @gba.storage[index] = value.to_u8! if @gba.storage.eeprom? index + when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value = value + when 0x8, 0xD # all address between aren't writable + if @gpio.address? index + @gpio[index] = value.to_u8! + elsif @gba.storage.eeprom? index + @gba.storage[index] = value.to_u8! + end when 0xE, 0xF then write_word_internal_slow(index, value) else log "Unmapped write: #{hex_str index.to_u32}" end diff --git a/src/crab/gba/gpio.cr b/src/crab/gba/gpio.cr new file mode 100644 index 0000000..1e6cb4a --- /dev/null +++ b/src/crab/gba/gpio.cr @@ -0,0 +1,46 @@ +require "./rtc" + +module GBA + class GPIO + @data : UInt8 = 0x00 + @direction : UInt8 = 0x00 + getter allow_reads : Bool = false + + def initialize(@gba : GBA) + @rtc = RTC.new(@gba) # todo: support other forms of gpio + end + + def [](io_addr : Int) : Byte + case io_addr & 0xFF + when 0xC4 # IO Port Data + if @allow_reads + (@data & ~@direction) & 0xF_u8 + @rtc.read & 0xF_u8 + else + 0_u8 + end + when 0xC6 # IO Port Direction + @direction & 0xF_u8 + when 0xC8 # IO Port Control + @allow_reads ? 1_u8 : 0_u8 + else 0_u8 + end + end + + def []=(io_addr : Int, value : Byte) : Nil + case io_addr & 0xFF + when 0xC4 # IO Port Data + @data &= value & 0xF_u8 + @rtc.write(value & 0xF_u8) + when 0xC6 # IO Port Direction + @direction = value & 0x0F + when 0xC8 # IO Port Control + @allow_reads = bit?(value, 0) + end + end + + def address?(address : Int) : Bool + (0x080000C4..0x080000C9).includes?(address) + end + end +end diff --git a/src/crab/gba/rtc.cr b/src/crab/gba/rtc.cr new file mode 100644 index 0000000..110f361 --- /dev/null +++ b/src/crab/gba/rtc.cr @@ -0,0 +1,204 @@ +module GBA + class RTC + @sck : Bool = false + @sio : Bool = false + @cs : Bool = false + + @state : State = State::WAITING + @reg : Register = Register::CONTROL + @buffer = Buffer.new + + # control reg + @irq : Bool = false # irqs every 30s + @m24 : Bool = true # 24-hour mode + # todo: how does the power off bit work? + + enum State + WAITING # waiting to start accepting a command + COMMAND # reading a command + READING # reading from a register + WRITING # writing to a register + end + + enum Register + RESET = 0 + CONTROL = 1 + DATE_TIME = 2 + TIME = 3 + IRQ = 6 + + def bytes : Int + case self + when CONTROL then 1 + when DATE_TIME then 7 + when TIME then 3 + else 0 + end + end + end + + def initialize(@gba : GBA) + end + + def read : UInt8 + @sck.to_unsafe.to_u8! | @sio.to_unsafe.to_u8! << 1 | @cs.to_unsafe.to_u8! << 2 + end + + def write(value : UInt8) : Nil + sck = bit?(value, 0) + sio = bit?(value, 1) + cs = bit?(value, 2) + + case @state + in State::WAITING + if @sck && sck && !@cs && cs # cs rises + @state = State::COMMAND + @cs = true # cs stays high until command is complete + end + @sck = sck + @sio = sio + in State::COMMAND + if !@sck && sck # sck rises + @buffer.push sio + if @buffer.size == 8 # commands are 8 bits wide + @state, @reg = read_command(@buffer.shift_byte) + if @state == State::READING + prepare_read + else + execute_write if @reg.bytes == 0 + end + end + end + @sck = sck + @sio = sio + in State::READING + if !@sck && sck # sck rises + @sio = @buffer.shift + if @buffer.size == 0 + @state = State::WAITING + @cs = false # command has finished + end + end + @sck = sck + in State::WRITING + if !@sck && sck # sck rises + @buffer.push sio + execute_write if @buffer.size == @reg.bytes * 8 + end + @sck = sck + @sio = sio + end + end + + # Fill buffer with the data to read from the set register. + private def prepare_read : Nil + case @reg + when Register::CONTROL + control = 0b10_u8 | @irq.to_unsafe.to_u8! << 3 | @m24.to_unsafe.to_u8! << 6 + @buffer.push(control) + when Register::DATE_TIME + time = Time.local + hour = time.hour + hour %= 12 unless @m24 + @buffer.push bcd time.year % 100 + @buffer.push bcd time.month + @buffer.push bcd time.day + @buffer.push bcd time.day_of_week.value % 7 + @buffer.push bcd hour + @buffer.push bcd time.minute + @buffer.push bcd time.second + when Register::TIME + time = Time.local + hour = time.hour + hour %= 12 unless @m24 + @buffer.push bcd hour + @buffer.push bcd time.minute + @buffer.push bcd time.second + end + end + + # Execute a write to the set register. + private def execute_write : Nil + case @reg + when Register::CONTROL + byte = @buffer.shift_byte + @irq = bit?(byte, 3) + @m24 = bit?(byte, 6) + puts "TODO: implement rtc irq" if @irq + when Register::RESET + @irq = false + @m24 = false # todo: does this reset to 12hr or 24hr mode? + when Register::IRQ + @gba.interrupts.reg_if.game_pak = true + @gba.interrupts.schedule_interrupt_check + end + @buffer.clear + @state = State::WAITING + @cs = false + end + + # Read the given command, reversing if necessary. + private def read_command(full_command : UInt8) : Tuple(State, Register) + command_bits = 0xF_u8 & if full_command.bits(0..3) == 0b0110 + reverse_bits(full_command) + else + full_command + end + {bit?(command_bits, 0) ? State::READING : State::WRITING, Register.from_value(command_bits >> 1)} + end + + # Reverse the bits in the given byte + private def reverse_bits(byte : UInt8) : UInt8 + result = 0_u8 + (0..7).each do |bit| + result |= 1 << bit if bit?(byte, 7 - bit) + end + result + end + + # Convert the given number to binary-coded decimal. Expects numbers less + # than 100. Result is undefined otherwise. + private def bcd(int : Int) : UInt8 + ((int.to_u8! // 10) << 4) | (int.to_u8! % 10) + end + + # FIFO bit buffer implementation. + # todo: This impl is similar to what's used in eeprom.cr. Maybe merge. + private class Buffer + property size = 0 + property value : UInt64 = 0 + + def push(value : Bool) : Nil + @size += 1 + @value = (@value << 1) | (value.to_unsafe & 1) + end + + # push a byte in increasing significance + def push(byte : UInt8) : Nil + (0..7).each do |bit| + push(bit?(byte, bit)) + end + end + + def shift : Bool + abort "Invalid buffer size #{@size}" if @size <= 0 + @size -= 1 + @value >> @size & 1 == 1 + end + + # shift a byte, reading bits in increasing significance + def shift_byte : UInt8 + result = 0_u8 + 8.times do |bit| + result |= 1 << bit if shift + end + result + end + + def clear : Nil + @size = 0 + @value = 0 + end + end + end +end