| /* Copyright (c) 2017, LGE Inc. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/err.h> |
| #include <linux/gpio.h> |
| #include <linux/kernel.h> |
| #include <linux/delay.h> |
| #include <linux/firmware.h> |
| #include <linux/i2c.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| #include <linux/string.h> |
| #include <linux/sysfs.h> |
| #include <linux/timer.h> |
| #include <linux/time.h> |
| #include <linux/random.h> |
| #include <linux/rtc.h> |
| |
| #include "sop716_firmware.h" |
| |
| #define SOP716_USE_DISTANCE 0 |
| |
| #define SOP716_I2C_DATA_LENGTH 8 |
| |
| #define SOP716_CMD_READ_RETRY 10 |
| |
| #define SOP716_RESET_DELAY_MS 400 |
| #define SOP716_SETTLE_TIME_MS 2000 |
| #define SOP716_EOL_MIN_MV 1500 |
| #define SOP716_EOL_MAX_MV 4200 |
| #define SOP716_BATTERY_CHECK_INTERVAL 30 |
| #define SOP716_EOL_LIMIT_MV 3700 |
| #define SOP716_EOL_THRESHOLD_MV 3900 |
| |
| #define SOP716_MOTOR_MAX_POS 179 |
| #define SOP716_MOTOR_TIME_PER_STEP_MS 17 |
| |
| /* FIXME: i2c error on subsequent i2c reads in 100ms */ |
| #define SOP716_I2C_READ_POST_DELAY_MS 100 |
| |
| /* SOP716 COMMANDS SUB OPTIONS */ |
| #define SOP716_OPT_ACC_DEC 0x80 |
| #define SOP716_OPT_STACKED 0x40 |
| #define SOP716_OPT_DIRECTION 0x20 |
| #define SOP716_OPT_DIR_CCW 0x10 |
| #define SOP716_OPT_NO_HANDS_MOVE 0x08 |
| |
| enum sop716_motor_ids { |
| MOTOR1 = 0, |
| MOTOR2, |
| }; |
| |
| enum sop716_cmd_ids { |
| CMD_SOP716_SET_CURRENT_TIME = 0, |
| CMD_SOP716_MOTOR_MOVE_ONE, |
| CMD_SOP716_MOTOR_MOVE_ALL, |
| CMD_SOP716_MOTOR_INIT, |
| CMD_SOP716_READ_CURRENT_TIME, |
| CMD_SOP716_READ_FW_VERSION, /* 5 */ |
| CMD_SOP716_RESERVED_6, |
| CMD_SOP716_BATTERY_CHECK_INTERVAL, |
| CMD_SOP716_READ_BATTERY_LEVEL, |
| CMD_SOP716_READ_BATTERY_CHECK_INTERVAL, |
| CMD_SOP716_SET_TIMEZONE, /* 10 */ |
| CMD_SOP716_READ_TIMEZONE, |
| CMD_SOP716_SET_EOL_BATTERY_LEVEL, |
| CMD_SOP716_READ_EOL_BATTERY_LEVEL, |
| CMD_SOP716_READ_MOTORS_POSITION, |
| CMD_SOP716_READ_MODE, /* 15 */ |
| CMD_SOP716_SET_AGING_TEST, |
| CMD_SOP716_MAX |
| }; |
| |
| enum sop716_mode_ids { |
| MODE_IDLE, |
| MODE_START_TURN_CW, |
| MODE_START_TURN_CCW, |
| MODE_RETURN_POS_0, |
| MODE_WATCH = 5, |
| MODE_MOTOR, |
| MODE_MAX = 10 |
| }; |
| |
| struct sop716_command { |
| char *desc; |
| u8 size; |
| u8 rev; |
| }; |
| |
| struct sop716_info { |
| struct i2c_client *client; |
| struct device *dev; |
| |
| int gpio_reset; |
| int gpio_interrupt; |
| int gpio_bslen; |
| int gpio_temp_1; |
| |
| int reset_status; |
| int tz_minuteswest; |
| |
| int reset_delay_ms; |
| int hctosys_pre_delay_ms; |
| int hctosys_post_delay_ms; |
| int fw_ver_check_delay_ms; |
| int hands_settle_time_ms; |
| int battery_check_interval; |
| int eol_limit_mv; |
| int eol_threshold_mv; |
| |
| u8 fw_ver; |
| u8 fw_rev; |
| |
| bool update_sysclock_on_boot; |
| bool watch_mode; |
| |
| struct sop716_command *cmds; |
| |
| struct work_struct fw_work; |
| struct work_struct sysclock_work; |
| struct work_struct init_work; |
| struct mutex lock; |
| |
| u64 saved_ts_ns; /* workaround for i2c read errors */ |
| }; |
| |
| struct sop716_error { |
| struct sop716_info *si; |
| int errno; |
| u32 ts; /* in seconds */ |
| u8 hour; |
| u8 minute; |
| u16 code; /* (type << 8)|param */ |
| }; |
| |
| static struct sop716_info *sop716_info; |
| |
| static void sop716_hw_reset_locked(struct sop716_info *si, bool need_dump); |
| static int sop716_restore_default_config(struct sop716_info *si); |
| static void sop716_print_errors(struct sop716_info *si); |
| static int sop716_do_self_test(struct sop716_info *si); |
| |
| static inline int REG_CMD(struct sop716_command *cmd, u8 reg, char *desc, |
| u8 size, u8 rev) |
| { |
| if (reg >= CMD_SOP716_MAX) { |
| pr_err("%s: unknown command %d\n", __func__, reg); |
| return -EINVAL; |
| } |
| |
| cmd[reg].desc = desc; |
| cmd[reg].size = size; |
| cmd[reg].rev = rev; |
| |
| return 0; |
| } |
| |
| static int sop716_register_commands(struct sop716_info *si) |
| { |
| int err; |
| |
| si->cmds = devm_kzalloc(si->dev, |
| sizeof(struct sop716_command) * CMD_SOP716_MAX, |
| GFP_KERNEL); |
| if (!si->cmds) { |
| pr_err("%s: no mem\n", __func__); |
| return -ENOMEM; |
| } |
| |
| err = REG_CMD(si->cmds, CMD_SOP716_SET_CURRENT_TIME, |
| "set time", 8, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_MOTOR_MOVE_ONE, |
| "motor move one", 4, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_MOTOR_MOVE_ALL, |
| "motor move all", 4, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_MOTOR_INIT, |
| "motor init", 4, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_CURRENT_TIME, |
| "read time", 6, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_FW_VERSION, |
| "read fw version", 2, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_BATTERY_CHECK_INTERVAL, |
| "battery check interval", 3, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_BATTERY_LEVEL, |
| "read battery level" , 2, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_BATTERY_CHECK_INTERVAL, |
| "read battery check interval", 2, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_SET_TIMEZONE, |
| "set timezone", 3, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_TIMEZONE, |
| "read timezone", 2, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_SET_EOL_BATTERY_LEVEL, |
| "set eol battery level" , 5, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_EOL_BATTERY_LEVEL, |
| "read eol battery level", 4, 0); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_MOTORS_POSITION, |
| "read motors position", 2, 16); |
| err |= REG_CMD(si->cmds, CMD_SOP716_READ_MODE, |
| "read mode", 1, 16); |
| err |= REG_CMD(si->cmds, CMD_SOP716_SET_AGING_TEST, |
| "set aging test", 3, 16); |
| |
| return err; |
| } |
| |
| static int sop716_write(struct sop716_info *si, u8 reg, u8 *val) |
| { |
| int ret; |
| int retry = 1; |
| |
| pr_debug("%s: reg:%d val:%d\n", __func__, val[0], *val); |
| |
| if (reg >= CMD_SOP716_MAX) { |
| pr_err("%s: unknown command %d\n", __func__, reg); |
| return -EINVAL; |
| } |
| |
| if (si->cmds[reg].rev > si->fw_rev) { |
| pr_warn("%s: cmd %s(%d) not supported by fw v%d.%d\n", |
| __func__, |
| si->cmds[reg].desc, reg, |
| si->fw_ver, si->fw_rev); |
| return -ENOTSUPP; |
| } |
| |
| /* sop716 writes size instead of reg */ |
| do { |
| ret = i2c_smbus_write_i2c_block_data(si->client, |
| si->cmds[reg].size, si->cmds[reg].size, val); |
| if (ret < 0) { |
| pr_warn("%s: cannot write i2c: cmd %s(%d)\n" |
| "and force reset to recover\n", |
| __func__, si->cmds[reg].desc, reg); |
| sop716_hw_reset_locked(si, true); |
| } |
| } while (retry-- && ret < 0); |
| |
| if (ret < 0) |
| pr_err("%s: cannot write i2c: cmd %s(%d)\n", |
| __func__, si->cmds[reg].desc, reg); |
| |
| return ret; |
| } |
| |
| static int sop716_read(struct sop716_info *si, u8 reg, u8 *val) |
| { |
| int ret; |
| int retry = 2; |
| u64 saved_ts_ns = si->saved_ts_ns; |
| u64 off_ms; |
| bool retried_with_delay = false; |
| |
| pr_debug("%s: reg:%d\n", __func__, reg); |
| |
| if (reg >= CMD_SOP716_MAX) { |
| pr_err("%s: unknown command %d\n", __func__, reg); |
| return -EINVAL; |
| } |
| |
| if (si->cmds[reg].rev > si->fw_rev) { |
| pr_warn("%s: cmd %s(%d) not supported by FW %d.%d\n", |
| __func__, |
| si->cmds[reg].desc, reg, |
| si->fw_ver, si->fw_rev); |
| return -ENOTSUPP; |
| } |
| |
| /* FIXME: i2c error on subsequent i2c reads */ |
| off_ms = div_u64(ktime_get_boot_ns() - saved_ts_ns, NSEC_PER_MSEC); |
| if (off_ms < SOP716_I2C_READ_POST_DELAY_MS) |
| msleep(SOP716_I2C_READ_POST_DELAY_MS - off_ms); |
| |
| /* read size + data, So "size + 1" need to be added */ |
| do { |
| ret = i2c_smbus_read_i2c_block_data(si->client, |
| reg, si->cmds[reg].size + 1, val); |
| if (ret < 0) { |
| pr_warn("%s: cannot read i2c: cmd %s(%d)\n" |
| "and force reset to recover\n", |
| __func__, si->cmds[reg].desc, reg); |
| sop716_hw_reset_locked(si, true); |
| /* If i2c error happens during retring with delay, |
| * rery it one more after reset |
| */ |
| if (retried_with_delay) |
| retried_with_delay = false; |
| else |
| retry--; |
| |
| } else if ((ret - 1) != val[0]) { |
| pr_warn("%s: size is not matched with requested: " |
| "cmd %s(%d) size %d, ret %d, val[0] %d\n" |
| "and retry it later\n", |
| __func__, si->cmds[reg].desc, reg, |
| si->cmds[reg].size, ret, val[0]); |
| msleep(100); |
| ret = -EPIPE; /* for retry */ |
| retried_with_delay = true; |
| retry--; |
| } |
| } while (retry && ret < 0); |
| |
| si->saved_ts_ns = ktime_get_boot_ns(); |
| |
| if (ret < 0) { |
| pr_err("%s: cannot read i2c: cmd %s(%d)\n", |
| __func__, si->cmds[reg].desc, reg); |
| return ret; |
| } |
| |
| return ret; |
| } |
| |
| static int sop716_read_error(struct sop716_error *serr) |
| { |
| struct sop716_info *si = serr->si; |
| u8 errno = serr->errno; |
| u64 saved_ts_ns = si->saved_ts_ns; |
| u64 off_ms; |
| u8 val[8] = {0, }; |
| int ret; |
| int retry = 1; |
| |
| pr_debug("%s: errno:%d\n", __func__, errno); |
| |
| if (errno > 0xF) { |
| pr_err("%s: unknown errno %d\n", __func__, errno); |
| return -EINVAL; |
| } |
| |
| /* FIXME: i2c error on subsequent i2c reads */ |
| off_ms = div_u64(ktime_get_boot_ns() - saved_ts_ns, NSEC_PER_MSEC); |
| if (off_ms < SOP716_I2C_READ_POST_DELAY_MS) |
| msleep(SOP716_I2C_READ_POST_DELAY_MS - off_ms); |
| |
| do { |
| ret = i2c_smbus_read_i2c_block_data(si->client, |
| 0x80 + errno, 8, val); |
| if (ret < 0) |
| sop716_hw_reset_locked(si, false); |
| } while (retry-- && ret < 0); |
| |
| si->saved_ts_ns = ktime_get_boot_ns(); |
| |
| if (ret < 0) { |
| pr_err("%s: cannot read i2c: errno:%d\n", __func__, errno); |
| return ret; |
| } |
| |
| serr->errno = errno; |
| serr->ts = (val[0] << 24) | (val[1] << 16) | (val[2] << 8) | val[3]; |
| serr->hour = val[4]; |
| serr->minute = val[5]; |
| serr->code = (val[6] << 8) | val[7]; |
| |
| return ret; |
| } |
| |
| static void sop716_print_error_single(struct sop716_error *serr) |
| { |
| char *reason = "Unknown"; |
| |
| switch (serr->code) { |
| /* system reset */ |
| case 0x100: |
| reason = "No interrupt pending"; |
| break; |
| case 0x102: |
| reason = "Brownout (BOR)"; |
| break; |
| case 0x104: |
| reason = "RST/NMI (BOR)"; |
| break; |
| case 0x106: |
| reason = "PMMSWBOR (BOR)"; |
| break; |
| case 0x108: |
| reason = "Wakeup from LPMx.5"; |
| break; |
| case 0x10A: |
| reason = "Security violation"; |
| break; |
| case 0x10C: |
| reason = "SVSL (POR)"; |
| break; |
| case 0x10E: |
| reason = "SVSH (POR)"; |
| break; |
| case 0x110: |
| reason = "SVML_OVP (POR)"; |
| break; |
| case 0x112: |
| reason = "SVMH_OVP (POR)"; |
| break; |
| case 0x114: |
| reason = "PMMSWPOR (POR)"; |
| break; |
| case 0x116: |
| reason = "WDT time out (PUC)"; |
| break; |
| case 0x118: |
| reason = "WDT password violation (PUC)"; |
| break; |
| case 0x11A: |
| reason = "Flash password violation (PUC)"; |
| break; |
| case 0x11E: |
| reason = "PERF peripheral/configuration area fetch (PUC)"; |
| break; |
| case 0x120: |
| reason = "PMM password vioation (PUC)"; |
| break; |
| /* A/D converter error */ |
| case 0x202: |
| reason = "Register Overflow"; |
| break; |
| case 0x204: |
| reason = "Conversion time Overflow"; |
| break; |
| /* serial error */ |
| case 0x302: |
| reason = "Too few byte received"; |
| break; |
| case 0x304: |
| reason = "Too many byte received"; |
| break; |
| case 0x306: |
| reason = "Stop bit not received"; |
| break; |
| case 0x308: |
| reason = "I2C timeout"; |
| break; |
| /* motor error */ |
| case 0x402: |
| reason = "Wrong motor position"; |
| |
| break; |
| case 0x200: |
| case 0x300: |
| case 0x400: |
| reason = "Nothing"; |
| break; |
| default: |
| break; |
| } |
| |
| printk("%2u [%10u] %2u:%-2u %s (%3x)\n", |
| serr->errno, serr->ts, serr->hour, serr->minute, |
| reason, serr->code); |
| } |
| |
| static void sop716_print_errors(struct sop716_info *si) |
| { |
| u8 errno; |
| struct sop716_error serr; |
| int ret; |
| |
| mutex_lock(&si->lock); |
| printk("sop716 dump errors:\n" |
| "no [ts in second] h:m reason_string (code)\n" |
| "---------------------------------------\n"); |
| for (errno = 0; errno < 0x10; errno++) { |
| memset(&serr, 0, sizeof(struct sop716_error)); |
| |
| serr.si = si; |
| serr.errno = errno; |
| |
| ret = sop716_read_error(&serr); |
| if (ret < 0) |
| break; |
| sop716_print_error_single(&serr); |
| } |
| printk("---------------------------------------\n"); |
| mutex_unlock(&si->lock); |
| } |
| |
| static void sop716_hw_reset_locked(struct sop716_info *si, bool need_dump) |
| { |
| gpio_set_value(si->gpio_reset, 1); |
| msleep(10); |
| gpio_set_value(si->gpio_reset, 0); |
| msleep(si->reset_delay_ms); |
| |
| if (need_dump) { |
| mutex_unlock(&si->lock); |
| sop716_print_errors(si); |
| mutex_lock(&si->lock); |
| } |
| } |
| |
| static int sop716_read_tz_minutes(struct sop716_info *si) |
| { |
| int ret; |
| int tz_minutes; |
| s8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| |
| mutex_lock(&si->lock); |
| ret = sop716_read(si, CMD_SOP716_READ_TIMEZONE, data); |
| mutex_unlock(&si->lock); |
| if (ret < 0) { |
| pr_err("%s: error: read timezone\n", __func__); |
| return 0; |
| } |
| |
| tz_minutes = (int)data[2]; /* hour */ |
| tz_minutes = (tz_minutes * 60) + data[1]; |
| |
| return tz_minutes; |
| } |
| |
| static int sop716_set_battery_check_interval(struct sop716_info *si, |
| int interval) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| int rc; |
| |
| pr_debug("%s: interval:%d\n", __func__, interval); |
| |
| if ((interval < 1 || interval > 7200)) { |
| pr_err("%s: out of range %d\n", __func__, interval); |
| return -EINVAL; |
| } |
| |
| data[0] = CMD_SOP716_BATTERY_CHECK_INTERVAL; |
| data[1] = (interval >> 8) & 0xff; |
| data[2] = interval & 0xff; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_write(si, CMD_SOP716_BATTERY_CHECK_INTERVAL, data); |
| if (rc < 0) |
| goto out; |
| |
| si->battery_check_interval = interval; |
| rc = 0; |
| out: |
| mutex_unlock(&si->lock); |
| return rc; |
| } |
| |
| static int sop716_set_eol_limit(struct sop716_info *si, int eol_limit) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| int rc = 0; |
| |
| if (eol_limit < SOP716_EOL_MIN_MV || eol_limit > SOP716_EOL_MAX_MV ) { |
| pr_err("%s: out of range!\n", __func__); |
| return -EINVAL; |
| } |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_EOL_BATTERY_LEVEL, data); |
| if (rc < 0) { |
| pr_err("%s: failed to read EOL info\n", __func__); |
| goto out; |
| } |
| |
| data[0] = CMD_SOP716_SET_EOL_BATTERY_LEVEL; |
| data[1] = (eol_limit >> 8) & 0xff; |
| data[2] = eol_limit & 0xff; |
| |
| rc = sop716_write(si, CMD_SOP716_SET_EOL_BATTERY_LEVEL, data); |
| if (rc < 0) |
| goto out; |
| |
| si->eol_limit_mv = eol_limit; |
| rc = 0; |
| out: |
| mutex_unlock(&si->lock); |
| return rc; |
| } |
| |
| static int sop716_set_eol_threshold(struct sop716_info *si, int eol_threshold) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| int rc = 0; |
| |
| if (eol_threshold < SOP716_EOL_MIN_MV || |
| eol_threshold > SOP716_EOL_MAX_MV) { |
| pr_err("%s: out of range!\n", __func__); |
| return -EINVAL; |
| } |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_EOL_BATTERY_LEVEL, data); |
| if (rc < 0) { |
| pr_err("%s: failed to read EOL info\n", __func__); |
| goto out; |
| } |
| |
| data[0] = CMD_SOP716_SET_EOL_BATTERY_LEVEL; |
| data[3] = (eol_threshold >> 8) & 0xff; |
| data[4] = eol_threshold & 0xff; |
| |
| rc = sop716_write(si, CMD_SOP716_SET_EOL_BATTERY_LEVEL, data); |
| if (rc < 0) |
| goto out; |
| |
| si->eol_threshold_mv = eol_threshold; |
| rc = 0; |
| out: |
| mutex_unlock(&si->lock); |
| return rc; |
| } |
| |
| static int sop716_read_mode(struct sop716_info *si) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| int rc; |
| int mode; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_MODE, data); |
| mutex_unlock(&si->lock); |
| |
| mode = data[1]; |
| |
| /* if mode is not determined, set the mode to WATCH by default */ |
| if (rc < 0 || mode > MODE_WATCH) |
| mode = MODE_WATCH; |
| |
| return mode; |
| } |
| |
| static void sop716_read_fw_version(struct sop716_info *si) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| |
| mutex_lock(&si->lock); |
| sop716_read(si, CMD_SOP716_READ_FW_VERSION, data); |
| mutex_unlock(&si->lock); |
| |
| si->fw_ver = data[1]; |
| si->fw_rev = data[2]; |
| } |
| |
| static void sop716_read_motors_position_locked(struct sop716_info *si, |
| u8 motor_pos[]) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| |
| sop716_read(si, CMD_SOP716_READ_MOTORS_POSITION, data); |
| |
| motor_pos[MOTOR1] = min(data[1], (u8)SOP716_MOTOR_MAX_POS); |
| motor_pos[MOTOR2] = min(data[2], (u8)SOP716_MOTOR_MAX_POS); |
| pr_debug("%s: motor1 pos %d motor2 pos %d\n", __func__, |
| motor_pos[MOTOR1], motor_pos[MOTOR2]); |
| } |
| |
| #if SOP716_USE_DISTANCE |
| static u8 sop716_get_distance_locked(struct sop716_info *si, |
| u8 motor, u8 pos, u8 opt) |
| { |
| u8 val[2]; |
| int distance; |
| |
| if (pos > SOP716_MOTOR_MAX_POS) { |
| pr_err("%s: invalid vlaue (%d)\n", __func__, pos); |
| return -EINVAL; |
| } |
| |
| if (opt & SOP716_OPT_STACKED) /* stacked request */ |
| return 0; |
| |
| /* get current motors position */ |
| sop716_read_motors_position_locked(si, val); |
| |
| /* calculate distance */ |
| distance = SOP716_MOTOR_MAX_POS + 1 - val[motor] + pos; |
| distance = distance % (SOP716_MOTOR_MAX_POS + 1); |
| |
| distance = clamp_val(distance, 0, SOP716_MOTOR_MAX_POS); |
| if (opt & SOP716_OPT_DIRECTION) { /* force direction */ |
| if (opt & SOP716_OPT_DIR_CCW) /* counter clockwise */ |
| distance = SOP716_MOTOR_MAX_POS - distance + 1; |
| } else { /* shortest way */ |
| if (distance > ((SOP716_MOTOR_MAX_POS + 1) >> 1)) |
| distance = SOP716_MOTOR_MAX_POS - distance + 1; |
| } |
| |
| return distance; |
| } |
| #else |
| static u8 sop716_get_distance_locked(struct sop716_info *si, |
| u8 motor, u8 pos, u8 opt) |
| { |
| return 0; |
| } |
| #endif |
| |
| static inline void sop716_wait_for_motor_move_locked(u8 distance) |
| { |
| pr_debug("%s: distance %d\n", __func__, distance); |
| msleep(SOP716_MOTOR_TIME_PER_STEP_MS * distance); |
| } |
| |
| static ssize_t sop716_reset_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int reset; |
| int rc; |
| |
| rc = kstrtoint(buf, 10, &reset); |
| if (rc) { |
| pr_err("%s: invalid value\n", __func__); |
| return rc; |
| } |
| |
| if (reset) { |
| mutex_lock(&si->lock); |
| sop716_hw_reset_locked(si, false); |
| mutex_unlock(&si->lock); |
| } |
| |
| pr_debug("%s: reset %d\n", __func__, reset); |
| return count; |
| } |
| |
| static int convert_to_integers(char **str, const char *delimit, |
| int result[], int items) |
| { |
| char *p; |
| int rc = 0; |
| int i = 0; |
| |
| while (i < items && (p = strsep(str, delimit)) != NULL) { |
| rc = kstrtoint(p, 10, &result[i++]); |
| if (rc) { |
| pr_err("%s: Invalid value\n", __func__); |
| return rc; |
| } |
| } |
| |
| if (i != items) { |
| pr_err("%s: data is too short: %s\n", __func__, *str); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * Code for CMD_SOP716_SET_CURRENT_TIME |
| * input format: YYYY-MM-DD hh:mm:ss [option] |
| */ |
| static ssize_t sop716_time_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| char tmp[32]; |
| char *date = NULL; |
| char *time = NULL; |
| char *opt = NULL; |
| char *p; |
| int user_data[7] = {0, }; |
| int rc = 0; |
| u8 pos[2]; |
| u8 dist[2]; |
| |
| if (!count) { |
| pr_err("%s: invalid input count!\n", __func__); |
| return -EINVAL; |
| } |
| |
| strlcpy(tmp, buf, sizeof(tmp)); |
| p = tmp; |
| |
| date = strsep(&p, " "); |
| if (!date) { |
| pr_err("%s: No date info!\n", __func__); |
| return -EINVAL; |
| } |
| time = strsep(&p, " "); |
| if (!time) { |
| pr_err("%s: No time info\n", __func__); |
| return -EINVAL; |
| } |
| opt = strsep(&p, " "); |
| |
| /* parse date */ |
| rc = convert_to_integers(&date, "-", user_data, 3); |
| if (rc < 0) { |
| pr_err("%s: Invalid date info: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| |
| /* parse time */ |
| rc = convert_to_integers(&time, ":", &user_data[3], 3); |
| if (rc < 0) { |
| pr_err("%s: Invalid time info: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| |
| if (opt) { |
| rc = kstrtoint(opt, 0, &user_data[6]); |
| if (rc < 0) { |
| pr_err("%s: Invalid opt: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| } |
| |
| user_data[0] -= 2000; |
| if (user_data[0] < 0 || user_data[0] > 99 || // year |
| user_data[1] < 1 || user_data[1] > 12 || // month |
| user_data[2] < 1 || user_data[2] > 31 || // day |
| user_data[3] < 0 || user_data[3] > 23 || // hour |
| user_data[4] < 0 || user_data[4] > 59 || // minute |
| user_data[5] < 0 || user_data[5] > 59 || // second |
| user_data[6] < 0 || user_data[6] > 255) { |
| pr_err("%s: out of range: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| |
| pr_info("sop set time: %04d-%02d-%02d %02d:%02d:%02d opt:%02x\n", |
| user_data[0] + 2000, |
| user_data[1], |
| user_data[2], |
| user_data[3], |
| user_data[4], |
| user_data[5], |
| user_data[6]); |
| |
| data[0] = CMD_SOP716_SET_CURRENT_TIME; |
| data[1] = user_data[3]; //hour |
| data[2] = user_data[4]; //minute |
| data[3] = user_data[5]; //second |
| data[4] = user_data[0]; //year |
| data[5] = user_data[1]; //month |
| data[6] = user_data[2]; //day |
| data[7] = user_data[6]; //opt |
| |
| mutex_lock(&si->lock); |
| /* covert h:m to motors position */ |
| pos[MOTOR1] = 3 * data[2]; |
| pos[MOTOR2] = (15 * (data[1] % 12)) + (data[2] >> 2); |
| dist[MOTOR1] = sop716_get_distance_locked(si, MOTOR1, pos[MOTOR1], |
| user_data[6]); |
| dist[MOTOR2] = sop716_get_distance_locked(si, MOTOR2, pos[MOTOR2], |
| user_data[6]); |
| |
| rc = sop716_write(si, CMD_SOP716_SET_CURRENT_TIME, data); |
| if (rc < 0) |
| pr_err("%s: cannot set time\n", __func__); |
| |
| /* wait for motor move */ |
| dist[0] = max(dist[MOTOR1], dist[MOTOR2]); |
| sop716_wait_for_motor_move_locked(dist[0]); |
| /* |
| * Do not set the watch mode if opt is not moving hands |
| */ |
| if (!(user_data[6] & SOP716_OPT_NO_HANDS_MOVE)) |
| si->watch_mode = true; |
| mutex_unlock(&si->lock); |
| |
| return rc < 0? rc : count; |
| } |
| |
| /* |
| * CMD_SOP716_MOTOR_MOVE_ONE |
| * input format: type:position[:option] |
| */ |
| static ssize_t sop716_motor_move_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| char tmp[16] = {0, }; |
| char *str, *p; |
| int result[2]; |
| int option = 0; |
| int rc = 0; |
| u8 dist; |
| |
| if (!count) { |
| pr_err("%s: invalid input count!\n", __func__); |
| return -EINVAL; |
| } |
| |
| strlcpy(tmp, buf, sizeof(tmp)); |
| str = tmp; |
| |
| rc = convert_to_integers(&str, ":", result, 2); |
| if (rc < 0) { |
| pr_err("%s: Invalid arguments\n", __func__); |
| return rc; |
| } |
| |
| p = strsep(&str, ":"); |
| if (p) { |
| rc = kstrtoint(p, 0, &option); |
| if (rc) { |
| pr_err("%s: Invalid value: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| } |
| |
| if (result[0] < 0 || result[0]> 1 || // motor type |
| result[1] < 0 || result[1] > SOP716_MOTOR_MAX_POS || // motor position |
| option < 0 || option > 255) { |
| pr_err("%s: out of range: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| |
| pr_debug("%s: cnt:%d motortype:%d destination:%d option:%d\n", |
| __func__, count, result[0], result[1], option); |
| |
| data[0] = CMD_SOP716_MOTOR_MOVE_ONE; |
| data[1] = result[0]; |
| data[2] = result[1]; |
| data[3] = option; |
| |
| mutex_lock(&si->lock); |
| dist = sop716_get_distance_locked(si, data[1], data[2], data[3]); |
| |
| sop716_write(si, CMD_SOP716_MOTOR_MOVE_ONE, data); |
| |
| /* wait for motor move */ |
| sop716_wait_for_motor_move_locked(dist); |
| |
| si->watch_mode = false; |
| mutex_unlock(&si->lock); |
| |
| return count; |
| } |
| |
| /* |
| * CMD_SOP716_MOTOR_MOVE_ALL |
| * input format: motor1:motor2[:option] |
| */ |
| static ssize_t sop716_motor_move_all_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| char tmp[16] = {0, }; |
| char *str, *p; |
| int result[2]; |
| int option = 0; |
| int rc = 0; |
| u8 dist[2]; |
| |
| if (!count) { |
| pr_err("%s: invalid input count!\n", __func__); |
| return -EINVAL; |
| } |
| |
| strlcpy(tmp, buf, sizeof(tmp)); |
| str = tmp; |
| |
| rc = convert_to_integers(&str, ":", result, 2); |
| if (rc < 0) { |
| pr_err("%s: invalid arguments\n", __func__); |
| return rc; |
| } |
| |
| p = strsep(&str, ":"); |
| if (p) { |
| rc = kstrtoint(p, 0, &option); |
| if (rc) { |
| pr_err("%s: Invalid value: %s\n", __func__, buf); |
| return -EINVAL; |
| } |
| } |
| |
| if (result[0] < 0 || result[0] > SOP716_MOTOR_MAX_POS || // motor1 |
| result[1] < 0 || result[1] > SOP716_MOTOR_MAX_POS || // motor2 |
| option < 0 || option > 255) { |
| pr_err("%s: invalid input format!\n", __func__); |
| return -EINVAL; |
| } |
| |
| pr_debug("%s: cnt:%d motor1:%d motor2:%d op:%d\n", |
| __func__, count, result[0], result[1], option); |
| |
| data[0] = CMD_SOP716_MOTOR_MOVE_ALL; |
| data[1] = result[0]; |
| data[2] = result[1]; |
| data[3] = option; |
| |
| mutex_lock(&si->lock); |
| dist[MOTOR1] = sop716_get_distance_locked(si, MOTOR1, data[1], data[3]); |
| dist[MOTOR2] = sop716_get_distance_locked(si, MOTOR2, data[2], data[3]); |
| |
| sop716_write(si, CMD_SOP716_MOTOR_MOVE_ALL, data); |
| |
| /* wait for motor move */ |
| dist[0] = max(dist[MOTOR1], dist[MOTOR2]); |
| sop716_wait_for_motor_move_locked(dist[0]); |
| |
| si->watch_mode = false; |
| mutex_unlock(&si->lock); |
| |
| return count; |
| } |
| |
| /* |
| * CMD_SOP716_MOTOR_INIT |
| * input format: type:dir:steps |
| */ |
| static ssize_t sop716_motor_init_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| char tmp[16] = {0, }; |
| char *str; |
| int result[3]; |
| int rc = 0; |
| u8 dist, opt; |
| |
| if (!count) { |
| pr_err("%s: invalid input count!\n", __func__); |
| return -EINVAL; |
| } |
| |
| strlcpy(tmp, buf, sizeof(tmp)); |
| str = tmp; |
| |
| rc = convert_to_integers(&str, ":", result, 3); |
| if (rc < 0) { |
| pr_err("%s: invalid arguments\n", __func__); |
| return rc; |
| } |
| |
| if (result[0] < 0 || result[0] > 1 || // motor type |
| result[1] < 0 || result[1] > 1 || // motor direction |
| result[2] < 0 || result[2] > 179) { // motor steps |
| pr_err("%s: invalid input format!\n", __func__); |
| return -EINVAL; |
| } |
| |
| pr_debug("%s: cnt:%d motor_type:%d direction:%d num_of_steps:%d\n", |
| __func__, count, result[0], result[1], result[2]); |
| |
| data[0] = CMD_SOP716_MOTOR_INIT; |
| data[1] = result[0]; |
| data[2] = result[1]; |
| data[3] = result[2]; |
| |
| mutex_lock(&si->lock); |
| opt = SOP716_OPT_DIRECTION; |
| if (data[2]) /* counter clockwise */ |
| opt |= SOP716_OPT_DIR_CCW; |
| |
| sop716_write(si, CMD_SOP716_MOTOR_INIT, data); |
| |
| /* wait for motor move */ |
| dist = data[3]; |
| sop716_wait_for_motor_move_locked(dist); |
| |
| si->watch_mode = false; |
| mutex_unlock(&si->lock); |
| |
| return count; |
| } |
| |
| /* Code for CMD_SOP716_READ_CURRENT_TIME */ |
| static ssize_t sop716_time_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int err; |
| |
| mutex_lock(&si->lock); |
| err = sop716_read(si, CMD_SOP716_READ_CURRENT_TIME, data); |
| mutex_unlock(&si->lock); |
| if (err < 0) { |
| pr_err("%s: cannot read time from sop716\n", __func__); |
| return err; |
| } |
| |
| pr_debug("sop get time: %04d-%02d-%02d %02d:%02d:%02d\n", |
| data[4] + 2000, |
| data[5], |
| data[6], |
| data[1], |
| data[2], |
| data[3]); |
| |
| return snprintf(buf, PAGE_SIZE, "%04d-%02d-%02d %02d:%02d:%02d\n", |
| data[4] + 2000, data[5], data[6], |
| data[1], data[2], data[3]); |
| } |
| |
| /* Code for CMD_SOP716_READ_FW_VERSION */ |
| static ssize_t sop716_version_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| struct sop716_info *si = dev_get_drvdata(dev); |
| |
| mutex_lock(&si->lock); |
| sop716_read(si, CMD_SOP716_READ_FW_VERSION, data); |
| mutex_unlock(&si->lock); |
| |
| return snprintf(buf, PAGE_SIZE, "%d.%d\n", data[1], data[2]); |
| } |
| |
| /* Code for CMD_SOP716_BATTERY_CHECK_INTERVAL */ |
| static ssize_t sop716_battery_check_interval_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| int interval; |
| int rc = 0; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_BATTERY_CHECK_INTERVAL, data); |
| mutex_unlock(&si->lock); |
| if (rc < 0) |
| return rc; |
| |
| interval = ((data[1] << 8) + data[2]); |
| |
| return snprintf(buf, PAGE_SIZE, "%d second(s)\n", interval); |
| } |
| |
| static ssize_t sop716_battery_check_interval_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int interval; |
| int rc = 0; |
| |
| if (!count) { |
| pr_err("%s: invalid input\n", __func__); |
| return -EINVAL; |
| } |
| |
| rc = kstrtoint(buf, 10, &interval); |
| if (rc) { |
| pr_err("%s: invalid value\n", __func__); |
| return rc; |
| } |
| |
| rc = sop716_set_battery_check_interval(si, interval); |
| if (rc < 0) { |
| pr_err("%s: failed to set battery check interval\n", __func__); |
| return rc; |
| } |
| |
| return count; |
| } |
| |
| /* Code for CMD_SOP716_READ_BATTERY_LEVEL */ |
| static ssize_t sop716_battery_level_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| struct sop716_info *si = dev_get_drvdata(dev); |
| |
| mutex_lock(&si->lock); |
| sop716_read(si, CMD_SOP716_READ_BATTERY_LEVEL, data); |
| mutex_unlock(&si->lock); |
| |
| return snprintf(buf, PAGE_SIZE, "%d mV\n", (data[1] << 8) + data[2]); |
| } |
| |
| static ssize_t sop716_tz_minutes_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| s8 data[SOP716_I2C_DATA_LENGTH]; |
| int tz_minutes; |
| int rc = 0; |
| |
| if (!count) { |
| pr_err("%s: invalid input count\n", __func__); |
| return -EINVAL; |
| } |
| |
| rc = kstrtoint(buf, 10, &tz_minutes); |
| if (rc) { |
| pr_err("%s: invalid input! rc:%d\n", |
| __func__, rc); |
| return -EINVAL; |
| } |
| |
| if (tz_minutes < -720 || tz_minutes > 840) { |
| pr_err("%s: out of range!\n", __func__); |
| return -EINVAL; |
| } |
| |
| data[0] = CMD_SOP716_SET_TIMEZONE; |
| data[2] = tz_minutes / 60; /* hour */ |
| data[1] = tz_minutes - (((int)data[2]) * 60); |
| |
| mutex_lock(&si->lock); |
| sop716_write(si, CMD_SOP716_SET_TIMEZONE, data); |
| mutex_unlock(&si->lock); |
| |
| return count; |
| } |
| |
| static ssize_t sop716_tz_minutes_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int tz_minutes; |
| |
| tz_minutes = sop716_read_tz_minutes(si); |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", tz_minutes); |
| } |
| |
| static ssize_t sop716_eol_limit_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int eol_limit; |
| int rc = 0; |
| |
| if (!count) { |
| pr_err("%s: invalid input count\n", __func__); |
| return -EINVAL; |
| } |
| |
| rc = kstrtoint(buf, 10, &eol_limit); |
| if (rc) { |
| pr_err("%s: invalid input! rc:%d\n", __func__, rc); |
| return -EINVAL; |
| } |
| |
| rc = sop716_set_eol_limit(si, eol_limit); |
| if (rc) { |
| pr_err("%s: failed to set eol limit\n", __func__); |
| return rc; |
| } |
| |
| return count; |
| } |
| |
| static ssize_t sop716_eol_limit_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int rc; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_EOL_BATTERY_LEVEL, data); |
| mutex_unlock(&si->lock); |
| if (rc < 0) |
| return rc; |
| |
| return snprintf(buf, PAGE_SIZE, "%d mV\n", (data[1] << 8) + data[2]); |
| } |
| |
| static ssize_t sop716_eol_threshold_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int eol_threshold; |
| int rc = 0; |
| |
| if (!count) { |
| pr_err("%s: invalid input count\n", __func__); |
| return -EINVAL; |
| } |
| |
| rc = kstrtoint(buf, 10, &eol_threshold); |
| if (rc) { |
| pr_err("%s: invalid input! rc:%d\n", __func__, rc); |
| return -EINVAL; |
| } |
| |
| rc = sop716_set_eol_threshold(si, eol_threshold); |
| if (rc) { |
| pr_err("%s: failed to set eol threshold\n", __func__); |
| return rc; |
| } |
| |
| return count; |
| } |
| |
| static ssize_t sop716_eol_threshold_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int rc; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, CMD_SOP716_READ_EOL_BATTERY_LEVEL, data); |
| mutex_unlock(&si->lock); |
| if (rc < 0) |
| return rc; |
| |
| return snprintf(buf, PAGE_SIZE, "%d mV\n", (data[3] << 8) + data[4]); |
| } |
| |
| |
| static ssize_t sop716_update_fw_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int err, value; |
| |
| err = kstrtoint(buf, 10, &value); |
| if (err) { |
| pr_err("%s: invalid value\n", __func__); |
| return err; |
| } |
| |
| if (value) |
| schedule_work(&si->fw_work); |
| |
| return count; |
| } |
| |
| static int sop716_hctosys(struct sop716_info *si) |
| { |
| struct rtc_time tm; |
| struct timespec tv = { |
| .tv_nsec = NSEC_PER_SEC >> 1, |
| }; |
| u8 time_data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| int err; |
| |
| mutex_lock(&si->lock); |
| msleep(si->hctosys_pre_delay_ms); |
| err = sop716_read(si, CMD_SOP716_READ_CURRENT_TIME, time_data); |
| msleep(si->hctosys_post_delay_ms); |
| if (err < 0) { |
| pr_err("%s: cannot read time from device\n", __func__); |
| goto out; |
| } |
| |
| tm.tm_hour = time_data[1]; |
| tm.tm_min = time_data[2]; |
| tm.tm_sec = time_data[3]; |
| tm.tm_year = time_data[4] + 100; |
| tm.tm_mon = time_data[5] - 1; |
| tm.tm_mday = time_data[6]; |
| |
| err = rtc_valid_tm(&tm); |
| if (err) { |
| pr_err("%s: invalid date/time\n", __func__); |
| goto out; |
| } |
| |
| rtc_tm_to_time(&tm, &tv.tv_sec); |
| |
| /* Convert to UTC */ |
| tv.tv_sec += si->tz_minuteswest * 60; |
| |
| /* It will override the system clock */ |
| err = do_settimeofday(&tv); |
| |
| rtc_time_to_tm(tv.tv_sec, &tm); |
| |
| dev_info(si->dev, "setting system clock to " |
| "%d-%02d-%02d %02d:%02d:%02d UTC (%u)\n", |
| tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, |
| tm.tm_hour, tm.tm_min, tm.tm_sec, |
| (unsigned int) tv.tv_sec); |
| |
| out: |
| mutex_unlock(&si->lock); |
| return err; |
| } |
| |
| static ssize_t sop716_update_sysclock_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int err, value; |
| |
| err = kstrtoint(buf, 10, &value); |
| if (err) { |
| pr_err("%s: invalid value\n", __func__); |
| return err; |
| } |
| |
| if (value) { |
| /* read the sys_tz and use it */ |
| si->tz_minuteswest = sys_tz.tz_minuteswest; |
| |
| schedule_work(&si->sysclock_work); |
| } |
| |
| return count; |
| } |
| |
| static ssize_t sop716_watch_mode_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| |
| return snprintf(buf, PAGE_SIZE, "%d\n", si->watch_mode); |
| } |
| |
| static ssize_t sop716_watch_mode_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int value = 0; |
| int err; |
| struct rtc_time tm; |
| struct timeval tv; |
| u8 pos[2]; |
| u8 dist[2]; |
| u8 data[SOP716_I2C_DATA_LENGTH] = { |
| CMD_SOP716_SET_CURRENT_TIME, |
| 0xFF, |
| 0xFF, |
| 0, |
| }; |
| |
| |
| if (si->watch_mode) |
| return count; |
| |
| err = kstrtoint(buf, 10, &value); |
| if (err || !value) { |
| pr_err("%s: invalid value\n", __func__); |
| return err; |
| } |
| |
| mutex_lock(&si->lock); |
| /* Get local time */ |
| do_gettimeofday(&tv); |
| tv.tv_sec -= si->tz_minuteswest * 60; |
| rtc_time_to_tm(tv.tv_sec, &tm); |
| |
| pr_debug("%s: current time: %d-%02d-%02d %02d:%02d:%02d\n", |
| __func__, |
| tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, |
| tm.tm_hour, tm.tm_min, tm.tm_sec); |
| |
| /* covert h:m to motors position */ |
| pos[MOTOR1] = 3 * tm.tm_min; |
| pos[MOTOR2] = (15 * (tm.tm_hour % 12)) + (tm.tm_min >> 2); |
| dist[MOTOR1] = sop716_get_distance_locked(si, MOTOR1, pos[MOTOR1], 0); |
| dist[MOTOR2] = sop716_get_distance_locked(si, MOTOR2, pos[MOTOR2], 0); |
| |
| err = sop716_write(si, CMD_SOP716_SET_CURRENT_TIME, data); |
| |
| /* wait for motor move */ |
| dist[0] = max(dist[MOTOR1], dist[MOTOR2]); |
| sop716_wait_for_motor_move_locked(dist[0]); |
| if (err < 0) { |
| pr_err("%s: cannot set watch mode\n", __func__); |
| goto out; |
| } |
| |
| si->watch_mode = true; |
| out: |
| mutex_unlock(&si->lock); |
| return (err < 0)? err : count; |
| } |
| |
| /* Code for CMD_SOP716_READ_MOTORS_POSITION */ |
| static ssize_t sop716_read_motors_position_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 pos[2] = {0, }; |
| |
| mutex_lock(&si->lock); |
| sop716_read_motors_position_locked(si, pos); |
| mutex_unlock(&si->lock); |
| |
| return snprintf(buf, PAGE_SIZE, "%d:%d\n", pos[MOTOR1], pos[MOTOR2]); |
| } |
| |
| /* |
| * CMD_SOP716_SET_AGING_TEST |
| * intput format: 0~255 (0: stop, 1~255: start the aging test |
| */ |
| static ssize_t sop716_aging_test_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| u8 data[SOP716_I2C_DATA_LENGTH]; |
| unsigned int v = -1; |
| int rc; |
| |
| if (!count) { |
| pr_err("%s: invalid input count!\n", __func__); |
| return -EINVAL; |
| } |
| |
| rc = kstrtouint(buf, 0, &v); |
| if (rc < 0 || v > 255) { |
| pr_err("%s: invalid input %u\n", __func__, v); |
| return -EINVAL; |
| } |
| |
| pr_debug("%s: No. of sequence:%u\n", __func__, v); |
| |
| data[0] = CMD_SOP716_SET_AGING_TEST; |
| data[1] = v? 1 : 0; |
| data[2] = v; |
| |
| mutex_lock(&si->lock); |
| sop716_write(si, CMD_SOP716_SET_AGING_TEST, data); |
| si->watch_mode = false; |
| mutex_unlock(&si->lock); |
| |
| return count; |
| } |
| |
| static ssize_t sop716_dump_errors_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| sop716_print_errors(si); |
| return count; |
| } |
| |
| static ssize_t sop716_self_test_store(struct device *dev, |
| struct device_attribute *attr, const char *buf, |
| size_t count) |
| { |
| struct sop716_info *si = dev_get_drvdata(dev); |
| int rc; |
| |
| rc = sop716_do_self_test(si); |
| return rc < 0? rc : count; |
| } |
| |
| static DEVICE_ATTR(reset, S_IWUSR, NULL, sop716_reset_store); |
| static DEVICE_ATTR(time, S_IWUSR | S_IRUGO, |
| sop716_time_show, sop716_time_store); |
| static DEVICE_ATTR(motor_init, S_IWUSR, NULL, sop716_motor_init_store); |
| static DEVICE_ATTR(motor_move, S_IWUSR, NULL, sop716_motor_move_store); |
| static DEVICE_ATTR(motor_move_all, S_IWUSR, NULL, sop716_motor_move_all_store); |
| static DEVICE_ATTR(version, S_IRUGO, sop716_version_show, NULL); |
| static DEVICE_ATTR(battery_check_interval, S_IRUGO | S_IWUSR, |
| sop716_battery_check_interval_show, |
| sop716_battery_check_interval_store); |
| static DEVICE_ATTR(battery_level, S_IRUGO, |
| sop716_battery_level_show, NULL); |
| static DEVICE_ATTR(tz_minutes, S_IWUSR | S_IRUGO, |
| sop716_tz_minutes_show, sop716_tz_minutes_store); |
| static DEVICE_ATTR(eol_limit, S_IWUSR | S_IRUGO, |
| sop716_eol_limit_show, sop716_eol_limit_store); |
| static DEVICE_ATTR(eol_threshold, S_IWUSR | S_IRUGO, |
| sop716_eol_threshold_show, sop716_eol_threshold_store); |
| static DEVICE_ATTR(update_fw, S_IWUSR, NULL, sop716_update_fw_store); |
| static DEVICE_ATTR(update_sysclock, S_IWUSR, NULL, |
| sop716_update_sysclock_store); |
| static DEVICE_ATTR(watch_mode, S_IWUSR | S_IRUGO, sop716_watch_mode_show, |
| sop716_watch_mode_store); |
| static DEVICE_ATTR(position, S_IRUGO, sop716_read_motors_position_show, NULL); |
| static DEVICE_ATTR(aging_test, S_IWUSR, NULL, sop716_aging_test_store); |
| static DEVICE_ATTR(dump_errors, S_IWUSR, NULL, sop716_dump_errors_store); |
| static DEVICE_ATTR(self_test, S_IWUSR, NULL, sop716_self_test_store); |
| |
| static struct attribute *sop716_dev_attrs[] = { |
| &dev_attr_reset.attr, |
| &dev_attr_time.attr, |
| &dev_attr_motor_init.attr, |
| &dev_attr_motor_move.attr, |
| &dev_attr_motor_move_all.attr, |
| &dev_attr_version.attr, |
| &dev_attr_battery_check_interval.attr, |
| &dev_attr_battery_level.attr, |
| &dev_attr_tz_minutes.attr, |
| &dev_attr_eol_limit.attr, |
| &dev_attr_eol_threshold.attr, |
| &dev_attr_update_fw.attr, |
| &dev_attr_update_sysclock.attr, |
| &dev_attr_watch_mode.attr, |
| &dev_attr_position.attr, |
| &dev_attr_aging_test.attr, |
| &dev_attr_dump_errors.attr, |
| &dev_attr_self_test.attr, |
| NULL |
| }; |
| |
| static struct attribute_group sop716_dev_attr_group = { |
| .attrs = sop716_dev_attrs, |
| }; |
| |
| static int do_self_test_read(struct sop716_info *si) |
| { |
| u8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| u8 read_cmds[] = { |
| CMD_SOP716_READ_CURRENT_TIME, |
| CMD_SOP716_READ_FW_VERSION, |
| CMD_SOP716_READ_MODE, |
| CMD_SOP716_READ_BATTERY_LEVEL, |
| CMD_SOP716_READ_BATTERY_CHECK_INTERVAL, |
| CMD_SOP716_READ_TIMEZONE, |
| CMD_SOP716_READ_EOL_BATTERY_LEVEL, |
| CMD_SOP716_READ_MOTORS_POSITION |
| }; |
| u32 rn = prandom_u32() % sizeof(read_cmds); |
| u8 cmd = read_cmds[rn]; |
| int rc; |
| |
| mutex_lock(&si->lock); |
| rc = sop716_read(si, cmd, data); |
| mutex_unlock(&si->lock); |
| |
| return rc; |
| } |
| |
| static int do_self_test_configs(struct sop716_info *si) |
| { |
| sop716_read_fw_version(si); |
| sop716_read_mode(si); |
| return sop716_restore_default_config(si); |
| } |
| |
| static int do_self_test_write(struct sop716_info *si) |
| { |
| int rc; |
| u8 pos1 = prandom_u32() % (SOP716_MOTOR_MAX_POS + 1); |
| u8 pos2 = prandom_u32() % (SOP716_MOTOR_MAX_POS + 1); |
| char buf[32]; |
| |
| snprintf(buf, sizeof(buf), "%d:%d:0", pos1, pos2); |
| rc = sop716_motor_move_all_store(si->dev, NULL, |
| buf, strlen(buf)); |
| rc |= sop716_motor_move_all_store(si->dev, NULL, |
| "0:0:0", strlen("0:0:0")); |
| rc |= sop716_watch_mode_store(si->dev, NULL, "1", strlen("1")); |
| |
| return rc; |
| } |
| |
| static void print_self_test_result(const char *str, int n, int rc) |
| { |
| pr_info("sop716: %2d: %16s\t%s\n", n, str, |
| rc < 0? "FAILED" : "PASSED"); |
| } |
| |
| static int sop716_do_self_test(struct sop716_info *si) |
| { |
| int i, rc; |
| int results[4] = {0, }; |
| const char *passed = "PASSED"; |
| const char *failed = "FAILED"; |
| |
| prandom_seed((u32)jiffies); |
| /* read test */ |
| pr_info("\nsop716: SELF TEST for READ\n" |
| "--------------------------------------\n"); |
| for (i = 0; i < 80; i++) { |
| rc = do_self_test_read(si); |
| print_self_test_result("read", i, rc); |
| if (rc < 0) |
| results[0] = -1; |
| |
| } |
| |
| /* initial config test */ |
| pr_info("\nsop716: SELF TEST for initial configs\n" |
| "--------------------------------------\n"); |
| for (i = 0; i < 10; i++) { |
| rc = do_self_test_configs(si); |
| print_self_test_result("config", i, rc); |
| if (rc < 0) |
| results[1] = -1; |
| } |
| |
| /* write and hands test */ |
| pr_info("\nsop716: SELF TEST for write and hands move\n" |
| "--------------------------------------\n"); |
| for (i = 0; i < 10; i++) { |
| rc = do_self_test_write(si); |
| print_self_test_result("write", i, rc); |
| if (rc < 0) |
| results[2] = -1; |
| } |
| |
| |
| /* mixed */ |
| pr_info("\nsop716: SELF TEST for mixed\n" |
| "--------------------------------------\n"); |
| for (i = 0; i < 20; i++) { |
| u8 which = prandom_u32() % 3; |
| switch (which) { |
| case 0: |
| rc = do_self_test_read(si); |
| print_self_test_result("read", i, rc); |
| break; |
| case 1: |
| rc = do_self_test_configs(si); |
| print_self_test_result("config", i, rc); |
| break; |
| case 2: |
| rc = do_self_test_write(si); |
| print_self_test_result("write", i, rc); |
| break; |
| default: |
| break; |
| } |
| if (rc < 0) |
| results[3] = -1; |
| } |
| |
| pr_info("SELF TEST: read %s, config %s write %s, mixed %s\n", |
| results[0] ? failed : passed, |
| results[1] ? failed : passed, |
| results[2] ? failed : passed, |
| results[3] ? failed : passed); |
| pr_info("--------------------------------------\n"); |
| return (results[0] | results[1] | results[2] | results[3]); |
| } |
| |
| static int sop716_parse_dt(struct i2c_client *client) |
| { |
| struct device *dev = &client->dev; |
| struct device_node *node = dev->of_node; |
| struct sop716_info *si = i2c_get_clientdata(client); |
| u32 val; |
| int ret; |
| |
| si->update_sysclock_on_boot = of_property_read_bool(node, |
| "lge,update-syslock-on-boot"); |
| |
| ret = of_property_read_u32(node, "lge,reset-delay-ms", &val); |
| si->reset_delay_ms = ret? SOP716_RESET_DELAY_MS : val; |
| |
| ret = of_property_read_u32(node, "lge,hctosys-pre-delay-ms", &val); |
| si->hctosys_pre_delay_ms = ret? 0 : val; |
| |
| ret = of_property_read_u32(node, "lge,hctosys-post-delay-ms", &val); |
| si->hctosys_post_delay_ms = ret? 0 : val; |
| |
| ret = of_property_read_u32(node, "lge,fw-ver-check-delay-ms", &val); |
| si->fw_ver_check_delay_ms = ret? 0 : val; |
| |
| ret = of_property_read_u32(node, "lge,hands-settle-time-ms", &val); |
| si->hands_settle_time_ms = ret? SOP716_SETTLE_TIME_MS : val; |
| |
| pr_info("sop716: hctosys pre-delay: %dms, hctosys post-delay: %dms\n", |
| si->hctosys_pre_delay_ms, |
| si->hctosys_post_delay_ms); |
| pr_info("sop716: reset-delay: %dms, fw ver check delay: %dms, " |
| "hands settle time: %dms\n", |
| si->reset_delay_ms, |
| si->fw_ver_check_delay_ms, |
| si->hands_settle_time_ms); |
| |
| ret = of_property_read_u32(node, "lge,battery-check-interval", &val); |
| si->battery_check_interval = ret? SOP716_BATTERY_CHECK_INTERVAL : val; |
| pr_info("sop716: battery check interval %d second(s)\n", |
| si->battery_check_interval); |
| |
| ret = of_property_read_u32(node, "lge,eol-limit-mv", &val); |
| si->eol_limit_mv = ret? SOP716_EOL_LIMIT_MV : val; |
| |
| ret = of_property_read_u32(node, "lge,eol-threshold-mv", &val); |
| si->eol_threshold_mv = ret? SOP716_EOL_THRESHOLD_MV : val; |
| pr_info("sop716: EOL limit %dmv threshold %dmv\n", |
| si->eol_limit_mv, si->eol_threshold_mv); |
| return 0; |
| } |
| |
| static int sop716_read_and_request_gpio(struct device *dev, |
| const char *dt_name, int *gpio, |
| unsigned long gpio_flags, const char *label) |
| { |
| struct device_node *node = dev->of_node; |
| int err; |
| |
| *gpio = of_get_named_gpio(node, dt_name, 0); |
| if (!gpio_is_valid(*gpio)) { |
| pr_err("%s: %s not in device tree\n", __func__, dt_name); |
| return -EINVAL; |
| } |
| |
| err = devm_gpio_request_one(dev, *gpio, gpio_flags, label); |
| if (err < 0) { |
| pr_err("%s: cannot request gpio %d (%s)\n", __func__, |
| *gpio, label); |
| return err; |
| } |
| return 0; |
| } |
| |
| static int sop716_config_gpios(struct i2c_client *client) |
| { |
| struct sop716_info *si = i2c_get_clientdata(client); |
| struct device *dev = &client->dev; |
| int err; |
| |
| /* Interrupt pin : input */ |
| err = sop716_read_and_request_gpio(dev, "lge,gpio-interrupt", |
| &si->gpio_interrupt, GPIOF_IN, "interrupt"); |
| if (err) |
| return err; |
| |
| /* bslen */ |
| err = sop716_read_and_request_gpio(dev, "lge,gpio-bslen", |
| &si->gpio_bslen, GPIOF_OUT_INIT_LOW, "bslen"); |
| if (err) |
| return err; |
| |
| /* gpio temp 1 */ |
| err = sop716_read_and_request_gpio(dev, "lge,gpio-temp-1", |
| &si->gpio_temp_1, GPIOF_OUT_INIT_LOW, |
| "temp_1"); |
| if (err) |
| return err; |
| |
| /* reset signal high --> low */ |
| err = sop716_read_and_request_gpio(dev, "lge,gpio-reset", |
| &si->gpio_reset, GPIOF_OUT_INIT_LOW, "reset"); |
| if (err) |
| return err; |
| |
| return 0; |
| } |
| |
| static void sop716_firmware_update(struct sop716_info *si, const u8 *fw) |
| { |
| int err; |
| |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_reset, 1); |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_bslen, 1); |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_bslen, 0); |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_bslen, 1); |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_reset, 0); /* bootloader starts */ |
| /* |
| * BSLEN must remain logically high long enough for the |
| * boot code to detect its level and enter the BSL sequence |
| */ |
| msleep(100); |
| gpio_set_value(si->gpio_bslen, 0); |
| usleep_range(1, 1000); |
| |
| err = sop716fw_update_firmware(si->client, fw); |
| if (err) { |
| pr_warn("%s: failed to update firmware\n", __func__); |
| gpio_set_value(si->gpio_bslen, 0); |
| gpio_set_value(si->gpio_reset, 1); |
| usleep_range(1, 1000); |
| gpio_set_value(si->gpio_reset, 0); /* user program starts */ |
| msleep(100); |
| } |
| } |
| |
| static void sop716_update_fw_work(struct work_struct *work) |
| { |
| struct sop716_info *si = container_of(work, |
| struct sop716_info, fw_work); |
| u8 data[SOP716_I2C_DATA_LENGTH] = {0, }; |
| u8 img_ver, img_rev; |
| int err; |
| int retry = SOP716_CMD_READ_RETRY; |
| const struct firmware *fw_entry = NULL; |
| |
| err = request_firmware(&fw_entry, "sop716.fw", si->dev); |
| if (err) { |
| pr_err("%s: cannot read firmware\n", __func__); |
| return; |
| }; |
| |
| sop716fw_validate_firmware(fw_entry->data, &img_ver, &img_rev); |
| if (!img_ver && !img_rev) { |
| pr_err("%s: wrong firmware file\n", __func__); |
| } |
| |
| pr_info("sop firmware version: device v%d.%d, image v%d.%d\n", |
| si->fw_ver, si->fw_rev, img_ver, img_rev); |
| if (si->fw_ver == img_ver && si->fw_rev == img_rev) { |
| release_firmware(fw_entry); |
| return; |
| } |
| |
| mutex_lock(&si->lock); |
| |
| /* Set the position 0 */ |
| memset(data, 0, sizeof(data)); |
| data[0] = CMD_SOP716_MOTOR_MOVE_ALL; |
| sop716_write(si, CMD_SOP716_MOTOR_MOVE_ALL, data); |
| |
| /* wait for hands move */ |
| msleep(si->hands_settle_time_ms); |
| |
| /* save current time */ |
| err = sop716_read(si, CMD_SOP716_READ_CURRENT_TIME, data); |
| if (err < 0) |
| pr_err("%s: cannot read current time\n", __func__); |
| |
| pr_info("sop get time: %04d-%02d-%02d %02d:%02d:%02d\n", |
| data[4] + 2000, |
| data[5], |
| data[6], |
| data[1], |
| data[2], |
| data[3]); |
| |
| sop716_firmware_update(si, fw_entry->data); |
| |
| /* boot up time */ |
| msleep(SOP716_RESET_DELAY_MS); |
| |
| /* restore time */ |
| data[0] = CMD_SOP716_SET_CURRENT_TIME; |
| if (!si->watch_mode) |
| data[7] = SOP716_OPT_NO_HANDS_MOVE; |
| do { |
| err = sop716_write(si, CMD_SOP716_SET_CURRENT_TIME, data); |
| msleep(100); |
| } while(retry-- && err); |
| |
| if (err < 0) |
| pr_err("%s: cannot set time\n", __func__); |
| |
| mutex_unlock(&si->lock); |
| |
| /* save old fw version info */ |
| img_ver = si->fw_ver; |
| img_rev = si->fw_rev; |
| |
| /* read new fw version */ |
| sop716_read_fw_version(si); |
| msleep(si->fw_ver_check_delay_ms); |
| |
| pr_info("sop firmware update: v%d.%d -> v%d.%d\n", |
| img_ver, img_rev, si->fw_ver, si->fw_rev); |
| |
| release_firmware(fw_entry); |
| |
| sop716_restore_default_config(si); |
| } |
| |
| static void sop716_update_sysclock_work(struct work_struct *work) |
| { |
| struct sop716_info *si = container_of(work, |
| struct sop716_info, sysclock_work); |
| |
| if (sop716_hctosys(si)) |
| sop716_hctosys(si); /* again one more */ |
| } |
| |
| static int sop716_restore_default_config(struct sop716_info *si) |
| { |
| int rc; |
| |
| rc = sop716_set_battery_check_interval(si, si->battery_check_interval); |
| if (rc) { |
| pr_err("%s: failed to restore the battery check interval\n", |
| __func__); |
| return rc; |
| } |
| |
| rc = sop716_set_eol_limit(si, si->eol_limit_mv); |
| if (rc) { |
| pr_err("%s: failed to restore eol limit\n", __func__); |
| return rc; |
| } |
| |
| rc = sop716_set_eol_threshold(si, si->eol_threshold_mv); |
| if (rc) { |
| pr_err("%s: failed to restore eol threshold\n", __func__); |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static void sop716_init_work(struct work_struct *work) |
| { |
| struct sop716_info *si = container_of(work, |
| struct sop716_info, init_work); |
| |
| sop716_read_fw_version(si); |
| if (sop716_read_mode(si) == MODE_WATCH) |
| si->watch_mode = true; |
| |
| sop716_restore_default_config(si); |
| } |
| |
| static int sop716_movement_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| struct sop716_info *si; |
| int err; |
| struct kobject *kobj = NULL; |
| |
| pr_debug("%s\n", __func__); |
| |
| si = devm_kzalloc(&client->dev, sizeof(struct sop716_info), |
| GFP_KERNEL); |
| if (!si) { |
| pr_err("%s: no mem\n", __func__); |
| return -ENOMEM; |
| } |
| |
| if (!i2c_check_functionality(client->adapter, |
| I2C_FUNC_SMBUS_BYTE_DATA)) { |
| pr_err("%s: error i2c_check_functionality\n", __func__); |
| return -ENODEV; |
| } |
| |
| si->client = client; |
| si->dev = &client->dev; |
| |
| i2c_set_clientdata(client, si); |
| |
| err = sop716_parse_dt(client); |
| if (err) |
| return err; |
| |
| err = sop716_config_gpios(client); |
| if (err) |
| return err; |
| |
| err = sop716_register_commands(si); |
| if (err) |
| return err; |
| |
| INIT_WORK(&si->fw_work, sop716_update_fw_work); |
| INIT_WORK(&si->sysclock_work, sop716_update_sysclock_work); |
| INIT_WORK(&si->init_work, sop716_init_work); |
| |
| mutex_init(&si->lock); |
| |
| if (client->dev.kobj.kset) |
| kobj = &client->dev.kobj.kset->kobj; |
| |
| err = sysfs_create_link(kobj, &client->dev.kobj, "sop716"); |
| if (err < 0) { |
| pr_err("%s: cannot create link\n", __func__); |
| goto err_sysfs_create_link; |
| } |
| |
| err = sysfs_create_group(&client->dev.kobj, &sop716_dev_attr_group); |
| if (err) { |
| pr_err("%s: cannot create sysfs\n", __func__); |
| goto err_sysfs_create; |
| } |
| |
| sop716_info = si; |
| |
| /* hw init */ |
| schedule_work(&si->init_work); |
| |
| pr_info("sop716 movement probed\n"); |
| return 0; |
| |
| err_sysfs_create: |
| sysfs_remove_link(&client->dev.kobj, "sop716"); |
| |
| err_sysfs_create_link: |
| mutex_destroy(&si->lock); |
| i2c_set_clientdata(client, NULL); |
| |
| return err; |
| } |
| |
| static int sop716_movement_remove(struct i2c_client *client) |
| { |
| struct sop716_info *si = i2c_get_clientdata(client); |
| |
| mutex_destroy(&si->lock); |
| sysfs_remove_group(&client->dev.kobj, &sop716_dev_attr_group); |
| sysfs_remove_link(&client->dev.kobj, "sop716"); |
| i2c_set_clientdata(client, NULL); |
| sop716_info = NULL; |
| |
| return 0; |
| } |
| |
| static void sop716_movement_shutdown(struct i2c_client *client) |
| { |
| return; |
| } |
| |
| static struct of_device_id sop716_match_table[] = { |
| { .compatible = "lge,soprod_movement", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, sop716_match_table); |
| |
| static const struct i2c_device_id sop716_id[] = { |
| {"sop716", -1}, |
| { }, |
| }; |
| |
| static struct i2c_driver sop716_movement_driver = { |
| .driver = { |
| .name = "soprod716_movement", |
| .owner = THIS_MODULE, |
| .of_match_table = sop716_match_table, |
| }, |
| .probe = sop716_movement_probe, |
| .remove = sop716_movement_remove, |
| .shutdown = sop716_movement_shutdown, |
| .id_table = sop716_id, |
| }; |
| |
| /** |
| * sop716_movement_init() - initialized sop716 movement driver |
| * This function registers the sop716 movement platform driver. |
| * |
| * Returns 0 on success or errno on failure. |
| */ |
| static int __init sop716_movement_init(void) |
| { |
| return i2c_add_driver(&sop716_movement_driver); |
| } |
| |
| static void __exit sop716_movement_exit(void) |
| { |
| i2c_del_driver(&sop716_movement_driver); |
| } |
| |
| module_init(sop716_movement_init); |
| module_exit(sop716_movement_exit); |
| |
| static int __init sop716_hctosys_init(void) |
| { |
| struct sop716_info *si = sop716_info; |
| int err; |
| |
| if (!si) { |
| pr_err("%s: unable to open sop716 device\n", __func__); |
| return -ENODEV; |
| } |
| |
| if (!si->update_sysclock_on_boot) |
| return 0; |
| |
| /* Update tz_minuteswest before sop716_hctosys() */ |
| si->tz_minuteswest = sop716_read_tz_minutes(si) * -1; |
| sys_tz.tz_minuteswest = si->tz_minuteswest; |
| |
| err = sop716_hctosys(si); |
| |
| return err; |
| } |
| |
| late_initcall_sync(sop716_hctosys_init); |
| |
| MODULE_DESCRIPTION("Soprod 716 movement"); |
| MODULE_AUTHOR("LGE"); |
| MODULE_LICENSE("GPL v2"); |