[PATCH v8 2/4] i2c-smbus: add SMBus Host Notify support

From: Benjamin Tissoires
Date: Thu Jun 09 2016 - 10:54:46 EST


SMBus Host Notify allows a slave device to act as a master on a bus to
notify the host of an interrupt. On Intel chipsets, the functionality
is directly implemented in the firmware. We just need to export a
function to call .alert() on the proper device driver.

i2c_handle_smbus_host_notify() behaves like i2c_handle_smbus_alert().
When called, it schedules a task that will be able to sleep to go through
the list of devices attached to the adapter.

The current implementation allows one Host Notification to be scheduled
while an other is running.

Tested-by: Andrew Duggan <aduggan@xxxxxxxxxxxxx>
Signed-off-by: Benjamin Tissoires <benjamin.tissoires@xxxxxxxxxx>
---
changes in v2:
- do the actual processing of finding the device in i2c-smbus.c
- remove the i2c-core implementations
- remove the manual toggle of SMBus Host Notify

no changes in v3

changes in v4:
- schedule the worker in i2c_handle_smbus_host_notify() -> it can now be called
in an interrupt context.
- introduce i2c_setup_smbus_host_notify()

no changes in v5

changes in v6:
- fix the parameters of the inlined functions when the module is not compiled

changes in v7:
- fixed typo in i2c_handle_smbus_host_notify() warning message ('.' and '\n'
were inverted)

changes in v8:
- Documentation updated to reflect the code
- s/-EFAULT/-EBUSY
- use of IS_ENABLED(CONFIG_I2C_SMBUS) instead of a custom made test :)

Documentation/i2c/smbus-protocol | 6 +++
drivers/i2c/i2c-smbus.c | 113 +++++++++++++++++++++++++++++++++++++--
include/linux/i2c-smbus.h | 44 +++++++++++++++
include/linux/i2c.h | 3 ++
include/uapi/linux/i2c.h | 1 +
5 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/Documentation/i2c/smbus-protocol b/Documentation/i2c/smbus-protocol
index 6012b12..14d4ec1 100644
--- a/Documentation/i2c/smbus-protocol
+++ b/Documentation/i2c/smbus-protocol
@@ -199,6 +199,12 @@ alerting device's address.

[S] [HostAddr] [Wr] A [DevAddr] A [DataLow] A [DataHigh] A [P]

+This is implemented in the following way in the Linux kernel:
+* I2C bus drivers which support SMBus Host Notify should call
+ i2c_setup_smbus_host_notify() to setup SMBus Host Notify support.
+* I2C drivers for devices which can trigger SMBus Host Notify should implement
+ the optional alert() callback.
+

