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.