diff --git a/.github/workflows/scala.yml b/.github/workflows/ci.yml similarity index 65% rename from .github/workflows/scala.yml rename to .github/workflows/ci.yml index 5f418dc..59fe1f8 100644 --- a/.github/workflows/scala.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Scala CI +name: CI on: push: @@ -21,10 +21,16 @@ jobs: run: | sbt scalastyle sbt test:scalastyle - - name: Run tests + - name: Run unit tests run: sbt test - name: Upload vcd files uses: actions/upload-artifact@v2 with: name: vcds path: ./test_run_dir/**/*.vcd + - name: Set up python for integration test + uses: actions/setup-python@v3 + - name: Install integration test dependency + run: pip install jinja2 + - name: Run integration test + run: make integration_test diff --git a/.gitignore b/.gitignore index 4a458bd..bda096f 100644 --- a/.gitignore +++ b/.gitignore @@ -745,3 +745,5 @@ $RECYCLE.BIN/ *.fir *.v target +.dccache +*.thex diff --git a/.java-version b/.java-version new file mode 100644 index 0000000..b4de394 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +11 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..212a692 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +integration_test: + python3 ./scripts/integration_test/run.py +clean: + python3 ./scripts/integration_test/clean.py + \ No newline at end of file diff --git a/scripts/integration_test/checker.py b/scripts/integration_test/checker.py new file mode 100644 index 0000000..ec48e6c --- /dev/null +++ b/scripts/integration_test/checker.py @@ -0,0 +1,29 @@ +from pathlib import Path +import sys +from jinja2 import Environment, PackageLoader, select_autoescape + + +def intToBin32(i): + return int((bin(((1 << 32) - 1) & i)[2:]).zfill(32), 2) + +def render_program_rom(expected): + env = Environment( + loader=PackageLoader("checker", "templates"), + autoescape=select_autoescape() + ) + template = env.get_template("TopTest.scala.template") + content = template.render(expected=expected) + print(content) + + +def main(): + expected_file = Path(sys.argv[1]) + expected = [] + with expected_file.open('r') as f: + for line in (line.strip() for line in f if line.strip() != ""): + expected.append(intToBin32(int(line))) + render_program_rom(expected) + + +if __name__ == "__main__": + main() diff --git a/scripts/integration_test/clean.py b/scripts/integration_test/clean.py new file mode 100644 index 0000000..7758e9c --- /dev/null +++ b/scripts/integration_test/clean.py @@ -0,0 +1,10 @@ +import os +import shutil + + +shutil.copyfile("./src/main/scala/ProgramROM.scala.backup", + "./src/main/scala/ProgramROM.scala") +shutil.copyfile("./src/test/scala/TopTest.scala.backup", + "./src/test/scala/TopTest.scala") +os.remove("./src/main/scala/ProgramROM.scala.backup") +os.remove("./src/test/scala/TopTest.scala.backup") diff --git a/scripts/integration_test/rom.py b/scripts/integration_test/rom.py new file mode 100644 index 0000000..2190fc6 --- /dev/null +++ b/scripts/integration_test/rom.py @@ -0,0 +1,36 @@ +import sys +from pathlib import Path + +from jinja2 import Environment, PackageLoader, select_autoescape + + +def render_program_rom(address_table, code_table): + env = Environment( + loader=PackageLoader("rom", "templates"), + autoescape=select_autoescape() + ) + template = env.get_template("ProgramROM.scala.template") + content = template.render( + address_table=address_table, code_table=code_table) + print(content) + + +def main(): + lot_file = Path(sys.argv[1]) + address_table = {} + with lot_file.open('r') as f: + for line in (l.strip() for l in f.readlines() if l.strip() != ""): + [section_name, section_address] = line.split(":") + section_address = int(section_address, 16) + address_table[section_name] = section_address + code_table = {} + for section in address_table.keys(): + path = lot_file.parent.joinpath('{}.thex'.format(section)) + with path.open('r') as f: + code_table[section] = [l.strip() + for l in f.readlines() if l.strip() != ""] + render_program_rom(address_table, code_table) + + +if __name__ == "__main__": + main() diff --git a/scripts/integration_test/run.py b/scripts/integration_test/run.py new file mode 100644 index 0000000..6305764 --- /dev/null +++ b/scripts/integration_test/run.py @@ -0,0 +1,44 @@ +import glob +import os +from pathlib import Path +import shutil +import sys + +shutil.copyfile("./src/main/scala/ProgramROM.scala", + "./src/main/scala/ProgramROM.scala.backup") +shutil.copyfile("./src/test/scala/TopTest.scala", "./src/test/scala/TopTest.scala.backup") +fail = False +for filenamestr in glob.glob('test_cases/*'): + filename = Path(filenamestr) + for asmstr in glob.glob(filenamestr + '/*.asm'): + asm = Path(asmstr) + os.system( + "docker run -i shuosc/shuasm < {} > {}" + .format(asmstr, asm.parent.joinpath("{}.thex".format(asm.stem))) + ) + rom = os.popen( + "python3 ./scripts/integration_test/rom.py ./{}/layout.lot" + .format(filename) + ).read() + expected = os.popen( + "python3 ./scripts/integration_test/checker.py ./{}/expected.exp" + .format(filename) + ).read() + with open('./src/main/scala/ProgramROM.scala', 'w') as f: + f.write(rom) + with open('./src/test/scala/TopTest.scala', 'w') as f: + f.write(expected) + return_value = os.system('sbt "testOnly TopSpec" > /dev/null') + if return_value & 0xff00 != 0: + print("Test failed: {}".format(filename)) + fail = True + break + +shutil.copyfile("./src/main/scala/ProgramROM.scala.backup", + "./src/main/scala/ProgramROM.scala") +shutil.copyfile("./src/test/scala/TopTest.scala.backup", + "./src/test/scala/TopTest.scala") +os.remove("./src/main/scala/ProgramROM.scala.backup") +os.remove("./src/test/scala/TopTest.scala.backup") +if fail: + sys.exit(1) \ No newline at end of file diff --git a/scripts/integration_test/templates/ProgramROM.scala.template b/scripts/integration_test/templates/ProgramROM.scala.template new file mode 100644 index 0000000..07e1437 --- /dev/null +++ b/scripts/integration_test/templates/ProgramROM.scala.template @@ -0,0 +1,23 @@ +import chisel3._ + +class ProgramROMBundle extends Bundle { + val address = Input(UInt(32.W)) + val value = Output(UInt(32.W)) +} + +// A temporary program ROM +// Just for testing, use a real flash rom in real world +class ProgramROM extends Module { + val io = IO(new ProgramROMBundle) + {% for section_name, section_content in code_table.items() %} + val {{section_name}}Content = VecInit(Array( + {% for instruction in section_content %}"h{{instruction}}".U(32.W), + {% endfor %} + )){% endfor %} + {% for section_name, section_start in address_table.items() | sort(attribute='1')%} + {% if loop.first %}when(io.address < "h{{"{:x}".format(section_start + 4*(code_table[section_name] | length))}}".U){% else %}.elsewhen(io.address < "h{{"{}".format(section_start + 4*(code_table[section_name] | length))}}".U){% endif %} { + io.value := {{section_name}}Content((io.address - "h80000000".U) (31, 2)) + }{% endfor %}.otherwise { + io.value := 0xdead.U + } +} diff --git a/scripts/integration_test/templates/TopTest.scala.template b/scripts/integration_test/templates/TopTest.scala.template new file mode 100644 index 0000000..a30b3d0 --- /dev/null +++ b/scripts/integration_test/templates/TopTest.scala.template @@ -0,0 +1,38 @@ +import chisel3._ +import chisel3.iotesters._ +import org.scalatest.{FlatSpec, Matchers} + +class TopTest(top: Top) extends PeekPokeTester(top) { + var expected = List( + {% for e in expected %}BigInt("{{e}}"), + {% endfor %} + ) + var last = BigInt(0) + var countDown = 65536 + while (!expected.isEmpty) { + val next = peek(top.io.gpio) + if (next != last) { + expect(top.io.gpio, expected.head) + expected = expected.tail + countDown = 65536 + } else { + countDown -= 1 + if (countDown <= 0) { + fail + expected = List() + } + } + last = next + step(1) + } +} + +class TopSpec extends FlatSpec with Matchers { + behavior of "TopSpec" + + it should "compute successfully" in { + chisel3.iotesters.Driver.execute(Array("--generate-vcd-output", "on"), () => new Top) { top => + new TopTest(top) + } should be(true) + } +} diff --git a/src/main/scala/ProgramROM.scala b/src/main/scala/ProgramROM.scala index c1632f1..c7016ad 100644 --- a/src/main/scala/ProgramROM.scala +++ b/src/main/scala/ProgramROM.scala @@ -9,47 +9,16 @@ class ProgramROMBundle extends Bundle { // Just for testing, use a real flash rom in real world class ProgramROM extends Module { val io = IO(new ProgramROMBundle) - val content = VecInit(Array( - "h00004fb7".U(32.W), // li t6, 0x4000 - "h000f8f93".U(32.W), - "h01200513".U(32.W), // li a0, 0x12 - "h00afa023".U(32.W), // sw a0, 0(t6) - "h00000513".U(32.W), // li a0, 0x0 - "h00afa223".U(32.W), // sw a0, 4(t6) - - "h80020f37".U(32.W), // li t5, 0x80020000 - "h00100293".U(32.W), // li t0, 1 - "h00200313".U(32.W), // li t1, 2 - "h006303b3".U(32.W), // add t2, t1, t1 - "h005383b3".U(32.W), // add t2, t2, t0 - "h10012e37".U(32.W), // li t3, 0x10012000 - "h000e0e13".U(32.W), - "h007e2023".U(32.W), // sw t2, 0(t3) - // label - "h007f2223".U(32.W), // sw t2, 4(t5) - "h004f2e03".U(32.W), // lw t3, 4(t5) - "h01c38eb3".U(32.W), // add t4, t2, t3 - "h007eeeb3".U(32.W), // or t4, t4, t2 - "h10012e37".U(32.W), // li t3, 0x10012000 - "h000e0e13".U(32.W), - "h01de2023".U(32.W), // sw t4, 0(t3) - "h00138393".U(32.W), // addi t2, t2, 1 - // j label - "hfe1ff06f".U(32.W))) - - val interruptContent = VecInit(Array( - "h000fa503".U(32.W), // lw a0, 0(t6) - "h00a50533".U(32.W), // add a0, a0, a0 - "h00afa023".U(32.W), // sw a0, 0(t6) - "h100125b7".U(32.W), // li a1, 0x10012000 - "h00058593".U(32.W), - "h00a5a023".U(32.W), // sw a0, 0(a1) - // mret - "h30200073".U(32.W))) - - when(io.address < "h80010000".U) { - io.value := content((io.address - "h80000000".U) (31, 2)) + + val codeContent = VecInit(Array( + "h10012537".U(32.W), + "h00100593".U(32.W), + "h00b52023".U(32.W) + )) + + when(io.address < "h8000000c".U) { + io.value := codeContent((io.address - "h80000000".U) (31, 2)) }.otherwise { - io.value := interruptContent((io.address - "h80010000".U) (31, 2)) + io.value := 0xdead.U } -} +} \ No newline at end of file diff --git a/src/main/scala/Top.scala b/src/main/scala/Top.scala index 0317b92..301680a 100644 --- a/src/main/scala/Top.scala +++ b/src/main/scala/Top.scala @@ -18,3 +18,7 @@ class Top extends Module { cpu.io.programROMBundle <> programROM.io cpu.io.timerInterruptPending := dataBus.io.timerInterruptPending } + +object TopDriver extends App { + (new chisel3.stage.ChiselStage).emitVerilog(new Top, args) +} \ No newline at end of file diff --git a/src/test/scala/TopTest.scala b/src/test/scala/TopTest.scala index 1b0b4a7..acfc528 100644 --- a/src/test/scala/TopTest.scala +++ b/src/test/scala/TopTest.scala @@ -3,26 +3,27 @@ import chisel3.iotesters._ import org.scalatest.{FlatSpec, Matchers} class TopTest(top: Top) extends PeekPokeTester(top) { - for (i <- 0 to 15) { + var expected = List( + BigInt("1") + ) + var last = BigInt(0) + var countDown = 65536 + while (!expected.isEmpty) { + val next = peek(top.io.gpio) + if (next != last) { + expect(top.io.gpio, expected.head) + expected = expected.tail + countDown = 65536 + } else { + countDown -= 1 + if (countDown <= 0) { + fail + expected = List() + } + } + last = next step(1) } - expect(top.io.gpio, 5.U(32.W)) - for (i <- 0 to 10) { - step(1) - } - expect(top.io.gpio, 0x24.U(32.W)) - for (i <- 0 to 5) { - step(1) - } - expect(top.io.gpio, 0xa.U(32.W)) - for (i <- 0 to 12) { - step(1) - } - expect(top.io.gpio, 0x48.U(32.W)) - for (i <- 0 to 5) { - step(1) - } - expect(top.io.gpio, 0xc.U(32.W)) } class TopSpec extends FlatSpec with Matchers { diff --git a/test_cases/basic_math/code.asm b/test_cases/basic_math/code.asm new file mode 100644 index 0000000..b97dd0c --- /dev/null +++ b/test_cases/basic_math/code.asm @@ -0,0 +1,24 @@ +main: + li a0, 0x10012000 + sw zero, 0(a0) + li a2, 1 + li a3, 2 + add a2, a2, a3 + sw a2, 0(a0) + li a2, 3 + li a3, 2 + sub a2, a2, a3 + sw a2, 0(a0) + sub a2, a2, a3 + sw a2, 0(a0) + sll a2, a2, a3 + sw a2, 0(a0) + sra a2, a2, a3 + addi a2, a2, 1 + sw a2, 0(a0) + addi a2, a2, -2 + sw a2, 0(a0) + slli a2, a2, 3 + sw a2, 0(a0) + srai a2, a2, 2 + sw a2, 0(a0) diff --git a/test_cases/basic_math/expected.exp b/test_cases/basic_math/expected.exp new file mode 100644 index 0000000..64fa50a --- /dev/null +++ b/test_cases/basic_math/expected.exp @@ -0,0 +1,8 @@ +3 +1 +-1 +-4 +0 +-2 +-16 +-4 \ No newline at end of file diff --git a/test_cases/basic_math/layout.lot b/test_cases/basic_math/layout.lot new file mode 100644 index 0000000..2befb46 --- /dev/null +++ b/test_cases/basic_math/layout.lot @@ -0,0 +1 @@ +code: 0x80001000 \ No newline at end of file