| // Copyright lowRISC contributors. | |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| // SPDX-License-Identifier: Apache-2.0 | |
| // | |
| // Link state detection | |
| // | |
| `include "prim_assert.sv" | |
| module usbdev_linkstate ( | |
| input logic clk_48mhz_i, | |
| input logic rst_ni, | |
| input logic us_tick_i, | |
| input logic usb_sense_i, | |
| input logic usb_dp_i, | |
| input logic usb_dn_i, | |
| input logic usb_oe_i, | |
| input logic usb_pullup_en_i, | |
| input logic rx_idle_det_i, | |
| input logic rx_j_det_i, | |
| input logic sof_valid_i, | |
| input logic resume_link_active_i, // pulse | |
| output logic link_disconnect_o, // level | |
| output logic link_powered_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 | |
| output logic sof_missed_o, // pulse | |
| output logic [2:0] link_state_o | |
| ); | |
| // Suspend signaling is 3ms of J by spec. | |
| localparam logic [11:0] SUSPEND_TIMEOUT = 12'd3000; | |
| // Reset is 2.5us - 10ms of SE0 by spec, though care should be taken to not | |
| // 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 | |
| LinkDisconnected = 0, | |
| // Powered / connected states | |
| LinkPowered = 1, | |
| LinkPoweredSuspended = 2, | |
| // Active states | |
| LinkActiveNoSOF = 5, | |
| LinkActive = 3, | |
| LinkSuspended = 4, | |
| LinkResuming = 6 | |
| } link_state_e; | |
| typedef enum logic [1:0] { | |
| NoRst, | |
| RstCnt, | |
| RstPend | |
| } link_rst_state_e; | |
| typedef enum logic [1:0] { | |
| Active, | |
| InactCnt, | |
| InactPend | |
| } link_inac_state_e; | |
| link_state_e link_state_d, link_state_q; | |
| logic see_pwr_sense; | |
| // Reset FSM | |
| logic [2:0] link_rst_timer_d, link_rst_timer_q; | |
| link_rst_state_e link_rst_state_d, link_rst_state_q; | |
| logic link_reset; // reset detected (level) | |
| // Link inactivity detection | |
| logic monitor_inac; // monitor link inactivity | |
| logic [11:0] link_inac_timer_d, link_inac_timer_q; | |
| link_inac_state_e link_inac_state_d, link_inac_state_q; | |
| // Events that are not triggered by a timeout | |
| logic ev_bus_active; | |
| // Events that are triggered by timeout | |
| logic ev_bus_inactive, ev_reset; | |
| assign link_disconnect_o = (link_state_q == LinkDisconnected); | |
| assign link_powered_o = see_pwr_sense; | |
| assign link_suspend_o = (link_state_q == LinkSuspended || | |
| link_state_q == LinkPoweredSuspended); | |
| assign link_active_o = (link_state_q == LinkActive) || | |
| (link_state_q == LinkActiveNoSOF); | |
| // Link state is stable, so we can output it to the register | |
| assign link_state_o = link_state_q; | |
| // If the PHY reflects the line state on rx pins when the device is driving | |
| // then the usb_oe_i check isn't needed here. But it seems best to do the check | |
| // to be robust in the face of different PHY designs. | |
| logic see_se0, line_se0_raw; | |
| assign line_se0_raw = (usb_dn_i == 1'b0) & (usb_dp_i == 1'b0) & (usb_oe_i == 1'b0); | |
| // four ticks is a bit time | |
| // Could completely filter out 2-cycle EOP SE0 here but | |
| // does not seem needed | |
| prim_filter #( | |
| .AsyncOn(0), // No synchronizer required | |
| .Cycles(6) | |
| ) filter_se0 ( | |
| .clk_i (clk_48mhz_i), | |
| .rst_ni (rst_ni), | |
| .enable_i (1'b1), | |
| .filter_i (line_se0_raw), | |
| .filter_o (see_se0) | |
| ); | |
| prim_filter #( | |
| .AsyncOn(0), // No synchronizer required | |
| .Cycles(6) | |
| ) filter_pwr_sense ( | |
| .clk_i (clk_48mhz_i), | |
| .rst_ni (rst_ni), | |
| .enable_i (1'b1), | |
| .filter_i (usb_sense_i), | |
| .filter_o (see_pwr_sense) | |
| ); | |
| // Simple events | |
| assign ev_bus_active = !rx_idle_det_i; | |
| assign monitor_inac = see_pwr_sense ? ((link_state_q == LinkPowered) | link_active_o) : | |
| 1'b0; | |
| always_comb begin | |
| link_state_d = link_state_q; | |
| link_resume_o = 0; | |
| // If VBUS ever goes away the link has disconnected (likewise if the | |
| // pull-up goes away / user requested disconnection) | |
| if (!see_pwr_sense || !usb_pullup_en_i) begin | |
| link_state_d = LinkDisconnected; | |
| end else begin | |
| unique case (link_state_q) | |
| // No USB supply detected (USB spec: Attached) | |
| LinkDisconnected: begin | |
| if (see_pwr_sense & usb_pullup_en_i) begin | |
| link_state_d = LinkPowered; | |
| end | |
| end | |
| LinkPowered: begin | |
| if (ev_reset) begin | |
| link_state_d = LinkActiveNoSOF; | |
| end else if (resume_link_active_i) begin | |
| // Software-directed jump to resume from LinkSuspended, in case | |
| // this module was previously powered down. | |
| link_state_d = LinkResuming; | |
| end else if (ev_bus_inactive) begin | |
| link_state_d = LinkPoweredSuspended; | |
| end | |
| end | |
| LinkPoweredSuspended: begin | |
| if (ev_reset) begin | |
| link_state_d = LinkActiveNoSOF; | |
| end else if (ev_bus_active) begin | |
| link_resume_o = 1; | |
| link_state_d = LinkPowered; | |
| end | |
| end | |
| // An event occurred that brought the link out of LinkSuspended, but | |
| // the end-of-resume signaling may not have occurred yet. | |
| // Park here before starting to count towards not seeing SOF. Wait for | |
| // the end of resume signaling before expecting SOF. The host will | |
| // return the link to idle after a low-speed EOP. Instead of trying to | |
| // capture the termination of resume signaling direclty, wait for | |
| // any J / idle symbol (or a bus reset). | |
| LinkResuming: begin | |
| if (rx_j_det_i | ev_reset) begin | |
| link_resume_o = 1; | |
| link_state_d = LinkActiveNoSOF; | |
| end | |
| end | |
| // Active but not yet seen a frame | |
| // One reason for getting stuck here is the host thinks it is a LS link | |
| // which could happen if the flipped bit does not match the actual pins | |
| // Annother is the SI is bad so good data is not recovered from the link | |
| LinkActiveNoSOF: begin | |
| if (ev_bus_inactive) begin | |
| link_state_d = LinkSuspended; | |
| end else if (sof_valid_i) begin | |
| link_state_d = LinkActive; | |
| end | |
| end | |
| // Active (USB spec: Default / Address / Configured) | |
| LinkActive: begin | |
| if (ev_bus_inactive) begin | |
| link_state_d = LinkSuspended; | |
| end else if (ev_reset) begin | |
| link_state_d = LinkActiveNoSOF; | |
| end | |
| end | |
| LinkSuspended: begin | |
| if (ev_reset) begin | |
| link_resume_o = 1; | |
| link_state_d = LinkActiveNoSOF; | |
| end else if (ev_bus_active) begin | |
| link_state_d = LinkResuming; | |
| end | |
| end | |
| default: begin | |
| link_state_d = LinkDisconnected; | |
| end | |
| endcase // case (link_state_q) | |
| end | |
| end | |
| `ASSERT(LinkStateValid_A, link_state_q inside {LinkDisconnected, LinkPowered, | |
| LinkPoweredSuspended, LinkResuming, LinkActiveNoSOF, LinkActive, LinkSuspended}, clk_48mhz_i) | |
| always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin | |
| if (!rst_ni) begin | |
| link_state_q <= LinkDisconnected; | |
| end else begin | |
| link_state_q <= link_state_d; | |
| end | |
| end | |
| ///////////////////// | |
| // Reset detection // | |
| ///////////////////// | |
| // Here we clean up the SE0 signal and generate a single ev_reset at | |
| // the end of a valid reset | |
| always_comb begin : proc_rst_fsm | |
| link_rst_state_d = link_rst_state_q; | |
| link_rst_timer_d = link_rst_timer_q; | |
| ev_reset = 1'b0; | |
| link_reset = 1'b0; | |
| unique case (link_rst_state_q) | |
| // No reset signal detected | |
| NoRst: begin | |
| if (see_se0) begin | |
| link_rst_state_d = RstCnt; | |
| link_rst_timer_d = 0; | |
| end | |
| end | |
| // Reset signal detected -> counting | |
| RstCnt: begin | |
| if (!see_se0) begin | |
| link_rst_state_d = NoRst; | |
| end else begin | |
| if (us_tick_i) begin | |
| if (link_rst_timer_q == RESET_TIMEOUT) begin | |
| link_rst_state_d = RstPend; | |
| end else begin | |
| link_rst_timer_d = link_rst_timer_q + 1; | |
| end | |
| end | |
| end | |
| end | |
| // Detected reset -> wait for falling edge | |
| RstPend: begin | |
| if (!see_se0) begin | |
| link_rst_state_d = NoRst; | |
| ev_reset = 1'b1; | |
| end | |
| link_reset = 1'b1; | |
| end | |
| default : link_rst_state_d = NoRst; | |
| endcase | |
| end | |
| `ASSERT(LinkRstStateValid_A, link_rst_state_q inside {NoRst, RstCnt, RstPend}, clk_48mhz_i) | |
| assign link_reset_o = link_reset; | |
| always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin : proc_reg_rst | |
| if (!rst_ni) begin | |
| link_rst_state_q <= NoRst; | |
| link_rst_timer_q <= 0; | |
| end else begin | |
| link_rst_state_q <= link_rst_state_d; | |
| link_rst_timer_q <= link_rst_timer_d; | |
| end | |
| end | |
| //////////////////// | |
| // Idle detection // | |
| //////////////////// | |
| // Here we clean up the idle signal and generate a single ev_bus_inactive | |
| // after the timer expires | |
| always_comb begin : proc_idle_det | |
| link_inac_state_d = link_inac_state_q; | |
| link_inac_timer_d = link_inac_timer_q; | |
| ev_bus_inactive = 0; | |
| unique case (link_inac_state_q) | |
| // Active or disabled | |
| Active: begin | |
| link_inac_timer_d = 0; | |
| if (!ev_bus_active && monitor_inac) begin | |
| link_inac_state_d = InactCnt; | |
| end | |
| end | |
| // Got an inactivity signal -> count duration | |
| InactCnt: begin | |
| if (ev_bus_active || !monitor_inac) begin | |
| link_inac_state_d = Active; | |
| end else if (us_tick_i) begin | |
| if (link_inac_timer_q == SUSPEND_TIMEOUT) begin | |
| link_inac_state_d = InactPend; | |
| ev_bus_inactive = 1; | |
| end else begin | |
| link_inac_timer_d = link_inac_timer_q + 1; | |
| end | |
| end | |
| end | |
| // Counter expired & event sent, wait here | |
| InactPend: begin | |
| if (ev_bus_active || !monitor_inac) begin | |
| link_inac_state_d = Active; | |
| end | |
| end | |
| default : link_inac_state_d = Active; | |
| endcase | |
| end | |
| `ASSERT(LincInacStateValid_A, link_inac_state_q inside {Active, InactCnt, InactPend}, clk_48mhz_i) | |
| always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin : proc_reg_idle_det | |
| if (!rst_ni) begin | |
| link_inac_state_q <= Active; | |
| link_inac_timer_q <= 0; | |
| end else begin | |
| link_inac_state_q <= link_inac_state_d; | |
| link_inac_timer_q <= link_inac_timer_d; | |
| end | |
| end | |
| ///////////////////////////////////////// | |
| // 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 = missed_sof_count[2]; | |
| always_ff @(posedge clk_48mhz_i or negedge rst_ni) begin | |
| if (!rst_ni) begin | |
| missed_sof_count <= '0; | |
| end else begin | |
| if (sof_valid_i || !link_active_o || link_reset) begin | |
| 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 |