f90cf6079b
Digital TV devices consist of several independent hardware components which are controlled by different drivers. Each media device is controlled by a group of cooperating drivers with the bridge driver as the main driver. This patch adds a bridge driver for the Virtual Digital TV driver [vidtv]. The bridge driver binds to the other drivers, that is, vidtv_tuner and vidtv_demod and implements the digital demux logic, providing userspace with a MPEG Transport Stream. The MPEG related code is split in the following way: - vidtv_ts: code to work with MPEG TS packets, such as TS headers, adaptation fields, PCR packets and NULL packets. - vidtv_psi: this is the PSI generator. PSI packets contain general information about a MPEG Transport Stream. A PSI generator is needed so userspace apps can retrieve information about the Transport Stream and eventually tune into a (dummy) channel. Because the generator is implemented in a separate file, it can be reused elsewhere in the media subsystem. Currently vidtv supports working with 3 PSI tables: PAT, PMT and SDT. - vidtv_pes: implements the PES logic to convert encoder data into MPEG TS packets. These can then be fed into a TS multiplexer and eventually into userspace. - vidtv_s302m: implements a S302M encoder to make it possible to insert PCM audio data in the generated MPEG Transport Stream. This shall enable passing an audio signal into userspace so it can be decoded and played by media software. - vidtv_channels: Implements a 'channel' abstraction When vidtv boots, it will create some hardcoded channels: Their services will be concatenated to populate the SDT. Their programs will be concatenated to populate the PAT For each program in the PAT, a PMT section will be created The PMT section for a channel will be assigned its streams. Every stream will have its corresponding encoder polled to produce TS packets These packets may be interleaved by the mux and then delivered to the bridge - vidtv_mux - Implements a MPEG TS mux, loosely based on the ffmpeg implementation The multiplexer is responsible for polling encoders, interleaving packets, padding the resulting stream with NULL packets if necessary and then delivering the resulting TS packets to the bridge driver so it can feed the demux. Signed-off-by: Daniel W. S. Almeida <dwlsalmeida@gmail.com> Signed-off-by: Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
547 lines
13 KiB
C
547 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* The Virtual DTV test driver serves as a reference DVB driver and helps
|
|
* validate the existing APIs in the media subsystem. It can also aid
|
|
* developers working on userspace applications.
|
|
*
|
|
* When this module is loaded, it will attempt to modprobe 'dvb_vidtv_tuner' and 'dvb_vidtv_demod'.
|
|
*
|
|
* Copyright (C) 2020 Daniel W. S. Almeida
|
|
*/
|
|
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ":%s, %d: " fmt, __func__, __LINE__
|
|
|
|
#include <linux/moduleparam.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/printk.h>
|
|
#include <linux/time.h>
|
|
#include <linux/types.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
#include "vidtv_bridge.h"
|
|
#include "vidtv_demod.h"
|
|
#include "vidtv_tuner.h"
|
|
#include "vidtv_ts.h"
|
|
#include "vidtv_mux.h"
|
|
#include "vidtv_common.h"
|
|
|
|
//#define MUX_BUF_MAX_SZ
|
|
//#define MUX_BUF_MIN_SZ
|
|
#define TUNER_DEFAULT_ADDR 0x68
|
|
#define DEMOD_DEFAULT_ADDR 0x60
|
|
|
|
static unsigned int drop_tslock_prob_on_low_snr;
|
|
module_param(drop_tslock_prob_on_low_snr, uint, 0);
|
|
MODULE_PARM_DESC(drop_tslock_prob_on_low_snr,
|
|
"Probability of losing the TS lock if the signal quality is bad");
|
|
|
|
static unsigned int recover_tslock_prob_on_good_snr;
|
|
module_param(recover_tslock_prob_on_good_snr, uint, 0);
|
|
MODULE_PARM_DESC(recover_tslock_prob_on_good_snr,
|
|
"Probability recovering the TS lock when the signal improves");
|
|
|
|
static unsigned int mock_power_up_delay_msec;
|
|
module_param(mock_power_up_delay_msec, uint, 0);
|
|
MODULE_PARM_DESC(mock_power_up_delay_msec, "Simulate a power up delay");
|
|
|
|
static unsigned int mock_tune_delay_msec;
|
|
module_param(mock_tune_delay_msec, uint, 0);
|
|
MODULE_PARM_DESC(mock_tune_delay_msec, "Simulate a tune delay");
|
|
|
|
static unsigned int vidtv_valid_dvb_t_freqs[NUM_VALID_TUNER_FREQS];
|
|
module_param_array(vidtv_valid_dvb_t_freqs, uint, NULL, 0);
|
|
MODULE_PARM_DESC(vidtv_valid_dvb_t_freqs,
|
|
"Valid DVB-T frequencies to simulate");
|
|
|
|
static unsigned int vidtv_valid_dvb_c_freqs[NUM_VALID_TUNER_FREQS];
|
|
module_param_array(vidtv_valid_dvb_c_freqs, uint, NULL, 0);
|
|
MODULE_PARM_DESC(vidtv_valid_dvb_c_freqs,
|
|
"Valid DVB-C frequencies to simulate");
|
|
|
|
static unsigned int vidtv_valid_dvb_s_freqs[NUM_VALID_TUNER_FREQS];
|
|
module_param_array(vidtv_valid_dvb_s_freqs, uint, NULL, 0);
|
|
MODULE_PARM_DESC(vidtv_valid_dvb_s_freqs,
|
|
"Valid DVB-C frequencies to simulate");
|
|
|
|
static unsigned int max_frequency_shift_hz;
|
|
module_param(max_frequency_shift_hz, uint, 0);
|
|
MODULE_PARM_DESC(max_frequency_shift_hz,
|
|
"Maximum shift in HZ allowed when tuning in a channel");
|
|
|
|
DVB_DEFINE_MOD_OPT_ADAPTER_NR(adapter_nums);
|
|
|
|
/*
|
|
* Influences the signal acquisition time. See ISO/IEC 13818-1 : 2000. p. 113.
|
|
*/
|
|
static unsigned int si_period_msec = 40;
|
|
module_param(si_period_msec, uint, 0);
|
|
MODULE_PARM_DESC(si_period_msec, "How often to send SI packets. Default: 40ms");
|
|
|
|
static unsigned int pcr_period_msec = 40;
|
|
module_param(pcr_period_msec, uint, 0);
|
|
MODULE_PARM_DESC(pcr_period_msec, "How often to send PCR packets. Default: 40ms");
|
|
|
|
static unsigned int mux_rate_kbytes_sec = 4096;
|
|
module_param(mux_rate_kbytes_sec, uint, 0);
|
|
MODULE_PARM_DESC(mux_rate_kbytes_sec, "Mux rate: will pad stream if below");
|
|
|
|
static unsigned int pcr_pid = 0x200;
|
|
module_param(pcr_pid, uint, 0);
|
|
MODULE_PARM_DESC(pcr_pid, "PCR PID for all channels: defaults to 0x200");
|
|
|
|
static unsigned int mux_buf_sz_pkts;
|
|
module_param(mux_buf_sz_pkts, uint, 0);
|
|
MODULE_PARM_DESC(mux_buf_sz_pkts, "Size for the internal mux buffer in multiples of 188 bytes");
|
|
|
|
static u32 vidtv_bridge_mux_buf_sz_for_mux_rate(void)
|
|
{
|
|
u64 max_elapsed_time_msecs = VIDTV_MAX_SLEEP_USECS / 1000;
|
|
u32 nbytes_expected;
|
|
u32 mux_buf_sz = mux_buf_sz_pkts * TS_PACKET_LEN;
|
|
u32 slack;
|
|
|
|
nbytes_expected = div64_u64(mux_rate_kbytes_sec * 1000, MSEC_PER_SEC);
|
|
nbytes_expected *= max_elapsed_time_msecs;
|
|
|
|
mux_buf_sz = roundup(nbytes_expected, TS_PACKET_LEN);
|
|
slack = mux_buf_sz / 10;
|
|
|
|
//if (mux_buf_sz < MUX_BUF_MIN_SZ)
|
|
// mux_buf_sz = MUX_BUF_MIN_SZ;
|
|
|
|
//if (mux_buf_sz > MUX_BUF_MAX_SZ)
|
|
// mux_buf_sz = MUX_BUF_MAX_SZ;
|
|
|
|
return mux_buf_sz + slack;
|
|
}
|
|
|
|
static bool vidtv_bridge_check_demod_lock(struct vidtv_dvb *dvb, u32 n)
|
|
{
|
|
enum fe_status status;
|
|
|
|
dvb->fe[n]->ops.read_status(dvb->fe[n], &status);
|
|
|
|
return status == (FE_HAS_SIGNAL |
|
|
FE_HAS_CARRIER |
|
|
FE_HAS_VITERBI |
|
|
FE_HAS_SYNC |
|
|
FE_HAS_LOCK);
|
|
}
|
|
|
|
static void
|
|
vidtv_bridge_on_new_pkts_avail(void *priv, u8 *buf, u32 npkts)
|
|
{
|
|
/*
|
|
* called on a separate thread by the mux when new packets become
|
|
* available
|
|
*/
|
|
struct vidtv_dvb *dvb = (struct vidtv_dvb *)priv;
|
|
|
|
/* drop packets if we lose the lock */
|
|
if (vidtv_bridge_check_demod_lock(dvb, 0))
|
|
dvb_dmx_swfilter_packets(&dvb->demux, buf, npkts);
|
|
}
|
|
|
|
static int vidtv_start_streaming(struct vidtv_dvb *dvb)
|
|
{
|
|
struct vidtv_mux_init_args mux_args = {0};
|
|
u32 mux_buf_sz;
|
|
|
|
if (dvb->streaming) {
|
|
pr_warn_ratelimited("Already streaming. Skipping.\n");
|
|
return 0;
|
|
}
|
|
|
|
mux_buf_sz = (mux_buf_sz_pkts) ? mux_buf_sz_pkts : vidtv_bridge_mux_buf_sz_for_mux_rate();
|
|
|
|
mux_args.mux_rate_kbytes_sec = mux_rate_kbytes_sec;
|
|
mux_args.on_new_packets_available_cb = vidtv_bridge_on_new_pkts_avail;
|
|
mux_args.mux_buf_sz = mux_buf_sz;
|
|
mux_args.pcr_period_usecs = pcr_period_msec * 1000;
|
|
mux_args.si_period_usecs = si_period_msec * 1000;
|
|
mux_args.pcr_pid = pcr_pid;
|
|
mux_args.transport_stream_id = VIDTV_DEFAULT_TS_ID;
|
|
mux_args.priv = dvb;
|
|
|
|
dvb->streaming = true;
|
|
dvb->mux = vidtv_mux_init(mux_args);
|
|
vidtv_mux_start_thread(dvb->mux);
|
|
|
|
pr_info_ratelimited("Started streaming\n");
|
|
return 0;
|
|
}
|
|
|
|
static int vidtv_stop_streaming(struct vidtv_dvb *dvb)
|
|
{
|
|
dvb->streaming = false;
|
|
vidtv_mux_stop_thread(dvb->mux);
|
|
vidtv_mux_destroy(dvb->mux);
|
|
dvb->mux = NULL;
|
|
|
|
pr_info_ratelimited("Stopped streaming\n");
|
|
return 0;
|
|
}
|
|
|
|
static int vidtv_start_feed(struct dvb_demux_feed *feed)
|
|
{
|
|
struct dvb_demux *demux = feed->demux;
|
|
struct vidtv_dvb *dvb = demux->priv;
|
|
int rc;
|
|
int ret;
|
|
|
|
if (!demux->dmx.frontend)
|
|
return -EINVAL;
|
|
|
|
mutex_lock(&dvb->feed_lock);
|
|
|
|
dvb->nfeeds++;
|
|
rc = dvb->nfeeds;
|
|
|
|
if (dvb->nfeeds == 1) {
|
|
ret = vidtv_start_streaming(dvb);
|
|
if (ret < 0)
|
|
rc = ret;
|
|
}
|
|
|
|
mutex_unlock(&dvb->feed_lock);
|
|
return rc;
|
|
}
|
|
|
|
static int vidtv_stop_feed(struct dvb_demux_feed *feed)
|
|
{
|
|
struct dvb_demux *demux = feed->demux;
|
|
struct vidtv_dvb *dvb = demux->priv;
|
|
int err = 0;
|
|
|
|
mutex_lock(&dvb->feed_lock);
|
|
dvb->nfeeds--;
|
|
|
|
if (!dvb->nfeeds)
|
|
err = vidtv_stop_streaming(dvb);
|
|
|
|
mutex_unlock(&dvb->feed_lock);
|
|
return err;
|
|
}
|
|
|
|
static struct dvb_frontend *vidtv_get_frontend_ptr(struct i2c_client *c)
|
|
{
|
|
/* the demod will set this when its probe function runs */
|
|
struct vidtv_demod_state *state = i2c_get_clientdata(c);
|
|
|
|
return &state->frontend;
|
|
}
|
|
|
|
static int vidtv_master_xfer(struct i2c_adapter *i2c_adap,
|
|
struct i2c_msg msgs[],
|
|
int num)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static u32 vidtv_i2c_func(struct i2c_adapter *adapter)
|
|
{
|
|
return I2C_FUNC_I2C;
|
|
}
|
|
|
|
static const struct i2c_algorithm vidtv_i2c_algorithm = {
|
|
.master_xfer = vidtv_master_xfer,
|
|
.functionality = vidtv_i2c_func,
|
|
};
|
|
|
|
static int vidtv_bridge_i2c_register_adap(struct vidtv_dvb *dvb)
|
|
{
|
|
struct i2c_adapter *i2c_adapter = &dvb->i2c_adapter;
|
|
|
|
strscpy(i2c_adapter->name, "vidtv_i2c", sizeof(i2c_adapter->name));
|
|
i2c_adapter->owner = THIS_MODULE;
|
|
i2c_adapter->algo = &vidtv_i2c_algorithm;
|
|
i2c_adapter->algo_data = NULL;
|
|
i2c_adapter->timeout = 500;
|
|
i2c_adapter->retries = 3;
|
|
i2c_adapter->dev.parent = &dvb->pdev->dev;
|
|
|
|
i2c_set_adapdata(i2c_adapter, dvb);
|
|
return i2c_add_adapter(&dvb->i2c_adapter);
|
|
}
|
|
|
|
static int vidtv_bridge_register_adap(struct vidtv_dvb *dvb)
|
|
{
|
|
int ret = 0;
|
|
|
|
ret = dvb_register_adapter(&dvb->adapter,
|
|
KBUILD_MODNAME,
|
|
THIS_MODULE,
|
|
&dvb->i2c_adapter.dev,
|
|
adapter_nums);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int vidtv_bridge_dmx_init(struct vidtv_dvb *dvb)
|
|
{
|
|
dvb->demux.dmx.capabilities = DMX_TS_FILTERING |
|
|
DMX_SECTION_FILTERING;
|
|
|
|
dvb->demux.priv = dvb;
|
|
dvb->demux.filternum = 256;
|
|
dvb->demux.feednum = 256;
|
|
dvb->demux.start_feed = vidtv_start_feed;
|
|
dvb->demux.stop_feed = vidtv_stop_feed;
|
|
|
|
return dvb_dmx_init(&dvb->demux);
|
|
}
|
|
|
|
static int vidtv_bridge_dmxdev_init(struct vidtv_dvb *dvb)
|
|
{
|
|
dvb->dmx_dev.filternum = 256;
|
|
dvb->dmx_dev.demux = &dvb->demux.dmx;
|
|
dvb->dmx_dev.capabilities = 0;
|
|
|
|
return dvb_dmxdev_init(&dvb->dmx_dev, &dvb->adapter);
|
|
}
|
|
|
|
static int vidtv_bridge_probe_demod(struct vidtv_dvb *dvb, u32 n)
|
|
{
|
|
struct vidtv_demod_config cfg = {};
|
|
|
|
cfg.drop_tslock_prob_on_low_snr = drop_tslock_prob_on_low_snr;
|
|
cfg.recover_tslock_prob_on_good_snr = recover_tslock_prob_on_good_snr;
|
|
|
|
dvb->i2c_client_demod[n] = dvb_module_probe("dvb_vidtv_demod",
|
|
NULL,
|
|
&dvb->i2c_adapter,
|
|
DEMOD_DEFAULT_ADDR,
|
|
&cfg);
|
|
|
|
/* driver will not work anyways so bail out */
|
|
if (!dvb->i2c_client_demod[n])
|
|
return -ENODEV;
|
|
|
|
/* retrieve a ptr to the frontend state */
|
|
dvb->fe[n] = vidtv_get_frontend_ptr(dvb->i2c_client_demod[n]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vidtv_bridge_probe_tuner(struct vidtv_dvb *dvb, u32 n)
|
|
{
|
|
struct vidtv_tuner_config cfg = {};
|
|
|
|
cfg.fe = dvb->fe[n];
|
|
cfg.mock_power_up_delay_msec = mock_power_up_delay_msec;
|
|
cfg.mock_tune_delay_msec = mock_tune_delay_msec;
|
|
|
|
memcpy(cfg.vidtv_valid_dvb_t_freqs,
|
|
vidtv_valid_dvb_t_freqs,
|
|
sizeof(vidtv_valid_dvb_t_freqs));
|
|
|
|
memcpy(cfg.vidtv_valid_dvb_c_freqs,
|
|
vidtv_valid_dvb_c_freqs,
|
|
sizeof(vidtv_valid_dvb_c_freqs));
|
|
|
|
memcpy(cfg.vidtv_valid_dvb_s_freqs,
|
|
vidtv_valid_dvb_s_freqs,
|
|
sizeof(vidtv_valid_dvb_s_freqs));
|
|
|
|
cfg.max_frequency_shift_hz = max_frequency_shift_hz;
|
|
|
|
dvb->i2c_client_tuner[n] = dvb_module_probe("dvb_vidtv_tuner",
|
|
NULL,
|
|
&dvb->i2c_adapter,
|
|
TUNER_DEFAULT_ADDR,
|
|
&cfg);
|
|
|
|
return (dvb->i2c_client_tuner[n]) ? 0 : -ENODEV;
|
|
}
|
|
|
|
static int vidtv_bridge_dvb_init(struct vidtv_dvb *dvb)
|
|
{
|
|
int ret;
|
|
int i;
|
|
int j;
|
|
|
|
ret = vidtv_bridge_i2c_register_adap(dvb);
|
|
if (ret < 0)
|
|
goto fail_i2c;
|
|
|
|
ret = vidtv_bridge_register_adap(dvb);
|
|
if (ret < 0)
|
|
goto fail_adapter;
|
|
|
|
for (i = 0; i < NUM_FE; ++i) {
|
|
ret = vidtv_bridge_probe_demod(dvb, i);
|
|
if (ret < 0)
|
|
goto fail_demod_probe;
|
|
|
|
ret = vidtv_bridge_probe_tuner(dvb, i);
|
|
if (ret < 0)
|
|
goto fail_tuner_probe;
|
|
|
|
ret = dvb_register_frontend(&dvb->adapter, dvb->fe[i]);
|
|
if (ret < 0)
|
|
goto fail_fe;
|
|
}
|
|
|
|
ret = vidtv_bridge_dmx_init(dvb);
|
|
if (ret < 0)
|
|
goto fail_dmx;
|
|
|
|
ret = vidtv_bridge_dmxdev_init(dvb);
|
|
if (ret < 0)
|
|
goto fail_dmx_dev;
|
|
|
|
for (j = 0; j < NUM_FE; ++j) {
|
|
ret = dvb->demux.dmx.connect_frontend(&dvb->demux.dmx,
|
|
&dvb->dmx_fe[j]);
|
|
if (ret < 0)
|
|
goto fail_dmx_conn;
|
|
|
|
/*
|
|
* The source of the demux is a frontend connected
|
|
* to the demux.
|
|
*/
|
|
dvb->dmx_fe[j].source = DMX_FRONTEND_0;
|
|
}
|
|
|
|
return ret;
|
|
|
|
fail_dmx_conn:
|
|
for (j = j - 1; j >= 0; --j)
|
|
dvb->demux.dmx.remove_frontend(&dvb->demux.dmx,
|
|
&dvb->dmx_fe[j]);
|
|
fail_dmx_dev:
|
|
dvb_dmxdev_release(&dvb->dmx_dev);
|
|
fail_dmx:
|
|
dvb_dmx_release(&dvb->demux);
|
|
fail_fe:
|
|
for (j = i; j >= 0; --j)
|
|
dvb_unregister_frontend(dvb->fe[j]);
|
|
fail_tuner_probe:
|
|
for (j = i; j >= 0; --j)
|
|
if (dvb->i2c_client_tuner[j])
|
|
dvb_module_release(dvb->i2c_client_tuner[j]);
|
|
|
|
fail_demod_probe:
|
|
for (j = i; j >= 0; --j)
|
|
if (dvb->i2c_client_demod[j])
|
|
dvb_module_release(dvb->i2c_client_demod[j]);
|
|
|
|
fail_adapter:
|
|
dvb_unregister_adapter(&dvb->adapter);
|
|
|
|
fail_i2c:
|
|
i2c_del_adapter(&dvb->i2c_adapter);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int vidtv_bridge_probe(struct platform_device *pdev)
|
|
{
|
|
int ret;
|
|
struct vidtv_dvb *dvb;
|
|
|
|
dvb = kzalloc(sizeof(*dvb), GFP_KERNEL);
|
|
if (!dvb)
|
|
return -ENOMEM;
|
|
|
|
dvb->pdev = pdev;
|
|
|
|
ret = vidtv_bridge_dvb_init(dvb);
|
|
if (ret < 0)
|
|
goto err_dvb;
|
|
|
|
mutex_init(&dvb->feed_lock);
|
|
|
|
platform_set_drvdata(pdev, dvb);
|
|
|
|
pr_info("Successfully initialized vidtv!\n");
|
|
return ret;
|
|
|
|
err_dvb:
|
|
kfree(dvb);
|
|
return ret;
|
|
}
|
|
|
|
static int vidtv_bridge_remove(struct platform_device *pdev)
|
|
{
|
|
struct vidtv_dvb *dvb;
|
|
u32 i;
|
|
|
|
dvb = platform_get_drvdata(pdev);
|
|
|
|
mutex_destroy(&dvb->feed_lock);
|
|
|
|
for (i = 0; i < NUM_FE; ++i)
|
|
dvb->demux.dmx.remove_frontend(&dvb->demux.dmx,
|
|
&dvb->dmx_fe[i]);
|
|
|
|
dvb_dmxdev_release(&dvb->dmx_dev);
|
|
dvb_dmx_release(&dvb->demux);
|
|
|
|
for (i = 0; i < NUM_FE; ++i) {
|
|
dvb_unregister_frontend(dvb->fe[i]);
|
|
dvb_frontend_detach(dvb->fe[i]);
|
|
}
|
|
|
|
dvb_unregister_adapter(&dvb->adapter);
|
|
|
|
for (i = 0; i < NUM_FE; i++)
|
|
dvb_module_release(dvb->i2c_client_tuner[i]);
|
|
|
|
for (i = 0; i < NUM_FE ; i++)
|
|
dvb_module_release(dvb->i2c_client_demod[i]);
|
|
|
|
dvb_unregister_adapter(&dvb->adapter);
|
|
|
|
i2c_del_adapter(&dvb->i2c_adapter);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void vidtv_bridge_dev_release(struct device *dev)
|
|
{
|
|
}
|
|
|
|
static struct platform_device vidtv_bridge_dev = {
|
|
.name = "vidtv_bridge",
|
|
.dev.release = vidtv_bridge_dev_release,
|
|
};
|
|
|
|
static struct platform_driver vidtv_bridge_driver = {
|
|
.driver = {
|
|
.name = "vidtv_bridge",
|
|
.suppress_bind_attrs = true,
|
|
},
|
|
.probe = vidtv_bridge_probe,
|
|
.remove = vidtv_bridge_remove,
|
|
};
|
|
|
|
static void __exit vidtv_bridge_exit(void)
|
|
{
|
|
platform_driver_unregister(&vidtv_bridge_driver);
|
|
platform_device_unregister(&vidtv_bridge_dev);
|
|
}
|
|
|
|
static int __init vidtv_bridge_init(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = platform_device_register(&vidtv_bridge_dev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = platform_driver_register(&vidtv_bridge_driver);
|
|
if (ret)
|
|
platform_device_unregister(&vidtv_bridge_dev);
|
|
|
|
return ret;
|
|
}
|
|
|
|
module_init(vidtv_bridge_init);
|
|
module_exit(vidtv_bridge_exit);
|
|
|
|
MODULE_DESCRIPTION("Virtual Digital TV Test Driver");
|
|
MODULE_AUTHOR("Daniel W. S. Almeida");
|
|
MODULE_LICENSE("GPL");
|