/* * Copyright 2019 The Chromium OS Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ /* GPIO expander for Nuvoton NCT38XX. */ #include "console.h" #include "gpio.h" #include "i2c.h" #include "ioexpander.h" #include "ioexpander_nct38xx.h" #include "tcpci.h" #define CPRINTF(format, args...) cprintf(CC_GPIO, format, ## args) #define CPRINTS(format, args...) cprints(CC_GPIO, format, ## args) /* * Store the GPIO_ALERT_MASK_0/1 and chip ID registers locally. In this way, * we don't have to read it via I2C transaction everytime. */ struct nct38xx_chip_data { uint8_t int_mask[2]; int chip_id; }; static struct nct38xx_chip_data chip_data[CONFIG_IO_EXPANDER_PORT_COUNT] = { [0 ... (CONFIG_IO_EXPANDER_PORT_COUNT - 1)] = { {0, 0}, -1 } }; static int nct38xx_ioex_check_is_valid(int ioex, int port, int mask) { if (chip_data[ioex].chip_id == NCT38XX_VARIANT_3808) { if (port == 1) { CPRINTF("Port 1 is not support in NCT3808\n"); return EC_ERROR_INVAL; } if (mask & ~NCT38XXX_3808_VALID_GPIO_MASK) { CPRINTF("GPIO%02d is not support in NCT3808\n", __fls(mask)); return EC_ERROR_INVAL; } } return EC_SUCCESS; } static int nct38xx_ioex_init(int ioex) { int rv, val; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; /* * Check the NCT38xx part number in the register DEVICE_ID[4:2]: * 000: NCT3807 * 010: NCT3808 */ rv = i2c_read8(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, TCPC_REG_BCD_DEV, &val); if (rv != EC_SUCCESS) { CPRINTF("Failed to read NCT38XX DEV ID for IOexpander %d\n", ioex); return rv; } chip_data[ioex].chip_id = ((uint8_t)val & NCT38XX_VARIANT_MASK) >> 2; /* * NCT38XX uses the Vendor Define bit in the ALERT event to indicate * that an IOEX IO's interrupt is triggered. * Normally, The ALERT MASK for Vendor Define event should be set by * the NCT38XX TCPCI driver's init function. * However, it should be also set here if we want to test the interrupt * function of IOEX when the NCT38XX TCPCI driver is not included. */ if (!IS_ENABLED(CONFIG_USB_PD_TCPM_NCT38XX)) { rv = i2c_write16(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, TCPC_REG_ALERT_MASK, TCPC_REG_ALERT_VENDOR_DEF); if (rv != EC_SUCCESS) return rv; } return EC_SUCCESS; } static int nct38xx_ioex_get_level(int ioex, int port, int mask, int *val) { int rv, reg; rv = nct38xx_ioex_check_is_valid(ioex, port, mask); if (rv != EC_SUCCESS) return rv; reg = NCT38XX_REG_GPIO_DATA_IN(port); rv = i2c_read8(ioex_config[ioex].i2c_host_port, ioex_config[ioex].i2c_slave_addr, reg, val); if (rv != EC_SUCCESS) return rv; *val = !!(*val & mask); return EC_SUCCESS; } static int nct38xx_ioex_set_level(int ioex, int port, int mask, int value) { int rv, reg, val; rv = nct38xx_ioex_check_is_valid(ioex, port, mask); if (rv != EC_SUCCESS) return rv; reg = NCT38XX_REG_GPIO_DATA_OUT(port); rv = i2c_read8(ioex_config[ioex].i2c_host_port, ioex_config[ioex].i2c_slave_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (value) val |= mask; else val &= ~mask; return i2c_write8(ioex_config[ioex].i2c_host_port, ioex_config[ioex].i2c_slave_addr, reg, val); } static int nct38xx_ioex_get_flags(int ioex, int port, int mask, int *flags) { int rv, reg, val, i2c_port, i2c_addr; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; i2c_port = ioex_p->i2c_host_port; i2c_addr = ioex_p->i2c_slave_addr; rv = nct38xx_ioex_check_is_valid(ioex, port, mask); if (rv != EC_SUCCESS) return rv; reg = NCT38XX_REG_GPIO_DIR(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (val & mask) *flags |= GPIO_OUTPUT; else *flags |= GPIO_INPUT; reg = NCT38XX_REG_GPIO_DATA_IN(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (val & mask) *flags |= GPIO_HIGH; else *flags |= GPIO_LOW; reg = NCT38XX_REG_GPIO_OD_SEL(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (val & mask) *flags |= GPIO_OPEN_DRAIN; return EC_SUCCESS; } static int nct38xx_ioex_sel_int_type(int i2c_port, int i2c_addr, int port, int mask, int flags) { int rv; int reg_rising, reg_falling; int rising, falling; reg_rising = NCT38XX_REG_GPIO_ALERT_RISE(port); rv = i2c_read8(i2c_port, i2c_addr, reg_rising, &rising); if (rv != EC_SUCCESS) return rv; reg_falling = NCT38XX_REG_GPIO_ALERT_FALL(port); rv = i2c_read8(i2c_port, i2c_addr, reg_falling, &falling); if (rv != EC_SUCCESS) return rv; /* Handle interrupt for level trigger */ if ((flags & GPIO_INT_F_HIGH) || (flags & GPIO_INT_F_LOW)) { int reg_level, level; reg_level = NCT38XX_REG_GPIO_ALERT_LEVEL(port); rv = i2c_read8(i2c_port, i2c_addr, reg_level, &level); if (rv != EC_SUCCESS) return rv; /* * For "level" triggered interrupt, the related bit in * ALERT_RISE and ALERT_FALL registers must be 0 */ rising &= ~mask; falling &= ~mask; if (flags & GPIO_INT_F_HIGH) level |= mask; else level &= ~mask; rv = i2c_write8(i2c_port, i2c_addr, reg_rising, rising); if (rv != EC_SUCCESS) return rv; rv = i2c_write8(i2c_port, i2c_addr, reg_falling, falling); if (rv != EC_SUCCESS) return rv; rv = i2c_write8(i2c_port, i2c_addr, reg_level, level); if (rv != EC_SUCCESS) return rv; } else if ((flags & GPIO_INT_F_RISING) || (flags & GPIO_INT_F_FALLING)) { if (flags & GPIO_INT_F_RISING) rising |= mask; else rising &= ~mask; if (flags & GPIO_INT_F_FALLING) falling |= mask; else falling &= ~mask; rv = i2c_write8(i2c_port, i2c_addr, reg_rising, rising); if (rv != EC_SUCCESS) return rv; rv = i2c_write8(i2c_port, i2c_addr, reg_falling, falling); if (rv != EC_SUCCESS) return rv; } return EC_SUCCESS; } static int nct38xx_ioex_set_flags_by_mask(int ioex, int port, int mask, int flags) { int rv, reg, val, i2c_port, i2c_addr; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; i2c_port = ioex_p->i2c_host_port; i2c_addr = ioex_p->i2c_slave_addr; rv = nct38xx_ioex_check_is_valid(ioex, port, mask); if (rv != EC_SUCCESS) return rv; /* * GPIO port 0 muxs with alternative function. Disable the alternative * function before setting flags. */ if (port == 0) { /* GPIO03 in NCT3807 is not muxed with other function. */ if (!(chip_data[ioex].chip_id == NCT38XX_VARIANT_3807 && mask & 0x08)) { reg = NCT38XX_REG_MUX_CONTROL; rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; val = (val | mask); rv = i2c_write8(i2c_port, i2c_addr, reg, val); if (rv != EC_SUCCESS) return rv; } } val = flags & ~NCT38XX_SUPPORT_GPIO_FLAGS; if (val) { CPRINTF("Flag 0x%08x is not supported\n", val); return EC_ERROR_INVAL; } /* Select open drain 0:push-pull 1:open-drain */ reg = NCT38XX_REG_GPIO_OD_SEL(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (flags & GPIO_OPEN_DRAIN) val |= mask; else val &= ~mask; rv = i2c_write8(i2c_port, i2c_addr, reg, val); if (rv != EC_SUCCESS) return rv; nct38xx_ioex_sel_int_type(i2c_port, i2c_addr, port, mask, flags); /* Configure the output level */ reg = NCT38XX_REG_GPIO_DATA_OUT(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (flags & GPIO_HIGH) val |= mask; else if (flags & GPIO_LOW) val &= ~mask; rv = i2c_write8(i2c_port, i2c_addr, reg, val); if (rv != EC_SUCCESS) return rv; reg = NCT38XX_REG_GPIO_DIR(port); rv = i2c_read8(i2c_port, i2c_addr, reg, &val); if (rv != EC_SUCCESS) return rv; if (flags & GPIO_OUTPUT) val |= mask; else val &= ~mask; return i2c_write8(i2c_port, i2c_addr, reg, val); } /* * The following functions are used for IO's interrupt support. * * please note that if the system needs to use an IO on NCT38XX to support * the interrupt, the following two consideration should be taken into account. * 1. Interrupt latency: * Because it requires to access the registers of NCT38XX via I2C * transaction to know the interrupt event, there is some added latency * for the interrupt handling. If the interrupt requires short latency, * we do not recommend to connect such a signal to the NCT38XX. * * 2. Shared ALERT pin: * Because the ALERT pin is shared also with the TCPC ALERT, we do not * recommend to connect any signal that may generate a high rate of * interrupts so it will not interfere with the normal work of the * TCPC. */ static int nct38xx_ioex_enable_interrupt(int ioex, int port, int mask, int enable) { int rv, reg, val; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; rv = nct38xx_ioex_check_is_valid(ioex, port, mask); if (rv != EC_SUCCESS) return rv; /* Clear the pending bit */ reg = NCT38XX_REG_GPIO_ALERT_STAT(port); rv = i2c_read8(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, reg, &val); if (rv != EC_SUCCESS) return rv; val |= mask; rv = i2c_write8(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, reg, val); if (rv != EC_SUCCESS) return rv; reg = NCT38XX_REG_GPIO_ALERT_MASK(port); if (enable) { /* Enable the alert mask */ chip_data[ioex].int_mask[port] |= mask; val = chip_data[ioex].int_mask[port]; } else { /* Disable the alert mask */ chip_data[ioex].int_mask[port] &= ~mask; val = chip_data[ioex].int_mask[port]; } return i2c_write8(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, reg, val); } int nct38xx_ioex_event_handler(int ioex) { int reg, int_status, int_mask; int i, j, total_port; const struct ioex_info *g; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; int rv = 0; int_mask = chip_data[ioex].int_mask[0] | ( chip_data[ioex].int_mask[1] << 8); reg = NCT38XX_REG_GPIO_ALERT_STAT(0); /* * Read ALERT_STAT_0 and ALERT_STAT_1 register in a single I2C * transaction to increase efficiency */ rv = i2c_read16(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, reg, &int_status); if (rv != EC_SUCCESS) return rv; int_status = int_status & int_mask; /* * Clear the changed status bits in ALERT_STAT_0 and ALERT_STAT_1 * register in a single I2C transaction to increase efficiency */ rv = i2c_write16(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, reg, int_status); if (rv != EC_SUCCESS) return rv; /* For NCT3808, only check one port */ total_port = (chip_data[ioex].chip_id == NCT38XX_VARIANT_3808) ? NCT38XX_NCT3808_MAX_IO_PORT : NCT38XX_NCT3807_MAX_IO_PORT; for (i = 0; i < total_port; i++) { uint8_t pending; pending = int_status >> (i * 8); if (!pending) continue; for (j = 0, g = ioex_list; j < ioex_ih_count; j++, g++) { if (ioex == g->ioex && i == g->port && (pending & g->mask)) { ioex_irq_handlers[j](j); pending &= ~g->mask; if (!pending) break; } } } return EC_SUCCESS; } /* * Normally, the ALERT MASK for Vendor Define event should be checked by * the NCT38XX TCPCI driver's tcpc_alert function. * However, it should be checked here if we want to test the interrupt * function of IOEX when the NCT38XX TCPCI driver is not included. */ void nct38xx_ioex_handle_alert(int ioex) { int rv, status; struct ioexpander_config_t *ioex_p = &ioex_config[ioex]; rv = i2c_read16(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, TCPC_REG_ALERT, &status); if (rv != EC_SUCCESS) CPRINTF("fail to read ALERT register\n"); if (status & TCPC_REG_ALERT_VENDOR_DEF) { rv = i2c_write16(ioex_p->i2c_host_port, ioex_p->i2c_slave_addr, TCPC_REG_ALERT, TCPC_REG_ALERT_VENDOR_DEF); if (rv != EC_SUCCESS) { CPRINTF("Fail to clear Vendor Define mask\n"); return; } nct38xx_ioex_event_handler(ioex); } } const struct ioexpander_drv nct38xx_ioexpander_drv = { .init = &nct38xx_ioex_init, .get_level = &nct38xx_ioex_get_level, .set_level = &nct38xx_ioex_set_level, .get_flags_by_mask = &nct38xx_ioex_get_flags, .set_flags_by_mask = &nct38xx_ioex_set_flags_by_mask, .enable_interrupt = &nct38xx_ioex_enable_interrupt, };