| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * I2C slave mode testunit |
| * |
| * Copyright (C) 2020 by Wolfram Sang, Sang Engineering <wsa@sang-engineering.com> |
| * Copyright (C) 2020 by Renesas Electronics Corporation |
| */ |
| |
| #include <generated/utsrelease.h> |
| #include <linux/bitops.h> |
| #include <linux/completion.h> |
| #include <linux/gpio/consumer.h> |
| #include <linux/i2c.h> |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/slab.h> |
| #include <linux/workqueue.h> /* FIXME: is system_long_wq the best choice? */ |
| |
| #define TU_VERSION_MAX_LENGTH 128 |
| |
| enum testunit_cmds { |
| TU_CMD_READ_BYTES = 1, /* save 0 for ABORT, RESET or similar */ |
| TU_CMD_SMBUS_HOST_NOTIFY, |
| TU_CMD_SMBUS_BLOCK_PROC_CALL, |
| TU_CMD_GET_VERSION_WITH_REP_START, |
| TU_CMD_SMBUS_ALERT_REQUEST, |
| TU_NUM_CMDS |
| }; |
| |
| enum testunit_regs { |
| TU_REG_CMD, |
| TU_REG_DATAL, |
| TU_REG_DATAH, |
| TU_REG_DELAY, |
| TU_NUM_REGS |
| }; |
| |
| enum testunit_flags { |
| TU_FLAG_IN_PROCESS, |
| }; |
| |
| struct testunit_data { |
| unsigned long flags; |
| u8 regs[TU_NUM_REGS]; |
| u8 reg_idx; |
| u8 read_idx; |
| struct i2c_client *client; |
| struct delayed_work worker; |
| struct gpio_desc *gpio; |
| struct completion alert_done; |
| }; |
| |
| static char tu_version_info[] = "v" UTS_RELEASE "\n\0"; |
| |
| static int i2c_slave_testunit_smbalert_cb(struct i2c_client *client, |
| enum i2c_slave_event event, u8 *val) |
| { |
| struct testunit_data *tu = i2c_get_clientdata(client); |
| |
| switch (event) { |
| case I2C_SLAVE_READ_PROCESSED: |
| gpiod_set_value(tu->gpio, 0); |
| fallthrough; |
| case I2C_SLAVE_READ_REQUESTED: |
| *val = tu->regs[TU_REG_DATAL]; |
| break; |
| |
| case I2C_SLAVE_STOP: |
| complete(&tu->alert_done); |
| break; |
| |
| case I2C_SLAVE_WRITE_REQUESTED: |
| case I2C_SLAVE_WRITE_RECEIVED: |
| return -EOPNOTSUPP; |
| } |
| |
| return 0; |
| } |
| |
| static int i2c_slave_testunit_slave_cb(struct i2c_client *client, |
| enum i2c_slave_event event, u8 *val) |
| { |
| struct testunit_data *tu = i2c_get_clientdata(client); |
| bool is_proc_call = tu->reg_idx == 3 && tu->regs[TU_REG_DATAL] == 1 && |
| tu->regs[TU_REG_CMD] == TU_CMD_SMBUS_BLOCK_PROC_CALL; |
| bool is_get_version = tu->reg_idx == 3 && |
| tu->regs[TU_REG_CMD] == TU_CMD_GET_VERSION_WITH_REP_START; |
| int ret = 0; |
| |
| switch (event) { |
| case I2C_SLAVE_WRITE_REQUESTED: |
| if (test_bit(TU_FLAG_IN_PROCESS, &tu->flags)) |
| return -EBUSY; |
| |
| memset(tu->regs, 0, TU_NUM_REGS); |
| tu->reg_idx = 0; |
| tu->read_idx = 0; |
| break; |
| |
| case I2C_SLAVE_WRITE_RECEIVED: |
| if (test_bit(TU_FLAG_IN_PROCESS, &tu->flags)) |
| return -EBUSY; |
| |
| if (tu->reg_idx < TU_NUM_REGS) |
| tu->regs[tu->reg_idx] = *val; |
| else |
| ret = -EMSGSIZE; |
| |
| if (tu->reg_idx <= TU_NUM_REGS) |
| tu->reg_idx++; |
| |
| /* TU_REG_CMD always written at this point */ |
| if (tu->regs[TU_REG_CMD] >= TU_NUM_CMDS) |
| ret = -EINVAL; |
| |
| break; |
| |
| case I2C_SLAVE_STOP: |
| if (tu->reg_idx == TU_NUM_REGS) { |
| set_bit(TU_FLAG_IN_PROCESS, &tu->flags); |
| queue_delayed_work(system_long_wq, &tu->worker, |
| msecs_to_jiffies(10 * tu->regs[TU_REG_DELAY])); |
| } |
| |
| /* |
| * Reset reg_idx to avoid that work gets queued again in case of |
| * STOP after a following read message. But do not clear TU regs |
| * here because we still need them in the workqueue! |
| */ |
| tu->reg_idx = 0; |
| break; |
| |
| case I2C_SLAVE_READ_PROCESSED: |
| /* Advance until we reach the NUL character */ |
| if (is_get_version && tu_version_info[tu->read_idx] != 0) |
| tu->read_idx++; |
| else if (is_proc_call && tu->regs[TU_REG_DATAH]) |
| tu->regs[TU_REG_DATAH]--; |
| |
| fallthrough; |
| |
| case I2C_SLAVE_READ_REQUESTED: |
| if (is_get_version) |
| *val = tu_version_info[tu->read_idx]; |
| else if (is_proc_call) |
| *val = tu->regs[TU_REG_DATAH]; |
| else |
| *val = test_bit(TU_FLAG_IN_PROCESS, &tu->flags) ? |
| tu->regs[TU_REG_CMD] : 0; |
| break; |
| } |
| |
| return ret; |
| } |
| |
| static void i2c_slave_testunit_work(struct work_struct *work) |
| { |
| struct testunit_data *tu = container_of(work, struct testunit_data, worker.work); |
| unsigned long time_left; |
| struct i2c_msg msg; |
| u8 msgbuf[256]; |
| u16 orig_addr; |
| int ret = 0; |
| |
| msg.addr = I2C_CLIENT_END; |
| msg.buf = msgbuf; |
| |
| switch (tu->regs[TU_REG_CMD]) { |
| case TU_CMD_READ_BYTES: |
| msg.addr = tu->regs[TU_REG_DATAL]; |
| msg.flags = I2C_M_RD; |
| msg.len = tu->regs[TU_REG_DATAH]; |
| break; |
| |
| case TU_CMD_SMBUS_HOST_NOTIFY: |
| msg.addr = 0x08; |
| msg.flags = 0; |
| msg.len = 3; |
| msgbuf[0] = tu->client->addr; |
| msgbuf[1] = tu->regs[TU_REG_DATAL]; |
| msgbuf[2] = tu->regs[TU_REG_DATAH]; |
| break; |
| |
| case TU_CMD_SMBUS_ALERT_REQUEST: |
| i2c_slave_unregister(tu->client); |
| orig_addr = tu->client->addr; |
| tu->client->addr = 0x0c; |
| ret = i2c_slave_register(tu->client, i2c_slave_testunit_smbalert_cb); |
| if (ret) |
| goto out_smbalert; |
| |
| reinit_completion(&tu->alert_done); |
| gpiod_set_value(tu->gpio, 1); |
| time_left = wait_for_completion_timeout(&tu->alert_done, HZ); |
| if (!time_left) |
| ret = -ETIMEDOUT; |
| |
| i2c_slave_unregister(tu->client); |
| out_smbalert: |
| tu->client->addr = orig_addr; |
| i2c_slave_register(tu->client, i2c_slave_testunit_slave_cb); |
| break; |
| |
| default: |
| break; |
| } |
| |
| if (msg.addr != I2C_CLIENT_END) { |
| ret = i2c_transfer(tu->client->adapter, &msg, 1); |
| /* convert '0 msgs transferred' to errno */ |
| ret = (ret == 0) ? -EIO : ret; |
| } |
| |
| if (ret < 0) |
| dev_err(&tu->client->dev, "CMD%02X failed (%d)\n", tu->regs[TU_REG_CMD], ret); |
| |
| clear_bit(TU_FLAG_IN_PROCESS, &tu->flags); |
| } |
| |
| static int i2c_slave_testunit_probe(struct i2c_client *client) |
| { |
| struct testunit_data *tu; |
| |
| tu = devm_kzalloc(&client->dev, sizeof(struct testunit_data), GFP_KERNEL); |
| if (!tu) |
| return -ENOMEM; |
| |
| tu->client = client; |
| i2c_set_clientdata(client, tu); |
| init_completion(&tu->alert_done); |
| INIT_DELAYED_WORK(&tu->worker, i2c_slave_testunit_work); |
| |
| tu->gpio = devm_gpiod_get_index_optional(&client->dev, NULL, 0, GPIOD_OUT_LOW); |
| if (gpiod_cansleep(tu->gpio)) { |
| dev_err(&client->dev, "GPIO access which may sleep is not allowed\n"); |
| return -EDEADLK; |
| } |
| |
| if (sizeof(tu_version_info) > TU_VERSION_MAX_LENGTH) |
| tu_version_info[TU_VERSION_MAX_LENGTH - 1] = 0; |
| |
| return i2c_slave_register(client, i2c_slave_testunit_slave_cb); |
| }; |
| |
| static void i2c_slave_testunit_remove(struct i2c_client *client) |
| { |
| struct testunit_data *tu = i2c_get_clientdata(client); |
| |
| cancel_delayed_work_sync(&tu->worker); |
| i2c_slave_unregister(client); |
| } |
| |
| static const struct i2c_device_id i2c_slave_testunit_id[] = { |
| { "slave-testunit" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(i2c, i2c_slave_testunit_id); |
| |
| static struct i2c_driver i2c_slave_testunit_driver = { |
| .driver = { |
| .name = "i2c-slave-testunit", |
| }, |
| .probe = i2c_slave_testunit_probe, |
| .remove = i2c_slave_testunit_remove, |
| .id_table = i2c_slave_testunit_id, |
| }; |
| module_i2c_driver(i2c_slave_testunit_driver); |
| |
| MODULE_AUTHOR("Wolfram Sang <wsa@sang-engineering.com>"); |
| MODULE_DESCRIPTION("I2C slave mode test unit"); |
| MODULE_LICENSE("GPL v2"); |