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
|
- SRAM
|
||||||
- EEPROM
|
- EEPROM
|
||||||
- Timers run effeciently on the scheduler
|
- Timers run effeciently on the scheduler
|
||||||
|
- Real-time clock support
|
||||||
|
|
||||||
### Remaining Work
|
### Remaining Work
|
||||||
- GB / GBC
|
- 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/DenSinH
|
||||||
- https://github.com/fleroviux
|
- https://github.com/fleroviux
|
||||||
- https://github.com/destoer
|
- https://github.com/destoer
|
||||||
|
- https://github.com/GhostRain0
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./gpio"
|
||||||
|
|
||||||
module GBA
|
module GBA
|
||||||
class Bus
|
class Bus
|
||||||
# Timings for rom are estimated for game compatibility.
|
# Timings for rom are estimated for game compatibility.
|
||||||
|
@ -11,8 +13,11 @@ module GBA
|
||||||
getter wram_board = Bytes.new 0x40000
|
getter wram_board = Bytes.new 0x40000
|
||||||
getter wram_chip = Bytes.new 0x08000
|
getter wram_chip = Bytes.new 0x08000
|
||||||
|
|
||||||
|
@gpio : GPIO
|
||||||
|
|
||||||
def initialize(@gba : GBA, bios_path : String)
|
def initialize(@gba : GBA, bios_path : String)
|
||||||
File.open(bios_path) { |file| file.read @bios }
|
File.open(bios_path) { |file| file.read @bios }
|
||||||
|
@gpio = GPIO.new(@gba)
|
||||||
end
|
end
|
||||||
|
|
||||||
def [](index : Int) : Byte
|
def [](index : Int) : Byte
|
||||||
|
@ -87,11 +92,10 @@ module GBA
|
||||||
address -= 0x8000 if address > 0x17FFF
|
address -= 0x8000 if address > 0x17FFF
|
||||||
@gba.ppu.vram[address]
|
@gba.ppu.vram[address]
|
||||||
when 0x7 then @gba.ppu.oam[index & 0x3FF]
|
when 0x7 then @gba.ppu.oam[index & 0x3FF]
|
||||||
when 0x8, 0x9,
|
when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD
|
||||||
0xA, 0xB,
|
if @gpio.address?(index) && @gpio.allow_reads
|
||||||
0xC then @gba.cartridge.rom[index & 0x01FFFFFF]
|
@gpio[index]
|
||||||
when 0xD
|
elsif @gba.storage.eeprom?(index)
|
||||||
if @gba.storage.eeprom? index
|
|
||||||
@gba.storage[index]
|
@gba.storage[index]
|
||||||
else
|
else
|
||||||
@gba.cartridge.rom[index & 0x01FFFFFF]
|
@gba.cartridge.rom[index & 0x01FFFFFF]
|
||||||
|
@ -116,11 +120,10 @@ module GBA
|
||||||
address -= 0x8000 if address > 0x17FFF
|
address -= 0x8000 if address > 0x17FFF
|
||||||
(@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value
|
(@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value
|
||||||
when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value
|
when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(HalfWord*).value
|
||||||
when 0x8, 0x9,
|
when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD
|
||||||
0xA, 0xB,
|
if @gpio.address?(index) && @gpio.allow_reads
|
||||||
0xC then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value
|
@gpio[index].to_u16!
|
||||||
when 0xD
|
elsif @gba.storage.eeprom?(index)
|
||||||
if @gba.storage.eeprom? index
|
|
||||||
@gba.storage[index].to_u16!
|
@gba.storage[index].to_u16!
|
||||||
else
|
else
|
||||||
(@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value
|
(@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(HalfWord*).value
|
||||||
|
@ -145,11 +148,10 @@ module GBA
|
||||||
address -= 0x8000 if address > 0x17FFF
|
address -= 0x8000 if address > 0x17FFF
|
||||||
(@gba.ppu.vram.to_unsafe + address).as(Word*).value
|
(@gba.ppu.vram.to_unsafe + address).as(Word*).value
|
||||||
when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value
|
when 0x7 then (@gba.ppu.oam.to_unsafe + (index & 0x3FF)).as(Word*).value
|
||||||
when 0x8, 0x9,
|
when 0x8, 0x9, 0xA, 0xB, 0xC, 0xD
|
||||||
0xA, 0xB,
|
if @gpio.address?(index) && @gpio.allow_reads
|
||||||
0xC then (@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value
|
@gpio[index].to_u32!
|
||||||
when 0xD
|
elsif @gba.storage.eeprom?(index)
|
||||||
if @gba.storage.eeprom? index
|
|
||||||
@gba.storage[index].to_u32!
|
@gba.storage[index].to_u32!
|
||||||
else
|
else
|
||||||
(@gba.cartridge.rom.to_unsafe + (index & 0x01FFFFFF)).as(Word*).value
|
(@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 = 0x1FFFE_u32 & index # (u8 write only) halfword-aligned
|
||||||
address -= 0x8000 if address > 0x17FFF # todo: determine if this happens before or after the limit check
|
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
|
(@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
|
when 0xE, 0xF then @gba.storage[index] = value
|
||||||
else log "Unmapped write: #{hex_str index.to_u32}"
|
else log "Unmapped write: #{hex_str index.to_u32}"
|
||||||
end
|
end
|
||||||
|
@ -193,8 +201,13 @@ module GBA
|
||||||
address = 0x1FFFF_u32 & index
|
address = 0x1FFFF_u32 & index
|
||||||
address -= 0x8000 if address > 0x17FFF
|
address -= 0x8000 if address > 0x17FFF
|
||||||
(@gba.ppu.vram.to_unsafe + address).as(HalfWord*).value = value
|
(@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 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 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)
|
when 0xE, 0xF then write_half_internal_slow(index, value)
|
||||||
else log "Unmapped write: #{hex_str index.to_u32}"
|
else log "Unmapped write: #{hex_str index.to_u32}"
|
||||||
end
|
end
|
||||||
|
@ -214,8 +227,13 @@ module GBA
|
||||||
address = 0x1FFFF_u32 & index
|
address = 0x1FFFF_u32 & index
|
||||||
address -= 0x8000 if address > 0x17FFF
|
address -= 0x8000 if address > 0x17FFF
|
||||||
(@gba.ppu.vram.to_unsafe + address).as(Word*).value = value
|
(@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 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 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)
|
when 0xE, 0xF then write_word_internal_slow(index, value)
|
||||||
else log "Unmapped write: #{hex_str index.to_u32}"
|
else log "Unmapped write: #{hex_str index.to_u32}"
|
||||||
end
|
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