Coding for Reuse Course - Module 2 Lesson 5
Iterative Compilation and the Instruction Decode and Sequencing Block Part 2.

In this lesson we will study the sequencer block in detail. At the conclusion of this part of the lesson you will have a microprocessor that can execute some of the basic instructions.

License Terms  

The source and/or object code (subsequently referred to as code) presented in this and subsequent lessons is the property of saxelec.com and owner who retains copyright protection. This code in whole or in part is licensed only for single copy per user, non-commercial use; except in the case of educational institutions, where a license of single copy per student, only as part of course curricula, is permitted.

Donald Gerard Polak (owner)

Introduction  

The sequencer block consists of a microcode state machine that coordinates the actions of all of the other microprocessor blocks, an instruction latch which retains the fetched instruction until its execution is complete, a flag mask decoder that generates a distinct flag mask for each instruction, and an expander that creates a large number of control signals based upon the microcode state and other conditions. The instruction latch and flag mask decoder were addressed in lesson 4. The state machine and expander, however, are closely coupled. Therefore, they must be discussed as a single unit.

When the microcode state machine begins its sequence it must examine a variety of microprocessor conditions that may exist at that moment and decide the appropriate course of action. Therefore, the state machine must employ priority encoders in its earliest states to avoid delays.

Priority Encoders  

A priority encoder is a logical construct which assigns levels of importance to various stimuli. The priority encoder then generates an output appropriate to the condition that must be "recognized" first. Priority encoders are logic intensive and can easily be unintentionally encoded, so most compilers will generate a warning, and some an error, when one is encountered.

Priority encoders are intentionally or unintentionally encoded in a variety of ways, however the use of the 'else if' statement is the most frequent:

  1. Sequential logic priority encoder using the 'else if' statement.

        if <condition> begin
          .
          .
        end
        else if <condition> begin
          .
          .
        end
        else begin
          .
          .
        end

  2. Corrected version not using priority encoding:

        if <condition> begin
          .
          .
          if <condition> begin
            .
            .
          end     end
        else begin
          .
          .
        end

Microcode State Machine Organization  

The microcode state machine opens with a large number of definitions to allow states to be referenced by name rather than trying to remember large bit patterns. These defines also make it feasible to quickly alter code execution.

The microcode state machine uses common states, that is a state that is shared by all instructions or groups of instructions, and specific states that are used by one instruction only. The microcode state machine begins in its idle state, where it must examine the halt register and the interrupts before beginning to fetch instructions. The instruction fetch also employs common states, as the microcode state machine must request an instruction fetch cycle from the bus state machine, ascertain that the request is being processed, increment the program counter, and enable the instruction latch. It directs these functions via the state bits in the coded fields on each clock cycle.

Once the instruction is fetched, the microcode state machine can either use specific states, or more common states, based upon the instruction. The additional common states will be followed by specific states, and some specific states can be followed by more common states. After the instruction fetch, all instructions require the examination of the contents of the instruction register in order to determine the next state transition.

Getting Started - Instruction Fetch  

Let's begin coding our microcode state machine and add the instruction fetch.

Project Skeleton with Preliminary Microcode State Machine and Instruction Fetch

The "default" case allows any unintended or uncoded states to terminate the current instruction and reset the state machine. We will see later how we use it to force uncoded instructions to perform a "NOP"

The interrupts are currently defaulted to the idle state to allow us to compile the code. The 'intp' input must not be set to a one until the interrupt block is coded, or the microcode could become locked in an idle state.

You will notice that the microcode states are defined as sets of bit fields. This format allows you to easily encode and understand the states. Also remember that the state bit pattern is asserted immediately on the clock edge that satisfies the requirements of the transition. Therefore, the conditions to increment the program counter are setup during the clock edge of 'IFETCH2' and the increment actually occurs or the first clock edge of 'IFETCH3'.

Even though Lesson 2 Module 4 refers to this method as a particularly dangerous thing to do, we rely on a combined fields approach to minimize the state machine width. Since the 'mem' destination always uses only the 8 least significant bits, we can use 'ude' and 'lde' to virtually expand the width of the destination field when we are using a 'mem' destination. In this case, the use of both 'ude' and 'lde' with the 'mem' destination allows us to enable the instruction latch. We will also use similar methods, in the future to allow field expansions without increasing the width of the state machine. These combined field approaches must be carefully researched in advance, as explained in Lesson 2 Module 4, but they may also help us to reduce the number of required differentiator bits. Reducing the state machine width will significantly improve speed and reduce logic requirements.

A Few Simple Instructions  

As stated previously, in order to execute the instructions, we must decode the contents of the instruction latch. We chose a transparent latch for the instruction latch to allow us to begin decoding the instruction as soon as the instruction data is stable and valid, in our case this occurs in microcode state 'IFETCH3' when 'dack' is asserted.

