CINXE.COM
LKML: Vitaly Wool: [PATCH] SPI: add support for PNX/SPI controller
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>LKML: Vitaly Wool: [PATCH] SPI: add support for PNX/SPI controller</title><link href="/css/message.css" rel="stylesheet" type="text/css" /><link href="/css/wrap.css" rel="alternate stylesheet" type="text/css" title="wrap" /><link href="/css/nowrap.css" rel="stylesheet" type="text/css" title="nowrap" /><link href="/favicon.ico" rel="shortcut icon" /><script src="/js/simple-calendar.js" type="text/javascript"></script><script src="/js/styleswitcher.js" type="text/javascript"></script><link rel="alternate" type="application/rss+xml" title="lkml.org : last 100 messages" href="/rss.php" /><link rel="alternate" type="application/rss+xml" title="lkml.org : last messages by Vitaly Wool" href="/groupie.php?aid=29396" /><!--Matomo--><script> var _paq = window._paq = window._paq || []; /* tracker methods like "setCustomDimension" should be called before "trackPageView" */ _paq.push(["setDoNotTrack", true]); _paq.push(["disableCookies"]); _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); (function() { var u="//m.lkml.org/"; _paq.push(['setTrackerUrl', u+'matomo.php']); _paq.push(['setSiteId', '1']); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s); })(); </script><!--End Matomo Code--></head><body onload="es.jasper.simpleCalendar.init();" itemscope="itemscope" itemtype="http://schema.org/BlogPosting"><table border="0" cellpadding="0" cellspacing="0"><tr><td width="180" align="center"><a href="/"><img style="border:0;width:135px;height:32px" src="/images/toprowlk.gif" alt="lkml.org" /></a></td><td width="32">聽</td><td class="nb"><div><a class="nb" href="/lkml"> [lkml]</a> 聽 <a class="nb" href="/lkml/2005"> [2005]</a> 聽 <a class="nb" href="/lkml/2005/12"> [Dec]</a> 聽 <a class="nb" href="/lkml/2005/12/23"> [23]</a> 聽 <a class="nb" href="/lkml/last100"> [last100]</a> 聽 <a href="/rss.php"><img src="/images/rss-or.gif" border="0" alt="RSS Feed" /></a></div><div>Views: <a href="#" class="nowrap" onclick="setActiveStyleSheet('wrap');return false;">[wrap]</a><a href="#" class="wrap" onclick="setActiveStyleSheet('nowrap');return false;">[no wrap]</a> 聽 <a class="nb" href="/lkml/mheaders/2005/12/23/57" onclick="this.href='/lkml/headers'+'/2005/12/23/57';">[headers]</a>聽 <a href="/lkml/bounce/2005/12/23/57">[forward]</a>聽 </div></td><td width="32">聽</td></tr><tr><td valign="top"><div class="es-jasper-simpleCalendar" baseurl="/lkml/"></div><div class="threadlist">Messages in this thread</div><ul class="threadlist"><li class="root"><a href="/lkml/2005/12/23/57">First message in thread</a></li><li class="origin"><a href="/lkml/2005/12/27/106">Vitaly Wool</a><ul><li><a href="/lkml/2005/12/27/106">David Brownell</a><ul><li><a href="/lkml/2005/12/27/122">Vitaly Wool</a></li></ul></li></ul></li></ul><div class="threadlist">Patch in this message</div><ul class="threadlist"><li><a href="/lkml/diff/2005/12/23/57/1">Get diff 1</a></li></ul></td><td width="32" rowspan="2" class="c" valign="top"><img src="/images/icornerl.gif" width="32" height="32" alt="/" /></td><td class="c" rowspan="2" valign="top" style="padding-top: 1em"><table><tr><td><table><tr><td class="lp">Date</td><td class="rp" itemprop="datePublished">Fri, 23 Dec 2005 17:15:06 +0300</td></tr><tr><td class="lp">From</td><td class="rp" itemprop="author">Vitaly Wool <></td></tr><tr><td class="lp">Subject</td><td class="rp" itemprop="name">[PATCH] SPI: add support for PNX/SPI controller</td></tr></table></td><td></td></tr></table><pre itemprop="articleBody">Hi,<br />inlined is a pretty much _development_ version of the Philips SPI controller support we're working with on some Philips ARM targets (PNX0106 and PNX4008, namely) based on spi_bitbang library from David Brownell. It's quite a notable thing that this buggy and in some way complicated SPI controller fits well in this bitbang framework! :)<br />This patch also contains dumb EEPROM driver for the onboard EEPROM behind the SPI bus. This driver was used by us for controller driver testing, mainly.<br /><br />Signed-off-by: Vitaly Wool <vwool@ru.mvista.com><br />Signed-off-by: Dmitry Pervusin <dpervushin@gmail.com><br /><br /> drivers/spi/Kconfig | 8<br /> drivers/spi/Makefile | 2<br /> drivers/spi/eeprom.c | 127 ++++++++++++<br /> drivers/spi/spi_pnx.c | 365 ++++++++++++++++++++++++++++++++++++<br /> drivers/spi/spi_pnx.h | 60 ++++++<br /> include/asm-arm/arch-pnx4008/spi.h | 370 +++++++++++++++++++++++++++++++++++++<br /> 6 files changed, 932 insertions(+)<br /><br />Index: linux-2.6/drivers/spi/Kconfig<br />===================================================================<br />--- linux-2.6.orig/drivers/spi/Kconfig<br />+++ linux-2.6/drivers/spi/Kconfig<br />@@ -95,6 +95,14 @@ config SPI_BUTTERFLY<br /> # Add new SPI master controllers in alphabetical order above this line<br /> #<br /> <br />+config SPI_PNX<br />+ tristate "PNX SPI bus support"<br />+ depends on ARCH_PNX4008 && SPI_BITBANG<br />+<br />+config SPI_EEPROM<br />+ tristate "SPI EEPROM"<br />+ depends on SPI<br />+<br /> <br /> #<br /> # There are lots of SPI device types, with sensors and memory<br />Index: linux-2.6/drivers/spi/Makefile<br />===================================================================<br />--- linux-2.6.orig/drivers/spi/Makefile<br />+++ linux-2.6/drivers/spi/Makefile<br />@@ -13,9 +13,11 @@ obj-$(CONFIG_SPI_MASTER) += spi.o<br /> # SPI master controller drivers (bus)<br /> obj-$(CONFIG_SPI_BITBANG) += spi_bitbang.o<br /> obj-$(CONFIG_SPI_BUTTERFLY) += spi_butterfly.o<br />+obj-$(CONFIG_SPI_PNX) += spi_pnx.o<br /> # ... add above this line ...<br /> <br /> # SPI protocol drivers (device/link on bus)<br />+obj-$(CONFIG_SPI_EEPROM) += eeprom.o<br /> # ... add above this line ...<br /> <br /> # SPI slave controller drivers (upstream link)<br />Index: linux-2.6/drivers/spi/eeprom.c<br />===================================================================<br />--- /dev/null<br />+++ linux-2.6/drivers/spi/eeprom.c<br />@@ -0,0 +1,127 @@<br />+#include <linux/kernel.h><br />+#include <linux/init.h><br />+#include <linux/module.h><br />+#include <linux/slab.h><br />+#include <linux/sched.h><br />+#include <linux/device.h><br />+#include <linux/spi/spi.h><br />+#include <linux/proc_fs.h><br />+#include <linux/ctype.h><br />+<br />+#define EEPROM_SIZE 256<br />+#define DRIVER_NAME "EEPROM"<br />+#define READ_BUFF_SIZE 160<br />+<br />+static int __init spi_eeprom_init(void);<br />+static void __exit spi_eeprom_cleanup(void);<br />+<br />+static int spiee_read_block (struct device *d, void *block)<br />+{<br />+ struct spi_device *spi = to_spi_device(d);<br />+ char cmd[2];<br />+ struct spi_transfer t[2] = {<br />+ [0] = {<br />+ .tx_buf = cmd,<br />+ .len = sizeof cmd,<br />+ },<br />+ [1] = {<br />+ .rx_buf = block,<br />+ .len = 256,<br />+ },<br />+ };<br />+ DECLARE_SPI_MESSAGE(m);<br />+<br />+ cmd[ 0 ] = 0x03;<br />+ cmd[ 1 ] = 0x00;<br />+ list_add_tail(&t[0].link, &m.transfers);<br />+ list_add_tail(&t[1].link, &m.transfers);<br />+<br />+ spi_sync(spi, &m);<br />+ return 256;<br />+}<br />+static ssize_t blk_show (struct device *d, struct device_attribute *attr, char *text )<br />+{<br />+ char *rdbuff = kmalloc (256, SLAB_KERNEL);<br />+ char line1[80],line2[80];<br />+ char item1[5], item2[5];<br />+ int bytes, i, x, blen;<br />+<br />+ blen = spiee_read_block (d, rdbuff);<br />+<br />+ bytes = 0;<br />+<br />+ strcpy(text, "");<br />+ for (i = 0; i < blen; i += 8) {<br />+ strcpy(line1, "");<br />+ strcpy(line2, "" );<br />+ for (x = i; x < i + 8; x++) {<br />+ if (x > blen) {<br />+ sprintf(item1, " ");<br />+ sprintf(item2, " " );<br />+ } else {<br />+ sprintf(item1, "%02x ", rdbuff[x]);<br />+ if (isprint(rdbuff[x])) {<br />+ sprintf(item2, "%c", rdbuff[x]);<br />+ } else {<br />+ sprintf(item2, ".");<br />+ }<br />+ }<br />+ strcat(line1, item1);<br />+ strcat(line2, item2);<br />+ }<br />+<br />+ strcat(text, line1);<br />+ strcat(text, "| " );<br />+ strcat(text, line2);<br />+ strcat(text, "\n" );<br />+<br />+ bytes += (strlen (line1 ) + strlen(line2) + 4);<br />+ }<br />+<br />+ kfree (rdbuff);<br />+<br />+ return bytes + 1;<br />+}<br />+<br />+static DEVICE_ATTR(blk, S_IRUGO, blk_show, NULL );<br />+<br />+<br />+static int spiee_probe(struct spi_device *this_dev)<br />+{<br />+ device_create_file(&this_dev->dev, &dev_attr_blk);<br />+ return 0;<br />+}<br />+<br />+static int spiee_remove(struct spi_device *this_dev)<br />+{<br />+ device_remove_file(&this_dev->dev, &dev_attr_blk);<br />+ return 0;<br />+}<br />+<br />+static struct spi_driver eeprom_driver = {<br />+ .driver = {<br />+ .name = DRIVER_NAME,<br />+ .bus = &spi_bus_type,<br />+ .owner = THIS_MODULE,<br />+ },<br />+ .probe = spiee_probe,<br />+ .remove = __devexit_p(spiee_remove),<br />+};<br />+<br />+static int __init spi_eeprom_init(void)<br />+{<br />+ int rc = spi_register_driver(&eeprom_driver);<br />+ printk("%s: %d\n", __FUNCTION__, rc);<br />+ return rc;<br />+}<br />+static void __exit spi_eeprom_cleanup(void)<br />+{<br />+ spi_unregister_driver(&eeprom_driver);<br />+}<br />+<br />+module_init(spi_eeprom_init);<br />+module_exit(spi_eeprom_cleanup);<br />+<br />+MODULE_LICENSE("GPL");<br />+MODULE_AUTHOR("dmitry pervushin <dpervushin@gmail.com>");<br />+MODULE_DESCRIPTION("SPI EEPROM driver");<br />Index: linux-2.6/drivers/spi/spi_pnx.c<br />===================================================================<br />--- /dev/null<br />+++ linux-2.6/drivers/spi/spi_pnx.c<br />@@ -0,0 +1,365 @@<br />+/*<br />+ * drivers/spi/spi-pnx.c<br />+ *<br />+ * SPI support for PNX 010x/4008 boards.<br />+ *<br />+ * Author: dmitry pervushin <dpervushin@ru.mvista.com><br />+ * Based on Dennis Kovalev's <dkovalev@ru.mvista.com> bus driver for pnx010x<br />+ *<br />+ * 2004 (c) MontaVista Software, Inc. This file is licensed under<br />+ * the terms of the GNU General Public License version 2. This program<br />+ * is licensed "as is" without any warranty of any kind, whether express<br />+ * or implied.<br />+ */<br />+<br />+#include <linux/module.h><br />+#include <linux/config.h><br />+#include <linux/version.h><br />+#include <linux/device.h><br />+#include <linux/kernel.h><br />+#include <linux/ioport.h><br />+#include <linux/dma-mapping.h><br />+#include <linux/delay.h><br />+#include <linux/spi/spi.h><br />+#include <linux/spi/spi_bitbang.h><br />+#include <asm/io.h><br />+<br />+#include "spi_pnx.h"<br />+<br />+#define lock_device( dev ) /* down( &dev->sem ); */<br />+#define unlock_device( dev ) /* up( &dev->sem ); */<br />+<br />+void spipnx_dma_handler (int channel, int cause, void* context, struct pt_regs* pt_regs )<br />+{<br />+ struct spi_pnx_data *dd = context;<br />+<br />+ if (cause & DMA_TC_INT)<br />+ complete(&dd->threshold);<br />+}<br />+<br />+int spipnx_request_hardware(struct spi_pnx_data *dd,<br />+ struct platform_device *dev)<br />+{<br />+ int err = 0;<br />+ struct resource *rsrc;<br />+<br />+ lock_device(&dev->dev);<br />+ rsrc = platform_get_resource(dev, IORESOURCE_MEM, 0);<br />+ if (rsrc) {<br />+ dd->iostart = rsrc->start;<br />+ dd->ioend = rsrc->end;<br />+ dd->spi_regs = ioremap(dd->iostart, SZ_4K);<br />+ /*<br />+ if (!request_mem_region<br />+ (dd->iostart, dd->ioend - dd->iostart + 1, dev->dev.bus_id))<br />+ err = -ENODEV;<br />+ else<br />+ */<br />+ dd->io_flags |= IORESOURCE_MEM;<br />+ }<br />+ dd->clk = spipnx_clk_init(dev);<br />+ if (IS_ERR(dd->clk)) {<br />+ err = PTR_ERR(dd->clk);<br />+ goto out;<br />+ }<br />+<br />+ spipnx_clk_start(dd->clk);<br />+<br />+ dd->irq = platform_get_irq(dev, 0);<br />+ if (dd->irq != NO_IRQ) {<br />+ dd->spi_regs->ier = 0;<br />+ dd->spi_regs->stat = 0;<br />+ }<br />+<br />+ rsrc = platform_get_resource(dev, IORESOURCE_DMA, 0);<br />+ if (rsrc) {<br />+ dd->slave_nr = rsrc->start;<br />+ err = spipnx_request_dma(&dd->dma_channel, dev->dev.bus_id, spipnx_dma_handler, dd);<br />+ if (!err)<br />+ dd->io_flags |= IORESOURCE_DMA;<br />+ }<br />+out:<br />+ unlock_device(&dev.dev);<br />+ return err;<br />+}<br />+<br />+static void spipnx_free_hardware(struct spi_pnx_data *dd,<br />+ struct platform_device *dev)<br />+{<br />+ lock_device(&dev->dev);<br />+<br />+ spipnx_clk_stop(dd->clk);<br />+<br />+ if (dd->io_flags & IORESOURCE_IRQ) {<br />+ free_irq(dd->irq, dev->dev.bus_id);<br />+ dd->io_flags &= ~IORESOURCE_IRQ;<br />+ }<br />+ if (dd->io_flags & IORESOURCE_MEM) {<br />+ /*<br />+ release_mem_region((unsigned long)dd->iostart,<br />+ dd->ioend - dd->iostart + 1);<br />+ */<br />+ dd->io_flags &= ~IORESOURCE_MEM;<br />+ }<br />+ if (dd->io_flags & IORESOURCE_DMA) {<br />+ spipnx_release_dma(&dd->dma_channel);<br />+ dd->io_flags &= ~IORESOURCE_DMA;<br />+ }<br />+<br />+ spipnx_clk_free(dd->clk);<br />+<br />+#ifdef DEBUG<br />+ if (dd->io_flags)<br />+ printk(KERN_ERR<br />+ "Oops, dd_ioflags for driver data %p is still 0x%x!\n",<br />+ dd, dd->io_flags);<br />+#endif<br />+ unlock_device(&dev->dev);<br />+}<br />+<br />+static void<br />+spi_pnx_chipselect(struct spi_device *spi, int is_on)<br />+{<br />+ spipnx_arch_cs(spi, is_on);<br />+}<br />+<br />+static int spi_pnx_xfer(struct spi_device *spidev, struct spi_transfer *t)<br />+{<br />+ u8* dat;<br />+ int len;<br />+ struct device *dev;<br />+ struct spi_pnx_data *dd;<br />+ vhblas_spiregs* regs;<br />+ int i;<br />+ u32 stat, fc, con, timeout;<br />+ struct spipnx_dma_transfer_t params;<br />+<br />+ dev = &spidev->dev;<br />+ dd = dev_get_drvdata(spidev->master->cdev.dev);<br />+ regs = dd->spi_regs;<br />+ len = t->len;<br />+<br />+ /* REVISIT */<br />+ con = regs->con;<br />+ regs->global = SPI_GLOBAL_BLRES_SPI | SPI_GLOBAL_SPI_ON;<br />+ udelay(10);<br />+ regs->global = SPI_GLOBAL_SPI_ON;<br />+ for (timeout = 10000; timeout >= 0; --timeout)<br />+ if (dd->spi_regs->global & SPIPNX_GLOBAL_SPI_ON)<br />+ break;<br />+ con |= (7<<9);<br />+ con &= ~(SPIPNX_CON_RXTX);<br />+ regs->con = con;<br />+<br />+ if (spidev->max_speed_hz)<br />+ spi_pnx_set_clock_rate(spidev->max_speed_hz / 1000, regs);<br />+<br />+ regs->stat |= 0x100;<br />+ regs->con |= SPI_CON_SPI_BIDIR | SPI_CON_SPI_BHALT | SPI_CON_SPI_BPOL |<br />+ SPI_CON_THR | SPI_CON_MS;<br />+<br />+ if (t->tx_buf) {<br />+ dat = (u8 *)t->tx_buf;<br />+ regs->con |= SPIPNX_CON_RXTX;<br />+ regs->ier |= SPIPNX_IER_EOT;<br />+ regs->con &= ~SPIPNX_CON_SHIFT_OFF;<br />+ regs->frm = len;<br />+<br />+ if (dd->dma_mode && len >= FIFO_CHUNK_SIZE) {<br />+ void *dmasafe = NULL;<br />+ if (t->tx_dma)<br />+ params.dma_buffer = t->tx_dma;<br />+ else {<br />+ dmasafe = kmalloc(len, SLAB_KERNEL);<br />+ if (!dmasafe) {<br />+ len = 0;<br />+ goto out;<br />+ }<br />+ params.dma_buffer = dma_map_single(dev->parent, dmasafe, len, DMA_TO_DEVICE);<br />+ memcpy(dmasafe, dat, len);<br />+ }<br />+ params.saved_ll = NULL;<br />+ params.saved_ll_dma = 0;<br />+<br />+ spipnx_setup_dma(dd->iostart, dd->slave_nr, dd->dma_channel,<br />+ DMA_MODE_WRITE, &params, len);<br />+ init_completion (&dd->threshold);<br />+ spipnx_start_dma(dd->dma_channel);<br />+ wait_for_completion (&dd->threshold);<br />+ spipnx_stop_dma(dd->dma_channel, &params);<br />+ if (dmasafe) {<br />+ dma_unmap_single(dev->parent, params.dma_buffer, len, DMA_TO_DEVICE);<br />+ kfree(dmasafe);<br />+ }<br />+ } else {<br />+ regs->con &= ~SPIPNX_CON_SHIFT_OFF;<br />+ for (i = 0; i < len ; i ++ ) {<br />+ while (regs->stat & 0x04)<br />+ continue;<br />+ regs->dat = *dat;<br />+ dat ++;<br />+ }<br />+ }<br />+ while ((regs->stat & SPIPNX_STAT_EOT) == 0)<br />+ continue;<br />+ }<br />+<br />+ if (t->rx_buf) {<br />+ int stopped;<br />+ u8 c;<br />+ void *dmasafe = NULL;<br />+<br />+ dat = t->rx_buf;<br />+ regs->con &= ~SPIPNX_CON_RXTX;<br />+ regs->ier |= SPIPNX_IER_EOT;<br />+ regs->con &= ~SPIPNX_CON_SHIFT_OFF;<br />+ regs->frm = len;<br />+<br />+ regs->dat = 0; /* kick off read */<br />+<br />+ if (dd->dma_mode && len > FIFO_CHUNK_SIZE - 1) {<br />+ if (t->rx_dma)<br />+ params.dma_buffer = t->rx_dma;<br />+ else {<br />+ dmasafe = kmalloc(len, SLAB_KERNEL);<br />+ if (!dmasafe) {<br />+ len = 0;<br />+ goto out;<br />+ }<br />+ params.dma_buffer = dma_map_single(dev->parent, dmasafe, len - (FIFO_CHUNK_SIZE - 1), DMA_FROM_DEVICE);<br />+ }<br />+ params.saved_ll = NULL;<br />+ params.saved_ll_dma = 0;<br />+<br />+ spipnx_setup_dma(dd->iostart, dd->slave_nr, dd->dma_channel,<br />+ DMA_MODE_READ, &params, len - (FIFO_CHUNK_SIZE - 1));<br />+ init_completion (&dd->threshold);<br />+ spipnx_start_dma(dd->dma_channel);<br />+ wait_for_completion (&dd->threshold);<br />+ spipnx_stop_dma(dd->dma_channel, &params);<br />+ if (dmasafe) {<br />+ memcpy(dat, dmasafe, len - (FIFO_CHUNK_SIZE - 1));<br />+ dma_unmap_single(dev->parent, params.dma_buffer, len - (FIFO_CHUNK_SIZE - 1), DMA_FROM_DEVICE);<br />+ kfree(dmasafe);<br />+ }<br />+ dat = dat + len - (FIFO_CHUNK_SIZE - 1);<br />+ len = FIFO_CHUNK_SIZE - 1;<br />+ }<br />+<br />+ stopped = 0;<br />+ stat = 0;<br />+ for(i = 0; i < len; ) {<br />+ unsigned long irq_flags;<br />+ local_irq_save(irq_flags);<br />+<br />+ stat = dd->spi_regs->stat;;<br />+ fc = dd->spi_regs->frm;<br />+ /* has hardware finished ? */<br />+ if(fc == 0) {<br />+ regs->con |= SPIPNX_CON_SHIFT_OFF;<br />+ stopped = 1;<br />+ }<br />+ /* FIFO not empty and hardware not about to finish */<br />+ if((!(stat & SPI_STAT_SPI_BE)) && ((fc > 4) || (stopped ))) {<br />+ *dat++= c = dd->spi_regs->dat;<br />+ i++;<br />+ }<br />+ if((stat & SPI_STAT_SPI_BF) && (!stopped)) {<br />+ *dat++= c = dd->spi_regs->dat;<br />+ i++;<br />+ }<br />+ local_irq_restore(irq_flags);<br />+ }<br />+ }<br />+out:<br />+ return len;<br />+}<br />+<br />+static int spi_pnx_setup(struct spi_device *spi)<br />+{<br />+ return 0;<br />+}<br />+<br />+static int spi_pnx_probe(struct device *device)<br />+{<br />+ struct spi_master *master;<br />+ struct spi_pnx_data *data;<br />+ struct spi_bitbang *pnx;<br />+ int rc = 0;<br />+<br />+ printk("spi probe called\n");<br />+ master = spi_alloc_master(device, sizeof *data);<br />+ if (!master) {<br />+ rc = -ENOMEM;<br />+ goto out;<br />+ }<br />+ master->setup = &spi_pnx_setup;<br />+ master->bus_num = to_platform_device(device)->id;<br />+<br />+ pnx = spi_master_get_devdata(master);<br />+ pnx->master = master;<br />+ pnx->chipselect = &spi_pnx_chipselect;<br />+ pnx->txrx_bufs = &spi_pnx_xfer;<br />+ rc = spi_bitbang_start(pnx);<br />+ if (rc < 0)<br />+ goto out;<br />+<br />+ data = kzalloc(sizeof *data, GFP_KERNEL);<br />+ if (!data) {<br />+ spi_bitbang_stop(pnx);<br />+ kfree(master);<br />+ rc = -ENOMEM;<br />+ goto out;<br />+ }<br />+ data->dma_channel = -1;<br />+ data->slave_nr = -1;<br />+ data->irq = NO_IRQ;<br />+ data->master = master;<br />+ data->dma_mode = 1;<br />+ dev_set_drvdata(device, data);<br />+<br />+ rc = spipnx_request_hardware(data, to_platform_device(device));<br />+ if (rc < 0) {<br />+ spi_bitbang_stop(pnx);<br />+ kfree(master);<br />+ kfree(data);<br />+ goto out;<br />+ }<br />+out:<br />+ return rc;<br />+}<br />+<br />+static int spi_pnx_remove(struct device *device)<br />+{<br />+ struct spi_pnx_data *data = dev_get_drvdata(device);<br />+ struct spi_bitbang *pnx;<br />+<br />+ pnx = spi_master_get_devdata(data->master);<br />+<br />+ spipnx_free_hardware(data, to_platform_device(device));<br />+ spi_bitbang_stop(pnx);<br />+ kfree(data->master);<br />+ kfree(data);<br />+ return 0;<br />+}<br />+<br />+<br />+static struct device_driver spi_pnx_driver = {<br />+ .name = "spipnx",<br />+ .bus = &platform_bus_type,<br />+ .probe = spi_pnx_probe,<br />+ .remove = spi_pnx_remove,<br />+};<br />+<br />+static int __init spi_pnx_init(void)<br />+{<br />+ return driver_register(&spi_pnx_driver);<br />+}<br />+<br />+static void __exit spi_pnx_cleanup(void)<br />+{<br />+ driver_unregister(&spi_pnx_driver);<br />+}<br />+<br />+module_init(spi_pnx_init);<br />+module_exit(spi_pnx_cleanup);<br />Index: linux-2.6/include/asm-arm/arch-pnx4008/spi.h<br />===================================================================<br />--- /dev/null<br />+++ linux-2.6/include/asm-arm/arch-pnx4008/spi.h<br />@@ -0,0 +1,370 @@<br />+/*<br />+ * include/asm-arm/arch-pnx4008/spi.h<br />+ *<br />+ * Author: Vitaly Wool <vwool@ru.mvista.com><br />+ *<br />+ * PNX4008-specific tweaks for SPI block<br />+ *<br />+ * 2005 (c) MontaVista Software, Inc. This file is licensed under<br />+ * the terms of the GNU General Public License version 2. This program<br />+ * is licensed "as is" without any warranty of any kind, whether express<br />+ * or implied.<br />+ */<br />+#ifndef __ASM_ARCH_SPI_H__<br />+#define __ASM_ARCH_SPI_H__<br />+<br />+#include <linux/spi/spi.h><br />+#include <linux/err.h><br />+#include <linux/device.h><br />+#include <linux/platform_device.h><br />+<br />+#include <asm/dma.h><br />+<br />+#include <asm/arch/dma.h><br />+#include <asm/arch/gpio.h><br />+#include <asm/arch/pm.h><br />+#include <asm/arch/gpio.h><br />+<br />+#include <asm/hardware/clock.h><br />+<br />+#define SPI_ADR_OFFSET_GLOBAL 0x0000 /* R/W - Global control register */<br />+#define SPI_ADR_OFFSET_CON 0x0004 /* R/W - Control register */<br />+#define SPI_ADR_OFFSET_FRM 0x0008 /* R/W - Frame count register */<br />+#define SPI_ADR_OFFSET_IER 0x000C /* R/W - Interrupt enable register */<br />+#define SPI_ADR_OFFSET_STAT 0x0010 /* R/W - Status register */<br />+#define SPI_ADR_OFFSET_DAT 0x0014 /* R/W - Data register */<br />+#define SPI_ADR_OFFSET_DAT_MSK 0x0018 /* W - Data register for using mask mode */<br />+#define SPI_ADR_OFFSET_MASK 0x001C /* R/W - Mask register */<br />+#define SPI_ADR_OFFSET_ADDR 0x0020 /* R/W - Address register */<br />+#define SPI_ADR_OFFSET_TIMER_CTRL_REG 0x0400 /* R/W - IRQ timer control register */<br />+#define SPI_ADR_OFFSET_TIMER_COUNT_REG 0x0404 /* R/W - Timed interrupt period register */<br />+#define SPI_ADR_OFFSET_TIMER_STATUS_REG 0x0408 /* R;R/C - RxDepth and interrupt status register */<br />+<br />+/* Bit definitions for SPI_GLOBAL register */<br />+#define SPI_GLOBAL_BLRES_SPI 0x00000002 /* R/W - Software reset, active high */<br />+#define SPI_GLOBAL_SPI_ON 0x00000001 /* R/W - SPI interface on */<br />+<br />+/* Bit definitions for SPI_CON register */<br />+#define SPI_CON_SPI_BIDIR 0x00800000 /* R/W - SPI data in bidir. mux */<br />+#define SPI_CON_SPI_BHALT 0x00400000 /* R/W - Halt control */<br />+#define SPI_CON_SPI_BPOL 0x00200000 /* R/W - Busy signal active polarity */<br />+#define SPI_CON_SPI_SHFT 0x00100000 /* R/W - Data shifting enable in mask mode */<br />+#define SPI_CON_SPI_MSB 0x00080000 /* R/W - Transfer LSB or MSB first */<br />+#define SPI_CON_SPI_EN 0x00040000 /* R/W - SPULSE signal enable */<br />+#define SPI_CON_SPI_MODE 0x00030000 /* R/W - SCK polarity and phase modes */<br />+#define SPI_CON_SPI_MODE3 0x00030000 /* R/W - SCK polarity and phase mode 3 */<br />+#define SPI_CON_SPI_MODE2 0x00020000 /* R/W - SCK polarity and phase mode 2 */<br />+#define SPI_CON_SPI_MODE1 0x00010000 /* R/W - SCK polarity and phase mode 1 */<br />+#define SPI_CON_SPI_MODE0 0x00000000 /* R/W - SCK polarity and phase mode 0 */<br />+#define SPI_CON_RxTx 0x00008000 /* R/W - Transfer direction */<br />+#define SPI_CON_THR 0x00004000 /* R/W - Threshold selection */<br />+#define SPI_CON_SHIFT_OFF 0x00002000 /* R/W - Inhibits generation of clock pulses on sck_spi pin */<br />+#define SPI_CON_BITNUM 0x00001E00 /* R/W - No of bits to tx or rx in one block transfer */<br />+#define SPI_CON_CS_EN 0x00000100 /* R/W - Disable use of CS in slave mode */<br />+#define SPI_CON_MS 0x00000080 /* R/W - Selection of master or slave mode */<br />+#define SPI_CON_RATE 0x0000007F /* R/W - Transmit/receive rate */<br />+<br />+#define SPI_CON_RATE_0 0x00<br />+#define SPI_CON_RATE_1 0x01<br />+#define SPI_CON_RATE_2 0x02<br />+#define SPI_CON_RATE_3 0x03<br />+#define SPI_CON_RATE_4 0x04<br />+#define SPI_CON_RATE_13 0x13<br />+<br />+/* Bit definitions for SPI_FRM register */<br />+#define SPI_FRM_SPIF 0x0000FFFF /* R/W - Number of frames to be transfered */<br />+<br />+/* Bit definitions for SPI_IER register */<br />+#define SPI_IER_SPI_INTCS 0x00000004 /* R/W - SPI CS level change interrupt enable */<br />+#define SPI_IER_SPI_INTEOT 0x00000002 /* R/W - End of transfer interrupt enable */<br />+#define SPI_IER_SPI_INTTHR 0x00000001 /* R/W - FIFO threshold interrupt enable */<br />+<br />+/* Bit definitions for SPI_STAT register */<br />+#define SPI_STAT_SPI_INTCLR 0x00000100 /* R/WC - Clear interrupt */<br />+#define SPI_STAT_SPI_EOT 0x00000080 /* R/W - End of transfer */<br />+#define SPI_STAT_SPI_BSY_SL 0x00000040 /* R/W - Status of the input pin spi_busy */<br />+#define SPI_STAT_SPI_CSL 0x00000020 /* R/W - Indication of the edge on SPI_CS that caused an int. */<br />+#define SPI_STAT_SPI_CSS 0x00000010 /* R/W - Level of SPI_CS has changed in slave mode */<br />+#define SPI_STAT_SPI_BUSY 0x00000008 /* R/W - A data transfer is ongoing */<br />+#define SPI_STAT_SPI_BF 0x00000004 /* R/W - FIFO is full */<br />+#define SPI_STAT_SPI_THR 0x00000002 /* R/W - No of entries in Tx/Rx FIFO */<br />+#define SPI_STAT_SPI_BE 0x00000001 /* R/W - FIFO is empty */<br />+<br />+/* Bit definitions for SPI_DAT register */<br />+#define SPI_DAT_SPID 0x0000FFFF /* R/W - SPI data bits */<br />+<br />+/* Bit definitions for SPI_DAT_MSK register */<br />+#define SPI_DAT_MSK_SPIDM 0x0000FFFF /* W - SPI data bits to send using the masking mode */<br />+<br />+/* Bit definitions for SPI_MASK register */<br />+#define SPI_MASK_SPIMSK 0x0000FFFF /* R/W - Masking bits used for validating data bits */<br />+<br />+/* Bit definitions for SPI_ADDR register */<br />+#define SPI_ADDR_SPIAD 0x0000FFFF /* R/W - Address bits to add to the data bits */<br />+<br />+/* Bit definitions for SPI_TIMER_CTRL_REG register */<br />+#define SPI_TIMER_CTRL_REG_TIRQE 0x00000004 /* R/W - Timer interrupt enable */<br />+#define SPI_TIMER_CTRL_REG_PIRQE 0x00000002 /* R/W - Peripheral interrupt enable */<br />+#define SPI_TIMER_CTRL_REG_MODE 0x00000001 /* R/W - Mode */<br />+<br />+/* Bit definitions for SPI_TIMER_COUNT_REG register */<br />+#define SPI_TIMER_COUNT_REG 0x0000FFFF /* R/W - Timed interrupt period */<br />+<br />+/* Bit definitions for SPI_TIMER_STATUS_REG register */<br />+#define SPI_TIMER_STATUS_REG_INT_STAT 0x00008000 /* R/C - Interrupt status */<br />+#define SPI_TIMER_STATUS_REG_FIFO_DEPTH 0x0000007F /* R - FIFO depth value (for debug purpose) */<br />+<br />+#define SPIPNX_STAT_EOT SPI_STAT_SPI_EOT<br />+#define SPIPNX_STAT_THR SPI_STAT_SPI_THR<br />+#define SPIPNX_IER_EOT SPI_IER_SPI_INTEOT<br />+#define SPIPNX_IER_THR SPI_IER_SPI_INTTHR<br />+#define SPIPNX_STAT_CLRINT SPI_STAT_SPI_INTCLR<br />+<br />+#define SPIPNX_GLOBAL_RESET_SPI SPI_GLOBAL_BLRES_SPI<br />+#define SPIPNX_GLOBAL_SPI_ON SPI_GLOBAL_SPI_ON<br />+#define SPIPNX_CON_MS SPI_CON_MS<br />+#define SPIPNX_CON_BIDIR SPI_CON_SPI_BIDIR<br />+#define SPIPNX_CON_RXTX SPI_CON_RxTx<br />+#define SPIPNX_CON_THR SPI_CON_THR<br />+#define SPIPNX_CON_SHIFT_OFF SPI_CON_SHIFT_OFF<br />+#define SPIPNX_DATA SPI_ADR_OFFSET_DAT<br />+#define SPI_PNX4008_CLOCK_IN 104000000<br />+#define SPI_PNX4008_CLOCK 2670000<br />+#define SPIPNX_CLOCK ((((SPI_PNX4008_CLOCK_IN / SPI_PNX4008_CLOCK) - 2) / 2) & SPI_CON_RATE_MASK )<br />+#define SPIPNX_DATA_REG(s) ( (u32)(s) + SPIPNX_DATA )<br />+<br />+typedef volatile struct {<br />+ u32 global; /* 0x000 */<br />+ u32 con; /* 0x004 */<br />+ u32 frm; /* 0x008 */<br />+ u32 ier; /* 0x00C */<br />+ u32 stat; /* 0x010 */<br />+ u32 dat; /* 0x014 */<br />+ u32 dat_msk; /* 0x018 */<br />+ u32 mask; /* 0x01C */<br />+ u32 addr; /* 0x020 */<br />+ u32 _d0[(SPI_ADR_OFFSET_TIMER_CTRL_REG -<br />+ (SPI_ADR_OFFSET_ADDR + sizeof(u32))) / sizeof(u32)];<br />+ u32 timer_ctrl; /* 0x400 */<br />+ u32 timer_count; /* 0x404 */<br />+ u32 timer_status; /* 0x408 */<br />+}<br />+vhblas_spiregs, *pvhblas_spiregs;<br />+<br />+#define SPI_CON_RATE_MASK 0x7f<br />+<br />+static void spipnx_arch_set_clock_rate( int clock_khz, int ref_clock_khz, vhblas_spiregs* regs )<br />+{<br />+ u32 spi_clock;<br />+<br />+ printk( "ref clock = %d, requested clock = %d\n", ref_clock_khz, clock_khz);<br />+ if(!clock_khz)<br />+ spi_clock = ref_clock_khz < 104000 ? 0 : 1;<br />+ else<br />+ spi_clock = (ref_clock_khz/1000) / (2*(clock_khz/1000) + 2);<br />+ regs->con &= ~SPI_CON_RATE_MASK;<br />+ printk("SPI clock = %x\n", spi_clock);<br />+ regs->con |= spi_clock;<br />+}<br />+<br />+static inline void spi_pnx_set_clock_rate(int saved_clock_khz, vhblas_spiregs *regs)<br />+{<br />+ struct clk *div_clk= clk_get(0, "hclk_ck");<br />+ struct clk *arm_clk = clk_get(0, "ck_pll4");<br />+ if (!IS_ERR(arm_clk) && !IS_ERR(div_clk))<br />+ spipnx_arch_set_clock_rate(<br />+ saved_clock_khz,<br />+ clk_get_rate(arm_clk) / clk_get_rate(div_clk),<br />+ regs);<br />+ if (!IS_ERR(arm_clk))<br />+ clk_put(arm_clk);<br />+ if (!IS_ERR(div_clk))<br />+ clk_put(div_clk);<br />+}<br />+<br />+static inline void spipnx_clk_stop(void *clk_data)<br />+{<br />+ struct clk *clk = (struct clk *)clk_data;<br />+ if (clk && !IS_ERR(clk)) {<br />+ clk_set_rate(clk, 0);<br />+ clk_put(clk);<br />+ }<br />+}<br />+<br />+static inline void spipnx_clk_start(void *clk_data)<br />+{<br />+ struct clk *clk = (struct clk *)clk_data;<br />+ if (clk && !IS_ERR(clk)) {<br />+ clk_set_rate(clk, 1);<br />+ clk_put(clk);<br />+ }<br />+}<br />+<br />+static inline void *spipnx_clk_init(struct platform_device *dev)<br />+{<br />+ struct clk *clk = clk_get(0, dev->id == 1 ? "spi0_ck" : "spi1_ck");<br />+ if (IS_ERR(clk))<br />+ printk("spi%d_ck pointer err\n", dev->id);<br />+ return clk;<br />+}<br />+<br />+static inline void spipnx_clk_free( void* clk )<br />+{<br />+ clk_put( clk );<br />+}<br />+<br />+static inline void spipnx_arch_cs(struct spi_device *spi, int is_on)<br />+{<br />+ switch (spi->chip_select)<br />+ {<br />+ case 0:<br />+ printk("setting pin %x to %d\n", GPO_02, !is_on);<br />+ pnx4008_gpio_write_pin(GPO_02, !is_on);<br />+ break;<br />+ case 1:<br />+ printk( "setting pin %x to %d\n", GPIO_03, !is_on);<br />+ pnx4008_gpio_write_pin(GPIO_03, !is_on);<br />+ break;<br />+ }<br />+}<br />+<br />+static inline int spipnx_request_dma(<br />+ int *dma_channel, char *name,<br />+ void (*handler)( int, int, void*, struct pt_regs*),<br />+ void *dma_context)<br />+{<br />+ int err;<br />+<br />+ if (*dma_channel != -1) {<br />+ err = *dma_channel;<br />+ goto out;<br />+ }<br />+ err = pnx4008_request_channel(name, -1, handler, dma_context);<br />+ if (err >= 0) {<br />+ *dma_channel = err;<br />+ }<br />+out:<br />+ return err < 0 ? err : 0;<br />+}<br />+<br />+static inline void spipnx_release_dma(int *dma_channel)<br />+{<br />+ if (*dma_channel >= 0) {<br />+ pnx4008_free_channel(*dma_channel);<br />+ }<br />+ *dma_channel = -1;<br />+}<br />+<br />+static inline void spipnx_start_dma(int dma_channel)<br />+{<br />+ pnx4008_dma_ch_enable(dma_channel);<br />+}<br />+<br />+struct spipnx_dma_transfer_t<br />+{<br />+ dma_addr_t dma_buffer;<br />+ void *saved_ll;<br />+ u32 saved_ll_dma;<br />+};<br />+<br />+static inline void spipnx_stop_dma(int dma_channel, void* dev_buf)<br />+{<br />+ struct spipnx_dma_transfer_t *buf = dev_buf;<br />+<br />+ pnx4008_dma_ch_disable(dma_channel);<br />+ if (buf->saved_ll) {<br />+ pnx4008_free_ll(buf->saved_ll_dma, buf->saved_ll);<br />+ buf->saved_ll = NULL;<br />+ }<br />+}<br />+<br />+static inline int spipnx_setup_dma(u32 iostart, int slave_nr, int dma_channel,<br />+ int mode, void* params, int len)<br />+{<br />+ int err = 0;<br />+ struct spipnx_dma_transfer_t *buff = params;<br />+<br />+ pnx4008_dma_config_t cfg;<br />+ pnx4008_dma_ch_config_t ch_cfg;<br />+ pnx4008_dma_ch_ctrl_t ch_ctrl;<br />+<br />+ memset(&cfg, 0, sizeof(cfg));<br />+<br />+ if (mode == DMA_MODE_READ) {<br />+ cfg.dest_addr = buff->dma_buffer; /* buff->dma_buffer; */<br />+ cfg.src_addr = (u32) SPIPNX_DATA_REG(iostart);<br />+ ch_cfg.flow_cntrl = FC_PER2MEM_DMA;<br />+ ch_cfg.src_per = slave_nr;<br />+ ch_cfg.dest_per = 0;<br />+ ch_ctrl.di = 1;<br />+ ch_ctrl.si = 0;<br />+ } else if (mode == DMA_MODE_WRITE) {<br />+ cfg.src_addr = buff->dma_buffer; /* buff->dma_buffer; */<br />+ cfg.dest_addr = (u32) SPIPNX_DATA_REG(iostart);<br />+ ch_cfg.flow_cntrl = FC_MEM2PER_DMA;<br />+ ch_cfg.dest_per = slave_nr;<br />+ ch_cfg.src_per = 0;<br />+ ch_ctrl.di = 0;<br />+ ch_ctrl.si = 1;<br />+ } else {<br />+ err = -EINVAL;<br />+ }<br />+<br />+ ch_cfg.halt = 0;<br />+ ch_cfg.active = 1;<br />+ ch_cfg.lock = 0;<br />+ ch_cfg.itc = 1;<br />+ ch_cfg.ie = 1;<br />+ ch_ctrl.tc_mask = 1;<br />+ ch_ctrl.cacheable = 0;<br />+ ch_ctrl.bufferable = 0;<br />+ ch_ctrl.priv_mode = 1;<br />+ ch_ctrl.dest_ahb1 = 0;<br />+ ch_ctrl.src_ahb1 = 0;<br />+ ch_ctrl.dwidth = WIDTH_BYTE;<br />+ ch_ctrl.swidth = WIDTH_BYTE;<br />+ ch_ctrl.dbsize = 1;<br />+ ch_ctrl.sbsize = 1;<br />+ ch_ctrl.tr_size = len;<br />+ if (0 > (err = pnx4008_dma_pack_config(&ch_cfg, &cfg.ch_cfg))) {<br />+ goto out;<br />+ }<br />+ if (0 > (err = pnx4008_dma_pack_control(&ch_ctrl, &cfg.ch_ctrl))) {<br />+ if ( err == -E2BIG ) {<br />+ printk( KERN_DEBUG"buffer is too large, splitting\n" );<br />+ pnx4008_dma_split_head_entry(&cfg, &ch_ctrl);<br />+ buff->saved_ll = cfg.ll;<br />+ buff->saved_ll_dma = cfg.ll_dma;<br />+ err = 0;<br />+ } else {<br />+ goto out;<br />+ }<br />+ }<br />+ err = pnx4008_config_channel(dma_channel, &cfg);<br />+out:<br />+ return err;<br />+}<br />+<br />+static inline void spipnx_start_event_init(int id)<br />+{<br />+ int se = id==0 ? SE_SPI1_DATAIN_INT : SE_SPI2_DATAIN_INT;<br />+<br />+ /*setup wakeup interrupt*/<br />+ start_int_set_rising_edge(se);<br />+ start_int_ack(se);<br />+ start_int_umask(se);<br />+}<br />+<br />+static inline void spipnx_start_event_deinit(int id)<br />+{<br />+ int se = id==0 ? SE_SPI1_DATAIN_INT : SE_SPI2_DATAIN_INT;<br />+<br />+ start_int_mask(se);<br />+}<br />+<br />+struct spipnx_wifi_params {<br />+ int gpio_cs_line;<br />+};<br />+<br />+#endif /* __ASM_ARCH_SPI_H___ */<br />+<br />Index: linux-2.6/drivers/spi/spi_pnx.h<br />===================================================================<br />--- /dev/null<br />+++ linux-2.6/drivers/spi/spi_pnx.h<br />@@ -0,0 +1,60 @@<br />+/*<br />+ * SPI support for Philips SPI bus on PNX's.<br />+ *<br />+ * Author: Dennis Kovalev <dkovalev@ru.mvista.com><br />+ *<br />+ * 2004 (c) MontaVista Software, Inc. This file is licensed under<br />+ * the terms of the GNU General Public License version 2. This program<br />+ * is licensed "as is" without any warranty of any kind, whether express<br />+ * or implied.<br />+ */<br />+<br />+#ifndef _SPI_PNX_BUS_DRIVER<br />+#define _SPI_PNX_BUS_DRIVER<br />+<br />+#include <linux/spi/spi.h><br />+#include <asm/arch/platform.h><br />+#include <asm/arch/dma.h><br />+#include <asm/dma.h><br />+#include <asm/arch/spi.h><br />+<br />+/* structures */<br />+struct spi_pnx_data {<br />+ unsigned io_flags;<br />+<br />+ int irq;<br />+<br />+ int dma_mode;<br />+ int dma_channel;<br />+ int slave_nr;<br />+<br />+ u32 iostart, ioend;<br />+ vhblas_spiregs *spi_regs;<br />+<br />+ struct clk *clk;<br />+ int clk_id[4];<br />+<br />+ int state;<br />+ u32 saved_clock;<br />+<br />+ struct completion op_complete;<br />+ struct completion threshold;<br />+<br />+ int progress;<br />+ struct spi_master *master;<br />+<br />+ void (*control) (struct spi_device * dev, int code, u32 ctl);<br />+};<br />+<br />+#define SPIPNX_STATE_UNINITIALIZED 0<br />+#define SPIPNX_STATE_READY 1<br />+#define SPIPNX_STATE_SUSPENDED 2<br />+<br />+#define SPIPNX_IS_READY( bus ) ( (bus)->state == SPIPNX_STATE_READY )<br />+<br />+#define FIFO_CHUNK_SIZE 56<br />+<br />+#define SPI_ENDIAN_SWAP_NO 0<br />+#define SPI_ENDIAN_SWAP_YES 1<br />+<br />+#endif // __SPI_PNX_BUS_DRIVER<br />-<br />To unsubscribe from this list: send the line "unsubscribe linux-kernel" in<br />the body of a message to majordomo@vger.kernel.org<br />More majordomo info at <a href="http://vger.kernel.org/majordomo-info.html">http://vger.kernel.org/majordomo-info.html</a><br />Please read the FAQ at <a href="http://www.tux.org/lkml/">http://www.tux.org/lkml/</a><br /></pre></td><td width="32" rowspan="2" class="c" valign="top"><img src="/images/icornerr.gif" width="32" height="32" alt="\" /></td></tr><tr><td align="right" valign="bottom"> 聽 </td></tr><tr><td align="right" valign="bottom">聽</td><td class="c" valign="bottom" style="padding-bottom: 0px"><img src="/images/bcornerl.gif" width="32" height="32" alt="\" /></td><td class="c">聽</td><td class="c" valign="bottom" style="padding-bottom: 0px"><img src="/images/bcornerr.gif" width="32" height="32" alt="/" /></td></tr><tr><td align="right" valign="top" colspan="2"> 聽 </td><td class="lm">Last update: 2005-12-23 15:16 聽聽 [from the cache]<br />漏2003-2020 <a href="http://blog.jasper.es/"><span itemprop="editor">Jasper Spaans</span></a>|hosted at <a href="https://www.digitalocean.com/?refcode=9a8e99d24cf9">Digital Ocean</a> and my Meterkast|<a href="http://blog.jasper.es/categories.html#lkml-ref">Read the blog</a></td><td>聽</td></tr></table><script language="javascript" src="/js/styleswitcher.js" type="text/javascript"></script></body></html>