| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * STM32 CEC driver |
| * Copyright (C) STMicroelectronics SA 2017 |
| * |
| */ |
| |
| #include <linux/clk.h> |
| #include <linux/interrupt.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_device.h> |
| #include <linux/platform_device.h> |
| #include <linux/regmap.h> |
| |
| #include <media/cec.h> |
| |
| #define CEC_NAME "stm32-cec" |
| |
| /* CEC registers */ |
| #define CEC_CR 0x0000 /* Control Register */ |
| #define CEC_CFGR 0x0004 /* ConFiGuration Register */ |
| #define CEC_TXDR 0x0008 /* Rx data Register */ |
| #define CEC_RXDR 0x000C /* Rx data Register */ |
| #define CEC_ISR 0x0010 /* Interrupt and status Register */ |
| #define CEC_IER 0x0014 /* Interrupt enable Register */ |
| |
| #define TXEOM BIT(2) |
| #define TXSOM BIT(1) |
| #define CECEN BIT(0) |
| |
| #define LSTN BIT(31) |
| #define OAR GENMASK(30, 16) |
| #define SFTOP BIT(8) |
| #define BRDNOGEN BIT(7) |
| #define LBPEGEN BIT(6) |
| #define BREGEN BIT(5) |
| #define BRESTP BIT(4) |
| #define RXTOL BIT(3) |
| #define SFT GENMASK(2, 0) |
| #define FULL_CFG (LSTN | SFTOP | BRDNOGEN | LBPEGEN | BREGEN | BRESTP \ |
| | RXTOL) |
| |
| #define TXACKE BIT(12) |
| #define TXERR BIT(11) |
| #define TXUDR BIT(10) |
| #define TXEND BIT(9) |
| #define TXBR BIT(8) |
| #define ARBLST BIT(7) |
| #define RXACKE BIT(6) |
| #define RXOVR BIT(2) |
| #define RXEND BIT(1) |
| #define RXBR BIT(0) |
| |
| #define ALL_TX_IT (TXEND | TXBR | TXACKE | TXERR | TXUDR | ARBLST) |
| #define ALL_RX_IT (RXEND | RXBR | RXACKE | RXOVR) |
| |
| struct stm32_cec { |
| struct cec_adapter *adap; |
| struct device *dev; |
| struct clk *clk_cec; |
| struct clk *clk_hdmi_cec; |
| struct reset_control *rstc; |
| struct regmap *regmap; |
| int irq; |
| u32 irq_status; |
| struct cec_msg rx_msg; |
| struct cec_msg tx_msg; |
| int tx_cnt; |
| }; |
| |
| static void cec_hw_init(struct stm32_cec *cec) |
| { |
| regmap_update_bits(cec->regmap, CEC_CR, TXEOM | TXSOM | CECEN, 0); |
| |
| regmap_update_bits(cec->regmap, CEC_IER, ALL_TX_IT | ALL_RX_IT, |
| ALL_TX_IT | ALL_RX_IT); |
| |
| regmap_update_bits(cec->regmap, CEC_CFGR, FULL_CFG, FULL_CFG); |
| } |
| |
| static void stm32_tx_done(struct stm32_cec *cec, u32 status) |
| { |
| if (status & (TXERR | TXUDR)) { |
| cec_transmit_done(cec->adap, CEC_TX_STATUS_ERROR, |
| 0, 0, 0, 1); |
| return; |
| } |
| |
| if (status & ARBLST) { |
| cec_transmit_done(cec->adap, CEC_TX_STATUS_ARB_LOST, |
| 1, 0, 0, 0); |
| return; |
| } |
| |
| if (status & TXACKE) { |
| cec_transmit_done(cec->adap, CEC_TX_STATUS_NACK, |
| 0, 1, 0, 0); |
| return; |
| } |
| |
| if (cec->irq_status & TXBR) { |
| /* send next byte */ |
| if (cec->tx_cnt < cec->tx_msg.len) |
| regmap_write(cec->regmap, CEC_TXDR, |
| cec->tx_msg.msg[cec->tx_cnt++]); |
| |
| /* TXEOM is set to command transmission of the last byte */ |
| if (cec->tx_cnt == cec->tx_msg.len) |
| regmap_update_bits(cec->regmap, CEC_CR, TXEOM, TXEOM); |
| } |
| |
| if (cec->irq_status & TXEND) |
| cec_transmit_done(cec->adap, CEC_TX_STATUS_OK, 0, 0, 0, 0); |
| } |
| |
| static void stm32_rx_done(struct stm32_cec *cec, u32 status) |
| { |
| if (cec->irq_status & (RXACKE | RXOVR)) { |
| cec->rx_msg.len = 0; |
| return; |
| } |
| |
| if (cec->irq_status & RXBR) { |
| u32 val; |
| |
| regmap_read(cec->regmap, CEC_RXDR, &val); |
| cec->rx_msg.msg[cec->rx_msg.len++] = val & 0xFF; |
| } |
| |
| if (cec->irq_status & RXEND) { |
| cec_received_msg(cec->adap, &cec->rx_msg); |
| cec->rx_msg.len = 0; |
| } |
| } |
| |
| static irqreturn_t stm32_cec_irq_thread(int irq, void *arg) |
| { |
| struct stm32_cec *cec = arg; |
| |
| if (cec->irq_status & ALL_TX_IT) |
| stm32_tx_done(cec, cec->irq_status); |
| |
| if (cec->irq_status & ALL_RX_IT) |
| stm32_rx_done(cec, cec->irq_status); |
| |
| cec->irq_status = 0; |
| |
| return IRQ_HANDLED; |
| } |
| |
| static irqreturn_t stm32_cec_irq_handler(int irq, void *arg) |
| { |
| struct stm32_cec *cec = arg; |
| |
| regmap_read(cec->regmap, CEC_ISR, &cec->irq_status); |
| |
| regmap_update_bits(cec->regmap, CEC_ISR, |
| ALL_TX_IT | ALL_RX_IT, |
| ALL_TX_IT | ALL_RX_IT); |
| |
| return IRQ_WAKE_THREAD; |
| } |
| |
| static int stm32_cec_adap_enable(struct cec_adapter *adap, bool enable) |
| { |
| struct stm32_cec *cec = adap->priv; |
| int ret = 0; |
| |
| if (enable) { |
| ret = clk_enable(cec->clk_cec); |
| if (ret) |
| dev_err(cec->dev, "fail to enable cec clock\n"); |
| |
| clk_enable(cec->clk_hdmi_cec); |
| regmap_update_bits(cec->regmap, CEC_CR, CECEN, CECEN); |
| } else { |
| clk_disable(cec->clk_cec); |
| clk_disable(cec->clk_hdmi_cec); |
| regmap_update_bits(cec->regmap, CEC_CR, CECEN, 0); |
| } |
| |
| return ret; |
| } |
| |
| static int stm32_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr) |
| { |
| struct stm32_cec *cec = adap->priv; |
| u32 oar = (1 << logical_addr) << 16; |
| |
| regmap_update_bits(cec->regmap, CEC_CR, CECEN, 0); |
| |
| if (logical_addr == CEC_LOG_ADDR_INVALID) |
| regmap_update_bits(cec->regmap, CEC_CFGR, OAR, 0); |
| else |
| regmap_update_bits(cec->regmap, CEC_CFGR, oar, oar); |
| |
| regmap_update_bits(cec->regmap, CEC_CR, CECEN, CECEN); |
| |
| return 0; |
| } |
| |
| static int stm32_cec_adap_transmit(struct cec_adapter *adap, u8 attempts, |
| u32 signal_free_time, struct cec_msg *msg) |
| { |
| struct stm32_cec *cec = adap->priv; |
| |
| /* Copy message */ |
| cec->tx_msg = *msg; |
| cec->tx_cnt = 0; |
| |
| /* |
| * If the CEC message consists of only one byte, |
| * TXEOM must be set before of TXSOM. |
| */ |
| if (cec->tx_msg.len == 1) |
| regmap_update_bits(cec->regmap, CEC_CR, TXEOM, TXEOM); |
| |
| /* TXSOM is set to command transmission of the first byte */ |
| regmap_update_bits(cec->regmap, CEC_CR, TXSOM, TXSOM); |
| |
| /* Write the header (first byte of message) */ |
| regmap_write(cec->regmap, CEC_TXDR, cec->tx_msg.msg[0]); |
| cec->tx_cnt++; |
| |
| return 0; |
| } |
| |
| static const struct cec_adap_ops stm32_cec_adap_ops = { |
| .adap_enable = stm32_cec_adap_enable, |
| .adap_log_addr = stm32_cec_adap_log_addr, |
| .adap_transmit = stm32_cec_adap_transmit, |
| }; |
| |
| static const struct regmap_config stm32_cec_regmap_cfg = { |
| .reg_bits = 32, |
| .val_bits = 32, |
| .reg_stride = sizeof(u32), |
| .max_register = 0x14, |
| .fast_io = true, |
| }; |
| |
| static int stm32_cec_probe(struct platform_device *pdev) |
| { |
| u32 caps = CEC_CAP_DEFAULTS | CEC_CAP_PHYS_ADDR | CEC_MODE_MONITOR_ALL; |
| struct resource *res; |
| struct stm32_cec *cec; |
| void __iomem *mmio; |
| int ret; |
| |
| cec = devm_kzalloc(&pdev->dev, sizeof(*cec), GFP_KERNEL); |
| if (!cec) |
| return -ENOMEM; |
| |
| cec->dev = &pdev->dev; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| mmio = devm_ioremap_resource(&pdev->dev, res); |
| if (IS_ERR(mmio)) |
| return PTR_ERR(mmio); |
| |
| cec->regmap = devm_regmap_init_mmio_clk(&pdev->dev, "cec", mmio, |
| &stm32_cec_regmap_cfg); |
| |
| if (IS_ERR(cec->regmap)) |
| return PTR_ERR(cec->regmap); |
| |
| cec->irq = platform_get_irq(pdev, 0); |
| if (cec->irq < 0) |
| return cec->irq; |
| |
| ret = devm_request_threaded_irq(&pdev->dev, cec->irq, |
| stm32_cec_irq_handler, |
| stm32_cec_irq_thread, |
| 0, |
| pdev->name, cec); |
| if (ret) |
| return ret; |
| |
| cec->clk_cec = devm_clk_get(&pdev->dev, "cec"); |
| if (IS_ERR(cec->clk_cec)) { |
| dev_err(&pdev->dev, "Cannot get cec clock\n"); |
| return PTR_ERR(cec->clk_cec); |
| } |
| |
| ret = clk_prepare(cec->clk_cec); |
| if (ret) { |
| dev_err(&pdev->dev, "Unable to prepare cec clock\n"); |
| return ret; |
| } |
| |
| cec->clk_hdmi_cec = devm_clk_get(&pdev->dev, "hdmi-cec"); |
| if (!IS_ERR(cec->clk_hdmi_cec)) { |
| ret = clk_prepare(cec->clk_hdmi_cec); |
| if (ret) { |
| dev_err(&pdev->dev, "Unable to prepare hdmi-cec clock\n"); |
| return ret; |
| } |
| } |
| |
| /* |
| * CEC_CAP_PHYS_ADDR caps should be removed when a cec notifier is |
| * available for example when a drm driver can provide edid |
| */ |
| cec->adap = cec_allocate_adapter(&stm32_cec_adap_ops, cec, |
| CEC_NAME, caps, CEC_MAX_LOG_ADDRS); |
| ret = PTR_ERR_OR_ZERO(cec->adap); |
| if (ret) |
| return ret; |
| |
| ret = cec_register_adapter(cec->adap, &pdev->dev); |
| if (ret) { |
| cec_delete_adapter(cec->adap); |
| return ret; |
| } |
| |
| cec_hw_init(cec); |
| |
| platform_set_drvdata(pdev, cec); |
| |
| return 0; |
| } |
| |
| static int stm32_cec_remove(struct platform_device *pdev) |
| { |
| struct stm32_cec *cec = platform_get_drvdata(pdev); |
| |
| clk_unprepare(cec->clk_cec); |
| clk_unprepare(cec->clk_hdmi_cec); |
| |
| cec_unregister_adapter(cec->adap); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id stm32_cec_of_match[] = { |
| { .compatible = "st,stm32-cec" }, |
| { /* end node */ } |
| }; |
| MODULE_DEVICE_TABLE(of, stm32_cec_of_match); |
| |
| static struct platform_driver stm32_cec_driver = { |
| .probe = stm32_cec_probe, |
| .remove = stm32_cec_remove, |
| .driver = { |
| .name = CEC_NAME, |
| .of_match_table = stm32_cec_of_match, |
| }, |
| }; |
| |
| module_platform_driver(stm32_cec_driver); |
| |
| MODULE_AUTHOR("Benjamin Gaignard <benjamin.gaignard@st.com>"); |
| MODULE_AUTHOR("Yannick Fertre <yannick.fertre@st.com>"); |
| MODULE_DESCRIPTION("STMicroelectronics STM32 Consumer Electronics Control"); |
| MODULE_LICENSE("GPL v2"); |