|  | // SPDX-License-Identifier: GPL-2.0-only | 
|  | /* | 
|  | * Copyright (c) 2022, 2024-2025 Qualcomm Innovation Center, Inc. All rights reserved. | 
|  | */ | 
|  |  | 
|  | #include <linux/bitfield.h> | 
|  | #include <linux/bits.h> | 
|  | #include <linux/leds.h> | 
|  | #include <linux/led-class-flash.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/platform_device.h> | 
|  | #include <linux/property.h> | 
|  | #include <linux/regmap.h> | 
|  | #include <media/v4l2-flash-led-class.h> | 
|  |  | 
|  | /* registers definitions */ | 
|  | #define FLASH_REVISION_REG		0x00 | 
|  | #define FLASH_4CH_REVISION_V0P1		0x01 | 
|  |  | 
|  | #define FLASH_TYPE_REG			0x04 | 
|  | #define FLASH_TYPE_VAL			0x18 | 
|  |  | 
|  | #define FLASH_SUBTYPE_REG		0x05 | 
|  | #define FLASH_SUBTYPE_3CH_PM8150_VAL	0x04 | 
|  | #define FLASH_SUBTYPE_3CH_PMI8998_VAL	0x03 | 
|  | #define FLASH_SUBTYPE_4CH_VAL		0x07 | 
|  |  | 
|  | #define FLASH_STS_3CH_OTST1		BIT(0) | 
|  | #define FLASH_STS_3CH_OTST2		BIT(1) | 
|  | #define FLASH_STS_3CH_OTST3		BIT(2) | 
|  | #define FLASH_STS_3CH_BOB_THM_OVERLOAD	BIT(3) | 
|  | #define FLASH_STS_3CH_VPH_DROOP		BIT(4) | 
|  | #define FLASH_STS_3CH_BOB_ILIM_S1	BIT(5) | 
|  | #define FLASH_STS_3CH_BOB_ILIM_S2	BIT(6) | 
|  | #define FLASH_STS_3CH_BCL_IBAT		BIT(7) | 
|  |  | 
|  | #define FLASH_STS_4CH_VPH_LOW		BIT(0) | 
|  | #define FLASH_STS_4CH_BCL_IBAT		BIT(1) | 
|  | #define FLASH_STS_4CH_BOB_ILIM_S1	BIT(2) | 
|  | #define FLASH_STS_4CH_BOB_ILIM_S2	BIT(3) | 
|  | #define FLASH_STS_4CH_OTST2		BIT(4) | 
|  | #define FLASH_STS_4CH_OTST1		BIT(5) | 
|  | #define FLASH_STS_4CHG_BOB_THM_OVERLOAD	BIT(6) | 
|  |  | 
|  | #define FLASH_TIMER_EN_BIT		BIT(7) | 
|  | #define FLASH_TIMER_VAL_MASK		GENMASK(6, 0) | 
|  | #define FLASH_TIMER_STEP_MS		10 | 
|  |  | 
|  | #define FLASH_STROBE_HW_SW_SEL_BIT	BIT(2) | 
|  | #define SW_STROBE_VAL			0 | 
|  | #define HW_STROBE_VAL			1 | 
|  | #define FLASH_HW_STROBE_TRIGGER_SEL_BIT	BIT(1) | 
|  | #define STROBE_LEVEL_TRIGGER_VAL	0 | 
|  | #define STROBE_EDGE_TRIGGER_VAL		1 | 
|  | #define FLASH_STROBE_POLARITY_BIT	BIT(0) | 
|  | #define STROBE_ACTIVE_HIGH_VAL		1 | 
|  |  | 
|  | #define FLASH_IRES_MASK_4CH		BIT(0) | 
|  | #define FLASH_IRES_MASK_3CH		GENMASK(1, 0) | 
|  | #define FLASH_IRES_12P5MA_VAL		0 | 
|  | #define FLASH_IRES_5MA_VAL_4CH		1 | 
|  | #define FLASH_IRES_5MA_VAL_3CH		3 | 
|  |  | 
|  | /* constants */ | 
|  | #define FLASH_CURRENT_MAX_UA		1500000 | 
|  | #define TORCH_CURRENT_MAX_UA		500000 | 
|  | #define FLASH_TOTAL_CURRENT_MAX_UA	2000000 | 
|  | #define FLASH_CURRENT_DEFAULT_UA	1000000 | 
|  | #define TORCH_CURRENT_DEFAULT_UA	200000 | 
|  |  | 
|  | #define TORCH_IRES_UA			5000 | 
|  | #define FLASH_IRES_UA			12500 | 
|  |  | 
|  | #define FLASH_TIMEOUT_MAX_US		1280000 | 
|  | #define FLASH_TIMEOUT_STEP_US		10000 | 
|  |  | 
|  | #define UA_PER_MA			1000 | 
|  |  | 
|  | /* thermal threshold constants */ | 
|  | #define OTST_3CH_MIN_VAL		3 | 
|  | #define OTST1_4CH_MIN_VAL		0 | 
|  | #define OTST1_4CH_V0P1_MIN_VAL		3 | 
|  | #define OTST2_4CH_MIN_VAL		0 | 
|  |  | 
|  | #define OTST1_MAX_CURRENT_MA		1000 | 
|  | #define OTST2_MAX_CURRENT_MA		500 | 
|  | #define OTST3_MAX_CURRENT_MA		200 | 
|  |  | 
|  | enum hw_type { | 
|  | QCOM_MVFLASH_3CH, | 
|  | QCOM_MVFLASH_4CH, | 
|  | }; | 
|  |  | 
|  | enum led_mode { | 
|  | FLASH_MODE, | 
|  | TORCH_MODE, | 
|  | }; | 
|  |  | 
|  | enum led_strobe { | 
|  | SW_STROBE, | 
|  | HW_STROBE, | 
|  | }; | 
|  |  | 
|  | enum { | 
|  | REG_STATUS1, | 
|  | REG_STATUS2, | 
|  | REG_STATUS3, | 
|  | REG_CHAN_TIMER, | 
|  | REG_ITARGET, | 
|  | REG_MODULE_EN, | 
|  | REG_IRESOLUTION, | 
|  | REG_CHAN_STROBE, | 
|  | REG_CHAN_EN, | 
|  | REG_THERM_THRSH1, | 
|  | REG_THERM_THRSH2, | 
|  | REG_THERM_THRSH3, | 
|  | REG_TORCH_CLAMP, | 
|  | REG_MAX_COUNT, | 
|  | }; | 
|  |  | 
|  | static const struct reg_field mvflash_3ch_pmi8998_regs[REG_MAX_COUNT] = { | 
|  | [REG_STATUS1]		= REG_FIELD(0x08, 0, 5), | 
|  | [REG_STATUS2]		= REG_FIELD(0x09, 0, 7), | 
|  | [REG_STATUS3]		= REG_FIELD(0x0a, 0, 7), | 
|  | [REG_CHAN_TIMER]	= REG_FIELD_ID(0x40, 0, 7, 3, 1), | 
|  | [REG_ITARGET]		= REG_FIELD_ID(0x43, 0, 6, 3, 1), | 
|  | [REG_MODULE_EN]		= REG_FIELD(0x46, 7, 7), | 
|  | [REG_IRESOLUTION]	= REG_FIELD(0x47, 0, 5), | 
|  | [REG_CHAN_STROBE]	= REG_FIELD_ID(0x49, 0, 2, 3, 1), | 
|  | [REG_CHAN_EN]		= REG_FIELD(0x4c, 0, 2), | 
|  | [REG_THERM_THRSH1]	= REG_FIELD(0x56, 0, 2), | 
|  | [REG_THERM_THRSH2]	= REG_FIELD(0x57, 0, 2), | 
|  | [REG_THERM_THRSH3]	= REG_FIELD(0x58, 0, 2), | 
|  | [REG_TORCH_CLAMP]	= REG_FIELD(0xea, 0, 6), | 
|  | }; | 
|  |  | 
|  | static const struct reg_field mvflash_3ch_regs[REG_MAX_COUNT] = { | 
|  | [REG_STATUS1]		= REG_FIELD(0x08, 0, 7), | 
|  | [REG_STATUS2]		= REG_FIELD(0x09, 0, 7), | 
|  | [REG_STATUS3]		= REG_FIELD(0x0a, 0, 7), | 
|  | [REG_CHAN_TIMER]	= REG_FIELD_ID(0x40, 0, 7, 3, 1), | 
|  | [REG_ITARGET]		= REG_FIELD_ID(0x43, 0, 6, 3, 1), | 
|  | [REG_MODULE_EN]		= REG_FIELD(0x46, 7, 7), | 
|  | [REG_IRESOLUTION]	= REG_FIELD(0x47, 0, 5), | 
|  | [REG_CHAN_STROBE]	= REG_FIELD_ID(0x49, 0, 2, 3, 1), | 
|  | [REG_CHAN_EN]		= REG_FIELD(0x4c, 0, 2), | 
|  | [REG_THERM_THRSH1]	= REG_FIELD(0x56, 0, 2), | 
|  | [REG_THERM_THRSH2]	= REG_FIELD(0x57, 0, 2), | 
|  | [REG_THERM_THRSH3]	= REG_FIELD(0x58, 0, 2), | 
|  | [REG_TORCH_CLAMP]	= REG_FIELD(0xec, 0, 6), | 
|  | }; | 
|  |  | 
|  | static const struct reg_field mvflash_4ch_regs[REG_MAX_COUNT] = { | 
|  | [REG_STATUS1]		= REG_FIELD(0x06, 0, 7), | 
|  | [REG_STATUS2]		= REG_FIELD(0x07, 0, 6), | 
|  | [REG_STATUS3]		= REG_FIELD(0x09, 0, 7), | 
|  | [REG_CHAN_TIMER]	= REG_FIELD_ID(0x3e, 0, 7, 4, 1), | 
|  | [REG_ITARGET]		= REG_FIELD_ID(0x42, 0, 6, 4, 1), | 
|  | [REG_MODULE_EN]		= REG_FIELD(0x46, 7, 7), | 
|  | [REG_IRESOLUTION]	= REG_FIELD(0x49, 0, 3), | 
|  | [REG_CHAN_STROBE]	= REG_FIELD_ID(0x4a, 0, 6, 4, 1), | 
|  | [REG_CHAN_EN]		= REG_FIELD(0x4e, 0, 3), | 
|  | [REG_THERM_THRSH1]	= REG_FIELD(0x7a, 0, 2), | 
|  | [REG_THERM_THRSH2]	= REG_FIELD(0x78, 0, 2), | 
|  | [REG_TORCH_CLAMP]	= REG_FIELD(0xed, 0, 6), | 
|  | }; | 
|  |  | 
|  | struct qcom_flash_data { | 
|  | struct v4l2_flash	**v4l2_flash; | 
|  | struct regmap_field     *r_fields[REG_MAX_COUNT]; | 
|  | struct mutex		lock; | 
|  | enum hw_type		hw_type; | 
|  | u32			total_ma; | 
|  | u8			leds_count; | 
|  | u8			max_channels; | 
|  | u8			chan_en_bits; | 
|  | u8			revision; | 
|  | u8			torch_clamp; | 
|  | }; | 
|  |  | 
|  | struct qcom_flash_led { | 
|  | struct qcom_flash_data		*flash_data; | 
|  | struct led_classdev_flash	flash; | 
|  | u32				max_flash_current_ma; | 
|  | u32				max_torch_current_ma; | 
|  | u32				max_timeout_ms; | 
|  | u32				flash_current_ma; | 
|  | u32				flash_timeout_ms; | 
|  | u32				current_in_use_ma; | 
|  | u8				*chan_id; | 
|  | u8				chan_count; | 
|  | bool				enabled; | 
|  | }; | 
|  |  | 
|  | static int set_flash_module_en(struct qcom_flash_led *led, bool en) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u8 led_mask = 0, enable; | 
|  | int i, rc; | 
|  |  | 
|  | for (i = 0; i < led->chan_count; i++) | 
|  | led_mask |= BIT(led->chan_id[i]); | 
|  |  | 
|  | mutex_lock(&flash_data->lock); | 
|  | if (en) | 
|  | flash_data->chan_en_bits |= led_mask; | 
|  | else | 
|  | flash_data->chan_en_bits &= ~led_mask; | 
|  |  | 
|  | enable = !!flash_data->chan_en_bits; | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_MODULE_EN], enable); | 
|  | if (rc) | 
|  | dev_err(led->flash.led_cdev.dev, "write module_en failed, rc=%d\n", rc); | 
|  | mutex_unlock(&flash_data->lock); | 
|  |  | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | static int update_allowed_flash_current(struct qcom_flash_led *led, u32 *current_ma, bool strobe) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u32 therm_ma, avail_ma, thrsh[3], min_thrsh, sts; | 
|  | int rc = 0; | 
|  |  | 
|  | mutex_lock(&flash_data->lock); | 
|  | /* | 
|  | * Put previously allocated current into allowed budget in either of these two cases: | 
|  | * 1) LED is disabled; | 
|  | * 2) LED is enabled repeatedly | 
|  | */ | 
|  | if (!strobe || led->current_in_use_ma != 0) { | 
|  | if (flash_data->total_ma >= led->current_in_use_ma) | 
|  | flash_data->total_ma -= led->current_in_use_ma; | 
|  | else | 
|  | flash_data->total_ma = 0; | 
|  |  | 
|  | led->current_in_use_ma = 0; | 
|  | if (!strobe) | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | /* | 
|  | * Cache the default thermal threshold settings, and set them to the lowest levels before | 
|  | * reading over-temp real time status. If over-temp has been triggered at the lowest | 
|  | * threshold, it's very likely that it would be triggered at a higher (default) threshold | 
|  | * when more flash current is requested. Prevent device from triggering over-temp condition | 
|  | * by limiting the flash current for the new request. | 
|  | */ | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_THERM_THRSH1], &thrsh[0]); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  |  | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_THERM_THRSH2], &thrsh[1]); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_THERM_THRSH3], &thrsh[2]); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  | } | 
|  |  | 
|  | min_thrsh = OTST_3CH_MIN_VAL; | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_4CH) | 
|  | min_thrsh = (flash_data->revision == FLASH_4CH_REVISION_V0P1) ? | 
|  | OTST1_4CH_V0P1_MIN_VAL : OTST1_4CH_MIN_VAL; | 
|  |  | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH1], min_thrsh); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_4CH) | 
|  | min_thrsh = OTST2_4CH_MIN_VAL; | 
|  |  | 
|  | /* | 
|  | * The default thermal threshold settings have been updated hence | 
|  | * restore them if any fault happens starting from here. | 
|  | */ | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH2], min_thrsh); | 
|  | if (rc < 0) | 
|  | goto restore; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH3], min_thrsh); | 
|  | if (rc < 0) | 
|  | goto restore; | 
|  | } | 
|  |  | 
|  | /* Read thermal level status to get corresponding derating flash current */ | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_STATUS2], &sts); | 
|  | if (rc) | 
|  | goto restore; | 
|  |  | 
|  | therm_ma = FLASH_TOTAL_CURRENT_MAX_UA / 1000; | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | if (sts & FLASH_STS_3CH_OTST3) | 
|  | therm_ma = OTST3_MAX_CURRENT_MA; | 
|  | else if (sts & FLASH_STS_3CH_OTST2) | 
|  | therm_ma = OTST2_MAX_CURRENT_MA; | 
|  | else if (sts & FLASH_STS_3CH_OTST1) | 
|  | therm_ma = OTST1_MAX_CURRENT_MA; | 
|  | } else { | 
|  | if (sts & FLASH_STS_4CH_OTST2) | 
|  | therm_ma = OTST2_MAX_CURRENT_MA; | 
|  | else if (sts & FLASH_STS_4CH_OTST1) | 
|  | therm_ma = OTST1_MAX_CURRENT_MA; | 
|  | } | 
|  |  | 
|  | /* Calculate the allowed flash current for the request */ | 
|  | if (therm_ma <= flash_data->total_ma) | 
|  | avail_ma = 0; | 
|  | else | 
|  | avail_ma = therm_ma - flash_data->total_ma; | 
|  |  | 
|  | *current_ma = min_t(u32, *current_ma, avail_ma); | 
|  | led->current_in_use_ma = *current_ma; | 
|  | flash_data->total_ma += led->current_in_use_ma; | 
|  |  | 
|  | dev_dbg(led->flash.led_cdev.dev, "allowed flash current: %dmA, total current: %dmA\n", | 
|  | led->current_in_use_ma, flash_data->total_ma); | 
|  |  | 
|  | restore: | 
|  | /* Restore to default thermal threshold settings */ | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH1], thrsh[0]); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  |  | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH2], thrsh[1]); | 
|  | if (rc < 0) | 
|  | goto unlock; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) | 
|  | rc = regmap_field_write(flash_data->r_fields[REG_THERM_THRSH3], thrsh[2]); | 
|  |  | 
|  | unlock: | 
|  | mutex_unlock(&flash_data->lock); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | static int set_flash_current(struct qcom_flash_led *led, u32 current_ma, enum led_mode mode) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u32 itarg_ua, ires_ua; | 
|  | u8 shift, ires_mask = 0, ires_val = 0, chan_id; | 
|  | int i, rc; | 
|  |  | 
|  | /* | 
|  | * Split the current across the channels and set the | 
|  | * IRESOLUTION and ITARGET registers accordingly. | 
|  | */ | 
|  | itarg_ua = (current_ma * UA_PER_MA) / led->chan_count + 1; | 
|  | ires_ua = (mode == FLASH_MODE) ? FLASH_IRES_UA : TORCH_IRES_UA; | 
|  |  | 
|  | for (i = 0; i < led->chan_count; i++) { | 
|  | u8 itarget = 0; | 
|  |  | 
|  | if (itarg_ua > ires_ua) | 
|  | itarget = itarg_ua / ires_ua - 1; | 
|  |  | 
|  | chan_id = led->chan_id[i]; | 
|  |  | 
|  | rc = regmap_fields_write(flash_data->r_fields[REG_ITARGET], chan_id, itarget); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | shift = chan_id * 2; | 
|  | ires_mask |= FLASH_IRES_MASK_3CH << shift; | 
|  | ires_val |= ((mode == FLASH_MODE) ? | 
|  | (FLASH_IRES_12P5MA_VAL << shift) : | 
|  | (FLASH_IRES_5MA_VAL_3CH << shift)); | 
|  | } else if (flash_data->hw_type == QCOM_MVFLASH_4CH) { | 
|  | shift = chan_id; | 
|  | ires_mask |= FLASH_IRES_MASK_4CH << shift; | 
|  | ires_val |= ((mode == FLASH_MODE) ? | 
|  | (FLASH_IRES_12P5MA_VAL << shift) : | 
|  | (FLASH_IRES_5MA_VAL_4CH << shift)); | 
|  | } else { | 
|  | dev_err(led->flash.led_cdev.dev, | 
|  | "HW type %d is not supported\n", flash_data->hw_type); | 
|  | return -EOPNOTSUPP; | 
|  | } | 
|  | } | 
|  |  | 
|  | return regmap_field_update_bits(flash_data->r_fields[REG_IRESOLUTION], ires_mask, ires_val); | 
|  | } | 
|  |  | 
|  | static int set_flash_timeout(struct qcom_flash_led *led, u32 timeout_ms) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u8 timer, chan_id; | 
|  | int rc, i; | 
|  |  | 
|  | /* set SAFETY_TIMER for all the channels connected to the same LED */ | 
|  | timeout_ms = min_t(u32, timeout_ms, led->max_timeout_ms); | 
|  |  | 
|  | for (i = 0; i < led->chan_count; i++) { | 
|  | chan_id = led->chan_id[i]; | 
|  |  | 
|  | timer = timeout_ms / FLASH_TIMER_STEP_MS; | 
|  | timer = clamp_t(u8, timer, 0, FLASH_TIMER_VAL_MASK); | 
|  |  | 
|  | if (timeout_ms) | 
|  | timer |= FLASH_TIMER_EN_BIT; | 
|  |  | 
|  | rc = regmap_fields_write(flash_data->r_fields[REG_CHAN_TIMER], chan_id, timer); | 
|  | if (rc) | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int set_flash_strobe(struct qcom_flash_led *led, enum led_strobe strobe, bool state) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u8 strobe_sel, chan_en, chan_id, chan_mask = 0; | 
|  | int rc, i; | 
|  |  | 
|  | /* Set SW strobe config for all channels connected to the LED */ | 
|  | for (i = 0; i < led->chan_count; i++) { | 
|  | chan_id = led->chan_id[i]; | 
|  |  | 
|  | if (strobe == SW_STROBE) | 
|  | strobe_sel = FIELD_PREP(FLASH_STROBE_HW_SW_SEL_BIT, SW_STROBE_VAL); | 
|  | else | 
|  | strobe_sel = FIELD_PREP(FLASH_STROBE_HW_SW_SEL_BIT, HW_STROBE_VAL); | 
|  |  | 
|  | strobe_sel |= | 
|  | FIELD_PREP(FLASH_HW_STROBE_TRIGGER_SEL_BIT, STROBE_LEVEL_TRIGGER_VAL) | | 
|  | FIELD_PREP(FLASH_STROBE_POLARITY_BIT, STROBE_ACTIVE_HIGH_VAL); | 
|  |  | 
|  | rc = regmap_fields_write( | 
|  | flash_data->r_fields[REG_CHAN_STROBE], chan_id, strobe_sel); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | chan_mask |= BIT(chan_id); | 
|  | } | 
|  |  | 
|  | /* Enable/disable flash channels */ | 
|  | chan_en = state ? chan_mask : 0; | 
|  | rc = regmap_field_update_bits(flash_data->r_fields[REG_CHAN_EN], chan_mask, chan_en); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | led->enabled = state; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static inline struct qcom_flash_led *flcdev_to_qcom_fled(struct led_classdev_flash *flcdev) | 
|  | { | 
|  | return container_of(flcdev, struct qcom_flash_led, flash); | 
|  | } | 
|  |  | 
|  | static int qcom_flash_brightness_set(struct led_classdev_flash *fled_cdev, u32 brightness) | 
|  | { | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  |  | 
|  | led->flash_current_ma = min_t(u32, led->max_flash_current_ma, brightness / UA_PER_MA); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qcom_flash_timeout_set(struct led_classdev_flash *fled_cdev, u32 timeout) | 
|  | { | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  |  | 
|  | led->flash_timeout_ms = timeout / USEC_PER_MSEC; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qcom_flash_strobe_set(struct led_classdev_flash *fled_cdev, bool state) | 
|  | { | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  | int rc; | 
|  |  | 
|  | rc = set_flash_strobe(led, SW_STROBE, false); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = update_allowed_flash_current(led, &led->flash_current_ma, state); | 
|  | if (rc < 0) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_current(led, led->flash_current_ma, FLASH_MODE); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_timeout(led, led->flash_timeout_ms); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_module_en(led, state); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | return set_flash_strobe(led, SW_STROBE, state); | 
|  | } | 
|  |  | 
|  | static int qcom_flash_strobe_get(struct led_classdev_flash *fled_cdev, bool *state) | 
|  | { | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  |  | 
|  | *state = led->enabled; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qcom_flash_fault_get(struct led_classdev_flash *fled_cdev, u32 *fault) | 
|  | { | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | u8 shift, chan_id, chan_mask = 0; | 
|  | u8 ot_mask = 0, oc_mask = 0, uv_mask = 0; | 
|  | u32 val, fault_sts = 0; | 
|  | int i, rc; | 
|  |  | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_STATUS1], &val); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | for (i = 0; i < led->chan_count; i++) { | 
|  | chan_id = led->chan_id[i]; | 
|  | shift = chan_id * 2; | 
|  |  | 
|  | if (val & BIT(shift)) | 
|  | fault_sts |= LED_FAULT_SHORT_CIRCUIT; | 
|  |  | 
|  | chan_mask |= BIT(chan_id); | 
|  | } | 
|  |  | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_STATUS2], &val); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | ot_mask = FLASH_STS_3CH_OTST1 | | 
|  | FLASH_STS_3CH_OTST2 | | 
|  | FLASH_STS_3CH_OTST3 | | 
|  | FLASH_STS_3CH_BOB_THM_OVERLOAD; | 
|  | oc_mask = FLASH_STS_3CH_BOB_ILIM_S1 | | 
|  | FLASH_STS_3CH_BOB_ILIM_S2 | | 
|  | FLASH_STS_3CH_BCL_IBAT; | 
|  | uv_mask = FLASH_STS_3CH_VPH_DROOP; | 
|  | } else if (flash_data->hw_type == QCOM_MVFLASH_4CH) { | 
|  | ot_mask = FLASH_STS_4CH_OTST2 | | 
|  | FLASH_STS_4CH_OTST1 | | 
|  | FLASH_STS_4CHG_BOB_THM_OVERLOAD; | 
|  | oc_mask = FLASH_STS_4CH_BCL_IBAT | | 
|  | FLASH_STS_4CH_BOB_ILIM_S1 | | 
|  | FLASH_STS_4CH_BOB_ILIM_S2; | 
|  | uv_mask = FLASH_STS_4CH_VPH_LOW; | 
|  | } | 
|  |  | 
|  | if (val & ot_mask) | 
|  | fault_sts |= LED_FAULT_OVER_TEMPERATURE; | 
|  |  | 
|  | if (val & oc_mask) | 
|  | fault_sts |= LED_FAULT_OVER_CURRENT; | 
|  |  | 
|  | if (val & uv_mask) | 
|  | fault_sts |= LED_FAULT_INPUT_VOLTAGE; | 
|  |  | 
|  | rc = regmap_field_read(flash_data->r_fields[REG_STATUS3], &val); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | if (flash_data->hw_type == QCOM_MVFLASH_3CH) { | 
|  | if (val & chan_mask) | 
|  | fault_sts |= LED_FAULT_TIMEOUT; | 
|  | } else if (flash_data->hw_type == QCOM_MVFLASH_4CH) { | 
|  | for (i = 0; i < led->chan_count; i++) { | 
|  | chan_id = led->chan_id[i]; | 
|  | shift = chan_id * 2; | 
|  |  | 
|  | if (val & BIT(shift)) | 
|  | fault_sts |= LED_FAULT_TIMEOUT; | 
|  | } | 
|  | } | 
|  |  | 
|  | *fault = fault_sts; | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qcom_flash_led_brightness_set(struct led_classdev *led_cdev, | 
|  | enum led_brightness brightness) | 
|  | { | 
|  | struct led_classdev_flash *fled_cdev = lcdev_to_flcdev(led_cdev); | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  | u32 current_ma = brightness * led->max_torch_current_ma / LED_FULL; | 
|  | bool enable = !!brightness; | 
|  | int rc; | 
|  |  | 
|  | rc = set_flash_strobe(led, SW_STROBE, false); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_module_en(led, false); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = update_allowed_flash_current(led, ¤t_ma, enable); | 
|  | if (rc < 0) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_current(led, current_ma, TORCH_MODE); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | /* Disable flash timeout for torch LED */ | 
|  | rc = set_flash_timeout(led, 0); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | rc = set_flash_module_en(led, enable); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | return set_flash_strobe(led, SW_STROBE, enable); | 
|  | } | 
|  |  | 
|  | static const struct led_flash_ops qcom_flash_ops = { | 
|  | .flash_brightness_set = qcom_flash_brightness_set, | 
|  | .strobe_set = qcom_flash_strobe_set, | 
|  | .strobe_get = qcom_flash_strobe_get, | 
|  | .timeout_set = qcom_flash_timeout_set, | 
|  | .fault_get = qcom_flash_fault_get, | 
|  | }; | 
|  |  | 
|  | #if IS_ENABLED(CONFIG_V4L2_FLASH_LED_CLASS) | 
|  | static int qcom_flash_external_strobe_set(struct v4l2_flash *v4l2_flash, bool enable) | 
|  | { | 
|  | struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  | int rc; | 
|  |  | 
|  | rc = set_flash_module_en(led, enable); | 
|  | if (rc) | 
|  | return rc; | 
|  |  | 
|  | if (enable) | 
|  | return set_flash_strobe(led, HW_STROBE, true); | 
|  | else | 
|  | return set_flash_strobe(led, SW_STROBE, false); | 
|  | } | 
|  |  | 
|  | static enum led_brightness | 
|  | qcom_flash_intensity_to_led_brightness(struct v4l2_flash *v4l2_flash, s32 intensity) | 
|  | { | 
|  | struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  | u32 current_ma = intensity / UA_PER_MA; | 
|  |  | 
|  | current_ma = min_t(u32, current_ma, led->max_torch_current_ma); | 
|  | if (!current_ma) | 
|  | return LED_OFF; | 
|  |  | 
|  | return (current_ma * LED_FULL) / led->max_torch_current_ma; | 
|  | } | 
|  |  | 
|  | static s32 qcom_flash_brightness_to_led_intensity(struct v4l2_flash *v4l2_flash, | 
|  | enum led_brightness brightness) | 
|  | { | 
|  | struct led_classdev_flash *fled_cdev = v4l2_flash->fled_cdev; | 
|  | struct qcom_flash_led *led = flcdev_to_qcom_fled(fled_cdev); | 
|  |  | 
|  | return (brightness * led->max_torch_current_ma * UA_PER_MA) / LED_FULL; | 
|  | } | 
|  |  | 
|  | static const struct v4l2_flash_ops qcom_v4l2_flash_ops = { | 
|  | .external_strobe_set = qcom_flash_external_strobe_set, | 
|  | .intensity_to_led_brightness = qcom_flash_intensity_to_led_brightness, | 
|  | .led_brightness_to_intensity = qcom_flash_brightness_to_led_intensity, | 
|  | }; | 
|  |  | 
|  | static int | 
|  | qcom_flash_v4l2_init(struct device *dev, struct qcom_flash_led *led, struct fwnode_handle *fwnode) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | struct v4l2_flash_config v4l2_cfg = { 0 }; | 
|  | struct led_flash_setting *intensity = &v4l2_cfg.intensity; | 
|  | struct v4l2_flash *v4l2_flash; | 
|  |  | 
|  | if (!(led->flash.led_cdev.flags & LED_DEV_CAP_FLASH)) | 
|  | return 0; | 
|  |  | 
|  | intensity->min = intensity->step = TORCH_IRES_UA * led->chan_count; | 
|  | intensity->max = led->max_torch_current_ma * UA_PER_MA; | 
|  | intensity->val = min_t(u32, intensity->max, TORCH_CURRENT_DEFAULT_UA); | 
|  |  | 
|  | strscpy(v4l2_cfg.dev_name, led->flash.led_cdev.dev->kobj.name, | 
|  | sizeof(v4l2_cfg.dev_name)); | 
|  |  | 
|  | v4l2_cfg.has_external_strobe = true; | 
|  | v4l2_cfg.flash_faults = LED_FAULT_INPUT_VOLTAGE | | 
|  | LED_FAULT_OVER_CURRENT | | 
|  | LED_FAULT_SHORT_CIRCUIT | | 
|  | LED_FAULT_OVER_TEMPERATURE | | 
|  | LED_FAULT_TIMEOUT; | 
|  |  | 
|  | v4l2_flash = v4l2_flash_init(dev, fwnode, &led->flash, &qcom_v4l2_flash_ops, &v4l2_cfg); | 
|  | if (IS_ERR(v4l2_flash)) | 
|  | return PTR_ERR(v4l2_flash); | 
|  |  | 
|  | flash_data->v4l2_flash[flash_data->leds_count] = v4l2_flash; | 
|  | return 0; | 
|  | } | 
|  | # else | 
|  | static int | 
|  | qcom_flash_v4l2_init(struct device *dev, struct qcom_flash_led *led, struct fwnode_handle *fwnode) | 
|  | { | 
|  | return 0; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | static int qcom_flash_register_led_device(struct device *dev, | 
|  | struct fwnode_handle *node, struct qcom_flash_led *led) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = led->flash_data; | 
|  | struct led_init_data init_data; | 
|  | struct led_classdev_flash *flash = &led->flash; | 
|  | struct led_flash_setting *brightness, *timeout; | 
|  | u32 current_ua, timeout_us; | 
|  | u32 channels[4]; | 
|  | int i, rc, count; | 
|  | u8 torch_clamp; | 
|  |  | 
|  | count = fwnode_property_count_u32(node, "led-sources"); | 
|  | if (count <= 0) { | 
|  | dev_err(dev, "No led-sources specified\n"); | 
|  | return -ENODEV; | 
|  | } | 
|  |  | 
|  | if (count > flash_data->max_channels) { | 
|  | dev_err(dev, "led-sources count %u exceeds maximum channel count %u\n", | 
|  | count, flash_data->max_channels); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | rc = fwnode_property_read_u32_array(node, "led-sources", channels, count); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to read led-sources property, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | led->chan_count = count; | 
|  | led->chan_id = devm_kcalloc(dev, count, sizeof(u8), GFP_KERNEL); | 
|  | if (!led->chan_id) | 
|  | return -ENOMEM; | 
|  |  | 
|  | for (i = 0; i < count; i++) { | 
|  | if ((channels[i] == 0) || (channels[i] > flash_data->max_channels)) { | 
|  | dev_err(dev, "led-source out of HW support range [1-%u]\n", | 
|  | flash_data->max_channels); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | /* Make chan_id indexing from 0 */ | 
|  | led->chan_id[i] = channels[i] - 1; | 
|  | } | 
|  |  | 
|  | rc = fwnode_property_read_u32(node, "led-max-microamp", ¤t_ua); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to read led-max-microamp property, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | if (current_ua == 0) { | 
|  | dev_err(dev, "led-max-microamp shouldn't be 0\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | current_ua = min_t(u32, current_ua, TORCH_CURRENT_MAX_UA * led->chan_count); | 
|  | led->max_torch_current_ma = current_ua / UA_PER_MA; | 
|  |  | 
|  | torch_clamp = (current_ua / led->chan_count) / TORCH_IRES_UA; | 
|  | if (torch_clamp != 0) | 
|  | torch_clamp--; | 
|  |  | 
|  | flash_data->torch_clamp = max_t(u8, flash_data->torch_clamp, torch_clamp); | 
|  |  | 
|  | if (fwnode_property_present(node, "flash-max-microamp")) { | 
|  | flash->led_cdev.flags |= LED_DEV_CAP_FLASH; | 
|  |  | 
|  | rc = fwnode_property_read_u32(node, "flash-max-microamp", ¤t_ua); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to read flash-max-microamp property, rc=%d\n", | 
|  | rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | current_ua = min_t(u32, current_ua, FLASH_CURRENT_MAX_UA * led->chan_count); | 
|  | current_ua = min_t(u32, current_ua, FLASH_TOTAL_CURRENT_MAX_UA); | 
|  |  | 
|  | /* Initialize flash class LED device brightness settings */ | 
|  | brightness = &flash->brightness; | 
|  | brightness->min = brightness->step = FLASH_IRES_UA * led->chan_count; | 
|  | brightness->max = current_ua; | 
|  | brightness->val = min_t(u32, current_ua, FLASH_CURRENT_DEFAULT_UA); | 
|  |  | 
|  | led->max_flash_current_ma = current_ua / UA_PER_MA; | 
|  | led->flash_current_ma = brightness->val / UA_PER_MA; | 
|  |  | 
|  | rc = fwnode_property_read_u32(node, "flash-max-timeout-us", &timeout_us); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to read flash-max-timeout-us property, rc=%d\n", | 
|  | rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | timeout_us = min_t(u32, timeout_us, FLASH_TIMEOUT_MAX_US); | 
|  |  | 
|  | /* Initialize flash class LED device timeout settings */ | 
|  | timeout = &flash->timeout; | 
|  | timeout->min = timeout->step = FLASH_TIMEOUT_STEP_US; | 
|  | timeout->val = timeout->max = timeout_us; | 
|  |  | 
|  | led->max_timeout_ms = led->flash_timeout_ms = timeout_us / USEC_PER_MSEC; | 
|  |  | 
|  | flash->ops = &qcom_flash_ops; | 
|  | } | 
|  |  | 
|  | flash->led_cdev.brightness_set_blocking = qcom_flash_led_brightness_set; | 
|  |  | 
|  | init_data.fwnode = node; | 
|  | init_data.devicename = NULL; | 
|  | init_data.default_label = NULL; | 
|  | init_data.devname_mandatory = false; | 
|  |  | 
|  | rc = devm_led_classdev_flash_register_ext(dev, flash, &init_data); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Register flash LED classdev failed, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | return qcom_flash_v4l2_init(dev, led, node); | 
|  | } | 
|  |  | 
|  | static int qcom_flash_led_probe(struct platform_device *pdev) | 
|  | { | 
|  | struct qcom_flash_data *flash_data; | 
|  | struct qcom_flash_led *led; | 
|  | struct device *dev = &pdev->dev; | 
|  | struct regmap *regmap; | 
|  | struct reg_field *regs; | 
|  | int count, i, rc; | 
|  | u32 val, reg_base; | 
|  |  | 
|  | flash_data = devm_kzalloc(dev, sizeof(*flash_data), GFP_KERNEL); | 
|  | if (!flash_data) | 
|  | return -ENOMEM; | 
|  |  | 
|  | regmap = dev_get_regmap(dev->parent, NULL); | 
|  | if (!regmap) { | 
|  | dev_err(dev, "Failed to get parent regmap\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | rc = fwnode_property_read_u32(dev->fwnode, "reg", ®_base); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to get register base address, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | rc = regmap_read(regmap, reg_base + FLASH_TYPE_REG, &val); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Read flash LED module type failed, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | if (val != FLASH_TYPE_VAL) { | 
|  | dev_err(dev, "type %#x is not a flash LED module\n", val); | 
|  | return -ENODEV; | 
|  | } | 
|  |  | 
|  | rc = regmap_read(regmap, reg_base + FLASH_SUBTYPE_REG, &val); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Read flash LED module subtype failed, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | if (val == FLASH_SUBTYPE_3CH_PM8150_VAL) { | 
|  | flash_data->hw_type = QCOM_MVFLASH_3CH; | 
|  | flash_data->max_channels = 3; | 
|  | regs = devm_kmemdup(dev, mvflash_3ch_regs, sizeof(mvflash_3ch_regs), | 
|  | GFP_KERNEL); | 
|  | if (!regs) | 
|  | return -ENOMEM; | 
|  | } else if (val == FLASH_SUBTYPE_3CH_PMI8998_VAL) { | 
|  | flash_data->hw_type = QCOM_MVFLASH_3CH; | 
|  | flash_data->max_channels = 3; | 
|  | regs = devm_kmemdup(dev, mvflash_3ch_pmi8998_regs, | 
|  | sizeof(mvflash_3ch_pmi8998_regs), GFP_KERNEL); | 
|  | if (!regs) | 
|  | return -ENOMEM; | 
|  | } else if (val == FLASH_SUBTYPE_4CH_VAL) { | 
|  | flash_data->hw_type = QCOM_MVFLASH_4CH; | 
|  | flash_data->max_channels = 4; | 
|  | regs = devm_kmemdup(dev, mvflash_4ch_regs, sizeof(mvflash_4ch_regs), | 
|  | GFP_KERNEL); | 
|  | if (!regs) | 
|  | return -ENOMEM; | 
|  |  | 
|  | rc = regmap_read(regmap, reg_base + FLASH_REVISION_REG, &val); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to read flash LED module revision, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | flash_data->revision = val; | 
|  | } else { | 
|  | dev_err(dev, "flash LED subtype %#x is not yet supported\n", val); | 
|  | return -ENODEV; | 
|  | } | 
|  |  | 
|  | for (i = 0; i < REG_MAX_COUNT; i++) | 
|  | regs[i].reg += reg_base; | 
|  |  | 
|  | rc = devm_regmap_field_bulk_alloc(dev, regmap, flash_data->r_fields, regs, REG_MAX_COUNT); | 
|  | if (rc < 0) { | 
|  | dev_err(dev, "Failed to allocate regmap field, rc=%d\n", rc); | 
|  | return rc; | 
|  | } | 
|  | devm_kfree(dev, regs); /* devm_regmap_field_bulk_alloc() makes copies */ | 
|  |  | 
|  | platform_set_drvdata(pdev, flash_data); | 
|  | mutex_init(&flash_data->lock); | 
|  |  | 
|  | count = device_get_child_node_count(dev); | 
|  | if (count == 0 || count > flash_data->max_channels) { | 
|  | dev_err(dev, "No child or child count exceeds %d\n", flash_data->max_channels); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | flash_data->v4l2_flash = devm_kcalloc(dev, count, | 
|  | sizeof(*flash_data->v4l2_flash), GFP_KERNEL); | 
|  | if (!flash_data->v4l2_flash) | 
|  | return -ENOMEM; | 
|  |  | 
|  | device_for_each_child_node_scoped(dev, child) { | 
|  | led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); | 
|  | if (!led) { | 
|  | rc = -ENOMEM; | 
|  | goto release; | 
|  | } | 
|  |  | 
|  | led->flash_data = flash_data; | 
|  | rc = qcom_flash_register_led_device(dev, child, led); | 
|  | if (rc < 0) | 
|  | goto release; | 
|  |  | 
|  | flash_data->leds_count++; | 
|  | } | 
|  |  | 
|  | return regmap_field_write(flash_data->r_fields[REG_TORCH_CLAMP], flash_data->torch_clamp); | 
|  | release: | 
|  | while (flash_data->v4l2_flash[flash_data->leds_count] && flash_data->leds_count) | 
|  | v4l2_flash_release(flash_data->v4l2_flash[flash_data->leds_count--]); | 
|  | return rc; | 
|  | } | 
|  |  | 
|  | static void qcom_flash_led_remove(struct platform_device *pdev) | 
|  | { | 
|  | struct qcom_flash_data *flash_data = platform_get_drvdata(pdev); | 
|  |  | 
|  | while (flash_data->v4l2_flash[flash_data->leds_count] && flash_data->leds_count) | 
|  | v4l2_flash_release(flash_data->v4l2_flash[flash_data->leds_count--]); | 
|  |  | 
|  | mutex_destroy(&flash_data->lock); | 
|  | } | 
|  |  | 
|  | static const struct of_device_id qcom_flash_led_match_table[] = { | 
|  | { .compatible = "qcom,spmi-flash-led" }, | 
|  | { } | 
|  | }; | 
|  |  | 
|  | MODULE_DEVICE_TABLE(of, qcom_flash_led_match_table); | 
|  | static struct platform_driver qcom_flash_led_driver = { | 
|  | .driver = { | 
|  | .name = "leds-qcom-flash", | 
|  | .of_match_table = qcom_flash_led_match_table, | 
|  | }, | 
|  | .probe = qcom_flash_led_probe, | 
|  | .remove = qcom_flash_led_remove, | 
|  | }; | 
|  |  | 
|  | module_platform_driver(qcom_flash_led_driver); | 
|  |  | 
|  | MODULE_DESCRIPTION("QCOM Flash LED driver"); | 
|  | MODULE_LICENSE("GPL"); |