forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1081 lines
26 KiB
1081 lines
26 KiB
// SPDX-License-Identifier: GPL-2.0+ |
|
/* |
|
* Azoteq IQS620A/621/622/624/625 Multi-Function Sensors |
|
* |
|
* Copyright (C) 2019 Jeff LaBundy <[email protected]> |
|
* |
|
* These devices rely on application-specific register settings and calibration |
|
* data developed in and exported from a suite of GUIs offered by the vendor. A |
|
* separate tool converts the GUIs' ASCII-based output into a standard firmware |
|
* file parsed by the driver. |
|
* |
|
* Link to datasheets and GUIs: https://www.azoteq.com/ |
|
* |
|
* Link to conversion tool: https://github.com/jlabundy/iqs62x-h2bin.git |
|
*/ |
|
|
|
#include <linux/completion.h> |
|
#include <linux/delay.h> |
|
#include <linux/device.h> |
|
#include <linux/err.h> |
|
#include <linux/firmware.h> |
|
#include <linux/i2c.h> |
|
#include <linux/interrupt.h> |
|
#include <linux/kernel.h> |
|
#include <linux/list.h> |
|
#include <linux/mfd/core.h> |
|
#include <linux/mfd/iqs62x.h> |
|
#include <linux/module.h> |
|
#include <linux/notifier.h> |
|
#include <linux/of_device.h> |
|
#include <linux/property.h> |
|
#include <linux/regmap.h> |
|
#include <linux/slab.h> |
|
#include <asm/unaligned.h> |
|
|
|
#define IQS62X_PROD_NUM 0x00 |
|
|
|
#define IQS62X_SYS_FLAGS 0x10 |
|
|
|
#define IQS620_HALL_FLAGS 0x16 |
|
#define IQS621_HALL_FLAGS 0x19 |
|
#define IQS622_HALL_FLAGS IQS621_HALL_FLAGS |
|
|
|
#define IQS624_INTERVAL_NUM 0x18 |
|
#define IQS625_INTERVAL_NUM 0x12 |
|
|
|
#define IQS622_PROX_SETTINGS_4 0x48 |
|
#define IQS620_PROX_SETTINGS_4 0x50 |
|
#define IQS620_PROX_SETTINGS_4_SAR_EN BIT(7) |
|
|
|
#define IQS621_ALS_CAL_DIV_LUX 0x82 |
|
#define IQS621_ALS_CAL_DIV_IR 0x83 |
|
|
|
#define IQS620_TEMP_CAL_MULT 0xC2 |
|
#define IQS620_TEMP_CAL_DIV 0xC3 |
|
#define IQS620_TEMP_CAL_OFFS 0xC4 |
|
|
|
#define IQS62X_SYS_SETTINGS 0xD0 |
|
#define IQS62X_SYS_SETTINGS_ACK_RESET BIT(6) |
|
#define IQS62X_SYS_SETTINGS_EVENT_MODE BIT(5) |
|
#define IQS62X_SYS_SETTINGS_CLK_DIV BIT(4) |
|
#define IQS62X_SYS_SETTINGS_COMM_ATI BIT(3) |
|
#define IQS62X_SYS_SETTINGS_REDO_ATI BIT(1) |
|
|
|
#define IQS62X_PWR_SETTINGS 0xD2 |
|
#define IQS62X_PWR_SETTINGS_DIS_AUTO BIT(5) |
|
#define IQS62X_PWR_SETTINGS_PWR_MODE_MASK (BIT(4) | BIT(3)) |
|
#define IQS62X_PWR_SETTINGS_PWR_MODE_HALT (BIT(4) | BIT(3)) |
|
#define IQS62X_PWR_SETTINGS_PWR_MODE_NORM 0 |
|
|
|
#define IQS62X_OTP_CMD 0xF0 |
|
#define IQS62X_OTP_CMD_FG3 0x13 |
|
#define IQS62X_OTP_DATA 0xF1 |
|
#define IQS62X_MAX_REG 0xFF |
|
|
|
#define IQS62X_HALL_CAL_MASK GENMASK(3, 0) |
|
|
|
#define IQS62X_FW_REC_TYPE_INFO 0 |
|
#define IQS62X_FW_REC_TYPE_PROD 1 |
|
#define IQS62X_FW_REC_TYPE_HALL 2 |
|
#define IQS62X_FW_REC_TYPE_MASK 3 |
|
#define IQS62X_FW_REC_TYPE_DATA 4 |
|
|
|
#define IQS62X_ATI_STARTUP_MS 350 |
|
#define IQS62X_FILT_SETTLE_MS 250 |
|
|
|
struct iqs62x_fw_rec { |
|
u8 type; |
|
u8 addr; |
|
u8 len; |
|
u8 data; |
|
} __packed; |
|
|
|
struct iqs62x_fw_blk { |
|
struct list_head list; |
|
u8 addr; |
|
u8 mask; |
|
u8 len; |
|
u8 data[]; |
|
}; |
|
|
|
struct iqs62x_info { |
|
u8 prod_num; |
|
u8 sw_num; |
|
u8 hw_num; |
|
} __packed; |
|
|
|
static int iqs62x_dev_init(struct iqs62x_core *iqs62x) |
|
{ |
|
struct iqs62x_fw_blk *fw_blk; |
|
unsigned int val; |
|
int ret; |
|
|
|
list_for_each_entry(fw_blk, &iqs62x->fw_blk_head, list) { |
|
/* |
|
* In case ATI is in progress, wait for it to complete before |
|
* lowering the core clock frequency. |
|
*/ |
|
if (fw_blk->addr == IQS62X_SYS_SETTINGS && |
|
*fw_blk->data & IQS62X_SYS_SETTINGS_CLK_DIV) |
|
msleep(IQS62X_ATI_STARTUP_MS); |
|
|
|
if (fw_blk->mask) |
|
ret = regmap_update_bits(iqs62x->regmap, fw_blk->addr, |
|
fw_blk->mask, *fw_blk->data); |
|
else |
|
ret = regmap_raw_write(iqs62x->regmap, fw_blk->addr, |
|
fw_blk->data, fw_blk->len); |
|
if (ret) |
|
return ret; |
|
} |
|
|
|
switch (iqs62x->dev_desc->prod_num) { |
|
case IQS620_PROD_NUM: |
|
case IQS622_PROD_NUM: |
|
ret = regmap_read(iqs62x->regmap, |
|
iqs62x->dev_desc->prox_settings, &val); |
|
if (ret) |
|
return ret; |
|
|
|
if (val & IQS620_PROX_SETTINGS_4_SAR_EN) |
|
iqs62x->ui_sel = IQS62X_UI_SAR1; |
|
fallthrough; |
|
|
|
case IQS621_PROD_NUM: |
|
ret = regmap_write(iqs62x->regmap, IQS620_GLBL_EVENT_MASK, |
|
IQS620_GLBL_EVENT_MASK_PMU | |
|
iqs62x->dev_desc->prox_mask | |
|
iqs62x->dev_desc->sar_mask | |
|
iqs62x->dev_desc->hall_mask | |
|
iqs62x->dev_desc->hyst_mask | |
|
iqs62x->dev_desc->temp_mask | |
|
iqs62x->dev_desc->als_mask | |
|
iqs62x->dev_desc->ir_mask); |
|
if (ret) |
|
return ret; |
|
break; |
|
|
|
default: |
|
ret = regmap_write(iqs62x->regmap, IQS624_HALL_UI, |
|
IQS624_HALL_UI_WHL_EVENT | |
|
IQS624_HALL_UI_INT_EVENT | |
|
IQS624_HALL_UI_AUTO_CAL); |
|
if (ret) |
|
return ret; |
|
|
|
/* |
|
* The IQS625 default interval divider is below the minimum |
|
* permissible value, and the datasheet mandates that it is |
|
* corrected during initialization (unless an updated value |
|
* has already been provided by firmware). |
|
* |
|
* To protect against an unacceptably low user-entered value |
|
* stored in the firmware, the same check is extended to the |
|
* IQS624 as well. |
|
*/ |
|
ret = regmap_read(iqs62x->regmap, IQS624_INTERVAL_DIV, &val); |
|
if (ret) |
|
return ret; |
|
|
|
if (val >= iqs62x->dev_desc->interval_div) |
|
break; |
|
|
|
ret = regmap_write(iqs62x->regmap, IQS624_INTERVAL_DIV, |
|
iqs62x->dev_desc->interval_div); |
|
if (ret) |
|
return ret; |
|
} |
|
|
|
/* |
|
* Place the device in streaming mode at first so as not to miss the |
|
* limited number of interrupts that would otherwise occur after ATI |
|
* completes. The device is subsequently placed in event mode by the |
|
* interrupt handler. |
|
* |
|
* In the meantime, mask interrupts during ATI to prevent the device |
|
* from soliciting I2C traffic until the noise-sensitive ATI process |
|
* is complete. |
|
*/ |
|
ret = regmap_update_bits(iqs62x->regmap, IQS62X_SYS_SETTINGS, |
|
IQS62X_SYS_SETTINGS_ACK_RESET | |
|
IQS62X_SYS_SETTINGS_EVENT_MODE | |
|
IQS62X_SYS_SETTINGS_COMM_ATI | |
|
IQS62X_SYS_SETTINGS_REDO_ATI, |
|
IQS62X_SYS_SETTINGS_ACK_RESET | |
|
IQS62X_SYS_SETTINGS_REDO_ATI); |
|
if (ret) |
|
return ret; |
|
|
|
/* |
|
* The following delay gives the device time to deassert its RDY output |
|
* in case a communication window was open while the REDO_ATI field was |
|
* written. This prevents an interrupt from being serviced prematurely. |
|
*/ |
|
usleep_range(5000, 5100); |
|
|
|
return 0; |
|
} |
|
|
|
static int iqs62x_firmware_parse(struct iqs62x_core *iqs62x, |
|
const struct firmware *fw) |
|
{ |
|
struct i2c_client *client = iqs62x->client; |
|
struct iqs62x_fw_rec *fw_rec; |
|
struct iqs62x_fw_blk *fw_blk; |
|
unsigned int val; |
|
size_t pos = 0; |
|
int ret = 0; |
|
u8 mask, len, *data; |
|
u8 hall_cal_index = 0; |
|
|
|
while (pos < fw->size) { |
|
if (pos + sizeof(*fw_rec) > fw->size) { |
|
ret = -EINVAL; |
|
break; |
|
} |
|
fw_rec = (struct iqs62x_fw_rec *)(fw->data + pos); |
|
pos += sizeof(*fw_rec); |
|
|
|
if (pos + fw_rec->len - 1 > fw->size) { |
|
ret = -EINVAL; |
|
break; |
|
} |
|
pos += fw_rec->len - 1; |
|
|
|
switch (fw_rec->type) { |
|
case IQS62X_FW_REC_TYPE_INFO: |
|
continue; |
|
|
|
case IQS62X_FW_REC_TYPE_PROD: |
|
if (fw_rec->data == iqs62x->dev_desc->prod_num) |
|
continue; |
|
|
|
dev_err(&client->dev, |
|
"Incompatible product number: 0x%02X\n", |
|
fw_rec->data); |
|
ret = -EINVAL; |
|
break; |
|
|
|
case IQS62X_FW_REC_TYPE_HALL: |
|
if (!hall_cal_index) { |
|
ret = regmap_write(iqs62x->regmap, |
|
IQS62X_OTP_CMD, |
|
IQS62X_OTP_CMD_FG3); |
|
if (ret) |
|
break; |
|
|
|
ret = regmap_read(iqs62x->regmap, |
|
IQS62X_OTP_DATA, &val); |
|
if (ret) |
|
break; |
|
|
|
hall_cal_index = val & IQS62X_HALL_CAL_MASK; |
|
if (!hall_cal_index) { |
|
dev_err(&client->dev, |
|
"Uncalibrated device\n"); |
|
ret = -ENODATA; |
|
break; |
|
} |
|
} |
|
|
|
if (hall_cal_index > fw_rec->len) { |
|
ret = -EINVAL; |
|
break; |
|
} |
|
|
|
mask = 0; |
|
data = &fw_rec->data + hall_cal_index - 1; |
|
len = sizeof(*data); |
|
break; |
|
|
|
case IQS62X_FW_REC_TYPE_MASK: |
|
if (fw_rec->len < (sizeof(mask) + sizeof(*data))) { |
|
ret = -EINVAL; |
|
break; |
|
} |
|
|
|
mask = fw_rec->data; |
|
data = &fw_rec->data + sizeof(mask); |
|
len = sizeof(*data); |
|
break; |
|
|
|
case IQS62X_FW_REC_TYPE_DATA: |
|
mask = 0; |
|
data = &fw_rec->data; |
|
len = fw_rec->len; |
|
break; |
|
|
|
default: |
|
dev_err(&client->dev, |
|
"Unrecognized record type: 0x%02X\n", |
|
fw_rec->type); |
|
ret = -EINVAL; |
|
} |
|
|
|
if (ret) |
|
break; |
|
|
|
fw_blk = devm_kzalloc(&client->dev, |
|
struct_size(fw_blk, data, len), |
|
GFP_KERNEL); |
|
if (!fw_blk) { |
|
ret = -ENOMEM; |
|
break; |
|
} |
|
|
|
fw_blk->addr = fw_rec->addr; |
|
fw_blk->mask = mask; |
|
fw_blk->len = len; |
|
memcpy(fw_blk->data, data, len); |
|
|
|
list_add(&fw_blk->list, &iqs62x->fw_blk_head); |
|
} |
|
|
|
release_firmware(fw); |
|
|
|
return ret; |
|
} |
|
|
|
const struct iqs62x_event_desc iqs62x_events[IQS62X_NUM_EVENTS] = { |
|
[IQS62X_EVENT_PROX_CH0_T] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(4), |
|
.val = BIT(4), |
|
}, |
|
[IQS62X_EVENT_PROX_CH0_P] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(0), |
|
.val = BIT(0), |
|
}, |
|
[IQS62X_EVENT_PROX_CH1_T] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(5), |
|
.val = BIT(5), |
|
}, |
|
[IQS62X_EVENT_PROX_CH1_P] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(1), |
|
.val = BIT(1), |
|
}, |
|
[IQS62X_EVENT_PROX_CH2_T] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(6), |
|
.val = BIT(6), |
|
}, |
|
[IQS62X_EVENT_PROX_CH2_P] = { |
|
.reg = IQS62X_EVENT_PROX, |
|
.mask = BIT(2), |
|
.val = BIT(2), |
|
}, |
|
[IQS62X_EVENT_HYST_POS_T] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(6) | BIT(7), |
|
.val = BIT(6), |
|
}, |
|
[IQS62X_EVENT_HYST_POS_P] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(5) | BIT(7), |
|
.val = BIT(5), |
|
}, |
|
[IQS62X_EVENT_HYST_NEG_T] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(6) | BIT(7), |
|
.val = BIT(6) | BIT(7), |
|
}, |
|
[IQS62X_EVENT_HYST_NEG_P] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(5) | BIT(7), |
|
.val = BIT(5) | BIT(7), |
|
}, |
|
[IQS62X_EVENT_SAR1_ACT] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(4), |
|
.val = BIT(4), |
|
}, |
|
[IQS62X_EVENT_SAR1_QRD] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(2), |
|
.val = BIT(2), |
|
}, |
|
[IQS62X_EVENT_SAR1_MOVE] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(1), |
|
.val = BIT(1), |
|
}, |
|
[IQS62X_EVENT_SAR1_HALT] = { |
|
.reg = IQS62X_EVENT_HYST, |
|
.mask = BIT(0), |
|
.val = BIT(0), |
|
}, |
|
[IQS62X_EVENT_WHEEL_UP] = { |
|
.reg = IQS62X_EVENT_WHEEL, |
|
.mask = BIT(7) | BIT(6), |
|
.val = BIT(7), |
|
}, |
|
[IQS62X_EVENT_WHEEL_DN] = { |
|
.reg = IQS62X_EVENT_WHEEL, |
|
.mask = BIT(7) | BIT(6), |
|
.val = BIT(7) | BIT(6), |
|
}, |
|
[IQS62X_EVENT_HALL_N_T] = { |
|
.reg = IQS62X_EVENT_HALL, |
|
.mask = BIT(2) | BIT(0), |
|
.val = BIT(2), |
|
}, |
|
[IQS62X_EVENT_HALL_N_P] = { |
|
.reg = IQS62X_EVENT_HALL, |
|
.mask = BIT(1) | BIT(0), |
|
.val = BIT(1), |
|
}, |
|
[IQS62X_EVENT_HALL_S_T] = { |
|
.reg = IQS62X_EVENT_HALL, |
|
.mask = BIT(2) | BIT(0), |
|
.val = BIT(2) | BIT(0), |
|
}, |
|
[IQS62X_EVENT_HALL_S_P] = { |
|
.reg = IQS62X_EVENT_HALL, |
|
.mask = BIT(1) | BIT(0), |
|
.val = BIT(1) | BIT(0), |
|
}, |
|
[IQS62X_EVENT_SYS_RESET] = { |
|
.reg = IQS62X_EVENT_SYS, |
|
.mask = BIT(7), |
|
.val = BIT(7), |
|
}, |
|
[IQS62X_EVENT_SYS_ATI] = { |
|
.reg = IQS62X_EVENT_SYS, |
|
.mask = BIT(2), |
|
.val = BIT(2), |
|
}, |
|
}; |
|
EXPORT_SYMBOL_GPL(iqs62x_events); |
|
|
|
static irqreturn_t iqs62x_irq(int irq, void *context) |
|
{ |
|
struct iqs62x_core *iqs62x = context; |
|
struct i2c_client *client = iqs62x->client; |
|
struct iqs62x_event_data event_data; |
|
struct iqs62x_event_desc event_desc; |
|
enum iqs62x_event_reg event_reg; |
|
unsigned long event_flags = 0; |
|
int ret, i, j; |
|
u8 event_map[IQS62X_EVENT_SIZE]; |
|
|
|
/* |
|
* The device asserts the RDY output to signal the beginning of a |
|
* communication window, which is closed by an I2C stop condition. |
|
* As such, all interrupt status is captured in a single read and |
|
* broadcast to any interested sub-device drivers. |
|
*/ |
|
ret = regmap_raw_read(iqs62x->regmap, IQS62X_SYS_FLAGS, event_map, |
|
sizeof(event_map)); |
|
if (ret) { |
|
dev_err(&client->dev, "Failed to read device status: %d\n", |
|
ret); |
|
return IRQ_NONE; |
|
} |
|
|
|
for (i = 0; i < sizeof(event_map); i++) { |
|
event_reg = iqs62x->dev_desc->event_regs[iqs62x->ui_sel][i]; |
|
|
|
switch (event_reg) { |
|
case IQS62X_EVENT_UI_LO: |
|
event_data.ui_data = get_unaligned_le16(&event_map[i]); |
|
fallthrough; |
|
|
|
case IQS62X_EVENT_UI_HI: |
|
case IQS62X_EVENT_NONE: |
|
continue; |
|
|
|
case IQS62X_EVENT_ALS: |
|
event_data.als_flags = event_map[i]; |
|
continue; |
|
|
|
case IQS62X_EVENT_IR: |
|
event_data.ir_flags = event_map[i]; |
|
continue; |
|
|
|
case IQS62X_EVENT_INTER: |
|
event_data.interval = event_map[i]; |
|
continue; |
|
|
|
case IQS62X_EVENT_HYST: |
|
event_map[i] <<= iqs62x->dev_desc->hyst_shift; |
|
fallthrough; |
|
|
|
case IQS62X_EVENT_WHEEL: |
|
case IQS62X_EVENT_HALL: |
|
case IQS62X_EVENT_PROX: |
|
case IQS62X_EVENT_SYS: |
|
break; |
|
} |
|
|
|
for (j = 0; j < IQS62X_NUM_EVENTS; j++) { |
|
event_desc = iqs62x_events[j]; |
|
|
|
if (event_desc.reg != event_reg) |
|
continue; |
|
|
|
if ((event_map[i] & event_desc.mask) == event_desc.val) |
|
event_flags |= BIT(j); |
|
} |
|
} |
|
|
|
/* |
|
* The device resets itself in response to the I2C master stalling |
|
* communication past a fixed timeout. In this case, all registers |
|
* are restored and any interested sub-device drivers are notified. |
|
*/ |
|
if (event_flags & BIT(IQS62X_EVENT_SYS_RESET)) { |
|
dev_err(&client->dev, "Unexpected device reset\n"); |
|
|
|
ret = iqs62x_dev_init(iqs62x); |
|
if (ret) { |
|
dev_err(&client->dev, |
|
"Failed to re-initialize device: %d\n", ret); |
|
return IRQ_NONE; |
|
} |
|
|
|
iqs62x->event_cache |= BIT(IQS62X_EVENT_SYS_RESET); |
|
reinit_completion(&iqs62x->ati_done); |
|
} else if (event_flags & BIT(IQS62X_EVENT_SYS_ATI)) { |
|
iqs62x->event_cache |= BIT(IQS62X_EVENT_SYS_ATI); |
|
reinit_completion(&iqs62x->ati_done); |
|
} else if (!completion_done(&iqs62x->ati_done)) { |
|
ret = regmap_update_bits(iqs62x->regmap, IQS62X_SYS_SETTINGS, |
|
IQS62X_SYS_SETTINGS_EVENT_MODE, 0xFF); |
|
if (ret) { |
|
dev_err(&client->dev, |
|
"Failed to enable event mode: %d\n", ret); |
|
return IRQ_NONE; |
|
} |
|
|
|
msleep(IQS62X_FILT_SETTLE_MS); |
|
complete_all(&iqs62x->ati_done); |
|
} |
|
|
|
/* |
|
* Reset and ATI events are not broadcast to the sub-device drivers |
|
* until ATI has completed. Any other events that may have occurred |
|
* during ATI are ignored. |
|
*/ |
|
if (completion_done(&iqs62x->ati_done)) { |
|
event_flags |= iqs62x->event_cache; |
|
ret = blocking_notifier_call_chain(&iqs62x->nh, event_flags, |
|
&event_data); |
|
if (ret & NOTIFY_STOP_MASK) |
|
return IRQ_NONE; |
|
|
|
iqs62x->event_cache = 0; |
|
} |
|
|
|
/* |
|
* Once the communication window is closed, a small delay is added to |
|
* ensure the device's RDY output has been deasserted by the time the |
|
* interrupt handler returns. |
|
*/ |
|
usleep_range(150, 200); |
|
|
|
return IRQ_HANDLED; |
|
} |
|
|
|
static void iqs62x_firmware_load(const struct firmware *fw, void *context) |
|
{ |
|
struct iqs62x_core *iqs62x = context; |
|
struct i2c_client *client = iqs62x->client; |
|
int ret; |
|
|
|
if (fw) { |
|
ret = iqs62x_firmware_parse(iqs62x, fw); |
|
if (ret) { |
|
dev_err(&client->dev, "Failed to parse firmware: %d\n", |
|
ret); |
|
goto err_out; |
|
} |
|
} |
|
|
|
ret = iqs62x_dev_init(iqs62x); |
|
if (ret) { |
|
dev_err(&client->dev, "Failed to initialize device: %d\n", ret); |
|
goto err_out; |
|
} |
|
|
|
ret = devm_request_threaded_irq(&client->dev, client->irq, |
|
NULL, iqs62x_irq, IRQF_ONESHOT, |
|
client->name, iqs62x); |
|
if (ret) { |
|
dev_err(&client->dev, "Failed to request IRQ: %d\n", ret); |
|
goto err_out; |
|
} |
|
|
|
if (!wait_for_completion_timeout(&iqs62x->ati_done, |
|
msecs_to_jiffies(2000))) { |
|
dev_err(&client->dev, "Failed to complete ATI\n"); |
|
goto err_out; |
|
} |
|
|
|
ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_NONE, |
|
iqs62x->dev_desc->sub_devs, |
|
iqs62x->dev_desc->num_sub_devs, |
|
NULL, 0, NULL); |
|
if (ret) |
|
dev_err(&client->dev, "Failed to add sub-devices: %d\n", ret); |
|
|
|
err_out: |
|
complete_all(&iqs62x->fw_done); |
|
} |
|
|
|
static const struct mfd_cell iqs620at_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs620a-keys", |
|
}, |
|
{ |
|
.name = "iqs620a-pwm", |
|
.of_compatible = "azoteq,iqs620a-pwm", |
|
}, |
|
{ .name = "iqs620at-temp", }, |
|
}; |
|
|
|
static const struct mfd_cell iqs620a_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs620a-keys", |
|
}, |
|
{ |
|
.name = "iqs620a-pwm", |
|
.of_compatible = "azoteq,iqs620a-pwm", |
|
}, |
|
}; |
|
|
|
static const struct mfd_cell iqs621_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs621-keys", |
|
}, |
|
{ .name = "iqs621-als", }, |
|
}; |
|
|
|
static const struct mfd_cell iqs622_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs622-keys", |
|
}, |
|
{ .name = "iqs621-als", }, |
|
}; |
|
|
|
static const struct mfd_cell iqs624_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs624-keys", |
|
}, |
|
{ .name = "iqs624-pos", }, |
|
}; |
|
|
|
static const struct mfd_cell iqs625_sub_devs[] = { |
|
{ |
|
.name = "iqs62x-keys", |
|
.of_compatible = "azoteq,iqs625-keys", |
|
}, |
|
{ .name = "iqs624-pos", }, |
|
}; |
|
|
|
static const u8 iqs620at_cal_regs[] = { |
|
IQS620_TEMP_CAL_MULT, |
|
IQS620_TEMP_CAL_DIV, |
|
IQS620_TEMP_CAL_OFFS, |
|
}; |
|
|
|
static const u8 iqs621_cal_regs[] = { |
|
IQS621_ALS_CAL_DIV_LUX, |
|
IQS621_ALS_CAL_DIV_IR, |
|
}; |
|
|
|
static const enum iqs62x_event_reg iqs620a_event_regs[][IQS62X_EVENT_SIZE] = { |
|
[IQS62X_UI_PROX] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_PROX, /* 0x12 */ |
|
IQS62X_EVENT_HYST, /* 0x13 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_HALL, /* 0x16 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
}, |
|
[IQS62X_UI_SAR1] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_HYST, /* 0x13 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_HALL, /* 0x16 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
}, |
|
}; |
|
|
|
static const enum iqs62x_event_reg iqs621_event_regs[][IQS62X_EVENT_SIZE] = { |
|
[IQS62X_UI_PROX] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_PROX, /* 0x12 */ |
|
IQS62X_EVENT_HYST, /* 0x13 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_ALS, /* 0x16 */ |
|
IQS62X_EVENT_UI_LO, /* 0x17 */ |
|
IQS62X_EVENT_UI_HI, /* 0x18 */ |
|
IQS62X_EVENT_HALL, /* 0x19 */ |
|
}, |
|
}; |
|
|
|
static const enum iqs62x_event_reg iqs622_event_regs[][IQS62X_EVENT_SIZE] = { |
|
[IQS62X_UI_PROX] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_PROX, /* 0x12 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_ALS, /* 0x14 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_IR, /* 0x16 */ |
|
IQS62X_EVENT_UI_LO, /* 0x17 */ |
|
IQS62X_EVENT_UI_HI, /* 0x18 */ |
|
IQS62X_EVENT_HALL, /* 0x19 */ |
|
}, |
|
[IQS62X_UI_SAR1] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_HYST, /* 0x13 */ |
|
IQS62X_EVENT_ALS, /* 0x14 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_IR, /* 0x16 */ |
|
IQS62X_EVENT_UI_LO, /* 0x17 */ |
|
IQS62X_EVENT_UI_HI, /* 0x18 */ |
|
IQS62X_EVENT_HALL, /* 0x19 */ |
|
}, |
|
}; |
|
|
|
static const enum iqs62x_event_reg iqs624_event_regs[][IQS62X_EVENT_SIZE] = { |
|
[IQS62X_UI_PROX] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_PROX, /* 0x12 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_WHEEL, /* 0x14 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_UI_LO, /* 0x16 */ |
|
IQS62X_EVENT_UI_HI, /* 0x17 */ |
|
IQS62X_EVENT_INTER, /* 0x18 */ |
|
IQS62X_EVENT_NONE, |
|
}, |
|
}; |
|
|
|
static const enum iqs62x_event_reg iqs625_event_regs[][IQS62X_EVENT_SIZE] = { |
|
[IQS62X_UI_PROX] = { |
|
IQS62X_EVENT_SYS, /* 0x10 */ |
|
IQS62X_EVENT_PROX, /* 0x11 */ |
|
IQS62X_EVENT_INTER, /* 0x12 */ |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
IQS62X_EVENT_NONE, |
|
}, |
|
}; |
|
|
|
static const struct iqs62x_dev_desc iqs62x_devs[] = { |
|
{ |
|
.dev_name = "iqs620at", |
|
.sub_devs = iqs620at_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs620at_sub_devs), |
|
.prod_num = IQS620_PROD_NUM, |
|
.sw_num = 0x08, |
|
.cal_regs = iqs620at_cal_regs, |
|
.num_cal_regs = ARRAY_SIZE(iqs620at_cal_regs), |
|
.prox_mask = BIT(0), |
|
.sar_mask = BIT(1) | BIT(7), |
|
.hall_mask = BIT(2), |
|
.hyst_mask = BIT(3), |
|
.temp_mask = BIT(4), |
|
.prox_settings = IQS620_PROX_SETTINGS_4, |
|
.hall_flags = IQS620_HALL_FLAGS, |
|
.fw_name = "iqs620a.bin", |
|
.event_regs = &iqs620a_event_regs[IQS62X_UI_PROX], |
|
}, |
|
{ |
|
.dev_name = "iqs620a", |
|
.sub_devs = iqs620a_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs620a_sub_devs), |
|
.prod_num = IQS620_PROD_NUM, |
|
.sw_num = 0x08, |
|
.prox_mask = BIT(0), |
|
.sar_mask = BIT(1) | BIT(7), |
|
.hall_mask = BIT(2), |
|
.hyst_mask = BIT(3), |
|
.temp_mask = BIT(4), |
|
.prox_settings = IQS620_PROX_SETTINGS_4, |
|
.hall_flags = IQS620_HALL_FLAGS, |
|
.fw_name = "iqs620a.bin", |
|
.event_regs = &iqs620a_event_regs[IQS62X_UI_PROX], |
|
}, |
|
{ |
|
.dev_name = "iqs621", |
|
.sub_devs = iqs621_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs621_sub_devs), |
|
.prod_num = IQS621_PROD_NUM, |
|
.sw_num = 0x09, |
|
.cal_regs = iqs621_cal_regs, |
|
.num_cal_regs = ARRAY_SIZE(iqs621_cal_regs), |
|
.prox_mask = BIT(0), |
|
.hall_mask = BIT(1), |
|
.als_mask = BIT(2), |
|
.hyst_mask = BIT(3), |
|
.temp_mask = BIT(4), |
|
.als_flags = IQS621_ALS_FLAGS, |
|
.hall_flags = IQS621_HALL_FLAGS, |
|
.hyst_shift = 5, |
|
.fw_name = "iqs621.bin", |
|
.event_regs = &iqs621_event_regs[IQS62X_UI_PROX], |
|
}, |
|
{ |
|
.dev_name = "iqs622", |
|
.sub_devs = iqs622_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs622_sub_devs), |
|
.prod_num = IQS622_PROD_NUM, |
|
.sw_num = 0x06, |
|
.prox_mask = BIT(0), |
|
.sar_mask = BIT(1), |
|
.hall_mask = BIT(2), |
|
.als_mask = BIT(3), |
|
.ir_mask = BIT(4), |
|
.prox_settings = IQS622_PROX_SETTINGS_4, |
|
.als_flags = IQS622_ALS_FLAGS, |
|
.hall_flags = IQS622_HALL_FLAGS, |
|
.fw_name = "iqs622.bin", |
|
.event_regs = &iqs622_event_regs[IQS62X_UI_PROX], |
|
}, |
|
{ |
|
.dev_name = "iqs624", |
|
.sub_devs = iqs624_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs624_sub_devs), |
|
.prod_num = IQS624_PROD_NUM, |
|
.sw_num = 0x0B, |
|
.interval = IQS624_INTERVAL_NUM, |
|
.interval_div = 3, |
|
.fw_name = "iqs624.bin", |
|
.event_regs = &iqs624_event_regs[IQS62X_UI_PROX], |
|
}, |
|
{ |
|
.dev_name = "iqs625", |
|
.sub_devs = iqs625_sub_devs, |
|
.num_sub_devs = ARRAY_SIZE(iqs625_sub_devs), |
|
.prod_num = IQS625_PROD_NUM, |
|
.sw_num = 0x0B, |
|
.interval = IQS625_INTERVAL_NUM, |
|
.interval_div = 10, |
|
.fw_name = "iqs625.bin", |
|
.event_regs = &iqs625_event_regs[IQS62X_UI_PROX], |
|
}, |
|
}; |
|
|
|
static const struct regmap_config iqs62x_regmap_config = { |
|
.reg_bits = 8, |
|
.val_bits = 8, |
|
.max_register = IQS62X_MAX_REG, |
|
}; |
|
|
|
static int iqs62x_probe(struct i2c_client *client) |
|
{ |
|
struct iqs62x_core *iqs62x; |
|
struct iqs62x_info info; |
|
unsigned int val; |
|
int ret, i, j; |
|
u8 sw_num = 0; |
|
const char *fw_name = NULL; |
|
|
|
iqs62x = devm_kzalloc(&client->dev, sizeof(*iqs62x), GFP_KERNEL); |
|
if (!iqs62x) |
|
return -ENOMEM; |
|
|
|
i2c_set_clientdata(client, iqs62x); |
|
iqs62x->client = client; |
|
|
|
BLOCKING_INIT_NOTIFIER_HEAD(&iqs62x->nh); |
|
INIT_LIST_HEAD(&iqs62x->fw_blk_head); |
|
|
|
init_completion(&iqs62x->ati_done); |
|
init_completion(&iqs62x->fw_done); |
|
|
|
iqs62x->regmap = devm_regmap_init_i2c(client, &iqs62x_regmap_config); |
|
if (IS_ERR(iqs62x->regmap)) { |
|
ret = PTR_ERR(iqs62x->regmap); |
|
dev_err(&client->dev, "Failed to initialize register map: %d\n", |
|
ret); |
|
return ret; |
|
} |
|
|
|
ret = regmap_raw_read(iqs62x->regmap, IQS62X_PROD_NUM, &info, |
|
sizeof(info)); |
|
if (ret) |
|
return ret; |
|
|
|
/* |
|
* The following sequence validates the device's product and software |
|
* numbers. It then determines if the device is factory-calibrated by |
|
* checking for nonzero values in the device's designated calibration |
|
* registers (if applicable). Depending on the device, the absence of |
|
* calibration data indicates a reduced feature set or invalid device. |
|
* |
|
* For devices given in both calibrated and uncalibrated versions, the |
|
* calibrated version (e.g. IQS620AT) appears first in the iqs62x_devs |
|
* array. The uncalibrated version (e.g. IQS620A) appears next and has |
|
* the same product and software numbers, but no calibration registers |
|
* are specified. |
|
*/ |
|
for (i = 0; i < ARRAY_SIZE(iqs62x_devs); i++) { |
|
if (info.prod_num != iqs62x_devs[i].prod_num) |
|
continue; |
|
|
|
iqs62x->dev_desc = &iqs62x_devs[i]; |
|
|
|
if (info.sw_num < iqs62x->dev_desc->sw_num) |
|
continue; |
|
|
|
sw_num = info.sw_num; |
|
|
|
/* |
|
* Read each of the device's designated calibration registers, |
|
* if any, and exit from the inner loop early if any are equal |
|
* to zero (indicating the device is uncalibrated). This could |
|
* be acceptable depending on the device (e.g. IQS620A instead |
|
* of IQS620AT). |
|
*/ |
|
for (j = 0; j < iqs62x->dev_desc->num_cal_regs; j++) { |
|
ret = regmap_read(iqs62x->regmap, |
|
iqs62x->dev_desc->cal_regs[j], &val); |
|
if (ret) |
|
return ret; |
|
|
|
if (!val) |
|
break; |
|
} |
|
|
|
/* |
|
* If the number of nonzero values read from the device equals |
|
* the number of designated calibration registers (which could |
|
* be zero), exit from the outer loop early to signal that the |
|
* device's product and software numbers match a known device, |
|
* and the device is calibrated (if applicable). |
|
*/ |
|
if (j == iqs62x->dev_desc->num_cal_regs) |
|
break; |
|
} |
|
|
|
if (!iqs62x->dev_desc) { |
|
dev_err(&client->dev, "Unrecognized product number: 0x%02X\n", |
|
info.prod_num); |
|
return -EINVAL; |
|
} |
|
|
|
if (!sw_num) { |
|
dev_err(&client->dev, "Unrecognized software number: 0x%02X\n", |
|
info.sw_num); |
|
return -EINVAL; |
|
} |
|
|
|
if (i == ARRAY_SIZE(iqs62x_devs)) { |
|
dev_err(&client->dev, "Uncalibrated device\n"); |
|
return -ENODATA; |
|
} |
|
|
|
device_property_read_string(&client->dev, "firmware-name", &fw_name); |
|
|
|
ret = request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, |
|
fw_name ? : iqs62x->dev_desc->fw_name, |
|
&client->dev, GFP_KERNEL, iqs62x, |
|
iqs62x_firmware_load); |
|
if (ret) |
|
dev_err(&client->dev, "Failed to request firmware: %d\n", ret); |
|
|
|
return ret; |
|
} |
|
|
|
static int iqs62x_remove(struct i2c_client *client) |
|
{ |
|
struct iqs62x_core *iqs62x = i2c_get_clientdata(client); |
|
|
|
wait_for_completion(&iqs62x->fw_done); |
|
|
|
return 0; |
|
} |
|
|
|
static int __maybe_unused iqs62x_suspend(struct device *dev) |
|
{ |
|
struct iqs62x_core *iqs62x = dev_get_drvdata(dev); |
|
int ret; |
|
|
|
wait_for_completion(&iqs62x->fw_done); |
|
|
|
/* |
|
* As per the datasheet, automatic mode switching must be disabled |
|
* before the device is placed in or taken out of halt mode. |
|
*/ |
|
ret = regmap_update_bits(iqs62x->regmap, IQS62X_PWR_SETTINGS, |
|
IQS62X_PWR_SETTINGS_DIS_AUTO, 0xFF); |
|
if (ret) |
|
return ret; |
|
|
|
return regmap_update_bits(iqs62x->regmap, IQS62X_PWR_SETTINGS, |
|
IQS62X_PWR_SETTINGS_PWR_MODE_MASK, |
|
IQS62X_PWR_SETTINGS_PWR_MODE_HALT); |
|
} |
|
|
|
static int __maybe_unused iqs62x_resume(struct device *dev) |
|
{ |
|
struct iqs62x_core *iqs62x = dev_get_drvdata(dev); |
|
int ret; |
|
|
|
ret = regmap_update_bits(iqs62x->regmap, IQS62X_PWR_SETTINGS, |
|
IQS62X_PWR_SETTINGS_PWR_MODE_MASK, |
|
IQS62X_PWR_SETTINGS_PWR_MODE_NORM); |
|
if (ret) |
|
return ret; |
|
|
|
return regmap_update_bits(iqs62x->regmap, IQS62X_PWR_SETTINGS, |
|
IQS62X_PWR_SETTINGS_DIS_AUTO, 0); |
|
} |
|
|
|
static SIMPLE_DEV_PM_OPS(iqs62x_pm, iqs62x_suspend, iqs62x_resume); |
|
|
|
static const struct of_device_id iqs62x_of_match[] = { |
|
{ .compatible = "azoteq,iqs620a" }, |
|
{ .compatible = "azoteq,iqs621" }, |
|
{ .compatible = "azoteq,iqs622" }, |
|
{ .compatible = "azoteq,iqs624" }, |
|
{ .compatible = "azoteq,iqs625" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, iqs62x_of_match); |
|
|
|
static struct i2c_driver iqs62x_i2c_driver = { |
|
.driver = { |
|
.name = "iqs62x", |
|
.of_match_table = iqs62x_of_match, |
|
.pm = &iqs62x_pm, |
|
}, |
|
.probe_new = iqs62x_probe, |
|
.remove = iqs62x_remove, |
|
}; |
|
module_i2c_driver(iqs62x_i2c_driver); |
|
|
|
MODULE_AUTHOR("Jeff LaBundy <[email protected]>"); |
|
MODULE_DESCRIPTION("Azoteq IQS620A/621/622/624/625 Multi-Function Sensors"); |
|
MODULE_LICENSE("GPL");
|
|
|