1247 lines
29 KiB
C
1247 lines
29 KiB
C
/*
|
|
* H2W device detection driver.
|
|
*
|
|
* Copyright (C) 2008 Google, Inc.
|
|
* Copyright (C) 2008 HTC, Inc.
|
|
*
|
|
* Authors:
|
|
* Laurence Chen <Laurence_Chen@htc.com>
|
|
* Nick Pelly <npelly@google.com>
|
|
* Thomas Tsai <thomas_tsai@htc.com>
|
|
* Farmer Tseng <farmer_tseng@htc.com>
|
|
*
|
|
* 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; version 2 of the License.
|
|
*/
|
|
|
|
/* For detecting HTC 2 Wire devices, such as wired headset.
|
|
|
|
Logically, the H2W driver is always present, and H2W state (hi->state)
|
|
indicates what is currently plugged into the H2W interface.
|
|
|
|
When the headset is plugged in, CABLE_IN1 is pulled low. When the headset
|
|
button is pressed, CABLE_IN2 is pulled low. These two lines are shared with
|
|
the TX and RX (respectively) of UART3 - used for serial debugging.
|
|
|
|
This headset driver keeps the CPLD configured as UART3 for as long as
|
|
possible, so that we can do serial FIQ debugging even when the kernel is
|
|
locked and this driver no longer runs. So it only configures the CPLD to
|
|
GPIO while the headset is plugged in, and for 10ms during detection work.
|
|
|
|
Unfortunately we can't leave the CPLD as UART3 while a headset is plugged
|
|
in, UART3 is pullup on TX but the headset is pull-down, causing a 55 mA
|
|
drain on trout.
|
|
|
|
The headset detection work involves setting CPLD to GPIO, and then pulling
|
|
CABLE_IN1 high with a stronger pullup than usual. A H2W headset will still
|
|
pull this line low, whereas other attachments such as a serial console
|
|
would get pulled up by this stronger pullup.
|
|
|
|
Headset insertion/removal causes UEvent's to be sent, and
|
|
/sys/class/switch/h2w/state to be updated.
|
|
|
|
Button presses are interpreted as input event (KEY_MEDIA). Button presses
|
|
are ignored if the headset is plugged in, so the buttons on 11 pin -> 3.5mm
|
|
jack adapters do not work until a headset is plugged into the adapter. This
|
|
is to avoid serial RX traffic causing spurious button press events.
|
|
|
|
We tend to check the status of CABLE_IN1 a few more times than strictly
|
|
necessary during headset detection, to avoid spurious headset insertion
|
|
events caused by serial debugger TX traffic.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/sysdev.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/types.h>
|
|
#include <linux/input.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/err.h>
|
|
#include <linux/hrtimer.h>
|
|
#include <linux/switch.h>
|
|
#include <linux/input.h>
|
|
#include <linux/debugfs.h>
|
|
#include <asm/gpio.h>
|
|
#include <asm/atomic.h>
|
|
#include <mach/board.h>
|
|
#include <mach/vreg.h>
|
|
#include <asm/mach-types.h>
|
|
|
|
#include <mach/htc_headset.h>
|
|
|
|
#define H2WI(fmt, arg...) \
|
|
printk(KERN_INFO "[H2W] %s " fmt "\r\n", __func__, ## arg)
|
|
#define H2WE(fmt, arg...) \
|
|
printk(KERN_ERR "[H2W] %s " fmt "\r\n", __func__, ## arg)
|
|
|
|
#ifdef CONFIG_DEBUG_H2W
|
|
#define H2W_DBG(fmt, arg...) printk(KERN_INFO "[H2W] %s " fmt "\r\n", __func__, ## arg)
|
|
#else
|
|
#define H2W_DBG(fmt, arg...) do {} while (0)
|
|
#endif
|
|
|
|
static struct workqueue_struct *g_detection_work_queue;
|
|
static void detection_work(struct work_struct *work);
|
|
static DECLARE_WORK(g_detection_work, detection_work);
|
|
|
|
struct h2w_info {
|
|
struct switch_dev sdev;
|
|
struct input_dev *input;
|
|
struct mutex mutex_lock;
|
|
|
|
atomic_t btn_state;
|
|
int ignore_btn;
|
|
|
|
unsigned int irq;
|
|
unsigned int irq_btn;
|
|
|
|
int cable_in1;
|
|
int cable_in2;
|
|
int h2w_clk;
|
|
int h2w_data;
|
|
int debug_uart;
|
|
|
|
void (*config_cpld) (int);
|
|
void (*init_cpld) (void);
|
|
/* for h2w */
|
|
void (*set_dat)(int);
|
|
void (*set_clk)(int);
|
|
void (*set_dat_dir)(int);
|
|
void (*set_clk_dir)(int);
|
|
int (*get_dat)(void);
|
|
int (*get_clk)(void);
|
|
|
|
int htc_headset_flag;
|
|
|
|
struct hrtimer timer;
|
|
ktime_t debounce_time;
|
|
|
|
struct hrtimer btn_timer;
|
|
ktime_t btn_debounce_time;
|
|
|
|
H2W_INFO h2w_info;
|
|
H2W_SPEED speed;
|
|
struct vreg *vreg_h2w;
|
|
};
|
|
static struct h2w_info *hi;
|
|
|
|
static ssize_t h2w_print_name(struct switch_dev *sdev, char *buf)
|
|
{
|
|
switch (switch_get_state(&hi->sdev)) {
|
|
case H2W_NO_DEVICE:
|
|
return sprintf(buf, "No Device\n");
|
|
case H2W_HTC_HEADSET:
|
|
return sprintf(buf, "Headset\n");
|
|
}
|
|
return -EINVAL;
|
|
}
|
|
|
|
static void button_pressed(void)
|
|
{
|
|
H2W_DBG("button_pressed \n");
|
|
atomic_set(&hi->btn_state, 1);
|
|
input_report_key(hi->input, KEY_MEDIA, 1);
|
|
input_sync(hi->input);
|
|
}
|
|
|
|
static void button_released(void)
|
|
{
|
|
H2W_DBG("button_released \n");
|
|
atomic_set(&hi->btn_state, 0);
|
|
input_report_key(hi->input, KEY_MEDIA, 0);
|
|
input_sync(hi->input);
|
|
}
|
|
|
|
/*****************
|
|
* H2W proctocol *
|
|
*****************/
|
|
static inline void h2w_begin_command(void)
|
|
{
|
|
/* Disable H2W interrupt */
|
|
set_irq_type(hi->irq_btn, IRQF_TRIGGER_HIGH);
|
|
disable_irq(hi->irq);
|
|
disable_irq(hi->irq_btn);
|
|
|
|
/* Set H2W_CLK as output low */
|
|
hi->set_clk(0);
|
|
hi->set_clk_dir(1);
|
|
}
|
|
|
|
static inline void h2w_end_command(void)
|
|
{
|
|
/* Set H2W_CLK as input */
|
|
hi->set_clk_dir(0);
|
|
|
|
/* Enable H2W interrupt */
|
|
enable_irq(hi->irq);
|
|
enable_irq(hi->irq_btn);
|
|
set_irq_type(hi->irq_btn, IRQF_TRIGGER_RISING);
|
|
}
|
|
|
|
/*
|
|
* One bit write data
|
|
* ________
|
|
* SCLK O ______| |______O(L)
|
|
*
|
|
*
|
|
* SDAT I <XXXXXXXXXXXXXXXXXXXX>
|
|
*/
|
|
static inline void one_clock_write(unsigned short flag)
|
|
{
|
|
if (flag)
|
|
hi->set_dat(1);
|
|
else
|
|
hi->set_dat(0);
|
|
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(hi->speed);
|
|
hi->set_clk(0);
|
|
}
|
|
|
|
/*
|
|
* One bit write data R/W bit
|
|
* ________
|
|
* SCLK ______| |______O(L)
|
|
* 1----> 1----->
|
|
* 2-------> ______
|
|
* SDAT <XXXXXXXXXXXXXX> I
|
|
* O(H/L)
|
|
*/
|
|
static inline void one_clock_write_RWbit(unsigned short flag)
|
|
{
|
|
if (flag)
|
|
hi->set_dat(1);
|
|
else
|
|
hi->set_dat(0);
|
|
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(hi->speed);
|
|
hi->set_clk(0);
|
|
hi->set_dat_dir(0);
|
|
udelay(hi->speed);
|
|
}
|
|
|
|
/*
|
|
* H2W Reset
|
|
* ___________
|
|
* SCLK O(L)______| |___O(L)
|
|
* 1---->
|
|
* 4-->1-->1-->1us-->
|
|
* ____
|
|
* SDAT O(L)________ | |_______O(L)
|
|
*
|
|
* H2w reset command needs to be issued before every access
|
|
*/
|
|
static inline void h2w_reset(void)
|
|
{
|
|
/* Set H2W_DAT as output low */
|
|
hi->set_dat(0);
|
|
hi->set_dat_dir(1);
|
|
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(4 * hi->speed);
|
|
hi->set_dat(1);
|
|
udelay(hi->speed);
|
|
hi->set_dat(0);
|
|
udelay(hi->speed);
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
}
|
|
|
|
/*
|
|
* H2W Start
|
|
* ___________
|
|
* SCLK O(L)______| |___O(L)
|
|
* 1---->
|
|
* 2----------->1-->
|
|
*
|
|
* SDAT O(L)______________________O(L)
|
|
*/
|
|
static inline void h2w_start(void)
|
|
{
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(2 * hi->speed);
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
}
|
|
|
|
/*
|
|
* H2W Ack
|
|
* __________
|
|
* SCLK _____| |_______O(L)
|
|
* 1----> 1------>
|
|
* 2--------->
|
|
* ________________________
|
|
* SDAT become Input mode here I
|
|
*/
|
|
static inline int h2w_ack(void)
|
|
{
|
|
int retry_times = 0;
|
|
|
|
ack_resend:
|
|
if (retry_times == MAX_ACK_RESEND_TIMES)
|
|
return -1;
|
|
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(2 * hi->speed);
|
|
|
|
if (!hi->get_dat()) {
|
|
retry_times++;
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
goto ack_resend;
|
|
}
|
|
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* One bit read data
|
|
* ________
|
|
* SCLK ______| |______O(L)
|
|
* 2----> 2----->
|
|
* 2------->
|
|
* SDAT <XXXXXXXXXXXXXXXXXXXX>I
|
|
*/
|
|
static unsigned char h2w_readc(void)
|
|
{
|
|
unsigned char h2w_read_data = 0x0;
|
|
int index;
|
|
|
|
for (index = 0; index < 8; index++) {
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
hi->set_clk(1);
|
|
udelay(hi->speed);
|
|
if (hi->get_dat())
|
|
h2w_read_data |= (1 << (7 - index));
|
|
}
|
|
hi->set_clk(0);
|
|
udelay(hi->speed);
|
|
|
|
return h2w_read_data;
|
|
}
|
|
|
|
static int h2w_readc_cmd(H2W_ADDR address)
|
|
{
|
|
int ret = -1, retry_times = 0;
|
|
unsigned char read_data;
|
|
|
|
read_resend:
|
|
if (retry_times == MAX_HOST_RESEND_TIMES)
|
|
goto err_read;
|
|
|
|
h2w_reset();
|
|
h2w_start();
|
|
/* Write address */
|
|
one_clock_write(address & 0x1000);
|
|
one_clock_write(address & 0x0800);
|
|
one_clock_write(address & 0x0400);
|
|
one_clock_write(address & 0x0200);
|
|
one_clock_write(address & 0x0100);
|
|
one_clock_write(address & 0x0080);
|
|
one_clock_write(address & 0x0040);
|
|
one_clock_write(address & 0x0020);
|
|
one_clock_write(address & 0x0010);
|
|
one_clock_write(address & 0x0008);
|
|
one_clock_write(address & 0x0004);
|
|
one_clock_write(address & 0x0002);
|
|
one_clock_write(address & 0x0001);
|
|
one_clock_write_RWbit(1);
|
|
if (h2w_ack() < 0) {
|
|
H2W_DBG("Addr NO ACK(%d).\n", retry_times);
|
|
retry_times++;
|
|
hi->set_clk(0);
|
|
mdelay(RESEND_DELAY);
|
|
goto read_resend;
|
|
}
|
|
|
|
read_data = h2w_readc();
|
|
|
|
if (h2w_ack() < 0) {
|
|
H2W_DBG("Data NO ACK(%d).\n", retry_times);
|
|
retry_times++;
|
|
hi->set_clk(0);
|
|
mdelay(RESEND_DELAY);
|
|
goto read_resend;
|
|
}
|
|
ret = (int)read_data;
|
|
|
|
err_read:
|
|
if (ret < 0)
|
|
H2WE("NO ACK.\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int h2w_writec_cmd(H2W_ADDR address, unsigned char data)
|
|
{
|
|
int ret = -1;
|
|
int retry_times = 0;
|
|
|
|
write_resend:
|
|
if (retry_times == MAX_HOST_RESEND_TIMES)
|
|
goto err_write;
|
|
|
|
h2w_reset();
|
|
h2w_start();
|
|
|
|
/* Write address */
|
|
one_clock_write(address & 0x1000);
|
|
one_clock_write(address & 0x0800);
|
|
one_clock_write(address & 0x0400);
|
|
one_clock_write(address & 0x0200);
|
|
one_clock_write(address & 0x0100);
|
|
one_clock_write(address & 0x0080);
|
|
one_clock_write(address & 0x0040);
|
|
one_clock_write(address & 0x0020);
|
|
one_clock_write(address & 0x0010);
|
|
one_clock_write(address & 0x0008);
|
|
one_clock_write(address & 0x0004);
|
|
one_clock_write(address & 0x0002);
|
|
one_clock_write(address & 0x0001);
|
|
one_clock_write_RWbit(0);
|
|
if (h2w_ack() < 0) {
|
|
H2W_DBG("Addr NO ACK(%d).\n", retry_times);
|
|
retry_times++;
|
|
hi->set_clk(0);
|
|
mdelay(RESEND_DELAY);
|
|
goto write_resend;
|
|
}
|
|
|
|
/* Write data */
|
|
hi->set_dat_dir(1);
|
|
one_clock_write(data & 0x0080);
|
|
one_clock_write(data & 0x0040);
|
|
one_clock_write(data & 0x0020);
|
|
one_clock_write(data & 0x0010);
|
|
one_clock_write(data & 0x0008);
|
|
one_clock_write(data & 0x0004);
|
|
one_clock_write(data & 0x0002);
|
|
one_clock_write_RWbit(data & 0x0001);
|
|
if (h2w_ack() < 0) {
|
|
H2W_DBG("Data NO ACK(%d).\n", retry_times);
|
|
retry_times++;
|
|
hi->set_clk(0);
|
|
mdelay(RESEND_DELAY);
|
|
goto write_resend;
|
|
}
|
|
ret = 0;
|
|
|
|
err_write:
|
|
if (ret < 0)
|
|
H2WE("NO ACK.\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int h2w_get_fnkey(void)
|
|
{
|
|
int ret;
|
|
h2w_begin_command();
|
|
ret = h2w_readc_cmd(H2W_FNKEY_UPDOWN);
|
|
h2w_end_command();
|
|
return ret;
|
|
}
|
|
|
|
static int h2w_dev_init(H2W_INFO *ph2w_info)
|
|
{
|
|
int ret = -1;
|
|
unsigned char ascr0 = 0;
|
|
int h2w_sys = 0, maxgpadd = 0, maxadd = 0, key = 0;
|
|
|
|
hi->speed = H2W_50KHz;
|
|
h2w_begin_command();
|
|
|
|
/* read H2W_SYSTEM */
|
|
h2w_sys = h2w_readc_cmd(H2W_SYSTEM);
|
|
if (h2w_sys == -1) {
|
|
H2WE("read H2W_SYSTEM(0x0000) failed.\n");
|
|
goto err_plugin;
|
|
}
|
|
ph2w_info->ACC_CLASS = (h2w_sys & 0x03);
|
|
ph2w_info->AUDIO_DEVICE = (h2w_sys & 0x04) > 0 ? 1 : 0;
|
|
ph2w_info->HW_REV = (h2w_sys & 0x18) >> 3;
|
|
ph2w_info->SLEEP_PR = (h2w_sys & 0x20) >> 5;
|
|
ph2w_info->CLK_SP = (h2w_sys & 0xC0) >> 6;
|
|
|
|
/* enter init mode */
|
|
if (h2w_writec_cmd(H2W_ASCR0, H2W_ASCR_DEVICE_INI) < 0) {
|
|
H2WE("write H2W_ASCR0(0x0002) failed.\n");
|
|
goto err_plugin;
|
|
}
|
|
udelay(10);
|
|
|
|
/* read H2W_MAX_GP_ADD */
|
|
maxgpadd = h2w_readc_cmd(H2W_MAX_GP_ADD);
|
|
if (maxgpadd == -1) {
|
|
H2WE("write H2W_MAX_GP_ADD(0x0001) failed.\n");
|
|
goto err_plugin;
|
|
}
|
|
ph2w_info->CLK_SP += (maxgpadd & 0x60) >> 3;
|
|
ph2w_info->MAX_GP_ADD = (maxgpadd & 0x1F);
|
|
|
|
/* read key group */
|
|
if (ph2w_info->MAX_GP_ADD >= 1) {
|
|
ph2w_info->KEY_MAXADD = h2w_readc_cmd(H2W_KEY_MAXADD);
|
|
if (ph2w_info->KEY_MAXADD == -1)
|
|
goto err_plugin;
|
|
if (ph2w_info->KEY_MAXADD >= 1) {
|
|
key = h2w_readc_cmd(H2W_ASCII_DOWN);
|
|
if (key < 0)
|
|
goto err_plugin;
|
|
ph2w_info->ASCII_DOWN = (key == 0xFF) ? 1 : 0;
|
|
}
|
|
if (ph2w_info->KEY_MAXADD >= 2) {
|
|
key = h2w_readc_cmd(H2W_ASCII_UP);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->ASCII_UP = (key == 0xFF) ? 1 : 0;
|
|
}
|
|
if (ph2w_info->KEY_MAXADD >= 3) {
|
|
key = h2w_readc_cmd(H2W_FNKEY_UPDOWN);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->FNKEY_UPDOWN = (key == 0xFF) ? 1 : 0;
|
|
}
|
|
if (ph2w_info->KEY_MAXADD >= 4) {
|
|
key = h2w_readc_cmd(H2W_KD_STATUS);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->KD_STATUS = (key == 0x01) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
/* read led group */
|
|
if (ph2w_info->MAX_GP_ADD >= 2) {
|
|
ph2w_info->LED_MAXADD = h2w_readc_cmd(H2W_LED_MAXADD);
|
|
if (ph2w_info->LED_MAXADD == -1)
|
|
goto err_plugin;
|
|
if (ph2w_info->LED_MAXADD >= 1) {
|
|
key = h2w_readc_cmd(H2W_LEDCT0);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->LEDCT0 = (key == 0x02) ? 1 : 0;
|
|
}
|
|
}
|
|
|
|
/* read group 3, 4, 5 */
|
|
if (ph2w_info->MAX_GP_ADD >= 3) {
|
|
maxadd = h2w_readc_cmd(H2W_CRDL_MAXADD);
|
|
if (maxadd == -1)
|
|
goto err_plugin;
|
|
}
|
|
if (ph2w_info->MAX_GP_ADD >= 4) {
|
|
maxadd = h2w_readc_cmd(H2W_CARKIT_MAXADD);
|
|
if (maxadd == -1)
|
|
goto err_plugin;
|
|
}
|
|
if (ph2w_info->MAX_GP_ADD >= 5) {
|
|
maxadd = h2w_readc_cmd(H2W_USBHOST_MAXADD);
|
|
if (maxadd == -1)
|
|
goto err_plugin;
|
|
}
|
|
|
|
/* read medical group */
|
|
if (ph2w_info->MAX_GP_ADD >= 6) {
|
|
ph2w_info->MED_MAXADD = h2w_readc_cmd(H2W_MED_MAXADD);
|
|
if (ph2w_info->MED_MAXADD == -1)
|
|
goto err_plugin;
|
|
if (ph2w_info->MED_MAXADD >= 1) {
|
|
key = h2w_readc_cmd(H2W_MED_CONTROL);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->DATA_EN = (key & 0x01);
|
|
ph2w_info->AP_EN = (key & 0x02) >> 1;
|
|
ph2w_info->AP_ID = (key & 0x1c) >> 2;
|
|
}
|
|
if (ph2w_info->MED_MAXADD >= 2) {
|
|
key = h2w_readc_cmd(H2W_MED_IN_DATA);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
}
|
|
}
|
|
|
|
if (ph2w_info->AUDIO_DEVICE)
|
|
ascr0 = H2W_ASCR_AUDIO_IN | H2W_ASCR_ACT_EN;
|
|
else
|
|
ascr0 = H2W_ASCR_ACT_EN;
|
|
|
|
if (h2w_writec_cmd(H2W_ASCR0, ascr0) < 0)
|
|
goto err_plugin;
|
|
udelay(10);
|
|
|
|
ret = 0;
|
|
|
|
/* adjust speed */
|
|
if (ph2w_info->MAX_GP_ADD == 2) {
|
|
/* Remote control */
|
|
hi->speed = H2W_250KHz;
|
|
} else if (ph2w_info->MAX_GP_ADD == 6) {
|
|
if (ph2w_info->MED_MAXADD >= 1) {
|
|
key = h2w_readc_cmd(H2W_MED_CONTROL);
|
|
if (key == -1)
|
|
goto err_plugin;
|
|
ph2w_info->DATA_EN = (key & 0x01);
|
|
ph2w_info->AP_EN = (key & 0x02) >> 1;
|
|
ph2w_info->AP_ID = (key & 0x1c) >> 2;
|
|
}
|
|
}
|
|
|
|
err_plugin:
|
|
h2w_end_command();
|
|
|
|
return ret;
|
|
}
|
|
|
|
static inline void h2w_dev_power_on(int on)
|
|
{
|
|
if (!hi->vreg_h2w)
|
|
return;
|
|
|
|
if (on)
|
|
vreg_enable(hi->vreg_h2w);
|
|
else
|
|
vreg_disable(hi->vreg_h2w);
|
|
}
|
|
|
|
static int h2w_dev_detect(void)
|
|
{
|
|
int ret = -1;
|
|
int retry_times;
|
|
|
|
for (retry_times = 5; retry_times; retry_times--) {
|
|
/* Enable H2W Power */
|
|
h2w_dev_power_on(1);
|
|
msleep(100);
|
|
memset(&hi->h2w_info, 0, sizeof(H2W_INFO));
|
|
if (h2w_dev_init(&hi->h2w_info) < 0) {
|
|
h2w_dev_power_on(0);
|
|
msleep(100);
|
|
} else if (hi->h2w_info.MAX_GP_ADD == 2) {
|
|
ret = 0;
|
|
break;
|
|
} else {
|
|
printk(KERN_INFO "h2w_detect: detect error(%d)\n"
|
|
, hi->h2w_info.MAX_GP_ADD);
|
|
h2w_dev_power_on(0);
|
|
msleep(100);
|
|
}
|
|
printk(KERN_INFO "h2w_detect(%d)\n"
|
|
, hi->h2w_info.MAX_GP_ADD);
|
|
}
|
|
H2W_DBG("h2w_detect:(%d)\n", retry_times);
|
|
return ret;
|
|
}
|
|
|
|
static void remove_headset(void)
|
|
{
|
|
unsigned long irq_flags;
|
|
|
|
H2W_DBG("");
|
|
|
|
mutex_lock(&hi->mutex_lock);
|
|
switch_set_state(&hi->sdev, switch_get_state(&hi->sdev) &
|
|
~(BIT_HEADSET | BIT_HEADSET_NO_MIC));
|
|
mutex_unlock(&hi->mutex_lock);
|
|
hi->init_cpld();
|
|
|
|
/* Disable button */
|
|
switch (hi->htc_headset_flag) {
|
|
case H2W_HTC_HEADSET:
|
|
local_irq_save(irq_flags);
|
|
disable_irq(hi->irq_btn);
|
|
local_irq_restore(irq_flags);
|
|
|
|
if (atomic_read(&hi->btn_state))
|
|
button_released();
|
|
break;
|
|
case H2W_DEVICE:
|
|
h2w_dev_power_on(0);
|
|
set_irq_type(hi->irq_btn, IRQF_TRIGGER_LOW);
|
|
disable_irq(hi->irq_btn);
|
|
/* 10ms (5-15 with 10ms tick) */
|
|
hi->btn_debounce_time = ktime_set(0, 10000000);
|
|
hi->set_clk_dir(0);
|
|
hi->set_dat_dir(0);
|
|
break;
|
|
}
|
|
|
|
hi->htc_headset_flag = 0;
|
|
hi->debounce_time = ktime_set(0, 100000000); /* 100 ms */
|
|
|
|
}
|
|
|
|
#ifdef CONFIG_MSM_SERIAL_DEBUGGER
|
|
extern void msm_serial_debug_enable(int);
|
|
#endif
|
|
|
|
static void insert_headset(int type)
|
|
{
|
|
unsigned long irq_flags;
|
|
int state;
|
|
|
|
H2W_DBG("");
|
|
|
|
hi->htc_headset_flag = type;
|
|
state = BIT_HEADSET | BIT_HEADSET_NO_MIC;
|
|
|
|
state = switch_get_state(&hi->sdev);
|
|
state &= ~(BIT_HEADSET_NO_MIC | BIT_HEADSET);
|
|
switch (type) {
|
|
case H2W_HTC_HEADSET:
|
|
printk(KERN_INFO "insert_headset H2W_HTC_HEADSET\n");
|
|
state |= BIT_HEADSET;
|
|
hi->ignore_btn = !gpio_get_value(hi->cable_in2);
|
|
/* Enable button irq */
|
|
local_irq_save(irq_flags);
|
|
enable_irq(hi->irq_btn);
|
|
local_irq_restore(irq_flags);
|
|
hi->debounce_time = ktime_set(0, 200000000); /* 20 ms */
|
|
break;
|
|
case H2W_DEVICE:
|
|
if (h2w_dev_detect() < 0) {
|
|
printk(KERN_INFO "H2W_DEVICE -- Non detect\n");
|
|
remove_headset();
|
|
} else {
|
|
printk(KERN_INFO "H2W_DEVICE -- detect\n");
|
|
hi->btn_debounce_time = ktime_set(0, 0);
|
|
local_irq_save(irq_flags);
|
|
enable_irq(hi->irq_btn);
|
|
set_irq_type(hi->irq_btn, IRQF_TRIGGER_RISING);
|
|
local_irq_restore(irq_flags);
|
|
state |= BIT_HEADSET;
|
|
}
|
|
break;
|
|
case H2W_USB_CRADLE:
|
|
state |= BIT_HEADSET_NO_MIC;
|
|
break;
|
|
case H2W_UART_DEBUG:
|
|
hi->config_cpld(hi->debug_uart);
|
|
printk(KERN_INFO "switch to H2W_UART_DEBUG\n");
|
|
default:
|
|
return;
|
|
}
|
|
mutex_lock(&hi->mutex_lock);
|
|
switch_set_state(&hi->sdev, state);
|
|
mutex_unlock(&hi->mutex_lock);
|
|
|
|
#ifdef CONFIG_MSM_SERIAL_DEBUGGER
|
|
msm_serial_debug_enable(false);
|
|
#endif
|
|
|
|
}
|
|
#if 0
|
|
static void remove_headset(void)
|
|
{
|
|
unsigned long irq_flags;
|
|
|
|
H2W_DBG("");
|
|
|
|
switch_set_state(&hi->sdev, H2W_NO_DEVICE);
|
|
|
|
hi->init_cpld();
|
|
|
|
/* Disable button */
|
|
local_irq_save(irq_flags);
|
|
disable_irq(hi->irq_btn);
|
|
local_irq_restore(irq_flags);
|
|
|
|
if (atomic_read(&hi->btn_state))
|
|
button_released();
|
|
|
|
hi->debounce_time = ktime_set(0, 100000000); /* 100 ms */
|
|
}
|
|
#endif
|
|
static int is_accessary_pluged_in(void)
|
|
{
|
|
int type = 0;
|
|
int clk1 = 0, dat1 = 0, clk2 = 0, dat2 = 0, clk3 = 0, dat3 = 0;
|
|
|
|
/* Step1: save H2W_CLK and H2W_DAT */
|
|
/* Delay 10ms for pin stable. */
|
|
msleep(10);
|
|
clk1 = gpio_get_value(hi->h2w_clk);
|
|
dat1 = gpio_get_value(hi->h2w_data);
|
|
|
|
/*
|
|
* Step2: set GPIO_CABLE_IN1 as output high and GPIO_CABLE_IN2 as
|
|
* input
|
|
*/
|
|
gpio_direction_output(hi->cable_in1, 1);
|
|
gpio_direction_input(hi->cable_in2);
|
|
/* Delay 10ms for pin stable. */
|
|
msleep(10);
|
|
/* Step 3: save H2W_CLK and H2W_DAT */
|
|
clk2 = gpio_get_value(hi->h2w_clk);
|
|
dat2 = gpio_get_value(hi->h2w_data);
|
|
|
|
/*
|
|
* Step 4: set GPIO_CABLE_IN1 as input and GPIO_CABLE_IN2 as output
|
|
* high
|
|
*/
|
|
gpio_direction_input(hi->cable_in1);
|
|
gpio_direction_output(hi->cable_in2, 1);
|
|
/* Delay 10ms for pin stable. */
|
|
msleep(10);
|
|
/* Step 5: save H2W_CLK and H2W_DAT */
|
|
clk3 = gpio_get_value(hi->h2w_clk);
|
|
dat3 = gpio_get_value(hi->h2w_data);
|
|
|
|
/* Step 6: set both GPIO_CABLE_IN1 and GPIO_CABLE_IN2 as input */
|
|
gpio_direction_input(hi->cable_in1);
|
|
gpio_direction_input(hi->cable_in2);
|
|
|
|
H2W_DBG("(%d,%d) (%d,%d) (%d,%d)\n",
|
|
clk1, dat1, clk2, dat2, clk3, dat3);
|
|
|
|
if ((clk1 == 0) && (dat1 == 1) &&
|
|
(clk2 == 0) && (dat2 == 1) &&
|
|
(clk3 == 0) && (dat3 == 1))
|
|
type = H2W_HTC_HEADSET;
|
|
else if ((clk1 == 0) && (dat1 == 0) &&
|
|
(clk2 == 0) && (dat2 == 0) &&
|
|
(clk3 == 0) && (dat3 == 0))
|
|
type = NORMAL_HEARPHONE;
|
|
else if ((clk1 == 0) && (dat1 == 0) &&
|
|
(clk2 == 1) && (dat2 == 0) &&
|
|
(clk3 == 0) && (dat3 == 1))
|
|
type = H2W_DEVICE;
|
|
else if ((clk1 == 0) && (dat1 == 0) &&
|
|
(clk2 == 1) && (dat2 == 1) &&
|
|
(clk3 == 1) && (dat3 == 1))
|
|
type = H2W_USB_CRADLE;
|
|
else if ((clk1 == 0) && (dat1 == 1) &&
|
|
(clk2 == 1) && (dat2 == 1) &&
|
|
(clk3 == 0) && (dat3 == 1))
|
|
type = H2W_UART_DEBUG;
|
|
else
|
|
type = H2W_NO_DEVICE;
|
|
|
|
return type;
|
|
}
|
|
|
|
|
|
static void detection_work(struct work_struct *work)
|
|
{
|
|
unsigned long irq_flags;
|
|
int type;
|
|
|
|
H2W_DBG("");
|
|
|
|
if (gpio_get_value(hi->cable_in1) != 0) {
|
|
/* Headset not plugged in */
|
|
if (switch_get_state(&hi->sdev) != H2W_NO_DEVICE)
|
|
remove_headset();
|
|
return;
|
|
}
|
|
|
|
/* Something plugged in, lets make sure its a headset */
|
|
|
|
/* Switch CPLD to GPIO to do detection */
|
|
hi->config_cpld(H2W_GPIO);
|
|
|
|
/* Disable headset interrupt while detecting.*/
|
|
local_irq_save(irq_flags);
|
|
disable_irq(hi->irq);
|
|
local_irq_restore(irq_flags);
|
|
|
|
/* Something plugged in, lets make sure its a headset */
|
|
type = is_accessary_pluged_in();
|
|
|
|
/* Restore IRQs */
|
|
local_irq_save(irq_flags);
|
|
enable_irq(hi->irq);
|
|
local_irq_restore(irq_flags);
|
|
|
|
insert_headset(type);
|
|
}
|
|
|
|
static enum hrtimer_restart button_event_timer_func(struct hrtimer *data)
|
|
{
|
|
int key, press, keyname, h2w_key = 1;
|
|
|
|
H2W_DBG("");
|
|
|
|
if (switch_get_state(&hi->sdev) == H2W_HTC_HEADSET) {
|
|
switch (hi->htc_headset_flag) {
|
|
case H2W_HTC_HEADSET:
|
|
if (gpio_get_value(hi->cable_in2)) {
|
|
if (hi->ignore_btn)
|
|
hi->ignore_btn = 0;
|
|
else if (atomic_read(&hi->btn_state))
|
|
button_released();
|
|
} else {
|
|
if (!hi->ignore_btn &&
|
|
!atomic_read(&hi->btn_state))
|
|
button_pressed();
|
|
}
|
|
break;
|
|
case H2W_DEVICE:
|
|
if ((hi->get_dat() == 1) && (hi->get_clk() == 1)) {
|
|
/* Don't do anything because H2W pull out. */
|
|
H2WE("Remote Control pull out.\n");
|
|
} else {
|
|
key = h2w_get_fnkey();
|
|
press = (key > 0x7F) ? 0 : 1;
|
|
keyname = key & 0x7F;
|
|
/* H2WI("key = %d, press = %d,
|
|
keyname = %d \n",
|
|
key, press, keyname); */
|
|
switch (keyname) {
|
|
case H2W_KEY_PLAY:
|
|
H2WI("H2W_KEY_PLAY");
|
|
key = KEY_PLAYPAUSE;
|
|
break;
|
|
case H2W_KEY_FORWARD:
|
|
H2WI("H2W_KEY_FORWARD");
|
|
key = KEY_NEXTSONG;
|
|
break;
|
|
case H2W_KEY_BACKWARD:
|
|
H2WI("H2W_KEY_BACKWARD");
|
|
key = KEY_PREVIOUSSONG;
|
|
break;
|
|
case H2W_KEY_VOLUP:
|
|
H2WI("H2W_KEY_VOLUP");
|
|
key = KEY_VOLUMEUP;
|
|
break;
|
|
case H2W_KEY_VOLDOWN:
|
|
H2WI("H2W_KEY_VOLDOWN");
|
|
key = KEY_VOLUMEDOWN;
|
|
break;
|
|
case H2W_KEY_PICKUP:
|
|
H2WI("H2W_KEY_PICKUP");
|
|
key = KEY_SEND;
|
|
break;
|
|
case H2W_KEY_HANGUP:
|
|
H2WI("H2W_KEY_HANGUP");
|
|
key = KEY_END;
|
|
break;
|
|
case H2W_KEY_MUTE:
|
|
H2WI("H2W_KEY_MUTE");
|
|
key = KEY_MUTE;
|
|
break;
|
|
case H2W_KEY_HOLD:
|
|
H2WI("H2W_KEY_HOLD");
|
|
break;
|
|
default:
|
|
H2WI("default");
|
|
h2w_key = 0;
|
|
}
|
|
if (h2w_key) {
|
|
if (press)
|
|
H2WI("Press\n");
|
|
else
|
|
H2WI("Release\n");
|
|
input_report_key(hi->input, key, press);
|
|
}
|
|
}
|
|
break;
|
|
} /* end switch */
|
|
}
|
|
|
|
return HRTIMER_NORESTART;
|
|
}
|
|
|
|
static enum hrtimer_restart detect_event_timer_func(struct hrtimer *data)
|
|
{
|
|
H2W_DBG("");
|
|
|
|
queue_work(g_detection_work_queue, &g_detection_work);
|
|
return HRTIMER_NORESTART;
|
|
}
|
|
|
|
static irqreturn_t detect_irq_handler(int irq, void *dev_id)
|
|
{
|
|
int value1, value2;
|
|
int retry_limit = 10;
|
|
|
|
H2W_DBG("");
|
|
set_irq_type(hi->irq_btn, IRQF_TRIGGER_LOW);
|
|
do {
|
|
value1 = gpio_get_value(hi->cable_in1);
|
|
set_irq_type(hi->irq, value1 ?
|
|
IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);
|
|
value2 = gpio_get_value(hi->cable_in1);
|
|
} while (value1 != value2 && retry_limit-- > 0);
|
|
|
|
H2W_DBG("value2 = %d (%d retries), device=%d",
|
|
value2, (10-retry_limit), switch_get_state(&hi->sdev));
|
|
|
|
if ((switch_get_state(&hi->sdev) == H2W_NO_DEVICE) ^ value2) {
|
|
if (switch_get_state(&hi->sdev) == H2W_HTC_HEADSET)
|
|
hi->ignore_btn = 1;
|
|
/* Do the rest of the work in timer context */
|
|
hrtimer_start(&hi->timer, hi->debounce_time, HRTIMER_MODE_REL);
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static irqreturn_t button_irq_handler(int irq, void *dev_id)
|
|
{
|
|
int value1, value2;
|
|
int retry_limit = 10;
|
|
|
|
H2W_DBG("");
|
|
do {
|
|
value1 = gpio_get_value(hi->cable_in2);
|
|
if (hi->htc_headset_flag != H2W_DEVICE)
|
|
set_irq_type(hi->irq_btn, value1 ?
|
|
IRQF_TRIGGER_LOW : IRQF_TRIGGER_HIGH);
|
|
value2 = gpio_get_value(hi->cable_in2);
|
|
} while (value1 != value2 && retry_limit-- > 0);
|
|
|
|
H2W_DBG("value2 = %d (%d retries)", value2, (10-retry_limit));
|
|
|
|
hrtimer_start(&hi->btn_timer, hi->btn_debounce_time, HRTIMER_MODE_REL);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
#if defined(CONFIG_DEBUG_FS)
|
|
static int h2w_debug_set(void *data, u64 val)
|
|
{
|
|
mutex_lock(&hi->mutex_lock);
|
|
switch_set_state(&hi->sdev, (int)val);
|
|
mutex_unlock(&hi->mutex_lock);
|
|
return 0;
|
|
}
|
|
|
|
static int h2w_debug_get(void *data, u64 *val)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_SIMPLE_ATTRIBUTE(h2w_debug_fops, h2w_debug_get, h2w_debug_set, "%llu\n");
|
|
static int __init h2w_debug_init(void)
|
|
{
|
|
struct dentry *dent;
|
|
|
|
dent = debugfs_create_dir("h2w", 0);
|
|
if (IS_ERR(dent))
|
|
return PTR_ERR(dent);
|
|
|
|
debugfs_create_file("state", 0644, dent, NULL, &h2w_debug_fops);
|
|
|
|
return 0;
|
|
}
|
|
|
|
device_initcall(h2w_debug_init);
|
|
#endif
|
|
|
|
static int h2w_probe(struct platform_device *pdev)
|
|
{
|
|
int ret;
|
|
struct h2w_platform_data *pdata = pdev->dev.platform_data;
|
|
|
|
printk(KERN_INFO "H2W: Registering H2W (headset) driver\n");
|
|
hi = kzalloc(sizeof(struct h2w_info), GFP_KERNEL);
|
|
if (!hi)
|
|
return -ENOMEM;
|
|
|
|
atomic_set(&hi->btn_state, 0);
|
|
hi->ignore_btn = 0;
|
|
|
|
hi->debounce_time = ktime_set(0, 100000000); /* 100 ms */
|
|
hi->btn_debounce_time = ktime_set(0, 10000000); /* 10 ms */
|
|
|
|
hi->htc_headset_flag = 0;
|
|
hi->cable_in1 = pdata->cable_in1;
|
|
hi->cable_in2 = pdata->cable_in2;
|
|
hi->h2w_clk = pdata->h2w_clk;
|
|
hi->h2w_data = pdata->h2w_data;
|
|
hi->debug_uart = pdata->debug_uart;
|
|
hi->config_cpld = pdata->config_cpld;
|
|
hi->init_cpld = pdata->init_cpld;
|
|
hi->set_dat = pdata->set_dat;
|
|
hi->set_clk = pdata->set_clk;
|
|
hi->set_dat_dir = pdata->set_dat_dir;
|
|
hi->set_clk_dir = pdata->set_clk_dir;
|
|
hi->get_dat = pdata->get_dat;
|
|
hi->get_clk = pdata->get_clk;
|
|
hi->speed = H2W_50KHz;
|
|
/* obtain needed VREGs */
|
|
if (pdata->power_name)
|
|
hi->vreg_h2w = vreg_get(0, pdata->power_name);
|
|
|
|
mutex_init(&hi->mutex_lock);
|
|
|
|
hi->sdev.name = "h2w";
|
|
hi->sdev.print_name = h2w_print_name;
|
|
|
|
ret = switch_dev_register(&hi->sdev);
|
|
if (ret < 0)
|
|
goto err_switch_dev_register;
|
|
|
|
g_detection_work_queue = create_workqueue("detection");
|
|
if (g_detection_work_queue == NULL) {
|
|
ret = -ENOMEM;
|
|
goto err_create_work_queue;
|
|
}
|
|
|
|
ret = gpio_request(hi->cable_in1, "h2w_detect");
|
|
if (ret < 0)
|
|
goto err_request_detect_gpio;
|
|
|
|
ret = gpio_request(hi->cable_in2, "h2w_button");
|
|
if (ret < 0)
|
|
goto err_request_button_gpio;
|
|
|
|
ret = gpio_direction_input(hi->cable_in1);
|
|
if (ret < 0)
|
|
goto err_set_detect_gpio;
|
|
|
|
ret = gpio_direction_input(hi->cable_in2);
|
|
if (ret < 0)
|
|
goto err_set_button_gpio;
|
|
|
|
hi->irq = gpio_to_irq(hi->cable_in1);
|
|
if (hi->irq < 0) {
|
|
ret = hi->irq;
|
|
goto err_get_h2w_detect_irq_num_failed;
|
|
}
|
|
|
|
hi->irq_btn = gpio_to_irq(hi->cable_in2);
|
|
if (hi->irq_btn < 0) {
|
|
ret = hi->irq_btn;
|
|
goto err_get_button_irq_num_failed;
|
|
}
|
|
|
|
/* Set CPLD MUX to H2W <-> CPLD GPIO */
|
|
hi->init_cpld();
|
|
|
|
hrtimer_init(&hi->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
|
|
hi->timer.function = detect_event_timer_func;
|
|
hrtimer_init(&hi->btn_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
|
|
hi->btn_timer.function = button_event_timer_func;
|
|
|
|
ret = request_irq(hi->irq, detect_irq_handler,
|
|
IRQF_TRIGGER_LOW, "h2w_detect", NULL);
|
|
if (ret < 0)
|
|
goto err_request_detect_irq;
|
|
|
|
/* Disable button until plugged in */
|
|
set_irq_flags(hi->irq_btn, IRQF_VALID | IRQF_NOAUTOEN);
|
|
ret = request_irq(hi->irq_btn, button_irq_handler,
|
|
IRQF_TRIGGER_LOW, "h2w_button", NULL);
|
|
if (ret < 0)
|
|
goto err_request_h2w_headset_button_irq;
|
|
|
|
ret = set_irq_wake(hi->irq, 1);
|
|
if (ret < 0)
|
|
goto err_request_input_dev;
|
|
|
|
ret = set_irq_wake(hi->irq_btn, 1);
|
|
if (ret < 0)
|
|
goto err_request_input_dev;
|
|
|
|
|
|
|
|
hi->input = input_allocate_device();
|
|
if (!hi->input) {
|
|
ret = -ENOMEM;
|
|
goto err_request_input_dev;
|
|
}
|
|
|
|
hi->input->name = "h2w headset";
|
|
set_bit(EV_SYN, hi->input->evbit);
|
|
set_bit(EV_KEY, hi->input->evbit);
|
|
set_bit(KEY_MEDIA, hi->input->keybit);
|
|
set_bit(KEY_NEXTSONG, hi->input->keybit);
|
|
set_bit(KEY_PLAYPAUSE, hi->input->keybit);
|
|
set_bit(KEY_PREVIOUSSONG, hi->input->keybit);
|
|
set_bit(KEY_MUTE, hi->input->keybit);
|
|
set_bit(KEY_VOLUMEUP, hi->input->keybit);
|
|
set_bit(KEY_VOLUMEDOWN, hi->input->keybit);
|
|
set_bit(KEY_END, hi->input->keybit);
|
|
set_bit(KEY_SEND, hi->input->keybit);
|
|
|
|
ret = input_register_device(hi->input);
|
|
if (ret < 0)
|
|
goto err_register_input_dev;
|
|
|
|
return 0;
|
|
|
|
err_register_input_dev:
|
|
input_free_device(hi->input);
|
|
err_request_input_dev:
|
|
free_irq(hi->irq_btn, 0);
|
|
err_request_h2w_headset_button_irq:
|
|
free_irq(hi->irq, 0);
|
|
err_request_detect_irq:
|
|
err_get_button_irq_num_failed:
|
|
err_get_h2w_detect_irq_num_failed:
|
|
err_set_button_gpio:
|
|
err_set_detect_gpio:
|
|
gpio_free(hi->cable_in2);
|
|
err_request_button_gpio:
|
|
gpio_free(hi->cable_in1);
|
|
err_request_detect_gpio:
|
|
destroy_workqueue(g_detection_work_queue);
|
|
err_create_work_queue:
|
|
switch_dev_unregister(&hi->sdev);
|
|
err_switch_dev_register:
|
|
printk(KERN_ERR "H2W: Failed to register driver\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int h2w_remove(struct platform_device *pdev)
|
|
{
|
|
H2W_DBG("");
|
|
if (switch_get_state(&hi->sdev))
|
|
remove_headset();
|
|
input_unregister_device(hi->input);
|
|
gpio_free(hi->cable_in2);
|
|
gpio_free(hi->cable_in1);
|
|
free_irq(hi->irq_btn, 0);
|
|
free_irq(hi->irq, 0);
|
|
destroy_workqueue(g_detection_work_queue);
|
|
switch_dev_unregister(&hi->sdev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static struct platform_driver h2w_driver = {
|
|
.probe = h2w_probe,
|
|
.remove = h2w_remove,
|
|
.driver = {
|
|
.name = "h2w",
|
|
.owner = THIS_MODULE,
|
|
},
|
|
};
|
|
|
|
static int __init h2w_init(void)
|
|
{
|
|
H2W_DBG("");
|
|
return platform_driver_register(&h2w_driver);
|
|
}
|
|
|
|
static void __exit h2w_exit(void)
|
|
{
|
|
platform_driver_unregister(&h2w_driver);
|
|
}
|
|
|
|
module_init(h2w_init);
|
|
module_exit(h2w_exit);
|
|
|
|
MODULE_AUTHOR("Laurence Chen <Laurence_Chen@htc.com>");
|
|
MODULE_DESCRIPTION("HTC 2 Wire detection driver");
|
|
MODULE_LICENSE("GPL");
|