diff --git a/doc/bridle/releases/release-notes-3.7.0.rst b/doc/bridle/releases/release-notes-3.7.0.rst index 0ab40c4d28..fea36f58bc 100644 --- a/doc/bridle/releases/release-notes-3.7.0.rst +++ b/doc/bridle/releases/release-notes-3.7.0.rst @@ -240,6 +240,7 @@ These GitHub issues were addressed since project bootstrapping: * :github:`254` - [FCR] Bump to Zephyr v3.7 * :github:`252` - [FCR] Upgrade to Zephyr SDK 0.16.8 +* :github:`247` - [HW] NXP SC18IS604 SPI to I2C bridge * :github:`244` - [HW] Spotpear Raspberry Pi Pico LCD Modules as Shields * :github:`242` - [HW] 52Pi (GeeekPi) Pico Breadboard Kit -/Plus (EP-0164/0172) * :github:`239` - [HW] PiMoroni Raspberry Pi Pico LCD Modules as Shields 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/gpio/CMakeLists.txt b/drivers/gpio/CMakeLists.txt index d10b46d790..9e8a407e8b 100644 --- a/drivers/gpio/CMakeLists.txt +++ b/drivers/gpio/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2021-2023 TiaC Systems +# Copyright (c) 2021-2024 TiaC Systems # SPDX-License-Identifier: Apache-2.0 # Add to the existing gpio library from zephyr instead of defining our own. @@ -6,6 +6,7 @@ # build errors can occur! zephyr_library_amend() -zephyr_library_sources_ifdef(CONFIG_GPIO_PCA9554 gpio_pca9554.c) -zephyr_library_sources_ifdef(CONFIG_GPIO_PCA9555 gpio_pca9555.c) -zephyr_library_sources_ifdef(CONFIG_GPIO_SIPOMUX gpio_sipomux.c) +zephyr_library_sources_ifdef(CONFIG_GPIO_PCA9554 gpio_pca9554.c) +zephyr_library_sources_ifdef(CONFIG_GPIO_PCA9555 gpio_pca9555.c) +zephyr_library_sources_ifdef(CONFIG_GPIO_SC18IS604 gpio_sc18is604.c) +zephyr_library_sources_ifdef(CONFIG_GPIO_SIPOMUX gpio_sipomux.c) diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig index fcd5f7a6e8..df977c92cb 100644 --- a/drivers/gpio/Kconfig +++ b/drivers/gpio/Kconfig @@ -1,10 +1,11 @@ -# Copyright (c) 2021-2023 TiaC Systems +# Copyright (c) 2021-2024 TiaC Systems # SPDX-License-Identifier: Apache-2.0 if GPIO rsource "Kconfig.pca9554" rsource "Kconfig.pca9555" +rsource "Kconfig.sc18is604" rsource "Kconfig.sipomux" endif # GPIO diff --git a/drivers/gpio/Kconfig.sc18is604 b/drivers/gpio/Kconfig.sc18is604 new file mode 100644 index 0000000000..94a8391901 --- /dev/null +++ b/drivers/gpio/Kconfig.sc18is604 @@ -0,0 +1,17 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +menuconfig GPIO_SC18IS604 + bool "GPIO controller part of an SC18IM604 bridge" + default y + depends on DT_HAS_NXP_SC18IS604_GPIO_ENABLED + depends on MFD_SC18IS604 + help + Enable driver for GPIO controller part of an SC18IM604 bridge. + +config GPIO_SC18IS604_INIT_PRIORITY + int "Init priority" + default 85 + depends on GPIO_SC18IS604 + help + Device driver initialization priority. diff --git a/drivers/gpio/gpio_sc18is604.c b/drivers/gpio/gpio_sc18is604.c new file mode 100644 index 0000000000..93530e2228 --- /dev/null +++ b/drivers/gpio/gpio_sc18is604.c @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT nxp_sc18is604_gpio + +#include +#include +#include + +#include +#include + +#include + +#include +LOG_MODULE_REGISTER(gpio_sc18is604, CONFIG_GPIO_LOG_LEVEL); + +/** + * @brief SC18IM604 GPIO controller configuration data + * + * @a num_pins must be in the range [1, @ref SC18IS604_IO_NUM_PINS_MAX]. + * + * This structure contains all of the state for a given SC18IM604 GPIO + * controller as well as the binding to related MFD device. + */ +typedef struct gpio_sc18is604_config { + /** Common @ref gpio_driver_config (needs to be first) */ + struct gpio_driver_config common; + /** Number of pins available in the given GPIO controller instance */ + const gpio_pin_t num_pins; + /** Backend MFD (bridge) device for real operations on hardware */ + const struct device *bridge; +} gpio_sc18is604_config_t; + +/** + * @brief SC18IM604 GPIO controller data + * + * This structure contains data structures used by a SC18IM604 GPIO + * controller. + * + * Changes to @ref gpio_sc18is604_data and @ref gpio_sc18is604_config are + * synchronized using @a k_sem. + */ +typedef struct gpio_sc18is604_data { + /** Common @ref gpio_driver_data (needs to be first) */ + struct gpio_driver_data common; + /** Semaphore to synchronize accesses to driver data and config */ + struct k_sem lock; + /** GPIO pin direction and driver configuration */ + uint8_t pin_config; + /** GPIO pin level state */ + uint8_t pin_state; +} gpio_sc18is604_data_t; + +static int gpio_sc18is604_pin_write_config(const struct device *dev, + uint8_t val) +{ + const gpio_sc18is604_config_t * const config = dev->config; + gpio_sc18is604_data_t * const data = dev->data; + int ret = 0; + + ret = WRITE_SC18IS604_REG(config->bridge, IO_CONFIG, val); + if (ret != 0) { + goto end; + } + + k_sem_take(&data->lock, K_FOREVER); + + data->pin_config = val; + + k_sem_give(&data->lock); + +end: + return ret; +} + +static int gpio_sc18is604_pin_read_config(const struct device *dev, + uint8_t *val) +{ + const gpio_sc18is604_config_t * const config = dev->config; + gpio_sc18is604_data_t * const data = dev->data; + int ret = 0; + + ret = READ_SC18IS604_REG(config->bridge, IO_CONFIG, val); + if (ret != 0) { + goto end; + } + + k_sem_take(&data->lock, K_FOREVER); + + data->pin_config = *val; + + k_sem_give(&data->lock); + +end: + return ret; +} + +static int gpio_sc18is604_pin_write_state(const struct device *dev, + uint8_t val) +{ + const gpio_sc18is604_config_t * const config = dev->config; + gpio_sc18is604_data_t * const data = dev->data; + int ret = 0; + + ret = WRITE_SC18IS604_REG(config->bridge, IO_STATE, val); + if (ret != 0) { + goto end; + } + + k_sem_take(&data->lock, K_FOREVER); + + data->pin_state = val; + + k_sem_give(&data->lock); + +end: + return ret; +} + +static int gpio_sc18is604_pin_read_state(const struct device *dev, + uint8_t *val) +{ + const gpio_sc18is604_config_t * const config = dev->config; + gpio_sc18is604_data_t * const data = dev->data; + int ret = 0; + + ret = READ_SC18IS604_REG(config->bridge, IO_STATE, val); + if (ret != 0) { + goto end; + } + + k_sem_take(&data->lock, K_FOREVER); + + data->pin_state = *val; + + k_sem_give(&data->lock); + +end: + return ret; +} + +static int gpio_sc18is604_pin_configure(const struct device *dev, + gpio_pin_t pin, gpio_flags_t flags) +{ + gpio_sc18is604_data_t * const data = dev->data; + uint8_t shift, bits, pin_config, pin_state; + int ret = 0; + + /* Can't do bus operations from an ISR */ + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + /* Can only configure SC18IS604_IO_NUM_PINS_INOUT pins */ + if (pin >= SC18IS604_IO_NUM_PINS_INOUT) { + + /* Can only handle SC18IS604_IO_NUM_PINS_MAX pins */ + if (pin >= SC18IS604_IO_NUM_PINS_MAX) { + return -ENOTSUP; + + /* SC18IS604_IO_NUM_PINS_MAX - SC18IS604_IO_NUM_PINS_INOUT */ + } else if ((flags & GPIO_INPUT) == GPIO_INPUT) { + + /* + * Do nothing for the highest pin numbers, + * input is the default and can not change. + */ + goto end; + + } else { + return -ENOTSUP; + } + } + + /* Does not support disconnected pin */ + if ((flags & (GPIO_INPUT | GPIO_OUTPUT)) == GPIO_DISCONNECTED) { + return -ENOTSUP; + } + + k_sem_take(&data->lock, K_FOREVER); + + pin_config = data->pin_config; + pin_state = data->pin_state; + + k_sem_give(&data->lock); + + /* Prepare new configuration for target pin */ + shift = 2 * pin; /* Each pin has two configuration bits */ + bits = 0b00; + + if ((flags & GPIO_INPUT) == GPIO_INPUT) { + bits = SC18IS604_IO_CONFIG_INPUT; + } else if ((flags & GPIO_OUTPUT) == GPIO_OUTPUT) { + if ((flags & GPIO_OPEN_DRAIN) == GPIO_OPEN_DRAIN) { + bits = SC18IS604_IO_CONFIG_OPEN_DRAIN; + } else { + bits = SC18IS604_IO_CONFIG_PUSH_PULL; + } + } + + pin_config &= ~(0b11 << shift); /* Clear config bits */ + pin_config |= (bits << shift); /* Set new config */ + + /* Apply configuration */ + ret = gpio_sc18is604_pin_write_config(dev, pin_config); + if (ret != 0) { + goto end; + } + + /* For output pins, state might also be configured */ + if ((flags & GPIO_OUTPUT) == GPIO_OUTPUT) { + if ((flags & GPIO_OUTPUT_INIT_HIGH) == GPIO_OUTPUT_INIT_HIGH) { + pin_state |= BIT(pin); + } else if ((flags & GPIO_OUTPUT_INIT_LOW) == GPIO_OUTPUT_INIT_LOW) { + pin_state &= ~BIT(pin); + } + + /* Apply pin state */ + ret = gpio_sc18is604_pin_write_state(dev, pin_state); + if (ret != 0) { + goto end; + } + } + +end: + return ret; +} + +static int gpio_sc18is604_get_raw(const struct device *dev, uint32_t *value) +{ + uint8_t buf = 0x00; + int ret = 0; + + /* Can't do bus operations from an ISR */ + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + /* Read current state from device */ + ret = gpio_sc18is604_pin_read_state(dev, &buf); + if (ret != 0) { + goto end; + } + + /* Return through pointer */ + *value = buf; + +end: + return ret; +} + +static int gpio_sc18is604_set_masked_raw(const struct device *dev, + uint32_t mask, uint32_t value) +{ + gpio_sc18is604_data_t * const data = dev->data; + uint8_t pin_state; + + /* Can't do operations from an ISR */ + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + k_sem_take(&data->lock, K_FOREVER); + + /* Get (cached) state, and set bits to value according to mask */ + pin_state = data->pin_state; + pin_state = (pin_state & ~mask) | (mask & value); /* BYTE macro ? */ + + k_sem_give(&data->lock); + + /* Apply new pin state */ + return gpio_sc18is604_pin_write_state(dev, pin_state); +} + +static int gpio_sc18is604_set_bits_raw(const struct device *dev, uint32_t pins) +{ + return gpio_sc18is604_set_masked_raw(dev, pins, pins); +} + +static int gpio_sc18is604_clear_bits_raw(const struct device *dev, uint32_t pins) +{ + return gpio_sc18is604_set_masked_raw(dev, pins, 0U); +} + +static int gpio_sc18is604_toggle_bits(const struct device *dev, uint32_t pins) +{ + gpio_sc18is604_data_t * const data = dev->data; + uint8_t pin_state; + + /* Can't do operations from an ISR */ + if (k_is_in_isr()) { + return -EWOULDBLOCK; + } + + k_sem_take(&data->lock, K_FOREVER); + + /* Get (cached) state, and toggle bits according to mask */ + pin_state = data->pin_state; + pin_state ^= pins; /* BYTE macro ? */ + + k_sem_give(&data->lock); + + /* Apply new pin state */ + return gpio_sc18is604_pin_write_state(dev, pin_state); +} + +static const struct gpio_driver_api gpio_sc18is604_api = { + .pin_configure = gpio_sc18is604_pin_configure, + .port_get_raw = gpio_sc18is604_get_raw, + .port_set_masked_raw = gpio_sc18is604_set_masked_raw, + .port_set_bits_raw = gpio_sc18is604_set_bits_raw, + .port_clear_bits_raw = gpio_sc18is604_clear_bits_raw, + .port_toggle_bits = gpio_sc18is604_toggle_bits, +}; + +static int gpio_sc18is604_init(const struct device *dev) +{ + const gpio_sc18is604_config_t * const config = dev->config; + gpio_sc18is604_data_t * const data = dev->data; + uint8_t buf; + + /* Check MFD (bridge) readiness */ + if (!device_is_ready(config->bridge)) { + LOG_ERR("%s: bridge device %s not ready", + dev->name, config->bridge->name); + return -ENODEV; + } + + k_sem_init(&data->lock, 1, 1); + + /* Initialize register cache */ + gpio_sc18is604_pin_read_state(dev, &buf); + gpio_sc18is604_pin_read_config(dev, &buf); + + LOG_DBG("%s: ready for %u pins with bridge over %s!", dev->name, + config->num_pins, config->bridge->name); + return 0; +} + +#ifdef CONFIG_PM_DEVICE +static int gpio_sc18is604_pm_device_pm_action(const struct device *dev, + enum pm_device_action action) +{ + ARG_UNUSED(dev); + ARG_UNUSED(action); + + return 0; +} +#endif + +#define GPIO_SC18IS604_DEFINE(inst) \ + \ + static const gpio_sc18is604_config_t gpio_sc18is604_config_##inst = \ + { \ + .common = { \ + .port_pin_mask = \ + GPIO_PORT_PIN_MASK_FROM_DT_INST(inst), \ + }, \ + .num_pins = DT_INST_PROP(inst, ngpios), \ + .bridge = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + }; \ + BUILD_ASSERT( \ + DT_INST_PROP(inst, ngpios) <= SC18IS604_IO_NUM_PINS_MAX, \ + "Too many ngpios"); \ + \ + static gpio_sc18is604_data_t gpio_sc18is604_data_##inst; \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, gpio_sc18is604_pm_device_pm_action); \ + \ + DEVICE_DT_INST_DEFINE(inst, gpio_sc18is604_init, \ + PM_DEVICE_DT_INST_GET(inst), \ + &gpio_sc18is604_data_##inst, \ + &gpio_sc18is604_config_##inst, POST_KERNEL,\ + CONFIG_GPIO_SC18IS604_INIT_PRIORITY, \ + &gpio_sc18is604_api); + +DT_INST_FOREACH_STATUS_OKAY(GPIO_SC18IS604_DEFINE); 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..031dc4ebe9 --- /dev/null +++ b/drivers/i2c/i2c_sc18is604.c @@ -0,0 +1,440 @@ +/* + * 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.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.h b/drivers/i2c/i2c_sc18is604.h new file mode 100644 index 0000000000..d2591de170 --- /dev/null +++ b/drivers/i2c/i2c_sc18is604.h @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_I2C_SC18IS604_H_ +#define ZEPHYR_DRIVERS_I2C_SC18IS604_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#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); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_DRIVERS_I2C_SC18IS604_H_ */ diff --git a/drivers/i2c/i2c_sc18is604_callback.c b/drivers/i2c/i2c_sc18is604_callback.c new file mode 100644 index 0000000000..b3be02c4e3 --- /dev/null +++ b/drivers/i2c/i2c_sc18is604_callback.c @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include "i2c_sc18is604.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; + +/* + * 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/mfd/CMakeLists.txt b/drivers/mfd/CMakeLists.txt index 9916467089..f1830874f3 100644 --- a/drivers/mfd/CMakeLists.txt +++ b/drivers/mfd/CMakeLists.txt @@ -10,3 +10,7 @@ zephyr_library_sources_ifdef(CONFIG_MFD_DS3231 mfd_ds3231.c) zephyr_library_sources_ifdef(CONFIG_MFD_SIPOMUXGP mfd_sipomuxgp.c mfd_sipomuxgp_workq.c) zephyr_library_sources_ifdef(CONFIG_MFD_SIPOMUXGP_SPI mfd_sipomuxgp_spi.c) + +zephyr_library_sources_ifdef(CONFIG_MFD_SC18IS604 mfd_sc18is604.c) +zephyr_library_sources_ifdef(CONFIG_MFD_SC18IS604 mfd_sc18is604_transfer.c) +zephyr_library_sources_ifdef(CONFIG_MFD_SC18IS604_ASYNC mfd_sc18is604_transfer_async.c) diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index ba219baf68..c844a35394 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -4,6 +4,7 @@ if MFD rsource "Kconfig.ds3231" +rsource "Kconfig.sc18is604" rsource "Kconfig.sipomuxgp" endif # MFD diff --git a/drivers/mfd/Kconfig.sc18is604 b/drivers/mfd/Kconfig.sc18is604 new file mode 100644 index 0000000000..2a36cd9144 --- /dev/null +++ b/drivers/mfd/Kconfig.sc18is604 @@ -0,0 +1,37 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +menuconfig MFD_SC18IS604 + bool "SC18IM604 SPI to I2C & GPIO bridge" + default y + depends on DT_HAS_NXP_SC18IS604_ENABLED + depends on SPI + help + Enable driver for SC18IM604 bridge. + +config MFD_SC18IS604_INIT_PRIORITY + int "Init priority" + default 55 if I2C_SC18IS604 + default MFD_INIT_PRIORITY + depends on MFD_SC18IS604 + help + Device driver initialization 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 + +if MFD_SC18IS604_ASYNC + + config MFD_SC18IS604_WORKQUEUE_STACK_SIZE + int + default 512 + + config HEAP_MEM_POOL_ADD_SIZE_MFD_SC18IS604 + int + default 256 + +endif # MFD_SC18IS604_ASYNC diff --git a/drivers/mfd/mfd_sc18is604.c b/drivers/mfd/mfd_sc18is604.c new file mode 100644 index 0000000000..53b4b8f082 --- /dev/null +++ b/drivers/mfd/mfd_sc18is604.c @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT nxp_sc18is604 + +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include "mfd_sc18is604.h" + +#include +LOG_MODULE_REGISTER(mfd_sc18is604, CONFIG_MFD_LOG_LEVEL); + +#if defined(CONFIG_MFD_SC18IS604_ASYNC) \ + && (!CONFIG_DYNAMIC_THREAD_POOL_SIZE && !defined(CONFIG_DYNAMIC_THREAD_ALLOC)) +#error "SC18IS604 MFD driver requires either CONFIG_DYNAMIC_THREAD_POOL_SIZE>0" \ + "or CONFIG_DYNAMIC_THREAD_ALLOC" +#endif + +int mfd_sc18is604_add_callback(const struct device *dev, + struct gpio_callback *callback) +{ + mfd_sc18is604_data_t * const data = dev->data; + + return gpio_manage_callback(&data->child_callbacks, callback, true); +} + +int mfd_sc18is604_remove_callback(const struct device *dev, + struct gpio_callback *callback) +{ + mfd_sc18is604_data_t * const data = dev->data; + + return gpio_manage_callback(&data->child_callbacks, callback, false); +} + +int mfd_sc18is604_claim(const struct device *dev, k_timeout_t timeout) +{ + mfd_sc18is604_data_t * const data = dev->data; + + return k_sem_take(&data->lock, timeout); +} + +void mfd_sc18is604_release(const struct device *dev) +{ + mfd_sc18is604_data_t * const data = dev->data; + + k_sem_give(&data->lock); +} + +/** + * @brief Request device version string. The string will be placed in the + * internal buffer. The device interrupt is set once the string is ready + * to be read from the buffer. + * + * @param dev An SC18IS604 MFD device. + * + * @return A value from mfd_sc18is604_transfer(). + */ +static int mfd_sc18is604_request_version_string(const struct device *dev) +{ + uint8_t cmd[] = {SC18IS604_CMD_VERSION_STRING}; + + return mfd_sc18is604_transfer(dev, cmd, ARRAY_SIZE(cmd), + NULL, 0, NULL, 0); +} + +/** + * @brief Set up GPIO pin. + * + * @param dev An SC18IS604 MFD device. + * @param gpio The GPIO specification from devicetree. + * @param flags Additional GPIO flags. + * + * @retval 0 On success. + * @return Negative error code on failure. + */ +static int mfd_sc18is604_configure_gpio_pin(const struct device *dev, + const struct gpio_dt_spec *gpio, + const gpio_flags_t flags) +{ + int ret = 0; + + if (gpio->port != NULL) { + LOG_DBG("%s: configure GPIO port %s, pin %d", + dev->name, gpio->port->name, gpio->pin); + + if (!gpio_is_ready_dt(gpio)) { + LOG_ERR("%s: GPIO port %s not ready", + dev->name, gpio->port->name); + return -ENODEV; + } + + ret = gpio_pin_configure_dt(gpio, flags); + if (ret != 0) { + LOG_ERR("%s: couldn't configure GPIO pin %d", + dev->name, gpio->pin); + return ret; + } + } else { + return -EINVAL; + } + + return 0; +} + +/** + * @brief Reset device. + * + * @param dev An SC18IS604 MFD device. + * + * @retval 0 On success. + * @return Negative error code on failure. + */ +static int mfd_sc18is604_chip_reset(const struct device *dev) +{ + const mfd_sc18is604_config_t * const config = dev->config; + int ret = 0; + + LOG_DBG("%s: resetting device", dev->name); + if (config->reset.port != NULL) { + ret = gpio_pin_set_dt(&config->reset, 1); + if (ret != 0) + goto end; + + /* Should suffice to pull for reset acceptance time. */ + k_busy_wait(MFD_SC18IS604_RESET_SETUP_USEC); + + ret = gpio_pin_set_dt(&config->reset, 0); + if (ret != 0) + goto end; + + /* + * Device needs some time before becoming + * responsive again after reset. + */ + k_busy_wait(MFD_SC18IS604_RESET_TIME_USEC); + } + +end: + return ret; +} + +static int mfd_sc18is604_clear_interrupt_source(const struct device *dev) +{ + uint8_t buf = 0; + /* Reset interrupt by reading from I2CStat register. */ + int ret = READ_SC18IS604_REG(dev, I2C_STATUS, &buf); + + return ret; +} + +static void mfd_sc18is604_interrupt_callback(const struct device *dev, + struct gpio_callback *cb, + gpio_port_pins_t pins) +{ + mfd_sc18is604_data_t * const data = CONTAINER_OF(cb, + mfd_sc18is604_data_t, + interrupt_callback); + + /* Raise internal signal */ + k_sem_give(&data->interrupt_signal); + + /* Fire registered callbacks */ + gpio_fire_callbacks(&data->child_callbacks, dev, pins); +} + +static int mfd_sc18is604_check_chipid(const struct device *dev, + k_timeout_t timeout) +{ + mfd_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Lock device */ + ret = mfd_sc18is604_claim(dev, timeout); + if (ret != 0) { + return -ETIMEDOUT; + } + + /* Prepare signal for interrupt signaling */ + k_sem_reset(&data->interrupt_signal); + + /* Send command to place ID string in device buffer */ + mfd_sc18is604_request_version_string(dev); + + /* Await interrupt signaling completion */ + ret = k_sem_take(&data->interrupt_signal, timeout); + if (ret != 0) { + ret = -ETIMEDOUT; + goto release_and_return; + } + + /* Clear hardware interrupt signal */ + mfd_sc18is604_clear_interrupt_source(dev); + + /* Read generated string from device buffer */ + char id[16] = {0}; + + ret = mfd_sc18is604_read_buffer(dev, (uint8_t *) id, 16); + if (ret != 0) { + ret = -EIO; + goto release_and_return; + } + + /* Compare with expected value */ + if (strncmp(MFD_SC18IS604_VERSION_CHIP, id, + strlen(MFD_SC18IS604_VERSION_CHIP)) != 0) { + LOG_ERR("%s: chip mismatch: expected %s, got %s", + dev->name, MFD_SC18IS604_VERSION_CHIP, id); + ret = -ENODEV; + goto release_and_return; + } + +release_and_return: + mfd_sc18is604_release(dev); + return ret; +} + +/** + * @brief Set up interrupt handling. + * + * @param dev An SC18IS604 MFD device. + * @param gpio The GPIO specification from devicetree. + * @param flags Additional GPIO flags. + * @retval 0 On success. + * @return Negative error code on failure. + */ +static int mfd_sc18is604_bind_interrupt(const struct device *dev, + const struct gpio_dt_spec *gpio, + const gpio_flags_t flags) +{ + mfd_sc18is604_data_t * const data = dev->data; + int ret = 0; + + if (gpio->port == NULL) { + return -EINVAL; + } + + LOG_DBG("%s: bind GPIO port %s, pin %d to interrupt handling", + dev->name, gpio->port->name, gpio->pin); + + ret = gpio_pin_interrupt_configure_dt(gpio, flags); + if (ret != 0) { + LOG_ERR("%s: couldn't enable interrupt on GPIO pin %d", + dev->name, gpio->pin); + return ret; + } + + gpio_init_callback(&data->interrupt_callback, + mfd_sc18is604_interrupt_callback, + BIT(gpio->pin)); + + ret = gpio_add_callback_dt(gpio, &data->interrupt_callback); + if (ret != 0) { + LOG_ERR("%s: couldn't register callback on GPIO port %s", + dev->name, gpio->port->name); + return ret; + } + + return 0; +} + +static int mfd_sc18is604_init(const struct device *dev) +{ + const mfd_sc18is604_config_t * const config = dev->config; + mfd_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Check parent bus readiness for bridge. */ + if (!spi_is_ready_dt(&config->spi)) { + LOG_ERR("%s: SPI device %s not ready", + dev->name, config->spi.bus->name); + return -ENODEV; + } + + /* Set driver instance back reference */ + data->dev = dev; + + /* Initialize bus lock (initially open) */ + k_sem_init(&data->lock, 1, 1); + + /* Initialize interrupt signal (initially open) */ + k_sem_init(&data->interrupt_signal, 0, 1); + +#ifdef CONFIG_MFD_SC18IS604_ASYNC + /* Initialize private work queue. */ + size_t work_queue_stack_size = CONFIG_MFD_SC18IS604_WORKQUEUE_STACK_SIZE; + + k_work_queue_init(&data->work_queue); + data->work_queue_stack = k_thread_stack_alloc(work_queue_stack_size, 0); + k_work_queue_start(&data->work_queue, data->work_queue_stack, + work_queue_stack_size, K_HIGHEST_THREAD_PRIO, NULL); +#endif /* CONFIG_MFD_SC18IS604_ASYNC */ + + /* Configure interrupt input pin. */ + ret = mfd_sc18is604_configure_gpio_pin(dev, &config->interrupt, + GPIO_INPUT); + if (ret != 0) { + LOG_ERR("%s: interrupt GPIO port not given", dev->name); + return -EINVAL; + } + + /* Set up interrupt handling */ + ret = mfd_sc18is604_bind_interrupt(dev, &config->interrupt, + GPIO_INT_EDGE_TO_ACTIVE); + if (ret != 0) { + LOG_ERR("%s: interrupt callback binding failed", dev->name); + goto end; + } + + /* Configure reset output pin. */ + ret = mfd_sc18is604_configure_gpio_pin(dev, &config->reset, + GPIO_OUTPUT_INACTIVE); + if (ret != 0) { + LOG_WRN("%s: reset GPIO port not given (work w/o chip reset)", + dev->name); + } + + /* Reset device. */ + ret = mfd_sc18is604_chip_reset(dev); + if (ret != 0) { + LOG_ERR("%s: couldn't reset device", dev->name); + goto end; + } + + /* Check Chip ID */ + ret = mfd_sc18is604_check_chipid(dev, K_SECONDS(5)); + if (ret != 0) { + return ret; + } + +end: + return ret; +} + +#ifdef CONFIG_PM_DEVICE +static int mfd_sc18is604_pm_device_pm_action(const struct device *dev, + enum pm_device_action action) +{ + ARG_UNUSED(dev); + ARG_UNUSED(action); + + return 0; +} +#endif + +#define MFD_SC18IS604_DEFINE(inst) \ + \ + static const mfd_sc18is604_config_t mfd_sc18is604_config_##inst = \ + { \ + .spi = SPI_DT_SPEC_INST_GET(inst, SPI_OP_MODE_MASTER | \ + SPI_TRANSFER_MSB | \ + SPI_HOLD_ON_CS | \ + SPI_LOCK_ON | \ + SPI_MODE_CPOL | \ + SPI_MODE_CPHA | \ + SPI_WORD_SET(8), 0), \ + .interrupt = GPIO_DT_SPEC_INST_GET(inst, interrupt_gpios), \ + .reset = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, { 0 }), \ + }; \ + BUILD_ASSERT(DT_INST_PROP(inst, spi_max_frequency) \ + >= MFD_SC18IS604_SPI_HZ_MIN, \ + "SPI bus clock too low"); \ + BUILD_ASSERT(DT_INST_PROP(inst, spi_max_frequency) \ + <= MFD_SC18IS604_SPI_HZ_MAX, \ + "SPI bus clock too high"); \ + \ + static mfd_sc18is604_data_t mfd_sc18is604_data_##inst = { 0 }; \ + \ + PM_DEVICE_DT_INST_DEFINE(inst, mfd_sc18is604_pm_device_pm_action); \ + \ + DEVICE_DT_INST_DEFINE(inst, mfd_sc18is604_init, \ + PM_DEVICE_DT_INST_GET(inst), \ + &mfd_sc18is604_data_##inst, \ + &mfd_sc18is604_config_##inst, POST_KERNEL, \ + CONFIG_MFD_SC18IS604_INIT_PRIORITY, NULL); + +DT_INST_FOREACH_STATUS_OKAY(MFD_SC18IS604_DEFINE); diff --git a/drivers/mfd/mfd_sc18is604.h b/drivers/mfd/mfd_sc18is604.h new file mode 100644 index 0000000000..1434b106bf --- /dev/null +++ b/drivers/mfd/mfd_sc18is604.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_MFD_SC18IS604_H_ +#define ZEPHYR_DRIVERS_MFD_SC18IS604_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include + +/* + * Values in Hz, intentionally to be comparable with the spi-max-frequency + * property from DT bindings in spi-device.yaml. + */ +#define MFD_SC18IS604_SPI_HZ_MIN 100000 +#define MFD_SC18IS604_SPI_HZ_MAX 1200000 + +#define MFD_SC18IS604_SPI_DELAY_USEC 8 /* between two SPI bytes */ +#define MFD_SC18IS604_RESET_SETUP_USEC 1 /* >= 125ns acceptance time */ +#define MFD_SC18IS604_RESET_TIME_USEC 500 + +/* timeout for about two times a maximum command (three byte) at 100kHz */ +#define MFD_SC18IS604_CMD_MAX_TOUT_USEC 200 + +/* expected chip in version string */ +#define MFD_SC18IS604_VERSION_CHIP "SC18IS604" +BUILD_ASSERT( + ARRAY_SIZE(MFD_SC18IS604_VERSION_CHIP) <= SC18IS604_VERSION_STRING_SIZE, + "Chip name too long" +); + +/** + * @brief SC18IM604 MFD configuration data + * + * @a interrupt must be set to a valid input pin with interrupt capability. + * @a reset must be set to a valid output pin. + * + * This structure contains all of the state for a given SC18IM604 MFD + * controller as well as the binding to related SPI device. + */ +typedef struct mfd_sc18is604_config { + /** Specs of the underlying SPI bus. */ + const struct spi_dt_spec spi; + /** Specs of this device's interrupt pin. */ + const struct gpio_dt_spec interrupt; + /** Specs of this device's reset pin. */ + const struct gpio_dt_spec reset; +} mfd_sc18is604_config_t; + +/** + * @brief SC18IM604 MFD data + * + * This structure contains data structures used by a SC18IM604 MFD. + * + * Interrupt propagation is handled by @a k_sem. Changes to + * @ref mfd_sc18is604_data and @ref mfd_sc18is604_config are + * synchronized using @a k_mutex. + */ +typedef struct mfd_sc18is604_data { + /** Backreference to driver instance. */ + const struct device *dev; + /** Lock for ensuring uninterrupted access to the MFD bus. */ + struct k_sem lock; + /** Interrupt callback of this driver instance. */ + struct gpio_callback interrupt_callback; + /** Semaphore used for MFD-internal signaling of interrupts. */ + struct k_sem interrupt_signal; + /** Interrupt callbacks registered by child devices. */ + sys_slist_t child_callbacks; +#ifdef CONFIG_MFD_SC18IS604_ASYNC + /** Private work queue for offloading blocking work. */ + struct k_work_q work_queue; + /** Stack area used by this driver instance's work queue. */ + k_thread_stack_t *work_queue_stack; +#endif /* CONFIG_MFD_SC18IS604_ASYNC */ +} mfd_sc18is604_data_t; + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_DRIVERS_MFD_SC18IS604_H_ */ diff --git a/drivers/mfd/mfd_sc18is604_transfer.c b/drivers/mfd/mfd_sc18is604_transfer.c new file mode 100644 index 0000000000..a1147c1793 --- /dev/null +++ b/drivers/mfd/mfd_sc18is604_transfer.c @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include + +#include "mfd_sc18is604.h" + +int mfd_sc18is604_transfer(const struct device *dev, + uint8_t *cmd, size_t cmd_len, + uint8_t *tx_data, size_t tx_len, + uint8_t *rx_data, size_t rx_len) +{ + const mfd_sc18is604_config_t * const config = dev->config; + int ret = 0; + + /* + * The SC18IS604 requires significant delay between each byte + * sent via SPI. We achieve this by sending bytes in individual + * transactions, but keeping the CS line set, and the device + * locked, until we explicitly release it (see the flags + * SPI_HOLD_ON_CS and SPI_LOCK_ON set in device init). + */ + + struct spi_buf buffer = { + .buf = NULL, + .len = 1, + }; + + const struct spi_buf_set buf_set = { + .buffers = &buffer, + .count = 1 + }; + + /* Write command sequence */ + if (cmd != NULL) { + for (int i = 0; i < cmd_len; i++) { + buffer.buf = &cmd[i]; + + ret = spi_write_dt(&config->spi, &buf_set); + if (ret != 0) { + goto end; + } + + k_usleep(MFD_SC18IS604_SPI_DELAY_USEC); + } + } + + /* Write data */ + if (tx_data != NULL) { + for (int i = 0; i < tx_len; i++) { + buffer.buf = &tx_data[i]; + + ret = spi_write_dt(&config->spi, &buf_set); + if (ret != 0) { + goto end; + } + + k_usleep(MFD_SC18IS604_SPI_DELAY_USEC); + } + } + + /* Read data */ + if (rx_data != NULL) { + for (int i = 0; i < rx_len; i++) { + buffer.buf = &rx_data[i]; + + ret = spi_read_dt(&config->spi, &buf_set); + if (ret != 0) { + goto end; + } + + k_usleep(MFD_SC18IS604_SPI_DELAY_USEC); + } + } + +end: + /* + * Releases the SPI bus itself, which we locked with + * our first spi_write_dt call. + */ + spi_release_dt(&config->spi); + + return ret; +} + +int mfd_sc18is604_read_register(const struct device *dev, + uint8_t reg, uint8_t *val) +{ + uint8_t cmd[] = {SC18IS604_CMD_READ_REGISTER, reg}; + + return mfd_sc18is604_transfer(dev, cmd, ARRAY_SIZE(cmd), + NULL, 0, val, 1); +} + +int mfd_sc18is604_write_register(const struct device *dev, + uint8_t reg, uint8_t val) +{ + uint8_t cmd[] = {SC18IS604_CMD_WRITE_REGISTER, reg}; + + return mfd_sc18is604_transfer(dev, cmd, ARRAY_SIZE(cmd), + &val, 1, NULL, 0); +} + +int mfd_sc18is604_read_buffer(const struct device *dev, + uint8_t *data, size_t len) +{ + uint8_t cmd[] = {SC18IS604_CMD_READ_BUFFER}; + + return mfd_sc18is604_transfer(dev, cmd, ARRAY_SIZE(cmd), + NULL, 0, data, len); +} diff --git a/drivers/mfd/mfd_sc18is604_transfer_async.c b/drivers/mfd/mfd_sc18is604_transfer_async.c new file mode 100644 index 0000000000..6713900d2c --- /dev/null +++ b/drivers/mfd/mfd_sc18is604_transfer_async.c @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include + +#include "mfd_sc18is604.h" + +/** + * @brief Structure tracking an asynchronous transfer. + */ +struct mfd_sc18is604_transfer_work { + /** The work item performing the transfer. */ + struct k_work work; + /** The device on which the transfer is performed. */ + const struct device *dev; + /** The command sequence to be sent. */ + uint8_t *cmd; + /** Length of the command sequence. */ + size_t cmd_len; + /** Data to be sent to the device. */ + uint8_t *tx_data; + /** Length of tx_data. */ + size_t tx_len; + /** Buffer for receiving data from the device. */ + uint8_t *rx_data; + /** Length of rx_data */ + size_t rx_len; + /** Signal to be raised once the transfer is complete (or has failed). */ + struct k_poll_signal *signal; +}; + +/* + * Work item that performs the actual transfers on the underlying SPI bus. Must + * be submitted to the work queue owned by the driver instance . + */ +static void mfd_sc18is604_transfer_work_fn(struct k_work *work) +{ + /* Access bundled data */ + struct mfd_sc18is604_transfer_work *transfer_work = CONTAINER_OF(work, + struct mfd_sc18is604_transfer_work, work); + const struct device *dev = transfer_work->dev; + int ret = 0; + + /* Call blocking transfer */ + ret = mfd_sc18is604_transfer(dev, transfer_work->cmd, + transfer_work->cmd_len, + transfer_work->tx_data, + transfer_work->tx_len, + transfer_work->rx_data, + transfer_work->rx_len); + + /* Report result through signal */ + k_poll_signal_raise(transfer_work->signal, ret); + + /* Clean up this work item */ + k_free(transfer_work->cmd); + k_free(transfer_work); +} + +int mfd_sc18is604_transfer_signal(const struct device *dev, + uint8_t *cmd, size_t cmd_len, + uint8_t *tx_data, size_t tx_len, + uint8_t *rx_data, size_t rx_len, + struct k_poll_signal *signal) +{ + mfd_sc18is604_data_t * const data = dev->data; + int ret = 0; + + /* Create work item to manage transfers */ + struct mfd_sc18is604_transfer_work *transfer_work = k_malloc( + sizeof(struct mfd_sc18is604_transfer_work)); + if (transfer_work == NULL) { + return -ENOMEM; + } + + *transfer_work = (struct mfd_sc18is604_transfer_work) { + .dev = dev, + .cmd_len = cmd_len, + .tx_data = tx_data, + .tx_len = tx_len, + .rx_data = rx_data, + .rx_len = rx_len, + .signal = signal, + }; + k_work_init(&transfer_work->work, mfd_sc18is604_transfer_work_fn); + + /* + * We can expect the TX and RX buffer to stay live until + * transfer completion, but the command sequence was + * stack-allocated inbetween. To make sure it stays valid, + * we create an allocated copy attached to the transfer work. + */ + + transfer_work->cmd = k_malloc(cmd_len); + if (transfer_work->cmd == NULL) { + k_free(transfer_work); + return -ENOMEM; + } + memcpy(transfer_work->cmd, cmd, cmd_len); + + /* Submit work item to driver owned queue */ + ret = k_work_submit_to_queue(&data->work_queue, &transfer_work->work); + if (ret != 1) { + k_free(transfer_work->cmd); + k_free(transfer_work); + return -EAGAIN; + } + + return 0; +} + +int mfd_sc18is604_read_register_signal(const struct device *dev, + uint8_t reg, uint8_t *val, + struct k_poll_signal *signal) +{ + uint8_t cmd[] = { SC18IS604_CMD_READ_REGISTER, reg }; + + return mfd_sc18is604_transfer_signal(dev, cmd, ARRAY_SIZE(cmd), + NULL, 0, val, 1, signal); +} + +int mfd_sc18is604_read_buffer_signal(const struct device *dev, + uint8_t *data, size_t len, + struct k_poll_signal *signal) +{ + uint8_t cmd[] = { SC18IS604_CMD_READ_BUFFER }; + + return mfd_sc18is604_transfer_signal(dev, cmd, ARRAY_SIZE(cmd), + NULL, 0, data, len, signal); +} diff --git a/dts/bindings/gpio/nxp,sc18is604-gpio.yaml b/dts/bindings/gpio/nxp,sc18is604-gpio.yaml new file mode 100644 index 0000000000..9c46224735 --- /dev/null +++ b/dts/bindings/gpio/nxp,sc18is604-gpio.yaml @@ -0,0 +1,22 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +description: GPIO controller part of an SC18IM604 bridge + +compatible: "nxp,sc18is604-gpio" + +include: [gpio-controller.yaml] + +properties: + "#gpio-cells": + const: 2 + + ngpios: + required: true + const: 5 + +gpio-cells: + - pin + - flags + +on-bus: nxp,sc18is604 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 diff --git a/dts/bindings/mfd/nxp,sc18is604.yaml b/dts/bindings/mfd/nxp,sc18is604.yaml new file mode 100644 index 0000000000..e560ac599e --- /dev/null +++ b/dts/bindings/mfd/nxp,sc18is604.yaml @@ -0,0 +1,70 @@ +# Copyright (c) 2024 TiaC Systems +# SPDX-License-Identifier: Apache-2.0 + +description: | + NXP SC18IS604 SPI to I2C/GPIO bridge. + + The SC18IS604 acts as both an external I2C and GPIO controller. These + controllers have to be added to the Device Tree as children, while the + device itself has to be a child of an SPI controller. + + An example configuration: + + #include + #include + #include + + &spi0 { + status = "okay"; + pinctrl-0 = <&spi0_default>; + pinctrl-names = "default"; + cs-gpios = <&gpio0 16 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; + + sc18is604: sc18is604 { + compatible = "nxp,sc18is604"; + status = "okay"; + reg = <0>; + spi-max-frequency = ; + reset-gpios = <&gpio0 14 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; + interrupt-gpios = <&gpio0 15 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>; + + i2c_ext: sc18is604-i2c { + compatible = "nxp,sc18is604-i2c"; + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + clock-frequency = ; + }; + + gpio_ext: sc18is604-gpio { + compatible = "nxp,sc18is604-gpio"; + status = "okay"; + gpio-controller; + #gpio-cells = <2>; + ngpios = <5>; + }; + }; + }; + +compatible: "nxp,sc18is604" + +include: [spi-device.yaml] + +properties: + + reset-gpios: + type: phandle-array + description: | + Optional GPIO pin for chip reset. + Example usage: + reset-gpios = <&gpio0 14 GPIO_ACTIVE_LOW>; + + interrupt-gpios: + required: true + type: phandle-array + description: | + GPIO pin for interrupt request (mandatory for I2C controller). + Example usage: + interrupt-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>; + +bus: nxp,sc18is604 diff --git a/include/zephyr/drivers/mfd/sc18is604.h b/include/zephyr/drivers/mfd/sc18is604.h new file mode 100644 index 0000000000..331e7205fd --- /dev/null +++ b/include/zephyr/drivers/mfd/sc18is604.h @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2024 TiaC Systems + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_DRIVERS_MFD_SC18IS604_H_ +#define ZEPHYR_INCLUDE_DRIVERS_MFD_SC18IS604_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include +#include +#include + +#define SC18IS604_CMD_WRITE_I2C 0x00 +#define SC18IS604_CMD_READ_I2C 0x01 +#define SC18IS604_CMD_READ_AFTER_WRITE_I2C 0x02 +#define SC18IS604_CMD_WRITE_AFTER_WRITE_I2C 0x03 +#define SC18IS604_CMD_READ_BUFFER 0x06 +#define SC18IS604_CMD_SPI_CONFIG 0x18 +#define SC18IS604_CMD_WRITE_REGISTER 0x20 +#define SC18IS604_CMD_READ_REGISTER 0x21 +#define SC18IS604_CMD_POWER_DOWN 0x30 +#define SC18IS604_CMD_POWER_DOWN_KEY1 0x5A +#define SC18IS604_CMD_POWER_DOWN_KEY2 0xA5 +#define SC18IS604_CMD_VERSION_STRING 0xFE + +#define SC18IS604_VERSION_STRING_SIZE 16 + +#define SC18IS604_REG_IO_CONFIG 0x00 +#define SC18IS604_REG_IO_STATE 0x01 +#define SC18IS604_REG_I2C_CLOCK 0x02 +#define SC18IS604_REG_I2C_TIMEOUT 0x03 +#define SC18IS604_REG_I2C_STATUS 0x04 +#define SC18IS604_REG_I2C_ADDRESS 0x05 + +#define SC18IS604_IO_NUM_PINS_INOUT 4 +#define SC18IS604_IO_NUM_PINS_IONLY 1 +#define SC18IS604_IO_NUM_PINS_MAX (SC18IS604_IO_NUM_PINS_INOUT \ + + SC18IS604_IO_NUM_PINS_IONLY) + +#define SC18IS604_IO_CONFIG_INPUT 0b00 +#define SC18IS604_IO_CONFIG_PUSH_PULL 0b10 +#define SC18IS604_IO_CONFIG_OPEN_DRAIN 0b11 + +#define SC18IS604_I2C_STATUS_SUCCESS 0xF0 +#define SC18IS604_I2C_STATUS_DEVICE_ADDR_NACK 0xF1 +#define SC18IS604_I2C_STATUS_BYTE_NACK 0xF2 +#define SC18IS604_I2C_STATUS_BUSY 0xF3 + +#define SC18IS604_I2C_CLOCK_99KHZ 19 +#define SC18IS604_I2C_CLOCK_375KHZ 5 + +/** + * @defgroup mfd_interface_sc18is604 MFD SC18IM604 Interface + * @ingroup mfd_interfaces + * @{ + */ + +/** + * @brief Perform a data transfer to and from an SC18IS604 device. + * + * @param dev An SC18IS604 MFD device. + * @param cmd A command sequence sent before the TX data. Can be NULL, in which + * case no command sequence is sent). + * @param cmd_len Length of the command sequence. + * @param tx_data Data to be sent to the device. Can be NULL, in which case no + * data is send). + * @param tx_len Length of the TX data buffer. + * @param[out] rx_data Buffer to hold data received from the device. Can be + * NULL, in which case no data is received. + * @param rx_len Length of the RX data buffer. + * + * @retval 0 On success. + * @return Negative error code on failure. + */ +int mfd_sc18is604_transfer(const struct device *dev, + uint8_t *cmd, size_t cmd_len, + uint8_t *tx_data, size_t tx_len, + uint8_t *rx_data, size_t rx_len); + + +/** + * @brief Write to an internal register. + * + * @param dev An SC18IS604 MFD device. + * @param reg Register address to write to. + * @param value Value to write into the register. + * + * @return A value from mfd_sc18is604_transfer(). + */ +int mfd_sc18is604_read_register(const struct device *dev, + uint8_t reg, uint8_t *val); + +#define READ_SC18IS604_REG(dev, reg, val) \ + mfd_sc18is604_read_register((dev), SC18IS604_REG_##reg, (val)); + +/** + * @brief Read from an internal register. + * + * @param dev An SC18IS604 MFD device. + * @param reg Register address to read from. + * @param[out] value Value read from the register. + * + * @return A value from mfd_sc18is604_transfer(). + */ +int mfd_sc18is604_write_register(const struct device *dev, + uint8_t reg, uint8_t val); + +#define WRITE_SC18IS604_REG(dev, reg, val) \ + mfd_sc18is604_write_register((dev), SC18IS604_REG_##reg, (val)); + +/** + * @brief Read data from the internal buffer of an SC18IS604. + * + * @param dev An SC18IS604 MFD device. + * @param[out] data Data read from the buffer. + * @param len Number of bytes to read from the buffer. + * + * @return A value from mfd_sc18is604_transfer(). + */ +int mfd_sc18is604_read_buffer(const struct device *dev, + uint8_t *data, size_t len); + +/** + * @brief Register an interrupt callback. + * + * @param dev An SC18IS604 device + * @param callback The callback to add. + * + * @retval 0 On success. + * @return Negative error code on failure. + */ +int mfd_sc18is604_add_callback(const struct device *dev, + struct gpio_callback *callback); + +/** + * @brief Remove a previously registered interrupt callback. + * + * @param dev An SC18IS604 device + * @param callback The callback to remove. + * + * @retval 0 On success. + * @return Negative error code on failure. + */ +int mfd_sc18is604_remove_callback(const struct device *dev, + struct gpio_callback *callback); + +/** + * @brief Claim a lock on an SC18IS604 device, preventing other users from + * accessing the device until the lock is released. + * + * @param dev The SC18IS604 MFD device to be locked. + * @param timeout Timeout after which trying to claim the device is aborted. + * + * @return A value from k_sem_take. + */ +int mfd_sc18is604_claim(const struct device *dev, k_timeout_t timeout); + +/** + * @brief Release a previously acquired lock on an SC18IS604 device. + * + * @param dev The SC18IS604 MFD device to be unlocked. + */ +void mfd_sc18is604_release(const struct device *dev); + +#ifdef CONFIG_MFD_SC18IS604_ASYNC + +/** + * @brief Asynchronously transfer data to and from an SC18IS604 device. This is + * the most general transfer routine, all other transfer variants are + * implemented in terms of this one. + * + * @param dev An SC18IS604 MFD device. + * @param cmd A command sequence sent before the TX data. Can be NULL, in which + * case no command sequence is sent). + * @param cmd_len Length of the command sequence. + * @param tx_data Data to be sent to the device. Can be NULL, in which case no + * data is send). + * @param tx_len Length of the TX data buffer. + * @param[out] rx_data Buffer to hold data received from the device. Can be + * NULL, in which case no data is received. + * @param rx_len Length of the RX data buffer. + * @param signal Signal to be raised once the transfer is complete. + * + * @retval 0 If the transfer was successfully started. + * @return Negative error code on failure. + */ +int mfd_sc18is604_transfer_signal(const struct device *dev, + uint8_t *cmd, size_t cmd_len, + uint8_t *tx_data, size_t tx_len, + uint8_t *rx_data, size_t rx_len, + struct k_poll_signal *signal); + +/** + * @brief Read from an internal register asychronously. + * + * @param dev An SC18IS604 MFD device. + * @param reg Register address to read from. + * @param[out] value Value read from the register. Pointer must remain valid + * until the transfer is complete. + * @param signal Signal that will be raised on transfer completion. Pointer + * must remain valid until the transfer is complete. + * + * @return A value from mfd_sc18is604_transfer_signal(). + */ +int mfd_sc18is604_read_register_signal(const struct device *dev, + uint8_t reg, uint8_t *val, + struct k_poll_signal *signal); + +#define READ_SC18IS604_REG_SIGNAL(dev, reg, val, signal) \ + mfd_sc18is604_read_register_signal((dev), SC18IS604_REG_##reg, \ + (val), (signal)); + +/** + * @brief Read data from the internal buffer of an SC18IS604 asynchronously. + * + * @param dev An SC18IS604 MFD device. + * @param[out] data Data read from the buffer. Pointer must remain valid until + * the transfer is complete. + * @param len Number of bytes to read from the buffer. + * @param signal Signal that will be raised on transfer completion. Pointer + * must remain valid until the transfer is complete. + * + * @return A value from mfd_sc18is604_transfer(). + */ +int mfd_sc18is604_read_buffer_signal(const struct device *dev, + uint8_t *data, size_t len, + struct k_poll_signal *signal); + +#endif /* CONFIG_MFD_SC18IS604_ASYNC */ + +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_DRIVERS_MFD_SC18IS604_H_ */ diff --git a/snippets/tstdrv-bldall-gpio-adj/tstdrv-bldall-gpio-adj.overlay b/snippets/tstdrv-bldall-gpio-adj/tstdrv-bldall-gpio-adj.overlay index c36315adad..7bd591c5e2 100644 --- a/snippets/tstdrv-bldall-gpio-adj/tstdrv-bldall-gpio-adj.overlay +++ b/snippets/tstdrv-bldall-gpio-adj/tstdrv-bldall-gpio-adj.overlay @@ -62,6 +62,7 @@ /* one entry for every devices */ cs-gpios = <&test_gpio_adj 0 0>, + <&test_gpio_adj 0 0>, <&test_gpio_adj 0 0>, <&test_gpio_adj 0 0>, <&test_gpio_adj 0 0>; @@ -243,6 +244,23 @@ offset = <24>; }; }; + + test_spi_sc18is604: sc18is604-spi@4 { + compatible = "nxp,sc18is604"; + status = "okay"; + reg = <0x4>; + spi-max-frequency = <650000>; + reset-gpios = <&test_gpio_adj 0 0>; + interrupt-gpios = <&test_gpio_adj 0 0>; + + test_spi_sc18is604_gpio: sc18is604-spi-gpio { + compatible = "nxp,sc18is604-gpio"; + status = "okay"; + gpio-controller; + #gpio-cells = <2>; + ngpios = <5>; + }; + }; }; test_uart_adj: uart@eeeeffff { diff --git a/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.conf b/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.conf index e9ca22677a..6c678ce6b9 100644 --- a/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.conf +++ b/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.conf @@ -1,2 +1,9 @@ # Copyright (c) 2024 TiaC Systems # SPDX-License-Identifier: Apache-2.0 + +CONFIG_MFD=y +CONFIG_I2C=y +CONFIG_SPI=y +CONFIG_GPIO=y + +CONFIG_DYNAMIC_THREAD_POOL_SIZE=2 diff --git a/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.overlay b/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.overlay index deff50c02c..aafa041233 100644 --- a/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.overlay +++ b/snippets/tstdrv-bldall-i2c-adj/tstdrv-bldall-i2c-adj.overlay @@ -44,7 +44,24 @@ clock-frequency = <2000000>; /* one entry for every devices */ - /* cs-gpios = <&test_gpio_adj 0 0>; */ + cs-gpios = <&test_gpio_adj 0 0>; + + test_spi_sc18is604: sc18is604-spi@0 { + compatible = "nxp,sc18is604"; + status = "okay"; + reg = <0x0>; + spi-max-frequency = <650000>; + reset-gpios = <&test_gpio_adj 0 0>; + interrupt-gpios = <&test_gpio_adj 0 0>; + + test_spi_sc18is604_i2c: sc18is604-spi-i2c { + compatible = "nxp,sc18is604-i2c"; + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + clock-frequency = <100000>; + }; + }; }; test_uart_adj: uart@ddddeeee { diff --git a/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.conf b/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.conf index e7683e6f37..c400b86a23 100644 --- a/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.conf +++ b/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.conf @@ -5,3 +5,5 @@ CONFIG_MFD=y CONFIG_I2C=y CONFIG_SPI=y CONFIG_SERIAL=y + +CONFIG_DYNAMIC_THREAD_POOL_SIZE=2 diff --git a/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.overlay b/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.overlay index 9e74169a11..042d5da043 100644 --- a/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.overlay +++ b/snippets/tstdrv-bldall-mfd-adj/tstdrv-bldall-mfd-adj.overlay @@ -59,7 +59,8 @@ clock-frequency = <2000000>; /* one entry for every devices */ - cs-gpios = <&test_gpio_adj 0 0>; + cs-gpios = <&test_gpio_adj 0 0>, + <&test_gpio_adj 0 0>; test_spi_sipo_mux_gp: sipo-mux-gp@0 { compatible = "sipo-mux-gp-spi"; @@ -94,6 +95,31 @@ offset = <32>; }; }; + + test_spi_sc18is604: sc18is604-spi@1 { + compatible = "nxp,sc18is604"; + status = "okay"; + reg = <0x1>; + spi-max-frequency = <650000>; + reset-gpios = <&test_gpio_adj 0 0>; + interrupt-gpios = <&test_gpio_adj 0 0>; + + test_spi_sc18is604_gpio: sc18is604-spi-gpio { + compatible = "nxp,sc18is604-gpio"; + status = "okay"; + gpio-controller; + #gpio-cells = <2>; + ngpios = <5>; + }; + + test_spi_sc18is604_i2c: sc18is604-spi-i2c { + compatible = "nxp,sc18is604-i2c"; + status = "okay"; + #address-cells = <1>; + #size-cells = <0>; + clock-frequency = <100000>; + }; + }; }; test_uart_adj: uart@eeeeffff {