xref: /aosp_15_r20/external/autotest/server/site_tests/firmware_PDVbusRequest/firmware_PDVbusRequest.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Copyright 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import math
7import time
8
9from autotest_lib.client.common_lib import error
10from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
11from autotest_lib.server.cros.servo import pd_device
12
13
14class firmware_PDVbusRequest(FirmwareTest):
15    """
16    Servo based USB PD VBUS level test. This test is written to use both
17    the DUT and PDTester test board. It requires that the DUT support
18    dualrole (SRC or SNK) operation. VBUS change requests occur in two
19    methods.
20
21    The 1st test initiates the VBUS change by using special PDTester
22    feature to send new SRC CAP message. This causes the DUT to request
23    a new VBUS voltage matching what's in the SRC CAP message.
24
25    The 2nd test configures the DUT in SNK mode and uses the pd console
26    command 'pd 0/1 dev V' command where V is the desired voltage
27    5/12/20. This test is more risky and won't be executed if the 1st
28    test is failed. If the DUT max input voltage is not 20V, like 12V,
29    and the FAFT config is set wrong, it may negotiate to a voltage
30    higher than it can support, that may damage the DUT.
31
32    Pass critera is all voltage transitions are successful.
33
34    """
35    version = 1
36    PD_SETTLE_DELAY = 10
37    USBC_SINK_VOLTAGE = 5
38    VBUS_TOLERANCE = 0.12
39
40    VOLTAGE_SEQUENCE = [5, 9, 10, 12, 15, 20, 15, 12, 9, 5, 20,
41                        5, 5, 9, 9, 10, 10, 12, 12, 15, 15, 20]
42
43    def _compare_vbus(self, expected_vbus_voltage, ok_to_fail):
44        """Check VBUS using pdtester
45
46        @param expected_vbus_voltage: nominal VBUS level (in volts)
47        @param ok_to_fail: True to not treat voltage-not-matched as failure.
48
49        @returns: a tuple containing pass/fail indication and logging string
50        """
51        # Get Vbus voltage and current
52        vbus_voltage = self.pdtester.vbus_voltage
53        # Compute voltage tolerance range. To handle the case where VBUS is
54        # off, set the minimal tolerance to USBC_SINK_VOLTAGE * VBUS_TOLERANCE.
55        tolerance = (self.VBUS_TOLERANCE * max(expected_vbus_voltage,
56                                               self.USBC_SINK_VOLTAGE))
57        voltage_difference = math.fabs(expected_vbus_voltage - vbus_voltage)
58        result_str = 'Target = %02dV:\tAct = %.2f\tDelta = %.2f' % \
59                     (expected_vbus_voltage, vbus_voltage, voltage_difference)
60        # Verify that measured Vbus voltage is within expected range
61        if voltage_difference > tolerance:
62            result = 'ALLOWED_FAIL' if ok_to_fail else 'FAIL'
63        else:
64            result = 'PASS'
65        return result, result_str
66
67    def _is_batt_full(self):
68        """Check if battery is full
69
70        @returns: True if battery is full, False otherwise
71        """
72        self.ec.update_battery_info()
73        return not self.ec.get_battery_charging_allowed(print_result=False)
74
75    def _enable_dps(self, en):
76        """Enable/disable Dynamic PDO Selection
77
78        @param en: a bool, True for enable, disable otherwise.
79
80        """
81        self.usbpd.send_command('dps %s' % ('en' if en else 'dis'))
82
83    def initialize(self, host, cmdline_args, flip_cc=False, dts_mode=False,
84                   init_power_mode=None):
85        super(firmware_PDVbusRequest, self).initialize(host, cmdline_args)
86        # Only run on DUTs that can supply battery power.
87        if not self._client.has_battery():
88            raise error.TestNAError("DUT type does not have a battery.")
89        self.setup_pdtester(flip_cc, dts_mode)
90        # Only run in normal mode
91        self.switcher.setup_mode('normal')
92
93        self.shutdown_power_mode = False
94        if init_power_mode:
95            # Set the DUT to suspend or shutdown mode
96            self.set_ap_off_power_mode(init_power_mode)
97            if init_power_mode == "shutdown":
98                self.shutdown_power_mode = True
99
100        self.usbpd.send_command('chan 0')
101        logging.info('Disallow PR_SWAP request from DUT')
102        self.pdtester.allow_pr_swap(False)
103        # Disable dynamic PDO selection for voltage testing
104        self._enable_dps(False)
105
106    def cleanup(self):
107        logging.info('Allow PR_SWAP request from DUT')
108        self.pdtester.allow_pr_swap(True)
109        # Re-enable DPS
110        self._enable_dps(True)
111        # Set back to the max 20V SRC mode at the end.
112        self.pdtester.charge(self.pdtester.USBC_MAX_VOLTAGE)
113
114        self.usbpd.send_command('chan 0xffffffff')
115        self.restore_ap_on_power_mode()
116        super(firmware_PDVbusRequest, self).cleanup()
117
118    def run_once(self):
119        """Exectue VBUS request test.
120
121        """
122        consoles = [self.usbpd, self.pdtester]
123        port_partner = pd_device.PDPortPartner(consoles)
124
125        # Identify a valid test port pair
126        port_pair = port_partner.identify_pd_devices()
127        if not port_pair:
128            raise error.TestFail('No PD connection found!')
129
130        for port in port_pair:
131            if port.is_pdtester:
132                self.pdtester_port = port
133            else:
134                self.dut_port = port
135
136        dut_connect_state = self.dut_port.get_pd_state()
137        logging.info('Initial DUT connect state = %s', dut_connect_state)
138
139        if not self.dut_port.is_connected(dut_connect_state):
140            raise error.TestFail("pd connection not found")
141
142        dut_voltage_limit = self.faft_config.usbc_input_voltage_limit
143        dut_power_voltage_limit = dut_voltage_limit
144        dut_shutdown_and_full_batt_voltage_limit = (
145                self.faft_config.usbc_voltage_on_shutdown_and_full_batt)
146
147        is_override = self.faft_config.charger_profile_override
148        if is_override:
149            logging.info('*** Custom charger profile takes over, which may '
150                         'cause voltage-not-matched. It is OK to fail. *** ')
151
152        # Test will expect reduced voltage when battery is full and...:
153        # 1. We are running 'shutdown' variant of PDVbusRequest test (indicated
154        #    by self.shutdown_power_mode)
155        # 2. EC has battery capability
156        # 3. 'dut_shutdown_and_full_batt_voltage_limit' value will be less than
157        #    'dut_voltage_limit'. By default reduced voltage is set to maximum
158        #    voltage which means that no limit applies. Every board needs to
159        #    override this to correct value (most likely 5 or 9 volts)
160        is_voltage_reduced_if_batt_full = (
161                self.shutdown_power_mode
162                and self.check_ec_capability(['battery']) and
163                dut_shutdown_and_full_batt_voltage_limit < dut_voltage_limit)
164        if is_voltage_reduced_if_batt_full:
165            logging.info(
166                    '*** This DUT may reduce input voltage to %d volts '
167                    'when battery is full. ***',
168                    dut_shutdown_and_full_batt_voltage_limit)
169
170        # Obtain voltage limit due to maximum charging power. Note that this
171        # voltage limit applies only when EC follows the default policy. There
172        # are other policies like PREFER_LOW_VOLTAGE or PREFER_HIGH_VOLTAGE but
173        # they are not implemented in this test.
174        try:
175            srccaps = self.pdtester.get_adapter_source_caps()
176            dut_max_charging_power = self.faft_config.max_charging_power
177            selected_voltage = 0
178            selected_power = 0
179            for (mv, ma) in srccaps:
180                voltage = mv / 1000.0
181                current = ma / 1000.0
182                power = voltage * current
183
184                if (voltage > dut_voltage_limit or power <= selected_power
185                            or power > dut_max_charging_power):
186                    continue
187                selected_voltage = voltage
188                selected_power = power
189
190            if selected_voltage < dut_power_voltage_limit:
191                dut_power_voltage_limit = selected_voltage
192                logging.info(
193                        'EC may request maximum %dV due to adapter\'s max '
194                        'supported power and DUT\'s power constraints. DUT\'s '
195                        'max charging power %dW. Selected charging power %dW',
196                        dut_power_voltage_limit, dut_max_charging_power,
197                        selected_power)
198        except self.pdtester.PDTesterError:
199            logging.warning('Unable to get charging voltages and currents. '
200                         'Test may fail on high voltages.')
201
202        pdtester_failures = []
203        logging.info('Start PDTester initiated tests')
204        charging_voltages = self.pdtester.get_charging_voltages()
205
206        if dut_voltage_limit not in charging_voltages:
207            raise error.TestError('Plugged a wrong charger to servo v4? '
208                                  '%dV not in supported voltages %s.' %
209                                  (dut_voltage_limit, str(charging_voltages)))
210
211        for voltage in charging_voltages:
212            logging.info('********* %r *********', voltage)
213            # Set charging voltage
214            self.pdtester.charge(voltage)
215            # Wait for new PD contract to be established
216            time.sleep(self.PD_SETTLE_DELAY)
217            # Get current PDTester PD state
218            pdtester_state = self.pdtester_port.get_pd_state()
219            # If PDTester is in SNK mode and the DUT is in S0, the DUT should
220            # source VBUS = USBC_SINK_VOLTAGE. If PDTester is in SNK mode, and
221            # the DUT is not in S0, the DUT shouldn't source VBUS, which means
222            # VBUS = 0.
223            if self.pdtester_port.is_snk(pdtester_state):
224                expected_vbus_voltage = (self.USBC_SINK_VOLTAGE
225                        if self.get_power_state() == 'S0' else 0)
226                ok_to_fail = False
227            elif (is_voltage_reduced_if_batt_full and self._is_batt_full()):
228                expected_vbus_voltage = min(
229                        voltage, dut_shutdown_and_full_batt_voltage_limit)
230                ok_to_fail = False
231            else:
232                expected_vbus_voltage = min(voltage, dut_voltage_limit)
233                ok_to_fail = is_override or voltage > dut_power_voltage_limit
234
235            result, result_str = self._compare_vbus(expected_vbus_voltage,
236                                                    ok_to_fail)
237            logging.info('%s, %s', result_str, result)
238            if result == 'FAIL':
239                pdtester_failures.append(result_str)
240
241        # PDTester is set back to 20V SRC mode.
242        self.pdtester.charge(self.pdtester.USBC_MAX_VOLTAGE)
243        time.sleep(self.PD_SETTLE_DELAY)
244
245        if pdtester_failures:
246            logging.error('PDTester voltage source cap failures')
247            for fail in pdtester_failures:
248                logging.error('%s', fail)
249            number = len(pdtester_failures)
250            raise error.TestFail('PDTester failed %d times' % number)
251
252        if (is_voltage_reduced_if_batt_full and self._is_batt_full()):
253            logging.warning('This DUT reduces input voltage when chipset is in '
254                         'G3/S5 and battery is full. DUT initiated tests '
255                         'will be skipped. Please discharge battery to level '
256                         'that allows charging and run this test again')
257            return
258
259        # The DUT must be in SNK mode for the pd <port> dev <voltage>
260        # command to have an effect.
261        if not self.dut_port.is_snk():
262            # DUT needs to be in SINK Mode, attempt to force change
263            self.dut_port.drp_set('snk')
264            time.sleep(self.PD_SETTLE_DELAY)
265            if not self.dut_port.is_snk():
266                raise error.TestFail("DUT not able to connect in SINK mode")
267
268        logging.info('Start of DUT initiated tests')
269        dut_failures = []
270        for v in self.VOLTAGE_SEQUENCE:
271            if v > dut_voltage_limit:
272                logging.info('Target = %02dV: skipped, over the limit %0dV',
273                             v, dut_voltage_limit)
274                continue
275            if v not in charging_voltages:
276                logging.info(
277                        'Target = %02dV: skipped, voltage unsupported, '
278                        'update hdctools and servo_v4 firmware '
279                        'or attach a different charger', v)
280                continue
281            # Build 'pd <port> dev <voltage> command
282            cmd = 'pd %d dev %d' % (self.dut_port.port, v)
283            self.dut_port.utils.send_pd_command(cmd)
284            time.sleep(self.PD_SETTLE_DELAY)
285            ok_to_fail = is_override or v > dut_power_voltage_limit
286            result, result_str = self._compare_vbus(v, ok_to_fail)
287            logging.info('%s, %s', result_str, result)
288            if result == 'FAIL':
289                dut_failures.append(result_str)
290
291        # Make sure DUT is set back to its max voltage so DUT will accept all
292        # options
293        cmd = 'pd %d dev %d' % (self.dut_port.port, dut_voltage_limit)
294        self.dut_port.utils.send_pd_command(cmd)
295        time.sleep(self.PD_SETTLE_DELAY)
296        # The next group of tests need DUT to connect in SNK and SRC modes
297        self.dut_port.drp_set('on')
298
299        if dut_failures:
300            logging.error('DUT voltage request failures')
301            for fail in dut_failures:
302                logging.error('%s', fail)
303            number = len(dut_failures)
304            raise error.TestFail('DUT failed %d times' % number)
305