[usbdev] Add reference signals for synchronization of USB clock

This commit adds two new outputs to usbdev:
- `usb_ref_pulse_o`: indicates the reception of a start of frame (SOF)
   packet, sent by the host every 1 ms.
- `usb_ref_val_o`: indicates if the `usb_ref_pulse_o` signal is valid.

This information can be used as a reference to synchronize the USB clock
with the host.

Signed-off-by: Pirmin Vogel <vogelpi@lowrisc.org>
diff --git a/hw/ip/usbdev/data/usbdev.hjson b/hw/ip/usbdev/data/usbdev.hjson
index 9044314..ce2cdf8 100644
--- a/hw/ip/usbdev/data/usbdev.hjson
+++ b/hw/ip/usbdev/data/usbdev.hjson
@@ -23,6 +23,22 @@
     { name: "tx_mode_se", desc: "USB single-ended transmit mode control" }
     { name: "suspend", desc: "USB link suspend state" }
   ],
+  inter_signal_list: [
+    { name:    "usb_ref_val",
+      type:    "uni",
+      act:     "req",
+      package: "",
+      struct:  "logic",
+      width:   "1"
+    }
+    { name:    "usb_ref_pulse",
+      type:    "uni",
+      act:     "req",
+      package: "",
+      struct:  "logic",
+      width:   "1"
+    }
+  ]
   param_list: [
     { name:    "NEndpoints",
       type:    "int",
diff --git a/hw/ip/usbdev/doc/_index.md b/hw/ip/usbdev/doc/_index.md
index b917eb2..e365d95 100644
--- a/hw/ip/usbdev/doc/_index.md
+++ b/hw/ip/usbdev/doc/_index.md
@@ -55,6 +55,14 @@
 The USB specification for a Full-Speed device requires the average bit rate is 12 Mbps +/- 0.25%, so the clock needs to support maximum error of 2,500 ppm.
 The maximum allowable integrated jitter is +/- 1 ns over 1 to 7 bit periods.
 
+This module features the following output signals to provide a reference for synchronizing the 48 MHz clock source:
+- `usb_ref_pulse_o` indicates the reception of a start of frame (SOF) packet.
+  The host is required to send a SOF packet every 1 ms.
+- `usb_ref_val_o` serves as a valid signal for `usb_ref_pulse_o`.
+  It is set to one after the first SOF packet is received and remains high as long as `usb_ref_pulse_o` continues to behave as expected.
+  As soon as it is detected that SOF will not be received as expected (usually because the link is no longer active), `usb_ref_val_o` deasserts to zero until after the next `usb_ref_pulse_o`.
+Both these signals are synchronous to the 48 MHz clock.
+
 Control transfers pass through asynchronous FIFOs or have a ready bit
 synchronized across the clock domain boundary. A dual-port
 asynchronous buffer SRAM is used for data transfers between the bus
diff --git a/hw/ip/usbdev/rtl/usbdev.sv b/hw/ip/usbdev/rtl/usbdev.sv
index 48810a8..dfbb9de 100644
--- a/hw/ip/usbdev/rtl/usbdev.sv
+++ b/hw/ip/usbdev/rtl/usbdev.sv
@@ -43,6 +43,10 @@
   output logic       cio_tx_mode_se_o,
   output logic       cio_tx_mode_se_en_o,
 
+  // SOF reference for clock calibration
+  output logic       usb_ref_val_o,
+  output logic       usb_ref_pulse_o,
+
   // Interrupts
   output logic       intr_pkt_received_o, // Packet received
   output logic       intr_pkt_sent_o, // Packet sent
@@ -116,6 +120,7 @@
   logic              usb_event_rx_bitstuff_err;
   logic              usb_event_in_err;
   logic              usb_event_frame;
+  logic              usb_link_active;
 
   logic              event_link_reset, event_link_suspend, event_link_resume;
   logic              event_host_lost, event_disconnect, event_connect;
@@ -499,6 +504,7 @@
     .link_disconnect_o    (usb_event_disconnect),
     .link_connect_o       (usb_event_connect),
     .link_reset_o         (usb_event_link_reset),
+    .link_active_o        (usb_link_active),
     .link_suspend_o       (usb_event_link_suspend),
     .link_resume_o        (usb_event_link_resume),
     .host_lost_o          (usb_event_host_lost),
@@ -916,4 +922,30 @@
   assign cio_dp_pullup_o     = 1'b1;
   assign cio_dn_pullup_o     = 1'b1;
 
+  /////////////////////////////////////////
+  // SOF Reference for Clock Calibration //
+  /////////////////////////////////////////
+
+  logic usb_ref_val_d, usb_ref_val_q;
+
+  // Directly forward the pulse.
+  assign usb_ref_pulse_o = usb_event_frame;
+
+  // The first pulse is always ignored, but causes the valid to be asserted.
+  // The valid signal is deasserted when:
+  // - The link is no longer active.
+  // - The host is lost (no SOF for 4ms).
+  assign usb_ref_val_d = usb_ref_pulse_o                         ? 1'b1 :
+                       (!usb_link_active || usb_event_host_lost) ? 1'b0 : usb_ref_val_q;
+
+  always_ff @(posedge clk_usb_48mhz_i or negedge rst_usb_48mhz_ni) begin
+    if (!rst_usb_48mhz_ni) begin
+      usb_ref_val_q <= 1'b0;
+    end else begin
+      usb_ref_val_q <= usb_ref_val_d;
+    end
+  end
+
+  assign usb_ref_val_o = usb_ref_val_q;
+
 endmodule
diff --git a/hw/ip/usbdev/rtl/usbdev_linkstate.sv b/hw/ip/usbdev/rtl/usbdev_linkstate.sv
index 7a206e0..d4dea2d 100644
--- a/hw/ip/usbdev/rtl/usbdev_linkstate.sv
+++ b/hw/ip/usbdev/rtl/usbdev_linkstate.sv
@@ -16,6 +16,7 @@
   output logic link_disconnect_o,  // level
   output logic link_connect_o,     // level
   output logic link_reset_o,       // level
+  output logic link_active_o,      // level
   output logic link_suspend_o,     // level
   output logic link_resume_o,      // pulse
   output logic host_lost_o,        // level
@@ -50,7 +51,6 @@
   } link_inac_state_e;
 
   link_state_e  link_state_d, link_state_q;
-  logic         link_active;
   logic         line_se0_raw, line_idle_raw;
   logic         see_se0, see_idle, see_pwr_sense;
 
@@ -75,7 +75,7 @@
   assign link_connect_o    = (link_state_q != LinkDisconnect);
   assign link_suspend_o    = (link_state_q == LinkSuspend ||
     link_state_q == LinkPoweredSuspend);
-  assign link_active       = (link_state_q == LinkActive);
+  assign link_active_o     = (link_state_q == LinkActive);
   // Link state is stable, so we can output it to the register
   assign link_state_o      =  link_state_q;
 
@@ -303,7 +303,7 @@
     if (!rst_ni) begin
       host_presence_timer <= '0;
     end else begin
-      if (sof_valid_i || !link_active || link_reset) 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;
diff --git a/hw/ip/usbdev/rtl/usbdev_usbif.sv b/hw/ip/usbdev/rtl/usbdev_usbif.sv
index b79e4ce..a912ea3 100644
--- a/hw/ip/usbdev/rtl/usbdev_usbif.sv
+++ b/hw/ip/usbdev/rtl/usbdev_usbif.sv
@@ -80,6 +80,7 @@
   output logic                     link_disconnect_o,
   output logic                     link_connect_o,
   output logic                     link_reset_o,
+  output logic                     link_active_o,
   output logic                     link_suspend_o,
   output logic                     link_resume_o,
   output logic                     link_in_err_o,
@@ -346,6 +347,7 @@
     .link_disconnect_o (link_disconnect_o),
     .link_connect_o    (link_connect_o),
     .link_reset_o      (link_reset),
+    .link_active_o     (link_active_o),
     .link_suspend_o    (link_suspend_o),
     .link_resume_o     (link_resume_o),
     .link_state_o      (link_state_o),
diff --git a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
index 48d8cb6..f7d754e 100644
--- a/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
+++ b/hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson
@@ -1603,6 +1603,29 @@
       ]
       alert_list: []
       scan: "false"
