mirror of
https://github.com/mattrberry/crab.git
synced 2024-12-26 09:58:25 +01:00
GBA RTC implemented :)
Works in RTC test roms, Pokemon Emerald, and Sennen Kazoku
This commit is contained in:
parent
5f6a3310a1
commit
6604a4800e
4 changed files with 290 additions and 20 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
46
src/crab/gba/gpio.cr
Normal 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
204
src/crab/gba/rtc.cr
Normal 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
|
Loading…
Reference in a new issue