Skip to content

Commit

Permalink
Basic serial support
Browse files Browse the repository at this point in the history
  • Loading branch information
CLowcay committed May 5, 2020
1 parent bfdf230 commit 6c29fdd
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 48 deletions.
93 changes: 52 additions & 41 deletions core/src/Machine/GBC/Emulator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import qualified Machine.GBC.DMA as DMA
import qualified Machine.GBC.Graphics as Graphics
import qualified Machine.GBC.Keypad as Keypad
import qualified Machine.GBC.Memory as Memory
import qualified Machine.GBC.Serial as Serial
import qualified Machine.GBC.Timer as Timer

data EmulatorState = EmulatorState {
Expand All @@ -43,6 +44,7 @@ data EmulatorState = EmulatorState {
, keypadState :: !Keypad.State
, timerState :: !Timer.State
, audioState :: !Audio.State
, serialState :: !Serial.State
, hblankPending :: !(IORef Bool) -- Set if there is an HBlank but we're not ready to do HBlank DMA yet
, currentTime :: !(UnboxedRef Int) -- Time in clocks
, lastEventPoll :: !(UnboxedRef Int) -- The time of the last event poll (in clocks)
Expand All @@ -64,50 +66,59 @@ initEmulatorState
-> ROM
-> Maybe EmulatorMode
-> ColorCorrection
-> Serial.Sync
-> Graphics.Sync
-> Ptr Word8
-> IO EmulatorState
initEmulatorState bootROM rom requestedMode colorCorrection graphicsSync frameBufferBytes = mdo
let bootMode = bootROM <&> \content -> if B.length content > 0x100 then CGB else DMG
let romMode = case cgbSupport (romHeader rom) of
CGBCompatible -> CGB
CGBExclusive -> CGB
CGBIncompatible -> DMG
let mode = fromMaybe romMode (requestedMode <|> bootMode)
vram <- initVRAM colorCorrection

writeRGBPalette vram False 0 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)
writeRGBPalette vram True 0 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)
writeRGBPalette vram True 1 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)

modeRef <- newIORef mode
portIF <- newPort 0xE0 0x1F alwaysUpdate
portIE <- newPort 0x00 0xFF alwaysUpdate

cpu <- CPU.init portIF portIE mode (makeCatchupFunction emulatorState)
dmaState <- DMA.init
graphicsState <- Graphics.init vram modeRef frameBufferBytes portIF
keypadState <- Keypad.init portIF
audioState <- Audio.init
timerState <- Timer.init (Audio.clockFrameSequencer audioState) (CPU.portKEY1 cpu) portIF

let allPorts =
(IF, portIF)
: CPU.ports cpu
++ DMA.ports dmaState
++ Graphics.ports graphicsState
++ Keypad.ports keypadState
++ Timer.ports timerState
++ Audio.ports audioState

memory <- Memory.initForROM (VS.fromList . B.unpack <$> bootROM) rom vram allPorts portIE modeRef

hblankPending <- newIORef False
currentTime <- newUnboxedRef 0
lastEventPoll <- newUnboxedRef 0

let emulatorState = EmulatorState { .. }
pure emulatorState
initEmulatorState bootROM rom requestedMode colorCorrection serialSync graphicsSync frameBufferBytes
= mdo
let bootMode = bootROM <&> \content -> if B.length content > 0x100 then CGB else DMG
let romMode = case cgbSupport (romHeader rom) of
CGBCompatible -> CGB
CGBExclusive -> CGB
CGBIncompatible -> DMG
let mode = fromMaybe romMode (requestedMode <|> bootMode)
vram <- initVRAM colorCorrection

writeRGBPalette vram False 0 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)
writeRGBPalette vram True 0 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)
writeRGBPalette vram True 1 (0xffffffff, 0xaaaaaaff, 0x555555ff, 0x000000ff)

modeRef <- newIORef mode
portIF <- newPort 0xE0 0x1F alwaysUpdate
portIE <- newPort 0x00 0xFF alwaysUpdate

cpu <- CPU.init portIF portIE mode (makeCatchupFunction emulatorState)
dmaState <- DMA.init
graphicsState <- Graphics.init vram modeRef frameBufferBytes portIF
keypadState <- Keypad.init portIF
audioState <- Audio.init
timerState <- Timer.init (Audio.clockFrameSequencer audioState) (CPU.portKEY1 cpu) portIF
serialState <- Serial.init serialSync portIF

