Very high-level simulation of a CλaSH CPU
15 September 2018 (programming haskell fpga electronics retrochallenge retro clash chip-8)Initially, I wanted to talk this week about how I plan to structure the CλaSH description of the CHIP-8 CPU. However, I'm postponing that for now, because I ran into what seems like a CλaSH bug, and I want to see my design run on real hardware before I describe it in too much detail. So instead, here's a post on how I am testing in software.
CPUs as Mealy machines
After stripping away all the nice abstractions that I am using in my description of the CPU, what remains is a Mealy machine, which simply means it is described by a state transition and output function s -> i -> (s, o). If that looks familiar, that is not a coincidence: this is, of course, just one argument flip away from the Kleisli category of the State s monad. Just think of it as being either this or that, depending on which one you have more intuition about. A lot more on this in my upcoming blogpost.
My CHIP-8 CPU is currently described by a Mealy machine over these types:
data CPUIn = CPUIn
{ cpuInMem :: Word8
, cpuInFB :: Bit
, cpuInKeys :: KeypadState
, cpuInKeyEvent :: Maybe (Bool, Key)
, cpuInVBlank :: Bool
}
data Phase
= Init
| Fetch1
| Exec
| StoreReg Reg
| LoadReg Reg
| ClearFB (VidX, VidY)
| Draw DrawPhase (VidX, VidY) Nybble (Index 8)
| WaitKeyPress Reg
data CPUState = CPUState
{ opHi, opLo :: Word8
, pc, ptr :: Addr
, registers :: Vec 16 Word8
, stack :: Vec 24 Addr
, sp :: Index 24
, phase :: Phase
, timer :: Word8
}
data CPUOut = CPUOut
{ cpuOutMemAddr :: Addr
, cpuOutMemWrite :: Maybe Word8
, cpuOutFBAddr :: (VidX, VidY)
, cpuOutFBWrite :: Maybe Bit
}
cpu :: CPUIn -> State CPUState CPUOut
Running the CPU directly
Note that all the types involved are pure: signal inputs are turned into pure input by CλaSH's mealy function, and the pure output is similarly turned into a signal output. But what if we didn't use mealy, and ran cpu directly, completely sidestepping CλaSH, yet still running the exact same implementation?
That is exactly what I am doing for testing the CPU. By running its Mealy function directly, I can feed it a CPUIn and consume its CPUOut result while interacting with the world — completely outside the simulation! The main structure of the code that implements the above looks like this:
stateful :: (MonadIO m) => s -> (i -> State s o) -> IO (m i -> (o -> m a) -> m a)
stateful s0 step = do
state <- newIORef s0
return $ \mkInput applyOutput -> do
inp <- mkInput
out <- liftIO $ do
s <- readIORef state
let (out, s') = runState (step inp) s
writeIORef state s'
return out
applyOutput out
Hooking it up to SDL
I hooked up the main RAM and the framebuffer signals to IOArrays, and wrote some code that renders the framebuffer's contents into an SDL surface and translates keypress events. And, voilà: you can run the CHIP-8 computer, interactively, even allowing you to use good old trace-based debugging (which is thankfully removed by CλaSH during VHDL generation so can even leave them in). The below screencap shows this in action: :main is run from clashi and starts the interactive SDL program, with no Signal types involved.