Re: [PATCH 3/4] MIPS: Loongson64: Yeeloong add platform driver Yeeloong is a laptop with a MIPS Loongson 2F processor, AMD CS5536 chipset, and KB3310B controller.

From: Ralf Baechle
Date: Tue Nov 14 2017 - 08:22:03 EST


On Sun, Nov 12, 2017 at 02:36:16PM +0800, jiaxun.yang@xxxxxxxxxxx wrote:
> Date: Sun, 12 Nov 2017 14:36:16 +0800
> From: jiaxun.yang@xxxxxxxxxxx
> To: ralf@xxxxxxxxxxxxxx
> Cc: linux-mips@xxxxxxxxxxxxxx, linux-kernel@xxxxxxxxxxxxxxx, Jiaxun Yang
> <jiaxun.yang@xxxxxxxxxxx>
> Subject: [PATCH 3/4] MIPS: Loongson64: Yeeloong add platform driver
> Yeeloong is a laptop with a MIPS Loongson 2F processor, AMD CS5536
> chipset, and KB3310B controller.
> Content-Type: text/plain; charset=UTF-8

Some comment as for the previous patch:

Please don't cram the entire commit message into the subject line. The
standard for commit messages to keep lines only so long that when you
look at them in "git log" in a 80 column terminal they don't get line
wrapped or truncated.

> From: Jiaxun Yang <jiaxun.yang@xxxxxxxxxxx>
>
> This yeeloong_laptop module enables access to sensors, battery,
> video camera switch, external video connector event, and some
> additional buttons.
>
> This driver was orginally from linux-loongson-community. I Just do
> some clean up and port to mainline kernel tree.
>
> Signed-off-by: Jiaxun Yang <jiaxun.yang@xxxxxxxxxxx>
> ---
> drivers/platform/mips/Kconfig | 18 +
> drivers/platform/mips/Makefile | 3 +
> drivers/platform/mips/yeeloong_laptop.c | 1143 +++++++++++++++++++++++++++++++
> 3 files changed, 1164 insertions(+)
> create mode 100644 drivers/platform/mips/yeeloong_laptop.c
>
> diff --git a/drivers/platform/mips/Kconfig b/drivers/platform/mips/Kconfig
> index b3ae30a4c67b..0f054efc26e3 100644
> --- a/drivers/platform/mips/Kconfig
> +++ b/drivers/platform/mips/Kconfig
> @@ -23,4 +23,22 @@ config CPU_HWMON
> help
> Loongson-3A/3B CPU Hwmon (temperature sensor) driver.
>
> +config LEMOTE_YEELOONG2F
> + tristate "Lemote YeeLoong Laptop"
> + depends on LEMOTE_MACH2F
> + select BACKLIGHT_LCD_SUPPORT
> + select LCD_CLASS_DEVICE
> + select BACKLIGHT_CLASS_DEVICE
> + select POWER_SUPPLY
> + select HWMON
> + select INPUT_SPARSEKMAP
> + select INPUT_EVDEV
> + depends on INPUT
> + default m
> + help
> + YeeLoong netbook is a mini laptop made by Lemote, which is basically
> + compatible to FuLoong2F mini PC, but it has an extra Embedded
> + Controller(kb3310b) for battery, hotkey, backlight, temperature and
> + fan management.

Please don't depend on INPUT, it's going to drive users crazy because
mortals less familiar with the configuration process for this platform
almost certainly won't know that INPUT needs to be enabled so
LEMOTE_YEELOONG2F can be selected.

This will require further changes because you can't force INPUT_SPARSEKMAP
and INPUT_EVDEV on if you don't know INPUT is selected.

It also looks as if your Kconfig changes won't work properly if INPUT_MISC
has not been selected manually.

