[usbdev] Generate internal SOF when needed

If the host's SOF is missing, generate an internal SOF. Wait an extra
5us for the maximal frequency difference allowed. If no SOF is received,
increment the frame number and signal a new frame.

Signed-off-by: Alexander Williams <awill@google.com>
diff --git a/hw/ip/usbdev/rtl/usbdev.sv b/hw/ip/usbdev/rtl/usbdev.sv
index dbb7404..de36a30 100644
--- a/hw/ip/usbdev/rtl/usbdev.sv
+++ b/hw/ip/usbdev/rtl/usbdev.sv
@@ -136,7 +136,7 @@
   logic              usb_event_rx_bitstuff_err;
   logic              usb_event_in_err;
   logic              usb_event_out_err;
-  logic              usb_event_frame;
+  logic              usb_event_frame, usb_event_sof;
   logic              usb_link_active;
 
   logic              event_link_reset, event_link_suspend, event_link_resume;
@@ -585,6 +585,7 @@
     // status
     .frame_o              (usb_frame),
     .frame_start_o        (usb_event_frame),
+    .sof_valid_o          (usb_event_sof),
     .link_state_o         (usb_link_state),
     .link_disconnect_o    (usb_event_disconnect),
     .link_powered_o       (usb_event_powered),
@@ -1080,7 +1081,7 @@
   );
 
   // Directly forward the pulse unless disabled.
-  assign usb_ref_pulse_o = usb_ref_disable ? 1'b0 : usb_event_frame;
+  assign usb_ref_pulse_o = usb_ref_disable ? 1'b0 : usb_event_sof;
 
   // The first pulse is always ignored, but causes the valid to be asserted.
   // The valid signal is deasserted when:
diff --git a/hw/ip/usbdev/rtl/usbdev_linkstate.sv b/hw/ip/usbdev/rtl/usbdev_linkstate.sv
index 4108925..2b28f8b 100644
--- a/hw/ip/usbdev/rtl/usbdev_linkstate.sv
+++ b/hw/ip/usbdev/rtl/usbdev_linkstate.sv
@@ -28,6 +28,7 @@
   output logic link_suspend_o,     // level
   output logic link_resume_o,      // pulse
   output logic host_lost_o,        // level
+  output logic sof_missed_o,       // pulse
 
   output logic [2:0] link_state_o
 );
@@ -38,6 +39,10 @@
   // confuse the 2 *low-speed* bit times (1.33us) of SE0 that terminate resume
   // signaling. Use 3us here.
   localparam logic [2:0]  RESET_TIMEOUT   = 3'd3;
