diff options
Diffstat (limited to 'drivers/media/spi')
-rw-r--r-- | drivers/media/spi/Kconfig | 26 | ||||
-rw-r--r-- | drivers/media/spi/Makefile | 8 | ||||
-rw-r--r-- | drivers/media/spi/cxd2880-spi.c | 667 | ||||
-rw-r--r-- | drivers/media/spi/gs1662.c | 482 |
4 files changed, 1183 insertions, 0 deletions
diff --git a/drivers/media/spi/Kconfig b/drivers/media/spi/Kconfig new file mode 100644 index 0000000000..4656afae5b --- /dev/null +++ b/drivers/media/spi/Kconfig @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: GPL-2.0-only +if VIDEO_DEV && SPI + +comment "SPI I2C drivers auto-selected by 'Autoselect ancillary drivers'" + depends on MEDIA_HIDE_ANCILLARY_SUBDRV && SPI + +menu "Media SPI Adapters" + +config CXD2880_SPI_DRV + tristate "Sony CXD2880 SPI support" + depends on DVB_CORE && SPI + default m if !MEDIA_SUBDRV_AUTOSELECT + help + Choose if you would like to have SPI interface support for Sony CXD2880. + +config VIDEO_GS1662 + tristate "Gennum Serializers video" + depends on SPI && VIDEO_DEV + select MEDIA_CONTROLLER + select VIDEO_V4L2_SUBDEV_API + help + Enable the GS1662 driver which serializes video streams. + +endmenu + +endif diff --git a/drivers/media/spi/Makefile b/drivers/media/spi/Makefile new file mode 100644 index 0000000000..6ac7adc641 --- /dev/null +++ b/drivers/media/spi/Makefile @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-2.0-only + +ccflags-y += -I $(srctree)/drivers/media/dvb-frontends/cxd2880 + +# Please keep it alphabetically sorted by Kconfig name +# (e. g. LC_ALL=C sort Makefile) +obj-$(CONFIG_CXD2880_SPI_DRV) += cxd2880-spi.o +obj-$(CONFIG_VIDEO_GS1662) += gs1662.o diff --git a/drivers/media/spi/cxd2880-spi.c b/drivers/media/spi/cxd2880-spi.c new file mode 100644 index 0000000000..6be4e55288 --- /dev/null +++ b/drivers/media/spi/cxd2880-spi.c @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * cxd2880-spi.c + * Sony CXD2880 DVB-T2/T tuner + demodulator driver + * SPI adapter + * + * Copyright (C) 2016, 2017, 2018 Sony Semiconductor Solutions Corporation + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": %s: " fmt, __func__ + +#include <linux/spi/spi.h> +#include <linux/regulator/consumer.h> +#include <linux/ktime.h> + +#include <media/dvb_demux.h> +#include <media/dmxdev.h> +#include <media/dvb_frontend.h> +#include "cxd2880.h" + +#define CXD2880_MAX_FILTER_SIZE 32 +#define BURST_WRITE_MAX 128 +#define MAX_TRANS_PKT 300 + +struct cxd2880_ts_buf_info { + u8 read_ready:1; + u8 almost_full:1; + u8 almost_empty:1; + u8 overflow:1; + u8 underflow:1; + u16 pkt_num; +}; + +struct cxd2880_pid_config { + u8 is_enable; + u16 pid; +}; + +struct cxd2880_pid_filter_config { + u8 is_negative; + struct cxd2880_pid_config pid_config[CXD2880_MAX_FILTER_SIZE]; +}; + +struct cxd2880_dvb_spi { + struct dvb_frontend dvb_fe; + struct dvb_adapter adapter; + struct dvb_demux demux; + struct dmxdev dmxdev; + struct dmx_frontend dmx_fe; + struct task_struct *cxd2880_ts_read_thread; + struct spi_device *spi; + struct mutex spi_mutex; /* For SPI access exclusive control */ + int feed_count; + int all_pid_feed_count; + struct regulator *vcc_supply; + u8 *ts_buf; + struct cxd2880_pid_filter_config filter_config; +}; + +DVB_DEFINE_MOD_OPT_ADAPTER_NR(adapter_nr); + +static int cxd2880_write_spi(struct spi_device *spi, u8 *data, u32 size) +{ + struct spi_message msg; + struct spi_transfer tx = {}; + + if (!spi || !data) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + tx.tx_buf = data; + tx.len = size; + + spi_message_init(&msg); + spi_message_add_tail(&tx, &msg); + + return spi_sync(spi, &msg); +} + +static int cxd2880_write_reg(struct spi_device *spi, + u8 sub_address, const u8 *data, u32 size) +{ + u8 send_data[BURST_WRITE_MAX + 4]; + const u8 *write_data_top = NULL; + int ret = 0; + + if (!spi || !data) { + pr_err("invalid arg\n"); + return -EINVAL; + } + if (size > BURST_WRITE_MAX || size > U8_MAX) { + pr_err("data size > WRITE_MAX\n"); + return -EINVAL; + } + + if (sub_address + size > 0x100) { + pr_err("out of range\n"); + return -EINVAL; + } + + send_data[0] = 0x0e; + write_data_top = data; + + send_data[1] = sub_address; + send_data[2] = (u8)size; + + memcpy(&send_data[3], write_data_top, send_data[2]); + + ret = cxd2880_write_spi(spi, send_data, send_data[2] + 3); + if (ret) + pr_err("write spi failed %d\n", ret); + + return ret; +} + +static int cxd2880_spi_read_ts(struct spi_device *spi, + u8 *read_data, + u32 packet_num) +{ + int ret; + u8 data[3]; + struct spi_message message; + struct spi_transfer transfer[2] = {}; + + if (!spi || !read_data || !packet_num) { + pr_err("invalid arg\n"); + return -EINVAL; + } + if (packet_num > 0xffff) { + pr_err("packet num > 0xffff\n"); + return -EINVAL; + } + + data[0] = 0x10; + data[1] = packet_num >> 8; + data[2] = packet_num; + + spi_message_init(&message); + + transfer[0].len = 3; + transfer[0].tx_buf = data; + spi_message_add_tail(&transfer[0], &message); + transfer[1].len = packet_num * 188; + transfer[1].rx_buf = read_data; + spi_message_add_tail(&transfer[1], &message); + + ret = spi_sync(spi, &message); + if (ret) + pr_err("spi_sync failed\n"); + + return ret; +} + +static int cxd2880_spi_read_ts_buffer_info(struct spi_device *spi, + struct cxd2880_ts_buf_info *info) +{ + u8 send_data = 0x20; + u8 recv_data[2]; + int ret; + + if (!spi || !info) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + ret = spi_write_then_read(spi, &send_data, 1, + recv_data, sizeof(recv_data)); + if (ret) + pr_err("spi_write_then_read failed\n"); + + info->read_ready = (recv_data[0] & 0x80) ? 1 : 0; + info->almost_full = (recv_data[0] & 0x40) ? 1 : 0; + info->almost_empty = (recv_data[0] & 0x20) ? 1 : 0; + info->overflow = (recv_data[0] & 0x10) ? 1 : 0; + info->underflow = (recv_data[0] & 0x08) ? 1 : 0; + info->pkt_num = ((recv_data[0] & 0x07) << 8) | recv_data[1]; + + return ret; +} + +static int cxd2880_spi_clear_ts_buffer(struct spi_device *spi) +{ + u8 data = 0x03; + int ret; + + ret = cxd2880_write_spi(spi, &data, 1); + + if (ret) + pr_err("write spi failed\n"); + + return ret; +} + +static int cxd2880_set_pid_filter(struct spi_device *spi, + struct cxd2880_pid_filter_config *cfg) +{ + u8 data[65]; + int i; + u16 pid = 0; + int ret; + + if (!spi) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + data[0] = 0x00; + ret = cxd2880_write_reg(spi, 0x00, &data[0], 1); + if (ret) + return ret; + if (!cfg) { + data[0] = 0x02; + ret = cxd2880_write_reg(spi, 0x50, &data[0], 1); + } else { + data[0] = cfg->is_negative ? 0x01 : 0x00; + + for (i = 0; i < CXD2880_MAX_FILTER_SIZE; i++) { + pid = cfg->pid_config[i].pid; + if (cfg->pid_config[i].is_enable) { + data[1 + (i * 2)] = (pid >> 8) | 0x20; + data[2 + (i * 2)] = pid & 0xff; + } else { + data[1 + (i * 2)] = 0x00; + data[2 + (i * 2)] = 0x00; + } + } + ret = cxd2880_write_reg(spi, 0x50, data, 65); + } + + return ret; +} + +static int cxd2880_update_pid_filter(struct cxd2880_dvb_spi *dvb_spi, + struct cxd2880_pid_filter_config *cfg, + bool is_all_pid_filter) +{ + int ret; + + if (!dvb_spi || !cfg) { + pr_err("invalid arg.\n"); + return -EINVAL; + } + + mutex_lock(&dvb_spi->spi_mutex); + if (is_all_pid_filter) { + struct cxd2880_pid_filter_config tmpcfg; + + memset(&tmpcfg, 0, sizeof(tmpcfg)); + tmpcfg.is_negative = 1; + tmpcfg.pid_config[0].is_enable = 1; + tmpcfg.pid_config[0].pid = 0x1fff; + + ret = cxd2880_set_pid_filter(dvb_spi->spi, &tmpcfg); + } else { + ret = cxd2880_set_pid_filter(dvb_spi->spi, cfg); + } + mutex_unlock(&dvb_spi->spi_mutex); + + if (ret) + pr_err("set_pid_filter failed\n"); + + return ret; +} + +static int cxd2880_ts_read(void *arg) +{ + struct cxd2880_dvb_spi *dvb_spi = NULL; + struct cxd2880_ts_buf_info info; + ktime_t start; + u32 i; + int ret; + + dvb_spi = arg; + if (!dvb_spi) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + ret = cxd2880_spi_clear_ts_buffer(dvb_spi->spi); + if (ret) { + pr_err("set_clear_ts_buffer failed\n"); + return ret; + } + + start = ktime_get(); + while (!kthread_should_stop()) { + ret = cxd2880_spi_read_ts_buffer_info(dvb_spi->spi, + &info); + if (ret) { + pr_err("spi_read_ts_buffer_info error\n"); + return ret; + } + + if (info.pkt_num > MAX_TRANS_PKT) { + for (i = 0; i < info.pkt_num / MAX_TRANS_PKT; i++) { + cxd2880_spi_read_ts(dvb_spi->spi, + dvb_spi->ts_buf, + MAX_TRANS_PKT); + dvb_dmx_swfilter(&dvb_spi->demux, + dvb_spi->ts_buf, + MAX_TRANS_PKT * 188); + } + start = ktime_get(); + } else if ((info.pkt_num > 0) && + (ktime_to_ms(ktime_sub(ktime_get(), start)) >= 500)) { + cxd2880_spi_read_ts(dvb_spi->spi, + dvb_spi->ts_buf, + info.pkt_num); + dvb_dmx_swfilter(&dvb_spi->demux, + dvb_spi->ts_buf, + info.pkt_num * 188); + start = ktime_get(); + } else { + usleep_range(10000, 11000); + } + } + + return 0; +} + +static int cxd2880_start_feed(struct dvb_demux_feed *feed) +{ + int ret = 0; + int i = 0; + struct dvb_demux *demux = NULL; + struct cxd2880_dvb_spi *dvb_spi = NULL; + + if (!feed) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + demux = feed->demux; + if (!demux) { + pr_err("feed->demux is NULL\n"); + return -EINVAL; + } + dvb_spi = demux->priv; + + if (dvb_spi->feed_count == CXD2880_MAX_FILTER_SIZE) { + pr_err("Exceeded maximum PID count (32)."); + pr_err("Selected PID cannot be enabled.\n"); + return -EINVAL; + } + + if (feed->pid == 0x2000) { + if (dvb_spi->all_pid_feed_count == 0) { + ret = cxd2880_update_pid_filter(dvb_spi, + &dvb_spi->filter_config, + true); + if (ret) { + pr_err("update pid filter failed\n"); + return ret; + } + } + dvb_spi->all_pid_feed_count++; + + pr_debug("all PID feed (count = %d)\n", + dvb_spi->all_pid_feed_count); + } else { + struct cxd2880_pid_filter_config cfgtmp; + + cfgtmp = dvb_spi->filter_config; + + for (i = 0; i < CXD2880_MAX_FILTER_SIZE; i++) { + if (cfgtmp.pid_config[i].is_enable == 0) { + cfgtmp.pid_config[i].is_enable = 1; + cfgtmp.pid_config[i].pid = feed->pid; + pr_debug("store PID %d to #%d\n", + feed->pid, i); + break; + } + } + if (i == CXD2880_MAX_FILTER_SIZE) { + pr_err("PID filter is full.\n"); + return -EINVAL; + } + if (!dvb_spi->all_pid_feed_count) + ret = cxd2880_update_pid_filter(dvb_spi, + &cfgtmp, + false); + if (ret) + return ret; + + dvb_spi->filter_config = cfgtmp; + } + + if (dvb_spi->feed_count == 0) { + dvb_spi->ts_buf = + kmalloc(MAX_TRANS_PKT * 188, + GFP_KERNEL | GFP_DMA); + if (!dvb_spi->ts_buf) { + pr_err("ts buffer allocate failed\n"); + memset(&dvb_spi->filter_config, 0, + sizeof(dvb_spi->filter_config)); + dvb_spi->all_pid_feed_count = 0; + return -ENOMEM; + } + dvb_spi->cxd2880_ts_read_thread = kthread_run(cxd2880_ts_read, + dvb_spi, + "cxd2880_ts_read"); + if (IS_ERR(dvb_spi->cxd2880_ts_read_thread)) { + pr_err("kthread_run failed\n"); + kfree(dvb_spi->ts_buf); + dvb_spi->ts_buf = NULL; + memset(&dvb_spi->filter_config, 0, + sizeof(dvb_spi->filter_config)); + dvb_spi->all_pid_feed_count = 0; + return PTR_ERR(dvb_spi->cxd2880_ts_read_thread); + } + } + + dvb_spi->feed_count++; + + pr_debug("start feed (count %d)\n", dvb_spi->feed_count); + return 0; +} + +static int cxd2880_stop_feed(struct dvb_demux_feed *feed) +{ + int i = 0; + int ret; + struct dvb_demux *demux = NULL; + struct cxd2880_dvb_spi *dvb_spi = NULL; + + if (!feed) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + demux = feed->demux; + if (!demux) { + pr_err("feed->demux is NULL\n"); + return -EINVAL; + } + dvb_spi = demux->priv; + + if (!dvb_spi->feed_count) { + pr_err("no feed is started\n"); + return -EINVAL; + } + + if (feed->pid == 0x2000) { + /* + * Special PID case. + * Number of 0x2000 feed request was stored + * in dvb_spi->all_pid_feed_count. + */ + if (dvb_spi->all_pid_feed_count <= 0) { + pr_err("PID %d not found\n", feed->pid); + return -EINVAL; + } + dvb_spi->all_pid_feed_count--; + } else { + struct cxd2880_pid_filter_config cfgtmp; + + cfgtmp = dvb_spi->filter_config; + + for (i = 0; i < CXD2880_MAX_FILTER_SIZE; i++) { + if (feed->pid == cfgtmp.pid_config[i].pid && + cfgtmp.pid_config[i].is_enable != 0) { + cfgtmp.pid_config[i].is_enable = 0; + cfgtmp.pid_config[i].pid = 0; + pr_debug("removed PID %d from #%d\n", + feed->pid, i); + break; + } + } + dvb_spi->filter_config = cfgtmp; + + if (i == CXD2880_MAX_FILTER_SIZE) { + pr_err("PID %d not found\n", feed->pid); + return -EINVAL; + } + } + + ret = cxd2880_update_pid_filter(dvb_spi, + &dvb_spi->filter_config, + dvb_spi->all_pid_feed_count > 0); + dvb_spi->feed_count--; + + if (dvb_spi->feed_count == 0) { + int ret_stop = 0; + + ret_stop = kthread_stop(dvb_spi->cxd2880_ts_read_thread); + if (ret_stop) { + pr_err("kthread_stop failed. (%d)\n", ret_stop); + ret = ret_stop; + } + kfree(dvb_spi->ts_buf); + dvb_spi->ts_buf = NULL; + } + + pr_debug("stop feed ok.(count %d)\n", dvb_spi->feed_count); + + return ret; +} + +static const struct of_device_id cxd2880_spi_of_match[] = { + { .compatible = "sony,cxd2880" }, + { /* sentinel */ } +}; + +MODULE_DEVICE_TABLE(of, cxd2880_spi_of_match); + +static int +cxd2880_spi_probe(struct spi_device *spi) +{ + int ret; + struct cxd2880_dvb_spi *dvb_spi = NULL; + struct cxd2880_config config; + + if (!spi) { + pr_err("invalid arg\n"); + return -EINVAL; + } + + dvb_spi = kzalloc(sizeof(struct cxd2880_dvb_spi), GFP_KERNEL); + if (!dvb_spi) + return -ENOMEM; + + dvb_spi->vcc_supply = devm_regulator_get_optional(&spi->dev, "vcc"); + if (IS_ERR(dvb_spi->vcc_supply)) { + if (PTR_ERR(dvb_spi->vcc_supply) == -EPROBE_DEFER) { + ret = -EPROBE_DEFER; + goto fail_regulator; + } + dvb_spi->vcc_supply = NULL; + } else { + ret = regulator_enable(dvb_spi->vcc_supply); + if (ret) + goto fail_regulator; + } + + dvb_spi->spi = spi; + mutex_init(&dvb_spi->spi_mutex); + spi_set_drvdata(spi, dvb_spi); + config.spi = spi; + config.spi_mutex = &dvb_spi->spi_mutex; + + ret = dvb_register_adapter(&dvb_spi->adapter, + "CXD2880", + THIS_MODULE, + &spi->dev, + adapter_nr); + if (ret < 0) { + pr_err("dvb_register_adapter() failed\n"); + goto fail_adapter; + } + + if (!dvb_attach(cxd2880_attach, &dvb_spi->dvb_fe, &config)) { + pr_err("cxd2880_attach failed\n"); + ret = -ENODEV; + goto fail_attach; + } + + ret = dvb_register_frontend(&dvb_spi->adapter, + &dvb_spi->dvb_fe); + if (ret < 0) { + pr_err("dvb_register_frontend() failed\n"); + goto fail_frontend; + } + + dvb_spi->demux.dmx.capabilities = DMX_TS_FILTERING; + dvb_spi->demux.priv = dvb_spi; + dvb_spi->demux.filternum = CXD2880_MAX_FILTER_SIZE; + dvb_spi->demux.feednum = CXD2880_MAX_FILTER_SIZE; + dvb_spi->demux.start_feed = cxd2880_start_feed; + dvb_spi->demux.stop_feed = cxd2880_stop_feed; + + ret = dvb_dmx_init(&dvb_spi->demux); + if (ret < 0) { + pr_err("dvb_dmx_init() failed\n"); + goto fail_dmx; + } + + dvb_spi->dmxdev.filternum = CXD2880_MAX_FILTER_SIZE; + dvb_spi->dmxdev.demux = &dvb_spi->demux.dmx; + dvb_spi->dmxdev.capabilities = 0; + ret = dvb_dmxdev_init(&dvb_spi->dmxdev, + &dvb_spi->adapter); + if (ret < 0) { + pr_err("dvb_dmxdev_init() failed\n"); + goto fail_dmxdev; + } + + dvb_spi->dmx_fe.source = DMX_FRONTEND_0; + ret = dvb_spi->demux.dmx.add_frontend(&dvb_spi->demux.dmx, + &dvb_spi->dmx_fe); + if (ret < 0) { + pr_err("add_frontend() failed\n"); + goto fail_dmx_fe; + } + + ret = dvb_spi->demux.dmx.connect_frontend(&dvb_spi->demux.dmx, + &dvb_spi->dmx_fe); + if (ret < 0) { + pr_err("connect_frontend() failed\n"); + goto fail_fe_conn; + } + + pr_info("Sony CXD2880 has successfully attached.\n"); + + return 0; + +fail_fe_conn: + dvb_spi->demux.dmx.remove_frontend(&dvb_spi->demux.dmx, + &dvb_spi->dmx_fe); +fail_dmx_fe: + dvb_dmxdev_release(&dvb_spi->dmxdev); +fail_dmxdev: + dvb_dmx_release(&dvb_spi->demux); +fail_dmx: + dvb_unregister_frontend(&dvb_spi->dvb_fe); +fail_frontend: + dvb_frontend_detach(&dvb_spi->dvb_fe); +fail_attach: + dvb_unregister_adapter(&dvb_spi->adapter); +fail_adapter: + if (dvb_spi->vcc_supply) + regulator_disable(dvb_spi->vcc_supply); +fail_regulator: + kfree(dvb_spi); + return ret; +} + +static void +cxd2880_spi_remove(struct spi_device *spi) +{ + struct cxd2880_dvb_spi *dvb_spi = spi_get_drvdata(spi); + + dvb_spi->demux.dmx.remove_frontend(&dvb_spi->demux.dmx, + &dvb_spi->dmx_fe); + dvb_dmxdev_release(&dvb_spi->dmxdev); + dvb_dmx_release(&dvb_spi->demux); + dvb_unregister_frontend(&dvb_spi->dvb_fe); + dvb_frontend_detach(&dvb_spi->dvb_fe); + dvb_unregister_adapter(&dvb_spi->adapter); + + if (dvb_spi->vcc_supply) + regulator_disable(dvb_spi->vcc_supply); + + kfree(dvb_spi); + pr_info("cxd2880_spi remove ok.\n"); +} + +static const struct spi_device_id cxd2880_spi_id[] = { + { "cxd2880", 0 }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(spi, cxd2880_spi_id); + +static struct spi_driver cxd2880_spi_driver = { + .driver = { + .name = "cxd2880", + .of_match_table = cxd2880_spi_of_match, + }, + .id_table = cxd2880_spi_id, + .probe = cxd2880_spi_probe, + .remove = cxd2880_spi_remove, +}; +module_spi_driver(cxd2880_spi_driver); + +MODULE_DESCRIPTION("Sony CXD2880 DVB-T2/T tuner + demod driver SPI adapter"); +MODULE_AUTHOR("Sony Semiconductor Solutions Corporation"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/media/spi/gs1662.c b/drivers/media/spi/gs1662.c new file mode 100644 index 0000000000..75c21a93e6 --- /dev/null +++ b/drivers/media/spi/gs1662.c @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * GS1662 device registration. + * + * Copyright (C) 2015-2016 Nexvision + * Author: Charles-Antoine Couret <charles-antoine.couret@nexvision.fr> + */ + +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/spi/spi.h> +#include <linux/platform_device.h> +#include <linux/ctype.h> +#include <linux/err.h> +#include <linux/device.h> +#include <linux/module.h> + +#include <linux/videodev2.h> +#include <media/v4l2-common.h> +#include <media/v4l2-ctrls.h> +#include <media/v4l2-device.h> +#include <media/v4l2-subdev.h> +#include <media/v4l2-dv-timings.h> +#include <linux/v4l2-dv-timings.h> + +#define REG_STATUS 0x04 +#define REG_FORCE_FMT 0x06 +#define REG_LINES_PER_FRAME 0x12 +#define REG_WORDS_PER_LINE 0x13 +#define REG_WORDS_PER_ACT_LINE 0x14 +#define REG_ACT_LINES_PER_FRAME 0x15 + +#define MASK_H_LOCK 0x001 +#define MASK_V_LOCK 0x002 +#define MASK_STD_LOCK 0x004 +#define MASK_FORCE_STD 0x020 +#define MASK_STD_STATUS 0x3E0 + +#define GS_WIDTH_MIN 720 +#define GS_WIDTH_MAX 2048 +#define GS_HEIGHT_MIN 487 +#define GS_HEIGHT_MAX 1080 +#define GS_PIXELCLOCK_MIN 10519200 +#define GS_PIXELCLOCK_MAX 74250000 + +struct gs { + struct spi_device *pdev; + struct v4l2_subdev sd; + struct v4l2_dv_timings current_timings; + int enabled; +}; + +struct gs_reg_fmt { + u16 reg_value; + struct v4l2_dv_timings format; +}; + +struct gs_reg_fmt_custom { + u16 reg_value; + __u32 width; + __u32 height; + __u64 pixelclock; + __u32 interlaced; +}; + +static const struct spi_device_id gs_id[] = { + { "gs1662", 0 }, + { } +}; +MODULE_DEVICE_TABLE(spi, gs_id); + +static const struct v4l2_dv_timings fmt_cap[] = { + V4L2_DV_BT_SDI_720X487I60, + V4L2_DV_BT_CEA_720X576P50, + V4L2_DV_BT_CEA_1280X720P24, + V4L2_DV_BT_CEA_1280X720P25, + V4L2_DV_BT_CEA_1280X720P30, + V4L2_DV_BT_CEA_1280X720P50, + V4L2_DV_BT_CEA_1280X720P60, + V4L2_DV_BT_CEA_1920X1080P24, + V4L2_DV_BT_CEA_1920X1080P25, + V4L2_DV_BT_CEA_1920X1080P30, + V4L2_DV_BT_CEA_1920X1080I50, + V4L2_DV_BT_CEA_1920X1080I60, +}; + +static const struct gs_reg_fmt reg_fmt[] = { + { 0x00, V4L2_DV_BT_CEA_1280X720P60 }, + { 0x01, V4L2_DV_BT_CEA_1280X720P60 }, + { 0x02, V4L2_DV_BT_CEA_1280X720P30 }, + { 0x03, V4L2_DV_BT_CEA_1280X720P30 }, + { 0x04, V4L2_DV_BT_CEA_1280X720P50 }, + { 0x05, V4L2_DV_BT_CEA_1280X720P50 }, + { 0x06, V4L2_DV_BT_CEA_1280X720P25 }, + { 0x07, V4L2_DV_BT_CEA_1280X720P25 }, + { 0x08, V4L2_DV_BT_CEA_1280X720P24 }, + { 0x09, V4L2_DV_BT_CEA_1280X720P24 }, + { 0x0A, V4L2_DV_BT_CEA_1920X1080I60 }, + { 0x0B, V4L2_DV_BT_CEA_1920X1080P30 }, + + /* Default value: keep this field before 0xC */ + { 0x14, V4L2_DV_BT_CEA_1920X1080I50 }, + { 0x0C, V4L2_DV_BT_CEA_1920X1080I50 }, + { 0x0D, V4L2_DV_BT_CEA_1920X1080P25 }, + { 0x0E, V4L2_DV_BT_CEA_1920X1080P25 }, + { 0x10, V4L2_DV_BT_CEA_1920X1080P24 }, + { 0x12, V4L2_DV_BT_CEA_1920X1080P24 }, + { 0x16, V4L2_DV_BT_SDI_720X487I60 }, + { 0x19, V4L2_DV_BT_SDI_720X487I60 }, + { 0x18, V4L2_DV_BT_CEA_720X576P50 }, + { 0x1A, V4L2_DV_BT_CEA_720X576P50 }, + + /* Implement following timings before enable it. + * Because of we don't have access to these theoretical timings yet. + * Workaround: use functions to get and set registers for these formats. + */ +#if 0 + { 0x0F, V4L2_DV_BT_XXX_1920X1080I25 }, /* SMPTE 274M */ + { 0x11, V4L2_DV_BT_XXX_1920X1080I24 }, /* SMPTE 274M */ + { 0x13, V4L2_DV_BT_XXX_1920X1080I25 }, /* SMPTE 274M */ + { 0x15, V4L2_DV_BT_XXX_1920X1035I60 }, /* SMPTE 260M */ + { 0x17, V4L2_DV_BT_SDI_720X507I60 }, /* SMPTE 125M */ + { 0x1B, V4L2_DV_BT_SDI_720X507I60 }, /* SMPTE 125M */ + { 0x1C, V4L2_DV_BT_XXX_2048X1080P25 }, /* SMPTE 428.1M */ +#endif +}; + +static const struct v4l2_dv_timings_cap gs_timings_cap = { + .type = V4L2_DV_BT_656_1120, + /* keep this initialization for compatibility with GCC < 4.4.6 */ + .reserved = { 0 }, + V4L2_INIT_BT_TIMINGS(GS_WIDTH_MIN, GS_WIDTH_MAX, GS_HEIGHT_MIN, + GS_HEIGHT_MAX, GS_PIXELCLOCK_MIN, + GS_PIXELCLOCK_MAX, + V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_SDI, + V4L2_DV_BT_CAP_PROGRESSIVE + | V4L2_DV_BT_CAP_INTERLACED) +}; + +static int gs_read_register(struct spi_device *spi, u16 addr, u16 *value) +{ + int ret; + u16 buf_addr = (0x8000 | (0x0FFF & addr)); + u16 buf_value = 0; + struct spi_message msg; + struct spi_transfer tx[] = { + { + .tx_buf = &buf_addr, + .len = 2, + .delay = { + .value = 1, + .unit = SPI_DELAY_UNIT_USECS + }, + }, { + .rx_buf = &buf_value, + .len = 2, + .delay = { + .value = 1, + .unit = SPI_DELAY_UNIT_USECS + }, + }, + }; + + spi_message_init(&msg); + spi_message_add_tail(&tx[0], &msg); + spi_message_add_tail(&tx[1], &msg); + ret = spi_sync(spi, &msg); + + *value = buf_value; + + return ret; +} + +static int gs_write_register(struct spi_device *spi, u16 addr, u16 value) +{ + int ret; + u16 buf_addr = addr; + u16 buf_value = value; + struct spi_message msg; + struct spi_transfer tx[] = { + { + .tx_buf = &buf_addr, + .len = 2, + .delay = { + .value = 1, + .unit = SPI_DELAY_UNIT_USECS + }, + }, { + .tx_buf = &buf_value, + .len = 2, + .delay = { + .value = 1, + .unit = SPI_DELAY_UNIT_USECS + }, + }, + }; + + spi_message_init(&msg); + spi_message_add_tail(&tx[0], &msg); + spi_message_add_tail(&tx[1], &msg); + ret = spi_sync(spi, &msg); + + return ret; +} + +#ifdef CONFIG_VIDEO_ADV_DEBUG +static int gs_g_register(struct v4l2_subdev *sd, + struct v4l2_dbg_register *reg) +{ + struct spi_device *spi = v4l2_get_subdevdata(sd); + u16 val; + int ret; + + ret = gs_read_register(spi, reg->reg & 0xFFFF, &val); + reg->val = val; + reg->size = 2; + return ret; +} + +static int gs_s_register(struct v4l2_subdev *sd, + const struct v4l2_dbg_register *reg) +{ + struct spi_device *spi = v4l2_get_subdevdata(sd); + + return gs_write_register(spi, reg->reg & 0xFFFF, reg->val & 0xFFFF); +} +#endif + +static int gs_status_format(u16 status, struct v4l2_dv_timings *timings) +{ + int std = (status & MASK_STD_STATUS) >> 5; + int i; + + for (i = 0; i < ARRAY_SIZE(reg_fmt); i++) { + if (reg_fmt[i].reg_value == std) { + *timings = reg_fmt[i].format; + return 0; + } + } + + return -ERANGE; +} + +static u16 get_register_timings(struct v4l2_dv_timings *timings) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(reg_fmt); i++) { + if (v4l2_match_dv_timings(timings, ®_fmt[i].format, 0, + false)) + return reg_fmt[i].reg_value | MASK_FORCE_STD; + } + + return 0x0; +} + +static inline struct gs *to_gs(struct v4l2_subdev *sd) +{ + return container_of(sd, struct gs, sd); +} + +static int gs_s_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct gs *gs = to_gs(sd); + int reg_value; + + reg_value = get_register_timings(timings); + if (reg_value == 0x0) + return -EINVAL; + + gs->current_timings = *timings; + return 0; +} + +static int gs_g_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct gs *gs = to_gs(sd); + + *timings = gs->current_timings; + return 0; +} + +static int gs_query_dv_timings(struct v4l2_subdev *sd, + struct v4l2_dv_timings *timings) +{ + struct gs *gs = to_gs(sd); + struct v4l2_dv_timings fmt; + u16 reg_value, i; + int ret; + + if (gs->enabled) + return -EBUSY; + + /* + * Check if the component detect a line, a frame or something else + * which looks like a video signal activity. + */ + for (i = 0; i < 4; i++) { + gs_read_register(gs->pdev, REG_LINES_PER_FRAME + i, ®_value); + if (reg_value) + break; + } + + /* If no register reports a video signal */ + if (i >= 4) + return -ENOLINK; + + gs_read_register(gs->pdev, REG_STATUS, ®_value); + if (!(reg_value & MASK_H_LOCK) || !(reg_value & MASK_V_LOCK)) + return -ENOLCK; + if (!(reg_value & MASK_STD_LOCK)) + return -ERANGE; + + ret = gs_status_format(reg_value, &fmt); + + if (ret < 0) + return ret; + + *timings = fmt; + return 0; +} + +static int gs_enum_dv_timings(struct v4l2_subdev *sd, + struct v4l2_enum_dv_timings *timings) +{ + if (timings->index >= ARRAY_SIZE(fmt_cap)) + return -EINVAL; + + if (timings->pad != 0) + return -EINVAL; + + timings->timings = fmt_cap[timings->index]; + return 0; +} + +static int gs_s_stream(struct v4l2_subdev *sd, int enable) +{ + struct gs *gs = to_gs(sd); + int reg_value; + + if (gs->enabled == enable) + return 0; + + gs->enabled = enable; + + if (enable) { + /* To force the specific format */ + reg_value = get_register_timings(&gs->current_timings); + return gs_write_register(gs->pdev, REG_FORCE_FMT, reg_value); + } + + /* To renable auto-detection mode */ + return gs_write_register(gs->pdev, REG_FORCE_FMT, 0x0); +} + +static int gs_g_input_status(struct v4l2_subdev *sd, u32 *status) +{ + struct gs *gs = to_gs(sd); + u16 reg_value, i; + int ret; + + /* + * Check if the component detect a line, a frame or something else + * which looks like a video signal activity. + */ + for (i = 0; i < 4; i++) { + ret = gs_read_register(gs->pdev, + REG_LINES_PER_FRAME + i, ®_value); + if (reg_value) + break; + if (ret) { + *status = V4L2_IN_ST_NO_POWER; + return ret; + } + } + + /* If no register reports a video signal */ + if (i >= 4) + *status |= V4L2_IN_ST_NO_SIGNAL; + + ret = gs_read_register(gs->pdev, REG_STATUS, ®_value); + if (!(reg_value & MASK_H_LOCK)) + *status |= V4L2_IN_ST_NO_H_LOCK; + if (!(reg_value & MASK_V_LOCK)) + *status |= V4L2_IN_ST_NO_V_LOCK; + if (!(reg_value & MASK_STD_LOCK)) + *status |= V4L2_IN_ST_NO_STD_LOCK; + + return ret; +} + +static int gs_dv_timings_cap(struct v4l2_subdev *sd, + struct v4l2_dv_timings_cap *cap) +{ + if (cap->pad != 0) + return -EINVAL; + + *cap = gs_timings_cap; + return 0; +} + +/* V4L2 core operation handlers */ +static const struct v4l2_subdev_core_ops gs_core_ops = { +#ifdef CONFIG_VIDEO_ADV_DEBUG + .g_register = gs_g_register, + .s_register = gs_s_register, +#endif +}; + +static const struct v4l2_subdev_video_ops gs_video_ops = { + .s_dv_timings = gs_s_dv_timings, + .g_dv_timings = gs_g_dv_timings, + .s_stream = gs_s_stream, + .g_input_status = gs_g_input_status, + .query_dv_timings = gs_query_dv_timings, +}; + +static const struct v4l2_subdev_pad_ops gs_pad_ops = { + .enum_dv_timings = gs_enum_dv_timings, + .dv_timings_cap = gs_dv_timings_cap, +}; + +/* V4L2 top level operation handlers */ +static const struct v4l2_subdev_ops gs_ops = { + .core = &gs_core_ops, + .video = &gs_video_ops, + .pad = &gs_pad_ops, +}; + +static int gs_probe(struct spi_device *spi) +{ + int ret; + struct gs *gs; + struct v4l2_subdev *sd; + + gs = devm_kzalloc(&spi->dev, sizeof(struct gs), GFP_KERNEL); + if (!gs) + return -ENOMEM; + + gs->pdev = spi; + sd = &gs->sd; + + spi->mode = SPI_MODE_0; + spi->irq = -1; + spi->max_speed_hz = 10000000; + spi->bits_per_word = 16; + ret = spi_setup(spi); + v4l2_spi_subdev_init(sd, spi, &gs_ops); + + gs->current_timings = reg_fmt[0].format; + gs->enabled = 0; + + /* Set H_CONFIG to SMPTE timings */ + gs_write_register(spi, 0x0, 0x300); + + return ret; +} + +static void gs_remove(struct spi_device *spi) +{ + struct v4l2_subdev *sd = spi_get_drvdata(spi); + + v4l2_device_unregister_subdev(sd); +} + +static struct spi_driver gs_driver = { + .driver = { + .name = "gs1662", + }, + + .probe = gs_probe, + .remove = gs_remove, + .id_table = gs_id, +}; + +module_spi_driver(gs_driver); + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Charles-Antoine Couret <charles-antoine.couret@nexvision.fr>"); +MODULE_DESCRIPTION("Gennum GS1662 HD/SD-SDI Serializer driver"); |