diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:27:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:27:49 +0000 |
commit | ace9429bb58fd418f0c81d4c2835699bddf6bde6 (patch) | |
tree | b2d64bc10158fdd5497876388cd68142ca374ed3 /drivers/char/ipmi/ipmi_ipmb.c | |
parent | Initial commit. (diff) | |
download | linux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.tar.xz linux-ace9429bb58fd418f0c81d4c2835699bddf6bde6.zip |
Adding upstream version 6.6.15.upstream/6.6.15
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'drivers/char/ipmi/ipmi_ipmb.c')
-rw-r--r-- | drivers/char/ipmi/ipmi_ipmb.c | 583 |
1 files changed, 583 insertions, 0 deletions
diff --git a/drivers/char/ipmi/ipmi_ipmb.c b/drivers/char/ipmi/ipmi_ipmb.c new file mode 100644 index 0000000000..4e335832fc --- /dev/null +++ b/drivers/char/ipmi/ipmi_ipmb.c @@ -0,0 +1,583 @@ +// SPDX-License-Identifier: GPL-2.0 + +/* + * Driver to talk to a remote management controller on IPMB. + */ + +#include <linux/acpi.h> +#include <linux/errno.h> +#include <linux/i2c.h> +#include <linux/miscdevice.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/poll.h> +#include <linux/slab.h> +#include <linux/spinlock.h> +#include <linux/semaphore.h> +#include <linux/kthread.h> +#include <linux/wait.h> +#include <linux/ipmi_msgdefs.h> +#include <linux/ipmi_smi.h> + +#define DEVICE_NAME "ipmi-ipmb" + +static int bmcaddr = 0x20; +module_param(bmcaddr, int, 0644); +MODULE_PARM_DESC(bmcaddr, "Address to use for BMC."); + +static unsigned int retry_time_ms = 250; +module_param(retry_time_ms, uint, 0644); +MODULE_PARM_DESC(retry_time_ms, "Timeout time between retries, in milliseconds."); + +static unsigned int max_retries = 1; +module_param(max_retries, uint, 0644); +MODULE_PARM_DESC(max_retries, "Max resends of a command before timing out."); + +/* Add room for the two slave addresses, two checksums, and rqSeq. */ +#define IPMB_MAX_MSG_LEN (IPMI_MAX_MSG_LENGTH + 5) + +struct ipmi_ipmb_dev { + struct ipmi_smi *intf; + struct i2c_client *client; + struct i2c_client *slave; + + struct ipmi_smi_handlers handlers; + + bool ready; + + u8 curr_seq; + + u8 bmcaddr; + u32 retry_time_ms; + u32 max_retries; + + struct ipmi_smi_msg *next_msg; + struct ipmi_smi_msg *working_msg; + + /* Transmit thread. */ + struct task_struct *thread; + struct semaphore wake_thread; + struct semaphore got_rsp; + spinlock_t lock; + bool stopping; + + u8 xmitmsg[IPMB_MAX_MSG_LEN]; + unsigned int xmitlen; + + u8 rcvmsg[IPMB_MAX_MSG_LEN]; + unsigned int rcvlen; + bool overrun; +}; + +static bool valid_ipmb(struct ipmi_ipmb_dev *iidev) +{ + u8 *msg = iidev->rcvmsg; + u8 netfn; + + if (iidev->overrun) + return false; + + /* Minimum message size. */ + if (iidev->rcvlen < 7) + return false; + + /* Is it a response? */ + netfn = msg[1] >> 2; + if (netfn & 1) { + /* Response messages have an added completion code. */ + if (iidev->rcvlen < 8) + return false; + } + + if (ipmb_checksum(msg, 3) != 0) + return false; + if (ipmb_checksum(msg + 3, iidev->rcvlen - 3) != 0) + return false; + + return true; +} + +static void ipmi_ipmb_check_msg_done(struct ipmi_ipmb_dev *iidev) +{ + struct ipmi_smi_msg *imsg = NULL; + u8 *msg = iidev->rcvmsg; + bool is_cmd; + unsigned long flags; + + if (iidev->rcvlen == 0) + return; + if (!valid_ipmb(iidev)) + goto done; + + is_cmd = ((msg[1] >> 2) & 1) == 0; + + if (is_cmd) { + /* Ignore commands until we are up. */ + if (!iidev->ready) + goto done; + + /* It's a command, allocate a message for it. */ + imsg = ipmi_alloc_smi_msg(); + if (!imsg) + goto done; + imsg->type = IPMI_SMI_MSG_TYPE_IPMB_DIRECT; + imsg->data_size = 0; + } else { + spin_lock_irqsave(&iidev->lock, flags); + if (iidev->working_msg) { + u8 seq = msg[4] >> 2; + bool xmit_rsp = (iidev->working_msg->data[0] >> 2) & 1; + + /* + * Responses should carry the sequence we sent + * them with. If it's a transmitted response, + * ignore it. And if the message hasn't been + * transmitted, ignore it. + */ + if (!xmit_rsp && seq == iidev->curr_seq) { + iidev->curr_seq = (iidev->curr_seq + 1) & 0x3f; + + imsg = iidev->working_msg; + iidev->working_msg = NULL; + } + } + spin_unlock_irqrestore(&iidev->lock, flags); + } + + if (!imsg) + goto done; + + if (imsg->type == IPMI_SMI_MSG_TYPE_IPMB_DIRECT) { + imsg->rsp[0] = msg[1]; /* NetFn/LUN */ + /* + * Keep the source address, rqSeq. Drop the trailing + * checksum. + */ + memcpy(imsg->rsp + 1, msg + 3, iidev->rcvlen - 4); + imsg->rsp_size = iidev->rcvlen - 3; + } else { + imsg->rsp[0] = msg[1]; /* NetFn/LUN */ + /* + * Skip the source address, rqSeq. Drop the trailing + * checksum. + */ + memcpy(imsg->rsp + 1, msg + 5, iidev->rcvlen - 6); + imsg->rsp_size = iidev->rcvlen - 5; + } + ipmi_smi_msg_received(iidev->intf, imsg); + if (!is_cmd) + up(&iidev->got_rsp); + +done: + iidev->overrun = false; + iidev->rcvlen = 0; +} + +/* + * The IPMB protocol only supports i2c writes so there is no need to + * support I2C_SLAVE_READ* events, except to know if the other end has + * issued a read without going to stop mode. + */ +static int ipmi_ipmb_slave_cb(struct i2c_client *client, + enum i2c_slave_event event, u8 *val) +{ + struct ipmi_ipmb_dev *iidev = i2c_get_clientdata(client); + + switch (event) { + case I2C_SLAVE_WRITE_REQUESTED: + ipmi_ipmb_check_msg_done(iidev); + /* + * First byte is the slave address, to ease the checksum + * calculation. + */ + iidev->rcvmsg[0] = client->addr << 1; + iidev->rcvlen = 1; + break; + + case I2C_SLAVE_WRITE_RECEIVED: + if (iidev->rcvlen >= sizeof(iidev->rcvmsg)) + iidev->overrun = true; + else + iidev->rcvmsg[iidev->rcvlen++] = *val; + break; + + case I2C_SLAVE_READ_REQUESTED: + case I2C_SLAVE_STOP: + ipmi_ipmb_check_msg_done(iidev); + break; + + case I2C_SLAVE_READ_PROCESSED: + break; + } + + return 0; +} + +static void ipmi_ipmb_send_response(struct ipmi_ipmb_dev *iidev, + struct ipmi_smi_msg *msg, u8 cc) +{ + if ((msg->data[0] >> 2) & 1) { + /* + * It's a response being sent, we need to return a + * response to the response. Fake a send msg command + * response with channel 0. This will always be ipmb + * direct. + */ + msg->data[0] = (IPMI_NETFN_APP_REQUEST | 1) << 2; + msg->data[3] = IPMI_SEND_MSG_CMD; + msg->data[4] = cc; + msg->data_size = 5; + } + msg->rsp[0] = msg->data[0] | (1 << 2); + if (msg->type == IPMI_SMI_MSG_TYPE_IPMB_DIRECT) { + msg->rsp[1] = msg->data[1]; + msg->rsp[2] = msg->data[2]; + msg->rsp[3] = msg->data[3]; + msg->rsp[4] = cc; + msg->rsp_size = 5; + } else { + msg->rsp[1] = msg->data[1]; + msg->rsp[2] = cc; + msg->rsp_size = 3; + } + ipmi_smi_msg_received(iidev->intf, msg); +} + +static void ipmi_ipmb_format_for_xmit(struct ipmi_ipmb_dev *iidev, + struct ipmi_smi_msg *msg) +{ + if (msg->type == IPMI_SMI_MSG_TYPE_IPMB_DIRECT) { + iidev->xmitmsg[0] = msg->data[1]; + iidev->xmitmsg[1] = msg->data[0]; + memcpy(iidev->xmitmsg + 4, msg->data + 2, msg->data_size - 2); + iidev->xmitlen = msg->data_size + 2; + } else { + iidev->xmitmsg[0] = iidev->bmcaddr; + iidev->xmitmsg[1] = msg->data[0]; + iidev->xmitmsg[4] = 0; + memcpy(iidev->xmitmsg + 5, msg->data + 1, msg->data_size - 1); + iidev->xmitlen = msg->data_size + 4; + } + iidev->xmitmsg[3] = iidev->slave->addr << 1; + if (((msg->data[0] >> 2) & 1) == 0) + /* If it's a command, put in our own sequence number. */ + iidev->xmitmsg[4] = ((iidev->xmitmsg[4] & 0x03) | + (iidev->curr_seq << 2)); + + /* Now add on the final checksums. */ + iidev->xmitmsg[2] = ipmb_checksum(iidev->xmitmsg, 2); + iidev->xmitmsg[iidev->xmitlen] = + ipmb_checksum(iidev->xmitmsg + 3, iidev->xmitlen - 3); + iidev->xmitlen++; +} + +static int ipmi_ipmb_thread(void *data) +{ + struct ipmi_ipmb_dev *iidev = data; + + while (!kthread_should_stop()) { + long ret; + struct i2c_msg i2c_msg; + struct ipmi_smi_msg *msg = NULL; + unsigned long flags; + unsigned int retries = 0; + + /* Wait for a message to send */ + ret = down_interruptible(&iidev->wake_thread); + if (iidev->stopping) + break; + if (ret) + continue; + + spin_lock_irqsave(&iidev->lock, flags); + if (iidev->next_msg) { + msg = iidev->next_msg; + iidev->next_msg = NULL; + } + spin_unlock_irqrestore(&iidev->lock, flags); + if (!msg) + continue; + + ipmi_ipmb_format_for_xmit(iidev, msg); + +retry: + i2c_msg.len = iidev->xmitlen - 1; + if (i2c_msg.len > 32) { + ipmi_ipmb_send_response(iidev, msg, + IPMI_REQ_LEN_EXCEEDED_ERR); + continue; + } + + i2c_msg.addr = iidev->xmitmsg[0] >> 1; + i2c_msg.flags = 0; + i2c_msg.buf = iidev->xmitmsg + 1; + + /* Rely on i2c_transfer for a barrier. */ + iidev->working_msg = msg; + + ret = i2c_transfer(iidev->client->adapter, &i2c_msg, 1); + + if ((msg->data[0] >> 2) & 1) { + /* + * It's a response, nothing will be returned + * by the other end. + */ + + iidev->working_msg = NULL; + ipmi_ipmb_send_response(iidev, msg, + ret < 0 ? IPMI_BUS_ERR : 0); + continue; + } + if (ret < 0) { + iidev->working_msg = NULL; + ipmi_ipmb_send_response(iidev, msg, IPMI_BUS_ERR); + continue; + } + + /* A command was sent, wait for its response. */ + ret = down_timeout(&iidev->got_rsp, + msecs_to_jiffies(iidev->retry_time_ms)); + + /* + * Grab the message if we can. If the handler hasn't + * already handled it, the message will still be there. + */ + spin_lock_irqsave(&iidev->lock, flags); + msg = iidev->working_msg; + iidev->working_msg = NULL; + spin_unlock_irqrestore(&iidev->lock, flags); + + if (!msg && ret) { + /* + * If working_msg is not set and we timed out, + * that means the message grabbed by + * check_msg_done before we could grab it + * here. Wait again for check_msg_done to up + * the semaphore. + */ + down(&iidev->got_rsp); + } else if (msg && ++retries <= iidev->max_retries) { + spin_lock_irqsave(&iidev->lock, flags); + iidev->working_msg = msg; + spin_unlock_irqrestore(&iidev->lock, flags); + goto retry; + } + + if (msg) + ipmi_ipmb_send_response(iidev, msg, IPMI_TIMEOUT_ERR); + } + + if (iidev->next_msg) + /* Return an unspecified error. */ + ipmi_ipmb_send_response(iidev, iidev->next_msg, 0xff); + + return 0; +} + +static int ipmi_ipmb_start_processing(void *send_info, + struct ipmi_smi *new_intf) +{ + struct ipmi_ipmb_dev *iidev = send_info; + + iidev->intf = new_intf; + iidev->ready = true; + return 0; +} + +static void ipmi_ipmb_stop_thread(struct ipmi_ipmb_dev *iidev) +{ + if (iidev->thread) { + struct task_struct *t = iidev->thread; + + iidev->thread = NULL; + iidev->stopping = true; + up(&iidev->wake_thread); + up(&iidev->got_rsp); + kthread_stop(t); + } +} + +static void ipmi_ipmb_shutdown(void *send_info) +{ + struct ipmi_ipmb_dev *iidev = send_info; + + ipmi_ipmb_stop_thread(iidev); +} + +static void ipmi_ipmb_sender(void *send_info, + struct ipmi_smi_msg *msg) +{ + struct ipmi_ipmb_dev *iidev = send_info; + unsigned long flags; + + spin_lock_irqsave(&iidev->lock, flags); + BUG_ON(iidev->next_msg); + + iidev->next_msg = msg; + spin_unlock_irqrestore(&iidev->lock, flags); + + up(&iidev->wake_thread); +} + +static void ipmi_ipmb_request_events(void *send_info) +{ + /* We don't fetch events here. */ +} + +static void ipmi_ipmb_cleanup(struct ipmi_ipmb_dev *iidev) +{ + if (iidev->slave) { + i2c_slave_unregister(iidev->slave); + if (iidev->slave != iidev->client) + i2c_unregister_device(iidev->slave); + } + iidev->slave = NULL; + iidev->client = NULL; + ipmi_ipmb_stop_thread(iidev); +} + +static void ipmi_ipmb_remove(struct i2c_client *client) +{ + struct ipmi_ipmb_dev *iidev = i2c_get_clientdata(client); + + ipmi_ipmb_cleanup(iidev); + ipmi_unregister_smi(iidev->intf); +} + +static int ipmi_ipmb_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct ipmi_ipmb_dev *iidev; + struct device_node *slave_np; + struct i2c_adapter *slave_adap = NULL; + struct i2c_client *slave = NULL; + int rv; + + iidev = devm_kzalloc(&client->dev, sizeof(*iidev), GFP_KERNEL); + if (!iidev) + return -ENOMEM; + + if (of_property_read_u8(dev->of_node, "bmcaddr", &iidev->bmcaddr) != 0) + iidev->bmcaddr = bmcaddr; + if (iidev->bmcaddr == 0 || iidev->bmcaddr & 1) { + /* Can't have the write bit set. */ + dev_notice(&client->dev, + "Invalid bmc address value %2.2x\n", iidev->bmcaddr); + return -EINVAL; + } + + if (of_property_read_u32(dev->of_node, "retry-time", + &iidev->retry_time_ms) != 0) + iidev->retry_time_ms = retry_time_ms; + + if (of_property_read_u32(dev->of_node, "max-retries", + &iidev->max_retries) != 0) + iidev->max_retries = max_retries; + + slave_np = of_parse_phandle(dev->of_node, "slave-dev", 0); + if (slave_np) { + slave_adap = of_get_i2c_adapter_by_node(slave_np); + of_node_put(slave_np); + if (!slave_adap) { + dev_notice(&client->dev, + "Could not find slave adapter\n"); + return -EINVAL; + } + } + + iidev->client = client; + + if (slave_adap) { + struct i2c_board_info binfo; + + memset(&binfo, 0, sizeof(binfo)); + strscpy(binfo.type, "ipmb-slave", I2C_NAME_SIZE); + binfo.addr = client->addr; + binfo.flags = I2C_CLIENT_SLAVE; + slave = i2c_new_client_device(slave_adap, &binfo); + i2c_put_adapter(slave_adap); + if (IS_ERR(slave)) { + rv = PTR_ERR(slave); + dev_notice(&client->dev, + "Could not allocate slave device: %d\n", rv); + return rv; + } + i2c_set_clientdata(slave, iidev); + } else { + slave = client; + } + i2c_set_clientdata(client, iidev); + slave->flags |= I2C_CLIENT_SLAVE; + + rv = i2c_slave_register(slave, ipmi_ipmb_slave_cb); + if (rv) + goto out_err; + iidev->slave = slave; + slave = NULL; + + iidev->handlers.flags = IPMI_SMI_CAN_HANDLE_IPMB_DIRECT; + iidev->handlers.start_processing = ipmi_ipmb_start_processing; + iidev->handlers.shutdown = ipmi_ipmb_shutdown; + iidev->handlers.sender = ipmi_ipmb_sender; + iidev->handlers.request_events = ipmi_ipmb_request_events; + + spin_lock_init(&iidev->lock); + sema_init(&iidev->wake_thread, 0); + sema_init(&iidev->got_rsp, 0); + + iidev->thread = kthread_run(ipmi_ipmb_thread, iidev, + "kipmb%4.4x", client->addr); + if (IS_ERR(iidev->thread)) { + rv = PTR_ERR(iidev->thread); + dev_notice(&client->dev, + "Could not start kernel thread: error %d\n", rv); + goto out_err; + } + + rv = ipmi_register_smi(&iidev->handlers, + iidev, + &client->dev, + iidev->bmcaddr); + if (rv) + goto out_err; + + return 0; + +out_err: + if (slave && slave != client) + i2c_unregister_device(slave); + ipmi_ipmb_cleanup(iidev); + return rv; +} + +#ifdef CONFIG_OF +static const struct of_device_id of_ipmi_ipmb_match[] = { + { .type = "ipmi", .compatible = DEVICE_NAME }, + {}, +}; +MODULE_DEVICE_TABLE(of, of_ipmi_ipmb_match); +#else +#define of_ipmi_ipmb_match NULL +#endif + +static const struct i2c_device_id ipmi_ipmb_id[] = { + { DEVICE_NAME, 0 }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, ipmi_ipmb_id); + +static struct i2c_driver ipmi_ipmb_driver = { + .class = I2C_CLASS_HWMON, + .driver = { + .name = DEVICE_NAME, + .of_match_table = of_ipmi_ipmb_match, + }, + .probe = ipmi_ipmb_probe, + .remove = ipmi_ipmb_remove, + .id_table = ipmi_ipmb_id, +}; +module_i2c_driver(ipmi_ipmb_driver); + +MODULE_AUTHOR("Corey Minyard"); +MODULE_DESCRIPTION("IPMI IPMB driver"); +MODULE_LICENSE("GPL v2"); |