| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (c) 2024 Maya Matuszczyk <maccraft123mc@gmail.com> |
| * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. |
| */ |
| |
| #include <linux/bitfield.h> |
| #include <linux/bits.h> |
| #include <linux/device.h> |
| #include <linux/dev_printk.h> |
| #include <linux/err.h> |
| #include <linux/i2c.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/pm.h> |
| #include <linux/slab.h> |
| #include <linux/thermal.h> |
| |
| #define EC_SCI_EVT_READ_CMD 0x05 |
| #define EC_FW_VERSION_CMD 0x0e |
| #define EC_MODERN_STANDBY_CMD 0x23 |
| #define EC_FAN_DBG_CONTROL_CMD 0x30 |
| #define EC_SCI_EVT_CONTROL_CMD 0x35 |
| #define EC_THERMAL_CAP_CMD 0x42 |
| |
| #define EC_FW_VERSION_RESP_LEN 4 |
| #define EC_THERMAL_CAP_RESP_LEN 3 |
| #define EC_FAN_DEBUG_CMD_LEN 6 |
| #define EC_FAN_SPEED_DATA_SIZE 4 |
| |
| #define EC_MODERN_STANDBY_ENTER 0x01 |
| #define EC_MODERN_STANDBY_EXIT 0x00 |
| |
| #define EC_FAN_DEBUG_MODE_OFF 0 |
| #define EC_FAN_DEBUG_MODE_ON BIT(0) |
| #define EC_FAN_ON BIT(1) |
| #define EC_FAN_DEBUG_TYPE_PWM BIT(2) |
| #define EC_MAX_FAN_CNT 2 |
| #define EC_FAN_NAME_SIZE 20 |
| #define EC_FAN_MAX_PWM 255 |
| |
| enum qcom_ec_sci_events { |
| EC_FAN1_STATUS_CHANGE_EVT = 0x30, |
| EC_FAN2_STATUS_CHANGE_EVT, |
| EC_FAN1_SPEED_CHANGE_EVT, |
| EC_FAN2_SPEED_CHANGE_EVT, |
| EC_NEW_LUT_SET_EVT, |
| EC_FAN_PROFILE_SWITCH_EVT, |
| EC_THERMISTOR_1_THRESHOLD_CROSS_EVT, |
| EC_THERMISTOR_2_THRESHOLD_CROSS_EVT, |
| EC_THERMISTOR_3_THRESHOLD_CROSS_EVT, |
| /* Reserved: 0x39 - 0x3c/0x3f */ |
| EC_RECOVERED_FROM_RESET_EVT = 0x3d, |
| }; |
| |
| struct qcom_ec_version { |
| u8 main_version; |
| u8 sub_version; |
| u8 test_version; |
| }; |
| |
| struct qcom_ec_thermal_cap { |
| #define EC_THERMAL_FAN_CNT(x) (FIELD_GET(GENMASK(1, 0), (x))) |
| #define EC_THERMAL_FAN_TYPE(x) (FIELD_GET(GENMASK(4, 2), (x))) |
| #define EC_THERMAL_THERMISTOR_MASK(x) (FIELD_GET(GENMASK(7, 0), (x))) |
| u8 fan_cnt; |
| u8 fan_type; |
| u8 thermistor_mask; |
| }; |
| |
| struct qcom_ec_cooling_dev { |
| struct thermal_cooling_device *cdev; |
| struct device *parent_dev; |
| u8 fan_id; |
| u8 state; |
| }; |
| |
| struct qcom_ec { |
| struct qcom_ec_cooling_dev *ec_cdev; |
| struct qcom_ec_thermal_cap thermal_cap; |
| struct qcom_ec_version version; |
| struct i2c_client *client; |
| }; |
| |
| static int qcom_ec_read(struct qcom_ec *ec, u8 cmd, u8 resp_len, u8 *resp) |
| { |
| int ret; |
| |
| ret = i2c_smbus_read_i2c_block_data(ec->client, cmd, resp_len, resp); |
| if (ret < 0) |
| return ret; |
| else if (ret == 0 || ret == 0xff) |
| return -EOPNOTSUPP; |
| else if (ret != resp_len) |
| return -EIO; |
| |
| if (resp[0] != resp_len - 1) |
| return -EINVAL; |
| |
| return 0; |
| } |
| |
| /* |
| * EC Device Firmware Version: |
| * |
| * Read Response: |
| * ---------------------------------------------------------------------- |
| * | Offset | Name | Description | |
| * ---------------------------------------------------------------------- |
| * | 0x00 | Byte count | Number of bytes in response | |
| * | | | (excluding byte count) | |
| * ---------------------------------------------------------------------- |
| * | 0x01 | Test-version | Test-version of EC firmware | |
| * ---------------------------------------------------------------------- |
| * | 0x02 | Sub-version | Sub-version of EC firmware | |
| * ---------------------------------------------------------------------- |
| * | 0x03 | Main-version | Main-version of EC firmware | |
| * ---------------------------------------------------------------------- |
| * |
| */ |
| static int qcom_ec_read_fw_version(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| struct qcom_ec *ec = i2c_get_clientdata(client); |
| struct qcom_ec_version *version = &ec->version; |
| u8 resp[EC_FW_VERSION_RESP_LEN]; |
| int ret; |
| |
| ret = qcom_ec_read(ec, EC_FW_VERSION_CMD, EC_FW_VERSION_RESP_LEN, resp); |
| if (ret < 0) |
| return ret; |
| |
| version->main_version = resp[3]; |
| version->sub_version = resp[2]; |
| version->test_version = resp[1]; |
| |
| dev_dbg(dev, "EC Version %d.%d.%d\n", |
| version->main_version, version->sub_version, version->test_version); |
| |
| return 0; |
| } |
| |
| /* |
| * EC Device Thermal Capabilities: |
| * |
| * Read Response: |
| * ------------------------------------------------------------------------------ |
| * | Offset | Name | Description | |
| * ------------------------------------------------------------------------------ |
| * | 0x00 | Byte count | Number of bytes in response | |
| * | | | (excluding byte count) | |
| * ------------------------------------------------------------------------------ |
| * | 0x02 (LSB) | EC Thermal | Bit 0-1: Number of fans | |
| * | 0x03 | Capabilities | Bit 2-4: Type of fan | |
| * | | | Bit 5-6: Reserved | |
| * | | | Bit 7: Data Valid/Invalid | |
| * | | | (Valid - 1, Invalid - 0) | |
| * | | | Bit 8-15: Thermistor 0 - 7 presence | |
| * | | | (1 present, 0 absent) | |
| * ------------------------------------------------------------------------------ |
| * |
| */ |
| static int qcom_ec_thermal_capabilities(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| struct qcom_ec *ec = i2c_get_clientdata(client); |
| struct qcom_ec_thermal_cap *cap = &ec->thermal_cap; |
| u8 resp[EC_THERMAL_CAP_RESP_LEN]; |
| int ret; |
| |
| ret = qcom_ec_read(ec, EC_THERMAL_CAP_CMD, EC_THERMAL_CAP_RESP_LEN, resp); |
| if (ret < 0) |
| return ret; |
| |
| cap->fan_cnt = min(EC_MAX_FAN_CNT, EC_THERMAL_FAN_CNT(resp[1])); |
| cap->fan_type = EC_THERMAL_FAN_TYPE(resp[1]); |
| cap->thermistor_mask = EC_THERMAL_THERMISTOR_MASK(resp[2]); |
| |
| dev_dbg(dev, "Fan count: %d Fan Type: %d Thermistor Mask: %x\n", |
| cap->fan_cnt, cap->fan_type, cap->thermistor_mask); |
| |
| return 0; |
| } |
| |
| static irqreturn_t qcom_ec_irq(int irq, void *data) |
| { |
| struct qcom_ec *ec = data; |
| struct device *dev = &ec->client->dev; |
| int val; |
| |
| val = i2c_smbus_read_byte_data(ec->client, EC_SCI_EVT_READ_CMD); |
| if (val < 0) { |
| dev_err_ratelimited(dev, "Failed to read EC SCI Event: %d\n", val); |
| return IRQ_HANDLED; |
| } |
| |
| switch (val) { |
| case EC_FAN1_STATUS_CHANGE_EVT: |
| dev_dbg_ratelimited(dev, "Fan1 status changed\n"); |
| break; |
| case EC_FAN2_STATUS_CHANGE_EVT: |
| dev_dbg_ratelimited(dev, "Fan2 status changed\n"); |
| break; |
| case EC_FAN1_SPEED_CHANGE_EVT: |
| dev_dbg_ratelimited(dev, "Fan1 speed crossed low/high trip point\n"); |
| break; |
| case EC_FAN2_SPEED_CHANGE_EVT: |
| dev_dbg_ratelimited(dev, "Fan2 speed crossed low/high trip point\n"); |
| break; |
| case EC_NEW_LUT_SET_EVT: |
| dev_dbg_ratelimited(dev, "New LUT set\n"); |
| break; |
| case EC_FAN_PROFILE_SWITCH_EVT: |
| dev_dbg_ratelimited(dev, "FAN Profile switched\n"); |
| break; |
| case EC_THERMISTOR_1_THRESHOLD_CROSS_EVT: |
| dev_dbg_ratelimited(dev, "Thermistor 1 threshold crossed\n"); |
| break; |
| case EC_THERMISTOR_2_THRESHOLD_CROSS_EVT: |
| dev_dbg_ratelimited(dev, "Thermistor 2 threshold crossed\n"); |
| break; |
| case EC_THERMISTOR_3_THRESHOLD_CROSS_EVT: |
| dev_dbg_ratelimited(dev, "Thermistor 3 threshold crossed\n"); |
| break; |
| case EC_RECOVERED_FROM_RESET_EVT: |
| dev_dbg_ratelimited(dev, "EC recovered from reset\n"); |
| break; |
| default: |
| dev_notice_ratelimited(dev, "Unknown EC event: %d\n", val); |
| break; |
| } |
| |
| return IRQ_HANDLED; |
| } |
| |
| static int qcom_ec_sci_evt_control(struct device *dev, bool enable) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| |
| return i2c_smbus_write_byte_data(client, EC_SCI_EVT_CONTROL_CMD, enable ? 1 : 0); |
| } |
| |
| static int qcom_ec_fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) |
| { |
| *state = EC_FAN_MAX_PWM; |
| |
| return 0; |
| } |
| |
| static int qcom_ec_fan_get_cur_state(struct thermal_cooling_device *cdev, unsigned long *state) |
| { |
| struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; |
| |
| *state = ec_cdev->state; |
| |
| return 0; |
| } |
| |
| /* |
| * Fan Debug control command: |
| * |
| * Command Payload: |
| * -------------------------------------------------------------------------------------- |
| * | Offset | Name | Description | |
| * -------------------------------------------------------------------------------------- |
| * | 0x00 | Command | Fan control command | |
| * -------------------------------------------------------------------------------------- |
| * | 0x01 | Fan ID | 0x1 : Fan 1 | |
| * | | | 0x2 : Fan 2 | |
| * -------------------------------------------------------------------------------------- |
| * | 0x02 | Byte count = 4| Size of data to set fan speed | |
| * -------------------------------------------------------------------------------------- |
| * | 0x03 | Mode | Bit 0: Debug Mode On/Off (0 - OFF, 1 - ON ) | |
| * | | | Bit 1: Fan On/Off (0 - Off, 1 - ON) | |
| * | | | Bit 2: Debug Type (0 - RPM, 1 - PWM) | |
| * -------------------------------------------------------------------------------------- |
| * | 0x04 (LSB) | Speed in RPM | RPM value, if mode selected is RPM | |
| * | 0x05 | | | |
| * -------------------------------------------------------------------------------------- |
| * | 0x06 | Speed in PWM | PWM value, if mode selected is PWM (0 - 255) | |
| * ______________________________________________________________________________________ |
| * |
| */ |
| static int qcom_ec_fan_debug_mode_off(struct qcom_ec_cooling_dev *ec_cdev) |
| { |
| struct device *dev = ec_cdev->parent_dev; |
| struct i2c_client *client = to_i2c_client(dev); |
| u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, |
| EC_FAN_DEBUG_MODE_OFF, 0, 0, 0 }; |
| int ret; |
| |
| ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, |
| sizeof(request), request); |
| if (ret) { |
| dev_err(dev, "Failed to turn off fan%d debug mode: %d\n", |
| ec_cdev->fan_id, ret); |
| } |
| |
| return ret; |
| } |
| |
| static int qcom_ec_fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) |
| { |
| struct qcom_ec_cooling_dev *ec_cdev = cdev->devdata; |
| struct device *dev = ec_cdev->parent_dev; |
| struct i2c_client *client = to_i2c_client(dev); |
| u8 request[6] = { ec_cdev->fan_id, EC_FAN_SPEED_DATA_SIZE, |
| EC_FAN_DEBUG_MODE_ON | EC_FAN_ON | EC_FAN_DEBUG_TYPE_PWM, |
| 0, 0, state }; |
| int ret; |
| |
| ret = i2c_smbus_write_i2c_block_data(client, EC_FAN_DBG_CONTROL_CMD, |
| sizeof(request), request); |
| if (ret) { |
| dev_err(dev, "Failed to set fan pwm: %d\n", ret); |
| return ret; |
| } |
| |
| ec_cdev->state = state; |
| |
| return 0; |
| } |
| |
| static const struct thermal_cooling_device_ops qcom_ec_thermal_ops = { |
| .get_max_state = qcom_ec_fan_get_max_state, |
| .get_cur_state = qcom_ec_fan_get_cur_state, |
| .set_cur_state = qcom_ec_fan_set_cur_state, |
| }; |
| |
| static int qcom_ec_resume(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| |
| return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, |
| EC_MODERN_STANDBY_EXIT); |
| } |
| |
| static int qcom_ec_suspend(struct device *dev) |
| { |
| struct i2c_client *client = to_i2c_client(dev); |
| |
| return i2c_smbus_write_byte_data(client, EC_MODERN_STANDBY_CMD, |
| EC_MODERN_STANDBY_ENTER); |
| } |
| |
| static int qcom_ec_probe(struct i2c_client *client) |
| { |
| struct device *dev = &client->dev; |
| struct qcom_ec *ec; |
| unsigned int i; |
| int ret; |
| |
| ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL); |
| if (!ec) |
| return -ENOMEM; |
| |
| ec->client = client; |
| |
| ret = devm_request_threaded_irq(dev, client->irq, NULL, qcom_ec_irq, |
| IRQF_ONESHOT, "qcom_ec", ec); |
| if (ret < 0) |
| return ret; |
| |
| i2c_set_clientdata(client, ec); |
| |
| ret = qcom_ec_read_fw_version(dev); |
| if (ret < 0) |
| return dev_err_probe(dev, ret, "Failed to read EC firmware version\n"); |
| |
| ret = qcom_ec_sci_evt_control(dev, true); |
| if (ret < 0) |
| return dev_err_probe(dev, ret, "Failed to enable SCI events\n"); |
| |
| ret = qcom_ec_thermal_capabilities(dev); |
| if (ret < 0) |
| return dev_err_probe(dev, ret, "Failed to read thermal capabilities\n"); |
| |
| if (ec->thermal_cap.fan_cnt == 0) { |
| dev_warn(dev, FW_BUG "Failed to get fan count, firmware update required\n"); |
| return 0; |
| } |
| |
| ec->ec_cdev = devm_kcalloc(dev, ec->thermal_cap.fan_cnt, sizeof(*ec->ec_cdev), GFP_KERNEL); |
| if (!ec->ec_cdev) |
| return -ENOMEM; |
| |
| for (i = 0; i < ec->thermal_cap.fan_cnt; i++) { |
| struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; |
| char name[EC_FAN_NAME_SIZE]; |
| |
| scnprintf(name, sizeof(name), "qcom_ec_fan_%u", i); |
| ec_cdev->fan_id = i + 1; |
| ec_cdev->parent_dev = dev; |
| |
| ec_cdev->cdev = devm_thermal_of_child_cooling_device_register(dev, NULL, name, ec_cdev, |
| &qcom_ec_thermal_ops); |
| if (IS_ERR(ec_cdev->cdev)) { |
| return dev_err_probe(dev, PTR_ERR(ec_cdev->cdev), |
| "Failed to register fan%d cooling device\n", i); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static void qcom_ec_remove(struct i2c_client *client) |
| { |
| struct qcom_ec *ec = i2c_get_clientdata(client); |
| struct device *dev = &client->dev; |
| int ret; |
| |
| ret = qcom_ec_sci_evt_control(dev, false); |
| if (ret < 0) |
| dev_err(dev, "Failed to disable SCI events: %d\n", ret); |
| |
| for (int i = 0; i < ec->thermal_cap.fan_cnt; i++) { |
| struct qcom_ec_cooling_dev *ec_cdev = &ec->ec_cdev[i]; |
| |
| qcom_ec_fan_debug_mode_off(ec_cdev); |
| } |
| } |
| |
| static const struct of_device_id qcom_ec_of_match[] = { |
| { .compatible = "qcom,hamoa-crd-ec" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, qcom_ec_of_match); |
| |
| static const struct i2c_device_id qcom_ec_i2c_id_table[] = { |
| { "qcom-hamoa-ec", }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, qcom_ec_i2c_id_table); |
| |
| static DEFINE_SIMPLE_DEV_PM_OPS(qcom_ec_pm_ops, |
| qcom_ec_suspend, |
| qcom_ec_resume); |
| |
| static struct i2c_driver qcom_ec_i2c_driver = { |
| .driver = { |
| .name = "qcom-hamoa-ec", |
| .of_match_table = qcom_ec_of_match, |
| .pm = &qcom_ec_pm_ops, |
| }, |
| .probe = qcom_ec_probe, |
| .remove = qcom_ec_remove, |
| .id_table = qcom_ec_i2c_id_table, |
| }; |
| module_i2c_driver(qcom_ec_i2c_driver); |
| |
| MODULE_DESCRIPTION("QCOM Hamoa Embedded Controller"); |
| MODULE_LICENSE("GPL"); |