/* Copyright 2014 The Chromium OS Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "adc.h" #include "common.h" #include "console.h" #include "debug_printf.h" #include "ec_commands.h" #include "hooks.h" #include "registers.h" #include "system.h" #include "task.h" #include "timer.h" #include "util.h" #include "usb_pd.h" /* ------------------------- Power supply control ------------------------ */ /* GPIO level setting helpers through BSRR register */ #define GPIO_SET(n) (1 << (n)) #define GPIO_RESET(n) (1 << ((n) + 16)) /* Output voltage selection */ enum volt { VO_5V = GPIO_RESET(13) | GPIO_RESET(14), VO_12V = GPIO_SET(13) | GPIO_RESET(14), VO_13V = GPIO_RESET(13) | GPIO_SET(14), VO_20V = GPIO_SET(13) | GPIO_SET(14), }; static inline void set_output_voltage(enum volt v) { /* set voltage_select on PA13/PA14 */ STM32_GPIO_BSRR(GPIO_A) = v; } static inline void output_enable(void) { /* GPF0 (enable OR'ing FETs) = 1 */ STM32_GPIO_BSRR(GPIO_F) = GPIO_SET(0); } static inline void output_disable(void) { /* GPF0 (disable OR'ing FETs) = 0 */ STM32_GPIO_BSRR(GPIO_F) = GPIO_RESET(0); } static inline int output_is_enabled(void) { /* GPF0 = enable output FET */ return STM32_GPIO_ODR(GPIO_F) & 1; } /* ----- fault conditions ----- */ enum faults { FAULT_OK = 0, FAULT_OCP, /* Over-Current Protection */ FAULT_FAST_OCP, /* Over-Current Protection for interrupt context */ FAULT_OVP, /* Under or Over-Voltage Protection */ FAULT_DISCHARGE, /* Discharge was ineffective */ }; /* current fault condition */ static enum faults fault; /* expiration date of the last fault condition */ static timestamp_t fault_deadline; /* ADC in 12-bit mode */ #define ADC_SCALE BIT(12) /* ADC power supply : VDDA = 3.3V */ #define VDDA_MV 3300 /* Current sense resistor : 5 milliOhm */ #define R_SENSE 5 /* VBUS voltage is measured through 10k / 100k voltage divider = /11 */ #define VOLT_DIV ((10+100)/10) /* The current sensing op-amp has a x100 gain */ #define CURR_GAIN 100 /* convert VBUS voltage in raw ADC value */ #define VBUS_MV(mv) ((mv)*ADC_SCALE/VOLT_DIV/VDDA_MV) /* convert VBUS current in raw ADC value */ #define VBUS_MA(ma) ((ma)*ADC_SCALE*R_SENSE/1000*CURR_GAIN/VDDA_MV) /* convert raw ADC value to mA */ #define ADC_TO_CURR_MA(vbus) ((vbus)*1000/(ADC_SCALE*R_SENSE)*VDDA_MV/CURR_GAIN) /* convert raw ADC value to mV */ #define ADC_TO_VOLT_MV(vbus) ((vbus)*VOLT_DIV*VDDA_MV/ADC_SCALE) /* Max current */ #if defined(BOARD_ZINGER) #define RATED_CURRENT 3000 #elif defined(BOARD_MINIMUFFIN) #define RATED_CURRENT 2250 #endif /* Max current : 20% over rated current */ #define MAX_CURRENT VBUS_MA(RATED_CURRENT * 6/5) /* Fast short circuit protection : 50% over rated current */ #define MAX_CURRENT_FAST VBUS_MA(RATED_CURRENT * 3/2) /* reset over-current after 1 second */ #define OCP_TIMEOUT SECOND /* Threshold below which we stop fast OCP to save power */ #define SINK_IDLE_CURRENT VBUS_MA(500 /* mA */) /* Under-voltage limit is 0.8x Vnom */ #define UVP_MV(mv) VBUS_MV((mv) * 8 / 10) /* Over-voltage limit is 1.2x Vnom */ #define OVP_MV(mv) VBUS_MV((mv) * 12 / 10) /* Over-voltage recovery threshold is 1.1x Vnom */ #define OVP_REC_MV(mv) VBUS_MV((mv) * 11 / 10) /* Maximum discharging delay */ #define DISCHARGE_TIMEOUT (275*MSEC) /* Voltage overshoot below the OVP threshold for discharging to avoid OVP */ #define DISCHARGE_OVERSHOOT_MV VBUS_MV(200) /* Time to wait after last RX edge interrupt before allowing deep sleep */ #define PD_RX_SLEEP_TIMEOUT (100*MSEC) /* ----- output voltage discharging ----- */ /* expiration date of the discharge */ static timestamp_t discharge_deadline; static inline void discharge_enable(void) { STM32_GPIO_BSRR(GPIO_F) = GPIO_SET(1); } static inline void discharge_disable(void) { STM32_GPIO_BSRR(GPIO_F) = GPIO_RESET(1); adc_disable_watchdog(); } static inline int discharge_is_enabled(void) { /* GPF1 = enable discharge FET */ return STM32_GPIO_ODR(GPIO_F) & 2; } static void discharge_voltage(int target_volt) { discharge_enable(); discharge_deadline.val = get_time().val + DISCHARGE_TIMEOUT; /* Monitor VBUS voltage */ target_volt -= DISCHARGE_OVERSHOOT_MV; disable_sleep(SLEEP_MASK_USB_PWR); adc_enable_watchdog(ADC_CH_V_SENSE, 0xFFF, target_volt); } /* ----------------------- USB Power delivery policy ---------------------- */ #define PDO_FIXED_FLAGS (PDO_FIXED_UNCONSTRAINED | PDO_FIXED_DATA_SWAP) /* Voltage indexes for the PDOs */ enum volt_idx { PDO_IDX_5V = 0, PDO_IDX_12V = 1, PDO_IDX_20V = 2, PDO_IDX_COUNT }; /* Power Delivery Objects */ const uint32_t pd_src_pdo[] = { [PDO_IDX_5V] = PDO_FIXED(5000, RATED_CURRENT, PDO_FIXED_FLAGS), [PDO_IDX_12V] = PDO_FIXED(12000, RATED_CURRENT, PDO_FIXED_FLAGS), [PDO_IDX_20V] = PDO_FIXED(20000, RATED_CURRENT, PDO_FIXED_FLAGS), }; const int pd_src_pdo_cnt = ARRAY_SIZE(pd_src_pdo); BUILD_ASSERT(ARRAY_SIZE(pd_src_pdo) == PDO_IDX_COUNT); /* PDO voltages (should match the table above) */ static const struct { enum volt select; /* GPIO configuration to select the voltage */ int uvp; /* under-voltage limit in mV */ int ovp; /* over-voltage limit in mV */ int ovp_rec;/* over-voltage recovery threshold in mV */ } voltages[ARRAY_SIZE(pd_src_pdo)] = { [PDO_IDX_5V] = {VO_5V, UVP_MV(5000), OVP_MV(5000), OVP_REC_MV(5000)}, [PDO_IDX_12V] = {VO_12V, UVP_MV(12000), OVP_MV(12000), OVP_REC_MV(12000)}, [PDO_IDX_20V] = {VO_20V, UVP_MV(20000), OVP_MV(20000), OVP_REC_MV(20000)}, }; /* current and previous selected PDO entry */ static int volt_idx; static int last_volt_idx; /* target voltage at the end of discharge */ static int discharge_volt_idx; /* output current measurement */ int vbus_amp; __override int pd_board_check_request(uint32_t rdo, int pdo_cnt) { /* fault condition or output disabled: reject transitions */ if (fault != FAULT_OK || !output_is_enabled()) return EC_ERROR_INVAL; return EC_SUCCESS; } void pd_transition_voltage(int idx) { last_volt_idx = volt_idx; volt_idx = idx - 1; if (volt_idx < last_volt_idx) { /* down voltage transition */ /* Stop OCP monitoring */ adc_disable_watchdog(); discharge_volt_idx = volt_idx; /* from 20V : do an intermediate step at 12V */ if (volt_idx == PDO_IDX_5V && last_volt_idx == PDO_IDX_20V) volt_idx = PDO_IDX_12V; discharge_voltage(voltages[volt_idx].ovp); } else if (volt_idx > last_volt_idx) { /* up voltage transition */ if (discharge_is_enabled()) { /* Make sure discharging is disabled */ discharge_disable(); /* Enable over-current monitoring */ adc_enable_watchdog(ADC_CH_A_SENSE, MAX_CURRENT_FAST, 0); } } set_output_voltage(voltages[volt_idx].select); } int pd_set_power_supply_ready(int port) { /* fault condition not cleared : do not turn on power */ if ((fault != FAULT_OK) || discharge_is_enabled()) return EC_ERROR_INVAL; output_enable(); /* Over-current monitoring */ adc_enable_watchdog(ADC_CH_A_SENSE, MAX_CURRENT_FAST, 0); return EC_SUCCESS; /* we are ready */ } void pd_power_supply_reset(int port) { int need_discharge = (volt_idx > 0) || discharge_is_enabled(); output_disable(); last_volt_idx = volt_idx; /* from 20V : do an intermediate step at 12V */ volt_idx = volt_idx == PDO_IDX_20V ? PDO_IDX_12V : PDO_IDX_5V; set_output_voltage(voltages[volt_idx].select); /* TODO transition delay */ /* Stop OCP monitoring to save power */ adc_disable_watchdog(); /* discharge voltage to 5V ? */ if (need_discharge) { /* final target : 5V */ discharge_volt_idx = PDO_IDX_5V; discharge_voltage(voltages[volt_idx].ovp); } } int pd_check_data_swap(int port, enum pd_data_role data_role) { /* Allow data swap if we are a DFP, otherwise don't allow */ return (data_role == PD_ROLE_DFP) ? 1 : 0; } void pd_execute_data_swap(int port, enum pd_data_role data_role) { /* Do nothing */ } void pd_check_pr_role(int port, enum pd_power_role pr_role, int flags) { } void pd_check_dr_role(int port, enum pd_data_role dr_role, int flags) { /* If DFP, try to switch to UFP */ if ((flags & PD_FLAGS_PARTNER_DR_DATA) && dr_role == PD_ROLE_DFP) pd_request_data_swap(port); } int pd_board_checks(void) { #ifdef CONFIG_HIBERNATE static timestamp_t hib_to; static int hib_to_ready; #endif int vbus_volt; int ovp_idx; /* Reload the watchdog */ STM32_IWDG_KR = STM32_IWDG_KR_RELOAD; #ifdef CONFIG_HIBERNATE /* If output is disabled for long enough, then hibernate */ if (!pd_is_connected(0) && hib_to_ready) { if (get_time().val >= hib_to.val) { debug_printf("hib\n"); __enter_hibernate(0, 0); } } else { hib_to.val = get_time().val + 60*SECOND; hib_to_ready = 1; } #endif /* if it's been a while since last RX edge, then allow deep sleep */ if (get_time_since_last_edge(0) > PD_RX_SLEEP_TIMEOUT) enable_sleep(SLEEP_MASK_USB_PD); vbus_volt = adc_read_channel(ADC_CH_V_SENSE); vbus_amp = adc_read_channel(ADC_CH_A_SENSE); if (fault == FAULT_FAST_OCP) { debug_printf("Fast OCP\n"); pd_log_event(PD_EVENT_PS_FAULT, 0, PS_FAULT_FAST_OCP, NULL); fault = FAULT_OCP; /* reset over-current after 1 second */ fault_deadline.val = get_time().val + OCP_TIMEOUT; return EC_ERROR_INVAL; } if (vbus_amp > MAX_CURRENT) { /* 3 more samples to check whether this is just a transient */ int count; for (count = 0; count < 3; count++) if (adc_read_channel(ADC_CH_A_SENSE) < MAX_CURRENT) break; /* trigger the slow OCP iff all 4 samples are above the max */ if (count == 3) { debug_printf("OCP %d mA\n", vbus_amp * VDDA_MV / CURR_GAIN * 1000 / R_SENSE / ADC_SCALE); pd_log_event(PD_EVENT_PS_FAULT, 0, PS_FAULT_OCP, NULL); fault = FAULT_OCP; /* reset over-current after 1 second */ fault_deadline.val = get_time().val + OCP_TIMEOUT; return EC_ERROR_INVAL; } } /* * Optimize power consumption when the sink is idle : * Enable STOP mode while we are connected, * this kills fast OCP as the actual ADC conversion for the analog * watchdog will happen on the next wake-up (x0 ms latency). */ if (vbus_amp < SINK_IDLE_CURRENT && !discharge_is_enabled()) /* override the PD state machine sleep mask */ enable_sleep(SLEEP_MASK_USB_PWR); else if (vbus_amp > SINK_IDLE_CURRENT) disable_sleep(SLEEP_MASK_USB_PWR); /* * Set the voltage index to use for checking OVP. During a down step * transition, use the previous voltage index to check for OVP. */ ovp_idx = discharge_is_enabled() ? last_volt_idx : volt_idx; if ((output_is_enabled() && (vbus_volt > voltages[ovp_idx].ovp)) || (fault && (vbus_volt > voltages[ovp_idx].ovp_rec))) { if (!fault) { debug_printf("OVP %d mV\n", ADC_TO_VOLT_MV(vbus_volt)); pd_log_event(PD_EVENT_PS_FAULT, 0, PS_FAULT_OVP, NULL); } fault = FAULT_OVP; /* no timeout */ fault_deadline.val = get_time().val; return EC_ERROR_INVAL; } /* the discharge did not work properly */ if (discharge_is_enabled() && (get_time().val > discharge_deadline.val)) { /* ensure we always finish a 2-step discharge */ volt_idx = discharge_volt_idx; set_output_voltage(voltages[volt_idx].select); /* stop it */ discharge_disable(); /* enable over-current monitoring */ adc_enable_watchdog(ADC_CH_A_SENSE, MAX_CURRENT_FAST, 0); debug_printf("Disch FAIL %d mV\n", ADC_TO_VOLT_MV(vbus_volt)); pd_log_event(PD_EVENT_PS_FAULT, 0, PS_FAULT_DISCH, NULL); fault = FAULT_DISCHARGE; /* reset it after 1 second */ fault_deadline.val = get_time().val + OCP_TIMEOUT; return EC_ERROR_INVAL; } /* everything is good *and* the error condition has expired */ if ((fault != FAULT_OK) && (get_time().val > fault_deadline.val)) { fault = FAULT_OK; debug_printf("Reset fault\n"); /* * Reset the PD state and communication on both side, * so we can now re-negociate a voltage. */ return EC_ERROR_INVAL; } return EC_SUCCESS; } void pd_adc_interrupt(void) { /* Clear flags */ STM32_ADC_ISR = 0x8e; if (discharge_is_enabled()) { if (discharge_volt_idx != volt_idx) { /* first step of the discharge completed: now 12V->5V */ volt_idx = PDO_IDX_5V; set_output_voltage(VO_5V); discharge_voltage(voltages[PDO_IDX_5V].ovp); } else { /* discharge complete */ discharge_disable(); /* enable over-current monitoring */ adc_enable_watchdog(ADC_CH_A_SENSE, MAX_CURRENT_FAST, 0); } } else {/* Over-current detection */ /* cut the power output */ pd_power_supply_reset(0); /* record a special fault */ fault = FAULT_FAST_OCP; /* pd_board_checks() will record the timeout later */ } /* clear ADC irq so we don't get a second interrupt */ task_clear_pending_irq(STM32_IRQ_ADC_COMP); } DECLARE_IRQ(STM32_IRQ_ADC_COMP, pd_adc_interrupt, 1); /* ----------------- Vendor Defined Messages ------------------ */ const uint32_t vdo_idh = VDO_IDH(0, /* data caps as USB host */ 0, /* data caps as USB device */ IDH_PTYPE_UNDEF, /* Undefined */ 1, /* supports alt modes */ USB_VID_GOOGLE); const uint32_t vdo_product = VDO_PRODUCT(CONFIG_USB_PID, CONFIG_USB_BCD_DEV); /* When set true, we are in GFU mode */ static int gfu_mode; static int svdm_response_identity(int port, uint32_t *payload) { payload[VDO_I(IDH)] = vdo_idh; payload[VDO_I(CSTAT)] = VDO_CSTAT(0); payload[VDO_I(PRODUCT)] = vdo_product; return VDO_I(PRODUCT) + 1; } static int svdm_response_svids(int port, uint32_t *payload) { payload[1] = VDO_SVID(USB_VID_GOOGLE, 0); return 2; } /* Will only ever be a single mode for this device */ #define MODE_CNT 1 #define OPOS 1 const uint32_t vdo_dp_mode[MODE_CNT] = { VDO_MODE_GOOGLE(MODE_GOOGLE_FU) }; static int svdm_response_modes(int port, uint32_t *payload) { if (PD_VDO_VID(payload[0]) != USB_VID_GOOGLE) return 0; /* nak */ memcpy(payload + 1, vdo_dp_mode, sizeof(vdo_dp_mode)); return MODE_CNT + 1; } static int svdm_enter_mode(int port, uint32_t *payload) { /* SID & mode request is valid */ if ((PD_VDO_VID(payload[0]) != USB_VID_GOOGLE) || (PD_VDO_OPOS(payload[0]) != OPOS)) return 0; /* will generate NAK */ gfu_mode = 1; debug_printf("GFU\n"); return 1; } static int svdm_exit_mode(int port, uint32_t *payload) { gfu_mode = 0; return 1; /* Must return ACK */ } static struct amode_fx dp_fx = { .status = NULL, .config = NULL, }; const struct svdm_response svdm_rsp = { .identity = &svdm_response_identity, .svids = &svdm_response_svids, .modes = &svdm_response_modes, .enter_mode = &svdm_enter_mode, .amode = &dp_fx, .exit_mode = &svdm_exit_mode, }; __override int pd_custom_vdm(int port, int cnt, uint32_t *payload, uint32_t **rpayload) { int cmd = PD_VDO_CMD(payload[0]); int rsize; if (PD_VDO_VID(payload[0]) != USB_VID_GOOGLE || !gfu_mode) return 0; debug_printf("%pT] VDM/%d [%d] %08x\n", PRINTF_TIMESTAMP_NOW, cnt, cmd, payload[0]); *rpayload = payload; rsize = pd_custom_flash_vdm(port, cnt, payload); if (!rsize) { switch (cmd) { case VDO_CMD_PING_ENABLE: pd_ping_enable(0, payload[1]); rsize = 1; break; case VDO_CMD_CURRENT: /* return last measured current */ payload[1] = ADC_TO_CURR_MA(vbus_amp); rsize = 2; break; case VDO_CMD_GET_LOG: rsize = pd_vdm_get_log_entry(payload); break; default: /* Unknown : do not answer */ return 0; } } /* respond (positively) to the request */ payload[0] |= VDO_SRC_RESPONDER; return rsize; }