| /* 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. |
| */ |
| |
| /* Motion sense module to read from various motion sensors. */ |
| |
| #include "acpi.h" |
| #include "accelgyro.h" |
| #include "chipset.h" |
| #include "common.h" |
| #include "console.h" |
| #include "gesture.h" |
| #include "hooks.h" |
| #include "host_command.h" |
| #include "lid_angle.h" |
| #include "lid_switch.h" |
| #include "math_util.h" |
| #include "motion_lid.h" |
| #include "motion_sense.h" |
| #include "power.h" |
| #include "tablet_mode.h" |
| #include "timer.h" |
| #include "task.h" |
| #include "util.h" |
| |
| /* Console output macros */ |
| #define CPUTS(outstr) cputs(CC_MOTION_LID, outstr) |
| #define CPRINTS(format, args...) cprints(CC_MOTION_LID, format, ## args) |
| #define CPRINTF(format, args...) cprintf(CC_MOTION_LID, format, ## args) |
| |
| #ifdef CONFIG_TABLET_MODE |
| /* Previous lid_angle. */ |
| static fp_t last_lid_angle_fp = FLOAT_TO_FP(-1); |
| |
| /* |
| * This defines the range from 0 to SMALL_LID_ANGLE_RANGE of possible lid angle |
| * measurements when the lid is physically closed. This will be used in |
| * reliability calculations. |
| */ |
| #define SMALL_LID_ANGLE_RANGE (FLOAT_TO_FP(15)) |
| #endif |
| |
| /* Current acceleration vectors and current lid angle. */ |
| static int lid_angle_deg; |
| |
| static int lid_angle_is_reliable; |
| |
| /* Smoothed vectors to increase accurency. */ |
| static intv3_t smoothed_base, smoothed_lid; |
| |
| /* 8.7 m/s^2 is the the maximum acceleration parallel to the hinge */ |
| #define SCALED_HINGE_VERTICAL_MAXIMUM \ |
| ((int)((8.7f * MOTION_SCALING_FACTOR) / MOTION_ONE_G)) |
| |
| #define SCALED_HINGE_VERTICAL_SMOOTHING_START \ |
| ((int)((7.0f * MOTION_SCALING_FACTOR) / MOTION_ONE_G)) |
| |
| /* |
| * Constant to debounce lid angle changes around 360 - 0: |
| * If we have a rotation through the angle 0, ignore. |
| */ |
| #define DEBOUNCE_ANGLE_DELTA FLOAT_TO_FP(45) |
| |
| /* |
| * Since the accelerometers are on the same physical device, they should be |
| * under the same acceleration. This constant, which mirrors |
| * kNoisyMagnitudeDeviation used in Chromium, is an integer which defines the |
| * maximum deviation in magnitude between the base and lid vectors. The units |
| * are in g. Currently set at 1m/s^2. |
| */ |
| #define NOISY_MAGNITUDE_DEVIATION ((int)(MOTION_SCALING_FACTOR / MOTION_ONE_G)) |
| |
| /* |
| * Even with noise, any measurement greater than 1g on any axis is not suitable |
| * for lid calculation. It means the device is moving. |
| * To avoid using 64bits arithmetic, we need to be sure that square of magnitude |
| * is less than 1<<31, so magnitude is less sqrt(2)*(1<<15), less than ~40% over |
| * 1g. This is way above any usable noise. Assume noise is less than 10%. |
| */ |
| #define MOTION_SCALING_AXIS_MAX (MOTION_SCALING_FACTOR * 110) |
| |
| #define MOTION_SCALING_FACTOR2 (MOTION_SCALING_FACTOR * MOTION_SCALING_FACTOR) |
| |
| /* |
| * Define the accelerometer orientation matrices based on the standard |
| * reference frame in use (note: accel data is converted to standard ref |
| * frame before calculating lid angle). |
| */ |
| #ifdef CONFIG_ACCEL_STD_REF_FRAME_OLD |
| static const intv3_t hinge_axis = { 0, 1, 0}; |
| #define HINGE_AXIS Y |
| #else |
| static const intv3_t hinge_axis = { 1, 0, 0}; |
| #define HINGE_AXIS X |
| #endif |
| |
| static const struct motion_sensor_t * const accel_base = |
| &motion_sensors[CONFIG_LID_ANGLE_SENSOR_BASE]; |
| static const struct motion_sensor_t * const accel_lid = |
| &motion_sensors[CONFIG_LID_ANGLE_SENSOR_LID]; |
| |
| #ifdef CONFIG_TABLET_MODE |
| __attribute__((weak)) int board_is_lid_angle_tablet_mode(void) |
| { |
| return 1; |
| } |
| |
| /* |
| * We are in tablet mode when the lid angle has been calculated |
| * to be large. |
| * |
| * By default, at boot, we are in tablet mode. |
| * Once a lid angle is calculated, we will get out of this fake state and enter |
| * tablet mode only if a high angle has been calculated. |
| * |
| * There might be false positives: |
| * - when the EC enters RO or RW mode. |
| * - when lid is closed while the hinge is perpendicular to the floor, we will |
| * stay in tablet mode. |
| * |
| * Tablet mode is defined as the lid angle being greater than 180 degree(by |
| * default). We use 2 threshold to calculate tablet mode: |
| * tablet_mode: |
| * 1 | +-----<----+---------- |
| * | \/ /\ |
| * | | | |
| * 0 |------------------>----+ |
| * +------------+----------+----------+ lid angle |
| * 0 160 200 360 |
| * |
| * Host can configure the threshold to be different than default of 180 +/- 20 |
| * by using MOTIONSENSE_CMD_TABLET_MODE_LID_ANGLE. |
| */ |
| |
| #define DEFAULT_TABLET_MODE_ANGLE (180) |
| #define DEFAULT_TABLET_MODE_HYS (20) |
| |
| #define TABLET_ZONE_ANGLE(a, h) ((a) + (h)) |
| #define LAPTOP_ZONE_ANGLE(a, h) ((a) - (h)) |
| |
| static fp_t tablet_zone_lid_angle = |
| FLOAT_TO_FP(TABLET_ZONE_ANGLE(DEFAULT_TABLET_MODE_ANGLE, |
| DEFAULT_TABLET_MODE_HYS)); |
| static fp_t laptop_zone_lid_angle = |
| FLOAT_TO_FP(LAPTOP_ZONE_ANGLE(DEFAULT_TABLET_MODE_ANGLE, |
| DEFAULT_TABLET_MODE_HYS)); |
| |
| static int tablet_mode_lid_angle = DEFAULT_TABLET_MODE_ANGLE; |
| static int tablet_mode_hys_degree = DEFAULT_TABLET_MODE_HYS; |
| |
| static void motion_lid_set_tablet_mode(int reliable) |
| { |
| static int tablet_mode_debounce_cnt = TABLET_MODE_DEBOUNCE_COUNT; |
| const int current_mode = tablet_get_mode(); |
| int new_mode = current_mode; |
| |
| if (reliable) { |
| if (last_lid_angle_fp > tablet_zone_lid_angle) |
| new_mode = 1; |
| else if (last_lid_angle_fp < laptop_zone_lid_angle) |
| new_mode = 0; |
| |
| /* Only change tablet mode if we're sure. */ |
| if (current_mode != new_mode) { |
| if (tablet_mode_debounce_cnt == 0) { |
| /* Alright, we're convinced. */ |
| tablet_mode_debounce_cnt = |
| TABLET_MODE_DEBOUNCE_COUNT; |
| tablet_set_mode(new_mode); |
| return; |
| } |
| tablet_mode_debounce_cnt--; |
| return; |
| } |
| } |
| |
| /* |
| * If we got a reliable measurement that agrees with our current tablet |
| * mode, then reset the debounce counter. Also, make it harder to leave |
| * tablet mode by resetting the debounce count when we encounter an |
| * unreliable angle when we're already in tablet mode. |
| */ |
| if (((reliable == 0) && current_mode == 1) || |
| ((reliable == 1) && (current_mode == new_mode))) |
| tablet_mode_debounce_cnt = TABLET_MODE_DEBOUNCE_COUNT; |
| } |
| |
| static int lid_angle_set_tablet_mode_threshold(int angle, int hys) |
| { |
| if ((angle == EC_MOTION_SENSE_NO_VALUE) || |
| (hys == EC_MOTION_SENSE_NO_VALUE)) |
| return EC_RES_SUCCESS; |
| |
| if ((angle < 0) || (hys < 0) || (angle < hys) || ((angle + hys) > 360)) |
| return EC_RES_INVALID_PARAM; |
| |
| tablet_mode_lid_angle = angle; |
| tablet_mode_hys_degree = hys; |
| |
| tablet_zone_lid_angle = INT_TO_FP(TABLET_ZONE_ANGLE(angle, hys)); |
| laptop_zone_lid_angle = INT_TO_FP(LAPTOP_ZONE_ANGLE(angle, hys)); |
| |
| return EC_RES_SUCCESS; |
| } |
| |
| #endif /* CONFIG_TABLET_MODE */ |
| |
| #if defined(CONFIG_DPTF_MULTI_PROFILE) && \ |
| defined(CONFIG_DPTF_MOTION_LID_NO_HALL_SENSOR) |
| |
| /* |
| * If CONFIG_DPTF_MULTI_PROFILE is defined by a board, then lid motion driver |
| * sets different profile numbers depending upon the current lid |
| * angle. Following profiles are currently supported by this driver: |
| * 1. Clamshell mode - DPTF_PROFILE_CLAMSHELL |
| * 2. 360-degree flipped mode - DPTF_PROFILE_FLIPPED_360_MODE |
| * |
| * 360-degree flipped mode is defined as the mode with base being behind the |
| * lid. We use 2 threshold to calculate this: |
| * |
| * 360-degree mode |
| * 1 | +-----<----+---------- |
| * | \/ /\ |
| * | | | |
| * 0 |------------------------>----+ |
| * +------------------+----------+----------+ lid angle |
| * 0 240 300 360 |
| */ |
| #define FLIPPED_360_ZONE_LID_ANGLE FLOAT_TO_FP(300) |
| #define CLAMSHELL_ZONE_LID_ANGLE FLOAT_TO_FP(240) |
| |
| /* |
| * Detection of DPTF profile is very similar to tablet mode detection using |
| * debounce counter. This is done to avoid any spurious changes in setting DPTF |
| * profile numbers. |
| */ |
| #define DPTF_MODE_DEBOUNCE_COUNT 3 |
| |
| static void motion_lid_set_dptf_profile(int reliable) |
| { |
| static int debounce_cnt = DPTF_MODE_DEBOUNCE_COUNT; |
| int current_prof = acpi_dptf_get_profile_num(); |
| int new_prof = current_prof; |
| |
| if (reliable) { |
| if (last_lid_angle_fp > FLIPPED_360_ZONE_LID_ANGLE) |
| new_prof = DPTF_PROFILE_FLIPPED_360_MODE; |
| else if (last_lid_angle_fp < CLAMSHELL_ZONE_LID_ANGLE) |
| new_prof = DPTF_PROFILE_CLAMSHELL; |
| |
| if (current_prof != new_prof) { |
| if (debounce_cnt != 0) { |
| debounce_cnt--; |
| return; |
| } |
| |
| debounce_cnt = DPTF_MODE_DEBOUNCE_COUNT; |
| acpi_dptf_set_profile_num(new_prof); |
| return; |
| } |
| } |
| |
| debounce_cnt = DPTF_MODE_DEBOUNCE_COUNT; |
| } |
| |
| #endif /* CONFIG_DPTF_MULTI_PROFILE && CONFIG_DPTF_MOTION_LID_NO_HALL_SENSOR */ |
| |
| /** |
| * Calculate the lid angle using two acceleration vectors, one recorded in |
| * the base and one in the lid. |
| * |
| * @param base Base accel vector |
| * @param lid Lid accel vector |
| * @param lid_angle Pointer to location to store lid angle result |
| * |
| * @return flag representing if resulting lid angle calculation is reliable. |
| */ |
| static int calculate_lid_angle(const intv3_t base, const intv3_t lid, |
| int *lid_angle) |
| { |
| intv3_t cross, proj_lid, proj_base, scaled_base, scaled_lid; |
| fp_t lid_to_base_fp, smoothed_ratio; |
| int base_magnitude2, lid_magnitude2, largest_hinge_accel; |
| int reliable = 1, i; |
| |
| /* |
| * Scale the vectors by their range, to be able to compare them. |
| * If a single measurement is greated than 1g, we may overflow fixed |
| * point calculation. However, we can exclude such a measurement, it |
| * means the device is in movement and lid angle calculation is not |
| * possible. |
| */ |
| for (i = X; i <= Z; i++) { |
| scaled_base[i] = base[i] * |
| accel_base->drv->get_range(accel_base); |
| scaled_lid[i] = lid[i] * |
| accel_lid->drv->get_range(accel_lid); |
| if (ABS(scaled_base[i]) > MOTION_SCALING_AXIS_MAX || |
| ABS(scaled_lid[i]) > MOTION_SCALING_AXIS_MAX) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| } |
| |
| /* |
| * Calculate square of vector magnitude in g. |
| * Each entry is guaranteed to be up to +/- 1<<15, so the square will be |
| * less than 1<<30. |
| */ |
| base_magnitude2 = scaled_base[X] * scaled_base[X] + |
| scaled_base[Y] * scaled_base[Y] + |
| scaled_base[Z] * scaled_base[Z]; |
| lid_magnitude2 = scaled_lid[X] * scaled_lid[X] + |
| scaled_lid[Y] * scaled_lid[Y] + |
| scaled_lid[Z] * scaled_lid[Z]; |
| |
| /* |
| * Check to see if they differ than more than NOISY_MAGNITUDE_DEVIATION. |
| * If the vectors do, then the measured angle is unreliable. |
| * |
| * Note, that we don't actually have to take the square root to get the |
| * magnitude, but we can work with the magnitudes squared directly as |
| * shown below: |
| * |
| * If A is a magnitudes, and x is the noisy magnitude deviation: |
| * |
| * 0 < 1g - A < x |
| * 0 < 1g^2 - A^2 < x * (A + B) |
| * 0 < 1g^2 - A^2 < 2 * x * avg(A, B) |
| * |
| * If we assume that the average acceleration should be about 1g, then |
| * we have: |
| * |
| * 0 < 1g^2 - A^2 < 2 * 1g * NOISY_MAGNITUDE_DEVIATION |
| */ |
| if (MOTION_SCALING_FACTOR2 - base_magnitude2 > |
| 2 * MOTION_SCALING_FACTOR * NOISY_MAGNITUDE_DEVIATION) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| if (MOTION_SCALING_FACTOR2 - lid_magnitude2 > |
| 2 * MOTION_SCALING_FACTOR * NOISY_MAGNITUDE_DEVIATION) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| |
| largest_hinge_accel = MAX(ABS(scaled_base[HINGE_AXIS]), |
| ABS(scaled_lid[HINGE_AXIS])); |
| |
| smoothed_ratio = MAX(INT_TO_FP(0), MIN(INT_TO_FP(1), |
| fp_div(INT_TO_FP(largest_hinge_accel - |
| SCALED_HINGE_VERTICAL_SMOOTHING_START), |
| INT_TO_FP(SCALED_HINGE_VERTICAL_MAXIMUM - |
| SCALED_HINGE_VERTICAL_SMOOTHING_START)))); |
| |
| /* Check hinge is not too vertical */ |
| if (largest_hinge_accel > SCALED_HINGE_VERTICAL_MAXIMUM) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| |
| /* Smooth input to reduce calculation error due to noise. */ |
| vector_scale(smoothed_base, smoothed_ratio); |
| vector_scale(smoothed_lid, smoothed_ratio); |
| vector_scale(scaled_base, INT_TO_FP(1) - smoothed_ratio); |
| vector_scale(scaled_lid, INT_TO_FP(1) - smoothed_ratio); |
| for (i = X; i <= Z; i++) { |
| smoothed_base[i] += scaled_base[i]; |
| smoothed_lid[i] += scaled_lid[i]; |
| } |
| |
| /* Project vectors on the hinge hyperplan, putting smooth ones aside. */ |
| memcpy(proj_base, smoothed_base, sizeof(intv3_t)); |
| memcpy(proj_lid, smoothed_lid, sizeof(intv3_t)); |
| proj_base[HINGE_AXIS] = 0; |
| proj_lid[HINGE_AXIS] = 0; |
| |
| /* Calculate the clockwise angle */ |
| lid_to_base_fp = arc_cos(cosine_of_angle_diff(proj_base, proj_lid)); |
| cross_product(proj_base, proj_lid, cross); |
| |
| /* |
| * If the dot product of this cross product is normal, it means that |
| * the shortest angle between |base| and |lid| was counterclockwise |
| * with respect to the surface represented by |hinge_axis| and this |
| * angle must be reversed. |
| */ |
| if (dot_product(cross, hinge_axis) > 0) |
| lid_to_base_fp = FLOAT_TO_FP(360) - lid_to_base_fp; |
| |
| #ifndef CONFIG_ACCEL_STD_REF_FRAME_OLD |
| /* |
| * Angle is between the keyboard and the front of screen: we need to |
| * anlge between keyboard and back of screen: |
| * 180 instead of 0 when lid and base are flat on surface. |
| * 0 instead of 180 when lid is closed on keyboard. |
| */ |
| lid_to_base_fp = FLOAT_TO_FP(180) - lid_to_base_fp; |
| #endif |
| |
| /* Place lid angle between 0 and 360 degrees. */ |
| if (lid_to_base_fp < 0) |
| lid_to_base_fp += FLOAT_TO_FP(360); |
| |
| #ifdef CONFIG_TABLET_MODE |
| /* Ignore large angles when the lid is closed. */ |
| if (!lid_is_open() && |
| (lid_to_base_fp > SMALL_LID_ANGLE_RANGE)) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| |
| /* |
| * Ignore small angles when the lid is open. |
| * |
| * Note that we're not correcting the angle, but just marking it as |
| * unreliable. Attempting to correct the angle would cause bad angles |
| * when closing the lid. However, there is one edge case. If the |
| * device is suspended in laptop mode, but then is physically placed in |
| * tablet mode, but ALL the angles are read as unreliable, a keypress |
| * may wake us up. This is because we require at least 4 consecutive |
| * reliable readings over a threshold to disable key scanning. |
| */ |
| if (lid_is_open() && |
| (lid_to_base_fp <= SMALL_LID_ANGLE_RANGE)) { |
| reliable = 0; |
| goto end_calculate_lid_angle; |
| } |
| |
| /* Seed the lid angle now that we have a reliable measurement. */ |
| if (last_lid_angle_fp == FLOAT_TO_FP(-1)) |
| last_lid_angle_fp = lid_to_base_fp; |
| |
| /* |
| * If the angle was last seen as really large and now it's quite |
| * small, we may be rotating around from 360->0 so correct it to |
| * be large. But in case that the lid switch is closed, we can |
| * prove the small angle we see is correct so we take the angle |
| * as is. |
| */ |
| if ((last_lid_angle_fp >= |
| FLOAT_TO_FP(360) - DEBOUNCE_ANGLE_DELTA) && |
| (lid_to_base_fp <= DEBOUNCE_ANGLE_DELTA) && |
| (lid_is_open())) |
| last_lid_angle_fp = FLOAT_TO_FP(360) - lid_to_base_fp; |
| else |
| last_lid_angle_fp = lid_to_base_fp; |
| |
| end_calculate_lid_angle: |
| /* |
| * Round to nearest int by adding 0.5. Note, only works because lid |
| * angle is known to be positive. |
| */ |
| *lid_angle = FP_TO_INT(last_lid_angle_fp + FLOAT_TO_FP(0.5)); |
| |
| if (board_is_lid_angle_tablet_mode()) |
| motion_lid_set_tablet_mode(reliable); |
| |
| #if defined(CONFIG_DPTF_MULTI_PROFILE) && \ |
| defined(CONFIG_DPTF_MOTION_LID_NO_HALL_SENSOR) |
| motion_lid_set_dptf_profile(reliable); |
| #endif /* CONFIG_DPTF_MULTI_PROFILE && CONFIG_DPTF_MOTION_LID_NO_HALL_SENSOR */ |
| |
| #else /* CONFIG_TABLET_MODE */ |
| end_calculate_lid_angle: |
| if (reliable) |
| *lid_angle = FP_TO_INT(lid_to_base_fp + FLOAT_TO_FP(0.5)); |
| #endif |
| return reliable; |
| } |
| |
| int motion_lid_get_angle(void) |
| { |
| if (lid_angle_is_reliable) |
| return lid_angle_deg; |
| else |
| return LID_ANGLE_UNRELIABLE; |
| } |
| |
| /* |
| * Calculate lid angle and massage the results |
| */ |
| void motion_lid_calc(void) |
| { |
| /* Calculate angle of lid accel. */ |
| lid_angle_is_reliable = calculate_lid_angle( |
| accel_base->xyz, accel_lid->xyz, |
| &lid_angle_deg); |
| |
| #ifdef CONFIG_LID_ANGLE_UPDATE |
| lid_angle_update(motion_lid_get_angle()); |
| #endif |
| } |
| |
| /*****************************************************************************/ |
| /* Host commands */ |
| |
| |
| int host_cmd_motion_lid(struct host_cmd_handler_args *args) |
| { |
| const struct ec_params_motion_sense *in = args->params; |
| struct ec_response_motion_sense *out = args->response; |
| |
| switch (in->cmd) { |
| case MOTIONSENSE_CMD_KB_WAKE_ANGLE: |
| #ifdef CONFIG_LID_ANGLE_UPDATE |
| /* Set new keyboard wake lid angle if data arg has value. */ |
| if (in->kb_wake_angle.data != EC_MOTION_SENSE_NO_VALUE) |
| lid_angle_set_wake_angle(in->kb_wake_angle.data); |
| |
| out->kb_wake_angle.ret = lid_angle_get_wake_angle(); |
| #else |
| out->kb_wake_angle.ret = 0; |
| #endif |
| args->response_size = sizeof(out->kb_wake_angle); |
| |
| break; |
| |
| case MOTIONSENSE_CMD_LID_ANGLE: |
| #ifdef CONFIG_LID_ANGLE |
| out->lid_angle.value = motion_lid_get_angle(); |
| args->response_size = sizeof(out->lid_angle); |
| #else |
| return EC_RES_INVALID_PARAM; |
| #endif |
| break; |
| |
| case MOTIONSENSE_CMD_TABLET_MODE_LID_ANGLE: |
| { |
| #ifdef CONFIG_TABLET_MODE |
| int ret; |
| ret = lid_angle_set_tablet_mode_threshold( |
| in->tablet_mode_threshold.lid_angle, |
| in->tablet_mode_threshold.hys_degree); |
| |
| if (ret != EC_RES_SUCCESS) |
| return ret; |
| |
| out->tablet_mode_threshold.lid_angle = |
| tablet_mode_lid_angle; |
| out->tablet_mode_threshold.hys_degree = |
| tablet_mode_hys_degree; |
| |
| args->response_size = |
| sizeof(out->tablet_mode_threshold); |
| #else |
| return EC_RES_INVALID_PARAM; |
| #endif |
| } |
| break; |
| default: |
| return EC_RES_INVALID_PARAM; |
| } |
| |
| return EC_RES_SUCCESS; |
| } |
| |