| /* |
| * Copyright (c) 2020 Intel Corporation |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT microchip_xec_peci |
| |
| #include <errno.h> |
| #include <zephyr/device.h> |
| #include <zephyr/kernel.h> |
| #ifdef CONFIG_SOC_SERIES_MEC172X |
| #include <zephyr/drivers/clock_control/mchp_xec_clock_control.h> |
| #include <zephyr/drivers/interrupt_controller/intc_mchp_xec_ecia.h> |
| #endif |
| #include <zephyr/drivers/peci.h> |
| #include <zephyr/drivers/pinctrl.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/pm/policy.h> |
| #include <soc.h> |
| #include <zephyr/logging/log.h> |
| #include <zephyr/irq.h> |
| LOG_MODULE_REGISTER(peci_mchp_xec, CONFIG_PECI_LOG_LEVEL); |
| |
| /* Maximum PECI core clock is the main clock 48Mhz */ |
| #define MAX_PECI_CORE_CLOCK 48000u |
| /* 1 ms */ |
| #define PECI_RESET_DELAY 1000u |
| #define PECI_RESET_DELAY_MS 1u |
| /* 100 us */ |
| #define PECI_IDLE_DELAY 100u |
| /* 5 ms */ |
| #define PECI_IDLE_TIMEOUT 50u |
| /* Maximum retries */ |
| #define PECI_TIMEOUT_RETRIES 3u |
| /* Maximum read buffer fill wait retries */ |
| #define PECI_RX_BUF_FILL_WAIT_RETRY 100u |
| |
| /* 10 us */ |
| #define PECI_IO_DELAY 10 |
| |
| #define OPT_BIT_TIME_MSB_OFS 8u |
| |
| #define PECI_FCS_LEN 2 |
| |
| struct peci_xec_config { |
| struct peci_regs * const regs; |
| uint8_t irq_num; |
| uint8_t girq; |
| uint8_t girq_pos; |
| uint8_t pcr_idx; |
| uint8_t pcr_pos; |
| const struct pinctrl_dev_config *pcfg; |
| }; |
| |
| enum peci_pm_policy_state_flag { |
| PECI_PM_POLICY_FLAG, |
| PECI_PM_POLICY_FLAG_COUNT, |
| }; |
| |
| struct peci_xec_data { |
| struct k_sem tx_lock; |
| uint32_t bitrate; |
| int timeout_retries; |
| #ifdef CONFIG_PM_DEVICE |
| ATOMIC_DEFINE(pm_policy_state_flag, PECI_PM_POLICY_FLAG_COUNT); |
| #endif |
| }; |
| |
| #ifdef CONFIG_PM_DEVICE |
| static void peci_xec_pm_policy_state_lock_get(struct peci_xec_data *data, |
| enum peci_pm_policy_state_flag flag) |
| { |
| if (atomic_test_and_set_bit(data->pm_policy_state_flag, flag) == 0) { |
| pm_policy_state_lock_get(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES); |
| } |
| } |
| |
| static void peci_xec_pm_policy_state_lock_put(struct peci_xec_data *data, |
| enum peci_pm_policy_state_flag flag) |
| { |
| if (atomic_test_and_clear_bit(data->pm_policy_state_flag, flag) == 1) { |
| pm_policy_state_lock_put(PM_STATE_SUSPEND_TO_IDLE, PM_ALL_SUBSTATES); |
| } |
| } |
| #endif |
| |
| #ifdef CONFIG_SOC_SERIES_MEC172X |
| static inline void peci_girq_enable(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| |
| mchp_xec_ecia_girq_src_en(cfg->girq, cfg->girq_pos); |
| } |
| |
| static inline void peci_girq_status_clear(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| |
| mchp_soc_ecia_girq_src_clr(cfg->girq, cfg->girq_pos); |
| } |
| |
| static inline void peci_clr_slp_en(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| |
| z_mchp_xec_pcr_periph_sleep(cfg->pcr_idx, cfg->pcr_pos, 0); |
| } |
| #else |
| static inline void peci_girq_enable(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| |
| MCHP_GIRQ_ENSET(cfg->girq) = BIT(cfg->girq_pos); |
| } |
| |
| static inline void peci_girq_status_clear(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| |
| MCHP_GIRQ_SRC(cfg->girq) = BIT(cfg->girq_pos); |
| } |
| |
| static inline void peci_clr_slp_en(const struct device *dev) |
| { |
| ARG_UNUSED(dev); |
| |
| mchp_pcr_periph_slp_ctrl(PCR_PECI, 0); |
| } |
| #endif |
| |
| static int check_bus_idle(struct peci_regs * const regs) |
| { |
| uint8_t delay_cnt = PECI_IDLE_TIMEOUT; |
| |
| /* Wait until PECI bus becomes idle. |
| * Note that when IDLE bit in the status register changes, HW do not |
| * generate an interrupt, so need to poll. |
| */ |
| while (!(regs->STATUS2 & MCHP_PECI_STS2_IDLE)) { |
| k_busy_wait(PECI_IDLE_DELAY); |
| delay_cnt--; |
| |
| if (!delay_cnt) { |
| LOG_WRN("Bus is busy"); |
| return -EBUSY; |
| } |
| } |
| return 0; |
| } |
| |
| static int peci_xec_configure(const struct device *dev, uint32_t bitrate) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_xec_data * const data = dev->data; |
| struct peci_regs * const regs = cfg->regs; |
| uint16_t value; |
| |
| data->bitrate = bitrate; |
| |
| /* Power down PECI interface */ |
| regs->CONTROL = MCHP_PECI_CTRL_PD; |
| |
| /* Adjust bitrate */ |
| value = MAX_PECI_CORE_CLOCK / bitrate; |
| regs->OPT_BIT_TIME_LSB = value & MCHP_PECI_OPT_BT_LSB_MASK; |
| regs->OPT_BIT_TIME_MSB = ((value >> OPT_BIT_TIME_MSB_OFS) & |
| MCHP_PECI_OPT_BT_MSB_MASK); |
| |
| /* Power up PECI interface */ |
| regs->CONTROL &= ~MCHP_PECI_CTRL_PD; |
| |
| return 0; |
| } |
| |
| static int peci_xec_disable(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_regs * const regs = cfg->regs; |
| int ret; |
| |
| /* Make sure no transaction is interrupted before disabling the HW */ |
| ret = check_bus_idle(regs); |
| if (ret) { |
| return ret; |
| } |
| |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| peci_girq_status_clear(dev); |
| NVIC_ClearPendingIRQ(cfg->irq_num); |
| irq_disable(cfg->irq_num); |
| #endif |
| regs->CONTROL |= MCHP_PECI_CTRL_PD; |
| |
| return 0; |
| } |
| |
| static int peci_xec_enable(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_regs * const regs = cfg->regs; |
| |
| regs->CONTROL &= ~MCHP_PECI_CTRL_PD; |
| |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| peci_girq_status_clear(dev); |
| peci_girq_enable(dev); |
| irq_enable(cfg->irq_num); |
| #endif |
| return 0; |
| } |
| |
| static void peci_xec_bus_recovery(const struct device *dev, bool full_reset) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_xec_data * const data = dev->data; |
| struct peci_regs * const regs = cfg->regs; |
| |
| LOG_WRN("%s full_reset:%d", __func__, full_reset); |
| if (full_reset) { |
| regs->CONTROL = MCHP_PECI_CTRL_PD | MCHP_PECI_CTRL_RST; |
| |
| if (k_is_in_isr()) { |
| k_busy_wait(PECI_RESET_DELAY_MS); |
| } else { |
| k_msleep(PECI_RESET_DELAY); |
| } |
| |
| regs->CONTROL &= ~MCHP_PECI_CTRL_RST; |
| |
| peci_xec_configure(dev, data->bitrate); |
| } else { |
| /* Only reset internal FIFOs */ |
| regs->CONTROL |= MCHP_PECI_CTRL_FRST; |
| } |
| } |
| |
| static int peci_xec_write(const struct device *dev, struct peci_msg *msg) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_xec_data * const data = dev->data; |
| struct peci_regs * const regs = cfg->regs; |
| int i; |
| int ret; |
| |
| struct peci_buf *tx_buf = &msg->tx_buffer; |
| struct peci_buf *rx_buf = &msg->rx_buffer; |
| |
| /* Check if FIFO is full */ |
| if (regs->STATUS2 & MCHP_PECI_STS2_WFF) { |
| LOG_WRN("%s FIFO is full", __func__); |
| return -EIO; |
| } |
| |
| regs->CONTROL &= ~MCHP_PECI_CTRL_FRST; |
| |
| /* Add PECI transaction header to TX FIFO */ |
| regs->WR_DATA = msg->addr; |
| regs->WR_DATA = tx_buf->len; |
| regs->WR_DATA = rx_buf->len; |
| |
| /* Add PECI payload to Tx FIFO only if write length is valid */ |
| if (tx_buf->len) { |
| regs->WR_DATA = msg->cmd_code; |
| for (i = 0; i < tx_buf->len - 1; i++) { |
| if (!(regs->STATUS2 & MCHP_PECI_STS2_WFF)) { |
| regs->WR_DATA = tx_buf->buf[i]; |
| } |
| } |
| } |
| |
| /* Check bus is idle before starting a new transfer */ |
| ret = check_bus_idle(regs); |
| if (ret) { |
| return ret; |
| } |
| |
| regs->CONTROL |= MCHP_PECI_CTRL_TXEN; |
| k_busy_wait(PECI_IO_DELAY); |
| |
| /* Wait for transmission to complete */ |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| if (k_sem_take(&data->tx_lock, PECI_IO_DELAY * tx_buf->len)) { |
| return -ETIMEDOUT; |
| } |
| #else |
| /* In worst case, overall timeout will be 1msec (100 * 10usec) */ |
| uint8_t wait_timeout_cnt = 100; |
| |
| while (!(regs->STATUS1 & MCHP_PECI_STS1_EOF)) { |
| k_busy_wait(PECI_IO_DELAY); |
| wait_timeout_cnt--; |
| if (!wait_timeout_cnt) { |
| LOG_WRN("Tx timeout"); |
| data->timeout_retries++; |
| /* Full reset only if multiple consecutive failures */ |
| if (data->timeout_retries > PECI_TIMEOUT_RETRIES) { |
| peci_xec_bus_recovery(dev, true); |
| } else { |
| peci_xec_bus_recovery(dev, false); |
| } |
| |
| return -ETIMEDOUT; |
| } |
| } |
| #endif |
| data->timeout_retries = 0; |
| |
| return 0; |
| } |
| |
| static int peci_xec_read(const struct device *dev, struct peci_msg *msg) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_regs * const regs = cfg->regs; |
| int i; |
| int ret; |
| uint8_t tx_fcs; |
| uint8_t bytes_rcvd; |
| uint8_t wait_timeout_cnt; |
| struct peci_buf *rx_buf = &msg->rx_buffer; |
| |
| /* Attempt to read data from RX FIFO */ |
| bytes_rcvd = 0; |
| for (i = 0; i < (rx_buf->len + PECI_FCS_LEN); i++) { |
| /* Worst case timeout will be 1msec (100 * 10usec) */ |
| wait_timeout_cnt = PECI_RX_BUF_FILL_WAIT_RETRY; |
| /* Wait for read buffer to fill up */ |
| while (regs->STATUS2 & MCHP_PECI_STS2_RFE) { |
| k_usleep(PECI_IO_DELAY); |
| wait_timeout_cnt--; |
| if (!wait_timeout_cnt) { |
| LOG_WRN("Rx buffer empty"); |
| return -ETIMEDOUT; |
| } |
| } |
| |
| if (i == 0) { |
| /* Get write block FCS just for debug */ |
| tx_fcs = regs->RD_DATA; |
| LOG_DBG("TX FCS %x", tx_fcs); |
| |
| /* If a Ping is done, write Tx fcs to rx buffer*/ |
| if (msg->cmd_code == PECI_CMD_PING) { |
| rx_buf->buf[0] = tx_fcs; |
| break; |
| } |
| } else if (i == (rx_buf->len + 1)) { |
| /* Get read block FCS, but don't count it */ |
| rx_buf->buf[i-1] = regs->RD_DATA; |
| } else { |
| /* Get response */ |
| rx_buf->buf[i-1] = regs->RD_DATA; |
| bytes_rcvd++; |
| } |
| } |
| |
| /* Check if transaction is as expected */ |
| if (rx_buf->len != bytes_rcvd) { |
| LOG_INF("Incomplete %x vs %x", bytes_rcvd, rx_buf->len); |
| } |
| |
| /* Once write-read transaction is complete, ensure bus is idle |
| * before resetting the internal FIFOs |
| */ |
| ret = check_bus_idle(regs); |
| if (ret) { |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int peci_xec_transfer(const struct device *dev, struct peci_msg *msg) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_regs * const regs = cfg->regs; |
| int ret = 0; |
| uint8_t err_val = 0; |
| #ifdef CONFIG_PM_DEVICE |
| struct peci_xec_data *data = dev->data; |
| |
| peci_xec_pm_policy_state_lock_get(data, PECI_PM_POLICY_FLAG); |
| #endif |
| |
| do { |
| ret = peci_xec_write(dev, msg); |
| if (ret) { |
| break; |
| } |
| |
| /* If a PECI transmission is successful, it may or not involve |
| * a read operation, check if transaction expects a response |
| * Also perform a read when PECI cmd is Ping to get Write FCS |
| */ |
| if (msg->rx_buffer.len || (msg->cmd_code == PECI_CMD_PING)) { |
| ret = peci_xec_read(dev, msg); |
| if (ret) { |
| break; |
| } |
| } |
| |
| /* Cleanup */ |
| if (regs->STATUS1 & MCHP_PECI_STS1_EOF) { |
| regs->STATUS1 |= MCHP_PECI_STS1_EOF; |
| } |
| |
| /* Check for error conditions and perform bus recovery if necessary */ |
| err_val = regs->ERROR; |
| if (err_val) { |
| if (err_val & MCHP_PECI_ERR_RDOV) { |
| LOG_ERR("Read buffer is not empty"); |
| } |
| |
| if (err_val & MCHP_PECI_ERR_WRUN) { |
| LOG_ERR("Write buffer is not empty"); |
| } |
| |
| if (err_val & MCHP_PECI_ERR_BERR) { |
| LOG_ERR("PECI bus error"); |
| } |
| |
| LOG_DBG("PECI err %x", err_val); |
| LOG_DBG("PECI sts1 %x", regs->STATUS1); |
| LOG_DBG("PECI sts2 %x", regs->STATUS2); |
| |
| /* ERROR is a clear-on-write register, need to clear errors |
| * occurring at the end of a transaction. A temp variable is |
| * used to overcome complaints by the static code analyzer |
| */ |
| regs->ERROR = err_val; |
| peci_xec_bus_recovery(dev, false); |
| ret = -EIO; |
| break; |
| } |
| } while (0); |
| |
| #ifdef CONFIG_PM_DEVICE |
| peci_xec_pm_policy_state_lock_put(data, PECI_PM_POLICY_FLAG); |
| #endif |
| return ret; |
| } |
| |
| #ifdef CONFIG_PM_DEVICE |
| static int peci_xec_pm_action(const struct device *dev, enum pm_device_action action) |
| { |
| const struct peci_xec_config *const devcfg = dev->config; |
| struct peci_regs * const regs = devcfg->regs; |
| struct ecs_regs * const ecs_regs = (struct ecs_regs *)(DT_REG_ADDR(DT_NODELABEL(ecs))); |
| int ret; |
| |
| switch (action) { |
| case PM_DEVICE_ACTION_RESUME: |
| ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_DEFAULT); |
| /* VREF_VTT function is enabled*/ |
| ecs_regs->PECI_DIS = 0x00u; |
| |
| /* Power up PECI interface */ |
| regs->CONTROL &= ~MCHP_PECI_CTRL_PD; |
| break; |
| case PM_DEVICE_ACTION_SUSPEND: |
| regs->CONTROL |= MCHP_PECI_CTRL_PD; |
| /* This bit reduces leakage current through the CPU voltage reference |
| * pin if PECI is not used. VREF_VTT function is disabled. |
| */ |
| ecs_regs->PECI_DIS = 0x01u; |
| |
| /* If application does not want to turn off PECI pins it will |
| * not define pinctrl-1 for this node. |
| */ |
| ret = pinctrl_apply_state(devcfg->pcfg, PINCTRL_STATE_SLEEP); |
| if (ret == -ENOENT) { /* pinctrl-1 does not exist. */ |
| ret = 0; |
| } |
| break; |
| default: |
| ret = -ENOTSUP; |
| } |
| |
| return ret; |
| } |
| #endif /* CONFIG_PM_DEVICE */ |
| |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| static void peci_xec_isr(const void *arg) |
| { |
| const struct device *dev = arg; |
| struct peci_xec_config * const cfg = dev->config; |
| struct peci_xec_data * const data = dev->data; |
| struct peci_regs * const regs = cfg->regs; |
| uint8_t peci_error = regs->ERROR; |
| uint8_t peci_status2 = regs->STATUS2; |
| |
| peci_girq_status_clear(dev); |
| |
| if (peci_error) { |
| regs->ERROR = peci_error; |
| } |
| |
| if (peci_status2 & MCHP_PECI_STS2_WFE) { |
| LOG_WRN("TX FIFO empty ST2:%x", peci_status2); |
| k_sem_give(&data->tx_lock); |
| } |
| |
| if (peci_status2 & MCHP_PECI_STS2_RFE) { |
| LOG_WRN("RX FIFO full ST2:%x", peci_status2); |
| } |
| } |
| #endif |
| |
| static const struct peci_driver_api peci_xec_driver_api = { |
| .config = peci_xec_configure, |
| .enable = peci_xec_enable, |
| .disable = peci_xec_disable, |
| .transfer = peci_xec_transfer, |
| }; |
| |
| static int peci_xec_init(const struct device *dev) |
| { |
| const struct peci_xec_config * const cfg = dev->config; |
| struct peci_regs * const regs = cfg->regs; |
| struct ecs_regs * const ecs_regs = (struct ecs_regs *)(DT_REG_ADDR(DT_NODELABEL(ecs))); |
| |
| int ret = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT); |
| |
| if (ret != 0) { |
| LOG_ERR("XEC PECI pinctrl init failed (%d)", ret); |
| return ret; |
| } |
| |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| k_sem_init(&data->tx_lock, 0, 1); |
| #endif |
| |
| peci_clr_slp_en(dev); |
| |
| ecs_regs->PECI_DIS = 0x00u; |
| |
| /* Reset PECI interface */ |
| regs->CONTROL |= MCHP_PECI_CTRL_RST; |
| k_msleep(PECI_RESET_DELAY_MS); |
| regs->CONTROL &= ~MCHP_PECI_CTRL_RST; |
| |
| #ifdef CONFIG_PECI_INTERRUPT_DRIVEN |
| /* Enable interrupt for errors */ |
| regs->INT_EN1 = (MCHP_PECI_IEN1_EREN | MCHP_PECI_IEN1_EIEN); |
| |
| /* Enable interrupt for Tx FIFO is empty */ |
| regs->INT_EN2 |= MCHP_PECI_IEN2_ENWFE; |
| /* Enable interrupt for Rx FIFO is full */ |
| regs->INT_EN2 |= MCHP_PECI_IEN2_ENRFF; |
| |
| regs->CONTROL |= MCHP_PECI_CTRL_MIEN; |
| |
| /* Direct NVIC */ |
| IRQ_CONNECT(cfg->irq_num, |
| DT_INST_IRQ(0, priority), |
| peci_xec_isr, NULL, 0); |
| #endif |
| return 0; |
| } |
| |
| static struct peci_xec_data peci_data; |
| |
| PINCTRL_DT_INST_DEFINE(0); |
| |
| static const struct peci_xec_config peci_xec_config = { |
| .regs = (struct peci_regs * const)(DT_INST_REG_ADDR(0)), |
| .irq_num = DT_INST_IRQN(0), |
| .girq = DT_INST_PROP_BY_IDX(0, girqs, 0), |
| .girq_pos = DT_INST_PROP_BY_IDX(0, girqs, 1), |
| .pcr_idx = DT_INST_PROP_BY_IDX(0, pcrs, 0), |
| .pcr_pos = DT_INST_PROP_BY_IDX(0, pcrs, 1), |
| .pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0), |
| }; |
| |
| PM_DEVICE_DT_INST_DEFINE(0, peci_xec_pm_action); |
| |
| DEVICE_DT_INST_DEFINE(0, |
| &peci_xec_init, |
| PM_DEVICE_DT_INST_GET(0), |
| &peci_data, &peci_xec_config, |
| POST_KERNEL, CONFIG_PECI_INIT_PRIORITY, |
| &peci_xec_driver_api); |