////////////////////////////////////////////////////////////////////////////////
///
/// Solution to LSU EE 4702-1 Spring 2001 Homework 3
///
///
//  Includes a testbench (which was not graded).


`timescale 1us/1us

module microwave_oven_controller(beep,dmt,dmu,dst,dsu,mag_on,key_code,clk);
   input key_code;              // Key begin pressed (see parameters).
   input clk;                   // A 64 Hz clock.
   output mag_on;               // When 1, magnetron is on (oven is heating).
   output beep;                 // When 1, emit tone.
   output dmt;                  // Tens digit of minute display.
   output dmu;                  // Units digit of minute display.
   output dst, dsu;             // Tens and units digits of seconds display.

   wire   clk;
   wire [5:0] key_code;
   reg [3:0]  dmt, dmu, dst, dsu;
   reg        mag_on;
   
   parameter  key_none  = 6'd0;  // No key pressed.
   parameter  key_never = 6'd1;  // This code will never be returned.
   parameter  key_start = 6'd10;
   parameter  key_reset = 6'd11;
   parameter  key_power = 6'd12;

   parameter  key_0 = 6'd20;
   parameter  key_1 = 6'd21;
   parameter  key_2 = 6'd22;
   parameter  key_3 = 6'd23;
   parameter  key_4 = 6'd24;
   parameter  key_5 = 6'd25;
   parameter  key_6 = 6'd26;
   parameter  key_7 = 6'd27;
   parameter  key_8 = 6'd28;
   parameter  key_9 = 6'd29;

   /// States
   //
   parameter  st_reset    = 0;  
   parameter  st_entry_1  = 1;  // One digit entered.
   parameter  st_entry_1p = 2;  // One digit and power.
   parameter  st_entry_n  = 3;  // At least 2 digits (including power level).
   parameter  st_heating  = 4;
   parameter  st_paused   = 5;

   reg [3:0]  state, next_state;

   reg [2:0]  digit_count;      // Number of digits entered.
   reg [3:0]  power;            // Power level set by user.
   
   reg [5:0]  key_type;         // Type of key. (unless digit, key_code)
   parameter  kty_digit = 6'd30;

   // Number of tics before beep stops.  Zero if not beeping.
   //
   reg [7:0]  beep_timer;
   assign     beep = | beep_timer;
   always @( posedge clk ) if( beep_timer ) beep_timer = beep_timer - 1;

   // Add Digit to Display
   //
   task add_digit;
      begin
         dmt = dmu;
         dmu = dst;
         dst = dsu;
         dsu = key_code - key_0;
         digit_count = digit_count + 1;
      end
   endtask // add_digit

   // Actions when switching to st_reset, including setting next_state.
   //
   task do_reset;
      begin
         dmt = 0;
         dmu = 0;
         dst = 0;
         dsu = 0;
         digit_count = 0;
         beep_timer = 0;
         next_state = st_reset;
         mag_on = 0;
      end
   endtask // do_reset
   
   initial begin do_reset; state = st_reset; end

   /// State Transitions
   //
   always @( key_code ) if( key_code != key_none ) begin

      key_type = key_code >= key_0 && key_code <= key_9
                 ? kty_digit : key_code;

      casez( {state, key_type} )

        {st_reset,kty_digit}:
          begin
             add_digit;
             power = 10;
             next_state = st_entry_1;
          end

        {st_entry_1p,kty_digit}:
          begin
             dsu = 0;
             add_digit;
             next_state = st_entry_n;
          end

        {st_entry_n,kty_digit}:
          begin
             if( digit_count == 4 )
               beep_timer = 16;
             else
               add_digit;
             next_state = state;
          end

        {st_entry_1,kty_digit}:
          begin
             add_digit;
             next_state = st_entry_n;
          end

        {st_entry_1,key_power}:
          begin
             power = dsu;
             next_state = st_entry_1p;
          end

        {st_entry_n,key_start}, {st_entry_1,key_start}:
          begin
             if( dst > 5 )
               begin
                  beep_timer = 16;
                  next_state = state;
               end else
                 next_state = st_heating;
          end

        {st_heating,key_reset}:
          begin
             disable HEAT_LOOP;
             next_state = st_paused;
          end

        {st_paused,key_start}:
          begin
             next_state = st_heating;
          end

        {4'b????,key_reset}: do_reset;

        default: beep_timer = 16;

        
      endcase // casez( {state, key_type} )

      state = next_state;

   end // if ( key_code != key_none )

   // Clock Divider
   //
   // Divides 64 Hz clock by 64 so that sec_timer == 0 once per second.
   //
   reg [5:0]  sec_timer;
   initial sec_timer = 0;
   always @( posedge clk ) sec_timer = sec_timer + 1;


   
   always wait( state == st_heating ) begin
      fork:HEAT_LOOP
        reg [7:0]  on_timer, off_timer;

         // Turn magnetron on and off.
         //
        forever begin
           on_timer  = power * 64 * 2.5 / 10;
           off_timer = 64 * 2.5 - on_timer;
           mag_on    = 1;
           while ( on_timer ) @( posedge clk ) on_timer = on_timer - 1;
           if( off_timer ) begin
              mag_on = 0;
              while ( off_timer ) @( posedge clk ) off_timer = off_timer - 1;
           end
        end

         // Update display during heating.
         //
         forever @( posedge | sec_timer ) begin:T
            
            if( {dmt,dmu,dst,dsu} == 0 ) begin
               beep_timer = 128;
               state = st_reset;
               mag_on <= 0;
               disable HEAT_LOOP;
            end
            
            if( dsu ) begin dsu = dsu - 1; disable T; end
            dsu = 9;
            if( dst ) begin dst = dst - 1; disable T; end
            dst = 5;
            if( dmu ) begin dmu = dmu - 1; disable T; end
            dmu = 9;
            if( dmt ) dmt = dmt - 1;
            
         end // block: T
      join
      mag_on = 0;
   end // always wait

endmodule // microwave_oven_controller



module test_oven();

   reg  clk;
   wire [3:0] dmt, dmu, dst, dsu;
   wire       mag_on;
   reg [5:0]  key_mod;
   reg        reset;

   // Set this to 1 to have each change in the oven display appear
   // on the console.
   reg        monitor_display;
   // Set this to one to have each key press appear on the console.
   reg        monitor_keys;
   // Set this to one to get long test.
   reg        patient;
   
   microwave_oven_controller oven(beep,dmt,dmu,dst,dsu,mag_on,key_mod,clk);

   time       tics;

   initial tics = 0;
   always begin clk = 0; #5625; tics = tics + 1; #0; clk=1; #10000; end

   parameter   ss_reset  = 3'd0;
   parameter   ss_digit1 = 3'd1; // Single digit, power not entered.
   parameter   ss_digit2 = 3'd2; // Power entered or > 1 digit.
   parameter   ss_cook   = 3'd3;
   parameter   ss_pause  = 3'd4;

   parameter   kty_digit = 6'd30;

   reg [15:0]  shadow_display, alt_display;
   reg [2:0] shadow_state;
   integer   shadow_secs, mod_secs, delta;
   integer   shadow_tics, pause_tics, start_tics;
   integer   shadow_power, shadow_digits;
   integer   expected_beep_done, expecting_done_beep;

   integer   watch_display;

   integer   error_display, error_mag, error_beep, error_total;
   
`include "oven_keys.v"
   
   function integer abs;
      input a;
      integer a;
      abs = a < 0 ? -a : a;
   endfunction // abs

   
