diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index 0f721e757e..56a4fb07fc 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory_ifdef(CONFIG_DISPLAY display) add_subdirectory_ifdef(CONFIG_GPIO gpio) +add_subdirectory_ifdef(CONFIG_I2C i2c) add_subdirectory_ifdef(CONFIG_MFD mfd) add_subdirectory_ifdef(CONFIG_RTC rtc) add_subdirectory_ifdef(CONFIG_SENSOR sensor) diff --git a/drivers/Kconfig b/drivers/Kconfig index 6a3b62babc..76563505d1 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -6,6 +6,7 @@ menu "Device Drivers" rsource "display/Kconfig" rsource "gpio/Kconfig" +rsource "i2c/Kconfig" rsource "mfd/Kconfig" rsource "rtc/Kconfig" rsource "sensor/Kconfig" diff --git a/drivers/i2c/CMakeLists.txt b/drivers/i2c/CMakeLists.txt new file mode 100644 index 0000000000..28cd99b284 --- /dev/null +++ b/drivers/i2c/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +# Add to the existing i2c library from zephyr instead of defining our own. +# It is actually required to add I2C drivers to that library, otherwise +# build errors can occur! +zephyr_library_amend() + +zephyr_library_sources_ifdef(CONFIG_I2C_SC18IS604 i2c_sc18is604.c) +zephyr_library_sources_ifdef(CONFIG_I2C_CALLBACK i2c_sc18is604_callback.c) diff --git a/drivers/i2c/Kconfig b/drivers/i2c/Kconfig new file mode 100644 index 0000000000..375a15d50c --- /dev/null +++ b/drivers/i2c/Kconfig @@ -0,0 +1,8 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +if I2C + +rsource "Kconfig.sc18is604" + +endif # I2C diff --git a/drivers/i2c/Kconfig.sc18is604 b/drivers/i2c/Kconfig.sc18is604 new file mode 100644 index 0000000000..b07f937c4f --- /dev/null +++ b/drivers/i2c/Kconfig.sc18is604 @@ -0,0 +1,29 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +menuconfig I2C_SC18IS604 + bool "I2C controller part of an SC18IM604 bridge" + default y + depends on DT_HAS_NXP_SC18IS604_I2C_ENABLED + depends on MFD_SC18IS604 + select MFD_SC18IS604_ASYNC + select POLL # dependency of MFD_SC18IS604_ASYNC + select DYNAMIC_THREAD # dependency of MFD_SC18IS604_ASYNC + select THREAD_STACK_INFO # dependency of DYNAMIC_THREAD + help + Enable driver for I2C controller part of an SC18IM604 bridge. + +config I2C_SC18IS604_INIT_PRIORITY + int "Init priority" + default 60 + depends on I2C_SC18IS604 + help + Device driver initialization priority. + +if I2C_CALLBACK + + config HEAP_MEM_POOL_ADD_SIZE_I2C_SC18IS604 + int + default 512 # Enough for 5 in-flight async transfers + +endif diff --git a/drivers/i2c/i2c_sc18is604.c b/drivers/i2c/i2c_sc18is604.c new file mode 100644 index 0000000000..99c3dfdf82 --- /dev/null +++ b/drivers/i2c/i2c_sc18is604.c @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef CONFIG_MFD_SC18IS604_ASYNC +#error "CONFIG_MFD_SC18IS604_ASYNC must be enabled for I2C controller to function." +#endif // CONFIG_MFD_SC18IS604_ASYNC + +#define DT_DRV_COMPAT nxp_sc18is604_i2c + +#include +#include +#include + +#include + +#include +#include "i2c_sc18is604_priv.h" + +#include +LOG_MODULE_REGISTER(sc18is604_i2c, CONFIG_I2C_LOG_LEVEL); + +int await_signal(struct k_poll_signal *signal, int *result, k_timeout_t timeout) +{ + struct k_poll_event events[1] = { + K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + signal), + }; + + k_poll(events, 1, timeout); + + int signaled = 0; + + k_poll_signal_check(signal, &signaled, result); + + return signaled; +} + + + +/* Initialize reading out the interrupt source. */ +static void i2c_sc18is604_interrupt_work_fn_initial(struct k_work *work) +{ + i2c_sc18is604_data_t * const data = CONTAINER_OF(work, + i2c_sc18is604_data_t, + interrupt_work_initial); + const struct device *dev = data->dev; + const i2c_sc18is604_config_t * const config = dev->config; + int ret = 0; + + /* Soft spinning on interrupt handling lock */ + ret = k_sem_take(&data->interrupt_lock, K_NO_WAIT); + if (ret != 0) { + k_work_submit(work); + return; + } + + /* + * Begin asynchronous readout of i2c status register (this resets the + * hardware interrupt signal). + */ + k_poll_signal_reset(&data->interrupt_handling_data.signal); + ret = READ_SC18IS604_REG_SIGNAL(config->parent_dev, I2C_STATUS, + &data->interrupt_handling_data.i2cstat, + &data->interrupt_handling_data.signal); + if (ret != 0) { + /* Failed to read out I2C status, retry */ + k_work_submit(work); + k_sem_give(&data->interrupt_lock); + return; + } + + /* Submit work item for awaiting result */ + ret = k_work_schedule(&data->interrupt_work_final, K_NO_WAIT); + if (ret != 0 && ret != 1) { + /* + * ret == 0 shouldn't happen, but is permissible: at least we + * know the item will run. Other return values mean we can't + * rely on the item to run, so we must release the lock here to + * prevent a deadlock on the next interrupt. + */ + k_sem_give(&data->interrupt_lock); + } +} + +/* Await the result of reading out the interrupt source and act on it. */ +static void i2c_sc18is604_interrupt_work_fn_final(struct k_work *work) +{ + struct k_work_delayable *work_delayable = CONTAINER_OF(work, struct k_work_delayable, work); + i2c_sc18is604_data_t * const data = CONTAINER_OF(work_delayable, + i2c_sc18is604_data_t, + interrupt_work_final); + + /* Check result of i2c status readout */ + int result = 0; + int signaled = await_signal(&data->interrupt_handling_data.signal, + &result, + K_NO_WAIT); + + if (!signaled) { + /* Transfer not complete, keep spinning */ + k_work_schedule(work_delayable, K_MSEC(1)); + return; + } + + /* If bus transfer failed, try again */ + if (result != 0) { + k_work_submit(&data->interrupt_work_initial); + goto end; + } + + /* Otherwise, propagate the received status via signal */ + k_poll_signal_raise(&data->interrupt_signal, + data->interrupt_handling_data.i2cstat); + +end: + /* Release interrupt handling lock */ + k_sem_give(&data->interrupt_lock); +} + +/* + * Interrupt callback that is registered with the parent MFD which runs in ISR + * context. + */ +static void i2c_sc18is604_interrupt_callback(const struct device *dev, + struct gpio_callback *cb, + gpio_port_pins_t pins) +{ + i2c_sc18is604_data_t * const data = CONTAINER_OF(cb, + i2c_sc18is604_data_t, + interrupt_callback); + + /* + * Reading out the interrupt source requires bus communication, so we do + * it via work queue items + */ + k_work_submit(&data->interrupt_work_initial); +} + +static inline int i2c_sc18is604_set_clock_speed(const struct device *dev, + uint32_t speed) +{ + const i2c_sc18is604_config_t * const config = dev->config; + uint8_t value; + + switch (speed) { + case I2C_SPEED_STANDARD: + value = SC18IS604_I2C_CLOCK_99KHZ; + break; + case I2C_SPEED_FAST: + value = SC18IS604_I2C_CLOCK_375KHZ; + break; + default: + return -EINVAL; + } + + return WRITE_SC18IS604_REG(config->parent_dev, I2C_CLOCK, value); +} + +static int i2c_sc18is604_write_message(const struct device *dev, + uint8_t *data, size_t len, + uint16_t addr) +{ + const i2c_sc18is604_config_t * const config = dev->config; + i2c_sc18is604_data_t * const drv_data = dev->data; + int ret; + + uint8_t cmd[] = { + SC18IS604_CMD_WRITE_I2C, + len, (uint8_t) addr + }; + + /* Reset interrupt signal */ + k_poll_signal_reset(&drv_data->interrupt_signal); + + /* Send 'I2C WRITE' command with data attached */ + ret = mfd_sc18is604_transfer(config->parent_dev, + cmd, ARRAY_SIZE(cmd), + data, len, NULL, 0); + if (ret != 0) { + return ret; + } + /* Await interrupt signal */ + int result = 0; + int signaled = await_signal(&drv_data->interrupt_signal, &result, K_FOREVER); + + if (!signaled) { + return -EIO; + } + + if (result != SC18IS604_I2C_STATUS_SUCCESS) { + return -EIO; + } + + return 0; +} + +static int i2c_sc18is604_read_message(const struct device *dev, + uint8_t *data, size_t len, + uint16_t addr) +{ + const i2c_sc18is604_config_t * const config = dev->config; + i2c_sc18is604_data_t * const drv_data = dev->data; + int ret = 0; + + uint8_t cmd[] = { + SC18IS604_CMD_READ_I2C, + len, (uint8_t) addr + }; + + /* Claim lock on MFD */ + mfd_sc18is604_claim(config->parent_dev, K_FOREVER); + + /* Reset interrupt signal */ + k_poll_signal_reset(&drv_data->interrupt_signal); + + /* Send 'I2C READ' command */ + ret = mfd_sc18is604_transfer(config->parent_dev, + cmd, ARRAY_SIZE(cmd), + NULL, 0, NULL, 0); + if (ret != 0) { + goto release_and_return; + } + + /* Await interrupt signal */ + int result = 0; + int signaled = await_signal(&drv_data->interrupt_signal, &result, K_FOREVER); + + if (!signaled) { + ret = -EIO; + goto release_and_return; + } + + if (result != SC18IS604_I2C_STATUS_SUCCESS) { + ret = -EIO; + goto release_and_return; + } + + /* Read out the received data */ + ret = mfd_sc18is604_read_buffer(config->parent_dev, data, len); + if (ret != 0) { + ret = -EIO; + goto release_and_return; + } + +release_and_return: + mfd_sc18is604_release(config->parent_dev); + return ret; +} + +static int i2c_sc18is604_configure(const struct device *dev, uint32_t config) +{ + i2c_sc18is604_data_t * const data = dev->data; + + /* Device can only act as controller */ + if ((config & I2C_MODE_CONTROLLER) != I2C_MODE_CONTROLLER) { + return -EINVAL; + } + + /* Controller doesn't support 10bit addressing */ + if ((config & I2C_ADDR_10_BITS) == I2C_ADDR_10_BITS) { + return -EINVAL; + } + + /* Adjust bus speed if necessary */ + uint32_t speed = I2C_SPEED_GET(config); + + if (speed != I2C_SPEED_GET(data->i2c_config)) { + + /* + * Controller only supports 'standard' (100kHz) and 'fast' + * (400kHz) speeds. + */ + if ((speed == I2C_SPEED_STANDARD) || + (speed == I2C_SPEED_FAST)) { + + int ret = i2c_sc18is604_set_clock_speed(dev, speed); + + if (ret != 0) { + return ret; + } + + } else { + return -EINVAL; + } + } + + /* Store new config */ + data->i2c_config = config; + + return 0; +} + +static int i2c_sc18is604_get_config(const struct device *dev, uint32_t *config) +{ + const i2c_sc18is604_data_t * const data = dev->data; + *config = data->i2c_config; + return 0; +} + +static int i2c_sc18is604_transfer_msg(const struct device *dev, + struct i2c_msg *msg, uint16_t addr) +{ + /* Device doesn't support 10bit addressing */ + if ((msg->flags & I2C_MSG_ADDR_10_BITS) == I2C_MSG_ADDR_10_BITS) { + return -EINVAL; + } + + /* Add RW bit to address */ + addr <<= 1; + addr |= (msg->flags & I2C_MSG_RW_MASK); + + if ((msg->flags & I2C_MSG_READ) == I2C_MSG_READ) { + return i2c_sc18is604_read_message(dev, msg->buf, + msg->len, addr); + } else { + return i2c_sc18is604_write_message(dev, msg->buf, + msg->len, addr); + } + + /* Should be unreachable */ + return -EINVAL; +} + +static int i2c_sc18is604_transfer(const struct device *dev, + struct i2c_msg *msgs, + uint8_t num_msgs, + uint16_t addr) +{ + i2c_sc18is604_data_t * const data = dev->data; + int ret; + + if (num_msgs == 0) { + return 0; + } + + /* Lock driver instance before multi-transfer transaction */ + k_sem_take(&data->lock, K_FOREVER); + + /* Handle messages one-by-one */ + for (int i = 0; i < num_msgs; i++) { + ret = i2c_sc18is604_transfer_msg(dev, &msgs[i], addr); + if (ret != 0) { + break; + } + } + + /* Release lock */ + k_sem_give(&data->lock); + + return ret; +} + +static const struct i2c_driver_api i2c_sc18is604_api = { + .configure = i2c_sc18is604_configure, + .get_config = i2c_sc18is604_get_config, + .transfer = i2c_sc18is604_transfer, +#ifdef CONFIG_I2C_CALLBACK + .transfer_cb = i2c_sc18is604_transfer_cb, +#endif /* CONFIG_I2C_CALLBACK */ +}; + +static int i2c_sc18is604_init(const struct device *dev) +{ + const i2c_sc18is604_config_t * const config = dev->config; + i2c_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Check parent bus readiness */ + if (!device_is_ready(config->parent_dev)) { + LOG_ERR("%s: MFD device %s not ready", + dev->name, config->parent_dev->name); + return -ENODEV; + } + + /* Set back-reference */ + data->dev = dev; + + /* Initialize locks (both initially open) */ + k_sem_init(&data->lock, 1, 1); + k_sem_init(&data->interrupt_lock, 1, 1); + + /* Initialize interrupt signal */ + k_poll_signal_init(&data->interrupt_signal); + + /* Register interrupt callback with parent MFD */ + gpio_init_callback(&data->interrupt_callback, + i2c_sc18is604_interrupt_callback, + 0xffffff); + ret = mfd_sc18is604_add_callback(config->parent_dev, &data->interrupt_callback); + if (ret != 0) { + LOG_ERR("Failed to register interrupt callback for %s with %s", + config->parent_dev->name, + dev->name); + return ret; + } + + /* Set up work items for interrupt handling */ + k_work_init(&data->interrupt_work_initial, i2c_sc18is604_interrupt_work_fn_initial); + k_work_init_delayable(&data->interrupt_work_final, i2c_sc18is604_interrupt_work_fn_final); + + /* Initialize data used by interrupt handling work items */ + k_poll_signal_init(&data->interrupt_handling_data.signal); + + LOG_DBG("%s: ready over %s!", dev->name, + config->parent_dev->name); + return 0; +} + +#ifdef CONFIG_PM_DEVICE +static int i2c_sc18is604_pm_device_pm_action(const struct device *dev, + enum pm_device_action action) +{ + ARG_UNUSED(dev); + ARG_UNUSED(action); + + return 0; +} +#endif + +#define I2C_SC18IS604_DEFINE(inst) \ +\ + static const i2c_sc18is604_config_t i2c_sc18is604_config_##inst = \ + { \ + .parent_dev = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + }; \ +\ + static i2c_sc18is604_data_t i2c_sc18is604_data_##inst = { }; \ +\ + PM_DEVICE_DT_INST_DEFINE(inst, i2c_sc18is604_pm_device_pm_action); \ +\ + DEVICE_DT_INST_DEFINE(inst, i2c_sc18is604_init, \ + PM_DEVICE_DT_INST_GET(inst), \ + &i2c_sc18is604_data_##inst, \ + &i2c_sc18is604_config_##inst, POST_KERNEL, \ + CONFIG_I2C_SC18IS604_INIT_PRIORITY, \ + &i2c_sc18is604_api); + +DT_INST_FOREACH_STATUS_OKAY(I2C_SC18IS604_DEFINE); diff --git a/drivers/i2c/i2c_sc18is604_callback.c b/drivers/i2c/i2c_sc18is604_callback.c new file mode 100644 index 0000000000..39f8d4baec --- /dev/null +++ b/drivers/i2c/i2c_sc18is604_callback.c @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "i2c_sc18is604_priv.h" + +/* + * Asynchronous message transfers are processed by a tracking structure being + * passed between 3 separate work items: One for initiating transfers, one for + * performing RX buffer readouts, and one for awaiting completion of one message + * being processed. + * + * These work items can both re-schedule themselves to wait on data or locks + * without blocking, and they schedule each other to progress message + * processing. + * + * On completion, or if unrecoverable errors occur, each work item can call out + * to a shared completion function, which propagates the final return value to + * the user provided callback. + * + * A driver instance wide lock ensures that only one packet of messages given to + * `i2c_transfer_cb` is processed at the same time. Multiple calls can still be + * made in succession, but it is not guaranteed that they will be processed in + * the order they are submitted. + */ + +/** Data structure for tracking a asynchronous I2C transfer. */ +typedef struct i2c_sc18is604_transfer_cb_work { + /** Work items performing the transfer. */ + struct k_work work_init_msg; + struct k_work work_buffer_readout; + struct k_work work_finish_msg; + /** Device performing this transfer. */ + const struct device *dev; + /** Whether this transfer holds the transaction lock. */ + bool owns_lock; + /** Signal used during processing to await individual message transfers. */ + struct k_poll_signal signal; + /** Index of the message currently being processed. */ + uint8_t msg_index; + /** Messages to be processed. */ + struct i2c_msg *msgs; + /** Number of messages to be processed. */ + uint8_t num_msgs; + /** I2C bus address the messages should be sent to. */ + uint16_t addr; + /** User callback to be called once all messages are processed. */ + i2c_callback_t cb; + /** User provided data for the callback. */ + void *userdata; +} transfer_work_t; + +size_t size = sizeof(transfer_work_t); + +/* Finish an asynchronous transfer with some result, cleaning up locks and resources. */ +static void i2c_sc18is604_transfer_cb_process_done(transfer_work_t *transfer_work, + int result) +{ + const struct device *dev = transfer_work->dev; + i2c_sc18is604_data_t * const data = dev->data; + + /* Release driver lock */ + k_sem_give(&data->lock); + + /* Free this work item */ + i2c_callback_t cb = transfer_work->cb; + void *userdata = transfer_work->userdata; + + k_free(transfer_work); + + /* Fire user callback */ + cb(dev, result, userdata); +} + +static void i2c_sc18is604_init_msg_read(transfer_work_t *transfer_work, + uint16_t addr, struct i2c_msg *msg) +{ + const struct device *dev = transfer_work->dev; + const i2c_sc18is604_config_t * const config = dev->config; + int ret = 0; + + /* Call asynchronous transfer function with our internal signal. */ + uint8_t cmd[] = {SC18IS604_CMD_READ_I2C, msg->len, (uint8_t) addr}; + + ret = mfd_sc18is604_transfer_signal(config->parent_dev, + cmd, ARRAY_SIZE(cmd), + NULL, 0, NULL, 0, + &transfer_work->signal); + if (ret != 0) { + mfd_sc18is604_release(config->parent_dev); + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + + /* Schedule next work item to continue processing this message */ + ret = k_work_submit(&transfer_work->work_buffer_readout); + if (ret != 1 && ret != 0) { + mfd_sc18is604_release(config->parent_dev); + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } +} + +static void i2c_sc18is604_init_msg_write(transfer_work_t *transfer_work, + uint16_t addr, struct i2c_msg *msg) +{ + const struct device *dev = transfer_work->dev; + const i2c_sc18is604_config_t * const config = dev->config; + int ret = 0; + + uint8_t cmd[] = {SC18IS604_CMD_WRITE_I2C, msg->len, (uint8_t) addr}; + + /* Call asynchronous transfer function with our internal signal. */ + ret = mfd_sc18is604_transfer_signal(config->parent_dev, + cmd, ARRAY_SIZE(cmd), + msg->buf, msg->len, + NULL, 0, + &transfer_work->signal); + if (ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + + /* Schedule final work item to finish processing this message */ + ret = k_work_submit(&transfer_work->work_finish_msg); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } +} + +static void i2c_sc18is604_init_msg_work_fn(struct k_work *work) +{ + transfer_work_t *transfer_work = CONTAINER_OF(work, transfer_work_t, work_init_msg); + const struct device *dev = transfer_work->dev; + i2c_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Get device lock, or reschedule to retry later */ + if (!transfer_work->owns_lock) { + ret = k_sem_take(&data->lock, K_NO_WAIT); + if (ret != 0) { + ret = k_work_submit(work); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + return; + } + } + + /* Make sure there is a message to transfer */ + uint8_t index = transfer_work->msg_index; + + if (index >= transfer_work->num_msgs) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, 0); + } + + /* Reset signals */ + /* Signals completion of communication *with* the device */ + k_poll_signal_reset(&transfer_work->signal); + /* Signals comletion of the command *by* the device */ + k_poll_signal_reset(&data->interrupt_signal); + + /* Begin processing the i-th message */ + struct i2c_msg *msg = &transfer_work->msgs[index]; + + /* Device doesn't support 10bit addressing */ + if ((msg->flags & I2C_MSG_ADDR_10_BITS) == I2C_MSG_ADDR_10_BITS) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EINVAL); + return; + } + + /* Add RW bit to address */ + uint16_t addr = transfer_work->addr; + + addr <<= 1; + addr |= (msg->flags & I2C_MSG_RW_MASK); + + /* Process message depending on type */ + if ((msg->flags & I2C_MSG_READ) == I2C_MSG_READ) { + i2c_sc18is604_init_msg_read(transfer_work, addr, msg); + return; + } + + /* If it's not a 'read' message, it must be a 'write' message */ + i2c_sc18is604_init_msg_write(transfer_work, addr, msg); +} + +static void i2c_sc18is604_buffer_readout_work_fn(struct k_work *work) +{ + transfer_work_t *transfer_work = CONTAINER_OF(work, transfer_work_t, work_buffer_readout); + const struct device *dev = transfer_work->dev; + const i2c_sc18is604_config_t * const config = dev->config; + i2c_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Await completion of the previous transfer */ + int result = 0; + int signaled = await_signal(&transfer_work->signal, &result, K_NO_WAIT); + + if (!signaled) { + ret = k_work_submit(work); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + return; + } + + if (result != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + + /* Await interrupt signal */ + signaled = await_signal(&data->interrupt_signal, &result, K_NO_WAIT); + if (!signaled) { + /* + * We didn't reset the transfer signal, so we'll fall through to + * awaiting the interrupt again next time. + */ + ret = k_work_submit(work); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + return; + } + + /* + * Check contents of I2C_STATUS register (propagated through interrupt + * signal) + */ + if (result != SC18IS604_I2C_STATUS_SUCCESS) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + + /* Reset signal */ + k_poll_signal_reset(&transfer_work->signal); + + /* Begin readout of the received data from device buffer */ + uint8_t index = transfer_work->msg_index; + struct i2c_msg *msg = &transfer_work->msgs[index]; + + ret = mfd_sc18is604_read_buffer_signal(config->parent_dev, + msg->buf, msg->len, + &transfer_work->signal); + if (ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + + /* Schedule final work item to finish processing this message */ + ret = k_work_submit(&transfer_work->work_finish_msg); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } +} + +static void i2c_sc18is604_finish_msg_work_fn(struct k_work *work) +{ + transfer_work_t *transfer_work = CONTAINER_OF(work, transfer_work_t, work_finish_msg); + const struct device *dev = transfer_work->dev; + i2c_sc18is604_data_t * const data = dev->data; + int ret = 0; + + int result = 0; + int signaled = await_signal(&transfer_work->signal, &result, K_NO_WAIT); + + if (!signaled) { + ret = k_work_submit(work); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + return; + } + + /* + * For I2C_WRITE messages, we also need to await the interrupt signal. + * For I2C_READ messages, we're awaiting completion of an RX buffer + * readout, which doesn't require confirmation via interrupt. + */ + + uint8_t index = transfer_work->msg_index; + struct i2c_msg *msg = &transfer_work->msgs[index]; + + if ((msg->flags & I2C_MSG_READ) != I2C_MSG_READ) { + signaled = await_signal(&data->interrupt_signal, &result, K_NO_WAIT); + if (!signaled) { + ret = k_work_submit(work); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + return; + } + + /* + * Check status read from I2C_STATUS (returned through interrupt + * signal). + */ + if (result != SC18IS604_I2C_STATUS_SUCCESS) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } + } + + /* We've finished processing this message. Check if there are more. */ + transfer_work->msg_index += 1; + if (transfer_work->msg_index >= transfer_work->num_msgs) { + /* We've successfully processed all messages */ + i2c_sc18is604_transfer_cb_process_done(transfer_work, 0); + return; + } + + /* Schedule work item to begin processing the next message */ + ret = k_work_submit(&transfer_work->work_init_msg); + if (ret != 1 && ret != 0) { + i2c_sc18is604_transfer_cb_process_done(transfer_work, -EIO); + return; + } +} + +int i2c_sc18is604_transfer_cb(const struct device *dev, + struct i2c_msg *msgs, uint8_t num_msgs, + uint16_t addr, + i2c_callback_t cb, void *userdata) +{ + int ret = 0; + + /* Create work item for tracking this transfer */ + transfer_work_t *transfer_work = k_calloc(1, sizeof(transfer_work_t)); + + if (transfer_work == NULL) { + return -ENOMEM; + } + + *transfer_work = (transfer_work_t) { + .dev = dev, + .owns_lock = false, + .msg_index = 0, + .msgs = msgs, + .num_msgs = num_msgs, + .addr = addr, + .cb = cb, + .userdata = userdata, + }; + k_work_init(&transfer_work->work_init_msg, i2c_sc18is604_init_msg_work_fn); + k_work_init(&transfer_work->work_buffer_readout, i2c_sc18is604_buffer_readout_work_fn); + k_work_init(&transfer_work->work_finish_msg, i2c_sc18is604_finish_msg_work_fn); + k_poll_signal_init(&transfer_work->signal); + + ret = k_work_submit(&transfer_work->work_init_msg); + if (ret != 1 && ret != 0) { + return -EAGAIN; + } + + return 0; +} diff --git a/drivers/i2c/i2c_sc18is604_priv.h b/drivers/i2c/i2c_sc18is604_priv.h new file mode 100644 index 0000000000..7bcc3629e0 --- /dev/null +++ b/drivers/i2c/i2c_sc18is604_priv.h @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _I2C_SC18IS604_H_ +#define _I2C_SC18IS604_H_ + +#include +#include +#include +#include + + +/** + * @brief SC18IM604 I2C controller configuration data + * + * This structure contains all of the state for a given SC18IM604 I2C controller + * as well as the binding to related MFD device. + */ +typedef struct i2c_sc18is604_config { + /** Parent MFD device for real operations on hardware. */ + const struct device *parent_dev; +} i2c_sc18is604_config_t; + +/** + * @brief SC18IS604 I2C controller data + * + * This structure contains data structures used by a SC18IM604 I2C controller. + * + */ +typedef struct i2c_sc18is604_data { + /** Back-reference to driver instance. */ + const struct device *dev; + /** I2C bus configuration flags. */ + uint32_t i2c_config; + /** Lock for transactions. */ + struct k_sem lock; + /** Interrupt handling callback */ + struct gpio_callback interrupt_callback; + /** Lock for ongoing interrupt handling. */ + struct k_sem interrupt_lock; + /** Signal for waiting on interrupts. */ + struct k_poll_signal interrupt_signal; + /** Work items for interrupt handling */ + struct k_work interrupt_work_initial; + struct k_work_delayable interrupt_work_final; + /** Struct for passing data between interrupt handling work items. */ + struct sc18is604_interrupt_handling_data { + uint8_t i2cstat; + struct k_poll_signal signal; + } interrupt_handling_data; +} i2c_sc18is604_data_t; + +/** + * + * @brief Await a signal being raised. + * + * @param signal The signal to await. + * @param[out] result The result attached to the raised signal. + * @param timeout Timeout before awaiting the signal is aborted. + * + * @retval 1 If the signal was raised. + * @retval 0 If the signal was not raised within the timeout. + * @return Negative error code if some failure occurs during k_poll(). + */ +int await_signal(struct k_poll_signal *signal, int *result, k_timeout_t timeout); + +/** + * @brief Transfer I2C messages asynchronously. + * + * @param dev An SC18IS604 I2C controller. + * @param msgs I2C messages to be transmitted. Pointer must remain valid for the + * duration of the transfer. + * @param num_msgs Number of messages to be transmitted. + * @param addr I2C address of the target device. + * @param cb Callback to be invoked on transfer completion (or failure). + * @param userdata User data passed to callback. + */ +int i2c_sc18is604_transfer_cb(const struct device *dev, + struct i2c_msg *msgs, uint8_t num_msgs, + uint16_t addr, + i2c_callback_t cb, void *userdata); + +#endif /* _I2C_SC18IS604_H_ */ diff --git a/drivers/mfd/Kconfig.sc18is604 b/drivers/mfd/Kconfig.sc18is604 index da9e8c4680..2a36cd9144 100644 --- a/drivers/mfd/Kconfig.sc18is604 +++ b/drivers/mfd/Kconfig.sc18is604 @@ -19,6 +19,7 @@ config MFD_SC18IS604_INIT_PRIORITY config MFD_SC18IS604_ASYNC bool "Enable asynchronous transfer functions over SC18IS604 bus" + default y if I2C_SC18IS604 default n depends on POLL depends on DYNAMIC_THREAD diff --git a/dts/bindings/i2c/nxp,sc18is604-i2c.yaml b/dts/bindings/i2c/nxp,sc18is604-i2c.yaml new file mode 100644 index 0000000000..1b9f52966d --- /dev/null +++ b/dts/bindings/i2c/nxp,sc18is604-i2c.yaml @@ -0,0 +1,10 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +description: I2C controller part of an SC18IM604 bridge + +compatible: "nxp,sc18is604-i2c" + +include: [i2c-controller.yaml] + +on-bus: nxp,sc18is604