Packet Error Checking (PEC)
===========================
diff --git a/drivers/i2c/i2c-smbus.c b/drivers/i2c/i2c-smbus.c
index 3b6765a..f574995 100644
--- a/drivers/i2c/i2c-smbus.c
+++ b/drivers/i2c/i2c-smbus.c
@@ -33,7 +33,8 @@ struct i2c_smbus_alert {

struct alert_data {
unsigned short addr;
- u8 flag:1;
+ enum i2c_alert_protocol type;
+ unsigned int data;
};

/* If this is the alerting device, notify its driver */
@@ -56,8 +57,7 @@ static int smbus_do_alert(struct device *dev, void *addrp)
if (client->dev.driver) {
driver = to_i2c_driver(client->dev.driver);
if (driver->alert)
- driver->alert(client, I2C_PROTOCOL_SMBUS_ALERT,
- data->flag);
+ driver->alert(client, data->type, data->data);
else
dev_warn(&client->dev, "no driver alert()!\n");
} else
@@ -97,8 +97,9 @@ static void smbus_alert(struct work_struct *work)
if (status < 0)
break;

- data.flag = status & 1;
+ data.data = status & 1;
data.addr = status >> 1;
+ data.type = I2C_PROTOCOL_SMBUS_ALERT;

if (data.addr == prev_addr) {
dev_warn(&ara->dev, "Duplicate SMBALERT# from dev "
@@ -106,7 +107,7 @@ static void smbus_alert(struct work_struct *work)
break;
}
dev_dbg(&ara->dev, "SMBALERT# from dev 0x%02x, flag %d\n",
- data.addr, data.flag);
+ data.addr, data.data);

/* Notify driver for the device which issued the alert */
device_for_each_child(&ara->adapter->dev, &data,
@@ -240,6 +241,108 @@ int i2c_handle_smbus_alert(struct i2c_client *ara)
}
EXPORT_SYMBOL_GPL(i2c_handle_smbus_alert);

+static void smbus_host_notify_work(struct work_struct *work)
+{
+ struct alert_data alert;
+ struct i2c_adapter *adapter;
+ unsigned long flags;
+ u16 payload;
+ u8 addr;
+ struct smbus_host_notify *data;
+
+ data = container_of(work, struct smbus_host_notify, work);
+
+ spin_lock_irqsave(&data->lock, flags);
+ payload = data->payload;
+ addr = data->addr;
+ adapter = data->adapter;
+
+ /* clear the pending bit and release the spinlock */
+ data->pending = false;
+ spin_unlock_irqrestore(&data->lock, flags);
+
+ if (!adapter || !addr)
+ return;
+
+ alert.type = I2C_PROTOCOL_SMBUS_HOST_NOTIFY;
+ alert.addr = addr;
+ alert.data = payload;
+
+ device_for_each_child(&adapter->dev, &alert, smbus_do_alert);
+}
+
+/**
+ * i2c_setup_smbus_host_notify - Allocate a new smbus_host_notify for the given
+ * I2C adapter.
+ * @adapter: the adapter we want to associate a Host Notify function
+ *
+ * Returns a struct smbus_host_notify pointer on success, and NULL on failure.
+ * The resulting smbus_host_notify must not be freed afterwards, it is a
+ * managed resource already.
+ */
+struct smbus_host_notify *i2c_setup_smbus_host_notify(struct i2c_adapter *adap)
+{
+ struct smbus_host_notify *host_notify;
+
+ host_notify = devm_kzalloc(&adap->dev, sizeof(struct smbus_host_notify),
+ GFP_KERNEL);
+ if (!host_notify)
+ return NULL;
+
+ host_notify->adapter = adap;
+
+ spin_lock_init(&host_notify->lock);
+ INIT_WORK(&host_notify->work, smbus_host_notify_work);
+
+ return host_notify;
+}
+EXPORT_SYMBOL_GPL(i2c_setup_smbus_host_notify);
+
+/**
+ * i2c_handle_smbus_host_notify - Forward a Host Notify event to the correct
+ * I2C client.
+ * @host_notify: the struct host_notify attached to the relevant adapter
+ * @data: the Host Notify data which contains the payload and address of the
+ * client
+ * Context: can't sleep
+ *
+ * Helper function to be called from an I2C bus driver's interrupt
+ * handler. It will schedule the Host Notify work, in turn calling the
+ * corresponding I2C device driver's alert function.
+ *
+ * host_notify should be a valid pointer previously returned by
+ * i2c_setup_smbus_host_notify().
+ */
+int i2c_handle_smbus_host_notify(struct smbus_host_notify *host_notify,
+ unsigned short addr, unsigned int data)
+{
+ unsigned long flags;
+ struct i2c_adapter *adapter;
+
+ if (!host_notify || !host_notify->adapter)
+ return -EINVAL;
+
+ adapter = host_notify->adapter;
+
+ spin_lock_irqsave(&host_notify->lock, flags);
+
+ if (host_notify->pending) {
+ spin_unlock_irqrestore(&host_notify->lock, flags);
+ dev_warn(&adapter->dev, "Host Notify already scheduled.\n");
+ return -EBUSY;
+ }
+
+ host_notify->payload = data;
+ host_notify->addr = addr;
+
+ /* Mark that there is a pending notification and release the lock */
+ host_notify->pending = true;
+ spin_unlock_irqrestore(&host_notify->lock, flags);
+
+ return schedule_work(&host_notify->work);
+}
+EXPORT_SYMBOL_GPL(i2c_handle_smbus_host_notify);
+
module_i2c_driver(smbalert_driver);

MODULE_AUTHOR("Jean Delvare <jdelvare@xxxxxxx>");
diff --git a/include/linux/i2c-smbus.h b/include/linux/i2c-smbus.h
index 8f1b086..4ac95bb 100644
--- a/include/linux/i2c-smbus.h
+++ b/include/linux/i2c-smbus.h
@@ -23,6 +23,8 @@
#define _LINUX_I2C_SMBUS_H

#include <linux/i2c.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>


/**
@@ -48,4 +50,46 @@ struct i2c_client *i2c_setup_smbus_alert(struct i2c_adapter *adapter,
struct i2c_smbus_alert_setup *setup);
int i2c_handle_smbus_alert(struct i2c_client *ara);

+/**
+ * smbus_host_notify - internal structure used by the Host Notify mechanism.
+ * @adapter: the I2C adapter associated with this struct
+ * @work: worker used to schedule the IRQ in the slave device
+ * @lock: spinlock to check if a notification is already pending
+ * @pending: flag set when a notification is pending (any new notification will
+ * be rejected if pending is true)
+ * @payload: the actual payload of the Host Notify event
+ * @addr: the address of the slave device which raised the notification
+ *
+ * This struct needs to be allocated by i2c_setup_smbus_host_notify() and does
+ * not need to be freed. Internally, i2c_setup_smbus_host_notify() uses a
+ * managed resource to clean this up when the adapter get released.
+ */
+struct smbus_host_notify {
+ struct i2c_adapter *adapter;
+ struct work_struct work;
+ spinlock_t lock;
+ bool pending;
+ u16 payload;
+ u8 addr;
+};
+
+#if IS_ENABLED(CONFIG_I2C_SMBUS)
+struct smbus_host_notify *i2c_setup_smbus_host_notify(struct i2c_adapter *adap);
+int i2c_handle_smbus_host_notify(struct smbus_host_notify *host_notify,
+ unsigned short addr, unsigned int data);
+#else
+static inline struct smbus_host_notify *
+i2c_setup_smbus_host_notify(struct i2c_adapter *adap)
+{
+ return NULL;
+}
+
+static inline int
+i2c_handle_smbus_host_notify(struct smbus_host_notify *host_notify,
+ unsigned short addr, unsigned int data)
+{
+ return 0;
+}
+#endif /* I2C_SMBUS */
+
#endif /* _LINUX_I2C_SMBUS_H */
diff --git a/include/linux/i2c.h b/include/linux/i2c.h
index 29a2d3b7c..e8ccdfe 100644
--- a/include/linux/i2c.h
+++ b/include/linux/i2c.h
@@ -128,6 +128,7 @@ i2c_smbus_read_i2c_block_data_or_emulated(const struct i2c_client *client,

enum i2c_alert_protocol {
I2C_PROTOCOL_SMBUS_ALERT,
+ I2C_PROTOCOL_SMBUS_HOST_NOTIFY,
};

/**
@@ -184,6 +185,8 @@ struct i2c_driver {
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
+ * For the SMBus Host Notify protocol, the data corresponds to the
+ * 16-bit payload data reported by the slave device acting as master.
*/
void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
unsigned int data);
diff --git a/include/uapi/linux/i2c.h b/include/uapi/linux/i2c.h
index adcbef4..009e27b 100644
--- a/include/uapi/linux/i2c.h
+++ b/include/uapi/linux/i2c.h
@@ -102,6 +102,7 @@ struct i2c_msg {
#define I2C_FUNC_SMBUS_WRITE_BLOCK_DATA 0x02000000
#define I2C_FUNC_SMBUS_READ_I2C_BLOCK 0x04000000 /* I2C-like block xfer */
#define I2C_FUNC_SMBUS_WRITE_I2C_BLOCK 0x08000000 /* w/ 1-byte reg. addr. */
+#define I2C_FUNC_SMBUS_HOST_NOTIFY 0x10000000

#define I2C_FUNC_SMBUS_BYTE (I2C_FUNC_SMBUS_READ_BYTE | \
I2C_FUNC_SMBUS_WRITE_BYTE)
--
2.5.0