# Copyright 2017 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 pprint import time import re from autotest_lib.client.common_lib import error from autotest_lib.server import autotest from autotest_lib.server.cros.faft.cr50_test import Cr50Test class firmware_Cr50DeviceState(Cr50Test): """Verify Cr50 tracks the EC and AP state correctly. Put the device through S0, S0ix, S3, and G3. Cr50 responds to these state changes by enabling/disabling uart and changing its suspend type. Verify that none of these cause any interrupt storms on Cr50. Make sure that there aren't any interrupt storms and that Cr50 enters regular or deep sleep a reasonable amount of times. """ version = 1 DEEP_SLEEP_STEP_SUFFIX = ' Num Deep Sleep Steps' # Use negative numbers to keep track of counts not in the IRQ list. KEY_DEEP_SLEEP = -3 KEY_TIME = -2 KEY_RESET = -1 KEY_REGULAR_SLEEP = 112 INT_NAME = { KEY_RESET : 'Reset Count', KEY_DEEP_SLEEP : 'Deep Sleep Count', KEY_TIME : 'Cr50 Time', 4 : 'HOST_CMD_DONE', 81 : 'GPIO0', 98 : 'GPIO1', 103 : 'I2CS WRITE', KEY_REGULAR_SLEEP : 'PMU WAKEUP', 113 : 'AC present FED', 114 : 'AC present RED', 124 : 'RBOX_INTR_PWRB', 130 : 'SPS CS deassert', 138 : 'SPS RXFIFO LVL', 159 : 'SPS RXFIFO overflow', 160 : 'EVENT TIMER', 174 : 'CR50_RX_SERVO_TX', 177 : 'CR50_TX_SERVO_RX', 181 : 'AP_TX_CR50_RX', 184 : 'AP_RX_CR50_TX', 188 : 'EC_TX_CR50_RX', 191 : 'EC_RX_CR50_TX', 193 : 'USB', } SLEEP_KEYS = [ KEY_REGULAR_SLEEP, KEY_DEEP_SLEEP ] # Cr50 won't enable any form of sleep until it has been up for 20 seconds. SLEEP_DELAY = 20 # The time in seconds to wait in each state SLEEP_TIME = 30 SHORT_WAIT = 5 CONSERVATIVE_WAIT_TIME = SLEEP_TIME + SHORT_WAIT + 10 # Cr50 should wake up twice per second while in regular sleep SLEEP_RATE = 2 DEEP_SLEEP_MAX = 2 ARM = 'ARM ' # If there are over 100,000 interrupts, it is an interrupt storm. DEFAULT_COUNTS = [0, 100000] # A dictionary of ok count values for each irq that shouldn't follow the # DEFAULT_COUNTS range. EXPECTED_IRQ_COUNT_RANGE = { KEY_RESET : [0, 0], KEY_DEEP_SLEEP : [0, DEEP_SLEEP_MAX], KEY_TIME : [0, CONSERVATIVE_WAIT_TIME], 'S0ix' + DEEP_SLEEP_STEP_SUFFIX : [0, 0], # Cr50 may enter deep sleep an extra time, because of how the test # collects taskinfo counts. Just verify that it does enter deep sleep 'S3' + DEEP_SLEEP_STEP_SUFFIX : [1, 2], 'G3' + DEEP_SLEEP_STEP_SUFFIX : [1, 2], # ARM devices don't enter deep sleep in S3 ARM + 'S3' + DEEP_SLEEP_STEP_SUFFIX : [0, 0], ARM + 'G3' + DEEP_SLEEP_STEP_SUFFIX : [1, 2], # Regular sleep is calculated based on the cr50 time } GET_TASKINFO = ['IRQ counts by type:(.*)Service calls'] START = '' INCREASE = '+' DS_RESUME = 'DS' def initialize(self, host, cmdline_args, full_args): super(firmware_Cr50DeviceState, self).initialize(host, cmdline_args, full_args) # Don't bother if there is no Chrome EC or if EC hibernate doesn't work. if not self.check_ec_capability(): raise error.TestNAError("Nothing needs to be tested on this device") def get_taskinfo_output(self): """Return a dict with the irq numbers as keys and counts as values""" output = self.cr50.send_command_get_output('taskinfo', self.GET_TASKINFO)[0][1].strip() logging.info(output) return output def get_irq_counts(self): """Return a dict with the irq numbers as keys and counts as values""" output = self.get_taskinfo_output() irq_list = re.findall('\d+\s+\d+\r', output) # Make sure the regular sleep irq is in the dictionary, even if cr50 # hasn't seen any interrupts. It's important the test sees that there's # never an interrupt. irq_counts = { self.KEY_REGULAR_SLEEP : 0 } for irq_info in irq_list: logging.debug(irq_info) num, count = irq_info.split() irq_counts[int(num)] = int(count) irq_counts[self.KEY_RESET] = int(self.servo.get('cr50_reset_count')) irq_counts[self.KEY_DEEP_SLEEP] = int(self.cr50.get_deep_sleep_count()) irq_counts[self.KEY_TIME] = int(self.cr50.gettime()) return irq_counts def get_expected_count(self, irq_key, cr50_time): """Get the expected irq increase for the given irq and state Args: irq_key: the irq int cr50_time: the cr50 time in seconds Returns: A list with the expected irq count range [min, max] """ # CCD will prevent sleep if self.ccd_enabled and (irq_key in self.SLEEP_KEYS or self.DEEP_SLEEP_STEP_SUFFIX in str(irq_key)): return [0, 0] if irq_key == self.KEY_REGULAR_SLEEP: # If cr50_time is really low, we probably woke cr50 up using # taskinfo, which would be a pmu wakeup. if cr50_time == 0: return [0, 1] min_count = max(cr50_time - self.SLEEP_DELAY, 0) # Just checking there is not a lot of extra sleep wakeups. Add 1 to # the sleep rate so cr50 can have some extra wakeups, but not too # many. max_count = cr50_time * (self.SLEEP_RATE + 1) return [min_count, max_count] return self.EXPECTED_IRQ_COUNT_RANGE.get(irq_key, self.DEFAULT_COUNTS) def check_increase(self, irq_key, name, increase, expected_range): """Verify the irq count is within the expected range Args: irq_key: the irq int name: the irq name string increase: the irq count expected_range: A list with the valid irq count range [min, max] Returns: '' if increase is in the given range. If the increase isn't in the range, it returns an error message. """ min_count, max_count = expected_range if min_count > increase or max_count < increase: err_msg = '%s %s: %s not in range %s' % (name, irq_key, increase, expected_range) return err_msg return '' def get_step_events(self): """Use the deep sleep counts to determine the step events""" ds_counts = self.get_irq_step_counts(self.KEY_DEEP_SLEEP) events = [] for i, count in enumerate(ds_counts): if not i: events.append(self.START) elif count != ds_counts[i - 1]: # If the deep sleep count changed, Cr50 recovered deep sleep # and the irq counts are reset. events.append(self.DS_RESUME) else: events.append(self.INCREASE) return events def get_irq_step_counts(self, irq_key): """Get a list of the all of the step counts for the given irq""" return [ irq_dict.get(irq_key, 0) for irq_dict in self.steps ] def check_for_errors(self, state): """Check for unexpected IRQ counts at each step. Find the irq count errors and add them to run_errors. Args: state: The power state: S0, S0ix, S3, or G3. """ num_steps = len(self.steps) # Get all of the deep sleep counts events = self.get_step_events() irq_list = list(self.irqs) irq_list.sort() irq_diff = ['%23s' % 'step' + ''.join(self.step_names)] step_errors = [ [] for i in range(num_steps) ] cr50_times = self.get_irq_step_counts(self.KEY_TIME) cr50_diffs = [] for i, cr50_time in enumerate(cr50_times): if events[i] == self.INCREASE: cr50_time -= cr50_times[i - 1] cr50_diffs.append(cr50_time) # Go through each irq and update its info in the progress dict for irq_key in irq_list: name = self.INT_NAME.get(irq_key, 'Unknown') irq_progress_str = ['%19s %3s:' % (name, irq_key)] irq_counts = self.get_irq_step_counts(irq_key) for step, count in enumerate(irq_counts): event = events[step] # The deep sleep counts are not reset after deep sleep. Change # the event to INCREASE. if irq_key == self.KEY_DEEP_SLEEP and event == self.DS_RESUME: event = self.INCREASE if event == self.INCREASE: count -= irq_counts[step - 1] # Check that the count increase is within the expected value. if event != self.START: expected_range = self.get_expected_count(irq_key, cr50_diffs[step]) rv = self.check_increase(irq_key, name, count, expected_range) if rv: logging.info('Unexpected count for %s %s', state, rv) step_errors[step].append(rv) irq_progress_str.append(' %2s %7d' % (event, count)) irq_diff.append(''.join(irq_progress_str)) errors = {} ds_key = self.ARM if self.is_arm else '' ds_key += state + self.DEEP_SLEEP_STEP_SUFFIX expected_range = self.get_expected_count(ds_key, 0) rv = self.check_increase(None, ds_key, events.count(self.DS_RESUME), expected_range) if rv: logging.info('Unexpected count for %s %s', state, rv) errors[ds_key] = rv for i, step_error in enumerate(step_errors): if step_error: logging.error('Step %d errors:\n%s', i, pprint.pformat(step_error)) step = '%s step %d %s' % (state, i, self.step_names[i].strip()) errors[step] = step_error logging.info('DIFF %s IRQ Counts:\n%s', state, pprint.pformat(irq_diff)) if errors: logging.info('ERRORS %s IRQ Counts:\n%s', state, pprint.pformat(errors)) self.run_errors.update(errors) def trigger_s0(self): """Press the power button so the DUT will wake up.""" self.servo.power_short_press() def enter_state(self, state): """Get the command to enter the power state""" self.stage_irq_add(self.get_irq_counts(), 'start %s' % state) if state == 'S0': self.trigger_s0() else: if state == 'S0ix': full_command = 'echo freeze > /sys/power/state &' elif state == 'S3': full_command = 'echo mem > /sys/power/state &' elif state == 'G3': full_command = 'poweroff' self.faft_client.system.run_shell_command(full_command) time.sleep(self.SHORT_WAIT); # check state transition if not self.wait_power_state(state, self.SHORT_WAIT): raise error.TestFail('Platform failed to reach %s state.' % state) self.stage_irq_add(self.get_irq_counts(), 'in %s' % state) def stage_irq_add(self, irq_dict, name=''): """Add the current irq counts to the stored dictionary of irq info""" self.steps.append(irq_dict) self.step_names.append('%11s' % name) self.irqs.update(irq_dict.keys()) def reset_irq_counts(self): """Reset the test IRQ counts""" self.steps = [] self.step_names = [] self.irqs = set() def run_transition(self, state): """Enter the given power state and reenter s0 Enter the power state and return to S0. Wait long enough to ensure cr50 will enter sleep mode, so we can verify that as well. Args: state: the power state: S0ix, S3, or G3 """ self.reset_irq_counts() # Enter the given state self.enter_state(state) logging.info('waiting %d seconds', self.SLEEP_TIME) time.sleep(self.SLEEP_TIME) # Return to S0 self.enter_state('S0') def verify_state(self, state): """Verify cr50 behavior while running through the power state""" try: self.run_transition(state) finally: # reset the system to S0 no matter what happens self.trigger_s0() # Check that the progress of the irq counts seems reasonable self.check_for_errors(state) def is_arm_family(self): """Returns True if the DUT is an ARM device.""" arch = self.host.run('arch').stdout.strip() return arch in ['aarch64', 'armv7l'] def run_through_power_states(self): """Go through S0ix, S3, and G3. Verify there are no interrupt storms""" self.run_errors = {} self.ccd_str = 'ccd ' + ('enabled' if self.ccd_enabled else 'disabled') logging.info('Running through states with %s', self.ccd_str) # Initialize the Test IRQ counts self.reset_irq_counts() # Make sure the DUT is in s0 self.enter_state('S0') # Check if the device supports S0ix. The exit status will be 0 if it # does 1 if it doesn't. result = self.host.run('check_powerd_config --suspend_to_idle', ignore_status=True) if not result.exit_status: # Login before entering S0ix so cr50 will be able to enter regular # sleep client_at = autotest.Autotest(self.host) client_at.run_test('login_LoginSuccess') self.verify_state('S0ix') # Enter S3 self.verify_state('S3') # Enter G3 self.verify_state('G3') if self.run_errors: self.all_errors[self.ccd_str] = self.run_errors def run_once(self, host): """Go through S0ix, S3, and G3. Verify there are no interrupt storms""" self.all_errors = {} self.host = host self.is_arm = self.is_arm_family() supports_dts_control = self.cr50.servo_v4_supports_dts_mode() if supports_dts_control: self.cr50.ccd_disable(raise_error=True) self.ccd_enabled = self.cr50.ccd_is_enabled() self.run_through_power_states() if supports_dts_control: ccd_was_enabled = self.ccd_enabled self.cr50.ccd_enable(raise_error=supports_dts_control) self.ccd_enabled = self.cr50.ccd_is_enabled() # If the first run had ccd disabled, and the test was able to enable # ccd, run through the states again to make sure there are no issues # come up when ccd is enabled. if not ccd_was_enabled and self.ccd_enabled: self.run_through_power_states() else: logging.info('Current setup only supports test with ccd %sabled.', 'en' if self.ccd_enabled else 'dis') self.trigger_s0() if self.all_errors: raise error.TestFail('Unexpected Device State: %s' % self.all_errors) if not supports_dts_control: raise error.TestNAError('Verified device state with %s. Please ' 'run with type c servo v4 to test full device state.' % self.ccd_str)