| /* Copyright (c) 2010-2018, The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/io.h> |
| #include <linux/list.h> |
| #include <linux/types.h> |
| #include <linux/stat.h> |
| #include <linux/slab.h> |
| #include <linux/device.h> |
| #include <linux/input.h> |
| |
| #include "mdss_hdmi_cec.h" |
| #include "mdss_panel.h" |
| |
| #define CEC_STATUS_WR_ERROR BIT(0) |
| #define CEC_STATUS_WR_DONE BIT(1) |
| #define CEC_INTR (BIT(1) | BIT(3) | BIT(7)) |
| |
| #define CEC_SUPPORTED_HW_VERSION 0x30000001 |
| |
| /* Reference: HDMI 1.4a Specification section 7.1 */ |
| |
| #define CEC_OP_SET_STREAM_PATH 0x86 |
| #define CEC_OP_KEY_PRESS 0x44 |
| #define CEC_OP_STANDBY 0x36 |
| |
| struct hdmi_cec_ctrl { |
| bool cec_enabled; |
| bool cec_wakeup_en; |
| bool cec_device_suspend; |
| |
| u32 cec_msg_wr_status; |
| spinlock_t lock; |
| struct work_struct cec_read_work; |
| struct completion cec_msg_wr_done; |
| struct hdmi_cec_init_data init_data; |
| struct input_dev *input; |
| }; |
| |
| static int hdmi_cec_msg_send(void *data, struct cec_msg *msg) |
| { |
| int i, line_check_retry = 10, rc = 0; |
| u32 frame_retransmit = RETRANSMIT_MAX_NUM; |
| bool frame_type; |
| unsigned long flags; |
| struct mdss_io_data *io = NULL; |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)data; |
| |
| if (!cec_ctrl || !cec_ctrl->init_data.io || !msg) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| return -EINVAL; |
| } |
| |
| io = cec_ctrl->init_data.io; |
| |
| reinit_completion(&cec_ctrl->cec_msg_wr_done); |
| cec_ctrl->cec_msg_wr_status = 0; |
| frame_type = (msg->recvr_id == 15 ? BIT(0) : 0); |
| if (msg->retransmit > 0 && msg->retransmit < RETRANSMIT_MAX_NUM) |
| frame_retransmit = msg->retransmit; |
| |
| /* toggle cec in order to flush out bad hw state, if any */ |
| DSS_REG_W(io, HDMI_CEC_CTRL, 0); |
| DSS_REG_W(io, HDMI_CEC_CTRL, BIT(0)); |
| |
| frame_retransmit = (frame_retransmit & 0xF) << 4; |
| DSS_REG_W(io, HDMI_CEC_RETRANSMIT, BIT(0) | frame_retransmit); |
| |
| /* header block */ |
| DSS_REG_W_ND(io, HDMI_CEC_WR_DATA, |
| (((msg->sender_id << 4) | msg->recvr_id) << 8) | frame_type); |
| |
| /* data block 0 : opcode */ |
| DSS_REG_W_ND(io, HDMI_CEC_WR_DATA, |
| ((msg->frame_size < 2 ? 0 : msg->opcode) << 8) | frame_type); |
| |
| /* data block 1-14 : operand 0-13 */ |
| for (i = 0; i < msg->frame_size - 2; i++) |
| DSS_REG_W_ND(io, HDMI_CEC_WR_DATA, |
| (msg->operand[i] << 8) | frame_type); |
| |
| while ((DSS_REG_R(io, HDMI_CEC_STATUS) & BIT(0)) && |
| line_check_retry) { |
| line_check_retry--; |
| DEV_DBG("%s: CEC line is busy(%d)\n", __func__, |
| line_check_retry); |
| schedule(); |
| } |
| |
| if (!line_check_retry && (DSS_REG_R(io, HDMI_CEC_STATUS) & BIT(0))) { |
| DEV_ERR("%s: CEC line is busy. Retry\n", __func__); |
| return -EAGAIN; |
| } |
| |
| /* start transmission */ |
| DSS_REG_W(io, HDMI_CEC_CTRL, BIT(0) | BIT(1) | |
| ((msg->frame_size & 0x1F) << 4) | BIT(9)); |
| |
| if (!wait_for_completion_timeout( |
| &cec_ctrl->cec_msg_wr_done, HZ)) { |
| DEV_ERR("%s: timedout", __func__); |
| return -ETIMEDOUT; |
| } |
| |
| spin_lock_irqsave(&cec_ctrl->lock, flags); |
| if (cec_ctrl->cec_msg_wr_status == CEC_STATUS_WR_ERROR) { |
| rc = -ENXIO; |
| DEV_ERR("%s: msg write failed.\n", __func__); |
| } else { |
| DEV_DBG("%s: CEC write frame done (frame len=%d)", __func__, |
| msg->frame_size); |
| } |
| spin_unlock_irqrestore(&cec_ctrl->lock, flags); |
| |
| return rc; |
| } /* hdmi_cec_msg_send */ |
| |
| static void hdmi_cec_init_input_event(struct hdmi_cec_ctrl *cec_ctrl) |
| { |
| int rc = 0; |
| |
| if (!cec_ctrl) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| return; |
| } |
| |
| /* Initialize CEC input events */ |
| if (!cec_ctrl->input) |
| cec_ctrl->input = input_allocate_device(); |
| if (!cec_ctrl->input) { |
| DEV_ERR("%s: hdmi input device allocation failed\n", __func__); |
| return; |
| } |
| |
| cec_ctrl->input->name = "HDMI CEC User or Deck Control"; |
| cec_ctrl->input->phys = "hdmi/input0"; |
| cec_ctrl->input->id.bustype = BUS_VIRTUAL; |
| |
| input_set_capability(cec_ctrl->input, EV_KEY, KEY_POWER); |
| |
| rc = input_register_device(cec_ctrl->input); |
| if (rc) { |
| DEV_ERR("%s: cec input device registeration failed\n", |
| __func__); |
| input_free_device(cec_ctrl->input); |
| cec_ctrl->input = NULL; |
| return; |
| } |
| } |
| |
| static void hdmi_cec_deinit_input_event(struct hdmi_cec_ctrl *cec_ctrl) |
| { |
| if (cec_ctrl->input) |
| input_unregister_device(cec_ctrl->input); |
| cec_ctrl->input = NULL; |
| } |
| |
| static void hdmi_cec_msg_recv(struct work_struct *work) |
| { |
| int i; |
| u32 data; |
| struct hdmi_cec_ctrl *cec_ctrl = NULL; |
| struct mdss_io_data *io = NULL; |
| struct cec_msg msg; |
| struct cec_cbs *cbs; |
| |
| cec_ctrl = container_of(work, struct hdmi_cec_ctrl, cec_read_work); |
| if (!cec_ctrl || !cec_ctrl->init_data.io) { |
| DEV_ERR("%s: invalid input\n", __func__); |
| return; |
| } |
| |
| if (!cec_ctrl->cec_enabled) { |
| DEV_ERR("%s: cec not enabled\n", __func__); |
| return; |
| } |
| |
| io = cec_ctrl->init_data.io; |
| cbs = cec_ctrl->init_data.cbs; |
| |
| data = DSS_REG_R(io, HDMI_CEC_RD_DATA); |
| |
| msg.recvr_id = (data & 0x000F); |
| msg.sender_id = (data & 0x00F0) >> 4; |
| msg.frame_size = (data & 0x1F00) >> 8; |
| DEV_DBG("%s: Recvd init=[%u] dest=[%u] size=[%u]\n", __func__, |
| msg.sender_id, msg.recvr_id, |
| msg.frame_size); |
| |
| if (msg.frame_size < 1 || msg.frame_size > MAX_CEC_FRAME_SIZE) { |
| DEV_ERR("%s: invalid message (frame length = %d)\n", |
| __func__, msg.frame_size); |
| return; |
| } else if (msg.frame_size == 1) { |
| DEV_DBG("%s: polling message (dest[%x] <- init[%x])\n", |
| __func__, msg.recvr_id, msg.sender_id); |
| return; |
| } |
| |
| /* data block 0 : opcode */ |
| data = DSS_REG_R_ND(io, HDMI_CEC_RD_DATA); |
| msg.opcode = data & 0xFF; |
| |
| /* data block 1-14 : operand 0-13 */ |
| for (i = 0; i < msg.frame_size - 2; i++) { |
| data = DSS_REG_R_ND(io, HDMI_CEC_RD_DATA); |
| msg.operand[i] = data & 0xFF; |
| } |
| |
| for (; i < MAX_OPERAND_SIZE; i++) |
| msg.operand[i] = 0; |
| |
| DEV_DBG("%s: opcode 0x%x, wakup_en %d, device_suspend %d\n", __func__, |
| msg.opcode, cec_ctrl->cec_wakeup_en, |
| cec_ctrl->cec_device_suspend); |
| |
| if ((msg.opcode == CEC_OP_SET_STREAM_PATH || |
| msg.opcode == CEC_OP_KEY_PRESS) && |
| cec_ctrl->input && cec_ctrl->cec_wakeup_en && |
| cec_ctrl->cec_device_suspend) { |
| DEV_DBG("%s: Sending power on at wakeup\n", __func__); |
| input_report_key(cec_ctrl->input, KEY_POWER, 1); |
| input_sync(cec_ctrl->input); |
| input_report_key(cec_ctrl->input, KEY_POWER, 0); |
| input_sync(cec_ctrl->input); |
| } |
| |
| if ((msg.opcode == CEC_OP_STANDBY) && |
| cec_ctrl->input && cec_ctrl->cec_wakeup_en && |
| !cec_ctrl->cec_device_suspend) { |
| DEV_DBG("%s: Sending power off on standby\n", __func__); |
| input_report_key(cec_ctrl->input, KEY_POWER, 1); |
| input_sync(cec_ctrl->input); |
| input_report_key(cec_ctrl->input, KEY_POWER, 0); |
| input_sync(cec_ctrl->input); |
| } |
| |
| if (cbs && cbs->msg_recv_notify) |
| cbs->msg_recv_notify(cbs->data, &msg); |
| } |
| |
| /** |
| * hdmi_cec_isr() - interrupt handler for cec hw module |
| * @cec_ctrl: pointer to cec hw module's data |
| * |
| * Return: irq error code |
| * |
| * The API can be called by HDMI Tx driver on receiving hw interrupts |
| * to let the CEC related interrupts handled by this module. |
| */ |
| int hdmi_cec_isr(void *input) |
| { |
| int rc = 0; |
| u32 cec_intr, cec_status; |
| unsigned long flags; |
| struct mdss_io_data *io = NULL; |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| |
| if (!cec_ctrl || !cec_ctrl->init_data.io) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| return -EPERM; |
| } |
| |
| if (!cec_ctrl->cec_enabled) { |
| DEV_DBG("%s: CEC feature not enabled\n", __func__); |
| return 0; |
| } |
| |
| io = cec_ctrl->init_data.io; |
| |
| cec_intr = DSS_REG_R_ND(io, HDMI_CEC_INT); |
| |
| cec_status = DSS_REG_R_ND(io, HDMI_CEC_STATUS); |
| |
| if ((cec_intr & BIT(0)) && (cec_intr & BIT(1))) { |
| DEV_DBG("%s: CEC_IRQ_FRAME_WR_DONE\n", __func__); |
| DSS_REG_W(io, HDMI_CEC_INT, cec_intr | BIT(0)); |
| |
| spin_lock_irqsave(&cec_ctrl->lock, flags); |
| cec_ctrl->cec_msg_wr_status |= CEC_STATUS_WR_DONE; |
| spin_unlock_irqrestore(&cec_ctrl->lock, flags); |
| |
| if (!completion_done(&cec_ctrl->cec_msg_wr_done)) |
| complete_all(&cec_ctrl->cec_msg_wr_done); |
| } |
| |
| if ((cec_intr & BIT(2)) && (cec_intr & BIT(3))) { |
| DEV_DBG("%s: CEC_IRQ_FRAME_ERROR\n", __func__); |
| DSS_REG_W(io, HDMI_CEC_INT, cec_intr | BIT(2)); |
| |
| spin_lock_irqsave(&cec_ctrl->lock, flags); |
| cec_ctrl->cec_msg_wr_status |= CEC_STATUS_WR_ERROR; |
| spin_unlock_irqrestore(&cec_ctrl->lock, flags); |
| |
| if (!completion_done(&cec_ctrl->cec_msg_wr_done)) |
| complete_all(&cec_ctrl->cec_msg_wr_done); |
| } |
| |
| if ((cec_intr & BIT(6)) && (cec_intr & BIT(7))) { |
| DEV_DBG("%s: CEC_IRQ_FRAME_RD_DONE\n", __func__); |
| |
| DSS_REG_W(io, HDMI_CEC_INT, cec_intr | BIT(6)); |
| queue_work(cec_ctrl->init_data.workq, &cec_ctrl->cec_read_work); |
| } |
| |
| return rc; |
| } |
| |
| void hdmi_cec_device_suspend(void *input, bool suspend) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| |
| if (!cec_ctrl) { |
| DEV_WARN("%s: HDMI CEC HW module not initialized.\n", __func__); |
| return; |
| } |
| |
| cec_ctrl->cec_device_suspend = suspend; |
| } |
| |
| bool hdmi_cec_is_wakeup_en(void *input) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| |
| if (!cec_ctrl) { |
| DEV_WARN("%s: HDMI CEC HW module not initialized.\n", __func__); |
| return 0; |
| } |
| |
| return cec_ctrl->cec_wakeup_en; |
| } |
| |
| static void hdmi_cec_wakeup_en(void *input, bool enable) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| |
| if (!cec_ctrl) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| return; |
| } |
| |
| cec_ctrl->cec_wakeup_en = enable; |
| } |
| |
| static void hdmi_cec_write_logical_addr(void *input, u8 addr) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| |
| if (!cec_ctrl || !cec_ctrl->init_data.io) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| return; |
| } |
| |
| if (cec_ctrl->cec_enabled) |
| DSS_REG_W(cec_ctrl->init_data.io, HDMI_CEC_ADDR, addr & 0xF); |
| } |
| |
| static int hdmi_cec_enable(void *input, bool enable) |
| { |
| int ret = 0; |
| u32 hdmi_hw_version, reg_val; |
| struct mdss_io_data *io = NULL; |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)input; |
| struct mdss_panel_info *pinfo; |
| |
| if (!cec_ctrl || !cec_ctrl->init_data.io) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| ret = -EPERM; |
| goto end; |
| } |
| |
| io = cec_ctrl->init_data.io; |
| pinfo = cec_ctrl->init_data.pinfo; |
| |
| if (!pinfo) { |
| DEV_ERR("%s: invalid pinfo\n", __func__); |
| goto end; |
| } |
| |
| if (enable) { |
| /* 19.2Mhz * 0.00005 us = 950 = 0x3B6 */ |
| DSS_REG_W(io, HDMI_CEC_REFTIMER, (0x3B6 & 0xFFF) | BIT(16)); |
| |
| hdmi_hw_version = DSS_REG_R(io, HDMI_VERSION); |
| if (hdmi_hw_version >= CEC_SUPPORTED_HW_VERSION) { |
| DSS_REG_W(io, HDMI_CEC_RD_RANGE, 0x30AB9888); |
| DSS_REG_W(io, HDMI_CEC_WR_RANGE, 0x888AA888); |
| |
| DSS_REG_W(io, HDMI_CEC_RD_START_RANGE, 0x88888888); |
| DSS_REG_W(io, HDMI_CEC_RD_TOTAL_RANGE, 0x99); |
| DSS_REG_W(io, HDMI_CEC_COMPL_CTL, 0xF); |
| DSS_REG_W(io, HDMI_CEC_WR_CHECK_CONFIG, 0x4); |
| } else { |
| DEV_DBG("%s: CEC version %d is not supported.\n", |
| __func__, hdmi_hw_version); |
| ret = -EPERM; |
| goto end; |
| } |
| |
| DSS_REG_W(io, HDMI_CEC_RD_FILTER, BIT(0) | (0x7FF << 4)); |
| DSS_REG_W(io, HDMI_CEC_TIME, BIT(0) | ((7 * 0x30) << 7)); |
| |
| /* Enable CEC interrupts */ |
| DSS_REG_W(io, HDMI_CEC_INT, CEC_INTR); |
| |
| /* Enable Engine */ |
| DSS_REG_W(io, HDMI_CEC_CTRL, BIT(0)); |
| } else { |
| /* Disable Engine */ |
| DSS_REG_W(io, HDMI_CEC_CTRL, 0); |
| |
| /* Disable CEC interrupts */ |
| reg_val = DSS_REG_R(io, HDMI_CEC_INT); |
| DSS_REG_W(io, HDMI_CEC_INT, reg_val & ~CEC_INTR); |
| } |
| |
| cec_ctrl->cec_enabled = enable; |
| end: |
| return ret; |
| } |
| |
| /** |
| * hdmi_cec_init() - Initialize the CEC hw module |
| * @init_data: data needed to initialize the cec hw module |
| * |
| * Return: pointer to cec hw modules data that needs to be passed when |
| * calling cec hw modules API or error code. |
| * |
| * The API registers CEC HW modules with the client and provides HW |
| * specific operations. |
| */ |
| void *hdmi_cec_init(struct hdmi_cec_init_data *init_data) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl; |
| struct cec_ops *ops; |
| int ret = 0; |
| |
| if (!init_data) { |
| DEV_ERR("%s: Invalid input\n", __func__); |
| ret = -EINVAL; |
| goto error; |
| } |
| |
| ops = init_data->ops; |
| if (!ops) { |
| DEV_ERR("%s: no ops provided\n", __func__); |
| ret = -EINVAL; |
| goto error; |
| } |
| |
| cec_ctrl = kzalloc(sizeof(*cec_ctrl), GFP_KERNEL); |
| if (!cec_ctrl) { |
| DEV_ERR("%s: FAILED: out of memory\n", __func__); |
| ret = -EINVAL; |
| goto error; |
| } |
| |
| /* keep a copy of init data */ |
| cec_ctrl->init_data = *init_data; |
| |
| spin_lock_init(&cec_ctrl->lock); |
| INIT_WORK(&cec_ctrl->cec_read_work, hdmi_cec_msg_recv); |
| init_completion(&cec_ctrl->cec_msg_wr_done); |
| |
| /* populate hardware specific operations to client */ |
| ops->send_msg = hdmi_cec_msg_send; |
| ops->wt_logical_addr = hdmi_cec_write_logical_addr; |
| ops->enable = hdmi_cec_enable; |
| ops->data = cec_ctrl; |
| ops->wakeup_en = hdmi_cec_wakeup_en; |
| ops->is_wakeup_en = hdmi_cec_is_wakeup_en; |
| ops->device_suspend = hdmi_cec_device_suspend; |
| |
| hdmi_cec_init_input_event(cec_ctrl); |
| |
| return cec_ctrl; |
| error: |
| return ERR_PTR(ret); |
| } |
| |
| /** |
| * hdmi_cec_deinit() - de-initialize CEC HW module |
| * @data: CEC HW module data |
| * |
| * This API release all resources allocated. |
| */ |
| void hdmi_cec_deinit(void *data) |
| { |
| struct hdmi_cec_ctrl *cec_ctrl = (struct hdmi_cec_ctrl *)data; |
| |
| if (cec_ctrl) |
| hdmi_cec_deinit_input_event(cec_ctrl); |
| |
| kfree(cec_ctrl); |
| } |