Processor Project · Week 3 of 11

Single-cycle cpu_core

Released · Due

Provided

Given to you

  • A skeleton cpu_core.
  • Test programs with expected output.

Not given

  • More test programs, which your core is also graded on.

Due from you

Graded for correctness

Graded for feedback

  • Milestone Report.
  • Detailed block diagram for cpu_core, revised to match what you built.

Overview

This week you build your first complete processor: a single-cycle core that runs real ARMv6-M (Thumb) machine code. Each instruction is fetched, decoded, executed, and finished in one clock cycle, so the only state in the core is the PC, the register file, and memory.

This core is the foundation for the rest of the quarter. In weeks 4 and 5 you will pipeline it, and later weeks add a memory bus, a peripheral, exceptions, and a cache. Time spent now on clean module boundaries and readable code will pay off every week after this one.

Goals
  • More open-ended Verilog / multi-module work
  • Demonstrate mastery of v6m ISA
  • Demonstrate mastery of single cycle concepts

Specification

Your core must correctly execute every week 3 encoding in the instruction scope table (levels 0 through 4).

The starter repository's rtl/cpu_core.sv defines the module interface for your core. You will implement this core and all of the modules inside it.

We provide a simplified memory for your core to access, a top-level module that ties these together, an array of test programs, and a testbench that loads code and drives your core.

Your core has two “atypical” output signals to facilitate testing and simulation:

At the end of the day, how you implement your core is up to you, so long as it adheres to the ISA contract. For the moment, we are following the ARM ARM for instruction behavior, but otherwise simplifying a lot of things. More precisely, our testbenches assert that a correct core:

Do not change top_level.sv, simple_mem.sv, program_tb.sv (apart from the debugging example in it), the justfile, or anything in scripts/. We test your core with our copies.

Your test program

Write a test program of your own, assembled by hand into an .imem file in the same format as the provided ones (the README describes it), encoding each instruction yourself from section A6.7. Your program must:

Pick something with a result you can check: summing or copying an array, a checksum, sorting a few values.

For your test program, only data memory is checked, so you don't need to handwrite a .trace (though, it's probably a good idea for you to know how many cycles it is supposed to take, and validate that). Saying it again: Don't generate the expected output by running your core: a test whose answer comes from your core can’t tell you whether your core is right.

Put the program you intend to submit for grading in a folder named “submit”, e.g. test_programs/submit/01_sumArray/. Put exactly one program there: if there is more than one, we grade only the first, in the order ls lists them, and if there is none, you don't earn the test program point. You can (and definitely should) write additional programs to test your core, just don’t put them in the “submit” directory.

Milestone report

Fill in REPORT.md at the top of your repository. The starter has a template with the section headings:

Getting Started

  1. Clone the starter repository and read its README to understand what's in it.
  2. Run just test-program level0/00_justHalt. Your core is already passing one test case, great start!
  3. Run just test-programs. Here's the real picture for the work you have to do.
  4. Revise your block diagram from week 2 into the core you'll actually build, and keep revising it while you work.
  5. Build the core up one level at a time. Prioritize getting a whole instruction through your core end to end.
    • Level 0 needs only the PC, fetch, and enough decode for MOVS (immediate) and NOP.

Testing

The test programs are grouped into the same levels as the instruction scope table, and a level N program uses only instructions from level N or below. Within a level, the programs are sorted by difficulty. just test-programs runs them all; the README lists the other commands.

The provided programs don't use every instruction or check every case, so passing all of them doesn't mean your core is correct. Your core is also graded on test programs you don't have. Test your modules on their own with testbenches of your own (just test <name>), and use your own test programs to try things the provided ones don't.

The test programs can't see your registers or flags directly, only the PC and data memory. Until level 2, a program can only check that your core runs the right instructions in the right order, and halts in the right place. To see your registers and flags while debugging, use the example in program_tb.sv (the README explains).

Submission & Grading

Submit via GitHub.

Grading follows the syllabus's milestone structure, with 5 public points and 5 private points. This week has many test programs, so instead of losing a point per failing program, each level counts as one test: a level earns its point when every program in that level passes. A program passes when its .trace and .after.dmem match exactly.

Graded for correctness

Levels are scored independently, but a level N program can use any instruction from level N or below, so a bug in an early level can cost you later levels too.

Graded for feedback

Extra credit opportunities

Hints & FAQ

Tip: Add some internal monitoring signals and halt aggressively.

E.g., include an output signal decode_error_o in your decoding logic. In the top level, assign halt_o = decode_error_o | ..., and have a waveform view or testbench print that lets you quickly scan to see what error halted your core.

My core passes every provided program. Am I done?

Not necessarily. Go back through the instruction scope table and ask what each instruction could get wrong that no provided program checks, then write tests for it.

Why aren't my registers checked?

How you build your register file is up to you, so the testbench doesn't look inside it. A test program checks a result by storing it to memory, or (at level 4) by branching on it.

This also reflects what's feasible to watch on real cores via tracing (assuming you have the right $2,000 piece of hardware). Very loosely, ARM's Embedded Trace Macrocell (ETM) allows you to monitor which PC is being executed every cycle, and ARM's Data Watchpoint and Trace (DWT) lets you monitor memory bus transactions.

How do I hand-assemble an instruction?

Find its encoding in section A6.7, fill in the fields, and write the 16-bit value as two bytes, low byte first: movs r1, #42 is 0x212A, so it goes in the .imem as 2A 21. Write the assembly in a // comment next to each one, as the provided programs do.

The simulator prints “sorry: constant selects in always_* processes”. Is that a problem?

No. That one is a known Icarus Verilog limitation and the simulation is still correct. Other sorry messages are real errors; the README has details.