+      inter_signal_list:
+      [
+        {
+          name: usb_ref_val
+          type: uni
+          act: req
+          package: ""
+          struct: logic
+          width: "1"
+          inst_name: usbdev
+          index: -1
+        }
+        {
+          name: usb_ref_pulse
+          type: uni
+          act: req
+          package: ""
+          struct: logic
+          width: "1"
+          inst_name: usbdev
+          index: -1
+        }
+      ]
     }
   ]
   memory:
@@ -3408,6 +3431,26 @@
         index: -1
       }
       {
+        name: usb_ref_val
+        type: uni
+        act: req
+        package: ""
+        struct: logic
+        width: "1"
+        inst_name: usbdev
+        index: -1
+      }
+      {
+        name: usb_ref_pulse
+        type: uni
+        act: req
+        package: ""
+        struct: logic
+        width: "1"
+        inst_name: usbdev
+        index: -1
+      }
+      {
         struct: flash
         type: req_rsp
         name: flash_ctrl
diff --git a/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv b/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
index f6b694d..7abb4d8 100644
--- a/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
+++ b/hw/top_earlgrey/rtl/autogen/top_earlgrey.sv
@@ -813,6 +813,10 @@
       .intr_frame_o           (intr_usbdev_frame),
       .intr_connected_o       (intr_usbdev_connected),
 
+      // Inter-module signals
+      .usb_ref_val_o(),
+      .usb_ref_pulse_o(),
+
       .clk_i (clkmgr_clocks.clk_io_peri),
       .clk_usb_48mhz_i (clkmgr_clocks.clk_usb_peri),
       .rst_ni (rstmgr_resets.rst_sys_io_n),