new file mode 100644
@@ -0,0 +1,566 @@
+/*
+ * device driver for Telegent tlg2300 based TV cards
+ *
+ * Author :
+ * Kang Yong <kangyong@telegent.com>
+ * Zhang Xiaobing <xbzhang@telegent.com>
+ * Huang Shijie <zyziii@telegent.com> or <shijie8@gmail.com>
+ *
+ * (c) 2009 Telegent Systems
+ * (c) 2010 Telegent Systems
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+
+#include <linux/version.h>
+#include <linux/kernel.h>
+#include <linux/errno.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/module.h>
+#include <linux/kref.h>
+#include <linux/suspend.h>
+#include <linux/usb/quirks.h>
+#include <linux/ctype.h>
+#include <linux/string.h>
+#include <linux/types.h>
+#include <linux/firmware.h>
+#include <linux/smp_lock.h>
+
+#include "vendorcmds.h"
+#include "pd-common.h"
+
+#define VENDOR_ID 0x1B24
+#define PRODUCT_ID 0x4001
+static struct usb_device_id id_table[] = {
+ { USB_DEVICE_AND_INTERFACE_INFO(VENDOR_ID, PRODUCT_ID, 255, 1, 0) },
+ { USB_DEVICE_AND_INTERFACE_INFO(VENDOR_ID, PRODUCT_ID, 255, 1, 1) },
+ { },
+};
+MODULE_DEVICE_TABLE(usb, id_table);
+
+int debug_mode;
+module_param(debug_mode, int, 0644);
+MODULE_PARM_DESC(debug_mode, "0 = disable, 1 = enable, 2 = verbose");
+
+const char *firmware_name = "tlg2300_firmware.bin";
+struct usb_driver poseidon_driver;
+static LIST_HEAD(pd_device_list);
+
+/*
+ * send set request to USB firmware.
+ */
+s32 send_set_req(struct poseidon *pd, u8 cmdid, s32 param, s32 *cmd_status)
+{
+ s32 ret;
+ s8 data[32] = {};
+ u16 lower_16, upper_16;
+
+ if (pd->state & POSEIDON_STATE_DISCONNECT)
+ return -ENODEV;
+
+ mdelay(30);
+
+ if (param == 0) {
+ upper_16 = lower_16 = 0;
+ } else {
+ /* send 32 bit param as two 16 bit param,little endian */
+ lower_16 = (unsigned short)(param & 0xffff);
+ upper_16 = (unsigned short)((param >> 16) & 0xffff);
+ }
+ ret = usb_control_msg(pd->udev,
+ usb_rcvctrlpipe(pd->udev, 0),
+ REQ_SET_CMD | cmdid,
+ USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
+ lower_16,
+ upper_16,
+ &data,
+ sizeof(*cmd_status),
+ USB_CTRL_GET_TIMEOUT);
+
+ if (!ret) {
+ return -ENXIO;
+ } else {
+ /* 1st 4 bytes into cmd_status */
+ memcpy((char *)cmd_status, &(data[0]), sizeof(*cmd_status));
+ }
+ return 0;
+}
+
+/*
+ * send get request to Poseidon firmware.
+ */
+s32 send_get_req(struct poseidon *pd, u8 cmdid, s32 param,
+ void *buf, s32 *cmd_status, s32 datalen)
+{
+ s32 ret;
+ s8 data[128] = {};
+ u16 lower_16, upper_16;
+
+ if (pd->state & POSEIDON_STATE_DISCONNECT)
+ return -ENODEV;
+
+ mdelay(30);
+ if (param == 0) {
+ upper_16 = lower_16 = 0;
+ } else {
+ /*send 32 bit param as two 16 bit param, little endian */
+ lower_16 = (unsigned short)(param & 0xffff);
+ upper_16 = (unsigned short)((param >> 16) & 0xffff);
+ }
+ ret = usb_control_msg(pd->udev,
+ usb_rcvctrlpipe(pd->udev, 0),
+ REQ_GET_CMD | cmdid,
+ USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
+ lower_16,
+ upper_16,
+ &data,
+ (datalen + sizeof(*cmd_status)),
+ USB_CTRL_GET_TIMEOUT);
+
+ if (ret < 0) {
+ return -ENXIO;
+ } else {
+ /* 1st 4 bytes into cmd_status, remaining data into cmd_data */
+ memcpy((char *)cmd_status, &data[0], sizeof(*cmd_status));
+ memcpy((char *)buf, &data[sizeof(*cmd_status)], datalen);
+ }
+ return 0;
+}
+
+static int pm_notifier_block(struct notifier_block *nb,
+ unsigned long event, void *dummy)
+{
+ struct poseidon *pd = NULL;
+ struct list_head *node, *next;
+
+ switch (event) {
+ case PM_POST_HIBERNATION:
+ list_for_each_safe(node, next, &pd_device_list) {
+ struct usb_device *udev;
+ struct usb_interface *iface;
+ int rc = 0;
+
+ pd = container_of(node, struct poseidon, device_list);
+ udev = pd->udev;
+ iface = pd->interface;
+
+ /* It will cause the system to reload the firmware */
+ rc = usb_lock_device_for_reset(udev, iface);
+ if (rc >= 0) {
+ usb_reset_device(udev);
+ usb_unlock_device(udev);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ log("event :%ld\n", event);
+ return 0;
+}
+
+static struct notifier_block pm_notifer = {
+ .notifier_call = pm_notifier_block,
+};
+
+int set_tuner_mode(struct poseidon *pd, unsigned char mode)
+{
+ s32 ret, cmd_status;
+
+ if (pd->state & POSEIDON_STATE_DISCONNECT)
+ return -ENODEV;
+
+ ret = send_set_req(pd, TUNE_MODE_SELECT, mode, &cmd_status);
+ if (ret || cmd_status)
+ return -ENXIO;
+ return 0;
+}
+
+enum tlg__analog_audio_standard get_audio_std(s32 mode, s32 country_code)
+{
+ s32 nicam[] = {27, 32, 33, 34, 36, 44, 45, 46, 47, 48, 64,
+ 65, 86, 351, 352, 353, 354, 358, 372, 852, 972};
+ s32 btsc[] = {1, 52, 54, 55, 886};
+ s32 eiaj[] = {81};
+ s32 i;
+
+ if (mode == TLG_MODE_FM_RADIO) {
+ if (country_code == 1)
+ return TLG_TUNE_ASTD_FM_US;
+ else
+ return TLG_TUNE_ASTD_FM_EUR;
+ } else if (mode == TLG_MODE_ANALOG_TV_UNCOMP) {
+ for (i = 0; i < sizeof(nicam) / sizeof(s32); i++) {
+ if (country_code == nicam[i])
+ return TLG_TUNE_ASTD_NICAM;
+ }
+
+ for (i = 0; i < sizeof(btsc) / sizeof(s32); i++) {
+ if (country_code == btsc[i])
+ return TLG_TUNE_ASTD_BTSC;
+ }
+
+ for (i = 0; i < sizeof(eiaj) / sizeof(s32); i++) {
+ if (country_code == eiaj[i])
+ return TLG_TUNE_ASTD_EIAJ;
+ }
+
+ return TLG_TUNE_ASTD_A2;
+ } else {
+ return TLG_TUNE_ASTD_NONE;
+ }
+}
+
+void poseidon_delete(struct kref *kref)
+{
+ struct poseidon *pd = container_of(kref, struct poseidon, kref);
+
+ if (!pd)
+ return;
+ list_del_init(&pd->device_list);
+
+ pd_dvb_usb_device_cleanup(pd);
+ /* clean_audio_data(&pd->audio_data);*/
+
+ if (pd->udev) {
+ usb_put_dev(pd->udev);
+ pd->udev = NULL;
+ }
+ if (pd->interface) {
+ usb_put_intf(pd->interface);
+ pd->interface = NULL;
+ }
+ kfree(pd);
+ log();
+}
+
+static int firmware_download(struct usb_device *udev)
+{
+ int ret = 0, actual_length;
+ const struct firmware *fw = NULL;
+ void *fwbuf = NULL;
+ size_t fwlength = 0, offset;
+ size_t max_packet_size;
+
+ ret = request_firmware(&fw, firmware_name, &udev->dev);
+ if (ret) {
+ log("download err : %d", ret);
+ return ret;
+ }
+
+ fwlength = fw->size;
+
+ fwbuf = kzalloc(fwlength, GFP_KERNEL);
+ if (!fwbuf) {
+ ret = -ENOMEM;
+ goto out;
+ }
+ memcpy(fwbuf, fw->data, fwlength);
+
+ max_packet_size = udev->ep_out[0x1]->desc.wMaxPacketSize;
+ log("\t\t download size : %d", (int)max_packet_size);
+
+ for (offset = 0; offset < fwlength; offset += max_packet_size) {
+ actual_length = 0;
+ ret = usb_bulk_msg(udev,
+ usb_sndbulkpipe(udev, 0x01), /* ep 1 */
+ fwbuf + offset,
+ min(max_packet_size, fwlength - offset),
+ &actual_length,
+ HZ * 10);
+ if (ret)
+ break;
+ }
+ kfree(fwbuf);
+out:
+ release_firmware(fw);
+ return ret;
+}
+
+#ifdef CONFIG_PM
+/* one-to-one map : poseidon{} <----> usb_device{}'s port */
+static inline void set_map_flags(struct poseidon *pd, struct usb_device *udev)
+{
+ pd->portnum = udev->portnum;
+}
+
+static inline int get_autopm_ref(struct poseidon *pd)
+{
+ return pd->video_data.users + pd->vbi_data.users + pd->audio.users
+ + atomic_read(&pd->dvb_data.users) + pd->radio_data.users;
+}
+
+/* fixup something for poseidon */
+static inline struct poseidon *fixup(struct poseidon *pd)
+{
+ int count;
+
+ /* old udev and interface have gone, so put back reference . */
+ count = get_autopm_ref(pd);
+ log("count : %d, ref count : %d", count, get_pm_count(pd));
+ while (count--)
+ usb_autopm_put_interface(pd->interface);
+ /*usb_autopm_set_interface(pd->interface); */
+
+ usb_put_dev(pd->udev);
+ usb_put_intf(pd->interface);
+ log("event : %d\n", pd->msg.event);
+ return pd;
+}
+
+static struct poseidon *find_old_poseidon(struct usb_device *udev)
+{
+ struct poseidon *pd;
+
+ list_for_each_entry(pd, &pd_device_list, device_list) {
+ if (pd->portnum == udev->portnum && in_hibernation(pd))
+ return fixup(pd);
+ }
+ return NULL;
+}
+
+/* Is the card working now ? */
+static inline int is_working(struct poseidon *pd)
+{
+ return get_pm_count(pd) > 0;
+}
+
+static inline struct poseidon *get_pd(struct usb_interface *intf)
+{
+ return usb_get_intfdata(intf);
+}
+
+static int poseidon_suspend(struct usb_interface *intf, pm_message_t msg)
+{
+ struct poseidon *pd = get_pd(intf);
+
+ if (!pd)
+ return 0;
+ if (!is_working(pd)) {
+ if (get_pm_count(pd) <= 0 && !in_hibernation(pd)) {
+ pd->msg.event = PM_EVENT_AUTO_SUSPEND;
+ pd->pm_resume = NULL; /* a good guard */
+ printk(KERN_DEBUG "\n\t+ TLG2300 auto suspend +\n\n");
+ }
+ return 0;
+ }
+ pd->msg = msg; /* save it here */
+ logpm(pd);
+ return pd->pm_suspend ? pd->pm_suspend(pd) : 0;
+}
+
+static int poseidon_resume(struct usb_interface *intf)
+{
+ struct poseidon *pd = get_pd(intf);
+
+ if (!pd)
+ return 0;
+ printk(KERN_DEBUG "\n\t ++ TLG2300 resume ++\n\n");
+
+ if (!is_working(pd)) {
+ if (PM_EVENT_AUTO_SUSPEND == pd->msg.event)
+ pd->msg = PMSG_ON;
+ return 0;
+ }
+ if (in_hibernation(pd)) {
+ logpm(pd);
+ return 0;
+ }
+ logpm(pd);
+ return pd->pm_resume ? pd->pm_resume(pd) : 0;
+}
+
+static void hibernation_resume(struct work_struct *w)
+{
+ struct poseidon *pd = container_of(w, struct poseidon, pm_work);
+ int count;
+
+ pd->msg.event = 0; /* clear it here */
+ pd->state &= ~POSEIDON_STATE_DISCONNECT;
+
+ /* set the new interface's reference */
+ count = get_autopm_ref(pd);
+ while (count--)
+ usb_autopm_get_interface(pd->interface);
+
+ /* resume the context */
+ logpm(pd);
+ if (pd->pm_resume)
+ pd->pm_resume(pd);
+}
+#endif
+
+static bool check_firmware(struct usb_device *udev, int *down_firmware)
+{
+ void *buf;
+ int ret;
+ struct cmd_firmware_vers_s *cmd_firm;
+
+ buf = kzalloc(sizeof(*cmd_firm) + sizeof(u32), GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ ret = usb_control_msg(udev,
+ usb_rcvctrlpipe(udev, 0),
+ REQ_GET_CMD | GET_FW_ID,
+ USB_DIR_IN | USB_TYPE_VENDOR | USB_RECIP_DEVICE,
+ 0,
+ 0,
+ buf,
+ sizeof(*cmd_firm) + sizeof(u32),
+ USB_CTRL_GET_TIMEOUT);
+ kfree(buf);
+
+ if (ret < 0) {
+ *down_firmware = 1;
+ return firmware_download(udev);
+ }
+ return ret;
+}
+
+static int poseidon_probe(struct usb_interface *interface,
+ const struct usb_device_id *id)
+{
+ struct usb_device *udev = interface_to_usbdev(interface);
+ struct poseidon *pd = NULL;
+ int ret = 0;
+ int new_one = 0;
+
+ /* download firmware */
+ check_firmware(udev, &ret);
+ if (ret)
+ return 0;
+
+ /* Do I recovery from the hibernate ? */
+ pd = find_old_poseidon(udev);
+ if (!pd) {
+ pd = kzalloc(sizeof(*pd), GFP_KERNEL);
+ if (!pd)
+ return -ENOMEM;
+ kref_init(&pd->kref);
+ set_map_flags(pd, udev);
+ new_one = 1;
+ }
+
+ pd->udev = usb_get_dev(udev);
+ pd->interface = usb_get_intf(interface);
+ usb_set_intfdata(interface, pd);
+
+ if (new_one) {
+ struct device *dev = &interface->dev;
+
+ logpm(pd);
+ pd->country_code = 86;
+ mutex_init(&pd->lock);
+
+ /* register v4l2 device */
+ snprintf(pd->v4l2_dev.name, sizeof(pd->v4l2_dev.name), "%s %s",
+ dev->driver->name, dev_name(dev));
+ ret = v4l2_device_register(NULL, &pd->v4l2_dev);
+
+ /* register devices in directory /dev */
+ ret = pd_video_init(pd);
+ poseidon_audio_init(pd);
+ poseidon_fm_init(pd);
+ pd_dvb_usb_device_init(pd);
+
+ INIT_LIST_HEAD(&pd->device_list);
+ list_add_tail(&pd->device_list, &pd_device_list);
+ }
+
+ device_init_wakeup(&udev->dev, 1);
+#ifdef CONFIG_PM
+ pd->udev->autosuspend_disabled = 0;
+ pd->udev->autosuspend_delay = HZ * PM_SUSPEND_DELAY;
+
+ if (in_hibernation(pd)) {
+ INIT_WORK(&pd->pm_work, hibernation_resume);
+ schedule_work(&pd->pm_work);
+ }
+#endif
+ return 0;
+}
+
+static void poseidon_disconnect(struct usb_interface *interface)
+{
+ struct poseidon *pd = get_pd(interface);
+
+ if (!pd)
+ return;
+ logpm(pd);
+ if (in_hibernation(pd))
+ return;
+
+ mutex_lock(&pd->lock);
+ pd->state |= POSEIDON_STATE_DISCONNECT;
+ mutex_unlock(&pd->lock);
+
+ /* stop urb transferring */
+ stop_all_video_stream(pd);
+ dvb_stop_streaming(&pd->dvb_data);
+
+ /*unregister v4l2 device */
+ v4l2_device_unregister(&pd->v4l2_dev);
+
+ lock_kernel();
+ {
+ pd_dvb_usb_device_exit(pd);
+ poseidon_fm_exit(pd);
+
+ poseidon_audio_free(pd);
+ pd_video_exit(pd);
+ }
+ unlock_kernel();
+
+ usb_set_intfdata(interface, NULL);
+ kref_put(&pd->kref, poseidon_delete);
+}
+
+struct usb_driver poseidon_driver = {
+ .name = "poseidon",
+ .probe = poseidon_probe,
+ .disconnect = poseidon_disconnect,
+ .id_table = id_table,
+#ifdef CONFIG_PM
+ .suspend = poseidon_suspend,
+ .resume = poseidon_resume,
+#endif
+ .supports_autosuspend = 1,
+};
+
+static int __init poseidon_init(void)
+{
+ int ret;
+
+ ret = usb_register(&poseidon_driver);
+ if (ret)
+ return ret;
+ register_pm_notifier(&pm_notifer);
+ return ret;
+}
+
+static void __exit poseidon_exit(void)
+{
+ log();
+ unregister_pm_notifier(&pm_notifer);
+ usb_deregister(&poseidon_driver);
+}
+
+module_init(poseidon_init);
+module_exit(poseidon_exit);
+
+MODULE_AUTHOR("Telegent Systems");
+MODULE_DESCRIPTION("For tlg2300-based USB device ");
+MODULE_LICENSE("GPL");