| // SPDX-License-Identifier: GPL-2.0+ |
| // Copyright IBM Corp 2019 |
| |
| #include <linux/device.h> |
| #include <linux/errno.h> |
| #include <linux/fsi-occ.h> |
| #include <linux/i2c.h> |
| #include <linux/jiffies.h> |
| #include <linux/module.h> |
| #include <linux/sched.h> |
| #include <asm/unaligned.h> |
| |
| #include "common.h" |
| |
| #define OCC_TIMEOUT_MS 1000 |
| #define OCC_CMD_IN_PRG_WAIT_MS 50 |
| |
| /* OCB (on-chip control bridge - interface to OCC) registers */ |
| #define OCB_DATA1 0x6B035 |
| #define OCB_ADDR 0x6B070 |
| #define OCB_DATA3 0x6B075 |
| |
| /* OCC SRAM address space */ |
| #define OCC_SRAM_ADDR_CMD 0xFFFF6000 |
| #define OCC_SRAM_ADDR_RESP 0xFFFF7000 |
| |
| #define OCC_DATA_ATTN 0x20010000 |
| |
| struct p8_i2c_occ { |
| struct occ occ; |
| struct i2c_client *client; |
| }; |
| |
| #define to_p8_i2c_occ(x) container_of((x), struct p8_i2c_occ, occ) |
| |
| static int p8_i2c_occ_getscom(struct i2c_client *client, u32 address, u8 *data) |
| { |
| ssize_t rc; |
| __be64 buf; |
| struct i2c_msg msgs[2]; |
| |
| /* p8 i2c slave requires shift */ |
| address <<= 1; |
| |
| msgs[0].addr = client->addr; |
| msgs[0].flags = client->flags & I2C_M_TEN; |
| msgs[0].len = sizeof(u32); |
| /* address is a scom address; bus-endian */ |
| msgs[0].buf = (char *)&address; |
| |
| /* data from OCC is big-endian */ |
| msgs[1].addr = client->addr; |
| msgs[1].flags = (client->flags & I2C_M_TEN) | I2C_M_RD; |
| msgs[1].len = sizeof(u64); |
| msgs[1].buf = (char *)&buf; |
| |
| rc = i2c_transfer(client->adapter, msgs, 2); |
| if (rc < 0) |
| return rc; |
| |
| *(u64 *)data = be64_to_cpu(buf); |
| |
| return 0; |
| } |
| |
| static int p8_i2c_occ_putscom(struct i2c_client *client, u32 address, u8 *data) |
| { |
| u32 buf[3]; |
| ssize_t rc; |
| |
| /* p8 i2c slave requires shift */ |
| address <<= 1; |
| |
| /* address is bus-endian; data passed through from user as-is */ |
| buf[0] = address; |
| memcpy(&buf[1], &data[4], sizeof(u32)); |
| memcpy(&buf[2], data, sizeof(u32)); |
| |
| rc = i2c_master_send(client, (const char *)buf, sizeof(buf)); |
| if (rc < 0) |
| return rc; |
| else if (rc != sizeof(buf)) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static int p8_i2c_occ_putscom_u32(struct i2c_client *client, u32 address, |
| u32 data0, u32 data1) |
| { |
| u8 buf[8]; |
| |
| memcpy(buf, &data0, 4); |
| memcpy(buf + 4, &data1, 4); |
| |
| return p8_i2c_occ_putscom(client, address, buf); |
| } |
| |
| static int p8_i2c_occ_putscom_be(struct i2c_client *client, u32 address, |
| u8 *data) |
| { |
| __be32 data0, data1; |
| |
| memcpy(&data0, data, 4); |
| memcpy(&data1, data + 4, 4); |
| |
| return p8_i2c_occ_putscom_u32(client, address, be32_to_cpu(data0), |
| be32_to_cpu(data1)); |
| } |
| |
| static int p8_i2c_occ_send_cmd(struct occ *occ, u8 *cmd) |
| { |
| int i, rc; |
| unsigned long start; |
| u16 data_length; |
| const unsigned long timeout = msecs_to_jiffies(OCC_TIMEOUT_MS); |
| const long wait_time = msecs_to_jiffies(OCC_CMD_IN_PRG_WAIT_MS); |
| struct p8_i2c_occ *ctx = to_p8_i2c_occ(occ); |
| struct i2c_client *client = ctx->client; |
| struct occ_response *resp = &occ->resp; |
| |
| start = jiffies; |
| |
| /* set sram address for command */ |
| rc = p8_i2c_occ_putscom_u32(client, OCB_ADDR, OCC_SRAM_ADDR_CMD, 0); |
| if (rc) |
| return rc; |
| |
| /* write command (expected to already be BE), we need bus-endian... */ |
| rc = p8_i2c_occ_putscom_be(client, OCB_DATA3, cmd); |
| if (rc) |
| return rc; |
| |
| /* trigger OCC attention */ |
| rc = p8_i2c_occ_putscom_u32(client, OCB_DATA1, OCC_DATA_ATTN, 0); |
| if (rc) |
| return rc; |
| |
| do { |
| /* set sram address for response */ |
| rc = p8_i2c_occ_putscom_u32(client, OCB_ADDR, |
| OCC_SRAM_ADDR_RESP, 0); |
| if (rc) |
| return rc; |
| |
| rc = p8_i2c_occ_getscom(client, OCB_DATA3, (u8 *)resp); |
| if (rc) |
| return rc; |
| |
| /* wait for OCC */ |
| if (resp->return_status == OCC_RESP_CMD_IN_PRG) { |
| rc = -EALREADY; |
| |
| if (time_after(jiffies, start + timeout)) |
| break; |
| |
| set_current_state(TASK_INTERRUPTIBLE); |
| schedule_timeout(wait_time); |
| } |
| } while (rc); |
| |
| /* check the OCC response */ |
| switch (resp->return_status) { |
| case OCC_RESP_CMD_IN_PRG: |
| rc = -ETIMEDOUT; |
| break; |
| case OCC_RESP_SUCCESS: |
| rc = 0; |
| break; |
| case OCC_RESP_CMD_INVAL: |
| case OCC_RESP_CMD_LEN_INVAL: |
| case OCC_RESP_DATA_INVAL: |
| case OCC_RESP_CHKSUM_ERR: |
| rc = -EINVAL; |
| break; |
| case OCC_RESP_INT_ERR: |
| case OCC_RESP_BAD_STATE: |
| case OCC_RESP_CRIT_EXCEPT: |
| case OCC_RESP_CRIT_INIT: |
| case OCC_RESP_CRIT_WATCHDOG: |
| case OCC_RESP_CRIT_OCB: |
| case OCC_RESP_CRIT_HW: |
| rc = -EREMOTEIO; |
| break; |
| default: |
| rc = -EPROTO; |
| } |
| |
| if (rc < 0) |
| return rc; |
| |
| data_length = get_unaligned_be16(&resp->data_length); |
| if (data_length > OCC_RESP_DATA_BYTES) |
| return -EMSGSIZE; |
| |
| /* fetch the rest of the response data */ |
| for (i = 8; i < data_length + 7; i += 8) { |
| rc = p8_i2c_occ_getscom(client, OCB_DATA3, ((u8 *)resp) + i); |
| if (rc) |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static int p8_i2c_occ_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| struct occ *occ; |
| struct p8_i2c_occ *ctx = devm_kzalloc(&client->dev, sizeof(*ctx), |
| GFP_KERNEL); |
| if (!ctx) |
| return -ENOMEM; |
| |
| ctx->client = client; |
| occ = &ctx->occ; |
| occ->bus_dev = &client->dev; |
| dev_set_drvdata(&client->dev, occ); |
| |
| occ->powr_sample_time_us = 250; |
| occ->poll_cmd_data = 0x10; /* P8 OCC poll data */ |
| occ->send_cmd = p8_i2c_occ_send_cmd; |
| |
| return occ_setup(occ, "p8_occ"); |
| } |
| |
| static int p8_i2c_occ_remove(struct i2c_client *client) |
| { |
| struct occ *occ = dev_get_drvdata(&client->dev); |
| |
| occ_shutdown(occ); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id p8_i2c_occ_of_match[] = { |
| { .compatible = "ibm,p8-occ-hwmon" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, p8_i2c_occ_of_match); |
| |
| static struct i2c_driver p8_i2c_occ_driver = { |
| .class = I2C_CLASS_HWMON, |
| .driver = { |
| .name = "occ-hwmon", |
| .of_match_table = p8_i2c_occ_of_match, |
| }, |
| .probe = p8_i2c_occ_probe, |
| .remove = p8_i2c_occ_remove, |
| }; |
| |
| module_i2c_driver(p8_i2c_occ_driver); |
| |
| MODULE_AUTHOR("Eddie James <eajames@linux.ibm.com>"); |
| MODULE_DESCRIPTION("BMC P8 OCC hwmon driver"); |
| MODULE_LICENSE("GPL"); |