mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
583 lines
14 KiB
583 lines
14 KiB
// 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(max_retries, "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_new = 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");
|
|
|