Initial instruction decode begins by using a 'case' statement acting upon the contents of the instruction latch. This case statement allows you to execute all instructions, but for now we will concentrate on single cycle instructions only. Multi-cycle instructions will be addressed in later lessons. Don't worry, Verilog allows the nesting of 'case' and 'if' statements

So let's try adding some simple, single-cycle instructions to our microprocessor.

Project Skeleton with Preliminary Microcode State Machine and Some Simple Instructions

You will notice that the instructions, so far, are decoded and acted upon in the IFETCH3 state. Actually all instructions must be decoded, and a state transition occur for that instruction, during IFETCH3; which (if our architecture is correct) should correspond to the rising 'edge of rd_n (from the bus state machine perspective). This leaves us one and one half sysclk cycles before the next bus cycle should occur. One half sysclk cycle will be consumed by our next bus request posting:

Timing between bus and microcode state machines.

Since the bus state machine is setup to handle 'early' request postings, the microcode state machine does not have to worry about 'sysclk' timing. After instruction completion, the next bus request is posted from the 'MIDLE' state, which is entered via the 'default' microcode case state for now.

Each instruction will require a state transition after 'dack' is recognized in microcode state 'IFETCH3'. Some of these state transitions will be common states, while others will be specific states. You can notice, for example, that all "No Operation" instructions vector immediately to the 'MIDLE' microcode state; while the operational instructions (thus far) have an associated specific state. These specific states are then transitioned to the "MIDLE" state on the next clock cycle by the default statement at the end of the microcode case statement.

To code an instruction in 'IFETCH3' you must first determine an existing, or create a new 'parameter' statement to setup a microcode state for that instruction; which, in turn, sets up the necessary logic to perform that step of the instruction execution. Then you must create an 'instruction' case statement for that instruction within the 'IFETCH3' microcode state which invokes that microcode state.

You may notice that a number of single cycle instructions were left out of the instructions that I coded or left for you to code. The SIM, RIM, IE, DI, 'DAA', and 'XCHG' instructions do not yet have all the architecture in place to support them, and thus must be addressed in different lessons.

By now you've probably realized that it would be useful to keep track of all the coded and uncoded instructions. I generally favor importing the contents of the parameter file into a word processor, such as 'WORDPAD', trimming all except the instruction codes parameters, and then using the 'FONT' format 'STRIKEOUT' characteristic for the instructions I've already completely coded. Since I wanted to include instruction cycles and flags, I created a table in FrameBuilder and colored in the coded instructions.

Project Tracking with Coded Instructions Colored in

A piece of paper works good too.

Microcode Expander  

The microcode state machine is not very useful until the states that it generates can be translated into the various control signals required by the other modules. Since there are many more control signals required by the modules then there are state bits out of the micro-code state machine, the expander de-multiplexes the various fields of the current microcode state into these control signals.

This statement automatically implies two things:

  1. The expander block requires the same field definition parameters as the microcode block. According to Rule 6.6 at least some of the parameters from "microcode_r" need to be moved into the parameter file.

  2. The expander works best upon fields. This implies that the microcode state presented to the expander must either:

    1. Be conveyed as a set of fields.

    2. Have a set of "`define" statements to delineate the various fields. These "`define" statements consist of text replacements such as:

        `define CREQF 23
            .
            .
            .
        assign creq = microcode[`CREQF];

I personally favor the former approach, since changes in the microcode structure will only affect the microcode state machine itself, while using the definition style would cause both the microcode state machine and the expander modules to be affected. Also, since "`define" statements are global in scope, there is potential conflict with other modules. Generally, it is best practice to use "`define" statements only for items that are global in nature or that affect several modules; not for interaction between two modules only.

1. Parameters
    

When I chose the parameters for the various fields in the microcode state machine, I did so with minimizing the number of inputs required for each expander output signal in mind. For example:

  • The operational states that require the use of source complement, with the exceptions of DECOP and DECNF, are grouped so that bits 4, 3, and 2 will trigger comse whenever they are 3'b001.

  • Fields that can be mapped directly to signals are coded in such a way as two allow direct mapping, without the use of logic.

  • The expander does not require the use of the differentiator bits either, since they only affect the microcode state machine itself.

For this reason, some of the parameters may change from time to time during the remainder of this course, as we are designing this microprocessor together, and I haven't tried out all of the new code yet.

    

So let's start our coding by moving our field parameters into the parameter file:

Parameter file with microcode field parameters added

2. Incorporating Fields
    

Next we must make changes in microcode_r to remove the parameters that were just moved into the parameter file, break the microcode into separate fields, alter the module and output declarations to reflect the change from outputting micstate "en-masse" to outputting the separate fields. We must also alter sequencer_r to incorporate these changes in the interconnect between microcode_r and expander_r.

3. Encoding
    

The expander can now be encoded. The expander is essentially a sea of non-sequential logic. Each output from the expander must be coded in the most efficient manner possible. One technique uses the field parameters directly and lets the compiler decide the most efficient coding. Another technique involves writing the actual logic equation for the output. Wherever possible, direct mapping is used. During the development of the expander, we will periodically modify the field parameters for the microcode to optimize this block.

