438 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			438 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 void 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);
 | 
						|
}
 | 
						|
 | 
						|
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");
 |