> endif # MIPS_PLATFORM_DEVICES
> diff --git a/drivers/platform/mips/Makefile b/drivers/platform/mips/Makefile
> index 8dfd03924c37..b3172b081a2f 100644
> --- a/drivers/platform/mips/Makefile
> +++ b/drivers/platform/mips/Makefile
> @@ -1 +1,4 @@
> obj-$(CONFIG_CPU_HWMON) += cpu_hwmon.o
> +
> +obj-$(CONFIG_LEMOTE_YEELOONG2F) += yeeloong_laptop.o
> +CFLAGS_yeeloong_laptop.o = -I$(srctree)/arch/mips/loongson/lemote-2f
> diff --git a/drivers/platform/mips/yeeloong_laptop.c b/drivers/platform/mips/yeeloong_laptop.c
> new file mode 100644
> index 000000000000..280f2044858b
> --- /dev/null
> +++ b/drivers/platform/mips/yeeloong_laptop.c
> @@ -0,0 +1,1143 @@
> +/*
> + * Driver for YeeLoong laptop extras
> + *
> + * Copyright (C) 2017 Jiaxun Yang.
> + * Author: Jiaxun Yang <jiaxun.yang@xxxxxxxxxxx>
> + *
> + * Copyright (C) 2009 Lemote Inc.
> + * Author: Wu Zhangjin <wuzhangjin@xxxxxxxxx>, Liu Junliang <liujl@xxxxxxxxxx>
> + *
> + * Fixes: Petr Pisar <petr.pisar@xxxxxxxx>, 2012, 2013, 2014, 2015.
> + *
> + * This program is free software; you can redistribute it and/or modify
> + * it under the terms of the GNU General Public License version 2 as
> + * published by the Free Software Foundation.
> + */
> +
> +#include <linux/err.h>
> +#include <linux/platform_device.h>
> +#include <linux/backlight.h> /* for backlight subdriver */
> +#include <linux/fb.h>
> +#include <linux/hwmon.h> /* for hwmon subdriver */
> +#include <linux/hwmon-sysfs.h>
> +#include <linux/kernel.h> /* for clamp_val() */
> +#include <linux/input.h> /* for hotkey subdriver */
> +#include <linux/input/sparse-keymap.h>
> +#include <linux/interrupt.h>
> +#include <linux/delay.h>
> +#include <linux/power_supply.h> /* for AC & Battery subdriver */
> +#include <linux/module.h> /* For MODULE_DEVICE_TABLE() */
> +
> +#include <asm/bootinfo.h>
> +
> +#include <cs5536/cs5536.h>
> +
> +#include <loongson.h> /* for loongson_cmdline */
> +#include <ec_kb3310b.h>
> +
> +/* common function */
> +#define EC_VER_LEN 64
> +
> +static int ec_version_before(char *version)
> +{
> + char *p, ec_ver[EC_VER_LEN];
> +
> + p = strstr(loongson_cmdline, "EC_VER=");
> + if (!p)
> + memset(ec_ver, 0, EC_VER_LEN);
> + else {
> + strncpy(ec_ver, p, EC_VER_LEN);
> + p = strstr(ec_ver, " ");
> + if (p)
> + *p = '\0';
> + }
> +
> + return (strncasecmp(ec_ver, version, 64) < 0);
> +}
> +
> +/* backlight subdriver */
> +#define MAX_BRIGHTNESS 8
> +
> +static int yeeloong_set_brightness(struct backlight_device *bd)
> +{
> + unsigned int level, current_level;
> + static unsigned int old_level;
> +
> + level = (bd->props.fb_blank == FB_BLANK_UNBLANK &&
> + bd->props.power == FB_BLANK_UNBLANK) ?
> + bd->props.brightness : 0;
> +
> + level = clamp_val(level, 0, MAX_BRIGHTNESS);
> +
> + /* Avoid to modify the brightness when EC is tuning it */
> + if (old_level != level) {
> + current_level = ec_read(REG_DISPLAY_BRIGHTNESS);
> + if (old_level == current_level)
> + ec_write(REG_DISPLAY_BRIGHTNESS, level);
> + old_level = level;
> + }
> +
> + return 0;
> +}
> +
> +static int yeeloong_get_brightness(struct backlight_device *bd)
> +{
> + return ec_read(REG_DISPLAY_BRIGHTNESS);
> +}
> +
> +const struct backlight_ops backlight_ops = {
> + .get_brightness = yeeloong_get_brightness,
> + .update_status = yeeloong_set_brightness,
> +};
> +
> +static struct backlight_device *yeeloong_backlight_dev;
> +
> +static int yeeloong_backlight_init(void)
> +{
> + int ret;
> + struct backlight_properties props;
> +
> + memset(&props, 0, sizeof(struct backlight_properties));
> + props.type = BACKLIGHT_RAW;
> + props.max_brightness = MAX_BRIGHTNESS;
> + yeeloong_backlight_dev = backlight_device_register("backlight0", NULL,
> + NULL, &backlight_ops, &props);
> +
> + if (IS_ERR(yeeloong_backlight_dev)) {
> + ret = PTR_ERR(yeeloong_backlight_dev);
> + yeeloong_backlight_dev = NULL;
> + return ret;
> + }
> +
> + yeeloong_backlight_dev->props.brightness =
> + yeeloong_get_brightness(yeeloong_backlight_dev);
> + backlight_update_status(yeeloong_backlight_dev);
> +
> + return 0;
> +}
> +
> +static void yeeloong_backlight_exit(void)
> +{
> + if (yeeloong_backlight_dev) {
> + backlight_device_unregister(yeeloong_backlight_dev);
> + yeeloong_backlight_dev = NULL;
> + }
> +}
> +
> +/* AC & Battery subdriver */
> +
> +static struct power_supply *yeeloong_ac, *yeeloong_bat;
> +
> +#define RET (val->intval)
> +
> +static inline bool is_ac_in(void)
> +{
> + return !!(ec_read(REG_BAT_POWER) & BIT_BAT_POWER_ACIN);
> +}
> +
> +static int yeeloong_get_ac_props(struct power_supply *psy,
> + enum power_supply_property psp,
> + union power_supply_propval *val)
> +{
> + switch (psp) {
> + case POWER_SUPPLY_PROP_ONLINE:
> + RET = is_ac_in();
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static enum power_supply_property yeeloong_ac_props[] = {
> + POWER_SUPPLY_PROP_ONLINE,
> +};
> +
> +static const struct power_supply_desc yeeloong_ac_desc = {
> + .name = "yeeloong-ac",
> + .type = POWER_SUPPLY_TYPE_MAINS,
> + .properties = yeeloong_ac_props,
> + .num_properties = ARRAY_SIZE(yeeloong_ac_props),
> + .get_property = yeeloong_get_ac_props,
> +};
> +
> +#define BAT_CAP_CRITICAL 5
> +#define BAT_CAP_HIGH 95
> +
> +#define get_bat_info(type) \
> + ((ec_read(REG_BAT_##type##_HIGH) << 8) | \
> + (ec_read(REG_BAT_##type##_LOW)))
> +
> +static inline bool is_bat_in(void)
> +{
> + return !!(ec_read(REG_BAT_STATUS) & BIT_BAT_STATUS_IN);
> +}
> +
> +static inline int get_bat_status(void)
> +{
> + return ec_read(REG_BAT_STATUS);
> +}
> +
> +static int get_battery_temp(void)
> +{
> + int value;
> +
> + value = get_bat_info(TEMPERATURE);
> +
> + return value * 1000;
> +}
> +
> +static int get_battery_current(void)
> +{
> + s16 value;
> +
> + value = get_bat_info(CURRENT);
> +
> + return -value;
> +}
> +
> +static int get_battery_voltage(void)
> +{
> + int value;
> +
> + value = get_bat_info(VOLTAGE);
> +
> + return value;
> +}
> +
> +static inline char *get_manufacturer(void)
> +{
> + return (ec_read(REG_BAT_VENDOR) == FLAG_BAT_VENDOR_SANYO) ? "SANYO" :
> + "SIMPLO";
> +}
> +
> +static int yeeloong_get_bat_props(struct power_supply *psy,
> + enum power_supply_property psp,
> + union power_supply_propval *val)
> +{
> + switch (psp) {
> + /* Fixed information */
> + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
> + /* mV -> µV */
> + RET = get_bat_info(DESIGN_VOL) * 1000;
> + break;
> + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
> + /* mAh->µAh */
> + RET = get_bat_info(DESIGN_CAP) * 1000;
> + break;
> + case POWER_SUPPLY_PROP_CHARGE_FULL:
> + /* µAh */
> + RET = get_bat_info(FULLCHG_CAP) * 1000;
> + break;
> + case POWER_SUPPLY_PROP_MANUFACTURER:
> + val->strval = get_manufacturer();
> + break;
> + /* Dynamic information */
> + case POWER_SUPPLY_PROP_PRESENT:
> + RET = is_bat_in();
> + break;
> + case POWER_SUPPLY_PROP_CURRENT_NOW:
> + /* mA -> µA */
> + RET = is_bat_in() ? get_battery_current() * 1000 : 0;
> + break;
> + case POWER_SUPPLY_PROP_VOLTAGE_NOW:
> + /* mV -> µV */
> + RET = is_bat_in() ? get_battery_voltage() * 1000 : 0;
> + break;
> + case POWER_SUPPLY_PROP_TEMP:
> + /* Celcius */
> + RET = is_bat_in() ? get_battery_temp() : 0;
> + break;
> + case POWER_SUPPLY_PROP_CAPACITY:
> + RET = is_bat_in() ? get_bat_info(RELATIVE_CAP) : 0;
> + break;
> + case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
> + {
> + int status;
> +
> + if (!is_bat_in()) {
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN;
> + break;
> + }
> +
> + status = get_bat_status();
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
> +
> + if (unlikely(status & BIT_BAT_STATUS_DESTROY)) {
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN;
> + break;
> + }
> +
> + if (status & BIT_BAT_STATUS_LOW)
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_LOW;
> + else if (status & BIT_BAT_STATUS_FULL)
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_FULL;
> + else {
> + int curr_cap;
> +
> + curr_cap = get_bat_info(RELATIVE_CAP);
> +
> + if (curr_cap >= BAT_CAP_HIGH)
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_HIGH;
> + else if (curr_cap <= BAT_CAP_CRITICAL)
> + RET = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
> + }
> +
> + } break;
> + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW:
> + /* seconds */
> + RET = is_bat_in() ?
> + (get_bat_info(RELATIVE_CAP) - 3) * 54 + 142
> + : 0;
> + break;
> + case POWER_SUPPLY_PROP_STATUS:
> + {
> + int charge = ec_read(REG_BAT_CHARGE);
> +
> + if (charge & FLAG_BAT_CHARGE_DISCHARGE)
> + RET = POWER_SUPPLY_STATUS_DISCHARGING;
> + else if (charge & FLAG_BAT_CHARGE_CHARGE)
> + RET = POWER_SUPPLY_STATUS_CHARGING;
> + else
> + RET = POWER_SUPPLY_STATUS_NOT_CHARGING;
> + }
> + break;
> + case POWER_SUPPLY_PROP_HEALTH:
> + {
> + int status;
> +
> + if (!is_bat_in()) {
> + RET = POWER_SUPPLY_HEALTH_UNKNOWN;
> + break;
> + }
> +
> + status = get_bat_status();
> +
> + RET = POWER_SUPPLY_HEALTH_GOOD;
> + if (status & (BIT_BAT_STATUS_DESTROY |
> + BIT_BAT_STATUS_LOW))
> + RET = POWER_SUPPLY_HEALTH_DEAD;
> + if (ec_read(REG_BAT_CHARGE_STATUS) &
> + BIT_BAT_CHARGE_STATUS_OVERTEMP)
> + RET = POWER_SUPPLY_HEALTH_OVERHEAT;
> + }
> + break;
> + case POWER_SUPPLY_PROP_CHARGE_NOW: /* 1/100(%)*1000 µAh */
> + RET = get_bat_info(RELATIVE_CAP) *
> + get_bat_info(FULLCHG_CAP) * 10;
> + break;
> + default:
> + return -EINVAL;
> + }
> + return 0;
> +}
> +#undef RET
> +
> +static enum power_supply_property yeeloong_bat_props[] = {
> + POWER_SUPPLY_PROP_STATUS,
> + POWER_SUPPLY_PROP_PRESENT,
> + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN,
> + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
> + POWER_SUPPLY_PROP_CHARGE_FULL,
> + POWER_SUPPLY_PROP_CHARGE_NOW,
> + POWER_SUPPLY_PROP_CURRENT_NOW,
> + POWER_SUPPLY_PROP_VOLTAGE_NOW,
> + POWER_SUPPLY_PROP_HEALTH,
> + POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW,
> + POWER_SUPPLY_PROP_CAPACITY,
> + POWER_SUPPLY_PROP_CAPACITY_LEVEL,
> + POWER_SUPPLY_PROP_TEMP,
> + POWER_SUPPLY_PROP_MANUFACTURER,
> +};
> +
> +static const struct power_supply_desc yeeloong_bat_desc = {
> + .name = "yeeloongbattery",
> + .type = POWER_SUPPLY_TYPE_BATTERY,
> + .properties = yeeloong_bat_props,
> + .num_properties = ARRAY_SIZE(yeeloong_bat_props),
> + .get_property = yeeloong_get_bat_props,
> +};
> +
> +static int ac_bat_initialized;
> +
> +static int yeeloong_bat_init(void)
> +{
> + yeeloong_ac = power_supply_register(NULL, &yeeloong_ac_desc, NULL);
> + if (IS_ERR(yeeloong_ac))
> + return PTR_ERR(yeeloong_ac);
> + yeeloong_bat = power_supply_register(NULL, &yeeloong_bat_desc, NULL);
> + if (IS_ERR(yeeloong_bat)) {
> + power_supply_unregister(yeeloong_ac);
> + return PTR_ERR(yeeloong_bat);
> + }
> + ac_bat_initialized = 1;
> +
> + return 0;
> +}
> +
> +static void yeeloong_bat_exit(void)
> +{
> + if (ac_bat_initialized) {
> + ac_bat_initialized = 0;
> +
> + power_supply_unregister(yeeloong_ac);
> + power_supply_unregister(yeeloong_bat);
> + }
> +}
> +/* hwmon subdriver */
> +
> +#define MIN_FAN_SPEED 0
> +#define MAX_FAN_SPEED 3
> +
> +static int get_fan_pwm_enable(void)
> +{
> + int level, mode;
> +
> + level = ec_read(REG_FAN_SPEED_LEVEL);
> + mode = ec_read(REG_FAN_AUTO_MAN_SWITCH);
> +
> + if (level == MAX_FAN_SPEED && mode == BIT_FAN_MANUAL)
> + mode = 0;
> + else if (mode == BIT_FAN_MANUAL)
> + mode = 1;
> + else
> + mode = 2;
> +
> + return mode;
> +}
> +
> +static void set_fan_pwm_enable(int mode)
> +{
> + switch (mode) {
> + case 0:
> + /* fullspeed */
> + ec_write(REG_FAN_AUTO_MAN_SWITCH, BIT_FAN_MANUAL);
> + ec_write(REG_FAN_SPEED_LEVEL, MAX_FAN_SPEED);
> + break;
> + case 1:
> + ec_write(REG_FAN_AUTO_MAN_SWITCH, BIT_FAN_MANUAL);
> + break;
> + case 2:
> + ec_write(REG_FAN_AUTO_MAN_SWITCH, BIT_FAN_AUTO);
> + break;
> + default:
> + break;
> + }
> +}
> +
> +static int get_fan_pwm(void)
> +{
> + return ec_read(REG_FAN_SPEED_LEVEL);
> +}
> +
> +static void set_fan_pwm(int value)
> +{
> + int mode;
> +
> + mode = ec_read(REG_FAN_AUTO_MAN_SWITCH);
> + if (mode != BIT_FAN_MANUAL)
> + return;
> +
> + value = clamp_val(value, 0, 3);
> +
> + /* We must ensure the fan is on */
> + if (value > 0)
> + ec_write(REG_FAN_CONTROL, ON);
> +
> + ec_write(REG_FAN_SPEED_LEVEL, value);
> +}
> +
> +static int get_fan_rpm(void)
> +{
> + int value;
> +
> + value = FAN_SPEED_DIVIDER /
> + (((ec_read(REG_FAN_SPEED_HIGH) & 0x0f) << 8) |
> + ec_read(REG_FAN_SPEED_LOW));
> +
> + return value;
> +}
> +
> +static int get_cpu_temp(void)
> +{
> + s8 value;
> +
> + value = ec_read(REG_TEMPERATURE_VALUE);
> +
> + return value * 1000;
> +}
> +
> +static int get_cpu_temp_max(void)
> +{
> + return 60 * 1000;
> +}
> +
> +static int get_battery_temp_alarm(void)
> +{
> + int status;
> +
> + status = (ec_read(REG_BAT_CHARGE_STATUS) &
> + BIT_BAT_CHARGE_STATUS_OVERTEMP);
> +
> + return !!status;
> +}
> +
> +static ssize_t store_sys_hwmon(void (*set) (int), const char *buf, size_t count)
> +{
> + int ret;
> + unsigned long value;
> +
> + if (!count)
> + return 0;
> +
> + ret = kstrtoul(buf, 10, &value);
> + if (ret)
> + return ret;
> +
> + set(value);
> +
> + return count;
> +}
> +
> +static ssize_t show_sys_hwmon(int (*get) (void), char *buf)
> +{
> + return sprintf(buf, "%d\n", get());
> +}
> +
> +#define CREATE_SENSOR_ATTR(_name, _mode, _set, _get) \
> + static ssize_t show_##_name(struct device *dev, \
> + struct device_attribute *attr, \
> + char *buf) \
> + { \
> + return show_sys_hwmon(_set, buf); \
> + } \
> + static ssize_t store_##_name(struct device *dev, \
> + struct device_attribute *attr, \
> + const char *buf, size_t count) \
> + { \
> + return store_sys_hwmon(_get, buf, count); \
> + } \
> + static SENSOR_DEVICE_ATTR(_name, _mode, show_##_name, store_##_name, 0)
> +
> +CREATE_SENSOR_ATTR(fan1_input, 0444, get_fan_rpm, NULL);
> +CREATE_SENSOR_ATTR(pwm1, 0444 | 0644, get_fan_pwm, set_fan_pwm);
> +CREATE_SENSOR_ATTR(pwm1_enable, 0444 | 0644, get_fan_pwm_enable,
> + set_fan_pwm_enable);
> +CREATE_SENSOR_ATTR(temp1_input, 0444, get_cpu_temp, NULL);
> +CREATE_SENSOR_ATTR(temp1_max, 0444, get_cpu_temp_max, NULL);
> +CREATE_SENSOR_ATTR(temp2_input, 0444, get_battery_temp, NULL);
> +CREATE_SENSOR_ATTR(temp2_max_alarm, 0444, get_battery_temp_alarm, NULL);
> +CREATE_SENSOR_ATTR(curr1_input, 0444, get_battery_current, NULL);
> +CREATE_SENSOR_ATTR(in1_input, 0444, get_battery_voltage, NULL);
> +
> +static ssize_t
> +show_name(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> + return sprintf(buf, "yeeloong\n");
> +}
> +
> +static SENSOR_DEVICE_ATTR(name, 0444, show_name, NULL, 0);
> +
> +static struct attribute *hwmon_attributes[] = {
> + &sensor_dev_attr_pwm1.dev_attr.attr,
> + &sensor_dev_attr_pwm1_enable.dev_attr.attr,
> + &sensor_dev_attr_fan1_input.dev_attr.attr,
> + &sensor_dev_attr_temp1_input.dev_attr.attr,
> + &sensor_dev_attr_temp1_max.dev_attr.attr,
> + &sensor_dev_attr_temp2_input.dev_attr.attr,
> + &sensor_dev_attr_temp2_max_alarm.dev_attr.attr,
> + &sensor_dev_attr_curr1_input.dev_attr.attr,
> + &sensor_dev_attr_in1_input.dev_attr.attr,
> + &sensor_dev_attr_name.dev_attr.attr,
> + NULL
> +};
> +
> +static struct attribute_group hwmon_attribute_group = {
> + .attrs = hwmon_attributes
> +};
> +
> +static struct device *yeeloong_hwmon_dev;
> +
> +static int yeeloong_hwmon_init(void)
> +{
> + int ret;
> +
> + yeeloong_hwmon_dev = hwmon_device_register(NULL);
> + if (IS_ERR(yeeloong_hwmon_dev)) {
> + pr_err("Fail to register yeeloong hwmon device\n");
> + yeeloong_hwmon_dev = NULL;
> + return PTR_ERR(yeeloong_hwmon_dev);
> + }
> + ret = sysfs_create_group(&yeeloong_hwmon_dev->kobj,
> + &hwmon_attribute_group);
> + if (ret) {
> + hwmon_device_unregister(yeeloong_hwmon_dev);
> + yeeloong_hwmon_dev = NULL;
> + return ret;
> + }
> + /* ensure fan is set to auto mode */
> + set_fan_pwm_enable(2);
> +
> + return 0;
> +}
> +
> +static void yeeloong_hwmon_exit(void)
> +{
> + if (yeeloong_hwmon_dev) {
> + sysfs_remove_group(&yeeloong_hwmon_dev->kobj,
> + &hwmon_attribute_group);
> + hwmon_device_unregister(yeeloong_hwmon_dev);
> + yeeloong_hwmon_dev = NULL;
> + }
> +}
> +
> +/* video output controller */
> +
> +
> +#define LCD 0
> +#define CRT 1
> +
> +static void display_vo_set(int display, int on)
> +{
> + int addr;
> + unsigned long value;
> +
> + addr = (display == LCD) ? 0x31 : 0x21;
> +
> + outb(addr, 0x3c4);
> + value = inb(0x3c5);
> +
> + if (display == LCD)
> + value |= (on ? 0x03 : 0x02);
> + else {
> + if (on)
> + clear_bit(7, &value);
> + else
> + set_bit(7, &value);
> + }
> +
> + outb(addr, 0x3c4);
> + outb(value, 0x3c5);
> +}
> +
> +
> +
> +/* hotkey subdriver */
> +
> +static struct input_dev *yeeloong_hotkey_dev;
> +
> +static const struct key_entry yeeloong_keymap[] = {
> + {KE_SW, EVENT_LID, {SW_LID} },
> + /* Fn + ESC */
> + {KE_KEY, EVENT_CAMERA, {KEY_CAMERA} },
> + /* Fn + F1 */
> + {KE_KEY, EVENT_SLEEP, {KEY_SLEEP} },
> + /* Fn + F2 */
> + {KE_KEY, EVENT_DISPLAYTOGGLE, {KEY_DISPLAYTOGGLE} },
> + /* Fn + F3 */
> + {KE_KEY, EVENT_SWITCHVIDEOMODE, {KEY_SWITCHVIDEOMODE} },
> + /* Fn + F4 */
> + {KE_KEY, EVENT_AUDIO_MUTE, {KEY_MUTE} },
> + /* Fn + F5 */
> + {KE_KEY, EVENT_WLAN, {KEY_WLAN} },
> + /* Fn + up */
> + {KE_KEY, EVENT_DISPLAY_BRIGHTNESS, {KEY_BRIGHTNESSUP} },
> + /* Fn + down */
> + {KE_KEY, EVENT_DISPLAY_BRIGHTNESS, {KEY_BRIGHTNESSDOWN} },
> + /* Fn + right */
> + {KE_KEY, EVENT_AUDIO_VOLUME, {KEY_VOLUMEUP} },
> + /* Fn + left */
> + {KE_KEY, EVENT_AUDIO_VOLUME, {KEY_VOLUMEDOWN} },
> + {KE_END, 0} };
> +
> +static struct key_entry *get_event_key_entry(int event, int status)
> +{
> + struct key_entry *ke;
> + static int old_brightness_status = -1;
> + static int old_volume_status = -1;
> +
> + ke = sparse_keymap_entry_from_scancode(yeeloong_hotkey_dev, event);
> + if (!ke)
> + return NULL;
> +
> + switch (event) {
> + case EVENT_DISPLAY_BRIGHTNESS:
> + /* current status > old one, means up */
> + if ((status == 0) || (status < old_brightness_status))
> + ke++;
> + old_brightness_status = status;
> + break;
> + case EVENT_AUDIO_VOLUME:
> + if ((status == 0) || (status < old_volume_status))
> + ke++;
> + old_volume_status = status;
> + break;
> + default:
> + break;
> + }
> +
> + return ke;
> +}
> +
> +static int report_lid_switch(int status)
> +{
> + input_report_switch(yeeloong_hotkey_dev, SW_LID, !status);
> + input_sync(yeeloong_hotkey_dev);
> +
> + return status;
> +}
> +
> +static void yeeloong_vo_set(int lcd_status, int crt_status)
> +{
> + display_vo_set(LCD, lcd_status);
> + display_vo_set(CRT, crt_status);
> +}
> +
> +static int crt_detect_handler(int status)
> +{
> + if (status)
> + yeeloong_vo_set(ON, ON);
> + else
> + yeeloong_vo_set(ON, OFF);
> +
> + return status;
> +}
> +
> +static int displaytoggle_handler(int status)
> +{
> + /* EC(>=PQ1D26) does this job for us, we can not do it again,
> + * otherwise, the brightness will not resume to the normal level!
> + */
> + if (ec_version_before("EC_VER=PQ1D26"))
> + display_vo_set(LCD, status);
> +
> + return status;
> +}
> +
> +static int switchvideomode_handler(int status)
> +{
> + static int video_output_status;
> +
> + /* Only enable switch video output button
> + * when CRT is connected
> + */
> + if (ec_read(REG_CRT_DETECT) == OFF)
> + return 0;
> + /* 0. no CRT connected: LCD on, CRT off
> + * 1. BOTH on
> + * 2. LCD off, CRT on
> + * 3. BOTH off
> + * 4. LCD on, CRT off
> + */
> + video_output_status++;
> + if (video_output_status > 4)
> + video_output_status = 1;
> +
> + switch (video_output_status) {
> + case 1:
> + yeeloong_vo_set(ON, ON);
> + break;
> + case 2:
> + yeeloong_vo_set(OFF, ON);
> + break;
> + case 3:
> + yeeloong_vo_set(OFF, OFF);
> + break;
> + case 4:
> + yeeloong_vo_set(ON, OFF);
> + break;
> + default:
> + /* Ensure LCD is on */
> + display_vo_set(LCD, ON);
> + break;
> + }
> + return video_output_status;
> +}
> +
> +static int camera_handler(int status)
> +{
> + int value;
> +
> + value = ec_read(REG_CAMERA_CONTROL);
> + ec_write(REG_CAMERA_CONTROL, value | (1 << 1));
> +
> + return status;
> +}
> +
> +static int usb2_handler(int status)
> +{
> + pr_emerg("USB2 Over Current occurred\n");
> +
> + return status;
> +}
> +
> +static int usb0_handler(int status)
> +{
> + pr_emerg("USB0 Over Current occurred\n");
> +
> + return status;
> +}
> +
> +static int ac_bat_handler(int status)
> +{
> + if (ac_bat_initialized) {
> + power_supply_changed(yeeloong_ac);
> + power_supply_changed(yeeloong_bat);
> + }
> + return status;
> +}
> +
> +static void do_event_action(int event)
> +{
> + sci_handler handler;
> + int reg, status;
> + struct key_entry *ke;
> +
> + reg = 0;
> + handler = NULL;
> + status = 0;
> +
> + switch (event) {
> + case EVENT_LID:
> + reg = REG_LID_DETECT;
> + break;
> + case EVENT_SWITCHVIDEOMODE:
> + handler = switchvideomode_handler;
> + break;
> + case EVENT_CRT_DETECT:
> + reg = REG_CRT_DETECT;
> + handler = crt_detect_handler;
> + break;
> + case EVENT_CAMERA:
> + reg = REG_CAMERA_STATUS;
> + handler = camera_handler;
> + break;
> + case EVENT_USB_OC2:
> + reg = REG_USB2_FLAG;
> + handler = usb2_handler;
> + break;
> + case EVENT_USB_OC0:
> + reg = REG_USB0_FLAG;
> + handler = usb0_handler;
> + break;
> + case EVENT_DISPLAYTOGGLE:
> + reg = REG_DISPLAY_LCD;
> + handler = displaytoggle_handler;
> + break;
> + case EVENT_AUDIO_MUTE:
> + reg = REG_AUDIO_MUTE;
> + break;
> + case EVENT_DISPLAY_BRIGHTNESS:
> + reg = REG_DISPLAY_BRIGHTNESS;
> + break;
> + case EVENT_AUDIO_VOLUME:
> + reg = REG_AUDIO_VOLUME;
> + break;
> + case EVENT_AC_BAT:
> + handler = ac_bat_handler;
> + break;
> + default:
> + break;
> + }
> +
> + if (reg != 0)
> + status = ec_read(reg);
> +
> + if (handler != NULL)
> + status = handler(status);
> +
> + pr_debug("%s: event: %d status: %d\n", __func__, event, status);
> +
> + /* Report current key to user-space */
> + ke = get_event_key_entry(event, status);
> + if (ke) {
> + if (ke->keycode == SW_LID)
> + report_lid_switch(status);
> + else
> + sparse_keymap_report_entry(yeeloong_hotkey_dev, ke, 1,
> + true);
> + }
> +}
> +
> +/*
> + * SCI(system control interrupt) main interrupt routine
> + *
> + * We will do the query and get event number together so the interrupt routine
> + * should be longer than 120us now at least 3ms elpase for it.
> + */
> +static irqreturn_t sci_irq_handler(int irq, void *dev_id)
> +{
> + int ret, event;
> +
> + if (irq != SCI_IRQ_NUM)
> + return IRQ_NONE;
> +
> + /* Query the event number */
> + ret = ec_query_event_num();
> + if (ret < 0)
> + return IRQ_NONE;
> +
> + event = ec_get_event_num();
> + if (event < EVENT_START || event > EVENT_END)
> + return IRQ_NONE;
> +
> + /* Execute corresponding actions */
> + do_event_action(event);
> +
> + return IRQ_HANDLED;
> +}
> +
> +/*
> + * Config and init some msr and gpio register properly.
> + */
> +static int sci_irq_init(void)
> +{
> + u32 hi, lo;
> + u32 gpio_base;
> + unsigned long flags;
> + int ret;
> +
> + /* Get gpio base */
> + _rdmsr(DIVIL_MSR_REG(DIVIL_LBAR_GPIO), &hi, &lo);
> + gpio_base = lo & 0xff00;
> +
> + /* Filter the former kb3310 interrupt for security */
> + ret = ec_query_event_num();
> + if (ret)
> + return ret;
> +
> + /* For filtering next number interrupt */
> + mdelay(10000);
> +
> + /* Set gpio native registers and msrs for GPIO27 SCI EVENT PIN
> + * gpio :
> + * input, pull-up, no-invert, event-count and value 0,
> + * no-filter, no edge mode
> + * gpio27 map to Virtual gpio0
> + * msr :
> + * no primary and lpc
> + * Unrestricted Z input to IG10 from Virtual gpio 0.
> + */
> + local_irq_save(flags);
> + _rdmsr(0x80000024, &hi, &lo);
> + lo &= ~(1 << 10);
> + _wrmsr(0x80000024, hi, lo);
> + _rdmsr(0x80000025, &hi, &lo);
> + lo &= ~(1 << 10);
> + _wrmsr(0x80000025, hi, lo);
> + _rdmsr(0x80000023, &hi, &lo);
> + lo |= (0x0a << 0);
> + _wrmsr(0x80000023, hi, lo);
> + local_irq_restore(flags);
> +
> + /* Set gpio27 as sci interrupt
> + *
> + * input, pull-up, no-fliter, no-negedge, invert
> + * the sci event is just about 120us
> + */
> + asm(".set noreorder\n");
> + /* input enable */
> + outl(0x00000800, (gpio_base | 0xA0));
> + /* revert the input */
> + outl(0x00000800, (gpio_base | 0xA4));
> + /* event-int enable */
> + outl(0x00000800, (gpio_base | 0xB8));
> + asm(".set reorder\n");
> +
> + return 0;
> +}
> +
> +static int yeeloong_hotkey_init(void)
> +{
> + int ret;
> +
> + ret = sci_irq_init();
> + if (ret)
> + return -EFAULT;
> +
> + ret = request_threaded_irq(SCI_IRQ_NUM, NULL, &sci_irq_handler,
> + IRQF_ONESHOT, "sci", NULL);
> + if (ret)
> + return -EFAULT;
> +
> + yeeloong_hotkey_dev = input_allocate_device();
> +
> + if (!yeeloong_hotkey_dev) {
> + free_irq(SCI_IRQ_NUM, NULL);
> + return -ENOMEM;
> + }
> +
> + yeeloong_hotkey_dev->name = "HotKeys";
> + yeeloong_hotkey_dev->phys = "button/input0";
> + yeeloong_hotkey_dev->id.bustype = BUS_HOST;
> + yeeloong_hotkey_dev->dev.parent = NULL;
> +
> + ret = sparse_keymap_setup(yeeloong_hotkey_dev, yeeloong_keymap, NULL);
> + if (ret) {
> + pr_err("Fail to setup input device keymap\n");
> + input_free_device(yeeloong_hotkey_dev);
> + return ret;
> + }
> +
> + ret = input_register_device(yeeloong_hotkey_dev);
> + if (ret) {
> + input_free_device(yeeloong_hotkey_dev);
> + return ret;
> + }
> +
> + /* Update the current status of LID */
> + report_lid_switch(ON);
> +
> +#ifdef CONFIG_LOONGSON_SUSPEND
> + /* Install the real yeeloong_report_lid_status for pm.c */
> + yeeloong_report_lid_status = report_lid_switch;
> +#endif

CONFIG_LOONGSON_SUSPEND was removed by 5361832704d3 ("MIPS: Loongson:
Cleanup CONFIG_LOONGSON_SUSPEND.") on 2017-10-07. So while probably
replacing CONFIG_LOONGSON_SUSPEND with CONFIG_SUSPEND appears to be the
right thing I'm wondering if code that is not being built since two
years is still needed or can be considered tested ...

> +
> + return 0;
> +}
> +
> +static void yeeloong_hotkey_exit(void)
> +{
> + /* Free irq */
> + free_irq(SCI_IRQ_NUM, NULL);
> +
> +#ifdef CONFIG_LOONGSON_SUSPEND
> + /* Uninstall yeeloong_report_lid_status for pm.c */
> + if (yeeloong_report_lid_status == report_lid_switch)
> + yeeloong_report_lid_status = NULL;
> +#endif

Ditto.

> +
> + if (yeeloong_hotkey_dev) {
> + input_unregister_device(yeeloong_hotkey_dev);
> + yeeloong_hotkey_dev = NULL;
> + }
> +}
> +
> +#ifdef CONFIG_PM
> +static void usb_ports_set(int status)
> +{
> + status = !!status;
> +
> + ec_write(REG_USB0_FLAG, status);
> + ec_write(REG_USB1_FLAG, status);
> + ec_write(REG_USB2_FLAG, status);
> +}
> +
> +static int yeeloong_suspend(struct device *dev)
> +
> +{
> + if (ec_version_before("EC_VER=PQ1D27"))
> + display_vo_set(LCD, OFF);
> + display_vo_set(CRT, OFF);
> + usb_ports_set(OFF);
> +
> + return 0;
> +}
> +
> +static int yeeloong_resume(struct device *dev)
> +{
> + int ret;
> +
> + if (ec_version_before("EC_VER=PQ1D27"))
> + display_vo_set(LCD, ON);
> + display_vo_set(CRT, ON);
> + usb_ports_set(ON);
> +
> + ret = sci_irq_init();
> + if (ret)
> + return -EFAULT;
> +
> + return 0;
> +}
> +
> +static const SIMPLE_DEV_PM_OPS(yeeloong_pm_ops, yeeloong_suspend,
> + yeeloong_resume);
> +#endif
> +
> +static struct platform_device_id platform_device_ids[] = {
> + {
> + .name = "yeeloong_laptop",
> + },
> + {}
> +};
> +
> +MODULE_DEVICE_TABLE(platform, platform_device_ids);
> +
> +static struct platform_driver platform_driver = {
> + .driver = {
> + .name = "yeeloong_laptop",
> + .owner = THIS_MODULE,
> +#ifdef CONFIG_PM
> + .pm = &yeeloong_pm_ops,
> +#endif
> + },
> + .id_table = platform_device_ids,
> +};
> +
> +static int __init yeeloong_init(void)
> +{
> + int ret;
> +
> + if (mips_machtype != MACH_LEMOTE_YL2F89) {
> + pr_err("Unsupported system.\n");
> + return -ENODEV;
> + }
> +
> + pr_info("Load YeeLoong Laptop Platform Specific Driver.\n");
> +
> + /* Register platform stuff */
> + ret = platform_driver_register(&platform_driver);
> + if (ret) {
> + pr_err("Fail to register yeeloong platform driver.\n");
> + return ret;
> + }
> +
> + ret = yeeloong_backlight_init();
> + if (ret) {
> + pr_err("Fail to register yeeloong backlight driver.\n");
> + yeeloong_backlight_exit();
> + return ret;
> + }
> +
> + ret = yeeloong_bat_init();
> + if (ret) {
> + pr_err("Fail to register yeeloong battery driver.\n");
> + yeeloong_bat_exit();
> + return ret;
> + }
> +
> + ret = yeeloong_hwmon_init();
> + if (ret) {
> + pr_err("Fail to register yeeloong hwmon driver.\n");
> + yeeloong_hwmon_exit();
> + return ret;
> + }
> +
> + ret = yeeloong_hotkey_init();
> + if (ret) {
> + pr_err("Fail to register yeeloong hotkey driver.\n");
> + yeeloong_hotkey_exit();
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void __exit yeeloong_exit(void)
> +{
> + yeeloong_hotkey_exit();
> + yeeloong_hwmon_exit();
> + yeeloong_bat_exit();
> + yeeloong_backlight_exit();
> + platform_driver_unregister(&platform_driver);
> +
> + pr_info("Unload YeeLoong Platform Specific Driver.\n");
> +}
> +
> +module_init(yeeloong_init);
> +module_exit(yeeloong_exit);
> +
> +MODULE_AUTHOR("Wu Zhangjin <wuzhangjin@xxxxxxxxx>; Liu Junliang <liujl@xxxxxxxxxx>");
> +MODULE_DESCRIPTION("YeeLoong laptop driver");
> +MODULE_LICENSE("GPL");

Ralf