////////////////////////////////////////////////////////////////////////////////
///
/// Template for LSU EE 4702-1 Spring 2001 Homework 4
///

 /// Name:


 /// Instructions:
  //
  // Copy this to a file named hw04sol.v to directory ~/hw in your
  // class account. (~ is your home directory.)  Use this
  // file for your solution.  Your entire solution should be in
  // this file.
  //
  // Do not rename the modules in this file and be sure to use the
  // directory and filename given above.

  // Assignment: http://www.ee.lsu.edu/v/2001/hw04.pdf

////////////////////////////////////////////////////////////////////////////////

`timescale 1us/1us

module microwave_oven_controller(beep,dmt,dmu,dst,dsu,mag_on,
                                 key_code,reset,clk);
   input key_code;              // Key begin pressed (see parameters).
                                // Can be tested on the positive edge of clk.
   input reset;                 // Reset signal. Can be tested on posedge clk.
   input clk;                   // A 64 Hz clock. (Did Westinghouse consider it?)
   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;

   // Do not modify key numbers.
   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;


endmodule // microwave_oven_controller


// exemplar translate_off

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;
   reg        monitor_beep;
   reg        monitor_mag;
   // Set this to one to get long test.
   reg        patient;

   microwave_oven_controller oven(beep,dmt,dmu,dst,dsu,mag_on,key_mod,reset,clk);
   time       tics;

   wire [15:0] mod_digits = {dmt,dmu,dst,dsu};

   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;
   integer     alt_disp_stale;
   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;

   reg [7:0] error_display, error_mag, error_beep;
   integer   error_total;
   reg [7:0]   error_beep_total, error_mag_total, error_display_total;  
   
   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;

   parameter  show_key = 1;
   
   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( monitor_beep ) $display("Beep starting.");
        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;
        if( monitor_beep ) $display("Beep ending.");
        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 | 2;
        end
        expected_beep_done = 0;
     end

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

   always @( mag_on ) 
     if( mag_on ) begin
        if( monitor_mag ) $display("Mag on.");
        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 | 2;
             end
        end
        mag_on_start = tics;
     end else begin // if ( mag_on )
        if( monitor_mag ) $display("Mag off.");
        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( mag_on ) begin
            $display("Mag should be off.");
            error_mag = error_mag | 'h10;
         end

         if( delta > 128 ) begin
            $display("Wrong power level. %d %d ",
                     correct_mag_tics, mag_on_total);
            error_mag = error_mag | 4;
         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: %h   sh: %h  alt: %h   secs %d, state %d",
                 mod_digits, shadow_display,alt_display,
                 shadow_secs, shadow_state);

      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 | 4;
           end

         delta = shadow_tics - ( tics - start_tics );
         if( abs(delta) > 96 )
           begin
              $display("More than 96 tics off: %d",delta);
              error_display = error_display | 2;
           end
         
         if( mod_digits == 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 | 4;
            end
            shadow_state = ss_reset;
            shadow_display = 0;
         end else if ( shadow_secs == 0 )
           begin
              $display("Count problem.");
           end

      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 | 8;
         end
         
      end else if ( watch_display ) begin

         if( mod_digits !== shadow_display 
             && ( alt_disp_stale <= tics || mod_digits !== alt_display ) )
           begin
            $display("Wrong display, should be %h or maybe %h but not %h",
                     shadow_display,alt_display,mod_digits);
            error_display = error_display | 'h10;
         end

      end
      
   end 

   //  Reset shadow state and expected outputs maintained by
   //  testbench.
   //
   task to_reset;
      begin
         shadow_digits = 0;
         alt_display = shadow_display;
         alt_disp_stale = tics + 2;
         shadow_state = ss_reset;
         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];
            cmd = cmd << 8;

            if( c == 0 || c == " " ) disable COMMAND_LOOP;
            
            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
            
            if( key != key_none )
              consec_reset = key == key_reset ? consec_reset + 1 : 0;
            
            if( monitor_keys && key != key_none && consec_reset < 3 )
              $display("Key %s  ", c);

            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;
                   alt_disp_stale = tics + 2;
                   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;
                   alt_disp_stale = tics + 2;
                   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.
                   alt_disp_stale = 'h7fffffff;
                   shadow_power = shadow_display;
                   shadow_display = 0;
                end
              
              {ss_digit1,key_start},{ss_digit2,key_start}:
                begin
                   tosecs(shadow_secs);
                   start_tics = tics;
                   shadow_tics = 0;
                   mag_on_total = 0;
                   mag_on_start = 0;
                   shadow_state = ss_cook;
                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.
                     alt_disp_stale = tics + 2;
                     alt_display = mod_digits;
                     shadow_display = (shadow_display << 4) | key-key_0;
                  end

              {ss_cook,key_reset}: 
                begin
                   shadow_state = ss_pause;
                   shadow_display = mod_digits;
                   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 | 8;
                     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 );

         end // block: COMMAND_LOOP

      end
      
   endtask // command

   // Reset oven module either using reset line or
   // reset button.
   //
   task reset_oven;
      input hard;
      begin
         if( hard ) begin
            reset = 1;
            fork:F
              begin repeat ( 5 * 60 * 64 ) @( posedge clk ); disable F; end
               wait( !beep );
               wait( !mag_on );
            join
            reset = 0;
         end else begin
            command("rrrrrr");
            fork:T
               wait( !beep );
               wait( !mag_on );
               begin repeat ( 5 * 60 * 64 ) @( posedge clk ); disable T; end
            join
            command("rrrrrr");
         end // else: !if( 0 && hard )
         if( beep || mag_on )
           begin
              $display("Could not reset oven.");
              if( beep ) error_beep = error_beep | 'h20;
              if( mag_on ) error_mag = error_mag | 8;
           end
      end
   endtask // reset_oven

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

      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 | 'h10;
              expected_beep_done = 0;
           end

         if( mod_digits!==0 )
           begin
              $display("Display should be zero.");
              error_display = error_display + 'h20;
           end

         verify_cooking;

         watch_display = 0;

         error_count = ( error_display ? 1 : 0 )
         + ( error_beep ? 1 : 0 ) + ( error_mag ? 1 : 0 );
         error_total = error_total + error_count;

         error_beep_total = error_beep_total | error_beep;
         error_mag_total = error_mag_total | error_mag;
         error_display_total = error_display_total | error_display;

         $display("OUTCOME: %s on test %s.  (dsp,beep,mag) (%2h,%2h,%2h)",
                  error_count ? "FAIL" : "PASS",
                  name,
                  error_display, error_beep, error_mag);

         reset_oven(0);

         error_display = 0;
         error_beep = 0;
         error_mag = 0;
         mag_on_total = 0;
         shadow_tics = 0;
         
      end

   endtask // endtest


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

   initial begin
      monitor_display = 1;
      monitor_keys = 1;
      monitor_beep = 1;
      monitor_mag = 1;
      patient = 1;
      expected_beep_done = 0;
      expecting_done_beep = 0;
      error_display = 0;
      error_display_total = 0;
      error_beep = 0;
      error_beep_total = 0;
      error_mag = 0;
      error_mag_total = 0;
      error_total = 0;
      mag_on_total = 0;
      key_mod = key_never;

      #1;
      to_reset;
      reset_oven(1);

      error_display = 0;
      error_display_total = 0;
      error_beep = 0;
      error_beep_total = 0;
      error_mag = 0;
      error_mag_total = 0;
      error_total = 0;
      mag_on_total = 0;
      
      command("50prr");
      delay(54);
      endtest("Power Too High");

      command("32s"); delay(36); 
      endtest("Basic");

      if( patient ) begin:PATIENT
         integer old_mon, old_mag;
         old_mon = monitor_display;
         old_mag = monitor_mag;
         monitor_display = 0;
         $display("Starting test Long, be patient or modify testbench.");
         command("1234s"); delay(14*60);
         endtest("Long");
         monitor_display = old_mon;
         monitor_mag = old_mag;
      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(16);
      endtest("Reset Start");

      command("20s");
      delay(10);
      command("r");
      delay(5);
      command("r");
      delay(16);
      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(135);
      endtest("Twelve no 5");

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

      if( show_key ) begin

      $display("\n ** Error Codes **
  Display (Codes in hexadecimal. Error codes or'ed together.)
    1  Digit out of range. (Check digit.)
    2  Display digit change more than 96 tics off.
    4  Wrong count. (cooking)
    8  Wrong count. (paused)
   10  Wrong count displayed. (Not cooking nor paused.)
   20  Display should be zero.

  Beep: 
    1  Should not be beeping.
    2  Beep duration wrong.
    4  End of cooking beep missing.
    8  Missed a beep. (overlap)
   10  Missed a beep. (endtest)
   20  Wouldn't stop beeping!
  
  Mag
    1  Mag on when cooking off.
    2  Mag cycle error.
    4  Wrong power level.
    8  Would not turn off.
   10  Should be off (when cooking verified).

");

      $display("\n ** Testbench Monitoring ** ");
      $display("    Display %s", monitor_display ? "on" : "off");
      $display("    Keys %s", monitor_keys ? "on" : "off");
      $display("    Beep %s", monitor_beep ? "on" : "off");
      $display("    Mag %s", monitor_mag ? "on" : "off");
      $display("    To turn monitoring on and off edit monitor_FOO variables.");
      $display("    at last \"initial begin\" in testbench.\n\n");
      end
      
      $display("%s, beep tests (code: %h).",
               error_beep_total ? "FAIL" : "PASS", error_beep_total);
      $display("%s, mag tests (code: %h).",
               error_mag_total ? "FAIL" : "PASS", error_mag_total);
      $display("%s, display tests (code: %h).",
               error_display_total ? "FAIL" : "PASS", error_display_total);
      
      $display("All tests completed, %d total errors.",error_total);

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


// exemplar translate_on