Commit dcc21cc0 authored by Linus Walleij's avatar Linus Walleij Committed by Lee Jones

mfd: Add driver for Atmel Microcontroller on iPaq h3xxx

This adds a driver for the Atmel Microcontroller found on the
iPAQ h3xxx series. This device handles some keys, the
touchscreen, and the battery monitoring.

This is a port of a driver from handhelds.org 2.6.21 kernel,
written by Alessandro Gardich based on Andrew Christians
original HAL-driver. It has been heavily cleaned and
converted to mfd-core by Dmitry Artamonow and rewritten
again for the v3.x series kernels by Linus Walleij,
bringing back some of the functionality lost from Andrew's
original driver.
Acked-by: default avatarGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Signed-off-by: default avatarAlessandro Gardich <gremlin@gremlin.it>
Signed-off-by: default avatarDmitry Artamonow <mad_soft@inbox.ru>
Signed-off-by: default avatarLinus Walleij <linus.walleij@linaro.org>
Signed-off-by: default avatarLee Jones <lee.jones@linaro.org>
parent 3d237990
...@@ -262,6 +262,16 @@ config MFD_INTEL_MSIC ...@@ -262,6 +262,16 @@ config MFD_INTEL_MSIC
Passage) chip. This chip embeds audio, battery, GPIO, etc. Passage) chip. This chip embeds audio, battery, GPIO, etc.
devices used in Intel Medfield platforms. devices used in Intel Medfield platforms.
config MFD_IPAQ_MICRO
bool "Atmel Micro ASIC (iPAQ h3100/h3600/h3700) Support"
depends on SA1100_H3100 || SA1100_H3600
select MFD_CORE
help
Select this to get support for the Microcontroller found in
the Compaq iPAQ handheld computers. This is an Atmel
AT90LS8535 microcontroller flashed with a special iPAQ
firmware using the custom protocol implemented in this driver.
config MFD_JANZ_CMODIO config MFD_JANZ_CMODIO
tristate "Janz CMOD-IO PCI MODULbus Carrier Board" tristate "Janz CMOD-IO PCI MODULbus Carrier Board"
select MFD_CORE select MFD_CORE
......
...@@ -168,3 +168,4 @@ obj-$(CONFIG_MFD_RETU) += retu-mfd.o ...@@ -168,3 +168,4 @@ obj-$(CONFIG_MFD_RETU) += retu-mfd.o
obj-$(CONFIG_MFD_AS3711) += as3711.o obj-$(CONFIG_MFD_AS3711) += as3711.o
obj-$(CONFIG_MFD_AS3722) += as3722.o obj-$(CONFIG_MFD_AS3722) += as3722.o
obj-$(CONFIG_MFD_STW481X) += stw481x.o obj-$(CONFIG_MFD_STW481X) += stw481x.o
obj-$(CONFIG_MFD_IPAQ_MICRO) += ipaq-micro.o
/*
* Compaq iPAQ h3xxx Atmel microcontroller companion support
*
* This is an Atmel AT90LS8535 with a special flashed-in firmware that
* implements the special protocol used by this driver.
*
* based on previous kernel 2.4 version by Andrew Christian
* Author : Alessandro Gardich <gremlin@gremlin.it>
* Author : Dmitry Artamonow <mad_soft@inbox.ru>
* Author : Linus Walleij <linus.walleij@linaro.org>
*
* 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.
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/pm.h>
#include <linux/delay.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/mfd/core.h>
#include <linux/mfd/ipaq-micro.h>
#include <linux/string.h>
#include <linux/random.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <mach/hardware.h>
static void ipaq_micro_trigger_tx(struct ipaq_micro *micro)
{
struct ipaq_micro_txdev *tx = &micro->tx;
struct ipaq_micro_msg *msg = micro->msg;
int i, bp;
u8 checksum;
u32 val;
bp = 0;
tx->buf[bp++] = CHAR_SOF;
checksum = ((msg->id & 0x0f) << 4) | (msg->tx_len & 0x0f);
tx->buf[bp++] = checksum;
for (i = 0; i < msg->tx_len; i++) {
tx->buf[bp++] = msg->tx_data[i];
checksum += msg->tx_data[i];
}
tx->buf[bp++] = checksum;
tx->len = bp;
tx->index = 0;
print_hex_dump(KERN_DEBUG, "data: ", DUMP_PREFIX_OFFSET, 16, 1,
tx->buf, tx->len, true);
/* Enable interrupt */
val = readl(micro->base + UTCR3);
val |= UTCR3_TIE;
writel(val, micro->base + UTCR3);
}
int ipaq_micro_tx_msg(struct ipaq_micro *micro, struct ipaq_micro_msg *msg)
{
unsigned long flags;
dev_dbg(micro->dev, "TX msg: %02x, %d bytes\n", msg->id, msg->tx_len);
spin_lock_irqsave(&micro->lock, flags);
if (micro->msg) {
list_add_tail(&msg->node, &micro->queue);
spin_unlock_irqrestore(&micro->lock, flags);
return 0;
}
micro->msg = msg;
ipaq_micro_trigger_tx(micro);
spin_unlock_irqrestore(&micro->lock, flags);
return 0;
}
EXPORT_SYMBOL(ipaq_micro_tx_msg);
static void micro_rx_msg(struct ipaq_micro *micro, u8 id, int len, u8 *data)
{
int i;
dev_dbg(micro->dev, "RX msg: %02x, %d bytes\n", id, len);
spin_lock(&micro->lock);
switch (id) {
case MSG_VERSION:
case MSG_EEPROM_READ:
case MSG_EEPROM_WRITE:
case MSG_BACKLIGHT:
case MSG_NOTIFY_LED:
case MSG_THERMAL_SENSOR:
case MSG_BATTERY:
/* Handle synchronous messages */
if (micro->msg && micro->msg->id == id) {
struct ipaq_micro_msg *msg = micro->msg;
memcpy(msg->rx_data, data, len);
msg->rx_len = len;
complete(&micro->msg->ack);
if (!list_empty(&micro->queue)) {
micro->msg = list_entry(micro->queue.next,
struct ipaq_micro_msg,
node);
list_del_init(&micro->msg->node);
ipaq_micro_trigger_tx(micro);
} else
micro->msg = NULL;
dev_dbg(micro->dev, "OK RX message 0x%02x\n", id);
} else {
dev_err(micro->dev,
"out of band RX message 0x%02x\n", id);
if(!micro->msg)
dev_info(micro->dev, "no message queued\n");
else
dev_info(micro->dev, "expected message %02x\n",
micro->msg->id);
}
break;
case MSG_KEYBOARD:
if (micro->key)
micro->key(micro->key_data, len, data);
else
dev_dbg(micro->dev, "key message ignored, no handle \n");
break;
case MSG_TOUCHSCREEN:
if (micro->ts)
micro->ts(micro->ts_data, len, data);
else
dev_dbg(micro->dev, "touchscreen message ignored, no handle \n");
break;
default:
dev_err(micro->dev,
"unknown msg %d [%d] ", id, len);
for (i = 0; i < len; ++i)
pr_cont("0x%02x ", data[i]);
pr_cont("\n");
}
spin_unlock(&micro->lock);
}
static void micro_process_char(struct ipaq_micro *micro, u8 ch)
{
struct ipaq_micro_rxdev *rx = &micro->rx;
switch (rx->state) {
case STATE_SOF: /* Looking for SOF */
if (ch == CHAR_SOF)
rx->state = STATE_ID; /* Next byte is the id and len */
break;
case STATE_ID: /* Looking for id and len byte */
rx->id = (ch & 0xf0) >> 4 ;
rx->len = (ch & 0x0f);
rx->index = 0;
rx->chksum = ch;
rx->state = (rx->len > 0) ? STATE_DATA : STATE_CHKSUM;
break;
case STATE_DATA: /* Looking for 'len' data bytes */
rx->chksum += ch;
rx->buf[rx->index] = ch;
if (++rx->index == rx->len)
rx->state = STATE_CHKSUM;
break;
case STATE_CHKSUM: /* Looking for the checksum */
if (ch == rx->chksum)
micro_rx_msg(micro, rx->id, rx->len, rx->buf);
rx->state = STATE_SOF;
break;
}
}
static void micro_rx_chars(struct ipaq_micro *micro)
{
u32 status, ch;
while ((status = readl(micro->base + UTSR1)) & UTSR1_RNE) {
ch = readl(micro->base + UTDR);
if (status & UTSR1_PRE)
dev_err(micro->dev, "rx: parity error\n");
else if (status & UTSR1_FRE)
dev_err(micro->dev, "rx: framing error\n");
else if (status & UTSR1_ROR)
dev_err(micro->dev, "rx: overrun error\n");
micro_process_char(micro, ch);
}
}
static void ipaq_micro_get_version(struct ipaq_micro *micro)
{
struct ipaq_micro_msg msg = {
.id = MSG_VERSION,
};
ipaq_micro_tx_msg_sync(micro, &msg);
if (msg.rx_len == 4) {
memcpy(micro->version, msg.rx_data, 4);
micro->version[4] = '\0';
} else if (msg.rx_len == 9) {
memcpy(micro->version, msg.rx_data, 4);
micro->version[4] = '\0';
/* Bytes 4-7 are "pack", byte 8 is "boot type" */
} else {
dev_err(micro->dev,
"illegal version message %d bytes\n", msg.rx_len);
}
}
static void ipaq_micro_eeprom_read(struct ipaq_micro *micro,
u8 address, u8 len, u8 *data)
{
struct ipaq_micro_msg msg = {
.id = MSG_EEPROM_READ,
};
u8 i;
for (i = 0; i < len; i++) {
msg.tx_data[0] = address + i;
msg.tx_data[1] = 1;
msg.tx_len = 2;
ipaq_micro_tx_msg_sync(micro, &msg);
memcpy(data + (i * 2), msg.rx_data, 2);
}
}
static char *ipaq_micro_str(u8 *wchar, u8 len)
{
char retstr[256];
u8 i;
for (i = 0; i < len / 2; i++)
retstr[i] = wchar[i * 2];
return kstrdup(retstr, GFP_KERNEL);
}
static u16 ipaq_micro_to_u16(u8 *data)
{
return data[1] << 8 | data[0];
}
static void ipaq_micro_eeprom_dump(struct ipaq_micro *micro)
{
u8 dump[256];
char *str;
ipaq_micro_eeprom_read(micro, 0, 128, dump);
str = ipaq_micro_str(dump, 10);
if (str) {
dev_info(micro->dev, "HM version %s\n", str);
kfree(str);
}
str = ipaq_micro_str(dump+10, 40);
if (str) {
dev_info(micro->dev, "serial number: %s\n", str);
/* Feed the random pool with this */
add_device_randomness(str, strlen(str));
kfree(str);
}
str = ipaq_micro_str(dump+50, 20);
if (str) {
dev_info(micro->dev, "module ID: %s\n", str);
kfree(str);
}
str = ipaq_micro_str(dump+70, 10);
if (str) {
dev_info(micro->dev, "product revision: %s\n", str);
kfree(str);
}
dev_info(micro->dev, "product ID: %u\n", ipaq_micro_to_u16(dump+80));
dev_info(micro->dev, "frame rate: %u fps\n",
ipaq_micro_to_u16(dump+82));
dev_info(micro->dev, "page mode: %u\n", ipaq_micro_to_u16(dump+84));
dev_info(micro->dev, "country ID: %u\n", ipaq_micro_to_u16(dump+86));
dev_info(micro->dev, "color display: %s\n",
ipaq_micro_to_u16(dump+88) ? "yes" : "no");
dev_info(micro->dev, "ROM size: %u MiB\n", ipaq_micro_to_u16(dump+90));
dev_info(micro->dev, "RAM size: %u KiB\n", ipaq_micro_to_u16(dump+92));
dev_info(micro->dev, "screen: %u x %u\n",
ipaq_micro_to_u16(dump+94), ipaq_micro_to_u16(dump+96));
print_hex_dump(KERN_DEBUG, "eeprom: ", DUMP_PREFIX_OFFSET, 16, 1,
dump, 256, true);
}
static void micro_tx_chars(struct ipaq_micro *micro)
{
struct ipaq_micro_txdev *tx = &micro->tx;
u32 val;
while ((tx->index < tx->len) &&
(readl(micro->base + UTSR1) & UTSR1_TNF)) {
writel(tx->buf[tx->index], micro->base + UTDR);
tx->index++;
}
/* Stop interrupts */
val = readl(micro->base + UTCR3);
val &= ~UTCR3_TIE;
writel(val, micro->base + UTCR3);
}
static void micro_reset_comm(struct ipaq_micro *micro)
{
struct ipaq_micro_rxdev *rx = &micro->rx;
u32 val;
if (micro->msg)
complete(&micro->msg->ack);
/* Initialize Serial channel protocol frame */
rx->state = STATE_SOF; /* Reset the state machine */
/* Set up interrupts */
writel(0x01, micro->sdlc + 0x0); /* Select UART mode */
/* Clean up CR3 */
writel(0x0, micro->base + UTCR3);
/* Format: 8N1 */
writel(UTCR0_8BitData | UTCR0_1StpBit, micro->base + UTCR0);
/* Baud rate: 115200 */
writel(0x0, micro->base + UTCR1);
writel(0x1, micro->base + UTCR2);
/* Clear SR0 */
writel(0xff, micro->base + UTSR0);
/* Enable RX int, disable TX int */
writel(UTCR3_TXE | UTCR3_RXE | UTCR3_RIE, micro->base + UTCR3);
val = readl(micro->base + UTCR3);
val &= ~UTCR3_TIE;
writel(val, micro->base + UTCR3);
}
static irqreturn_t micro_serial_isr(int irq, void *dev_id)
{
struct ipaq_micro *micro = dev_id;
struct ipaq_micro_txdev *tx = &micro->tx;
u32 status;
status = readl(micro->base + UTSR0);
do {
if (status & (UTSR0_RID | UTSR0_RFS)) {
if (status & UTSR0_RID)
/* Clear the Receiver IDLE bit */
writel(UTSR0_RID, micro->base + UTSR0);
micro_rx_chars(micro);
}
/* Clear break bits */
if (status & (UTSR0_RBB | UTSR0_REB))
writel(status & (UTSR0_RBB | UTSR0_REB),
micro->base + UTSR0);
if (status & UTSR0_TFS)
micro_tx_chars(micro);
status = readl(micro->base + UTSR0);
} while (((tx->index < tx->len) && (status & UTSR0_TFS)) ||
(status & (UTSR0_RFS | UTSR0_RID)));
return IRQ_HANDLED;
}
static struct mfd_cell micro_cells[] = {
{ .name = "ipaq-micro-backlight", },
{ .name = "ipaq-micro-battery", },
{ .name = "ipaq-micro-keys", },
{ .name = "ipaq-micro-ts", },
{ .name = "ipaq-micro-leds", },
};
static int micro_resume(struct device *dev)
{
struct ipaq_micro *micro = dev_get_drvdata(dev);
micro_reset_comm(micro);
mdelay(10);
return 0;
}
static int micro_probe(struct platform_device *pdev)
{
struct ipaq_micro *micro;
struct resource *res;
int ret;
int irq;
micro = devm_kzalloc(&pdev->dev, sizeof(*micro), GFP_KERNEL);
if (!micro)
return -ENOMEM;
micro->dev = &pdev->dev;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -EINVAL;
micro->base = devm_request_and_ioremap(&pdev->dev, res);
if (!micro->base)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (!res)
return -EINVAL;
micro->sdlc = devm_request_and_ioremap(&pdev->dev, res);
if (!micro->sdlc)
return -ENOMEM;
micro_reset_comm(micro);
irq = platform_get_irq(pdev, 0);
if (!irq)
return -EINVAL;
ret = devm_request_irq(&pdev->dev, irq, micro_serial_isr,
IRQF_SHARED, "ipaq-micro",
micro);
if (ret) {
dev_err(&pdev->dev, "unable to grab serial port IRQ\n");
return ret;
} else
dev_info(&pdev->dev, "grabbed serial port IRQ\n");
spin_lock_init(&micro->lock);
INIT_LIST_HEAD(&micro->queue);
platform_set_drvdata(pdev, micro);
ret = mfd_add_devices(&pdev->dev, pdev->id, micro_cells,
ARRAY_SIZE(micro_cells), NULL, 0, NULL);
if (ret) {
dev_err(&pdev->dev, "error adding MFD cells");
return ret;
}
/* Check version */
ipaq_micro_get_version(micro);
dev_info(&pdev->dev, "Atmel micro ASIC version %s\n", micro->version);
ipaq_micro_eeprom_dump(micro);
return 0;
}
static int micro_remove(struct platform_device *pdev)
{
struct ipaq_micro *micro = platform_get_drvdata(pdev);
u32 val;
mfd_remove_devices(&pdev->dev);
val = readl(micro->base + UTCR3);
val &= ~(UTCR3_RXE | UTCR3_RIE); /* disable receive interrupt */
val &= ~(UTCR3_TXE | UTCR3_TIE); /* disable transmit interrupt */
writel(val, micro->base + UTCR3);
return 0;
}
static const struct dev_pm_ops micro_dev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(NULL, micro_resume)
};
static struct platform_driver micro_device_driver = {
.driver = {
.name = "ipaq-h3xxx-micro",
.pm = &micro_dev_pm_ops,
},
.probe = micro_probe,
.remove = micro_remove,
/* .shutdown = micro_suspend, // FIXME */
};
module_platform_driver(micro_device_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("driver for iPAQ Atmel micro core and backlight");
/*
* Header file for the compaq Micro MFD
*/
#ifndef _MFD_IPAQ_MICRO_H_
#define _MFD_IPAQ_MICRO_H_
#include <linux/spinlock.h>
#include <linux/completion.h>
#include <linux/list.h>
#define TX_BUF_SIZE 32
#define RX_BUF_SIZE 16
#define CHAR_SOF 0x02
/*
* These are the different messages that can be sent to the microcontroller
* to control various aspects.
*/
#define MSG_VERSION 0x0
#define MSG_KEYBOARD 0x2
#define MSG_TOUCHSCREEN 0x3
#define MSG_EEPROM_READ 0x4
#define MSG_EEPROM_WRITE 0x5
#define MSG_THERMAL_SENSOR 0x6
#define MSG_NOTIFY_LED 0x8
#define MSG_BATTERY 0x9
#define MSG_SPI_READ 0xb
#define MSG_SPI_WRITE 0xc
#define MSG_BACKLIGHT 0xd /* H3600 only */
#define MSG_CODEC_CTRL 0xe /* H3100 only */
#define MSG_DISPLAY_CTRL 0xf /* H3100 only */
/* state of receiver parser */
enum rx_state {
STATE_SOF = 0, /* Next byte should be start of frame */
STATE_ID, /* Next byte is ID & message length */
STATE_DATA, /* Next byte is a data byte */
STATE_CHKSUM /* Next byte should be checksum */
};
/**
* struct ipaq_micro_txdev - TX state
* @len: length of message in TX buffer
* @index: current index into TX buffer
* @buf: TX buffer
*/
struct ipaq_micro_txdev {
u8 len;
u8 index;
u8 buf[TX_BUF_SIZE];
};
/**
* struct ipaq_micro_rxdev - RX state
* @state: context of RX state machine
* @chksum: calculated checksum
* @id: message ID from packet
* @len: RX buffer length
* @index: RX buffer index
* @buf: RX buffer
*/
struct ipaq_micro_rxdev {
enum rx_state state;
unsigned char chksum;
u8 id;
unsigned int len;
unsigned int index;
u8 buf[RX_BUF_SIZE];
};
/**
* struct ipaq_micro_msg - message to the iPAQ microcontroller
* @id: 4-bit ID of the message
* @tx_len: length of TX data
* @tx_data: TX data to send
* @rx_len: length of receieved RX data
* @rx_data: RX data to recieve
* @ack: a completion that will be completed when RX is complete
* @node: list node if message gets queued
*/
struct ipaq_micro_msg {
u8 id;
u8 tx_len;
u8 tx_data[TX_BUF_SIZE];
u8 rx_len;
u8 rx_data[RX_BUF_SIZE];
struct completion ack;
struct list_head node;
};
/**
* struct ipaq_micro - iPAQ microcontroller state
* @dev: corresponding platform device
* @base: virtual memory base for underlying serial device
* @sdlc: virtual memory base for Synchronous Data Link Controller
* @version: version string
* @tx: TX state
* @rx: RX state
* @lock: lock for this state container
* @msg: current message
* @queue: message queue
* @key: callback for asynchronous key events
* @key_data: data to pass along with key events
* @ts: callback for asynchronous touchscreen events
* @ts_data: data to pass along with key events
*/
struct ipaq_micro {
struct device *dev;
void __iomem *base;
void __iomem *sdlc;
char version[5];
struct ipaq_micro_txdev tx; /* transmit ISR state */
struct ipaq_micro_rxdev rx; /* receive ISR state */
spinlock_t lock;
struct ipaq_micro_msg *msg;
struct list_head queue;
void (*key) (void *data, int len, unsigned char *rxdata);
void *key_data;
void (*ts) (void *data, int len, unsigned char *rxdata);
void *ts_data;
};
extern int
ipaq_micro_tx_msg(struct ipaq_micro *micro, struct ipaq_micro_msg *msg);
static inline int
ipaq_micro_tx_msg_sync(struct ipaq_micro *micro,
struct ipaq_micro_msg *msg)
{
int ret;
init_completion(&msg->ack);
ret = ipaq_micro_tx_msg(micro, msg);
wait_for_completion(&msg->ack);
return ret;
}
static inline int
ipaq_micro_tx_msg_async(struct ipaq_micro *micro,
struct ipaq_micro_msg *msg)
{
init_completion(&msg->ack);
return ipaq_micro_tx_msg(micro, msg);
}
#endif /* _MFD_IPAQ_MICRO_H_ */
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment