Approaches to Control Logic Design

Author: Craig Maiman – Principal RTL Design Engineer

1.   Introduction

Successfully designing control logic is a mixture of understanding the design requirements (e.g., protocols, timing, area, power consumption, etc.), experience and creativity.  Datapath architectures may have only one or just a few different ways to approach them and similarly control logic can be approached from several different angles which I’ll discuss in this paper.  But, at the end of the day, it’s usually simplicity that you should strive for.  The more convoluted the control logic, the more prone it’ll be to having obscure bugs that are hard to fix.

2.  General Approaches

Control logic is used to do many different things: Interface protocols, IP interfaces, CPU interfaces, datapath control, memory control, pipeline control, arithmetic control and other sequencing and state tasks.  The two general approaches are state machines and random logic.  Often, what should be used for the various parts of the control design is obvious (e.g., some bus protocols demand a state machine approach, or the block requires state be maintained), but not always.  Many designs can be done almost completely with state machines, just random logic, or a combination of the two (which is usually how it naturally ends up).

One thing I’ll say upfront is that your first attempt at designing the control logic shouldn’t necessarily be the last.  Consider it a first draft that could be completely redone if you see a better way or bugs are making it more complicated than you feel comfortable with – I always find that if the control logic seems to have too many bugs, then it’s entirely possible the approach taken was wrong and I won’t hesitate to completely rearchitect and rewrite the control logic (or maybe the datapath needs rethinking too).

Sometimes you can do a quick-and-dirty control design to get higher-level verification going, but then do a more efficient re-design.  For example, your first design might be completely state machine based as that can usually be put together more quickly, but then re-done with more random logic to be more efficient (i.e., less area, power, complexity).

3.  Diagramming

While a lot of control logic can be directly coded, sometimes the complexity requires that a protocol diagram be drawn.  While I’ve often attempted to bypass that step to get to the coding, I usually end up going back and drawing it out because my initial attempt was buggy (e.g., cycle counts wrong, not all conditions considered for state transitions, pipeline backpressure not handled for all cases).

This is an example of the type of protocol diagram that I’m referring to:

The basic idea is: What causes what (for all the cases that may occur).  Resist the temptation to skip this step because you’ll save yourself a lot of grief down the road.

4.  State Machine Design

Back in the earlier days of control logic design, designing a state machine was a multi-day affair of figuring out the states, the transitions, the state encoding, designing the logic and drawing out the gates (I don’t miss those days).  Now, it’s short work once you figure out the states and the transitions, to coding the result.  Now you’ll spend more time on the higher-level control architecture and less on the implementation.

Some of the decisions you need to make are whether the control is one state machine or multiple (it’s not always obvious).  An example would be multiple requesters to a common resource.  Is it one state machine that is oriented toward the resource or is it multiple state machines oriented toward the requesters with a final state machine that does the arbitration and resource access?  The answer will usually depend on the protocols of the requesters and resource to determine what makes more sense.  Perhaps the requesters have posted writes, so state will have to be maintained per requester.  Case-by-case basis…

You must determine what to maintain as state and what to separate out as random logic (to some degree as much art as engineering).  The bottom line is to make it be efficient (however you want to measure that) and understandable.

The importance of understandability can’t be overstated.  You (or someone else), at some point, will have to revisit the logic (to add a feature or fix a bug) and you’ll need to understand how it works.  This is achieved by having a logical flow, meaningful state and signal names and extensive commenting.  Comment your code!  What seems obvious now to you, may not seem so later.  For each state and transition, explain what and why.  For each control signal explain the logic.

State Machines come in two fundamental flavors, Moore and Mealy (though I don’t think most engineers think about them in those terms much anymore).  But they are different ways of approaching control logic.

Of course, your output control signals may be a combination of these two approaches, with some signals being a straight state decode (Moore) and some being state and inputs (Mealy).  The goal being to minimize the number of states, and maximize efficiency (e.g., minimum number of cycles to achieve some function), while also making it understandable.

Synthesis tools typically recommend you have two separate always blocks for the combinatorial and registered portions of the state machine.  You can put control outputs in the combinatorial always block, but I’ll typically separate those out to keep the state machine code simpler and easier to read.  Synthesis engines have specialized optimizations that are just for state machines, so I like to keep that code as streamlined as possible so the synthesis can produce the best possible results (also, don’t be tempted to hardcode an encoding such as one-hot.  The synthesis tools these days will almost certainly do a better job of figuring out the best encoding). They also recommend that the states be enumerated type and not to assign states to specific encodings because we want the synthesis engine to have the most flexibility in optimizing the code.

Some example state machine and control signal code would look like this:

      enum logic [1:0] {IDLE, MEM_RD, EXEC, MEM_WR} state, next_state;

       // State Machine

       always_comb begin

              unique case (state)

                    IDLE : begin

                           if (go)          next_state = MEM_RD;  // Operation beginning

                           else             next_state = IDLE;

                    end

                    MEM_RD : begin

                           if (data_avail)  next_state = EXEC;   // Wait here until…

                           else             next_state = MEM_RD;

                    end

                    EXEC : begin

                           if (op_complete) next_state = MEM_WR;

                           else             next_state = EXEC;

                    end

                    MEM_WR :                next_state = IDLE;

                    default :               next_state = IDLE;

              endcase

       end

       // State Machine Register

       always_ff @(posedge clock) begin

              if (reset) state <= IDLE;

              else       state <= next_state;

       end

// Control Signals (these outputs are dependent on the state and inputs, so are Mealy type control signals – make sure the timing is ok for this)

       always_comb begin

              mem_rd     = 1’b0; // Default assertions

              execute_op = 1’b0;

              mem_wr     = 1’b0;

              unique case (state)

                     IDLE :   mem_rd     = go;

                    MEM_RD : execute_op = data_avail;

                    EXEC :   mem_wr     = op_complete;

                    default : begin

                           mem_rd     = 1’b0;

                           execute_op = 1’b0;

                           mem_wr     = 1’b0;

                    end

              endcase

       end

The above control signals could be coded as simply state decodes too (Moore), though that could affect the latency.  The control signals could also be within an always_ff block to register the control signals.  It’s all about the trade-offs of simplicity, timing, latency, etc.

5.  Random Logic Design

Random control logic is control logic that is not part of a formal state machine.  Your final control logic design will almost certainly include such logic.  The trick is deciding what should be random logic and what should be part of a state machine.

I can’t give you a quantitative measure as to what should be random logic and what should be in a state machine, but my general approach is that if what I’ve written as random logic portion is getting overly complicated (or I’m finding more bugs than I like and the logic is getting too complex because of the fixes), then I’ll rethink the whole approach and consider redoing at least some of it as a state machine.

In terms of style, of the two ways to write combinatorial logic, assign statements or always_comb blocks, I tend to use always_comb blocks more often.  Unless the expression is simple, an always_comb block is usually more readable.  In particular I find conditional assign statements hard to read vs. an equivalent always_comb statement (I do use them if it’s simple, otherwise, no).  And while you can do it, please don’t nest conditional assign statement as they’re near impossible to decipher – instead use an if/else or case statement inside an always block.

Strangely, I’ve often seen the following kind of conditional statement:

          assign foo = (x == y) ? 1’b1: 1’b0;

That doesn’t need to be a conditional assignment and can be simplified to:

       assign foo = (x == y);

6.  Pipeline Control

It’s very common in digital design, that if you’re moving and/or operating on data, to have pipelines to meet frequency and throughput goals.  You’re trading latency for throughput, which is usually a good tradeoff.

Another common aspect of such designs is that one block which has a pipeline is interfaced with another block, which may or may not have its own pipeline.  Such an interface will often have a Ready signal indicating whether the downstream logic can accept new data.  If not, it de-asserts Ready to apply backpressure to the upstream block.

The question is, how do you handle this?  How do you stop the pipeline (i.e. Also known as a pipe stall)?  It may not be practical from a timing standpoint to try and directly stall the entire pipeline – maybe it’s deep with a wide datapath.  If you directly stall the whole pipeline, how does that effect logic which is further upstream?

A common solution is to add a FIFO at the end of the pipeline and make it of sufficient depth to store all (or more) of the “results” from the entire pipeline.  So, if the block gets backpressured, you stop reading from the FIFO and the results backup into it.  The Ready signal can leapfrog the pipeline to stall the input stream to the pipeline.  The input stream stops and the pipeline drains into the output FIFO.  This way, there is no need to stall the pipeline, you are just letting it drain into the output FIFO.  It stays there until the backpressure is relieved and pipeline can restart.

You can optimize it a bit further if you don’t want to incur the added pipe delay of a FIFO by doing some muxing of the output, as shown in this example design:

The output FIFO is only used if backpressure is being applied via the ready_in signal being deasserted.  Otherwise, the data passes the FIFO by, so you don’t incur the cycle delay.

7.  Using Higher-Level Abstractions

Higher-level abstractions refer to the usage of enumerated and struct types of SystemVerilog and using these types can improve the readability of the code, improve synthesis results by giving the synthesis engine more flexibility in optimizations and enable more efficient coding (and maintainability).

We already discussed using enumerated types for state machines, but they can also be used for any other information which needs to be passed and there’s more than two states.  For example, a request and reply interface may have several types:

          Request: Write, Posted Write, Read, ReadClear, etc.

          Reply: WriteOK, ReadOK, ReadErr, ReadClearOK, etc.

