| # Copyright 2016 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. |
| |
| import logging |
| import math |
| import time |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server.cros.faft.firmware_test import FirmwareTest |
| from autotest_lib.server.cros.servo import pd_device |
| |
| |
| class firmware_PDVbusRequest(FirmwareTest): |
| """ |
| Servo based USB PD VBUS level test. This test is written to use both |
| the DUT and PDTester test board. It requires that the DUT support |
| dualrole (SRC or SNK) operation. VBUS change requests occur in two |
| methods. |
| |
| The 1st test initiates the VBUS change by using special PDTester |
| feature to send new SRC CAP message. This causes the DUT to request |
| a new VBUS voltage matching what's in the SRC CAP message. |
| |
| The 2nd test configures the DUT in SNK mode and uses the pd console |
| command 'pd 0/1 dev V' command where V is the desired voltage |
| 5/12/20. This test is more risky and won't be executed if the 1st |
| test is failed. If the DUT max input voltage is not 20V, like 12V, |
| and the FAFT config is set wrong, it may negotiate to a voltage |
| higher than it can support, that may damage the DUT. |
| |
| Pass critera is all voltage transitions are successful. |
| |
| """ |
| version = 1 |
| PD_SETTLE_DELAY = 10 |
| USBC_SINK_VOLTAGE = 5 |
| VBUS_TOLERANCE = 0.12 |
| |
| VOLTAGE_SEQUENCE = [5, 9, 10, 12, 15, 20, 15, 12, 9, 5, 20, |
| 5, 5, 9, 9, 10, 10, 12, 12, 15, 15, 20] |
| |
| def _compare_vbus(self, expected_vbus_voltage, ok_to_fail): |
| """Check VBUS using pdtester |
| |
| @param expected_vbus_voltage: nominal VBUS level (in volts) |
| @param ok_to_fail: True to not treat voltage-not-matched as failure. |
| |
| @returns: a tuple containing pass/fail indication and logging string |
| """ |
| # Get Vbus voltage and current |
| vbus_voltage = self.pdtester.vbus_voltage |
| # Compute voltage tolerance range. To handle the case where VBUS is |
| # off, set the minimal tolerance to USBC_SINK_VOLTAGE * VBUS_TOLERANCE. |
| tolerance = (self.VBUS_TOLERANCE * max(expected_vbus_voltage, |
| self.USBC_SINK_VOLTAGE)) |
| voltage_difference = math.fabs(expected_vbus_voltage - vbus_voltage) |
| result_str = 'Target = %02dV:\tAct = %.2f\tDelta = %.2f' % \ |
| (expected_vbus_voltage, vbus_voltage, voltage_difference) |
| # Verify that measured Vbus voltage is within expected range |
| if voltage_difference > tolerance: |
| result = 'ALLOWED_FAIL' if ok_to_fail else 'FAIL' |
| else: |
| result = 'PASS' |
| return result, result_str |
| |
| def _is_batt_full(self): |
| """Check if battery is full |
| |
| @returns: True if battery is full, False otherwise |
| """ |
| self.ec.update_battery_info() |
| return not self.ec.get_battery_charging_allowed(print_result=False) |
| |
| def initialize(self, host, cmdline_args, flip_cc=False, dts_mode=False, |
| init_power_mode=None): |
| super(firmware_PDVbusRequest, self).initialize(host, cmdline_args) |
| # Only run on DUTs that can supply battery power. |
| if not self._client.has_battery(): |
| raise error.TestNAError("DUT type does not have a battery.") |
| self.setup_pdtester(flip_cc, dts_mode) |
| # Only run in normal mode |
| self.switcher.setup_mode('normal') |
| |
| self.shutdown_power_mode = False |
| if init_power_mode: |
| # Set the DUT to suspend or shutdown mode |
| self.set_ap_off_power_mode(init_power_mode) |
| if init_power_mode == "shutdown": |
| self.shutdown_power_mode = True |
| |
| self.usbpd.send_command('chan 0') |
| logging.info('Disallow PR_SWAP request from DUT') |
| self.pdtester.allow_pr_swap(False) |
| |
| def cleanup(self): |
| logging.info('Allow PR_SWAP request from DUT') |
| self.pdtester.allow_pr_swap(True) |
| # Set back to the max 20V SRC mode at the end. |
| self.pdtester.charge(self.pdtester.USBC_MAX_VOLTAGE) |
| |
| self.usbpd.send_command('chan 0xffffffff') |
| self.restore_ap_on_power_mode() |
| super(firmware_PDVbusRequest, self).cleanup() |
| |
| def run_once(self): |
| """Exectue VBUS request test. |
| |
| """ |
| consoles = [self.usbpd, self.pdtester] |
| port_partner = pd_device.PDPortPartner(consoles) |
| |
| # Identify a valid test port pair |
| port_pair = port_partner.identify_pd_devices() |
| if not port_pair: |
| raise error.TestFail('No PD connection found!') |
| |
| for port in port_pair: |
| if port.is_pdtester: |
| self.pdtester_port = port |
| else: |
| self.dut_port = port |
| |
| dut_connect_state = self.dut_port.get_pd_state() |
| logging.info('Initial DUT connect state = %s', dut_connect_state) |
| |
| if not self.dut_port.is_connected(dut_connect_state): |
| raise error.TestFail("pd connection not found") |
| |
| dut_voltage_limit = self.faft_config.usbc_input_voltage_limit |
| dut_power_voltage_limit = dut_voltage_limit |
| dut_shutdown_and_full_batt_voltage_limit = ( |
| self.faft_config.usbc_voltage_on_shutdown_and_full_batt) |
| |
| is_override = self.faft_config.charger_profile_override |
| if is_override: |
| logging.info('*** Custom charger profile takes over, which may ' |
| 'cause voltage-not-matched. It is OK to fail. *** ') |
| |
| # Test will expect reduced voltage when battery is full and...: |
| # 1. We are running 'shutdown' variant of PDVbusRequest test (indicated |
| # by self.shutdown_power_mode) |
| # 2. EC has battery capability |
| # 3. 'dut_shutdown_and_full_batt_voltage_limit' value will be less than |
| # 'dut_voltage_limit'. By default reduced voltage is set to maximum |
| # voltage which means that no limit applies. Every board needs to |
| # override this to correct value (most likely 5 or 9 volts) |
| is_voltage_reduced_if_batt_full = ( |
| self.shutdown_power_mode |
| and self.check_ec_capability(['battery']) and |
| dut_shutdown_and_full_batt_voltage_limit < dut_voltage_limit) |
| if is_voltage_reduced_if_batt_full: |
| logging.info( |
| '*** This DUT may reduce input voltage to %d volts ' |
| 'when battery is full. ***', |
| dut_shutdown_and_full_batt_voltage_limit) |
| |
| # Obtain voltage limit due to maximum charging power. Note that this |
| # voltage limit applies only when EC follows the default policy. There |
| # are other policies like PREFER_LOW_VOLTAGE or PREFER_HIGH_VOLTAGE but |
| # they are not implemented in this test. |
| try: |
| srccaps = self.pdtester.get_adapter_source_caps() |
| dut_max_charging_power = self.faft_config.max_charging_power |
| selected_voltage = 0 |
| selected_power = 0 |
| for (mv, ma) in srccaps: |
| voltage = mv / 1000.0 |
| current = ma / 1000.0 |
| power = min(voltage * current, dut_max_charging_power) |
| |
| if (voltage > dut_voltage_limit or power <= selected_power): |
| continue |
| selected_voltage = voltage |
| selected_power = power |
| |
| if selected_voltage < dut_power_voltage_limit: |
| dut_power_voltage_limit = selected_voltage |
| logging.info( |
| 'EC may request maximum %dV due to adapter\'s max ' |
| 'supported power and DUT\'s power constraints. DUT\'s ' |
| 'max charging power %dW. Selected charging power %dW', |
| dut_power_voltage_limit, dut_max_charging_power, |
| selected_power) |
| except self.pdtester.PDTesterError: |
| logging.warn('Unable to get charging voltages and currents. ' |
| 'Test may fail on high voltages.') |
| |
| pdtester_failures = [] |
| logging.info('Start PDTester initiated tests') |
| charging_voltages = self.pdtester.get_charging_voltages() |
| |
| if dut_voltage_limit not in charging_voltages: |
| raise error.TestError('Plugged a wrong charger to servo v4? ' |
| '%dV not in supported voltages %s.' % |
| (dut_voltage_limit, str(charging_voltages))) |
| |
| for voltage in charging_voltages: |
| logging.info('********* %r *********', voltage) |
| # Set charging voltage |
| self.pdtester.charge(voltage) |
| # Wait for new PD contract to be established |
| time.sleep(self.PD_SETTLE_DELAY) |
| # Get current PDTester PD state |
| pdtester_state = self.pdtester_port.get_pd_state() |
| # If PDTester is in SNK mode and the DUT is in S0, the DUT should |
| # source VBUS = USBC_SINK_VOLTAGE. If PDTester is in SNK mode, and |
| # the DUT is not in S0, the DUT shouldn't source VBUS, which means |
| # VBUS = 0. |
| if self.pdtester_port.is_snk(pdtester_state): |
| expected_vbus_voltage = (self.USBC_SINK_VOLTAGE |
| if self.get_power_state() == 'S0' else 0) |
| ok_to_fail = False |
| elif (is_voltage_reduced_if_batt_full and self._is_batt_full()): |
| expected_vbus_voltage = min( |
| voltage, dut_shutdown_and_full_batt_voltage_limit) |
| ok_to_fail = False |
| else: |
| expected_vbus_voltage = min(voltage, dut_voltage_limit) |
| ok_to_fail = is_override or voltage > dut_power_voltage_limit |
| |
| result, result_str = self._compare_vbus(expected_vbus_voltage, |
| ok_to_fail) |
| logging.info('%s, %s', result_str, result) |
| if result == 'FAIL': |
| pdtester_failures.append(result_str) |
| |
| # PDTester is set back to 20V SRC mode. |
| self.pdtester.charge(self.pdtester.USBC_MAX_VOLTAGE) |
| time.sleep(self.PD_SETTLE_DELAY) |
| |
| if pdtester_failures: |
| logging.error('PDTester voltage source cap failures') |
| for fail in pdtester_failures: |
| logging.error('%s', fail) |
| number = len(pdtester_failures) |
| raise error.TestFail('PDTester failed %d times' % number) |
| |
| if (is_voltage_reduced_if_batt_full and self._is_batt_full()): |
| logging.warn('This DUT reduces input voltage when chipset is in ' |
| 'G3/S5 and battery is full. DUT initiated tests ' |
| 'will be skipped. Please discharge battery to level ' |
| 'that allows charging and run this test again') |
| return |
| |
| # The DUT must be in SNK mode for the pd <port> dev <voltage> |
| # command to have an effect. |
| if not self.dut_port.is_snk(): |
| # DUT needs to be in SINK Mode, attempt to force change |
| self.dut_port.drp_set('snk') |
| time.sleep(self.PD_SETTLE_DELAY) |
| if not self.dut_port.is_snk(): |
| raise error.TestFail("DUT not able to connect in SINK mode") |
| |
| logging.info('Start of DUT initiated tests') |
| dut_failures = [] |
| for v in self.VOLTAGE_SEQUENCE: |
| if v > dut_voltage_limit: |
| logging.info('Target = %02dV: skipped, over the limit %0dV', |
| v, dut_voltage_limit) |
| continue |
| if v not in charging_voltages: |
| logging.info( |
| 'Target = %02dV: skipped, voltage unsupported, ' |
| 'update hdctools and servo_v4 firmware ' |
| 'or attach a different charger', v) |
| continue |
| # Build 'pd <port> dev <voltage> command |
| cmd = 'pd %d dev %d' % (self.dut_port.port, v) |
| self.dut_port.utils.send_pd_command(cmd) |
| time.sleep(self.PD_SETTLE_DELAY) |
| ok_to_fail = is_override or v > dut_power_voltage_limit |
| result, result_str = self._compare_vbus(v, ok_to_fail) |
| logging.info('%s, %s', result_str, result) |
| if result == 'FAIL': |
| dut_failures.append(result_str) |
| |
| # Make sure DUT is set back to its max voltage so DUT will accept all |
| # options |
| cmd = 'pd %d dev %d' % (self.dut_port.port, dut_voltage_limit) |
| self.dut_port.utils.send_pd_command(cmd) |
| time.sleep(self.PD_SETTLE_DELAY) |
| # The next group of tests need DUT to connect in SNK and SRC modes |
| self.dut_port.drp_set('on') |
| |
| if dut_failures: |
| logging.error('DUT voltage request failures') |
| for fail in dut_failures: |
| logging.error('%s', fail) |
| number = len(dut_failures) |
| raise error.TestFail('DUT failed %d times' % number) |