502 lines
12 KiB
C
Executable File
502 lines
12 KiB
C
Executable File
/*
|
|
* drivers/input/sensor/sunxi_tpadc.c
|
|
*
|
|
* Copyright (C) 2017 Allwinner.
|
|
* fuzhaoke <yangshounan@allwinnertech.com>
|
|
*
|
|
* SUNXI TPADC Controller Driver
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/init.h>
|
|
#include <linux/input.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/ioport.h>
|
|
#include <asm/irq.h>
|
|
#include <asm/io.h>
|
|
#include <linux/timer.h>
|
|
#include <linux/clk.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/of_irq.h>
|
|
#include <linux/of_address.h>
|
|
#include <linux/pm.h>
|
|
#include "sunxi_tpadc.h"
|
|
|
|
static void sunxi_tpadc_clk_division(void __iomem *reg_base, unsigned char divider)
|
|
{
|
|
u32 reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL0_REG);
|
|
reg |= (divider & 0x3) << ADC_CLK_DIVIDER;
|
|
writel(reg, reg_base + TP_CTRL0_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_fs_set(void __iomem *reg_base, unsigned char divider)
|
|
{
|
|
u32 reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL0_REG);
|
|
reg |= (divider & 0xf) << FS_DIVIDER;
|
|
writel(reg, reg_base + TP_CTRL0_REG);
|
|
}
|
|
static void sunxi_tpadc_ta_set(void __iomem *reg_base, int val)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL0_REG);
|
|
reg |= val & 0xfff;
|
|
writel(reg, reg_base + TP_CTRL0_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_clk_select(void __iomem *reg_base, bool on)
|
|
{
|
|
u32 reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL0_REG);
|
|
if (on)
|
|
reg &= ~TP_CLOCK;
|
|
else
|
|
reg |= TP_CLOCK;
|
|
writel(reg, reg_base + TP_CTRL0_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_select_adc(void __iomem *reg_base)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL1_REG);
|
|
reg |= ADC_ENABLE;
|
|
writel(reg, reg_base + TP_CTRL1_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_rtp_enable(void __iomem *reg_base)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL1_REG);
|
|
reg |= RTP_MODE_ENABLE;
|
|
writel(reg, reg_base + TP_CTRL1_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_filter_enable(void __iomem *reg_base)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL3_REG);
|
|
reg |= RTP_FILTER_ENABLE;
|
|
writel(reg, reg_base + TP_CTRL3_REG);
|
|
}
|
|
static void sunxi_tpadc_filter_type(void __iomem *reg_base)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_CTRL3_REG);
|
|
reg |= RTP_FILTER_TYPE;
|
|
writel(reg, reg_base + TP_CTRL3_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_trig_level(void __iomem *reg_base, int val)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_INT_FIFOC_REG);
|
|
reg |= (val & 0xf)<<8;
|
|
writel(reg, reg_base + TP_INT_FIFOC_REG);
|
|
}
|
|
static void sunxi_tpadc_downup_irq(void __iomem *reg_base)
|
|
{
|
|
int reg;
|
|
|
|
reg = readl(reg_base + TP_INT_FIFOC_REG);
|
|
reg |= 0x03;
|
|
writel(reg, reg_base + TP_INT_FIFOC_REG);
|
|
}
|
|
|
|
static u32 sunxi_tpadc_ch_select(void __iomem *reg_base, enum tp_channel_id id)
|
|
{
|
|
u32 reg_val;
|
|
|
|
reg_val = readl(reg_base + TP_CTRL1_REG);
|
|
switch (id) {
|
|
case TP_CH_0:
|
|
reg_val |= TP_CH0_SELECT;
|
|
break;
|
|
case TP_CH_1:
|
|
reg_val |= TP_CH1_SELECT;
|
|
break;
|
|
case TP_CH_2:
|
|
reg_val |= TP_CH2_SELECT;
|
|
break;
|
|
case TP_CH_3:
|
|
reg_val |= TP_CH3_SELECT;
|
|
break;
|
|
default:
|
|
pr_err("%s, invalid channel id!", __func__);
|
|
return -EINVAL;
|
|
}
|
|
writel(reg_val, reg_base + TP_CTRL1_REG);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sunxi_tpadc_ch_deselect(void __iomem *reg_base, enum tp_channel_id id)
|
|
{
|
|
u32 reg_val;
|
|
|
|
reg_val = readl(reg_base + TP_CTRL1_REG);
|
|
switch (id) {
|
|
case TP_CH_0:
|
|
reg_val &= ~TP_CH0_SELECT;
|
|
break;
|
|
case TP_CH_1:
|
|
reg_val &= ~TP_CH1_SELECT;
|
|
break;
|
|
case TP_CH_2:
|
|
reg_val &= ~TP_CH2_SELECT;
|
|
break;
|
|
case TP_CH_3:
|
|
reg_val &= ~TP_CH3_SELECT;
|
|
break;
|
|
default:
|
|
pr_err("%s, invalid channel id!", __func__);
|
|
return -EINVAL;
|
|
}
|
|
writel(reg_val, reg_base + TP_CTRL1_REG);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void sunxi_tpadc_enable_irq(void __iomem *reg_base)
|
|
{
|
|
u32 reg_val;
|
|
|
|
reg_val = readl(reg_base + TP_INT_FIFOC_REG);
|
|
reg_val |= TP_DATA_IRQ_EN;
|
|
writel(reg_val, reg_base + TP_INT_FIFOC_REG);
|
|
|
|
}
|
|
|
|
static void sunxi_tpadc_disable_irq(void __iomem *reg_base)
|
|
{
|
|
u32 reg_val;
|
|
|
|
reg_val = readl(reg_base + TP_INT_FIFOC_REG);
|
|
reg_val &= ~TP_DATA_IRQ_EN;
|
|
writel(reg_val, reg_base + TP_INT_FIFOC_REG);
|
|
}
|
|
|
|
static u32 sunxi_tpadc_irq_status(void __iomem *reg_base)
|
|
{
|
|
return readl(reg_base + TP_INT_FIFOS_REG);
|
|
}
|
|
|
|
static void sunxi_tpadc_clear_pending(void __iomem *reg_base, int reg_val)
|
|
{
|
|
writel(reg_val, reg_base + TP_INT_FIFOS_REG);
|
|
}
|
|
|
|
static u32 sunxi_tpadc_data_read(void __iomem *reg_base)
|
|
{
|
|
return readl(reg_base + TP_DATA_REG);
|
|
}
|
|
|
|
static u32 sunxi_tpadc_clk_config(void __iomem *reg_base, u32 clk_in, u32 sclk_freq)
|
|
{
|
|
u32 freq, divider = sclk_freq / clk_in;
|
|
u32 val;
|
|
|
|
if (divider > 3 && divider < 6)
|
|
divider = 3;
|
|
if (divider > 6)
|
|
divider = 6;
|
|
|
|
switch (divider) {
|
|
case 1:
|
|
val = 0x03;
|
|
break;
|
|
case 2:
|
|
val = 0x00;
|
|
break;
|
|
case 3:
|
|
val = 0x01;
|
|
break;
|
|
case 6:
|
|
val = 0x02;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
sunxi_tpadc_clk_division(reg_base, val);
|
|
freq = sclk_freq / divider;
|
|
|
|
return freq;
|
|
}
|
|
static u32 sunxi_tpadc_fs_config(void __iomem *reg_base, u32 clk_in, u32 fs_div)
|
|
{
|
|
u32 sample_freq;
|
|
|
|
sunxi_tpadc_fs_set(reg_base, fs_div);
|
|
sample_freq = clk_in / (1 << (20 - fs_div));
|
|
|
|
return sample_freq;
|
|
}
|
|
|
|
static int sunxi_tpadc_hw_init(struct sunxi_tpadc *sunxi_tpadc)
|
|
{
|
|
u32 clk_in;
|
|
u32 sample_freq;
|
|
|
|
sunxi_tpadc_clk_select(sunxi_tpadc->reg_base, HOSC);
|
|
clk_in = sunxi_tpadc_clk_config(sunxi_tpadc->reg_base, CLK_IN, 24000000);
|
|
sunxi_tpadc_fs_config(sunxi_tpadc->reg_base, clk_in, 0x06);
|
|
sunxi_tpadc_ta_set(sunxi_tpadc->reg_base, 0xff);
|
|
sunxi_tpadc_select_adc(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_ch_select(sunxi_tpadc->reg_base, 0);
|
|
sunxi_tpadc_ch_select(sunxi_tpadc->reg_base, 2);
|
|
sunxi_tpadc_enable_irq(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_filter_enable(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_filter_type(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_trig_level(sunxi_tpadc->reg_base, 0x03);
|
|
sunxi_tpadc_downup_irq(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_rtp_enable(sunxi_tpadc->reg_base);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static irqreturn_t sunxi_tpadc_interrupt(int irqno, void *dev_id)
|
|
{
|
|
struct sunxi_tpadc *sunxi_tpadc = (struct sunxi_tpadc *)dev_id;
|
|
u32 reg_val;
|
|
u32 data;
|
|
|
|
reg_val = sunxi_tpadc_irq_status(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_clear_pending(sunxi_tpadc->reg_base, reg_val);
|
|
|
|
if (reg_val & SUNXI_FIFO_DATA) {
|
|
data = sunxi_tpadc_data_read(sunxi_tpadc->reg_base);
|
|
input_event(sunxi_tpadc->input_channel0, EV_MSC, MSC_SCAN, data);
|
|
input_sync(sunxi_tpadc->input_channel0);
|
|
|
|
data = sunxi_tpadc_data_read(sunxi_tpadc->reg_base);
|
|
input_event(sunxi_tpadc->input_channel2, EV_MSC, MSC_SCAN, data);
|
|
input_sync(sunxi_tpadc->input_channel2);
|
|
}
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
static int sunxi_input_register_channel0(struct sunxi_tpadc *sunxi_tpadc)
|
|
{
|
|
static struct input_dev *input_dev;
|
|
int ret;
|
|
|
|
input_dev = input_allocate_device();
|
|
if (!input_dev) {
|
|
pr_err("input_dev: not enough memory for input device\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
input_dev->name = "sunxitpadc_channel0";
|
|
input_dev->phys = "sunxitpadc/input0";
|
|
input_dev->id.bustype = BUS_HOST;
|
|
input_dev->id.vendor = 0x0001;
|
|
input_dev->id.product = 0x0001;
|
|
input_dev->id.version = 0x0100;
|
|
|
|
input_dev->evbit[0] = BIT_MASK(EV_MSC);
|
|
set_bit(EV_MSC, input_dev->evbit);
|
|
set_bit(MSC_SCAN, input_dev->mscbit);
|
|
sunxi_tpadc->input_channel0 = input_dev;
|
|
|
|
ret = input_register_device(sunxi_tpadc->input_channel0);
|
|
if (ret) {
|
|
pr_err("input register fail\n");
|
|
goto fail;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail:
|
|
input_unregister_device(sunxi_tpadc->input_channel0);
|
|
input_free_device(sunxi_tpadc->input_channel0);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int sunxi_input_register_channel2(struct sunxi_tpadc *sunxi_tpadc)
|
|
{
|
|
static struct input_dev *input_dev;
|
|
int ret;
|
|
|
|
input_dev = input_allocate_device();
|
|
if (!input_dev) {
|
|
pr_err("input_dev: not enough memory for input device\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
input_dev->name = "sunxitpadc_channel2";
|
|
input_dev->phys = "sunxitpadc/input0";
|
|
input_dev->id.bustype = BUS_HOST;
|
|
input_dev->id.vendor = 0x0001;
|
|
input_dev->id.product = 0x0001;
|
|
input_dev->id.version = 0x0100;
|
|
|
|
input_dev->evbit[0] = BIT_MASK(EV_MSC);
|
|
set_bit(EV_MSC, input_dev->evbit);
|
|
set_bit(MSC_SCAN, input_dev->mscbit);
|
|
sunxi_tpadc->input_channel2 = input_dev;
|
|
|
|
ret = input_register_device(sunxi_tpadc->input_channel2);
|
|
if (ret) {
|
|
pr_err("input register fail\n");
|
|
goto fail;
|
|
}
|
|
|
|
return 0;
|
|
|
|
fail:
|
|
input_unregister_device(sunxi_tpadc->input_channel2);
|
|
input_free_device(sunxi_tpadc->input_channel2);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
int sunxi_tpadc_probe(struct platform_device *pdev)
|
|
{
|
|
struct device_node *np = pdev->dev.of_node;
|
|
struct sunxi_tpadc *sunxi_tpadc;
|
|
int ret = 0;
|
|
|
|
if (!of_device_is_available(np)) {
|
|
pr_err("%s: sunxi tpadc is disable\n", __func__);
|
|
return -EPERM;
|
|
}
|
|
|
|
sunxi_tpadc = kzalloc(sizeof(struct sunxi_tpadc), GFP_KERNEL);
|
|
if (IS_ERR_OR_NULL(sunxi_tpadc)) {
|
|
pr_err("not enough memory for sunxi_tpadc\n");
|
|
return -ENOMEM;
|
|
}
|
|
|
|
sunxi_tpadc->reg_base = of_iomap(np, 0);
|
|
if (NULL == sunxi_tpadc->reg_base) {
|
|
pr_err("sunxi_tpadc iomap fail\n");
|
|
ret = -EBUSY;
|
|
goto fail1;
|
|
}
|
|
|
|
sunxi_tpadc->irq_num = irq_of_parse_and_map(np, 0);
|
|
if (0 == sunxi_tpadc->irq_num) {
|
|
pr_err("sunxi_tpadc fail to map irq\n");
|
|
ret = -EBUSY;
|
|
goto fail2;
|
|
}
|
|
|
|
sunxi_tpadc_hw_init(sunxi_tpadc);
|
|
|
|
sunxi_input_register_channel0(sunxi_tpadc);
|
|
sunxi_input_register_channel2(sunxi_tpadc);
|
|
|
|
if (request_irq(sunxi_tpadc->irq_num, sunxi_tpadc_interrupt,
|
|
IRQF_TRIGGER_NONE, "sunxi-tpadc", sunxi_tpadc)) {
|
|
pr_err("sunxi_tpadc request irq failure\n");
|
|
return -1;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, sunxi_tpadc);
|
|
|
|
return 0;
|
|
|
|
fail2:
|
|
iounmap(sunxi_tpadc->reg_base);
|
|
fail1:
|
|
kfree(sunxi_tpadc);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int sunxi_tpadc_remove(struct platform_device *pdev)
|
|
{
|
|
struct sunxi_tpadc *sunxi_tpadc = platform_get_drvdata(pdev);
|
|
|
|
sunxi_tpadc_disable_irq(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_ch_deselect(sunxi_tpadc->reg_base, 0);
|
|
sunxi_tpadc_ch_deselect(sunxi_tpadc->reg_base, 2);
|
|
free_irq(sunxi_tpadc->irq_num, sunxi_tpadc);
|
|
iounmap(sunxi_tpadc->reg_base);
|
|
input_unregister_device(sunxi_tpadc->input_channel0);
|
|
input_free_device(sunxi_tpadc->input_channel0);
|
|
input_unregister_device(sunxi_tpadc->input_channel2);
|
|
input_free_device(sunxi_tpadc->input_channel2);
|
|
kfree(sunxi_tpadc);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int sunxi_tpadc_suspend(struct device *dev)
|
|
{
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
struct sunxi_tpadc *sunxi_tpadc = platform_get_drvdata(pdev);
|
|
|
|
sunxi_tpadc_disable_irq(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_ch_deselect(sunxi_tpadc->reg_base, 0);
|
|
sunxi_tpadc_ch_deselect(sunxi_tpadc->reg_base, 2);
|
|
disable_irq_nosync(sunxi_tpadc->irq_num);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int sunxi_tpadc_resume(struct device *dev)
|
|
{
|
|
struct platform_device *pdev = to_platform_device(dev);
|
|
struct sunxi_tpadc *sunxi_tpadc = platform_get_drvdata(pdev);
|
|
|
|
sunxi_tpadc_enable_irq(sunxi_tpadc->reg_base);
|
|
sunxi_tpadc_ch_select(sunxi_tpadc->reg_base, 0);
|
|
sunxi_tpadc_ch_select(sunxi_tpadc->reg_base, 2);
|
|
enable_irq(sunxi_tpadc->irq_num);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct dev_pm_ops sunxi_tpadc_dev_pm_ops = {
|
|
.suspend = sunxi_tpadc_suspend,
|
|
.resume = sunxi_tpadc_resume,
|
|
};
|
|
|
|
#define SUNXI_TPADC_DEV_PM_OPS (&sunxi_tpadc_dev_pm_ops)
|
|
#else
|
|
#define SUNXI_TPADC_DEV_PM_OPS NULL
|
|
#endif
|
|
|
|
static struct of_device_id sunxi_tpadc_of_match[] = {
|
|
{ .compatible = "allwinner,sunxi-tpadc"},
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(of, sunxi_tpadc_of_match);
|
|
|
|
static struct platform_driver sunxi_tpadc_driver = {
|
|
.probe = sunxi_tpadc_probe,
|
|
.remove = sunxi_tpadc_remove,
|
|
.driver = {
|
|
.name = "sunxi-tpadc",
|
|
.owner = THIS_MODULE,
|
|
.pm = SUNXI_TPADC_DEV_PM_OPS,
|
|
.of_match_table = of_match_ptr(sunxi_tpadc_of_match),
|
|
},
|
|
};
|
|
module_platform_driver(sunxi_tpadc_driver);
|
|
|
|
MODULE_AUTHOR("yangshounan");
|
|
MODULE_DESCRIPTION("sunxi-tpadc driver");
|
|
MODULE_LICENSE("GPL");
|