This is an obvious case to use an enumerated type for the Request and Reply signals:

          enum logic [2:0] {NOP, WRITE, WRITE_POST, READ, READ_CLEAR} Request;

If an enumerated type will be used by several signals, you could define your own type with typedef:

          typedef num logic [1:0] {IDLE, MEM_RD, EXEC, MEM_WR} reqtype_type;

And then use the type to define signals:

          req_type    mem_req, proc_req;

If that type will be used across more than one module, you could put the typedef in a package and import that package in each module where it is used.

Struct types are also very useful for coding efficiency and readability.  For example, in a request interface, instead of having separate signals for valid, request type, address, and write data, you can have a single struct that encompasses all of those in one signal between the requester and the resource.  This is quicker to code and easier to make changes down the line if it’s necessary that more information, or new encodings, are needed in the interface.

A struct for a request interface could look like this (assuming you’ll use this in several modules, we can create a type):

       typedef struct {

              logic valid;

              reqtype_type req; // This type defined as shown above

              logic [31:0] addr;

              logic [31:0] wdata;

       } req_type;

Then in the source-side module interface definition you can have:

          output req_type  requester,

To do assignments to the components of the struct, you just use “.” An example, inside an always_ff could be like this:

                    requester.valid <= 1’b1;

              requester.req   <= MEM_RD;

You can also test struct elements just as easily:

          if (requester.valid && requester.req == MEM_WR)…

Sometimes you need to know the exact width of a struct.  For example, if the struct above will be input into a FIFO you need to know the width of it to size the width of the FIFO.  To do that properly you need the struct to be of predictable width.  The default for structs is to be “unpacked”, meaning that the compiler can have gaps between the struct members.  To prevent that you need to declare the struct as packed.  The example above would become:

       typedef struct packed {

              logic valid;

              reqtype_type req; // This type defined as shown above

              logic [31:0] addr;

              logic [31:0] wdata;

       } req_type;

Now you can calculate the width of the struct by just adding up the widths of the individual components.

8.  Verification of Control Logic

The designer will typically want to do unit-level verification and for the best coverage of the functionality that will consist of two types: Directed and Randomized.

For the directed tests you’ll have deterministic tests that are designed to hit all the states in your state machines and toggle all the control signals.  You want these tests to be exhaustive in terms of going through all the possible transitions in your state machine(s) and any corner cases such as unusual combinations of states or signals that you think might not be hit too often.

With randomized tests you want to design your testbench to be able to control the inputs to your blocks in a random way.  Random requests/accesses (or whatever your incoming interface may be), random spacing between accesses, randomized address/data busses, etc.  Perhaps even different combinations of frequencies if the design has multiple clocks.  The more interfaces you have into your control logic, the more interesting and valuable all of this becomes.  What you’re trying to do is to exercise the logic in ways that the directed tests may not hit upon.

Make your testbenches to be as self-checking as possible, so that you can run the randomized tests as long as you wish, without having to hand-check anything.  And make the self-checking be thorough in terms of checking the signals/busses coming out of your block vs. what is expected.  Check the outputs, data, protocol, etc.  Whatever you can think of to check, check it.  You might also instrument the testbench to measure any important performance metrics vs. expectations.

If you have a coverage tool available, use that to see how well your tests cover your control logic – All states, state transitions and all random control logic stimulated.

9.  Documentation

When writing a microarchitecture specification, I usually do not include implementation-level details of the control logic, such as state diagrams.  There’s one reason why I would include a state diagram and two reasons why I won’t.

If the design’s intent is to implement a specific protocol that includes states (e.g. an Ethernet MAC), or the main purpose of the block is state control, then I will, of course, document that state diagram.  It’s an important part of the design, so it should be documented.

The two reasons I will not show state diagrams or write detailed descriptions of the underlying operation of the control logic are:

  1. Microarchitecture specifications are not implementation specifications.  They are written to describe the functionality of the block, not the low-level details of how it will be designed.  The control logic (e.g., state diagrams) are decided when the design is implemented, which is after the microarchitecture specification is written and accepted.  Of course, you will likely include a block diagram showing the datapaths, but that wouldn’t include details of the control logic.
  2. One of the main readers of a microarchitecture specification are the verification engineer(s).  It’s their job to verify the architectural functionality of the block and you don’t want to bias them to test how you implemented the control of the block.  You don’t want them testing that your state machine is going through all the states (you did that with your unit-level testing to verify the design is doing what you expect), you want them to test the “black-box” architectural functionality as seen, and expected, by the higher-level architecture/block and other interfacing blocks.

10.  Example Control Logic Design

As an example of how you can approach control design multiple ways consider the following design of a matrix multiplier:

In this design the two matrices are loaded via the Ready/Valid interface into two memory blocks.  Once that is complete a register is written in the CSR block which starts the multiply operation.  When the multiply is complete, another CSR register is written indicating that status.