`define check_digit(d,l)                                                        \
   if( (d) > (l) || (d) < 0 ) begin                                             \
      error_display = error_display + 1;                                        \
      secs = -1;                                                                \
      disable tosecs;                                                           \
                      end

   // Convert time on display to seconds.  
   //
   task tosecs;
      output secs;
      integer secs;

      begin
         `check_digit(dmt,9)
         `check_digit(dmu,9)
         `check_digit(dst,5)
         `check_digit(dsu,9)
         secs = dmt * 600 + dmu * 60 + dst * 10 + dsu;
      end

   endtask // tosecs

   /// Listen Beep
   //
   always @( beep )
     if( beep ) begin
        if( expecting_done_beep )
          begin
             expected_beep_done = 128;
             expecting_done_beep = 0;
          end
        if( expected_beep_done == 0 ) begin
           $display("Should not be beeping.");
           error_beep = error_beep + 1;
        end
     end else begin:B // if ( beep )
        integer delta;
        delta = abs( tics - expected_beep_done );
        if( shadow_state != ss_reset
            && expected_beep_done && delta > 5 ) begin
           $display("Beep wrong time. %d",delta);
           error_beep = error_beep + 1;
        end
        expected_beep_done = 0;
     end

   /// Watch Magnetron
   //
   integer mag_on_start, mag_on_total;

   always @( mag_on ) 
     if( mag_on ) begin
        if( shadow_state != ss_cook )
          begin
             $display("Mag on when cooking off.");
             error_mag = error_mag + 1;
          end
        if( mag_on_start != 0 ) begin:A
           integer cycle_this;
           cycle_this = tics - mag_on_start;
           if( shadow_power > 0 && shadow_power < 10
               && abs( cycle_this - 160 ) > 10 )
             begin
                $display("Mag cycle error.");
                error_mag = error_mag + 1;
             end
        end
        mag_on_start = tics;
     end else begin // if ( mag_on )
        mag_on_total = mag_on_total + tics - mag_on_start;
     end // else: !if( mag_on )

   // Verify correct magnetron-on time.
   //
   task verify_cooking;
      begin:A
         integer correct_mag_tics;
         integer delta;

         correct_mag_tics = shadow_tics * shadow_power / 10;

         delta = abs(correct_mag_tics - mag_on_total);

         if( delta > 128 ) begin
            $display("Wrong power level. %d %d ",
                     correct_mag_tics, mag_on_total);
            error_mag = error_mag + 1;
         end
      end // block: A
   endtask // verify_cooking
   
   ///  Watch display, etc.
   //
   always @( dmt or dmu or dst or dsu or shadow_display ) #1 begin

      if( monitor_display ) $display("Display: %d%d:%d%d",dmt,dmu,dst,dsu);

      if( shadow_state == ss_cook ) begin

         shadow_secs = shadow_secs - 1;
         shadow_tics = shadow_tics + 64;

         tosecs(mod_secs);

         if( mod_secs !== shadow_secs )
           begin
              $display("Wrong count. (cooking)");
              error_display = error_display + 1;
           end

         delta = shadow_tics - ( tics - start_tics );
         if( abs(delta) > 96 )
           begin
              $display("More than 96 tics off: %d",delta);
              error_display = error_display + 1;
           end
         
         if( shadow_secs == 0 ) begin
            expecting_done_beep = 1;
            delay(1.5);
            verify_cooking;
            if( expecting_done_beep ) begin
               $display("End of cooking beep missing.");
               error_beep = error_beep + 1;
            end
            shadow_state = ss_reset;
            shadow_digits = 0;
         end // if ( shadow_secs == 0 )

      end else if ( shadow_state == ss_pause ) begin 

         tosecs(mod_secs);
         if( mod_secs !== shadow_secs ) begin
            $display("Wrong count. (paused)");
            error_display = error_display + 1;
         end
         
      end else if ( watch_display ) begin

         if( {dmt,dmu,dst,dsu} !== shadow_display 
             && {dmt,dmu,dst,dsu} !== alt_display ) begin
           $display("Wrong display, should be %h",shadow_display);
            error_display = error_display + 1;
         end

      end
      
   end 

   //  Reset shadow state and expected outputs maintained by
   //  testbench.
   //
   task to_reset;
      begin
         shadow_digits = 0;
         shadow_state = ss_reset;
         alt_display = shadow_display;
         shadow_display = 0;
         shadow_power = 10;
         watch_display = 1;
      end
   endtask // to_reset

   // Send keys to module, update correct state and expected output information.
   //
   task command;

      input [799:0] cmd;

      integer initialized;
      integer c;
      integer consec_reset;
      reg [5:0] to_key [0:255];
      reg [5:0] key;

      begin

         if( initialized === 'bx ) begin

            for( c = 0; c < 256; c = c + 1 ) to_key[c] = key_never;
            for( c = 0; c < 10;  c = c + 1 ) to_key[ "0" + c ] = key_0 + c;

            to_key["s"] = key_start;
            to_key["r"] = key_reset;
            to_key["p"] = key_power;
            to_key[" "] = key_none;
            to_key[0]   = key_none;

            initialized = 1;
            consec_reset = 0;

         end // if ( initialized === 'bx )

         while( cmd ) begin:COMMAND_LOOP
            reg [7:0] c;
            reg [5:0] key_type;

            c = cmd[799:792];
            key_mod = to_key[ c ];
            key = key_mod;
            key_type = ( c >= "0" && c <= "9" ) ? kty_digit : key_mod;
            
            if( key == key_never ) begin
               $display("Testbench error: illegal key in command, %s (%d)",c,c);
               $stop;
            end

            casez( {shadow_state,key_type} )

              {3'b???,key_none}:;

              {ss_reset,key_reset}:
                begin
                   to_reset;
                end

              {ss_reset,kty_digit}:
                begin
                   shadow_digits = 1;
                   shadow_state = ss_digit1;
                   alt_display = shadow_display;
                   shadow_display = key-key_0;
                end

              {ss_pause,key_reset},
              {ss_digit1,key_reset},
              {ss_digit2,key_reset}:
                begin
                   to_reset;
                end

              {ss_digit1,kty_digit}:
                begin
                   shadow_digits = 2;
                   shadow_state = ss_digit2;
                   alt_display = shadow_display;
                   shadow_display = (shadow_display << 4) | key-key_0;
                end

              {ss_digit1,key_power}:
                begin
                   shadow_state = ss_digit2;
                   shadow_digits = 0;
                   alt_display = shadow_display; // Power level.
                   shadow_power = shadow_display;
                   shadow_display = 0;
                end
              
              {ss_digit1,key_start},{ss_digit2,key_start}:
                begin
                   if( shadow_display[7:4] > 5 )
                     begin
                        expected_beep_done = tics + 16;                        
                     end else begin
                        tosecs(shadow_secs);
                        start_tics = tics;
                        shadow_tics = 0;
                        mag_on_total = 0;
                        mag_on_start = 0;
                        shadow_state = ss_cook;
                     end
                end

              {ss_digit2,kty_digit}:
                if( shadow_digits == 4 )
                  expected_beep_done = tics + 16;
                else
                  begin
                     shadow_digits = shadow_digits + 1;
                     // If shadow_display zero then power was pressed.
                     if( shadow_display ) alt_display = shadow_display;
                     shadow_display = (shadow_display << 4) | key-key_0;
                  end

              {ss_cook,key_reset}: 
                begin
                   shadow_state = ss_pause;
                   pause_tics = tics;
                end

              {ss_pause,key_start}:
                begin
                   verify_cooking;
                   mag_on_start = 0;
                   shadow_tics = 0;
                   mag_on_total = 0;
                   start_tics = tics;
                   shadow_state = ss_cook;
                end
              
              default:
                begin
                   if( expected_beep_done && expected_beep_done < tics )
                     begin
                        $display("Missed a beep. (overlap)");
                        error_beep = error_beep + 1;
                     end
                   expected_beep_done = tics + 16;
                end
              
            endcase // casez( {shadow_state,key_type} )

            @( posedge clk ) @( negedge clk );
            repeat ( $random() & 15 + 3 ) @( negedge clk );
            key_mod = key_none;
            @( posedge clk ) @( negedge clk );
            @( posedge clk ) @( negedge clk );

            if( key != key_none )
              consec_reset = key == key_reset ? consec_reset + 1 : 0;
            
            if( monitor_keys && key != key_none && consec_reset < 2 )
              $display("Key %s  State %d, Display: %d%d:%d%d  B %d",
                       c,shadow_state, dmt,dmu,dst,dsu,beep);

            cmd = cmd << 8;

         end // block: COMMAND_LOOP

      end
      
   endtask // command

   // Reset oven module either using reset line or
   // reset button.
   //
   task reset_oven;
      input hard;
      begin
         if( 0 && hard ) begin
            reset = 1;
            fork:F
              begin repeat ( 256 ) @( clk ); disable F; end
               wait( !beep );
               wait( !mag_on );
            join
            reset = 0;
         end else begin
            command("rrrrrr");
            fork
               wait( !beep );
               wait( !mag_on );
            join
            command("rrrrrr");
         end // else: !if( 0 && hard )
         if( beep || mag_on )
           begin
              $display("Could not reset oven.");
           end
      end
   endtask // reset_oven

   // Actions to be done at the end of a test.
   //
   task endtest;
      input [159:0] name;

      begin

         if( expected_beep_done )
           begin
              if( expected_beep_done >= tics )
                $display("Testbench not waiting long enough for beep.");
              $display("Missed a beep.");
              error_beep = error_beep + 1;
              expected_beep_done = 0;
           end

         if( {dmt,dmu,dst,dsu}!==0 )
           begin
              $display("Expected to be finished.");
              error_display = error_display + 1;
           end

         watch_display = 0;

         $display("Test %s completed.  (dsp,beep,mag) (%d,%d,%d)",
                  name,
                  error_display, error_beep, error_mag);

         error_total = error_total + error_display + error_beep + error_mag; 

         reset_oven(0);

         error_display = 0;
         error_beep = 0;
         error_mag = 0;
         
      end

   endtask // endtest

   task delay;
      input [63:0] secs;
      #( secs * 1000000 );
   endtask // delay

   initial begin
      monitor_display = 0;
      monitor_keys = 0;
      patient = 0;
      expected_beep_done = 0;
      expecting_done_beep = 0;
      error_display = 0;
      error_beep = 0;
      error_mag = 0;
      error_total = 0;

      #1;
      to_reset;
      reset_oven(1);

      command("50prr");
      delay(40);
      endtest("Power Too High");

      command("12s"); delay(16); 
      endtest("Basic");

      if( patient ) begin
         $display("Starting test Long, be patient or modify testbench.");
         command("100s"); delay(90*60+5);
         endtest("Long");
      end
      
      command("30s"); delay(14); 
      command("1"); delay(2); command("p");
      delay(20);
      endtest("Basic Disturbed");

      command("5p30s");
      delay(40);
      endtest("Half Power");
            
      command("9p3p0s");
      delay(40);
      endtest("Power Twice");

      command("3ppp19s");
      delay(40);
      endtest("Power Thrice");

      command("20s");
      delay(10);
      command("r");
      delay(5);
      command("s");
      delay(10);
      endtest("Reset Start");

      command("20s");
      delay(10);
      command("r");
      delay(5);
      command("r");
      delay(1);
      endtest("Reset Reset");

      command("s");
      delay(5);
      endtest("Null Start");

      command("ps");
      delay(5);
      endtest("Null Power Start");

      command("7p30s"); delay(10); 
      command("1"); delay(2); command("s"); delay(3);
      command("12344321");
      delay(40);
      endtest("Power Disturbed");

      command("12r5s"); delay(10);
      endtest("Twelve no 5");

      command("90s"); delay(1); command("rr");
      endtest("Ninety Seconds");

      command("12345rr");
      endtest("Display Overflow");

      $display("All tests completed, %d total errors.",error_total);

      $stop;
   end // initial begin
   
endmodule // test_oven