let allPorts =
(IF, portIF)
: CPU.ports cpu
++ DMA.ports dmaState
++ Graphics.ports graphicsState
++ Keypad.ports keypadState
++ Timer.ports timerState
++ Audio.ports audioState
++ Serial.ports serialState

memory <- Memory.initForROM (VS.fromList . B.unpack <$> bootROM)
rom
vram
allPorts
portIE
modeRef

hblankPending <- newIORef False
currentTime <- newUnboxedRef 0
lastEventPoll <- newUnboxedRef 0

let emulatorState = EmulatorState { .. }
pure emulatorState

-- | Get the number of clocks since the emulator started.
getEmulatorClock :: ReaderT EmulatorState IO Int
Expand Down
3 changes: 0 additions & 3 deletions core/src/Machine/GBC/Registers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -201,9 +201,6 @@ pattern WY = 0xFF4A
pattern WX :: Word16
pattern WX = 0xFF4B

-- This undocumented register is suspected to have something to do with DMG
-- compatibility. It is disabled after BLCK is set to 1 and only reads FF after
-- that point.
pattern R4C :: Word16
pattern R4C = 0xFF4C

Expand Down
120 changes: 120 additions & 0 deletions core/src/Machine/GBC/Serial.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{-# LANGUAGE RecordWildCards #-}

module Machine.GBC.Serial
( Sync(..)
, State
, init
, ports
, newSync
, update
, notifyIncoming
)
where

import Control.Concurrent.MVar
import Control.Monad
import Data.Bits
import Data.Functor
import Data.IORef
import Data.Word
import Machine.GBC.CPU.Interrupts
import Machine.GBC.Primitive
import Machine.GBC.Primitive.UnboxedRef
import Machine.GBC.Registers
import Machine.GBC.Util
import Prelude hiding ( init )

data Sync = Sync {
out :: MVar Word8
, inp :: MVar Word8
}

data State = State {
portSB :: !(Port Word8)
, portSC :: !(Port Word8)
, portIF :: !(Port Word8)
, sync :: !Sync
, transferActiveRef :: !(IORef Bool)
, bitCounter :: !(UnboxedRef Word8)
, incoming :: !(UnboxedRef Word8)
, clockPeriod :: !(UnboxedRef Int)
, shiftClock :: !Counter
}

flagTransferStart, flagShiftSpeed, flagInternalClock :: Int
flagTransferStart = 7
flagShiftSpeed = 1
flagInternalClock = 0

-- | Create a new serial sync object.
newSync :: IO Sync
newSync = do
inp <- newEmptyMVar
out <- newEmptyMVar
pure Sync { .. }

init :: Sync -> Port Word8 -> IO State
init sync portIF = do
shiftClock <- newCounter 0
bitCounter <- newUnboxedRef 0
incoming <- newUnboxedRef 0xFF
clockPeriod <- newUnboxedRef 0
transferActiveRef <- newIORef False

portSB <- newPort 0xFF 0xFF alwaysUpdate
portSC <- newPort 0x7C 0x83 $ \_ sc' -> sc' <$ do
when (sc' `testBit` flagInternalClock) $ do
if sc' `testBit` flagTransferStart
then do
putMVar (out sync) =<< directReadPort portSB
writeIORef transferActiveRef True
else writeIORef transferActiveRef False
writeUnboxedRef clockPeriod (if sc' `testBit` flagShiftSpeed then 4 else 128)
pure State { .. }

ports :: State -> [(Word16, Port Word8)]
ports State {..} = [(SB, portSB), (SC, portSC)]

-- | Notify an incoming passive transfer
notifyIncoming :: State -> Int -> Word8 -> IO ()
notifyIncoming State {..} period incomingValue = do
sc <- directReadPort portSC
unless (sc `testBit` flagInternalClock) $ do
putMVar (inp sync) incomingValue
writeUnboxedRef clockPeriod period
writeIORef transferActiveRef True

-- | Advance the serial clock.
update :: Int -> State -> IO ()
update cycles State {..} = do
transferActive <- readIORef transferActiveRef
when transferActive $ do
reloads <- updateReloadingCounter shiftClock cycles $ readUnboxedRef clockPeriod
when (reloads > 0) $ do
counter <- readUnboxedRef bitCounter
writeUnboxedRef bitCounter =<< clockSerial counter reloads

where
clockSerial counter 0 = pure counter
clockSerial counter clocks = do
value <- if counter /= 0
then readUnboxedRef incoming
else do
v <- takeMVar (inp sync)
writeUnboxedRef incoming v
pure v

sb <- directReadPort portSB
let value' = rotateL value 1
writeUnboxedRef incoming value'
directWritePort portSB (sb .<<. 1 .|. (value' .&. 1))

let counter' = (counter + 1) .&. 3
if counter' /= 0
then clockSerial counter' (clocks - 1)
else do
sc <- directReadPort portSC
directWritePort portSC (sc .&. 0x7F)
raiseInterrupt portIF InterruptEndSerialTransfer
writeIORef transferActiveRef False
pure counter'
9 changes: 7 additions & 2 deletions core/test/Machine/GBC/EmulatorSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import qualified Machine.GBC.CPU as CPU
import qualified Machine.GBC.DMA as DMA
import qualified Machine.GBC.Graphics as Graphics
import qualified Machine.GBC.Keypad as Keypad
import qualified Machine.GBC.Serial as Serial
import qualified Machine.GBC.Timer as Timer

blankROM :: ROM
Expand All @@ -41,19 +42,23 @@ blankHeader romSize = Header { startAddress = 0

spec :: Spec
spec = describe "allPorts" $ it "all hardware ports are accounted for" $ do
sync <- Graphics.newSync
emulator <- initEmulatorState Nothing blankROM Nothing NoColorCorrection sync nullPtr
sync <- Graphics.newSync
serialSync <- Serial.newSync
emulator <- initEmulatorState Nothing blankROM Nothing NoColorCorrection serialSync sync nullPtr
let allPorts =
CPU.ports (cpu emulator)
++ DMA.ports (dmaState emulator)
++ Graphics.ports (graphicsState emulator)
++ Keypad.ports (keypadState emulator)
++ Timer.ports (timerState emulator)
++ Audio.ports (audioState emulator)
++ Serial.ports (serialState emulator)
nub (fst <$> allPorts) `shouldBe` (fst <$> allPorts)
sort (fst <$> allPorts) `shouldBe` sort
( [0xFF30 .. 0xFF3F]
++ [ P1
, SB
, SC
, DIV
, TIMA
, TMA
Expand Down
22 changes: 20 additions & 2 deletions main/src/HGBC/Emulator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ module HGBC.Emulator
)
where

import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Concurrent
import Control.Exception
import Control.Monad.Except
import Control.Monad.Reader
Expand All @@ -40,6 +42,8 @@ import qualified HGBC.Debugger.Labels as Labels
import qualified HGBC.Debugger.State as DebugState
import qualified HGBC.Events as Event
import qualified Machine.GBC as GBC
import qualified Machine.GBC.Graphics as Graphics
import qualified Machine.GBC.Serial as Serial

-- | A notification for the emulator thread.
data Command
Expand Down Expand Up @@ -93,7 +97,7 @@ configure options config = do
makeEmulatorState
:: FilePath
-> Config k Identity
-> GBC.Sync
-> Graphics.Sync
-> Ptr Word8
-> ExceptT FileParseErrors (WriterT [FileParseErrors] IO) GBC.EmulatorState
makeEmulatorState filename Config {..} graphicsSync frameBuffer = do
Expand All @@ -105,7 +109,14 @@ makeEmulatorState filename Config {..} graphicsSync frameBuffer = do
when (GBC.requiresSaveFiles rom)
$ createDirectoryIfMissing True (takeDirectory (GBC.romSaveFile (GBC.romPaths rom)))

s <- GBC.initEmulatorState bootROMData rom mode colorCorrection graphicsSync frameBuffer
serialSync <- nullSerial
s <- GBC.initEmulatorState bootROMData
rom
mode
colorCorrection
serialSync
graphicsSync
frameBuffer

when (GBC.mode s == GBC.DMG) $ liftIO $ do
GBC.writeBgRGBPalette s 0 backgroundPalette
Expand All @@ -122,6 +133,13 @@ makeEmulatorState filename Config {..} graphicsSync frameBuffer = do
tryReadFile path = ExceptT (first (convertIOException path) <$> liftIO (try (B.readFile path)))
convertIOException path err = (path, [displayException (err :: IOException)])

nullSerial = do
sync <- Serial.newSync
void $ forkIO $ forever $ do
void (takeMVar (Serial.out sync))
putMVar (Serial.inp sync) 0xFF
pure sync

-- | Run the emulator. Does not return until the Quit command is sent.
run :: RuntimeConfig -> ReaderT GBC.EmulatorState IO ()
run RuntimeConfig {..} = do
Expand Down

0 comments on commit 6c29fdd

Please sign in to comment.