/*
* Driver for the PN544 NFC chip.
*
* Copyright (C) Nokia Corporation
*
* Author: Jari Vanhala <ext-jari.vanhala@nokia.com>
* Contact: Matti Aaltonen <matti.j.aaltonen@nokia.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation.
*
* 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <linux/completion.h>
#include <linux/crc-ccitt.h>
#include <linux/delay.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/nfc/pn544.h>
#include <linux/poll.h>
#include <linux/regulator/consumer.h>
#include <linux/serial_core.h> /* for TCGETS */
#include <linux/slab.h>
#define DRIVER_CARD "PN544 NFC"
#define DRIVER_DESC "NFC driver for PN544"
static struct i2c_device_id pn544_id_table[] = {
{ PN544_DRIVER_NAME, 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, pn544_id_table);
#define HCI_MODE 0
#define FW_MODE 1
enum pn544_state {
PN544_ST_COLD,
PN544_ST_FW_READY,
PN544_ST_READY,
};
enum pn544_irq {
PN544_NONE,
PN544_INT,
};
struct pn544_info {
struct miscdevice miscdev;
struct i2c_client *i2c_dev;
struct regulator_bulk_data regs[2];
enum pn544_state state;
wait_queue_head_t read_wait;
loff_t read_offset;
enum pn544_irq read_irq;
struct mutex read_mutex; /* Serialize read_irq access */
struct mutex mutex; /* Serialize info struct access */
u8 *buf;
size_t buflen;
};
static const char reg_vdd_io[] = "Vdd_IO";
static const char reg_vbat[] = "VBat";
/* sysfs interface */
static ssize_t pn544_test(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct pn544_info *info = dev_get_drvdata(dev);
struct i2c_client *client = info->i2c_dev;
struct pn544_nfc_platform_data *pdata = client->dev.platform_data;
return snprintf(buf, PAGE_SIZE, "%d\n", pdata->test());
}
static int pn544_enable(struct pn544_info *info, int mode)
{
struct pn544_nfc_platform_data *pdata;
struct i2c_client *client = info->i2c_dev;
int r;
r = regulator_bulk_enable(ARRAY_SIZE(info->regs), info->regs);
if (r < 0)
return r;
pdata = client->dev.platform_data;
info->read_irq = PN544_NONE;
if (pdata->enable)
pdata->enable(mode);
if (mode) {
info->state = PN544_ST_FW_READY;
dev_dbg(&client->dev, "now in FW-mode\n");
} else {
info->state = PN544_ST_READY;
dev_dbg(&client->dev, "now in HCI-mode\n");
}
usleep_range(10000, 15000);
return 0;
}
static void pn544_disable(struct pn544_info *info)
{
struct pn544_nfc_platform_data *pdata;
struct i2c_client *client = info->i2c_dev;
pdata = client->dev.platform_data;
if (pdata->disable)
pdata->disable();
info->state = PN544_ST_COLD;
dev_dbg(&client->dev, "Now in OFF-mode\n");
msleep(PN544_RESETVEN_TIME);
info->read_irq = PN544_NONE;
regulator_bulk_disable(ARRAY_SIZE(info->regs), info->regs);
}
static int check_crc(u8 *buf, int buflen)
{
u8 len;
u16 crc;
len = buf[0] + 1;
if (len < 4 || len != buflen || len > PN544_MSG_MAX_SIZE) {
pr_err(PN544_DRIVER_NAME
": CRC; corrupt packet len %u (%d)\n", len, buflen);
print_hex_dump(KERN_DEBUG, "crc: ", DUMP_PREFIX_NONE,
16, 2, buf, buflen, false);
return -EPERM;
}
crc = crc_ccitt(0xffff, buf, len - 2);
crc = ~crc;
if (buf[len-2] != (crc & 0xff) || buf[len-1] != (crc >> 8)) {
pr_err(PN544_DRIVER_NAME ": CRC error 0x%x != 0x%x 0x%x\n",
crc, buf[len-1], buf[len-2]);
print_hex_dump(KERN_DEBUG, "crc: ", DUMP_PREFIX_NONE,
16, 2, buf, buflen, false);
return -EPERM;
}
return 0;
}
static int pn544_i2c_write(struct i2c_client *client, u8 *buf, int len)
{
int r;
if (len < 4 || len != (buf[0] + 1)) {
dev_err(&client->dev, "%s: Illegal message length: %d\n",
__func__, len);
return -EINVAL;