From ef58d5b07ee6b5a5112957b534757b237cfa8d7b Mon Sep 17 00:00:00 2001 From: Yehowshua Immanuel Date: Wed, 12 Feb 2025 23:54:15 -0500 Subject: [PATCH] first commit --- .gitignore | 227 ++++++++ LICENSE | 21 + README.md | 49 ++ Setup.hs | 5 + bin/Clash.hs | 7 + bin/Clashi.hs | 7 + bin/Main.hs | 46 ++ c/uart_sim_device.c | 74 +++ cabal.project | 16 + gen.sh | 20 + hs/Decode/Opcodes.hs | 52 ++ hs/Fetch.hs | 24 + hs/Machine.hs | 67 +++ hs/Peripherals/Ram.hs | 93 ++++ hs/Peripherals/Setup.hs | 8 + hs/Peripherals/Teardown.hs | 8 + hs/Peripherals/UartCFFI.hs | 52 ++ hs/RegFiles.hs | 56 ++ hs/Simulation.hs | 71 +++ hs/Types.hs | 24 + hs/Util.hs | 44 ++ rv_formal.cabal | 132 +++++ rv_tests/hello_world/Makefile | 37 ++ rv_tests/hello_world/build.sh | 8 + rv_tests/hello_world/hello.S | 23 + rv_tests/hello_world/hello.elf | Bin 0 -> 5048 bytes rv_tests/hello_world/linker.ld | 25 + shell.nix | 16 + stack.yaml | 10 + tables/instruction_forms.xlsx | Bin 0 -> 34297 bytes tables/isa.csv | 934 +++++++++++++++++++++++++++++++++ tests/Tests/Example/Project.hs | 24 + tests/doctests.hs | 20 + tests/unittests.hs | 10 + 34 files changed, 2210 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Setup.hs create mode 100644 bin/Clash.hs create mode 100644 bin/Clashi.hs create mode 100644 bin/Main.hs create mode 100644 c/uart_sim_device.c create mode 100644 cabal.project create mode 100644 gen.sh create mode 100644 hs/Decode/Opcodes.hs create mode 100644 hs/Fetch.hs create mode 100644 hs/Machine.hs create mode 100644 hs/Peripherals/Ram.hs create mode 100644 hs/Peripherals/Setup.hs create mode 100644 hs/Peripherals/Teardown.hs create mode 100644 hs/Peripherals/UartCFFI.hs create mode 100644 hs/RegFiles.hs create mode 100644 hs/Simulation.hs create mode 100644 hs/Types.hs create mode 100644 hs/Util.hs create mode 100644 rv_formal.cabal create mode 100644 rv_tests/hello_world/Makefile create mode 100644 rv_tests/hello_world/build.sh create mode 100644 rv_tests/hello_world/hello.S create mode 100755 rv_tests/hello_world/hello.elf create mode 100644 rv_tests/hello_world/linker.ld create mode 100644 shell.nix create mode 100644 stack.yaml create mode 100644 tables/instruction_forms.xlsx create mode 100644 tables/isa.csv create mode 100644 tests/Tests/Example/Project.hs create mode 100644 tests/doctests.hs create mode 100644 tests/unittests.hs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e28e10f --- /dev/null +++ b/.gitignore @@ -0,0 +1,227 @@ +stack.yaml.lock +src/Main +out/ +isa_json/ +odir/ +hidir/ +dist/ +dist-newstyle/ +.stack-work/ +cabal-dev +/cabal.project.local +.ghc.environment.* +*.o +*.o-boot +*.hi +*.hi-boot +*.po +*.po-boot +*.p_o +*.p_o-boot +*.chi +*.chs.h +*.dyn_o +*.dyn_o-boot +*.dyn_hi +*.dyn_hi-boot +.virtualenv +.hpc +.hsenv +.cabal-sandbox/ +cabal.sandbox.config +cabal.config +*.prof +*.aux +*.hp +*.bin +*.log +*.tar.gz + +*~ +*.DS_Store + +# IntelliJ +/.idea +*.iml + +# HDL directories often created during development cycle +/vhdl +/verilog +/systemverilog +.env +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19fac98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Yehowshua Immanuel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf3b06c --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# About +An attempt at a formal reference model for the RISC-V ISA written in +Clash Haskell. + +# Getting Started +This works with ghc 9.4.8 + +The clash compiler is basically a modified version of ghc designed to allow for first class support of haskell code as circuits. + +Note that this repository is currently very much W.I.P. That being said, +this is how you would currently run a simulation: + +```bash +cabal run main --ghc-options="-D_RAM_DEPTH=2048" -- --firmware=./rv_tests/hello_world/hello.bin +``` + +# Notes +All the context that we pick up as we execute an instruction in +essence forms the context of our micro-op machinery. + +## Installing GCC-RISC-V Toolchain on [Insert Platform Here] + +Change instructions to support Nix + + +# TODO + - [ ] fetch should invoke mem read function + +# Organization Thoughts + - Potential functions + 1. BitPat -> Opcode + 2. Opcode -> Fields + 3. Fields -> Field Vals + 4. Field Vals -> Reg Vals + +# Thoroughness + - [ ] Check that all forms get used!! Remove unused forms!! + +# Grant Notes + - [ ] Some forms may be redundant(may need to remove some) + +## Quality of Life Enhancements + - [ ] turn off derive generics + - [ ] update commands to use flags that prevent ghc + intermediate outputs from littering source tree + - [ ] enable Phoityne debug + - [ ] learn to use trace and jupyter + - [ ] draw conclusions on feasibility of debugging + without VCD viewer diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..470563e --- /dev/null +++ b/Setup.hs @@ -0,0 +1,5 @@ +import Prelude +import Distribution.Extra.Doctest (defaultMainWithDoctests) + +main :: IO () +main = defaultMainWithDoctests "doctests" diff --git a/bin/Clash.hs b/bin/Clash.hs new file mode 100644 index 0000000..7193d8b --- /dev/null +++ b/bin/Clash.hs @@ -0,0 +1,7 @@ + +import Prelude +import System.Environment (getArgs) +import Clash.Main (defaultMain) + +main :: IO () +main = getArgs >>= defaultMain diff --git a/bin/Clashi.hs b/bin/Clashi.hs new file mode 100644 index 0000000..227a201 --- /dev/null +++ b/bin/Clashi.hs @@ -0,0 +1,7 @@ + +import Prelude +import System.Environment (getArgs) +import Clash.Main (defaultMain) + +main :: IO () +main = getArgs >>= defaultMain . ("--interactive":) diff --git a/bin/Main.hs b/bin/Main.hs new file mode 100644 index 0000000..d0991b2 --- /dev/null +++ b/bin/Main.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ConstraintKinds #-} + +module Main where + +import Prelude +import System.Environment (getArgs, getProgName) +import System.Exit (exitFailure) +import Data.Maybe (listToMaybe) +import Data.List (isPrefixOf) +import Text.Show.Pretty (ppShow) + +import Simulation (simulation, Args(..)) + +main :: IO () +main = do + rawArgs <- getArgs + args <- parseArgs rawArgs + states <- simulation args + putStrLn "Simulating Machine" + -- mapM_ (putStrLn . ppShow) states -- Uncomment to print each state, if needed. + putStrLn $ "Last state: " ++ show (last states) + putStrLn $ "Executed for " ++ show (length states) ++ " cycles" + putStrLn "Simulation complete" + +-- Function to parse command line arguments into the Args data type +parseArgs :: [String] -> IO Args +parseArgs argv = + case extractKey "firmware" argv of + Just firmwarePath -> return Args { firmware = firmwarePath } + Nothing -> do + progName <- getProgName + putStrLn "Error: No firmware file found." + putStrLn $ "Usage: " ++ progName ++ " --firmware=FILE" + exitFailure + +filterByKey :: String -> [String] -> [String] +filterByKey key argv = filter (switch `isPrefixOf`) argv + where + switch = "--" ++ key ++ "=" + +extractKey :: String -> [String] -> Maybe String +extractKey key argv = listToMaybe $ map removePrefix $ filterByKey key argv + where removePrefix = drop $ length ("--" ++ key ++ "=") diff --git a/c/uart_sim_device.c b/c/uart_sim_device.c new file mode 100644 index 0000000..524b8f7 --- /dev/null +++ b/c/uart_sim_device.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include + +static volatile bool ctrl_c_received = false; + +void sigint_handler(int sig_num) { + ctrl_c_received = true; +} + +void setup_sigint_handler() { + signal(SIGINT, sigint_handler); +} + +bool was_ctrl_c_received() { + return ctrl_c_received; +} + +static struct termios oldt, newt; + +void init_terminal() { + // Get terminal attributes + tcgetattr(STDIN_FILENO, &oldt); + newt = oldt; + + // Set terminal to raw mode (no echo, non-canonical) + newt.c_lflag &= ~(ICANON | ECHO); + tcsetattr(STDIN_FILENO, TCSANOW, &newt); +} + +void restore_terminal() { + // Restore terminal to its old state + tcsetattr(STDIN_FILENO, TCSANOW, &oldt); +} + +char get_char_from_terminal() { + char c = getchar(); + return c; +} + +void write_char_to_terminal(char chr) { + putchar(chr); + fflush(stdout); +} + +int is_char_available() { + struct timeval tv; + fd_set read_fd_set; + + // Don't wait at all, not even a microsecond + tv.tv_sec = 0; + tv.tv_usec = 0; + + // Watch stdin (fd 0) to see when it has input + FD_ZERO(&read_fd_set); + FD_SET(0, &read_fd_set); + + // Check if there's any input available + if (select(1, &read_fd_set, NULL, NULL, &tv) == -1) { + perror("select"); + return 0; // 0 indicates no characters available + } + + if (FD_ISSET(0, &read_fd_set)) { + // Character is available + return 1; + } else { + // No character available + return 0; + } +} \ No newline at end of file diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..255da59 --- /dev/null +++ b/cabal.project @@ -0,0 +1,16 @@ +packages: + rv_formal.cabal + +packages: . +builddir: build + +write-ghc-environment-files: always + +-- Eliminates the need for `--enable-tests`, which is needed for HLS. +tests: true + +-- Works around: https://github.com/recursion-schemes/recursion-schemes/issues/128. This +-- shouldn't harm (runtime) performance of Clash, as we only use recursion-schemes with +-- TemplateHaskell. +package recursion-schemes + optimization: 0 diff --git a/gen.sh b/gen.sh new file mode 100644 index 0000000..02e48f8 --- /dev/null +++ b/gen.sh @@ -0,0 +1,20 @@ +set -ex + +# dependencies +# TODO : re-enable the following +# python3 -m venv .env +# source .env/bin/activate +# pushd hs_gen +# pip3 install -r requirements.txt +# popd + +# generate +rm -rf out +mkdir -p out +python3 -m py.gen_json.gen_forms_and_field_mappings_json out +python3 -m py.gen_hs.extract_opcodes_to_haskell ./hs/Decode/Opcodes.hs +python3 -m py.gen_hs.extract_bitpat_to_haskell ./hs/Decode/BitpatsToOpcodes.hs +python3 -m py.gen_hs.generate_forms ./out/forms_v_fields.json ./hs/Decode/Forms.hs +python3 -m py.gen_hs.generate_fields ./out/field_v_slice.json ./hs/Decode/Fields.hs +python3 -m py.gen_hs.generate_opcodeToForms ./hs/Decode/OpcodeToForm.hs +python3 -m py.gen_hs.extract_fields ./out/field_v_slice.json ./hs/Decode/ExtractField.hs \ No newline at end of file diff --git a/hs/Decode/Opcodes.hs b/hs/Decode/Opcodes.hs new file mode 100644 index 0000000..6c48b4e --- /dev/null +++ b/hs/Decode/Opcodes.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +module Decode.Opcodes(Opcode(..)) where +import Clash.Prelude +import Data.Functor.Contravariant (Op) + +type FUNCT7 = Unsigned 7 +type RS2 = Unsigned 5 +type RS1 = Unsigned 5 +type FUNCT3 = Unsigned 3 +type RD = Unsigned 5 +type OPCODE = Unsigned 7 + +type IMM12 = Unsigned 12 +type IMM13 = Unsigned 13 +type IMM20 = Unsigned 20 +type IMM21 = Unsigned 21 + +data RTypeFields = RTypeFields OPCODE RD FUNCT3 RS1 RS2 FUNCT7 +data ITypeFields = ITypeFields OPCODE RD FUNCT3 RS1 IMM12 +data STypeFields = STypeFields OPCODE FUNCT3 RS1 RS2 IMM12 +data BTypeFields = BTypeFields OPCODE FUNCT3 RS1 RS2 IMM13 +data UTypeFields = UTypeFields OPCODE RD IMM20 +data JTypeFields = JTypeFields OPCODE RD IMM21 + +data Opcode + = ADD RTypeFields + | SUB RTypeFields + | XOR RTypeFields + | OR RTypeFields + | AND RTypeFields + | SLL RTypeFields + | SRL RTypeFields + | SRA RTypeFields + | SLT RTypeFields + | SLTU RTypeFields + | ADDRI ITypeFields + | XORI ITypeFields + | ORI ITypeFields + | ANDI ITypeFields + | SLLI ITypeFields + | SRLI ITypeFields + | SRAI ITypeFields + | SLTI ITypeFields + | SLTIU ITypeFields + | LB ITypeFields + | LH ITypeFields + | LW ITypeFields + | LBU ITypeFields + | LHU ITypeFields + diff --git a/hs/Fetch.hs b/hs/Fetch.hs new file mode 100644 index 0000000..2bcc296 --- /dev/null +++ b/hs/Fetch.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +module Fetch(fetchInstruction) where + +import Clash.Prelude +import Types(Mem, Addr, FullWord) +import Util(endianSwapWord) + +data Insn = Instruction FullWord + | Misaligned Addr + +fetchInstruction :: KnownNat n => Mem n -> Addr -> Insn +fetchInstruction mem addr = + let + isWordAligned = addr .&. 3 == 0 + addrWordAligned = addr `shiftR` 2 + insn = mem !! addrWordAligned + -- TODO : check if instruction is word aligned and create type + -- to capture if its not. + in + case isWordAligned of + True -> Instruction insn + False -> Misaligned addr diff --git a/hs/Machine.hs b/hs/Machine.hs new file mode 100644 index 0000000..4910422 --- /dev/null +++ b/hs/Machine.hs @@ -0,0 +1,67 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +module Machine( + Machine(..), + RISCVCPU(..), + Endian(..), + machineInit) where + +import Clash.Prelude +import Types(Pc, Mem) +import RegFiles(GPR, FPR, CSR, gprInit, fprInit, csrInit) + +data Endian = Big | Little + deriving (Generic, Show, Eq, NFDataX) + +data PrivilegeLevel + = MachineMode + | SuperVisorMode + | UserMode + deriving (Generic, Show, Eq, NFDataX) + +data RISCVCPU = RISCVCPU + { pc :: Pc, + gpr :: GPR, + fpr :: FPR, + privilegeLevel :: PrivilegeLevel + } + deriving (Generic, Show, Eq, NFDataX) + +data Machine = Machine + { cpu :: RISCVCPU, + mem :: Mem 14 + } + deriving (Generic, Show, Eq, NFDataX) + +riscvCPUInit :: RISCVCPU +riscvCPUInit = + RISCVCPU + 0 + gprInit + fprInit + MachineMode + +machineInit :: Machine +machineInit = + Machine + riscvCPUInit + memInit + +memInit :: Vec 14 (Unsigned 32) +memInit = + 0x0000A03C + :> 0x3000A5E8 + :> 0x1A002038 + :> 0x18002598 + :> 0x10002588 + :> 0x01002170 + :> 0xF8FF8141 + :> 0x08002588 + :> 0x01002138 + :> 0x00002598 + :> 0xE8FFFF4B + :> 0x00000060 + :> 0x002000C0 + :> 0x00000000 + :> Nil diff --git a/hs/Peripherals/Ram.hs b/hs/Peripherals/Ram.hs new file mode 100644 index 0000000..36d63dc --- /dev/null +++ b/hs/Peripherals/Ram.hs @@ -0,0 +1,93 @@ +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Peripherals.Ram() where + +import Clash.Prelude +import qualified Prelude as P +import qualified Data.ByteString.Lazy as BL +import Data.Binary.Get +import Data.Int (Int32) +import qualified Clash.Sized.Vector as Vec + +-- vector depth has to be known statically at compile time +#ifndef _RAM_DEPTH +#define _RAM_DEPTH 1024 +#endif + +-- TODO : replace Unsigned 32 with BusVal types later... +type Ram = Vec _RAM_DEPTH (Unsigned 32) + +initRamFromFile :: FilePath -> IO (Maybe Ram) +initRamFromFile filePath = + let + initRam = Vec.replicate (SNat :: SNat _RAM_DEPTH) 0 + in + do + bs <- readFileIntoByteString filePath + let ints = getInts bs + pure $ populateVectorFromInt32 ints initRam + +readFileIntoByteString :: FilePath -> IO BL.ByteString +readFileIntoByteString filePath = BL.readFile filePath + +-- Define a function to read a ByteString and convert to [Int32] +getInts :: BL.ByteString -> [Int32] +getInts bs = runGet listOfInts bs + where + listOfInts = do + empty <- isEmpty + if empty + then pure [] + else do + i <- getInt32le -- Parse a single Int32 from the stream + rest <- listOfInts -- Recursively parse the rest + pure (i : rest) + +-- Adjusts the length of a list of integers by either truncating or padding with zeros +populateVectorFromInt32 :: + KnownNat n => + [Int32] -> + Vec n (Unsigned 32) -> + Maybe (Vec n (Unsigned 32)) +populateVectorFromInt32 ls v = Vec.fromList adjustedLs + where + vecLen = length v + adjustedLs = fromIntegral <$> adjustLength vecLen ls + adjustLength :: Int -> [Int32] -> [Int32] + adjustLength n xs = P.take n (xs P.++ P.repeat 0) + + + +-- Function to increment each element of a Clash vector +-- prepareVector :: KnownNat n => [Int32] -> Vec n (Unsigned 32) +-- prepareVector xs = let +-- unsigneds = map (fromIntegral :: Int32 -> Unsigned 32) xs -- Step 1: Convert Int32 to Unsigned 32 +-- len = length unsigneds +-- in case compare len (snatToNum (SNat @n)) of -- Step 2: Adjust the length of the list +-- LT -> takeI unsigneds ++ repeat 0 -- Pad with zeros if the list is shorter +-- GT -> takeI unsigneds -- Truncate if the list is longer +-- EQ -> takeI unsigneds -- No padding or truncation needed + +-- Function to load firmware +-- loadFirmware :: KnownNat n => [Int32] -> Vec n (Unsigned 32) +-- loadFirmware (x:xs) = vecHead ++ vecTail +-- where +-- vecHead = singleton (fromIntegral x) +-- vecTail = loadFirmware xs +-- loadFirmware [] = takeI $ repeat 0 + +-- loadFirmware xs = v +-- where +-- mapped :: [Unsigned 32] = Clash.Prelude.fromIntegral <$> xs +-- c = takeI (mapped ++ repeat 0) +-- v = takeI $ (mapped ++ repeat 0) + +-- -- Example usage +-- someList :: [Int32] +-- someList = [1, 2, 3, 4, 5] + +-- mem :: Vec 16 (Unsigned 32) +-- mem = loadFirmware someList \ No newline at end of file diff --git a/hs/Peripherals/Setup.hs b/hs/Peripherals/Setup.hs new file mode 100644 index 0000000..a129587 --- /dev/null +++ b/hs/Peripherals/Setup.hs @@ -0,0 +1,8 @@ +module Peripherals.Setup (setupPeripherals) where + +import Prelude +import Peripherals.UartCFFI(initTerminal) + +setupPeripherals :: IO () +setupPeripherals = do + initTerminal \ No newline at end of file diff --git a/hs/Peripherals/Teardown.hs b/hs/Peripherals/Teardown.hs new file mode 100644 index 0000000..b70d5a3 --- /dev/null +++ b/hs/Peripherals/Teardown.hs @@ -0,0 +1,8 @@ +module Peripherals.Teardown(teardownPeripherals) where + +import Prelude +import Peripherals.UartCFFI(restoreTerminal) + +teardownPeripherals :: IO () +teardownPeripherals = do + restoreTerminal \ No newline at end of file diff --git a/hs/Peripherals/UartCFFI.hs b/hs/Peripherals/UartCFFI.hs new file mode 100644 index 0000000..b0998e9 --- /dev/null +++ b/hs/Peripherals/UartCFFI.hs @@ -0,0 +1,52 @@ +{-# LANGUAGE ForeignFunctionInterface #-} + +module Peripherals.UartCFFI ( + initTerminal, + restoreTerminal, + getCharFromTerminal, + writeCharToTerminal, + isCharAvailable, + setupSigintHandler, + wasCtrlCReceived +) where + +import Prelude +import Foreign.C.Types +import Foreign.C.String +import Foreign.Ptr +import Data.Char (chr, ord) + +-- Foreign imports directly corresponding to the C functions +foreign import ccall "init_terminal" c_initTerminal :: IO () +foreign import ccall "restore_terminal" c_restoreTerminal :: IO () +foreign import ccall "get_char_from_terminal" c_getCharFromTerminal :: IO CChar +foreign import ccall "write_char_to_terminal" c_writeCharToTerminal :: CChar -> IO () +foreign import ccall "is_char_available" c_isCharAvailable :: IO CInt +foreign import ccall "setup_sigint_handler" c_setupSigintHandler :: IO () +foreign import ccall "was_ctrl_c_received" c_wasCtrlCReceived :: IO CInt + +-- Haskell friendly wrappers +initTerminal :: IO () +initTerminal = c_initTerminal + +restoreTerminal :: IO () +restoreTerminal = c_restoreTerminal + +getCharFromTerminal :: IO Char +getCharFromTerminal = fmap (chr . fromEnum) c_getCharFromTerminal + +writeCharToTerminal :: Char -> IO () +writeCharToTerminal char = c_writeCharToTerminal (toEnum $ ord char) + +isCharAvailable :: IO Int +isCharAvailable = fmap fromEnum c_isCharAvailable + +setupSigintHandler :: IO () +setupSigintHandler = c_setupSigintHandler + +wasCtrlCReceived :: IO Int +wasCtrlCReceived = fmap fromEnum c_wasCtrlCReceived + +-- Improved version of the ctrlCReceived to use the new wasCtrlCReceived signature +ctrlCReceived :: IO Bool +ctrlCReceived = fmap (/= 0) wasCtrlCReceived \ No newline at end of file diff --git a/hs/RegFiles.hs b/hs/RegFiles.hs new file mode 100644 index 0000000..53386d3 --- /dev/null +++ b/hs/RegFiles.hs @@ -0,0 +1,56 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +module RegFiles( + GPR, + FPR, + CSR, + gprInit, + fprInit, + csrInit + ) where + +import Clash.Prelude + +-- In RISC-V, besides the GPR, FPR, and CSR, we may also encounter +-- the following which are not modeled in this codebase. +-- * VRF(Vector Registers File) for vector processing. +-- * Debug Registers (DBR) for hardware debugging. +-- * Shadow Registers for fast context switching (optional). +-- * MPU Registers for memory protection. +-- * Counter/Timer Registers for time/cycle counting. +-- * Hypervisor Registers (HPR) for guest virtualization. + +type GPR = Vec 32 (Unsigned 64) +type FPR = Vec 32 (Unsigned 64) +type CSR = Vec 4096 (Unsigned 64) + +gprInit :: GPR +gprInit = repeat 0 + +fprInit :: FPR +fprInit = repeat 0 + +-- TODO: CSR can't actually be all 0 during initialization. +-- We need to revisit the following and properly initialize +-- various registers later. +csrInit :: CSR +csrInit = + replace (0x301 :: Integer) misa_init + $ replace (0x300 :: Integer) mstatus_init + $ replace (0x305 :: Integer) mtvec_init + $ replace (0xF11 :: Integer) mvendorid_init + $ replace (0xF12 :: Integer) marchid_init + $ replace (0xF13 :: Integer) mimpid_init + $ replace (0x701 :: Integer) mtime_init + $ replace (0x321 :: Integer) mtimecmp_init + $ repeat 0 + where + misa_init = 0x8000000000001104 -- `RV64IMAFD` + mstatus_init = 0x0000000000001800 -- Default `mstatus` + mtvec_init = 0x0000000000001000 -- Trap vector base + mvendorid_init = 0x00000000 + marchid_init = 0x00000000 + mimpid_init = 0x00000000 + mtime_init = 0x0000000000000000 + mtimecmp_init = 0xFFFFFFFFFFFFFFFF diff --git a/hs/Simulation.hs b/hs/Simulation.hs new file mode 100644 index 0000000..023bc6e --- /dev/null +++ b/hs/Simulation.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ConstraintKinds #-} + +module Simulation(Args(..), simulation) where + +import Peripherals.Setup(setupPeripherals) +import Peripherals.Teardown(teardownPeripherals) +import Text.Printf (printf) +import Clash.Prelude +import Machine( + Machine(..), + RISCVCPU(..), + machineInit, RISCVCPU (RISCVCPU)) +import Fetch(fetchInstruction) +import Peripherals.UartCFFI(writeCharToTerminal) +import Control.Concurrent (threadDelay) + +import Debug.Trace + +data Args = Args { + firmware :: FilePath + } deriving (Show) + +machine :: Machine +machine = machineInit + +machine' :: Machine -> Machine +machine' machine = + let + -- instruction = + -- traceShow + -- (printf "0x%X" (toInteger v) :: String) + -- v + -- where v = fetchInstruction mem msr pc + -- instruction = traceShow (bitpatToOpcode v) v + -- where v = fetchInstruction machineMem machinePC + machineMem = mem machine + machineCPU = cpu machine + machinePC = pc machineCPU + instruction = fetchInstruction machineMem machinePC + addr = 0 :: Integer + -- execute would go here, but right now, we simply + mem' = replace addr (3) machineMem + cpu' = machineCPU { pc = machinePC + 4 } + in + machine { cpu = cpu', mem = mem' } + +machineSignal :: HiddenClockResetEnable dom => Signal dom Machine +machineSignal = register machine (machine' <$> machineSignal) + +simulationLoop :: Int -> Machine -> IO [Machine] +simulationLoop 0 state = return [state] +simulationLoop n state = do + let newState = machine' state + rest <- simulationLoop (n - 1) newState + return (state : rest) + +simulation :: Args -> IO [Machine] +simulation args = do + setupPeripherals + + -- quick smoketest that UART works - remove later + writeCharToTerminal 'a' + threadDelay 1000000 -- Delay for 1 second (1,000,000 microseconds) + + let initState = machine + sim <- simulationLoop 5 initState + teardownPeripherals + return sim diff --git a/hs/Types.hs b/hs/Types.hs new file mode 100644 index 0000000..11300ee --- /dev/null +++ b/hs/Types.hs @@ -0,0 +1,24 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NumericUnderscores #-} + +module Types(Pc, BusVal(..), Mem, FullWord, Addr) where + +import Clash.Prelude + +type Byte = Unsigned 8 +type HalfWord = Unsigned 16 +type FullWord = Unsigned 32 +type DoubleWord = Unsigned 64 +type QuadWord = Unsigned 128 + +data BusVal + = BusByte Byte + | BusHalfWord HalfWord + | BusWord FullWord + | BusDoubleWord DoubleWord + | BusQuadWord QuadWord + deriving (Generic, Show, Eq, NFDataX) + +type Pc = DoubleWord +type Addr = DoubleWord +type Mem n = Vec n FullWord diff --git a/hs/Util.hs b/hs/Util.hs new file mode 100644 index 0000000..cfd3924 --- /dev/null +++ b/hs/Util.hs @@ -0,0 +1,44 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE ConstraintKinds #-} + +module Util( + powerIndex32, + powerIndex64, + endianSwapWord) where + +import Clash.Prelude +import Types(FullWord) + +data ValidIndex32 (n :: Nat) where + ValidIndex32 :: (0 <= n, n <= 31) => SNat n -> ValidIndex32 n + +mkValidIndex32 :: forall n. (KnownNat n, 0 <= n, n <= 31) => ValidIndex32 n +mkValidIndex32 = ValidIndex32 $ SNat @n + +powerIndex32 :: forall n. (KnownNat n, 0 <= n, n <= 31) => SNat (31 - n) +powerIndex32 = case mkValidIndex32 @n of + ValidIndex32 _ -> SNat @(31 - n) + +data ValidIndex63 (n :: Nat) where + ValidIndex63 :: (0 <= n, n <= 63) => SNat n -> ValidIndex63 n + +mkValidIndex64 :: forall n. (KnownNat n, 0 <= n, n <= 63) => ValidIndex63 n +mkValidIndex64 = ValidIndex63 $ SNat @n + +powerIndex64 :: forall n. (KnownNat n, 0 <= n, n <= 63) => SNat (63 - n) +powerIndex64 = case mkValidIndex64 @n of + ValidIndex63 _ -> SNat @(63 - n) + +endianSwapWord :: FullWord -> FullWord +endianSwapWord x = + (byte0 `shiftL` 24) .|. + (byte1 `shiftL` 16) .|. + (byte2 `shiftL` 8) .|. + byte3 + where + byte0 = (x .&. 0x000000FF) + byte1 = (x .&. 0x0000FF00) `shiftR` 8 + byte2 = (x .&. 0x00FF0000) `shiftR` 16 + byte3 = (x .&. 0xFF000000) `shiftR` 24 \ No newline at end of file diff --git a/rv_formal.cabal b/rv_formal.cabal new file mode 100644 index 0000000..f8ed3bd --- /dev/null +++ b/rv_formal.cabal @@ -0,0 +1,132 @@ +cabal-version: 2.4 +name: rvFormal +version: 0.1 +license: BSD-2-Clause +author: John Smith +maintainer: John Smith + +common common-options + default-extensions: + NumericUnderscores + DeriveDataTypeable + BangPatterns + BinaryLiterals + ConstraintKinds + DataKinds + DefaultSignatures + DeriveAnyClass + DeriveDataTypeable + DeriveFoldable + DeriveFunctor + DeriveGeneric + DeriveLift + DeriveTraversable + DerivingStrategies + InstanceSigs + KindSignatures + LambdaCase + NoStarIsType + PolyKinds + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + TypeFamilies + TypeOperators + ViewPatterns + FlexibleContexts + MagicHash + + -- TemplateHaskell is used to support convenience functions such as + -- 'listToVecTH' and 'bLit'. + TemplateHaskell + QuasiQuotes + + -- Prelude isn't imported by default as Clash offers Clash.Prelude + NoImplicitPrelude + ghc-options: + -Wall -Wcompat + -haddock + + -- Plugins to support type-level constraint solving on naturals + -fplugin GHC.TypeLits.Extra.Solver + -fplugin GHC.TypeLits.Normalise + -fplugin GHC.TypeLits.KnownNat.Solver + + -- Clash needs access to the source code in compiled modules + -fexpose-all-unfoldings + + -- Worker wrappers introduce unstable names for functions that might have + -- blackboxes attached for them. You can disable this, but be sure to add + -- a no-specialize pragma to every function with a blackbox. + -fno-worker-wrapper + build-depends: + base, + Cabal, + + -- clash-prelude will set suitable version bounds for the plugins + clash-prelude >= 1.6.4 && < 1.10, + ghc-typelits-natnormalise, + ghc-typelits-extra, + ghc-typelits-knownnat, + binary, + bytestring, + mtl, + pretty-show + +custom-setup + setup-depends: + base >= 4.11, + Cabal >= 2.4, + cabal-doctest >= 1.0.1 && <1.1 + +library + import: common-options + hs-source-dirs: hs + exposed-modules: + Simulation + other-modules: + Decode.Opcodes, + Peripherals.Ram, + Peripherals.UartCFFI, + Peripherals.Setup, + Peripherals.Teardown, + Types, + Machine, + RegFiles, + Fetch, + Util + c-sources: c/uart_sim_device.c + include-dirs: c + default-language: Haskell2010 + +-- Builds the executable 'clash', with rvFormal project in scope +executable clash + main-is: bin/Clash.hs + default-language: Haskell2010 + Build-Depends: base, clash-ghc, rvFormal + if !os(Windows) + ghc-options: -dynamic + +-- Builds the executable 'clashi', with rvFormal project in scope +executable clashi + main-is: bin/Clashi.hs + default-language: Haskell2010 + if !os(Windows) + ghc-options: -dynamic + build-depends: base, clash-ghc, rvFormal + +executable main + import: common-options + main-is: bin/Main.hs + -- hs-source-dirs: hs + default-language: Haskell2010 + build-depends: + base, + clash-ghc, + rvFormal + c-sources: c/uart_sim_device.c + include-dirs: c + if !os(Windows) + ghc-options: -dynamic diff --git a/rv_tests/hello_world/Makefile b/rv_tests/hello_world/Makefile new file mode 100644 index 0000000..1d021ae --- /dev/null +++ b/rv_tests/hello_world/Makefile @@ -0,0 +1,37 @@ +# RISC-V toolchain +CC = riscv64-unknown-elf-gcc +AS = riscv64-unknown-elf-as +LD = riscv64-unknown-elf-ld +OBJCOPY = riscv64-unknown-elf-objcopy +QEMU = qemu-system-riscv64 + +# Compilation flags +ARCH_FLAGS = -march=rv64imac -mabi=lp64 +LDSCRIPT = linker.ld + +# Output files +ELF = hello.elf +BIN = hello.bin +OBJ = hello.o +SRC = hello.S + +# Default rule +all: $(BIN) + +# Assemble and link to ELF +$(ELF): $(SRC) $(LDSCRIPT) + $(AS) $(ARCH_FLAGS) -o $(OBJ) $(SRC) + $(LD) -T $(LDSCRIPT) -o $(ELF) $(OBJ) + +# Convert ELF to raw binary +$(BIN): $(ELF) + $(OBJCOPY) -O binary $(ELF) $(BIN) + +# Run in QEMU +run: $(BIN) + echo "Press CTRL+A then X to exit QEMU" + $(QEMU) -machine virt -nographic -bios none -kernel $(BIN) -device loader,file=$(BIN),addr=0x80000000 + +# Clean up generated files +clean: + rm -f $(OBJ) $(ELF) $(BIN) diff --git a/rv_tests/hello_world/build.sh b/rv_tests/hello_world/build.sh new file mode 100644 index 0000000..3990d80 --- /dev/null +++ b/rv_tests/hello_world/build.sh @@ -0,0 +1,8 @@ +# Assemble the code +riscv64-unknown-elf-as -march=rv64imac -mabi=lp64 -o hello.o hello.S + +# Link with our linker script to create an ELF file +riscv64-unknown-elf-ld -T linker.ld -o hello.elf hello.o + +# Convert ELF to a raw binary +riscv64-unknown-elf-objcopy -O binary hello.elf hello.bin diff --git a/rv_tests/hello_world/hello.S b/rv_tests/hello_world/hello.S new file mode 100644 index 0000000..ffef385 --- /dev/null +++ b/rv_tests/hello_world/hello.S @@ -0,0 +1,23 @@ +.section .text.init +.global _start + +.equ UART_BASE, 0x10000000 # QEMU's UART base address + +_start: + # Load address of the string into a1 + la a1, message + +loop: + lbu a0, 0(a1) # Load a byte from the string + beqz a0, exit # If null terminator, exit + li t0, UART_BASE # Load UART address + sb a0, 0(t0) # Store character to UART (8-bit write) + addi a1, a1, 1 # Move to next character + j loop # Repeat + +exit: + j exit # Infinite loop + +.section .rodata +message: + .asciz "Hello, world!\n" diff --git a/rv_tests/hello_world/hello.elf b/rv_tests/hello_world/hello.elf new file mode 100755 index 0000000000000000000000000000000000000000..3994604b3dca6779c12694ad4bb7a9e77ddf6221 GIT binary patch literal 5048 zcmb<-^>JfjWMqH=CWg-pP+kK_%mG9&FfjZOgfKvI4h$9yObiYT>k)RqXL8YL=Y<( z#Hz~8EzL13Fd*avX2?hbxWA9AmYIQxK?XVwfGW<6%muSS;|MGaAOC|nNCYRC4Hick zIS>YmgLFa08Ngfy1_m=Q1JhnRus9rJ&y?lmH$DojS zC&yq{29*ke0hpUwT%4Gm%8-+vU%-%Bky*kJUtE${RKmcZS6rD}l9jwZ{n_kZV literal 0 HcmV?d00001 diff --git a/rv_tests/hello_world/linker.ld b/rv_tests/hello_world/linker.ld new file mode 100644 index 0000000..49e379f --- /dev/null +++ b/rv_tests/hello_world/linker.ld @@ -0,0 +1,25 @@ +OUTPUT_ARCH(riscv) +ENTRY(_start) + +SECTIONS +{ + /* Start execution at 0x80000000 */ + . = 0x80000000; + + .text : { + *(.text.init) + *(.text) + } + + .rodata : { + *(.rodata) + } + + .data : { + *(.data) + } + + .bss : { + *(.bss) + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..fb93d74 --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: + +let + riscv64-linux = pkgs.pkgsCross.riscv64; +in +pkgs.mkShell { + buildInputs = [ + riscv64-linux.gcc + riscv64-linux.binutils + riscv64-linux.glibc + ]; + + shellHook = '' + echo "RISC-V Linux cross-compilation environment initialized!" + ''; +} \ No newline at end of file diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..cb1198d --- /dev/null +++ b/stack.yaml @@ -0,0 +1,10 @@ +resolver: lts-21.20 + +extra-deps: + - GenericPretty-1.2.2 + - clash-ghc-1.8.1 + - clash-prelude-1.8.1 + - clash-lib-1.8.1 + - concurrent-supply-0.1.8 + - prettyprinter-interp-0.2.0.0 + - pretty-show-1.10 diff --git a/tables/instruction_forms.xlsx b/tables/instruction_forms.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..537f920855bb23560bb97cd644bb850c0ae112c3 GIT binary patch literal 34297 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 pure [] + Just _ -> pure . ("-package-db="++) <$> getGlobalPackageDb + + doctest (flags ++ extraFlags ++ pkgs ++ module_sources) diff --git a/tests/unittests.hs b/tests/unittests.hs new file mode 100644 index 0000000..bb3fa79 --- /dev/null +++ b/tests/unittests.hs @@ -0,0 +1,10 @@ +import Prelude + +import Test.Tasty + +import qualified Tests.Example.Project + +main :: IO () +main = defaultMain $ testGroup "." + [ Tests.Example.Project.tests + ]