Compiling and De-bugging  

Finally, we will make all corrections in any other modules that are required to get the full design to compile.

1. Synthesis Corrections
    

To make the corrections just mentioned, start with the errors. The errors are usually the result of typo's and are generally easily corrected. The next step is harder. You must examine the warnings. On a new project such as this, they can number in the hundreds (about 900 on my first synthesis). The windows presentation of the synthesis log may prove difficult to use at this stage. For most tools, the synthesis log file is a text file and can be opened with most text editors. I prefer to use jedit, because it allows me to open the source file and the synthesis log simultaneously. Focus on the warnings that mention 'unassigned' values and the ones that result in logic removal. These occur frequently because of undeclared signals or connections. A single correction at this stage can result in several (or sometimes hundreds) of warning removals. Periodically re-synthesize your design while making these corrections to determine your next step. In the end, you may still be left with a significant number of warnings that are the result of un-coded blocks, but it should be less than 100.\

2. Compile Preparations
    

To allow the code not only to synthesize, but to also compile we must remove any occurrences of "black boxes". A black box only sinks signals and does not source any. We use them early on in the development cycle as place holders for later logic. The only black box we are currently left with in this project is "interrupt_r". Since we know that signals such as "intp", "xint", and "mask" must originate from "interrupt_r", we can eliminate its black box status by assigning those signals to default values within that module and connecting them to the project.

You must also do some settings in your compiler. Since the expander uses the fields from the microcode state machine directly, you must disable the options that allow the compiler to change that encoding, such as "FSM expansion", FSM optimization", "Gray code encoding", etc.

Executable Code  

Congratulations. We have now reached a point where we have a 16MHz calculator that can add, subtract, increment, decrement, and perform some basic logic instructions; but do very little else. However, we have most of the guts in place to do a whole lot more.

First Executable Project Skeleton

I want to call your attention to a particular coding technique used in "expander_r" for "adde". As I explained in Rule 3.9 we must plan for a 4 input single output style logic cell. This constraint may seem very limiting at first, until you start employing this technique. In two logic levels you can theoretically achieve a single output that is the result of sixteen inputs. However, it seldom works so cleanly. Some terms, tend to be dominate, that are they are combined with an assortment of other terms, while others are subordinate. Therefore, the achievable function input levels trend downward somewhat. But actual fanins for two logic levels can generally reach 8 - 12. In our case, the fanin is only five.

Simulation  

Now that we have enough infrastructure in place, we can start simulating our project. However, we must update our test bench first. First of all, we will need some sort of ROM to supply the instructions during instruction fetch. We will also need some sort of address decoding, so that addresses will select the proper memory device, according to the memory map. In our project, I setup a memory map that references the ROM during the first 2K of memory addresses and RAM for any other memory address. We don't need the RAM just yet however.

In the real world, ROM is fairly slow, so I implement a wait state during ROM access by setting up a delay line that sets ready low for a single sysclk cycle.

Finally, we need to change the source select mux in the test bench to drive the ROM output onto the AD bus during read.

I wrote a few instructions into the ROM for demonstration purposes. I did not code the entire ROM, but if you're energetic feel free to do so. With that being said, here is my new test bench:

Updated Test Bench

I simulated our project with IVERILOG. First I created a directory for simulation. I then copied the source files into that directory and renamed them from the command prompt:

ren s8085d_skel2_r.v.txt s8085d_r.v
ren s8085d_t_pre2.v.txt s8085d_t.v
ren s8085d_p_pre4.v.txt s8085d_p.v
ren s8085d_t_d.v.txt s8085d_t_d.v

I added the IVERILOG binary path to my system environment PATH variable. I then launched the COMMAND PROMPT and changed my directory to my simulation directory. From there I entered the following commands:

iverilog -o s8085d_t.out s8085d_t.v
vvp s8085d_t.out -lxt

When vvp encounters the $stop directive it does not automatically exit. Instead, it will present a caret ('>') prompt and await further commands. I enter 'finish' at this prompt to exit vvp.

We can now use GTKWAVE to view the waveform. I entered the following command:

gtkwave s8085d_t.lxt

From the gtkwave window, I added the clock, and most external bus signals to my display so you can view the actual instruction bus cycle. I also added most registers and the instruction latch so you can see the results of the instructions:

GTKWAVE Display of Instruction Fetch

GTKWAVE Display of Instruction Results

Exercise  
  1. What is a "Priority Encoder"?

  2. Do you have to intentionally encode a "Priority Encoder"?

  3. Why did we need a transparent latch for the instruction latch?

  4. What must happen after "dack" is asserted?

  5. Encode the "left for you" instructions in microcode_r.

  6. What steps did you have to perform? Where did you put them?

  7. Why did we break the microcode into fields?

  8. Describe the microcode expander.

  9. How many inputs are theoretically possible with a two level logic function?

  10. What items should you focus on to get your project to compile and synthesize?