Second try sending SPI core patch.
Signed-off-by: Ken Mills <[email protected]>
---
drivers/spi/spi.c | 368 ++++++++++++++++++++++++++++++++++++++++++-----
include/linux/spi/spi.h | 100 ++++++++++++-
2 files changed, 427 insertions(+), 41 deletions(-)
diff --git a/drivers/spi/spi.c b/drivers/spi/spi.c
index 8eba98c..504f0ae 100644
--- a/drivers/spi/spi.c
+++ b/drivers/spi/spi.c
@@ -26,19 +26,34 @@
#include <linux/spi/spi.h>
-/* SPI bustype and spi_master class are registered after board init code
- * provides the SPI device tables, ensuring that both are present by the
- * time controller driver registration causes spi_devices to "enumerate".
- */
+/* SPI bustype, spi_master and spi_slave class are registered after board
+* init code provides the SPI device tables, ensuring that both are present
+* by the time controller driver registration causes spi_devices
+* to "enumerate".
+*/
+
+/* SPI Slave Support is added for new spi slave devices: It uses common APIs,
+* apart from few new APIs and a spi_slave structure.
+*/
+
static void spidev_release(struct device *dev)
{
struct spi_device *spi = to_spi_device(dev);
- /* spi masters may cleanup for released devices */
- if (spi->master->cleanup)
- spi->master->cleanup(spi);
+ if (spi->master) {
+ /* spi masters may cleanup for released devices */
+ if (spi->master->cleanup)
+ spi->master->cleanup(spi);
+
+ spi_master_put(spi->master);
+ } else {
+ /* spi slave controller */
+ if (spi->slave->cleanup)
+ spi->slave->cleanup(spi);
+
+ spi_slave_put(spi->slave);
+ }
- spi_master_put(spi->master);
kfree(dev);
}
@@ -46,7 +61,6 @@ static ssize_t
modalias_show(struct device *dev, struct device_attribute *a, char *buf)
{
const struct spi_device *spi = to_spi_device(dev);
-
return sprintf(buf, "%s\n", spi->modalias);
}
@@ -62,14 +76,12 @@ static struct device_attribute spi_dev_attrs[] = {
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
const struct spi_device *spi = to_spi_device(dev);
-
return strcmp(spi->modalias, drv->name) == 0;
}
static int spi_uevent(struct device *dev, struct kobj_uevent_env *env)
{
const struct spi_device *spi = to_spi_device(dev);
-
add_uevent_var(env, "MODALIAS=%s", spi->modalias);
return 0;
}
@@ -153,10 +165,13 @@ int spi_register_driver(struct spi_driver *sdrv)
sdrv->driver.bus = &spi_bus_type;
if (sdrv->probe)
sdrv->driver.probe = spi_drv_probe;
+
if (sdrv->remove)
sdrv->driver.remove = spi_drv_remove;
+
if (sdrv->shutdown)
sdrv->driver.shutdown = spi_drv_shutdown;
+
return driver_register(&sdrv->driver);
}
EXPORT_SYMBOL_GPL(spi_register_driver);
@@ -197,28 +212,70 @@ static DEFINE_MUTEX(board_lock);
*/
struct spi_device *spi_alloc_device(struct spi_master *master)
{
- struct spi_device *spi;
+ struct spi_device *spi_m_dev;
struct device *dev = master->dev.parent;
if (!spi_master_get(master))
return NULL;
- spi = kzalloc(sizeof *spi, GFP_KERNEL);
- if (!spi) {
+ spi_m_dev = kzalloc(sizeof *spi_m_dev, GFP_KERNEL);
+ if (!spi_m_dev) {
dev_err(dev, "cannot alloc spi_device\n");
spi_master_put(master);
return NULL;
}
- spi->master = master;
- spi->dev.parent = dev;
- spi->dev.bus = &spi_bus_type;
- spi->dev.release = spidev_release;
- device_initialize(&spi->dev);
- return spi;
+ spi_m_dev->master = master;
+ spi_m_dev->slave = NULL;
+ spi_m_dev->dev.parent = dev;
+ spi_m_dev->dev.bus = &spi_bus_type;
+ spi_m_dev->dev.release = spidev_release;
+ device_initialize(&spi_m_dev->dev);
+ return spi_m_dev;
}
EXPORT_SYMBOL_GPL(spi_alloc_device);
+/*
+* spi_alloc_slave_device - Allocate a new SPI device
+* @slave: Controller to which device is connected
+* Context: can sleep
+*
+* Allows a driver to allocate and initialize a spi_device without
+* registering it immediately. This allows a driver to directly
+* fill the spi_device with device parameters before calling
+* spi_add_slave_device() on it.
+*
+* Caller is responsible to call spi_add_slave_device() on the returned
+* spi_device structure to add it to the SPI slave. If the caller
+* needs to discard the spi_device without adding it, then it should
+* call spi_dev_slave_put() on it.
+* Returns a pointer to the new device, or NULL.
+*/
+struct spi_device *spi_alloc_slave_device(struct spi_slave *slave)
+{
+ struct spi_device *spi_s;
+ struct device *dev = slave->dev.parent;
+
+ if (!spi_slave_get(slave))
+ return NULL;
+
+ spi_s = kzalloc(sizeof *spi_s, GFP_KERNEL);
+ if (!spi_s) {
+ dev_err(dev, "cannot alloc spi_slave_device\n");
+ spi_slave_put(slave);
+ return NULL;
+ }
+
+ spi_s->slave = slave;
+ spi_s->master = NULL;
+ spi_s->dev.parent = dev;
+ spi_s->dev.bus = &spi_bus_type;
+ spi_s->dev.release = spidev_release;
+ device_initialize(&spi_s->dev);
+ return spi_s;
+}
+EXPORT_SYMBOL_GPL(spi_alloc_slave_device);
+
/**
* spi_add_device - Add spi_device allocated with spi_alloc_device
* @spi: spi_device to register
@@ -231,21 +288,29 @@ EXPORT_SYMBOL_GPL(spi_alloc_device);
int spi_add_device(struct spi_device *spi)
{
static DEFINE_MUTEX(spi_add_lock);
- struct device *dev = spi->master->dev.parent;
int status;
-
- /* Chipselects are numbered 0..max; validate. */
- if (spi->chip_select >= spi->master->num_chipselect) {
- dev_err(dev, "cs%d >= max %d\n",
- spi->chip_select,
- spi->master->num_chipselect);
- return -EINVAL;
- }
-
+ struct device *dev;
+
+ if (spi->master) {
+ /* Master Controller */
+ dev = spi->master->dev.parent;
+ /* Chipselects are numbered 0..max; validate. */
+ if (spi->chip_select >= spi->master->num_chipselect) {
+ dev_err(dev, "cs%d >= max %d\n",
+ spi->chip_select,
+ spi->master->num_chipselect);
+ return -EINVAL;
+ }
/* Set the bus ID string */
dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->master->dev),
spi->chip_select);
-
+ } else {
+ /* Slave Controller */
+ dev = spi->slave->dev.parent;
+ /* Set the bus ID string */
+ dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->slave->dev),
+ spi->chip_select);
+ }
/* We need to make sure there's no other device with this
* chipselect **BEFORE** we call setup(), else we'll trash
@@ -265,7 +330,11 @@ int spi_add_device(struct spi_device *spi)
* normally rely on the device being setup. Devices
* using SPI_CS_HIGH can't coexist well otherwise...
*/
- status = spi->master->setup(spi);
+ if (spi->master)
+ status = spi->master->setup(spi);
+ else
+ status = spi->slave->setup(spi); /* Slave Controller */
+
if (status < 0) {
dev_err(dev, "can't %s %s, status %d\n",
"setup", dev_name(&spi->dev), status);
@@ -286,6 +355,7 @@ done:
}
EXPORT_SYMBOL_GPL(spi_add_device);
+
/**
* spi_new_device - instantiate one new SPI device
* @master: Controller to which device is connected
@@ -317,6 +387,8 @@ struct spi_device *spi_new_device(struct spi_master *master,
if (!proxy)
return NULL;
+
+
WARN_ON(strlen(chip->modalias) >= sizeof(proxy->modalias));
proxy->chip_select = chip->chip_select;
@@ -339,6 +411,54 @@ struct spi_device *spi_new_device(struct spi_master
*master,
EXPORT_SYMBOL_GPL(spi_new_device);
/**
+* spi_slave_new_device - instantiate one new SPI device
+* @slave: Controller to which device is connected
+* @chip: Describes the SPI device
+* Context: can sleep
+*
+* On typical mainboards, this is purely internal; and it's not needed
+* after board init creates the hard-wired devices. Some development
+* platforms may not be able to use spi_register_board_info though, and
+* this is exported so that for example a USB or parport based adapter
+* driver could add devices (which it would learn about out-of-band).
+*
+* Returns the new device, or NULL.
+*/
+struct spi_device *spi_slave_new_device(struct spi_slave *slave,
+ struct spi_board_info *chip)
+{
+ struct spi_device *proxy_slave;
+ int status;
+
+ proxy_slave = spi_alloc_slave_device(slave);
+
+ if (!proxy_slave)
+ return NULL;
+
+ WARN_ON(strlen(chip->modalias) >= sizeof(proxy_slave->modalias));
+
+ proxy_slave->chip_select = chip->chip_select;
+ proxy_slave->max_speed_hz = chip->max_speed_hz;
+ proxy_slave->mode = chip->mode;
+ proxy_slave->irq = chip->irq;
+ strlcpy(proxy_slave->modalias, chip->modalias,
+ sizeof(proxy_slave->modalias));
+ proxy_slave->dev.platform_data = (void *) chip->platform_data;
+ proxy_slave->controller_data = chip->controller_data;
+ proxy_slave->controller_state = NULL;
+
+ status = spi_add_device(proxy_slave);
+ if (status < 0) {
+ spi_dev_put(proxy_slave);
+ return NULL;
+ }
+
+ return proxy_slave;
+}
+EXPORT_SYMBOL_GPL(spi_slave_new_device);
+
+
+/**
* spi_register_board_info - register SPI devices for a given board
* @info: array of chip descriptors
* @n: how many descriptors are provided
@@ -365,6 +485,7 @@ spi_register_board_info(struct spi_board_info const *info,
unsigned n)
bi = kmalloc(sizeof(*bi) + n * sizeof *info, GFP_KERNEL);
if (!bi)
return -ENOMEM;
+
bi->n_board_info = n;
memcpy(bi->board_info, info, n * sizeof *info);
@@ -374,6 +495,7 @@ spi_register_board_info(struct spi_board_info const *info,
unsigned n)
return 0;
}
+
/* FIXME someone should add support for a __setup("spi", ...) that
* creates board info from kernel command lines
*/
@@ -399,6 +521,28 @@ static void scan_boardinfo(struct spi_master *master)
mutex_unlock(&board_lock);
}
+static void spi_slave_scan_boardinfo(struct spi_slave *slave)
+{
+ struct boardinfo *bi;
+
+ mutex_lock(&board_lock);
+ list_for_each_entry(bi, &board_list, list) {
+ struct spi_board_info *chip = bi->board_info;
+ unsigned n;
+
+ for (n = bi->n_board_info; n > 0; n--, chip++) {
+ if (chip->bus_num != slave->bus_num)
+ continue;
+ /* NOTE: this relies on spi_new_device to
+ * issue diagnostics when given bogus inputs
+ */
+ (void) spi_slave_new_device(slave, chip);
+
+ }
+ }
+ mutex_unlock(&board_lock);
+}
+
/*-------------------------------------------------------------------------*/
static void spi_master_release(struct device *dev)
@@ -415,6 +559,19 @@ static struct class spi_master_class = {
.dev_release = spi_master_release,
};
+static void spi_slave_release(struct device *dev)
+{
+ struct spi_slave *slave;
+
+ slave = container_of(dev, struct spi_slave, dev);
+ kfree(slave);
+}
+
+static struct class spi_slave_class = {
+ .name = "spi_slave",
+ .owner = THIS_MODULE,
+ .dev_release = spi_slave_release,
+};
/**
* spi_alloc_master - allocate SPI master controller
@@ -456,6 +613,47 @@ struct spi_master *spi_alloc_master(struct device *dev,
unsigned size)
EXPORT_SYMBOL_GPL(spi_alloc_master);
/**
+* spi_alloc_slave - allocate SPI slave controller
+* @dev: the controller, possibly using the platform_bus
+* @size: how much zeroed driver-private data to allocate; the pointer to this
+* memory is in the driver_data field of the returned device,
+* accessible with spi_slave_get_devdata().
+* Context: can sleep
+*
+* This call is used only by SPI master controller drivers, which are the
+* only ones directly touching chip registers. It's how they allocate
+* an spi_master structure, prior to calling spi_register_slave().
+*
+* This must be called from context that can sleep. It returns the SPI
+* master structure on success, else NULL.
+*
+* The caller is responsible for assigning the bus number and initializing
+* the master's methods before calling spi_register_slave(); and (after errors
+* adding the device) calling spi_slave_put() to prevent a memory leak.
+*/
+struct spi_slave *spi_alloc_slave(struct device *dev, unsigned size)
+{
+ struct spi_slave *slave;
+
+ if (!dev)
+ return NULL;
+
+ slave = kzalloc(size + sizeof *slave, GFP_KERNEL);
+ if (!slave)
+ return NULL;
+
+ device_initialize(&slave->dev);
+ slave->dev.class = &spi_slave_class;
+ slave->dev.parent = get_device(dev);
+ spi_slave_set_devdata(slave, &slave[1]);
+
+ return slave;
+}
+EXPORT_SYMBOL_GPL(spi_alloc_slave);
+
+
+
+/**
* spi_register_master - register SPI master controller
* @master: initialized master, originally from spi_alloc_master()
* Context: can sleep
@@ -507,7 +705,8 @@ int spi_register_master(struct spi_master *master)
status = device_add(&master->dev);
if (status < 0)
goto done;
- dev_dbg(dev, "registered master %s%s\n", dev_name(&master->dev),
+
+ dev_dbg(dev, "spi_register_master() : %s%s\n", dev_name(&master->dev),
dynamic ? " (dynamic)" : "");
/* populate children from any spi device tables */
@@ -518,6 +717,69 @@ done:
}
EXPORT_SYMBOL_GPL(spi_register_master);
+/**
+* spi_register_slave - register SPI slave controller
+* @master: initialized master, originally from spi_alloc_slave()
+* Context: can sleep
+*
+* SPI slave controllers connect to their drivers using some non-SPI bus,
+* such as the platform bus. The final stage of probe() in that code
+* includes calling spi_register_slave() to hook up to this SPI bus glue.
+*
+* SPI controllers use board specific (often SOC specific) bus numbers,
+* and board-specific addressing for SPI devices combines those numbers
+* with chip select numbers. Since SPI does not directly support dynamic
+* device identification, boards need configuration tables telling which
+* chip is at which address.
+*
+* This must be called from context that can sleep. It returns zero on
+* success, else a negative error code (dropping the slave's refcount).
+* After a successful return, the caller is responsible for calling
+* spi_unregister_slave().
+*/
+int spi_register_slave(struct spi_slave *slave)
+{
+ static atomic_t dyn_bus_id = ATOMIC_INIT((1<<15) - 1);
+ struct device *dev = slave->dev.parent;
+ int status = -ENODEV;
+ int dynamic = 0;
+
+ if (!dev)
+ return -ENODEV;
+
+ /* even if it's just one always-selected device, there must
+ * be at least one chipselect
+ */
+ if (slave->num_chipselect == 0)
+ return -EINVAL;
+
+ /* convention: dynamically assigned bus IDs count down from the max */
+ if (slave->bus_num < 0) {
+ /* FIXME switch to an IDR based scheme, something like
+ * I2C now uses, so we can't run out of "dynamic" IDs
+ */
+ slave->bus_num = atomic_dec_return(&dyn_bus_id);
+ dynamic = 1;
+ }
+
+ /* register the device, then userspace will see it.
+ * registration fails if the bus ID is in use.
+ */
+ dev_set_name(&slave->dev, "spi%u", slave->bus_num);
+ status = device_add(&slave->dev);
+ if (status < 0)
+ goto done;
+
+ dev_dbg(dev, "registered slave %s%s\n", dev_name(&slave->dev),
+ dynamic ? " (dynamic)" : "");
+
+ /* populate children from any spi device tables */
+ spi_slave_scan_boardinfo(slave);
+ status = 0;
+done:
+ return status;
+}
+EXPORT_SYMBOL_GPL(spi_register_slave);
static int __unregister(struct device *dev, void *master_dev)
{
@@ -547,6 +809,27 @@ void spi_unregister_master(struct spi_master *master)
}
EXPORT_SYMBOL_GPL(spi_unregister_master);
+/**
+* spi_unregister_slave - unregister SPI slave controller
+* @master: the slave being unregistered
+* Context: can sleep
+*
+* This call is used only by SPI slave controller drivers, which are the
+* only ones directly touching chip registers.
+*
+* This must be called from context that can sleep.
+*/
+void spi_unregister_slave(struct spi_slave *slave)
+{
+ int dummy;
+
+ dummy = device_for_each_child(slave->dev.parent, &slave->dev,
+ __unregister);
+ device_unregister(&slave->dev);
+}
+EXPORT_SYMBOL_GPL(spi_unregister_slave);
+
+
static int __spi_master_match(struct device *dev, void *data)
{
struct spi_master *m;
@@ -626,6 +909,18 @@ int spi_sync(struct spi_device *spi, struct spi_message
*message)
}
EXPORT_SYMBOL_GPL(spi_sync);
+/* spi_transfer_async - Wraper function to allow spi_async to expose to
+* user protocol drivers for modem handshaking
+*/
+
+int spi_transfer_async(struct spi_device *spi, struct spi_message *message)
+{
+ int status;
+ status = spi_async(spi, message);
+ return status;
+}
+EXPORT_SYMBOL_GPL(spi_transfer_async);
+
/* portable code must never pass more than 32 bytes */
#define SPI_BUFSIZ max(32,SMP_CACHE_BYTES)
@@ -724,6 +1019,12 @@ static int __init spi_init(void)
status = class_register(&spi_master_class);
if (status < 0)
goto err2;
+
+ status = class_register(&spi_slave_class);
+
+ if (status < 0)
+ goto err2;
+
return 0;
err2:
@@ -743,4 +1044,3 @@ err0:
* include needing to have boardinfo data structures be much more public.
*/
postcore_initcall(spi_init);
-
diff --git a/include/linux/spi/spi.h b/include/linux/spi/spi.h
index a0faa18..5845bb1 100644
--- a/include/linux/spi/spi.h
+++ b/include/linux/spi/spi.h
@@ -22,15 +22,18 @@
#include <linux/device.h>
/*
- * INTERFACES between SPI master-side drivers and SPI infrastructure.
- * (There's no SPI slave support for Linux yet...)
+ * INTERFACES between SPI Master/Slave side drivers and
+ * SPI infrastructure.
+ * SPI Slave Support added : It uses few new APIs and
+ * a new spi_slave struct
*/
extern struct bus_type spi_bus_type;
/**
* struct spi_device - Master side proxy for an SPI slave device
* @dev: Driver model representation of the device.
- * @master: SPI controller used with the device.
+ * @master: SPI Master controller used with the device.
+ * @slave: SPI Slave Controller used with the device
* @max_speed_hz: Maximum clock rate to be used with this chip
* (on this board); may be changed by the device's driver.
* The spi_transfer.speed_hz can override this for each transfer.
@@ -67,6 +70,7 @@ extern struct bus_type spi_bus_type;
struct spi_device {
struct device dev;
struct spi_master *master;
+ struct spi_slave *slave;
u32 max_speed_hz;
u8 chip_select;
u8 mode;
@@ -140,7 +144,6 @@ static inline void *spi_get_drvdata(struct spi_device *spi)
struct spi_message;
-
/**
* struct spi_driver - Host side "protocol" driver
* @probe: Binds this driver to the spi device. Drivers can verify
@@ -279,16 +282,56 @@ struct spi_master {
void (*cleanup)(struct spi_device *spi);
};
+/**
+ * struct spi_slave - interface to SPI Slave Controller
+ * @dev: device interface to this driver
+ * @bus_num: board-specific (and often SOC-specific) identifier for a
+ * given SPI controller.
+ * @num_chipselect: chipselects are used to distinguish individual
+ * SPI slaves, and are numbered from zero to num_chipselects.
+ * each slave has a chipselect signal, but it's common that not
+ * every chipselect is connected to a slave.
+ * @setup: updates the device mode and clocking records used by a
+ * device's SPI controller; protocol code may call this. This
+ * must fail if an unrecognized or unsupported mode is requested.
+ * It's always safe to call this unless transfers are pending on
+ * the device whose settings are being modified.
+ * @transfer: adds a message to the controller's transfer queue.
+ * @cleanup: frees controller-specific state
+ */
+struct spi_slave {
+ struct device dev;
+ s16 bus_num;
+ u16 num_chipselect;
+
+ int (*setup)(struct spi_device *spi);
+
+ int (*transfer)(struct spi_device *spi,
+ struct spi_message *mesg);
+
+ void (*cleanup)(struct spi_device *spi);
+};
+
static inline void *spi_master_get_devdata(struct spi_master *master)
{
return dev_get_drvdata(&master->dev);
}
+static inline void *spi_slave_get_devdata(struct spi_slave *slave)
+{
+ return dev_get_drvdata(&slave->dev);
+}
+
static inline void spi_master_set_devdata(struct spi_master *master, void
*data)
{
dev_set_drvdata(&master->dev, data);
}
+static inline void spi_slave_set_devdata(struct spi_slave *slave, void *data)
+{
+ dev_set_drvdata(&slave->dev, data);
+}
+
static inline struct spi_master *spi_master_get(struct spi_master *master)
{
if (!master || !get_device(&master->dev))
@@ -296,20 +339,42 @@ static inline struct spi_master *spi_master_get(struct
spi_master *master)
return master;
}
+static inline struct spi_slave *spi_slave_get(struct spi_slave *slave)
+{
+ if (!slave || !get_device(&slave->dev))
+ return NULL;
+ return slave;
+}
+
static inline void spi_master_put(struct spi_master *master)
{
if (master)
put_device(&master->dev);
}
+static inline void spi_slave_put(struct spi_slave *slave)
+{
+ if (slave)
+ put_device(&slave->dev);
+}
+
/* the spi driver core manages memory for the spi_master classdev */
extern struct spi_master *
spi_alloc_master(struct device *host, unsigned size);
+extern struct spi_slave *
+spi_alloc_slave(struct device *host, unsigned size);
+
+
extern int spi_register_master(struct spi_master *master);
+
+extern int spi_register_slave(struct spi_slave *slave);
+
extern void spi_unregister_master(struct spi_master *master);
+extern void spi_unregister_slave(struct spi_slave *slave);
+
extern struct spi_master *spi_busnum_to_master(u16 busnum);
/*---------------------------------------------------------------------------*/
@@ -544,10 +609,12 @@ static inline void spi_message_free(struct spi_message *m)
static inline int
spi_setup(struct spi_device *spi)
{
- return spi->master->setup(spi);
+ if (spi->master)
+ return spi->master->setup(spi);
+ else
+ return spi->slave->setup(spi);
}
-
/**
* spi_async - asynchronous SPI transfer
* @spi: device with which data will be exchanged
@@ -581,7 +648,10 @@ static inline int
spi_async(struct spi_device *spi, struct spi_message *message)
{
message->spi = spi;
- return spi->master->transfer(spi, message);
+ if (spi->master)
+ return spi->master->transfer(spi, message); /* Master */
+ else
+ return spi->slave->transfer(spi, message); /* Slave */
}
/*---------------------------------------------------------------------------*/
@@ -593,6 +663,11 @@ spi_async(struct spi_device *spi, struct spi_message
*message)
extern int spi_sync(struct spi_device *spi, struct spi_message *message);
+/* spi_transfer_async() exposes spi_async() functionality */
+extern int spi_transfer_async(struct spi_device *spi,
+ struct spi_message *message);
+
+
/**
* spi_write - SPI synchronous write
* @spi: device to which data will be written
@@ -801,12 +876,23 @@ spi_register_board_info(struct spi_board_info const
*info, unsigned n)
extern struct spi_device *
spi_alloc_device(struct spi_master *master);
+extern struct spi_device *
+spi_alloc_slave_device(struct spi_slave *slave);
+
extern int
spi_add_device(struct spi_device *spi);
+extern int
+spi_add_slave_device(struct spi_device *spi);
+
+
extern struct spi_device *
spi_new_device(struct spi_master *, struct spi_board_info *);
+extern struct spi_device *
+spi_slave_new_device(struct spi_slave *, struct spi_board_info *);
+
+
static inline void
spi_unregister_device(struct spi_device *spi)
{
--
1.5.4.3
------------------------------------------------------------------------------
Crystal Reports - New Free Runtime and 30 Day Trial
Check out the new simplified licensing option that enables unlimited
royalty-free distribution of the report engine for externally facing
server and web deployment.
http://p.sf.net/sfu/businessobjects
_______________________________________________
spi-devel-general mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/spi-devel-general