+  // Consider an SOF lost after 1.005 ms. The extra 5 us helps accommodate
+  // the worst case frequency difference between the host and device, due to a
+  // +/- 2500 ppm range around 12 MHz.
+  localparam logic [9:0]  SOF_TIMEOUT     = 10'd1005;
 
   typedef enum logic [2:0] {
     // No power and/or no pull-up connected state
@@ -349,24 +354,37 @@
     end
   end
 
-  /////////////////////////
-  // Host loss detection //
-  /////////////////////////
-  // host_lost if no sof in 4.096ms (supposed to be every 1ms)
-  // and the link is active
-  logic [12:0] host_presence_timer;
+  /////////////////////////////////////////
+  // Host loss and missing sof detection //
+  /////////////////////////////////////////
+  // sof_missed if no SOF was observed in 1.005ms and the link is active
+  // host_lost if 4 frames have gone by without observing a SOF
+  logic [2:0] missed_sof_count;
+  logic [9:0] missing_sof_timer;
 
-  assign host_lost_o = host_presence_timer[12];
+  assign host_lost_o = missed_sof_count[2];
   always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin
     if (!rst_ni) begin
-      host_presence_timer <= '0;
+      missed_sof_count <= '0;
     end else begin
       if (sof_valid_i || !link_active_o || link_reset) begin
-        host_presence_timer <= '0;
-      end else if (us_tick_i && !host_lost_o) begin
-        host_presence_timer <= host_presence_timer + 1;
+        missed_sof_count <= '0;
+      end else if (sof_missed_o && !host_lost_o) begin
+        missed_sof_count <= missed_sof_count + '1;
       end
     end
   end
 
+  assign sof_missed_o = (missing_sof_timer == SOF_TIMEOUT);
+  always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin
+    if (!rst_ni) begin
+      missing_sof_timer <= '0;
+    end else begin
+      if (sof_missed_o || sof_valid_i || !link_active_o || link_reset) begin
+        missing_sof_timer <= '0;
+      end else if (us_tick_i) begin
+        missing_sof_timer <= missing_sof_timer + 1;
+      end
+    end
+  end
 endmodule
diff --git a/hw/ip/usbdev/rtl/usbdev_usbif.sv b/hw/ip/usbdev/rtl/usbdev_usbif.sv
index e1eca63..790cc4d 100644
--- a/hw/ip/usbdev/rtl/usbdev_usbif.sv
+++ b/hw/ip/usbdev/rtl/usbdev_usbif.sv
@@ -82,8 +82,10 @@
   input  logic                     resume_link_active_i, // Jump from LinkPowered to LinkResuming
 
   // status
-  output logic                     frame_start_o,
+  output logic                     frame_start_o, // Pulses with host-generated and internal SOF
   output logic [10:0]              frame_o,
+  output logic                     sof_valid_o, // Pulses with only host-generated SOF.
+                                                // Used for clock sync.
   output logic [2:0]               link_state_o,
   output logic                     link_disconnect_o,
   output logic                     link_powered_o,
@@ -116,7 +118,6 @@
   logic                              mem_read;
   logic [SramAw-1:0]                 mem_waddr, mem_raddr;
   logic                              link_reset;
-  logic                              sof_valid;
 
   // Make sure out_endpoint_o can safely be used to index signals of NEndpoints width.
   assign out_endpoint_val_o = int'(out_ep_current) < NEndpoints;
@@ -124,7 +125,6 @@
 
   assign link_reset_o   = link_reset;
   assign clr_devaddr_o  = ~enable_i | link_reset;
-  assign frame_start_o  = sof_valid;
   assign link_out_err_o = out_ep_rollback;
 
   always_comb begin
@@ -329,13 +329,14 @@
     .rx_bitstuff_err_o     (rx_bitstuff_err_o),
 
     // sof interface
-    .sof_valid_o           (sof_valid),
+    .sof_valid_o           (sof_valid_o),
     .frame_index_o         (frame_index_raw)
   );
 
   // us_tick ticks for one cycle every us
   logic [5:0]   ns_cnt;
   logic         us_tick;
+  logic         do_internal_sof;
 
   assign us_tick = (ns_cnt == 6'd48);
   always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin
@@ -350,15 +351,27 @@
     end
   end
 
-  // Capture frame number (host sends evert 1ms)
-  // TODO(#10678): Handle missing SOF packets
+  // Capture frame number (host sends every 1ms)
+  // Generate an internal SOF if the host's is missing.
+  logic [10:0] frame_d, frame_q;
+
+  assign frame_o = frame_q;
+  assign frame_start_o = (frame_q != frame_d);
+
+  always_comb begin
+    frame_d = frame_q;
+    if (sof_valid_o) begin
+      frame_d = frame_index_raw;
+    end else if (do_internal_sof) begin
+      frame_d = frame_q + 1;
+    end
+  end
+
   always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin
     if (!rst_ni) begin
-      frame_o <= '0;
+      frame_q <= '0;
     end else begin
-      if (sof_valid) begin
-        frame_o <= frame_index_raw;
-      end
+      frame_q <= frame_d;
     end
   end
 
@@ -373,7 +386,7 @@
     .usb_pullup_en_i       (enable_i),
     .rx_jjj_det_i          (rx_jjj_det),
     .rx_j_det_i            (rx_j_det),
-    .sof_valid_i           (sof_valid),
+    .sof_valid_i           (sof_valid_o),
     .resume_link_active_i  (resume_link_active_i),
     .link_disconnect_o     (link_disconnect_o),
     .link_powered_o        (link_powered_o),
@@ -382,7 +395,8 @@
     .link_suspend_o        (link_suspend_o),
     .link_resume_o         (link_resume_o),
     .link_state_o          (link_state_o),
-    .host_lost_o           (host_lost_o)
+    .host_lost_o           (host_lost_o),
+    .sof_missed_o          (do_internal_sof)
   );
 
   ////////////////