As the operands are streamed out of the two source memories, the calculations are done in the accumulator and written into the result memory as they become available.

Since this was supposed to be a quick and dirty test of the memories (on a very tight schedule), I quickly put it together with a single state machine controlling the whole operation.  I also wrote a quick testbench to test the operation, which proved out the testbench and the design.

After getting that working I realized that I could get better performance (and better test the memories) if I separated the source control and result control.  It would take a bit longer to design, but I saw that I could double the performance.  So, a few more days of work and I got it working again.  While I usually favor state machine-oriented control logic, in this case I went from one state machine to two separate groups of random control logic with no state machines, because in this case it was straightforward control logic that didn’t really need a state machine.

The point here is, that oftentimes it is worth doing a control design over if you think it better meets the goals.  You should consider your first design a draft (which might help get verification going sooner) and be open to a complete rewrite if it yields you better performance (however you define that).

11.   Summary

When designing control logic, you have three basic goals: Meeting performance (i.e. speed, area, power, etc.), functionality, and sustainability goals.  Meeting the first two are obvious, but you should design in such a way that the design can be easily fixed or enhanced by someone other than yourself.

To make a design sustainable, you need to architect and structure the design in such a way that is easily understandable (i.e.  I’ve never been impressed by designs or designers that are overly clever to the point of obscurity).  The style of coding also plays into understandability.  Use a consistent and clear style such as using abstractions that make the intent clear.

And don’t forget to comment your code!

XtremeEDA is an experienced partner you can trust!!

Cadence Design Systems helps engineers pick up the development tempo. A leader in the market for electronic design automation (EDA) software, Cadence sells and leases software and hardware products used to design integrated circuits (ICs), printed circuit boards (PCBs), and other electronic systems. Semiconductor and electronics systems manufacturers use its products to build components for wireless devices, networking equipment, and other applications. The company also provides maintenance and support, and offers design and methodology consulting services. Customers have included Pegatron, Silicon Labs, and Texas Instruments. Cadence gets more than half of its sales from customers outside the US.

Synopsys, Inc. (Nasdaq:SNPS) provides products and services that accelerate innovation in the global electronics market. As a leader in electronic design automation (EDA) and semiconductor intellectual property (IP), Synopsys’ comprehensive, integrated portfolio of system-level, IP, implementation, verification, manufacturing, optical and field-programmable gate array (FPGA) solutions help address the key challenges designers face such as power and yield management, system-to-silicon verification and time-to-results. These technology-leading solutions help give Synopsys customers a competitive edge in quickly bringing the best products to market while reducing costs and schedule risk. For more than 25 years, Synopsys has been at the heart of accelerating electronics innovation with engineers around the world having used Synopsys technology to successfully design and create billions of chips and systems. The company is headquartered in Mountain View, California, and has approximately 90 offices located throughout North America, Europe, Japan, Asia and India.

asicNorth was established in January 2000 with one purpose in mind: deliver the highest quality design services possible. In an industry that can be quite volatile at times, it is important to have a design partner that you can depend upon to deliver the skills you need when you need them. A project can only be successful if there are:

Top quality skills on the team
Communication with the customer
Attention to detail
Cost sensitivity
Focus on the schedule

Today, asicNorth is enabling high-tech industry leaders and startups alike with a combination of digital, analog, and mixed-signal design capabilities. Driven to produce successful results, asicNorth is Making Chips Happen™.

Codasip delivers leading-edge RISC-V processor IP and high-level processor design tools, providing IC designers with all the advantages of the RISC-V open ISA, along with the unique ability to customize the processor IP. As a founding member of RISC-V International and a long-term supplier of LLVM and GNU-based processor solutions, Codasip is committed to open standards for embedded and application processors. Formed in 2014 and headquartered in Munich, Germany, Codasip currently has R&D centers in Europe and sales representatives worldwide. For more information about our products and services, visit www.codasip.com. For more information about RISC-V, visit www.riscv.org.

Founded in 1999, Avery Design Systems, Inc. enables system and SOC design teams to achieve dramatic functional verification productivity improvements through the use of

Formal analysis applications for RTL and gate-level X verification;

Robust Verification IP for PCI Express, USB, AMBA, UFS, MIPI, DDR/LPDDR, HBM, HMC, ONFI/Toggle, NVM Express, SCSI Express, SATA Express, eMMC, SD/SDIO, Unipro, CSI/DSI, Soundwire, and CAN FD standards.

Siemens EDA
The pace of innovation in electronics is constantly accelerating. To enable our customers to deliver life-changing innovations to the world faster and to become market leaders, we are committed to delivering the world’s most comprehensive portfolio of electronic design automation (EDA) software, hardware, and services.