440 lines
10 KiB
C
440 lines
10 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (c) 2022 Rockchip Electronics Co. Ltd.
|
|
*
|
|
* Author: Jianwei Fan <jianwei.fan@rock-chips.com>
|
|
*
|
|
* V0.0X01.0X00 first version.
|
|
* V0.0X01.0X01 add device attr hdmirxsel.
|
|
* V0.0X01.0X02 add device attr hdmiautoswitch.
|
|
*
|
|
*/
|
|
|
|
// #define DEBUG
|
|
#include <linux/miscdevice.h>
|
|
#include <linux/cdev.h>
|
|
#include <linux/device.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/i2c-dev.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/init.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/notifier.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/uaccess.h>
|
|
#include <linux/compat.h>
|
|
#include <linux/printk.h>
|
|
#include <linux/kobject.h>
|
|
#include <linux/version.h>
|
|
#include <linux/time.h>
|
|
#include <linux/timer.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#define DRIVER_VERSION KERNEL_VERSION(0, 0x01, 0x02)
|
|
#define DRIVER_NAME "EP9461E"
|
|
|
|
/*control reg*/
|
|
#define RX_SIGNAL_DETECT 0x00
|
|
#define GENERAL_CONTROL 0x08
|
|
#define RX_SEL_CONTROL 0x09
|
|
#define EDID_ENABLE 0x0B
|
|
#define ENTER_CODE 0x07
|
|
|
|
/*control mask*/
|
|
#define MASK_RX0_SIGNAL 0x10
|
|
#define MASK_AUTO_SWITCH 0x40
|
|
#define MASK_CEC_SWITCH 0x20
|
|
#define MASK_POWER 0x80
|
|
#define MASK_RX_SEL 0x0f
|
|
|
|
struct ep9461e_dev {
|
|
struct device *dev;
|
|
struct miscdevice miscdev;
|
|
struct i2c_client *client;
|
|
struct mutex confctl_mutex;
|
|
struct timer_list timer;
|
|
struct delayed_work work_i2c_poll;
|
|
bool auto_switch_en;
|
|
bool power_up_chip_en;
|
|
bool cec_switch_en;
|
|
bool nosignal;
|
|
u32 hdmi_rx_sel;
|
|
int err_cnt;
|
|
};
|
|
|
|
static struct ep9461e_dev *g_ep9461e;
|
|
static struct ep9461e_dev *ep9461e;
|
|
|
|
static void ep9461e_rx_select(struct ep9461e_dev *ep9461e);
|
|
static void ep9461e_rx_manual_select(struct ep9461e_dev *ep9461e);
|
|
|
|
static void i2c_wr(struct ep9461e_dev *ep9461e, u16 reg, u8 *val, u32 n)
|
|
{
|
|
struct i2c_msg msg;
|
|
struct i2c_client *client = ep9461e->client;
|
|
int err;
|
|
u8 data[128];
|
|
|
|
data[0] = reg;
|
|
memcpy(&data[1], val, n);
|
|
msg.addr = client->addr;
|
|
msg.flags = 0;
|
|
msg.buf = data;
|
|
msg.len = n + 1;
|
|
|
|
err = i2c_transfer(client->adapter, &msg, 1);
|
|
if (err != 1) {
|
|
dev_err(ep9461e->dev, "writing register 0x%x from 0x%x failed\n",
|
|
reg, client->addr);
|
|
} else {
|
|
switch (n) {
|
|
case 1:
|
|
dev_dbg(ep9461e->dev, "I2C write 0x%02x = 0x%02x\n",
|
|
reg, data[1]);
|
|
break;
|
|
case 2:
|
|
dev_dbg(ep9461e->dev,
|
|
"I2C write 0x%02x = 0x%02x%02x\n",
|
|
reg, data[2], data[1]);
|
|
break;
|
|
case 4:
|
|
dev_dbg(ep9461e->dev,
|
|
"I2C write 0x%02x = 0x%02x%02x%02x%02x\n",
|
|
reg, data[4], data[3], data[2], data[1]);
|
|
break;
|
|
default:
|
|
dev_dbg(ep9461e->dev,
|
|
"I2C write %d bytes from address 0x%02x\n",
|
|
n, reg);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void i2c_rd(struct ep9461e_dev *ep9461e, u16 reg, u8 *val, u32 n)
|
|
{
|
|
struct i2c_msg msg[2];
|
|
struct i2c_client *client = ep9461e->client;
|
|
int err;
|
|
u8 buf[1] = { reg };
|
|
|
|
/*msg[0] addr to read*/
|
|
msg[0].addr = client->addr;
|
|
msg[0].flags = 0;
|
|
msg[0].buf = buf;
|
|
msg[0].len = 1;
|
|
|
|
/*msg[1] read data*/
|
|
msg[1].addr = client->addr;
|
|
msg[1].flags = I2C_M_RD;
|
|
msg[1].buf = val;
|
|
msg[1].len = n;
|
|
|
|
err = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
|
|
if (err != ARRAY_SIZE(msg)) {
|
|
dev_err(ep9461e->dev, "reading register 0x%x from 0x%x failed\n",
|
|
reg, client->addr);
|
|
}
|
|
}
|
|
|
|
static void i2c_rd8(struct ep9461e_dev *ep9461e, u16 reg, u8 *val)
|
|
{
|
|
i2c_rd(ep9461e, reg, val, 1);
|
|
}
|
|
|
|
static void i2c_wr8(struct ep9461e_dev *ep9461e, u16 reg, u8 buf)
|
|
{
|
|
i2c_wr(ep9461e, reg, &buf, 1);
|
|
}
|
|
|
|
static void i2c_wr8_and_or(struct ep9461e_dev *ep9461e, u16 reg, u32 mask,
|
|
u32 val)
|
|
{
|
|
u8 val_p;
|
|
|
|
i2c_rd8(ep9461e, reg, &val_p);
|
|
i2c_wr8(ep9461e, reg, (val_p & mask) | val);
|
|
}
|
|
|
|
static long ep9461e_ioctl(struct file *file, uint32_t cmd, unsigned long arg)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t ep9461e_write(struct file *file, const char __user *buf,
|
|
size_t size, loff_t *ppos)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
static ssize_t ep9461e_read(struct file *file, char __user *buf, size_t size,
|
|
loff_t *ppos)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
static ssize_t hdmirxsel_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct ep9461e_dev *ep9461e = g_ep9461e;
|
|
|
|
dev_info(ep9461e->dev, "%s: hdmi rx select state: %d\n",
|
|
__func__, ep9461e->hdmi_rx_sel);
|
|
|
|
return sprintf(buf, "%d\n", ep9461e->hdmi_rx_sel);
|
|
}
|
|
|
|
static ssize_t hdmirxsel_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct ep9461e_dev *ep9461e = g_ep9461e;
|
|
u32 hdmirxstate = 0;
|
|
int ret;
|
|
|
|
ret = kstrtouint(buf, 10, &hdmirxstate);
|
|
if (!ret) {
|
|
dev_dbg(ep9461e->dev, "state: %d\n", hdmirxstate);
|
|
ep9461e->hdmi_rx_sel = hdmirxstate;
|
|
if (ep9461e->auto_switch_en | ep9461e->cec_switch_en)
|
|
ep9461e->auto_switch_en = ep9461e->cec_switch_en = 0;
|
|
ep9461e_rx_select(ep9461e);
|
|
} else {
|
|
dev_err(ep9461e->dev, "write hdmi_rx_sel failed!!!\n");
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static ssize_t hdmiautoswitch_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct ep9461e_dev *ep9461e = g_ep9461e;
|
|
|
|
dev_info(ep9461e->dev, "hdmi rx select auto_switch state: %d\n",
|
|
ep9461e->auto_switch_en);
|
|
|
|
return sprintf(buf, "%d\n", ep9461e->auto_switch_en);
|
|
}
|
|
|
|
static ssize_t hdmiautoswitch_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
struct ep9461e_dev *ep9461e = g_ep9461e;
|
|
u32 hdmiautoswitch = 0;
|
|
int ret;
|
|
|
|
ret = kstrtouint(buf, 10, &hdmiautoswitch);
|
|
if (!ret) {
|
|
dev_dbg(ep9461e->dev, "state: %d\n", hdmiautoswitch);
|
|
ep9461e->auto_switch_en = hdmiautoswitch;
|
|
ep9461e_rx_select(ep9461e);
|
|
} else {
|
|
dev_err(ep9461e->dev, "write hdmi auto switch failed!!!\n");
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static DEVICE_ATTR_RW(hdmirxsel);
|
|
static DEVICE_ATTR_RW(hdmiautoswitch);
|
|
|
|
static inline bool detect_rx_signal(struct ep9461e_dev *ep9461e)
|
|
{
|
|
u8 val;
|
|
|
|
i2c_rd8(ep9461e, RX_SIGNAL_DETECT, &val);
|
|
if (!(val & MASK_RX0_SIGNAL))
|
|
return false;
|
|
else
|
|
return true;
|
|
}
|
|
|
|
static void ep9461e_init(struct ep9461e_dev *ep9461e)
|
|
{
|
|
ep9461e->power_up_chip_en = false;
|
|
ep9461e->auto_switch_en = false;
|
|
ep9461e->hdmi_rx_sel = 0;
|
|
ep9461e->err_cnt = 0;
|
|
|
|
if (ep9461e->power_up_chip_en) {
|
|
i2c_wr8_and_or(ep9461e, GENERAL_CONTROL, ~MASK_POWER,
|
|
MASK_POWER);
|
|
}
|
|
ep9461e_rx_select(ep9461e);
|
|
schedule_delayed_work(&ep9461e->work_i2c_poll, msecs_to_jiffies(1000));
|
|
}
|
|
|
|
static void ep9461e_rx_manual_select(struct ep9461e_dev *ep9461e)
|
|
{
|
|
i2c_wr8(ep9461e, RX_SEL_CONTROL, ep9461e->hdmi_rx_sel);
|
|
}
|
|
|
|
static void ep9461e_rx_select(struct ep9461e_dev *ep9461e)
|
|
{
|
|
int ret;
|
|
|
|
if (ep9461e->auto_switch_en) {
|
|
i2c_wr8_and_or(ep9461e, GENERAL_CONTROL, ~MASK_AUTO_SWITCH,
|
|
MASK_AUTO_SWITCH);
|
|
} else {
|
|
ep9461e_rx_manual_select(ep9461e);
|
|
}
|
|
|
|
ret = detect_rx_signal(ep9461e);
|
|
if (ret)
|
|
dev_info(ep9461e->dev, "Detect HDMI RX valid signal!\n");
|
|
else
|
|
dev_err(ep9461e->dev, "HDMI RX has no valid signal!\n");
|
|
}
|
|
|
|
|
|
static void ep9461e_work_i2c_poll(struct work_struct *work)
|
|
{
|
|
int ret;
|
|
struct delayed_work *dwork = to_delayed_work(work);
|
|
struct ep9461e_dev *ep9461e =
|
|
container_of(dwork, struct ep9461e_dev, work_i2c_poll);
|
|
|
|
ret = detect_rx_signal(ep9461e);
|
|
if (!ret && (ep9461e->err_cnt < 10)) {
|
|
ep9461e->err_cnt++;
|
|
dev_err(ep9461e->dev,
|
|
"ERROR: HDMI RX has no valid signal, err cnt: %d\n",
|
|
ep9461e->err_cnt);
|
|
if (ep9461e->err_cnt >= 10)
|
|
dev_err(ep9461e->dev,
|
|
"error count greater than 10, please check HDMIRX!");
|
|
} else if (ret) {
|
|
ep9461e->err_cnt = 0;
|
|
}
|
|
schedule_delayed_work(&ep9461e->work_i2c_poll, msecs_to_jiffies(1000));
|
|
}
|
|
|
|
static const struct file_operations ep9461e_fops = {
|
|
.owner = THIS_MODULE,
|
|
.read = ep9461e_read,
|
|
.write = ep9461e_write,
|
|
.unlocked_ioctl = ep9461e_ioctl,
|
|
};
|
|
|
|
struct miscdevice ep9461e_miscdev = {
|
|
.minor = MISC_DYNAMIC_MINOR,
|
|
.name = "ep9461e_dev",
|
|
.fops = &ep9461e_fops,
|
|
};
|
|
|
|
static int ep9461e_probe(struct i2c_client *client,
|
|
const struct i2c_device_id *id)
|
|
{
|
|
struct ep9461e_dev *ep9461e;
|
|
struct device *dev = &client->dev;
|
|
int ret;
|
|
|
|
dev_info(dev, "driver version: %02x.%02x.%02x",
|
|
DRIVER_VERSION >> 16,
|
|
(DRIVER_VERSION & 0xff00) >> 8,
|
|
DRIVER_VERSION & 0x00ff);
|
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
|
|
return -EIO;
|
|
dev_info(dev, "chip found @ 0x%x (%s)\n", client->addr << 1,
|
|
client->adapter->name);
|
|
|
|
ep9461e = devm_kzalloc(dev, sizeof(struct ep9461e_dev), GFP_KERNEL);
|
|
if (!ep9461e)
|
|
return -ENOMEM;
|
|
|
|
ep9461e->client = client;
|
|
ep9461e->dev = dev;
|
|
client->flags |= I2C_CLIENT_SCCB;
|
|
|
|
ret = misc_register(&ep9461e_miscdev);
|
|
if (ret) {
|
|
dev_err(ep9461e->dev,
|
|
"EP9461E ERROR: could not register ep9461e device\n");
|
|
return ret;
|
|
}
|
|
|
|
mutex_init(&ep9461e->confctl_mutex);
|
|
ret = device_create_file(ep9461e_miscdev.this_device,
|
|
&dev_attr_hdmirxsel);
|
|
if (ret) {
|
|
dev_err(ep9461e->dev, "failed to create attr hdmirxsel!\n");
|
|
goto err1;
|
|
}
|
|
|
|
ret = device_create_file(ep9461e_miscdev.this_device,
|
|
&dev_attr_hdmiautoswitch);
|
|
if (ret) {
|
|
dev_err(ep9461e->dev,
|
|
"failed to create attr hdmiautoswitch!\n");
|
|
goto err;
|
|
}
|
|
|
|
INIT_DELAYED_WORK(&ep9461e->work_i2c_poll, ep9461e_work_i2c_poll);
|
|
|
|
ep9461e_init(ep9461e);
|
|
g_ep9461e = ep9461e;
|
|
|
|
dev_info(ep9461e->dev, "%s found @ 0x%x (%s)\n",
|
|
client->name, client->addr << 1,
|
|
client->adapter->name);
|
|
|
|
return 0;
|
|
|
|
err:
|
|
device_remove_file(ep9461e_miscdev.this_device,
|
|
&dev_attr_hdmirxsel);
|
|
err1:
|
|
misc_deregister(&ep9461e_miscdev);
|
|
return ret;
|
|
}
|
|
|
|
static int ep9461e_remove(struct i2c_client *client)
|
|
{
|
|
cancel_delayed_work_sync(&ep9461e->work_i2c_poll);
|
|
device_remove_file(ep9461e_miscdev.this_device,
|
|
&dev_attr_hdmirxsel);
|
|
device_remove_file(ep9461e_miscdev.this_device,
|
|
&dev_attr_hdmiautoswitch);
|
|
mutex_destroy(&ep9461e->confctl_mutex);
|
|
misc_deregister(&ep9461e_miscdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id ep9461e_of_match[] = {
|
|
{ .compatible = "semiconn,ep9461e" },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, ep9461e_of_match);
|
|
|
|
static struct i2c_driver ep9461e_driver = {
|
|
.probe = ep9461e_probe,
|
|
.remove = ep9461e_remove,
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = DRIVER_NAME,
|
|
.of_match_table = of_match_ptr(ep9461e_of_match),
|
|
},
|
|
};
|
|
|
|
static int __init ep9461e_driver_init(void)
|
|
{
|
|
return i2c_add_driver(&ep9461e_driver);
|
|
}
|
|
|
|
static void __exit ep9461e_driver_exit(void)
|
|
{
|
|
i2c_del_driver(&ep9461e_driver);
|
|
}
|
|
|
|
device_initcall_sync(ep9461e_driver_init);
|
|
module_exit(ep9461e_driver_exit);
|
|
|
|
MODULE_DESCRIPTION("semiconn EP9461E 4 HDMI in switch driver");
|
|
MODULE_AUTHOR("Jianwei Fan <jianwei.fan@rock-chips.com>");
|
|
MODULE_LICENSE("GPL v2");
|