| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Driver for the Microchip PD692X0 PoE PSE Controller driver (I2C bus) | |
| * | |
| * Copyright (c) 2023 Bootlin, Kory Maincent <kory.maincent@bootlin.com> | |
| */ | |
| #include <linux/delay.h> | |
| #include <linux/firmware.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/i2c.h> | |
| #include <linux/module.h> | |
| #include <linux/of.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/pse-pd/pse.h> | |
| #include <linux/regulator/driver.h> | |
| #include <linux/regulator/machine.h> | |
| #define PD692X0_PSE_NAME "pd692x0_pse" | |
| #define PD692X0_MAX_PIS 48 | |
| #define PD692X0_MAX_MANAGERS 12 | |
| #define PD692X0_MAX_MANAGER_PORTS 8 | |
| #define PD692X0_MAX_HW_PORTS (PD692X0_MAX_MANAGERS * PD692X0_MAX_MANAGER_PORTS) | |
| #define PD69200_BT_PROD_VER 24 | |
| #define PD69210_BT_PROD_VER 26 | |
| #define PD69220_BT_PROD_VER 29 | |
| #define PD692X0_FW_MAJ_VER 3 | |
| #define PD692X0_FW_MIN_VER 5 | |
| #define PD692X0_FW_PATCH_VER 5 | |
| #define PD692X0_USER_BYTE 42 | |
| enum pd692x0_fw_state { | |
| PD692X0_FW_UNKNOWN, | |
| PD692X0_FW_OK, | |
| PD692X0_FW_BROKEN, | |
| PD692X0_FW_NEED_UPDATE, | |
| PD692X0_FW_PREPARE, | |
| PD692X0_FW_WRITE, | |
| PD692X0_FW_COMPLETE, | |
| }; | |
| struct pd692x0_msg { | |
| u8 key; | |
| u8 echo; | |
| u8 sub[3]; | |
| u8 data[8]; | |
| __be16 chksum; | |
| } __packed; | |
| struct pd692x0_msg_ver { | |
| u8 prod; | |
| u8 maj_sw_ver; | |
| u8 min_sw_ver; | |
| u8 pa_sw_ver; | |
| u8 param; | |
| u8 build; | |
| }; | |
| enum { | |
| PD692X0_KEY_CMD, | |
| PD692X0_KEY_PRG, | |
| PD692X0_KEY_REQ, | |
| PD692X0_KEY_TLM, | |
| PD692X0_KEY_TEST, | |
| PD692X0_KEY_REPORT = 0x52 | |
| }; | |
| enum { | |
| PD692X0_MSG_RESET, | |
| PD692X0_MSG_GET_SYS_STATUS, | |
| PD692X0_MSG_GET_SW_VER, | |
| PD692X0_MSG_SET_TMP_PORT_MATRIX, | |
| PD692X0_MSG_PRG_PORT_MATRIX, | |
| PD692X0_MSG_SET_PORT_PARAM, | |
| PD692X0_MSG_GET_PORT_STATUS, | |
| PD692X0_MSG_DOWNLOAD_CMD, | |
| PD692X0_MSG_GET_PORT_CLASS, | |
| PD692X0_MSG_GET_PORT_MEAS, | |
| PD692X0_MSG_GET_PORT_PARAM, | |
| PD692X0_MSG_GET_POWER_BANK, | |
| PD692X0_MSG_SET_POWER_BANK, | |
| PD692X0_MSG_SET_USER_BYTE, | |
| /* add new message above here */ | |
| PD692X0_MSG_CNT | |
| }; | |
| struct pd692x0_matrix { | |
| u8 hw_port_a; | |
| u8 hw_port_b; | |
| }; | |
| struct pd692x0_priv { | |
| struct i2c_client *client; | |
| struct pse_controller_dev pcdev; | |
| struct device_node *np; | |
| enum pd692x0_fw_state fw_state; | |
| struct fw_upload *fwl; | |
| bool cancel_request; | |
| u8 msg_id; | |
| bool last_cmd_key; | |
| unsigned long last_cmd_key_time; | |
| bool cfg_saved; | |
| enum ethtool_c33_pse_admin_state admin_state[PD692X0_MAX_PIS]; | |
| struct regulator_dev *manager_reg[PD692X0_MAX_MANAGERS]; | |
| int manager_pw_budget[PD692X0_MAX_MANAGERS]; | |
| int nmanagers; | |
| struct pd692x0_matrix *port_matrix; | |
| }; | |
| /* Template list of communication messages. The non-null bytes defined here | |
| * constitute the fixed portion of the messages. The remaining bytes will | |
| * be configured later within the functions. Refer to the "PD692x0 BT Serial | |
| * Communication Protocol User Guide" for comprehensive details on messages | |
| * content. | |
| */ | |
| static const struct pd692x0_msg pd692x0_msg_template_list[PD692X0_MSG_CNT] = { | |
| [PD692X0_MSG_RESET] = { | |
| .key = PD692X0_KEY_CMD, | |
| .sub = {0x07, 0x55, 0x00}, | |
| .data = {0x55, 0x00, 0x55, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_SYS_STATUS] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x07, 0xd0, 0x4e}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_SW_VER] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x07, 0x1e, 0x21}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_SET_TMP_PORT_MATRIX] = { | |
| .key = PD692X0_KEY_CMD, | |
| .sub = {0x05, 0x43}, | |
| .data = { 0, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_PRG_PORT_MATRIX] = { | |
| .key = PD692X0_KEY_CMD, | |
| .sub = {0x07, 0x43, 0x4e}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_SET_PORT_PARAM] = { | |
| .key = PD692X0_KEY_CMD, | |
| .sub = {0x05, 0xc0}, | |
| .data = { 0xf, 0xff, 0xff, 0xff, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_PORT_STATUS] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x05, 0xc1}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_DOWNLOAD_CMD] = { | |
| .key = PD692X0_KEY_PRG, | |
| .sub = {0xff, 0x99, 0x15}, | |
| .data = {0x16, 0x16, 0x99, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_PORT_CLASS] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x05, 0xc4}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_PORT_MEAS] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x05, 0xc5}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_PORT_PARAM] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x05, 0xc0}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_GET_POWER_BANK] = { | |
| .key = PD692X0_KEY_REQ, | |
| .sub = {0x07, 0x0b, 0x57}, | |
| .data = { 0, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| [PD692X0_MSG_SET_POWER_BANK] = { | |
| .key = PD692X0_KEY_CMD, | |
| .sub = {0x07, 0x0b, 0x57}, | |
| }, | |
| [PD692X0_MSG_SET_USER_BYTE] = { | |
| .key = PD692X0_KEY_PRG, | |
| .sub = {0x41, PD692X0_USER_BYTE, 0x4e}, | |
| .data = {0x4e, 0x4e, 0x4e, 0x4e, | |
| 0x4e, 0x4e, 0x4e, 0x4e}, | |
| }, | |
| }; | |
| static u8 pd692x0_build_msg(struct pd692x0_msg *msg, u8 echo) | |
| { | |
| u8 *data = (u8 *)msg; | |
| u16 chksum = 0; | |
| int i; | |
| msg->echo = echo++; | |
| if (echo == 0xff) | |
| echo = 0; | |
| for (i = 0; i < sizeof(*msg) - sizeof(msg->chksum); i++) | |
| chksum += data[i]; | |
| msg->chksum = cpu_to_be16(chksum); | |
| return echo; | |
| } | |
| static int pd692x0_send_msg(struct pd692x0_priv *priv, struct pd692x0_msg *msg) | |
| { | |
| const struct i2c_client *client = priv->client; | |
| int ret; | |
| if (msg->key == PD692X0_KEY_CMD && priv->last_cmd_key) { | |
| int cmd_msleep; | |
| cmd_msleep = 30 - jiffies_to_msecs(jiffies - priv->last_cmd_key_time); | |
| if (cmd_msleep > 0) | |
| msleep(cmd_msleep); | |
| } | |
| /* Add echo and checksum bytes to the message */ | |
| priv->msg_id = pd692x0_build_msg(msg, priv->msg_id); | |
| ret = i2c_master_send(client, (u8 *)msg, sizeof(*msg)); | |
| if (ret != sizeof(*msg)) | |
| return -EIO; | |
| return 0; | |
| } | |
| static int pd692x0_reset(struct pd692x0_priv *priv) | |
| { | |
| const struct i2c_client *client = priv->client; | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_RESET]; | |
| ret = pd692x0_send_msg(priv, &msg); | |
| if (ret) { | |
| dev_err(&client->dev, | |
| "Failed to reset the controller (%pe)\n", ERR_PTR(ret)); | |
| return ret; | |
| } | |
| msleep(30); | |
| ret = i2c_master_recv(client, (u8 *)&buf, sizeof(buf)); | |
| if (ret != sizeof(buf)) | |
| return ret < 0 ? ret : -EIO; | |
| /* Is the reply a successful report message */ | |
| if (buf.key != PD692X0_KEY_REPORT || buf.sub[0] || buf.sub[1]) | |
| return -EIO; | |
| msleep(300); | |
| ret = i2c_master_recv(client, (u8 *)&buf, sizeof(buf)); | |
| if (ret != sizeof(buf)) | |
| return ret < 0 ? ret : -EIO; | |
| /* Is the boot status without error */ | |
| if (buf.key != 0x03 || buf.echo != 0xff || buf.sub[0] & 0x1) { | |
| dev_err(&client->dev, "PSE controller error\n"); | |
| return -EIO; | |
| } | |
| return 0; | |
| } | |
| static bool pd692x0_try_recv_msg(const struct i2c_client *client, | |
| struct pd692x0_msg *msg, | |
| struct pd692x0_msg *buf) | |
| { | |
| /* Wait 30ms before readback as mandated by the protocol */ | |
| msleep(30); | |
| memset(buf, 0, sizeof(*buf)); | |
| i2c_master_recv(client, (u8 *)buf, sizeof(*buf)); | |
| if (buf->key) | |
| return 0; | |
| msleep(100); | |
| memset(buf, 0, sizeof(*buf)); | |
| i2c_master_recv(client, (u8 *)buf, sizeof(*buf)); | |
| if (buf->key) | |
| return 0; | |
| return 1; | |
| } | |
| /* Implementation of I2C communication, specifically addressing scenarios | |
| * involving communication loss. Refer to the "Synchronization During | |
| * Communication Loss" section in the Communication Protocol document for | |
| * further details. | |
| */ | |
| static int pd692x0_recv_msg(struct pd692x0_priv *priv, | |
| struct pd692x0_msg *msg, | |
| struct pd692x0_msg *buf) | |
| { | |
| const struct i2c_client *client = priv->client; | |
| int ret; | |
| ret = pd692x0_try_recv_msg(client, msg, buf); | |
| if (!ret) | |
| goto out_success; | |
| dev_warn(&client->dev, | |
| "Communication lost, rtnl is locked until communication is back!"); | |
| ret = pd692x0_send_msg(priv, msg); | |
| if (ret) | |
| return ret; | |
| ret = pd692x0_try_recv_msg(client, msg, buf); | |
| if (!ret) | |
| goto out_success2; | |
| msleep(10000); | |
| ret = pd692x0_send_msg(priv, msg); | |
| if (ret) | |
| return ret; | |
| ret = pd692x0_try_recv_msg(client, msg, buf); | |
| if (!ret) | |
| goto out_success2; | |
| return pd692x0_reset(priv); | |
| out_success2: | |
| dev_warn(&client->dev, "Communication is back, rtnl is unlocked!"); | |
| out_success: | |
| if (msg->key == PD692X0_KEY_CMD) { | |
| priv->last_cmd_key = true; | |
| priv->last_cmd_key_time = jiffies; | |
| } else { | |
| priv->last_cmd_key = false; | |
| } | |
| return 0; | |
| } | |
| static int pd692x0_sendrecv_msg(struct pd692x0_priv *priv, | |
| struct pd692x0_msg *msg, | |
| struct pd692x0_msg *buf) | |
| { | |
| struct device *dev = &priv->client->dev; | |
| int ret; | |
| ret = pd692x0_send_msg(priv, msg); | |
| if (ret) | |
| return ret; | |
| ret = pd692x0_recv_msg(priv, msg, buf); | |
| if (ret) | |
| return ret; | |
| if (msg->echo != buf->echo) { | |
| dev_err(dev, | |
| "Wrong match in message ID, expect %d received %d.\n", | |
| msg->echo, buf->echo); | |
| return -EIO; | |
| } | |
| /* If the reply is a report message is it successful */ | |
| if (buf->key == PD692X0_KEY_REPORT && | |
| (buf->sub[0] || buf->sub[1])) { | |
| return -EIO; | |
| } | |
| return 0; | |
| } | |
| static struct pd692x0_priv *to_pd692x0_priv(struct pse_controller_dev *pcdev) | |
| { | |
| return container_of(pcdev, struct pd692x0_priv, pcdev); | |
| } | |
| static int pd692x0_fw_unavailable(struct pd692x0_priv *priv) | |
| { | |
| switch (priv->fw_state) { | |
| case PD692X0_FW_OK: | |
| return 0; | |
| case PD692X0_FW_PREPARE: | |
| case PD692X0_FW_WRITE: | |
| case PD692X0_FW_COMPLETE: | |
| dev_err(&priv->client->dev, "Firmware update in progress!\n"); | |
| return -EBUSY; | |
| case PD692X0_FW_BROKEN: | |
| case PD692X0_FW_NEED_UPDATE: | |
| default: | |
| dev_err(&priv->client->dev, | |
| "Firmware issue. Please update it!\n"); | |
| return -EOPNOTSUPP; | |
| } | |
| } | |
| static int pd692x0_pi_enable(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| if (priv->admin_state[id] == ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED) | |
| return 0; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_PORT_PARAM]; | |
| msg.data[0] = 0x1; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| priv->admin_state[id] = ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED; | |
| return 0; | |
| } | |
| static int pd692x0_pi_disable(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| if (priv->admin_state[id] == ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED) | |
| return 0; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_PORT_PARAM]; | |
| msg.data[0] = 0x0; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| priv->admin_state[id] = ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED; | |
| return 0; | |
| } | |
| struct pd692x0_pse_ext_state_mapping { | |
| u32 status_code; | |
| enum ethtool_c33_pse_ext_state pse_ext_state; | |
| u32 pse_ext_substate; | |
| }; | |
| static const struct pd692x0_pse_ext_state_mapping | |
| pd692x0_pse_ext_state_map[] = { | |
| {0x06, ETHTOOL_C33_PSE_EXT_STATE_OPTION_VPORT_LIM, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OPTION_VPORT_LIM_HIGH_VOLTAGE}, | |
| {0x07, ETHTOOL_C33_PSE_EXT_STATE_OPTION_VPORT_LIM, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OPTION_VPORT_LIM_LOW_VOLTAGE}, | |
| {0x08, ETHTOOL_C33_PSE_EXT_STATE_MR_PSE_ENABLE, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_MR_PSE_ENABLE_DISABLE_PIN_ACTIVE}, | |
| {0x0C, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_NON_EXISTING_PORT}, | |
| {0x11, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_UNDEFINED_PORT}, | |
| {0x12, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_INTERNAL_HW_FAULT}, | |
| {0x1B, ETHTOOL_C33_PSE_EXT_STATE_OPTION_DETECT_TED, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OPTION_DETECT_TED_DET_IN_PROCESS}, | |
| {0x1C, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_UNKNOWN_PORT_STATUS}, | |
| {0x1E, ETHTOOL_C33_PSE_EXT_STATE_MR_MPS_VALID, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_MR_MPS_VALID_DETECTED_UNDERLOAD}, | |
| {0x1F, ETHTOOL_C33_PSE_EXT_STATE_OVLD_DETECTED, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OVLD_DETECTED_OVERLOAD}, | |
| {0x20, ETHTOOL_C33_PSE_EXT_STATE_POWER_NOT_AVAILABLE, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_POWER_NOT_AVAILABLE_BUDGET_EXCEEDED}, | |
| {0x21, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_INTERNAL_HW_FAULT}, | |
| {0x22, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_CONFIG_CHANGE}, | |
| {0x24, ETHTOOL_C33_PSE_EXT_STATE_OPTION_VPORT_LIM, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OPTION_VPORT_LIM_VOLTAGE_INJECTION}, | |
| {0x25, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_UNKNOWN_PORT_STATUS}, | |
| {0x34, ETHTOOL_C33_PSE_EXT_STATE_SHORT_DETECTED, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_SHORT_DETECTED_SHORT_CONDITION}, | |
| {0x35, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_DETECTED_OVER_TEMP}, | |
| {0x36, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_DETECTED_OVER_TEMP}, | |
| {0x37, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_UNKNOWN_PORT_STATUS}, | |
| {0x3C, ETHTOOL_C33_PSE_EXT_STATE_POWER_NOT_AVAILABLE, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_POWER_NOT_AVAILABLE_PORT_PW_LIMIT_EXCEEDS_CONTROLLER_BUDGET}, | |
| {0x3D, ETHTOOL_C33_PSE_EXT_STATE_POWER_NOT_AVAILABLE, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_POWER_NOT_AVAILABLE_PD_REQUEST_EXCEEDS_PORT_LIMIT}, | |
| {0x41, ETHTOOL_C33_PSE_EXT_STATE_POWER_NOT_AVAILABLE, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_POWER_NOT_AVAILABLE_HW_PW_LIMIT}, | |
| {0x43, ETHTOOL_C33_PSE_EXT_STATE_ERROR_CONDITION, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_ERROR_CONDITION_UNKNOWN_PORT_STATUS}, | |
| {0xA7, ETHTOOL_C33_PSE_EXT_STATE_OPTION_DETECT_TED, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_OPTION_DETECT_TED_CONNECTION_CHECK_ERROR}, | |
| {0xA8, ETHTOOL_C33_PSE_EXT_STATE_MR_MPS_VALID, | |
| ETHTOOL_C33_PSE_EXT_SUBSTATE_MR_MPS_VALID_CONNECTION_OPEN}, | |
| { /* sentinel */ } | |
| }; | |
| static int | |
| pd692x0_pi_get_ext_state(struct pse_controller_dev *pcdev, int id, | |
| struct pse_ext_state_info *ext_state_info) | |
| { | |
| struct ethtool_c33_pse_ext_state_info *c33_ext_state_info; | |
| const struct pd692x0_pse_ext_state_mapping *ext_state_map; | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| c33_ext_state_info = &ext_state_info->c33_ext_state_info; | |
| ext_state_map = pd692x0_pse_ext_state_map; | |
| while (ext_state_map->status_code) { | |
| if (ext_state_map->status_code == buf.sub[0]) { | |
| c33_ext_state_info->c33_pse_ext_state = ext_state_map->pse_ext_state; | |
| c33_ext_state_info->__c33_pse_ext_substate = ext_state_map->pse_ext_substate; | |
| return 0; | |
| } | |
| ext_state_map++; | |
| } | |
| return 0; | |
| } | |
| struct pd692x0_class_pw { | |
| int class; | |
| int class_cfg_value; | |
| int class_pw; | |
| int max_added_class_pw; | |
| }; | |
| #define PD692X0_CLASS_PW_TABLE_SIZE 4 | |
| /* 4/2 pairs class configuration power table in compliance mode. | |
| * Need to be arranged in ascending order of power support. | |
| */ | |
| static const struct pd692x0_class_pw | |
| pd692x0_class_pw_table[PD692X0_CLASS_PW_TABLE_SIZE] = { | |
| {.class = 3, .class_cfg_value = 0x3, .class_pw = 15000, .max_added_class_pw = 3100}, | |
| {.class = 4, .class_cfg_value = 0x2, .class_pw = 30000, .max_added_class_pw = 8000}, | |
| {.class = 6, .class_cfg_value = 0x1, .class_pw = 60000, .max_added_class_pw = 5000}, | |
| {.class = 8, .class_cfg_value = 0x0, .class_pw = 90000, .max_added_class_pw = 7500}, | |
| }; | |
| static int pd692x0_pi_get_pw_from_table(int op_mode, int added_pw) | |
| { | |
| const struct pd692x0_class_pw *pw_table; | |
| int i; | |
| pw_table = pd692x0_class_pw_table; | |
| for (i = 0; i < PD692X0_CLASS_PW_TABLE_SIZE; i++, pw_table++) { | |
| if (pw_table->class_cfg_value == op_mode) | |
| return pw_table->class_pw + added_pw * 100; | |
| } | |
| return -ERANGE; | |
| } | |
| static int pd692x0_pi_set_pw_from_table(struct device *dev, | |
| struct pd692x0_msg *msg, int pw) | |
| { | |
| const struct pd692x0_class_pw *pw_table; | |
| int i; | |
| pw_table = pd692x0_class_pw_table; | |
| if (pw < pw_table->class_pw) { | |
| dev_err(dev, | |
| "Power limit %dmW not supported. Ranges minimal available: [%d-%d]\n", | |
| pw, | |
| pw_table->class_pw, | |
| pw_table->class_pw + pw_table->max_added_class_pw); | |
| return -ERANGE; | |
| } | |
| for (i = 0; i < PD692X0_CLASS_PW_TABLE_SIZE; i++, pw_table++) { | |
| if (pw > (pw_table->class_pw + pw_table->max_added_class_pw)) | |
| continue; | |
| if (pw < pw_table->class_pw) { | |
| dev_err(dev, | |
| "Power limit %dmW not supported. Ranges available: [%d-%d] or [%d-%d]\n", | |
| pw, | |
| (pw_table - 1)->class_pw, | |
| (pw_table - 1)->class_pw + (pw_table - 1)->max_added_class_pw, | |
| pw_table->class_pw, | |
| pw_table->class_pw + pw_table->max_added_class_pw); | |
| return -ERANGE; | |
| } | |
| msg->data[2] = pw_table->class_cfg_value; | |
| msg->data[3] = (pw - pw_table->class_pw) / 100; | |
| return 0; | |
| } | |
| pw_table--; | |
| dev_warn(dev, | |
| "Power limit %dmW not supported. Set to highest power limit %dmW\n", | |
| pw, pw_table->class_pw + pw_table->max_added_class_pw); | |
| msg->data[2] = pw_table->class_cfg_value; | |
| msg->data[3] = pw_table->max_added_class_pw / 100; | |
| return 0; | |
| } | |
| static int | |
| pd692x0_pi_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id, | |
| struct pse_pw_limit_ranges *pw_limit_ranges) | |
| { | |
| struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges; | |
| const struct pd692x0_class_pw *pw_table; | |
| int i; | |
| pw_table = pd692x0_class_pw_table; | |
| c33_pw_limit_ranges = kzalloc_objs(*c33_pw_limit_ranges, | |
| PD692X0_CLASS_PW_TABLE_SIZE); | |
| if (!c33_pw_limit_ranges) | |
| return -ENOMEM; | |
| for (i = 0; i < PD692X0_CLASS_PW_TABLE_SIZE; i++, pw_table++) { | |
| c33_pw_limit_ranges[i].min = pw_table->class_pw; | |
| c33_pw_limit_ranges[i].max = pw_table->class_pw + | |
| pw_table->max_added_class_pw; | |
| } | |
| pw_limit_ranges->c33_pw_limit_ranges = c33_pw_limit_ranges; | |
| return i; | |
| } | |
| static int | |
| pd692x0_pi_get_admin_state(struct pse_controller_dev *pcdev, int id, | |
| struct pse_admin_state *admin_state) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| if (buf.sub[1]) | |
| admin_state->c33_admin_state = | |
| ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED; | |
| else | |
| admin_state->c33_admin_state = | |
| ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED; | |
| priv->admin_state[id] = admin_state->c33_admin_state; | |
| return 0; | |
| } | |
| static int | |
| pd692x0_pi_get_pw_status(struct pse_controller_dev *pcdev, int id, | |
| struct pse_pw_status *pw_status) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| /* Compare Port Status (Communication Protocol Document par. 7.1) */ | |
| if ((buf.sub[0] & 0xf0) == 0x80 || (buf.sub[0] & 0xf0) == 0x90) | |
| pw_status->c33_pw_status = | |
| ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING; | |
| else if (buf.sub[0] == 0x1b || buf.sub[0] == 0x22) | |
| pw_status->c33_pw_status = | |
| ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING; | |
| else if (buf.sub[0] == 0x12) | |
| pw_status->c33_pw_status = | |
| ETHTOOL_C33_PSE_PW_D_STATUS_FAULT; | |
| else | |
| pw_status->c33_pw_status = | |
| ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED; | |
| return 0; | |
| } | |
| static int | |
| pd692x0_pi_get_pw_class(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| u32 class; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_CLASS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| class = buf.data[3] >> 4; | |
| if (class <= 8) | |
| return class; | |
| return 0; | |
| } | |
| static int | |
| pd692x0_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_STATUS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| return (buf.data[0] << 4 | buf.data[1]) * 100; | |
| } | |
| static int | |
| pd692x0_pi_get_prio(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_PARAM]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| if (!buf.data[2] || buf.data[2] > pcdev->pis_prio_max + 1) | |
| return -ERANGE; | |
| /* PSE core priority start at 0 */ | |
| return buf.data[2] - 1; | |
| } | |
| static struct pd692x0_msg_ver pd692x0_get_sw_version(struct pd692x0_priv *priv) | |
| { | |
| struct device *dev = &priv->client->dev; | |
| struct pd692x0_msg msg, buf = {0}; | |
| struct pd692x0_msg_ver ver = {0}; | |
| int ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_SW_VER]; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) { | |
| dev_err(dev, "Failed to get PSE version (%pe)\n", ERR_PTR(ret)); | |
| return ver; | |
| } | |
| /* Extract version from the message */ | |
| ver.prod = buf.sub[2]; | |
| ver.maj_sw_ver = (buf.data[0] << 8 | buf.data[1]) / 100; | |
| ver.min_sw_ver = ((buf.data[0] << 8 | buf.data[1]) / 10) % 10; | |
| ver.pa_sw_ver = (buf.data[0] << 8 | buf.data[1]) % 10; | |
| ver.param = buf.data[2]; | |
| ver.build = buf.data[3]; | |
| return ver; | |
| } | |
| struct pd692x0_manager { | |
| struct device_node *port_node[PD692X0_MAX_MANAGER_PORTS]; | |
| struct device_node *node; | |
| int nports; | |
| }; | |
| static int | |
| pd692x0_of_get_ports_manager(struct pd692x0_priv *priv, | |
| struct pd692x0_manager *manager, | |
| struct device_node *np) | |
| { | |
| struct device_node *node; | |
| int ret, nports, i; | |
| nports = 0; | |
| for_each_child_of_node(np, node) { | |
| u32 port; | |
| if (!of_node_name_eq(node, "port")) | |
| continue; | |
| ret = of_property_read_u32(node, "reg", &port); | |
| if (ret) | |
| goto out; | |
| if (port >= PD692X0_MAX_MANAGER_PORTS || port != nports) { | |
| dev_err(&priv->client->dev, | |
| "wrong number or order of manager ports (%d)\n", | |
| port); | |
| ret = -EINVAL; | |
| goto out; | |
| } | |
| of_node_get(node); | |
| manager->port_node[port] = node; | |
| nports++; | |
| } | |
| manager->nports = nports; | |
| return 0; | |
| out: | |
| for (i = 0; i < nports; i++) { | |
| of_node_put(manager->port_node[i]); | |
| manager->port_node[i] = NULL; | |
| } | |
| of_node_put(node); | |
| return ret; | |
| } | |
| static int | |
| pd692x0_of_get_managers(struct pd692x0_priv *priv, | |
| struct pd692x0_manager *manager) | |
| { | |
| struct device_node *managers_node, *node; | |
| int ret, nmanagers, i, j; | |
| if (!priv->np) | |
| return -EINVAL; | |
| nmanagers = 0; | |
| managers_node = of_get_child_by_name(priv->np, "managers"); | |
| if (!managers_node) | |
| return -EINVAL; | |
| for_each_child_of_node(managers_node, node) { | |
| u32 manager_id; | |
| if (!of_node_name_eq(node, "manager")) | |
| continue; | |
| ret = of_property_read_u32(node, "reg", &manager_id); | |
| if (ret) | |
| goto out; | |
| if (manager_id >= PD692X0_MAX_MANAGERS || | |
| manager_id != nmanagers) { | |
| dev_err(&priv->client->dev, | |
| "wrong number or order of managers (%d)\n", | |
| manager_id); | |
| ret = -EINVAL; | |
| goto out; | |
| } | |
| ret = pd692x0_of_get_ports_manager(priv, &manager[manager_id], | |
| node); | |
| if (ret) | |
| goto out; | |
| of_node_get(node); | |
| manager[manager_id].node = node; | |
| nmanagers++; | |
| } | |
| of_node_put(managers_node); | |
| priv->nmanagers = nmanagers; | |
| return 0; | |
| out: | |
| for (i = 0; i < nmanagers; i++) { | |
| for (j = 0; j < manager[i].nports; j++) { | |
| of_node_put(manager[i].port_node[j]); | |
| manager[i].port_node[j] = NULL; | |
| } | |
| of_node_put(manager[i].node); | |
| manager[i].node = NULL; | |
| } | |
| of_node_put(node); | |
| of_node_put(managers_node); | |
| return ret; | |
| } | |
| static const struct regulator_ops dummy_ops; | |
| static struct regulator_dev * | |
| pd692x0_register_manager_regulator(struct device *dev, char *reg_name, | |
| struct device_node *node) | |
| { | |
| struct regulator_init_data *rinit_data; | |
| struct regulator_config rconfig = {0}; | |
| struct regulator_desc *rdesc; | |
| struct regulator_dev *rdev; | |
| rinit_data = devm_kzalloc(dev, sizeof(*rinit_data), | |
| GFP_KERNEL); | |
| if (!rinit_data) | |
| return ERR_PTR(-ENOMEM); | |
| rdesc = devm_kzalloc(dev, sizeof(*rdesc), GFP_KERNEL); | |
| if (!rdesc) | |
| return ERR_PTR(-ENOMEM); | |
| rdesc->name = reg_name; | |
| rdesc->type = REGULATOR_VOLTAGE; | |
| rdesc->ops = &dummy_ops; | |
| rdesc->owner = THIS_MODULE; | |
| rinit_data->supply_regulator = "vmain"; | |
| rconfig.dev = dev; | |
| rconfig.init_data = rinit_data; | |
| rconfig.of_node = node; | |
| rdev = devm_regulator_register(dev, rdesc, &rconfig); | |
| if (IS_ERR(rdev)) { | |
| dev_err_probe(dev, PTR_ERR(rdev), | |
| "Failed to register regulator\n"); | |
| return rdev; | |
| } | |
| return rdev; | |
| } | |
| static int | |
| pd692x0_register_managers_regulator(struct pd692x0_priv *priv, | |
| const struct pd692x0_manager *manager) | |
| { | |
| struct device *dev = &priv->client->dev; | |
| size_t reg_name_len; | |
| int i; | |
| /* Each regulator name len is dev name + 12 char + | |
| * int max digit number (10) + 1 | |
| */ | |
| reg_name_len = strlen(dev_name(dev)) + 23; | |
| for (i = 0; i < priv->nmanagers; i++) { | |
| static const char * const regulators[] = { "vaux5", "vaux3p3" }; | |
| struct regulator_dev *rdev; | |
| char *reg_name; | |
| int ret; | |
| reg_name = devm_kzalloc(dev, reg_name_len, GFP_KERNEL); | |
| if (!reg_name) | |
| return -ENOMEM; | |
| snprintf(reg_name, 26, "pse-%s-manager%d", dev_name(dev), i); | |
| rdev = pd692x0_register_manager_regulator(dev, reg_name, | |
| manager[i].node); | |
| if (IS_ERR(rdev)) | |
| return PTR_ERR(rdev); | |
| /* VMAIN is described as main supply for the manager. | |
| * Add other VAUX power supplies and link them to the | |
| * virtual device rdev->dev. | |
| */ | |
| ret = devm_regulator_bulk_get_enable(&rdev->dev, | |
| ARRAY_SIZE(regulators), | |
| regulators); | |
| if (ret) | |
| return dev_err_probe(&rdev->dev, ret, | |
| "Failed to enable regulators\n"); | |
| priv->manager_reg[i] = rdev; | |
| } | |
| return 0; | |
| } | |
| static int | |
| pd692x0_conf_manager_power_budget(struct pd692x0_priv *priv, int id) | |
| { | |
| struct pd692x0_msg msg, buf; | |
| int ret, pw_mW; | |
| pw_mW = priv->manager_pw_budget[id] / 1000; | |
| if (!pw_mW) | |
| return 0; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_POWER_BANK]; | |
| msg.data[0] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_POWER_BANK]; | |
| msg.data[0] = id; | |
| msg.data[1] = pw_mW >> 8; | |
| msg.data[2] = pw_mW & 0xff; | |
| msg.data[3] = buf.sub[2]; | |
| msg.data[4] = buf.data[0]; | |
| msg.data[5] = buf.data[1]; | |
| msg.data[6] = buf.data[2]; | |
| msg.data[7] = buf.data[3]; | |
| return pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| } | |
| static int | |
| pd692x0_req_managers_pw_budget(struct pd692x0_priv *priv) | |
| { | |
| int i, ret; | |
| for (i = 0; i < priv->nmanagers; i++) { | |
| struct regulator *supply = priv->manager_reg[i]->supply; | |
| int pw_budget; | |
| pw_budget = regulator_get_unclaimed_power_budget(supply); | |
| if (!pw_budget) | |
| /* Do nothing if no power budget */ | |
| continue; | |
| /* Max power budget per manager */ | |
| if (pw_budget > 6000000) | |
| pw_budget = 6000000; | |
| ret = regulator_request_power_budget(supply, pw_budget); | |
| if (ret < 0) | |
| return ret; | |
| priv->manager_pw_budget[i] = pw_budget; | |
| } | |
| return 0; | |
| } | |
| static int | |
| pd692x0_configure_managers(struct pd692x0_priv *priv) | |
| { | |
| int i, ret; | |
| for (i = 0; i < priv->nmanagers; i++) { | |
| ret = pd692x0_conf_manager_power_budget(priv, i); | |
| if (ret < 0) | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static int | |
| pd692x0_set_port_matrix(const struct pse_pi_pairset *pairset, | |
| const struct pd692x0_manager *manager, | |
| int nmanagers, struct pd692x0_matrix *port_matrix) | |
| { | |
| int i, j, port_cnt; | |
| bool found = false; | |
| if (!pairset->np) | |
| return 0; | |
| /* Look on every managers */ | |
| port_cnt = 0; | |
| for (i = 0; i < nmanagers; i++) { | |
| /* Look on every ports of the manager */ | |
| for (j = 0; j < manager[i].nports; j++) { | |
| if (pairset->np == manager[i].port_node[j]) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| port_cnt += j; | |
| if (found) | |
| break; | |
| } | |
| if (!found) | |
| return -ENODEV; | |
| if (pairset->pinout == ALTERNATIVE_A) | |
| port_matrix->hw_port_a = port_cnt; | |
| else if (pairset->pinout == ALTERNATIVE_B) | |
| port_matrix->hw_port_b = port_cnt; | |
| return 0; | |
| } | |
| static int | |
| pd692x0_set_ports_matrix(struct pd692x0_priv *priv, | |
| const struct pd692x0_manager *manager) | |
| { | |
| struct pd692x0_matrix *port_matrix = priv->port_matrix; | |
| struct pse_controller_dev *pcdev = &priv->pcdev; | |
| int i, ret; | |
| /* Init Matrix */ | |
| for (i = 0; i < PD692X0_MAX_PIS; i++) { | |
| port_matrix[i].hw_port_a = 0xff; | |
| port_matrix[i].hw_port_b = 0xff; | |
| } | |
| /* Update with values for every PSE PIs */ | |
| for (i = 0; i < pcdev->nr_lines; i++) { | |
| ret = pd692x0_set_port_matrix(&pcdev->pi[i].pairset[0], | |
| manager, priv->nmanagers, | |
| &port_matrix[i]); | |
| if (ret) { | |
| dev_err(&priv->client->dev, | |
| "unable to configure pi %d pairset 0", i); | |
| return ret; | |
| } | |
| ret = pd692x0_set_port_matrix(&pcdev->pi[i].pairset[1], | |
| manager, priv->nmanagers, | |
| &port_matrix[i]); | |
| if (ret) { | |
| dev_err(&priv->client->dev, | |
| "unable to configure pi %d pairset 1", i); | |
| return ret; | |
| } | |
| } | |
| return 0; | |
| } | |
| static int | |
| pd692x0_write_ports_matrix(struct pd692x0_priv *priv) | |
| { | |
| struct pd692x0_matrix *port_matrix = priv->port_matrix; | |
| struct pd692x0_msg msg, buf; | |
| int ret, i; | |
| /* Write temporary Matrix */ | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_TMP_PORT_MATRIX]; | |
| for (i = 0; i < PD692X0_MAX_PIS; i++) { | |
| msg.sub[2] = i; | |
| msg.data[0] = port_matrix[i].hw_port_b; | |
| msg.data[1] = port_matrix[i].hw_port_a; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| } | |
| /* Program Matrix */ | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_PRG_PORT_MATRIX]; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| return 0; | |
| } | |
| static int pd692x0_hw_conf_init(struct pd692x0_priv *priv) | |
| { | |
| int ret; | |
| /* Is PD692x0 ready to be configured? */ | |
| if (priv->fw_state != PD692X0_FW_OK && | |
| priv->fw_state != PD692X0_FW_COMPLETE) | |
| return 0; | |
| ret = pd692x0_configure_managers(priv); | |
| if (ret) | |
| return ret; | |
| ret = pd692x0_write_ports_matrix(priv); | |
| if (ret) | |
| return ret; | |
| return 0; | |
| } | |
| static void pd692x0_of_put_managers(struct pd692x0_priv *priv, | |
| struct pd692x0_manager *manager) | |
| { | |
| int i, j; | |
| for (i = 0; i < priv->nmanagers; i++) { | |
| for (j = 0; j < manager[i].nports; j++) | |
| of_node_put(manager[i].port_node[j]); | |
| of_node_put(manager[i].node); | |
| } | |
| } | |
| static void pd692x0_managers_free_pw_budget(struct pd692x0_priv *priv) | |
| { | |
| int i; | |
| for (i = 0; i < PD692X0_MAX_MANAGERS; i++) { | |
| struct regulator *supply; | |
| if (!priv->manager_reg[i] || !priv->manager_pw_budget[i]) | |
| continue; | |
| supply = priv->manager_reg[i]->supply; | |
| if (!supply) | |
| continue; | |
| regulator_free_power_budget(supply, | |
| priv->manager_pw_budget[i]); | |
| } | |
| } | |
| static int | |
| pd692x0_save_user_byte(struct pd692x0_priv *priv) | |
| { | |
| struct pd692x0_msg msg, buf; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_USER_BYTE]; | |
| return pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| } | |
| static int pd692x0_setup_pi_matrix(struct pse_controller_dev *pcdev) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_matrix *port_matrix; | |
| struct pd692x0_manager *manager; | |
| int ret; | |
| manager = kzalloc_objs(*manager, PD692X0_MAX_MANAGERS); | |
| if (!manager) | |
| return -ENOMEM; | |
| port_matrix = devm_kcalloc(&priv->client->dev, PD692X0_MAX_PIS, | |
| sizeof(*port_matrix), GFP_KERNEL); | |
| if (!port_matrix) { | |
| ret = -ENOMEM; | |
| goto err_free_manager; | |
| } | |
| priv->port_matrix = port_matrix; | |
| ret = pd692x0_of_get_managers(priv, manager); | |
| if (ret < 0) | |
| goto err_free_manager; | |
| ret = pd692x0_register_managers_regulator(priv, manager); | |
| if (ret) | |
| goto err_of_managers; | |
| ret = pd692x0_req_managers_pw_budget(priv); | |
| if (ret) | |
| goto err_of_managers; | |
| ret = pd692x0_set_ports_matrix(priv, manager); | |
| if (ret) | |
| goto err_managers_req_pw; | |
| /* Do not init the conf if it is already saved */ | |
| if (!priv->cfg_saved) { | |
| ret = pd692x0_hw_conf_init(priv); | |
| if (ret) | |
| goto err_managers_req_pw; | |
| ret = pd692x0_save_user_byte(priv); | |
| if (ret) | |
| goto err_managers_req_pw; | |
| } | |
| pd692x0_of_put_managers(priv, manager); | |
| kfree(manager); | |
| return 0; | |
| err_managers_req_pw: | |
| pd692x0_managers_free_pw_budget(priv); | |
| err_of_managers: | |
| pd692x0_of_put_managers(priv, manager); | |
| err_free_manager: | |
| kfree(manager); | |
| return ret; | |
| } | |
| static int pd692x0_pi_get_voltage(struct pse_controller_dev *pcdev, int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_MEAS]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| /* Convert 0.1V unit to uV */ | |
| return (buf.sub[0] << 8 | buf.sub[1]) * 100000; | |
| } | |
| static int pd692x0_pi_get_pw_limit(struct pse_controller_dev *pcdev, | |
| int id) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_PORT_PARAM]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) | |
| return ret; | |
| return pd692x0_pi_get_pw_from_table(buf.data[0], buf.data[1]); | |
| } | |
| static int pd692x0_pi_set_pw_limit(struct pse_controller_dev *pcdev, | |
| int id, int max_mW) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct device *dev = &priv->client->dev; | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_PORT_PARAM]; | |
| msg.sub[2] = id; | |
| ret = pd692x0_pi_set_pw_from_table(dev, &msg, max_mW); | |
| if (ret) | |
| return ret; | |
| return pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| } | |
| static int pd692x0_pi_set_prio(struct pse_controller_dev *pcdev, int id, | |
| unsigned int prio) | |
| { | |
| struct pd692x0_priv *priv = to_pd692x0_priv(pcdev); | |
| struct pd692x0_msg msg, buf = {0}; | |
| int ret; | |
| ret = pd692x0_fw_unavailable(priv); | |
| if (ret) | |
| return ret; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_SET_PORT_PARAM]; | |
| msg.sub[2] = id; | |
| /* Controller priority from 1 to 3 */ | |
| msg.data[4] = prio + 1; | |
| return pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| } | |
| static const struct pse_controller_ops pd692x0_ops = { | |
| .setup_pi_matrix = pd692x0_setup_pi_matrix, | |
| .pi_get_admin_state = pd692x0_pi_get_admin_state, | |
| .pi_get_pw_status = pd692x0_pi_get_pw_status, | |
| .pi_get_ext_state = pd692x0_pi_get_ext_state, | |
| .pi_get_pw_class = pd692x0_pi_get_pw_class, | |
| .pi_get_actual_pw = pd692x0_pi_get_actual_pw, | |
| .pi_enable = pd692x0_pi_enable, | |
| .pi_disable = pd692x0_pi_disable, | |
| .pi_get_voltage = pd692x0_pi_get_voltage, | |
| .pi_get_pw_limit = pd692x0_pi_get_pw_limit, | |
| .pi_set_pw_limit = pd692x0_pi_set_pw_limit, | |
| .pi_get_pw_limit_ranges = pd692x0_pi_get_pw_limit_ranges, | |
| .pi_get_prio = pd692x0_pi_get_prio, | |
| .pi_set_prio = pd692x0_pi_set_prio, | |
| }; | |
| #define PD692X0_FW_LINE_MAX_SZ 0xff | |
| static int pd692x0_fw_get_next_line(const u8 *data, | |
| char *line, size_t size) | |
| { | |
| size_t line_size; | |
| int i; | |
| line_size = min_t(size_t, size, PD692X0_FW_LINE_MAX_SZ); | |
| memset(line, 0, PD692X0_FW_LINE_MAX_SZ); | |
| for (i = 0; i < line_size - 1; i++) { | |
| if (*data == '\r' && *(data + 1) == '\n') { | |
| line[i] = '\r'; | |
| line[i + 1] = '\n'; | |
| return i + 2; | |
| } | |
| line[i] = *data; | |
| data++; | |
| } | |
| return -EIO; | |
| } | |
| static enum fw_upload_err | |
| pd692x0_fw_recv_resp(const struct i2c_client *client, unsigned long ms_timeout, | |
| const char *msg_ok, unsigned int msg_size) | |
| { | |
| /* Maximum controller response size */ | |
| char fw_msg_buf[5] = {0}; | |
| unsigned long timeout; | |
| int ret; | |
| if (msg_size > sizeof(fw_msg_buf)) | |
| return FW_UPLOAD_ERR_RW_ERROR; | |
| /* Read until we get something */ | |
| timeout = msecs_to_jiffies(ms_timeout) + jiffies; | |
| while (true) { | |
| if (time_is_before_jiffies(timeout)) | |
| return FW_UPLOAD_ERR_TIMEOUT; | |
| ret = i2c_master_recv(client, fw_msg_buf, 1); | |
| if (ret < 0 || *fw_msg_buf == 0) { | |
| usleep_range(1000, 2000); | |
| continue; | |
| } else { | |
| break; | |
| } | |
| } | |
| /* Read remaining characters */ | |
| ret = i2c_master_recv(client, fw_msg_buf + 1, msg_size - 1); | |
| if (strncmp(fw_msg_buf, msg_ok, msg_size)) { | |
| dev_err(&client->dev, | |
| "Wrong FW download process answer (%*pE)\n", | |
| msg_size, fw_msg_buf); | |
| return FW_UPLOAD_ERR_HW_ERROR; | |
| } | |
| return FW_UPLOAD_ERR_NONE; | |
| } | |
| static int pd692x0_fw_write_line(const struct i2c_client *client, | |
| const char line[PD692X0_FW_LINE_MAX_SZ], | |
| const bool last_line) | |
| { | |
| int ret; | |
| while (*line != 0) { | |
| ret = i2c_master_send(client, line, 1); | |
| if (ret < 0) | |
| return FW_UPLOAD_ERR_RW_ERROR; | |
| line++; | |
| } | |
| if (last_line) { | |
| ret = pd692x0_fw_recv_resp(client, 100, "TP\r\n", | |
| sizeof("TP\r\n") - 1); | |
| if (ret) | |
| return ret; | |
| } else { | |
| ret = pd692x0_fw_recv_resp(client, 100, "T*\r\n", | |
| sizeof("T*\r\n") - 1); | |
| if (ret) | |
| return ret; | |
| } | |
| return FW_UPLOAD_ERR_NONE; | |
| } | |
| static enum fw_upload_err pd692x0_fw_reset(const struct i2c_client *client) | |
| { | |
| const struct pd692x0_msg zero = {0}; | |
| struct pd692x0_msg buf = {0}; | |
| unsigned long timeout; | |
| char cmd[] = "RST"; | |
| int ret; | |
| ret = i2c_master_send(client, cmd, strlen(cmd)); | |
| if (ret < 0) { | |
| dev_err(&client->dev, | |
| "Failed to reset the controller (%pe)\n", | |
| ERR_PTR(ret)); | |
| return ret; | |
| } | |
| timeout = msecs_to_jiffies(10000) + jiffies; | |
| while (true) { | |
| if (time_is_before_jiffies(timeout)) | |
| return FW_UPLOAD_ERR_TIMEOUT; | |
| ret = i2c_master_recv(client, (u8 *)&buf, sizeof(buf)); | |
| if (ret < 0 || | |
| !memcmp(&buf, &zero, sizeof(buf))) | |
| usleep_range(1000, 2000); | |
| else | |
| break; | |
| } | |
| /* Is the reply a successful report message */ | |
| if (buf.key != PD692X0_KEY_TLM || buf.echo != 0xff || | |
| buf.sub[0] & 0x01) { | |
| dev_err(&client->dev, "PSE controller error\n"); | |
| return FW_UPLOAD_ERR_HW_ERROR; | |
| } | |
| /* Is the firmware operational */ | |
| if (buf.sub[0] & 0x02) { | |
| dev_err(&client->dev, | |
| "PSE firmware error. Please update it.\n"); | |
| return FW_UPLOAD_ERR_HW_ERROR; | |
| } | |
| return FW_UPLOAD_ERR_NONE; | |
| } | |
| static enum fw_upload_err pd692x0_fw_prepare(struct fw_upload *fwl, | |
| const u8 *data, u32 size) | |
| { | |
| struct pd692x0_priv *priv = fwl->dd_handle; | |
| const struct i2c_client *client = priv->client; | |
| enum pd692x0_fw_state last_fw_state; | |
| int ret; | |
| priv->cancel_request = false; | |
| last_fw_state = priv->fw_state; | |
| priv->fw_state = PD692X0_FW_PREPARE; | |
| /* Enter program mode */ | |
| if (last_fw_state == PD692X0_FW_BROKEN) { | |
| const char *msg = "ENTR"; | |
| const char *c; | |
| c = msg; | |
| do { | |
| ret = i2c_master_send(client, c, 1); | |
| if (ret < 0) | |
| return FW_UPLOAD_ERR_RW_ERROR; | |
| if (*(c + 1)) | |
| usleep_range(10000, 20000); | |
| } while (*(++c)); | |
| } else { | |
| struct pd692x0_msg msg, buf; | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_DOWNLOAD_CMD]; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) { | |
| dev_err(&client->dev, | |
| "Failed to enter programming mode (%pe)\n", | |
| ERR_PTR(ret)); | |
| return FW_UPLOAD_ERR_RW_ERROR; | |
| } | |
| } | |
| ret = pd692x0_fw_recv_resp(client, 100, "TPE\r\n", sizeof("TPE\r\n") - 1); | |
| if (ret) | |
| goto err_out; | |
| if (priv->cancel_request) { | |
| ret = FW_UPLOAD_ERR_CANCELED; | |
| goto err_out; | |
| } | |
| return FW_UPLOAD_ERR_NONE; | |
| err_out: | |
| pd692x0_fw_reset(priv->client); | |
| priv->fw_state = last_fw_state; | |
| return ret; | |
| } | |
| static enum fw_upload_err pd692x0_fw_write(struct fw_upload *fwl, | |
| const u8 *data, u32 offset, | |
| u32 size, u32 *written) | |
| { | |
| struct pd692x0_priv *priv = fwl->dd_handle; | |
| char line[PD692X0_FW_LINE_MAX_SZ]; | |
| const struct i2c_client *client; | |
| int ret, i; | |
| char cmd; | |
| client = priv->client; | |
| priv->fw_state = PD692X0_FW_WRITE; | |
| /* Erase */ | |
| cmd = 'E'; | |
| ret = i2c_master_send(client, &cmd, 1); | |
| if (ret < 0) { | |
| dev_err(&client->dev, | |
| "Failed to boot programming mode (%pe)\n", | |
| ERR_PTR(ret)); | |
| return FW_UPLOAD_ERR_RW_ERROR; | |
| } | |
| ret = pd692x0_fw_recv_resp(client, 100, "TOE\r\n", sizeof("TOE\r\n") - 1); | |
| if (ret) | |
| return ret; | |
| ret = pd692x0_fw_recv_resp(client, 5000, "TE\r\n", sizeof("TE\r\n") - 1); | |
| if (ret) | |
| dev_warn(&client->dev, | |
| "Failed to erase internal memory, however still try to write Firmware\n"); | |
| ret = pd692x0_fw_recv_resp(client, 100, "TPE\r\n", sizeof("TPE\r\n") - 1); | |
| if (ret) | |
| dev_warn(&client->dev, | |
| "Failed to erase internal memory, however still try to write Firmware\n"); | |
| if (priv->cancel_request) | |
| return FW_UPLOAD_ERR_CANCELED; | |
| /* Program */ | |
| cmd = 'P'; | |
| ret = i2c_master_send(client, &cmd, sizeof(char)); | |
| if (ret < 0) { | |
| dev_err(&client->dev, | |
| "Failed to boot programming mode (%pe)\n", | |
| ERR_PTR(ret)); | |
| return ret; | |
| } | |
| ret = pd692x0_fw_recv_resp(client, 100, "TOP\r\n", sizeof("TOP\r\n") - 1); | |
| if (ret) | |
| return ret; | |
| i = 0; | |
| while (i < size) { | |
| ret = pd692x0_fw_get_next_line(data, line, size - i); | |
| if (ret < 0) { | |
| ret = FW_UPLOAD_ERR_FW_INVALID; | |
| goto err; | |
| } | |
| i += ret; | |
| data += ret; | |
| if (line[0] == 'S' && line[1] == '0') { | |
| continue; | |
| } else if (line[0] == 'S' && line[1] == '7') { | |
| ret = pd692x0_fw_write_line(client, line, true); | |
| if (ret) | |
| goto err; | |
| } else { | |
| ret = pd692x0_fw_write_line(client, line, false); | |
| if (ret) | |
| goto err; | |
| } | |
| if (priv->cancel_request) { | |
| ret = FW_UPLOAD_ERR_CANCELED; | |
| goto err; | |
| } | |
| } | |
| *written = i; | |
| msleep(400); | |
| return FW_UPLOAD_ERR_NONE; | |
| err: | |
| strscpy_pad(line, "S7\r\n", sizeof(line)); | |
| pd692x0_fw_write_line(client, line, true); | |
| return ret; | |
| } | |
| static enum fw_upload_err pd692x0_fw_poll_complete(struct fw_upload *fwl) | |
| { | |
| struct pd692x0_priv *priv = fwl->dd_handle; | |
| const struct i2c_client *client = priv->client; | |
| struct pd692x0_msg_ver ver; | |
| int ret; | |
| priv->fw_state = PD692X0_FW_COMPLETE; | |
| ret = pd692x0_fw_reset(client); | |
| if (ret) | |
| return ret; | |
| ver = pd692x0_get_sw_version(priv); | |
| if (ver.maj_sw_ver < PD692X0_FW_MAJ_VER) { | |
| dev_err(&client->dev, | |
| "Too old firmware version. Please update it\n"); | |
| priv->fw_state = PD692X0_FW_NEED_UPDATE; | |
| return FW_UPLOAD_ERR_FW_INVALID; | |
| } | |
| ret = pd692x0_hw_conf_init(priv); | |
| if (ret < 0) { | |
| dev_err(&client->dev, "Error configuring ports matrix (%pe)\n", | |
| ERR_PTR(ret)); | |
| priv->fw_state = PD692X0_FW_NEED_UPDATE; | |
| return FW_UPLOAD_ERR_HW_ERROR; | |
| } | |
| priv->fw_state = PD692X0_FW_OK; | |
| return FW_UPLOAD_ERR_NONE; | |
| } | |
| static void pd692x0_fw_cancel(struct fw_upload *fwl) | |
| { | |
| struct pd692x0_priv *priv = fwl->dd_handle; | |
| priv->cancel_request = true; | |
| } | |
| static void pd692x0_fw_cleanup(struct fw_upload *fwl) | |
| { | |
| struct pd692x0_priv *priv = fwl->dd_handle; | |
| switch (priv->fw_state) { | |
| case PD692X0_FW_WRITE: | |
| pd692x0_fw_reset(priv->client); | |
| fallthrough; | |
| case PD692X0_FW_COMPLETE: | |
| priv->fw_state = PD692X0_FW_BROKEN; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| static const struct fw_upload_ops pd692x0_fw_ops = { | |
| .prepare = pd692x0_fw_prepare, | |
| .write = pd692x0_fw_write, | |
| .poll_complete = pd692x0_fw_poll_complete, | |
| .cancel = pd692x0_fw_cancel, | |
| .cleanup = pd692x0_fw_cleanup, | |
| }; | |
| static int pd692x0_i2c_probe(struct i2c_client *client) | |
| { | |
| static const char * const regulators[] = { "vdd", "vdda" }; | |
| struct pd692x0_msg msg, buf = {0}, zero = {0}; | |
| struct device *dev = &client->dev; | |
| struct gpio_desc *disable_ports; | |
| struct pd692x0_msg_ver ver; | |
| struct pd692x0_priv *priv; | |
| struct fw_upload *fwl; | |
| int ret; | |
| ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(regulators), | |
| regulators); | |
| if (ret) | |
| return dev_err_probe(dev, ret, | |
| "Failed to enable regulators\n"); | |
| if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { | |
| dev_err(dev, "i2c check functionality failed\n"); | |
| return -ENXIO; | |
| } | |
| priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); | |
| if (!priv) | |
| return -ENOMEM; | |
| priv->client = client; | |
| i2c_set_clientdata(client, priv); | |
| disable_ports = devm_gpiod_get_optional(dev, "disable-ports", GPIOD_OUT_LOW); | |
| if (IS_ERR(disable_ports)) | |
| return dev_err_probe(&client->dev, PTR_ERR(disable_ports), | |
| "Failed to get disable ports GPIO\n"); | |
| ret = i2c_master_recv(client, (u8 *)&buf, sizeof(buf)); | |
| if (ret != sizeof(buf)) { | |
| dev_err(dev, "Failed to get device status\n"); | |
| return -EIO; | |
| } | |
| /* Probe has been already run and the status dumped */ | |
| if (!memcmp(&buf, &zero, sizeof(buf))) { | |
| /* Ask again the controller status */ | |
| msg = pd692x0_msg_template_list[PD692X0_MSG_GET_SYS_STATUS]; | |
| ret = pd692x0_sendrecv_msg(priv, &msg, &buf); | |
| if (ret < 0) { | |
| dev_err(dev, "Failed to get device status\n"); | |
| return ret; | |
| } | |
| } | |
| if (buf.key != 0x03 || buf.sub[0] & 0x01) { | |
| dev_err(dev, "PSE controller error\n"); | |
| return -EIO; | |
| } | |
| if (buf.sub[0] & 0x02) { | |
| dev_err(dev, "PSE firmware error. Please update it.\n"); | |
| priv->fw_state = PD692X0_FW_BROKEN; | |
| } else { | |
| ver = pd692x0_get_sw_version(priv); | |
| dev_info(&client->dev, "Software version %d.%02d.%d.%d\n", | |
| ver.prod, ver.maj_sw_ver, ver.min_sw_ver, | |
| ver.pa_sw_ver); | |
| if (ver.maj_sw_ver < PD692X0_FW_MAJ_VER) { | |
| dev_err(dev, "Too old firmware version. Please update it\n"); | |
| priv->fw_state = PD692X0_FW_NEED_UPDATE; | |
| } else { | |
| priv->fw_state = PD692X0_FW_OK; | |
| } | |
| } | |
| if (buf.data[2] == PD692X0_USER_BYTE) | |
| priv->cfg_saved = true; | |
| priv->np = dev->of_node; | |
| priv->pcdev.nr_lines = PD692X0_MAX_PIS; | |
| priv->pcdev.owner = THIS_MODULE; | |
| priv->pcdev.ops = &pd692x0_ops; | |
| priv->pcdev.dev = dev; | |
| priv->pcdev.types = ETHTOOL_PSE_C33; | |
| priv->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC; | |
| priv->pcdev.pis_prio_max = 2; | |
| ret = devm_pse_controller_register(dev, &priv->pcdev); | |
| if (ret) | |
| return dev_err_probe(dev, ret, | |
| "failed to register PSE controller\n"); | |
| fwl = firmware_upload_register(THIS_MODULE, dev, dev_name(dev), | |
| &pd692x0_fw_ops, priv); | |
| if (IS_ERR(fwl)) | |
| return dev_err_probe(dev, PTR_ERR(fwl), | |
| "failed to register to the Firmware Upload API\n"); | |
| priv->fwl = fwl; | |
| return 0; | |
| } | |
| static void pd692x0_i2c_remove(struct i2c_client *client) | |
| { | |
| struct pd692x0_priv *priv = i2c_get_clientdata(client); | |
| pd692x0_managers_free_pw_budget(priv); | |
| firmware_upload_unregister(priv->fwl); | |
| } | |
| static const struct i2c_device_id pd692x0_id[] = { | |
| { .name = PD692X0_PSE_NAME }, | |
| { } | |
| }; | |
| MODULE_DEVICE_TABLE(i2c, pd692x0_id); | |
| static const struct of_device_id pd692x0_of_match[] = { | |
| { .compatible = "microchip,pd69200", }, | |
| { .compatible = "microchip,pd69210", }, | |
| { .compatible = "microchip,pd69220", }, | |
| { }, | |
| }; | |
| MODULE_DEVICE_TABLE(of, pd692x0_of_match); | |
| static struct i2c_driver pd692x0_driver = { | |
| .probe = pd692x0_i2c_probe, | |
| .remove = pd692x0_i2c_remove, | |
| .id_table = pd692x0_id, | |
| .driver = { | |
| .name = PD692X0_PSE_NAME, | |
| .of_match_table = pd692x0_of_match, | |
| }, | |
| }; | |
| module_i2c_driver(pd692x0_driver); | |
| MODULE_AUTHOR("Kory Maincent <kory.maincent@bootlin.com>"); | |
| MODULE_DESCRIPTION("Microchip PD692x0 PoE PSE Controller driver"); | |
| MODULE_LICENSE("GPL"); |