| /* Copyright 2021 The ChromiumOS Authors |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| /* |
| * Functions required for UFP_D operation |
| */ |
| |
| #include "console.h" |
| #include "gpio.h" |
| #include "hooks.h" |
| #include "system.h" |
| #include "task.h" |
| #include "usb_pd.h" |
| #include "usb_pd_dp_ufp.h" |
| |
| #define CPRINTF(format, args...) cprintf(CC_USBPD, format, ##args) |
| #define CPRINTS(format, args...) cprints(CC_USBPD, format, ##args) |
| |
| enum hpd_state { |
| LOW_WAIT, |
| HIGH_CHECK, |
| HIGH_WAIT, |
| LOW_CHECK, |
| IRQ_CHECK, |
| }; |
| |
| #define EDGE_QUEUE_DEPTH BIT(3) |
| #define EDGE_QUEUE_MASK (EDGE_QUEUE_DEPTH - 1) |
| #define HPD_QUEUE_DEPTH BIT(2) |
| #define HPD_QUEUE_MASK (HPD_QUEUE_DEPTH - 1) |
| #define HPD_T_IRQ_MIN_PULSE 250 |
| #define HPD_T_IRQ_MAX_PULSE (2 * MSEC) |
| #define HPD_T_MIN_DP_ATTEN (10 * MSEC) |
| |
| struct hpd_mark { |
| int level; |
| uint64_t ts; |
| }; |
| |
| struct hpd_edge { |
| int overflow; |
| uint32_t head; |
| uint32_t tail; |
| struct hpd_mark buffer[EDGE_QUEUE_DEPTH]; |
| }; |
| |
| struct hpd_info { |
| enum hpd_state state; |
| int count; |
| int send_enable; |
| uint64_t timer; |
| uint64_t last_send_ts; |
| enum hpd_event queue[HPD_QUEUE_DEPTH]; |
| struct hpd_edge edges; |
| }; |
| |
| static struct hpd_info hpd; |
| static struct mutex hpd_mutex; |
| |
| static int alt_dp_mode_opos[CONFIG_USB_PD_PORT_MAX_COUNT]; |
| |
| void pd_ufp_set_dp_opos(int port, int opos) |
| { |
| alt_dp_mode_opos[port] = opos; |
| } |
| |
| int pd_ufp_get_dp_opos(int port) |
| { |
| return alt_dp_mode_opos[port]; |
| } |
| |
| void pd_ufp_enable_hpd_send(int port) |
| { |
| /* |
| * This control is used ensure that a DP_ATTENTION message is not sent |
| * to the DFP-D before a DP_CONFIG messaage has been received. This |
| * control is not strictly required by the spec, but some port partners |
| * will get confused if DP_ATTENTION is sent prior to DP_CONFIG. |
| */ |
| hpd.send_enable = 1; |
| } |
| |
| static void hpd_to_dp_attention(void) |
| { |
| int port = hpd_config.port; |
| int evt_index = hpd.count - 1; |
| uint32_t vdm[2]; |
| uint32_t svdm_header; |
| enum hpd_event evt; |
| int opos = pd_ufp_get_dp_opos(port); |
| |
| if (!opos) |
| return; |
| |
| /* Get the next hpd event from the queue */ |
| evt = hpd.queue[evt_index]; |
| /* Save timestamp of when most recent DP attention message was sent */ |
| hpd.last_send_ts = get_time().val; |
| |
| /* |
| * Construct DP Attention message. This consists of the VDM header and |
| * the DP_STATUS VDO. |
| */ |
| svdm_header = VDO_SVDM_VERS(pd_get_vdo_ver(port, TCPCI_MSG_SOP)) | |
| VDO_OPOS(opos) | CMD_ATTENTION; |
| vdm[0] = VDO(USB_SID_DISPLAYPORT, 1, svdm_header); |
| |
| vdm[1] = VDO_DP_STATUS((evt == hpd_irq), /* IRQ_HPD */ |
| (evt != hpd_low), /* HPD_HI|LOW */ |
| 0, /* request exit DP */ |
| 0, /* request exit USB */ |
| dock_get_mf_preference(), /* MF pref */ |
| 1, /* enabled */ |
| 0, /* power low */ |
| 0x2); |
| |
| /* Send request to DPM to send an attention VDM */ |
| pd_request_vdm(port, vdm, ARRAY_SIZE(vdm), TCPCI_MSG_SOP); |
| |
| /* If there are still events, need to shift the buffer */ |
| if (--hpd.count) { |
| int i; |
| |
| for (i = 0; i < hpd.count; i++) |
| hpd.queue[i] = hpd.queue[i + 1]; |
| } |
| } |
| |
| static void hpd_queue_event(enum hpd_event evt) |
| { |
| /* |
| * HPD events are put into a queue. However, this queue is not a typical |
| * FIFO queue. Instead there are special rules based on which type of |
| * event is being added. |
| * HPD_LOW -> always resets the queue and must be in slot 0 |
| * HPD_HIGH -> must follow a HPD_LOW, so can only be in slot 0 or |
| * slot 1. |
| * HPD_IRQ -> There shall never be more than 2 HPD_IRQ events |
| * stored in the queue and HPD_IRQ must follow HPD_HIGH |
| * |
| * Worst case for queueing HPD events is 4 events in the queue: |
| * 0 - HPD_LOW |
| * 1 - HPD_HIGH |
| * 2 - HPD_IRQ |
| * 3 - HPD_IRQ |
| * |
| * The above rules mean that HPD_LOW and HPD_HIGH events can always be |
| * added to the queue since high must follow low and a low event resets |
| * the queue. HPD_IRQ events are checked to make sure that they don't |
| * overflow the queue and to ensure that no more than 2 hpd_irq events |
| * are kept in the queue. |
| */ |
| if (evt == hpd_irq) { |
| if ((hpd.count >= HPD_QUEUE_DEPTH) || |
| ((hpd.count >= 2) && |
| (hpd.queue[hpd.count - 2] == hpd_irq))) { |
| CPRINTS("hpd: discard hpd: count - %d", hpd.count); |
| return; |
| } |
| } |
| |
| if (evt == hpd_low) { |
| hpd.count = 0; |
| } |
| |
| /* Add event to the queue */ |
| hpd.queue[hpd.count++] = evt; |
| } |
| |
| static void hpd_to_pd_converter(int level, uint64_t ts) |
| { |
| /* |
| * HPD edges are marked in the irq routine. The converter state machine |
| * runs in the hooks task and so there will be some delay between when |
| * the edge was captured and when that edge is processed here in the |
| * state machine. This means that the delitch timer (250 uSec) may have |
| * already expired or is about to expire. |
| * |
| * If transitioning to timing dependent state, need to ensure the state |
| * machine is executed again. All timers are relative to the ts value |
| * passed into this routine. The timestamps passed into this routine |
| * are either the values latched in the irq routine, or the current |
| * time latched by the calling function. From the perspective of the |
| * state machine, ts represents the current time. |
| * |
| * Note that all hpd queue events are contingent on detecting edges |
| * on the incoming hpd gpio signal. The hpd->dp attention converter is |
| * enabled/disabled as part of the svdm dp enter/exit response handler |
| * functions. When the converter is disabled, gpio interrupts for the |
| * hpd gpio signal are disabled so it will never execute, unless the |
| * converter is enabled, and the converter is only enabled when the |
| * UFP-D is actively in ALT-DP mode. |
| */ |
| switch (hpd.state) { |
| case LOW_WAIT: |
| /* |
| * In this state only expected event is a level change from low |
| * to high. |
| */ |
| if (level) { |
| hpd.state = HIGH_CHECK; |
| hpd.timer = ts + HPD_T_IRQ_MIN_PULSE; |
| } |
| break; |
| case HIGH_CHECK: |
| /* |
| * In this state if level is high and deglitch timer is |
| * exceeded, then state advances to HIGH_WAIT, otherwise return |
| * to LOW_WAIT state. |
| */ |
| if (!level || (ts <= hpd.timer)) { |
| hpd.state = LOW_WAIT; |
| } else { |
| hpd.state = HIGH_WAIT; |
| hpd_queue_event(hpd_high); |
| } |
| break; |
| case HIGH_WAIT: |
| /* |
| * In this state, only expected event is a level change from |
| * high to low. If current level is low, then advance to |
| * LOW_CHECK for deglitch checking. |
| */ |
| if (!level) { |
| hpd.state = LOW_CHECK; |
| hpd.timer = ts + HPD_T_IRQ_MIN_PULSE; |
| } |
| break; |
| case LOW_CHECK: |
| /* |
| * This state is used to deglitch high->low level |
| * change. However, due to processing latency, it's possible to |
| * detect hpd_irq event if level is high and low pulse width was |
| * valid. |
| */ |
| if (!level) { |
| /* Still low, now wait for IRQ or LOW determination */ |
| hpd.timer = ts + |
| (HPD_T_IRQ_MAX_PULSE - HPD_T_IRQ_MIN_PULSE); |
| hpd.state = IRQ_CHECK; |
| |
| } else { |
| uint64_t irq_ts = hpd.timer + HPD_T_IRQ_MAX_PULSE - |
| HPD_T_IRQ_MIN_PULSE; |
| /* |
| * If hpd is high now, this must have been an edge |
| * event, but still need to determine if the pulse width |
| * is longer than hpd_irq min pulse width. State will |
| * advance to HIGH_WAIT, but if pulse width is < 2 msec, |
| * must send hpd_irq event. |
| */ |
| if ((ts >= hpd.timer) && (ts <= irq_ts)) { |
| /* hpd irq detected */ |
| hpd_queue_event(hpd_irq); |
| } |
| hpd.state = HIGH_WAIT; |
| } |
| break; |
| case IRQ_CHECK: |
| /* |
| * In this state deglitch time has already passed. If current |
| * level is low and hpd_irq timer has expired, then go to |
| * LOW_WAIT as hpd_low event has been detected. If level is high |
| * and low pulse is < hpd_irq, hpd_irq event has been detected. |
| */ |
| if (level) { |
| hpd.state = HIGH_WAIT; |
| if (ts <= hpd.timer) { |
| hpd_queue_event(hpd_irq); |
| } |
| } else if (ts > hpd.timer) { |
| hpd.state = LOW_WAIT; |
| hpd_queue_event(hpd_low); |
| } |
| break; |
| } |
| } |
| |
| static void manage_hpd(void); |
| DECLARE_DEFERRED(manage_hpd); |
| |
| static void manage_hpd(void) |
| { |
| int level; |
| uint64_t ts = get_time().val; |
| uint32_t num_hpd_events = (hpd.edges.head - hpd.edges.tail) & |
| EDGE_QUEUE_MASK; |
| |
| /* |
| * HPD edges are detected via GPIO interrupts. The ISR routine adds edge |
| * info to a queue and scheudles this routine. If this routine is called |
| * without a new edge detected, then it is being called due to a timer |
| * event. |
| */ |
| |
| /* First check to see overflow condition has occurred */ |
| if (hpd.edges.overflow) { |
| /* Disable hpd interrupts */ |
| usb_pd_hpd_converter_enable(0); |
| /* Re-enable hpd converter */ |
| usb_pd_hpd_converter_enable(1); |
| } |
| |
| if (num_hpd_events) { |
| while (num_hpd_events-- > 0) { |
| int idx = hpd.edges.tail; |
| |
| level = hpd.edges.buffer[idx].level; |
| ts = hpd.edges.buffer[idx].ts; |
| |
| hpd_to_pd_converter(level, ts); |
| hpd.edges.tail = (hpd.edges.tail + 1) & EDGE_QUEUE_MASK; |
| } |
| } else { |
| /* no new edge event, so get current time and level */ |
| level = gpio_get_level(hpd_config.signal); |
| ts = get_time().val; |
| hpd_to_pd_converter(level, ts); |
| } |
| |
| /* |
| * If min time spacing requirement is exceeded and a hpd_event is |
| * queued, then send DP_ATTENTION message. |
| */ |
| if (hpd.count > 0) { |
| /* |
| * If at least one hpd event is pending in the queue, send |
| * a DP_ATTENTION message if a DP_CONFIG message has been |
| * received and have passed the minimum spacing interval. |
| */ |
| if (hpd.send_enable && ((get_time().val - hpd.last_send_ts) > |
| HPD_T_MIN_DP_ATTEN)) { |
| /* Generate DP_ATTENTION event pending in queue */ |
| hpd_to_dp_attention(); |
| } else { |
| uint32_t callback_us; |
| |
| /* |
| * Need to wait until until min spacing requirement of |
| * DP attention messages. Set callback time to the min |
| * value required. This callback time could be changed |
| * based on hpd interrupts. |
| * |
| * This wait is also used to prevent a DP_ATTENTION |
| * message from being sent before at least one DP_CONFIG |
| * message has been received. If DP_ATTENTION messages |
| * need to be delayed for this reason, then just wait |
| * the minimum time spacing. |
| */ |
| callback_us = HPD_T_MIN_DP_ATTEN - |
| (get_time().val - hpd.last_send_ts); |
| if (callback_us <= 0 || |
| callback_us > HPD_T_MIN_DP_ATTEN) |
| callback_us = HPD_T_MIN_DP_ATTEN; |
| hook_call_deferred(&manage_hpd_data, callback_us); |
| } |
| } |
| |
| /* |
| * Because of the delay between gpio edge irq, and when those edge |
| * events are processed here, all timers must be done relative to the |
| * timing marker stored in the hpd edge queue. If the state machine |
| * required a new timer, then hpd.timer will be advanced relative to the |
| * ts that was passed into the state machine. |
| * |
| * If the deglitch timer is active, then it can likely already have been |
| * expired when the edge gets processed. So if the timer is active the |
| * deferred callback must be requested. |
| *. |
| */ |
| if (hpd.timer > ts) { |
| uint64_t callback_us = 0; |
| uint64_t now = get_time().val; |
| |
| /* If timer is in the future, adjust the callback timer */ |
| if (now < hpd.timer) |
| callback_us = (hpd.timer - now) & 0xffffffff; |
| |
| hook_call_deferred(&manage_hpd_data, callback_us); |
| } |
| } |
| |
| void usb_pd_hpd_converter_enable(int enable) |
| { |
| /* |
| * The hpd converter should be enabled as part of the UFP-D enter mode |
| * response function. Likewise, the converter should be disabled by the |
| * exit mode function. In addition, the coverter may get disabled so |
| * that it can be reset in the case that the input gpio edges queue |
| * overflows. A muxtex must be used here since this function may be |
| * called from the PD task (enter/exit response mode functions) or from |
| * the hpd event handler state machine (hook task). |
| */ |
| mutex_lock(&hpd_mutex); |
| |
| if (enable) { |
| gpio_disable_interrupt(hpd_config.signal); |
| /* Reset HPD event queue */ |
| hpd.state = LOW_WAIT; |
| hpd.count = 0; |
| hpd.timer = 0; |
| hpd.last_send_ts = 0; |
| hpd.send_enable = 0; |
| |
| /* Reset hpd signal edges queue */ |
| hpd.edges.head = 0; |
| hpd.edges.tail = 0; |
| hpd.edges.overflow = 0; |
| |
| /* If signal is high, need to ensure state machine executes */ |
| if (gpio_get_level(hpd_config.signal)) |
| hook_call_deferred(&manage_hpd_data, 0); |
| |
| /* Enable hpd edge detection */ |
| gpio_enable_interrupt(hpd_config.signal); |
| } else { |
| gpio_disable_interrupt(hpd_config.signal); |
| hook_call_deferred(&manage_hpd_data, -1); |
| } |
| |
| mutex_unlock(&hpd_mutex); |
| } |
| |
| void usb_pd_hpd_edge_event(int signal) |
| { |
| int next_head = (hpd.edges.head + 1) & EDGE_QUEUE_MASK; |
| struct hpd_mark mark; |
| |
| /* Get current timestamp and level */ |
| mark.ts = get_time().val; |
| mark.level = gpio_get_level(hpd_config.signal); |
| |
| /* Add this edge to the buffer if there is space */ |
| if (next_head != hpd.edges.tail) { |
| hpd.edges.buffer[hpd.edges.head].ts = mark.ts; |
| hpd.edges.buffer[hpd.edges.head].level = mark.level; |
| hpd.edges.head = next_head; |
| } else { |
| /* Edge queue is overflowing, need to reset the converter */ |
| hpd.edges.overflow = 1; |
| } |
| /* Schedule HPD state machine to run ASAP */ |
| hook_call_deferred(&manage_hpd_data, 0); |
| } |