GBA RTC implemented :)

Works in RTC test roms, Pokemon Emerald, and Sennen Kazoku
This commit is contained in:
Matthew Berry 2022-07-16 16:50:23 -07:00
parent 5f6a3310a1
commit 6604a4800e
4 changed files with 290 additions and 20 deletions

View file

@ -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

View file

@ -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

46
src/crab/gba/gpio.cr Normal file
View file

@ -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

204
src/crab/gba/rtc.cr Normal file
View file

@ -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