Commit b9618c0c authored by Michael Hennerich's avatar Michael Hennerich Committed by Greg Kroah-Hartman

staging: IIO: ADC: New driver for AD7606/AD7606-6/AD7606-4

This patch adds support for the:
AD7606/AD7606-6/AD7606-4 8/6/4-Channel Data Acquisition
system (DAS) with 16-Bit, Bipolar, Simultaneous Sampling ADC.

Changes since V1:
IIO: ADC: New driver for AD7606/AD7606-6/AD7606-4: Apply review feedback

Rename sysfs node oversampling to oversampling_ratio.
Kconfig: Add GPIOLIB dependency.
Use range in mV to better match HWMON.
Rename ad7606_check_oversampling.
Fix various comments and style.
Reorder is_visible cases.
Use new gpio_request_one/array and friends.
Drop check for SPI max_speed_hz.

Changes since V2:
IIO: ADC: New driver for AD7606/AD7606-6/AD7606-4: Apply review feedback

Documentation: specify unit
Avoid raise condition in ad7606_scan_direct()
Check return value of bus ops read_block()

Changes since V3:
IIO: ADC: New driver for AD7606/AD7606-6/AD7606-4: Add missing include file

Add linux/sched.h

Changes since V4:
IIO: ADC: New driver for AD7606/AD7606-6/AD7606-4: Fix kconfig declaration

consistently use tristate to avoid configuration mismatches
Signed-off-by: default avatarMichael Hennerich <michael.hennerich@analog.com>
Acked-by: default avatarJonathan Cameron <jic23@cam.ac.uk>
Signed-off-by: default avatarGreg Kroah-Hartman <gregkh@suse.de>
parent 363907af
......@@ -53,6 +53,31 @@ Description:
When the internal sampling clock can only take a small
discrete set of values, this file lists those available.
What: /sys/bus/iio/devices/deviceX/range
KernelVersion: 2.6.38
Contact: linux-iio@vger.kernel.org
Description:
Hardware dependent ADC Full Scale Range in mVolt.
What: /sys/bus/iio/devices/deviceX/range_available
KernelVersion: 2.6.38
Contact: linux-iio@vger.kernel.org
Description:
Hardware dependent supported vales for ADC Full Scale Range.
What: /sys/bus/iio/devices/deviceX/oversampling_ratio
KernelVersion: 2.6.38
Contact: linux-iio@vger.kernel.org
Description:
Hardware dependent ADC oversampling. Controls the sampling ratio
of the digital filter if available.
What: /sys/bus/iio/devices/deviceX/oversampling_ratio_available
KernelVersion: 2.6.38
Contact: linux-iio@vger.kernel.org
Description:
Hardware dependent values supported by the oversampling filter.
What: /sys/bus/iio/devices/deviceX/inY_raw
What: /sys/bus/iio/devices/deviceX/inY_supply_raw
KernelVersion: 2.6.35
......
......@@ -62,6 +62,34 @@ config AD7314
Say yes here to build support for Analog Devices AD7314
temperature sensors.
config AD7606
tristate "Analog Devices AD7606 ADC driver"
depends on GPIOLIB
select IIO_RING_BUFFER
select IIO_TRIGGER
select IIO_SW_RING
help
Say yes here to build support for Analog Devices:
ad7606, ad7606-6, ad7606-4 analog to digital convertors (ADC).
To compile this driver as a module, choose M here: the
module will be called ad7606.
config AD7606_IFACE_PARALLEL
tristate "parallel interface support"
depends on AD7606
help
Say yes here to include parallel interface support on the AD7606
ADC driver.
config AD7606_IFACE_SPI
tristate "spi interface support"
depends on AD7606
depends on SPI
help
Say yes here to include parallel interface support on the AD7606
ADC driver.
config AD799X
tristate "Analog Devices AD799x ADC driver"
depends on I2C
......
......@@ -7,6 +7,12 @@ max1363-y += max1363_ring.o
obj-$(CONFIG_MAX1363) += max1363.o
ad7606-y := ad7606_core.o
ad7606-$(CONFIG_IIO_RING_BUFFER) += ad7606_ring.o
ad7606-$(CONFIG_AD7606_IFACE_PARALLEL) += ad7606_par.o
ad7606-$(CONFIG_AD7606_IFACE_SPI) += ad7606_spi.o
obj-$(CONFIG_AD7606) += ad7606.o
ad799x-y := ad799x_core.o
ad799x-$(CONFIG_AD799X_RING_BUFFER) += ad799x_ring.o
obj-$(CONFIG_AD799X) += ad799x.o
......
/*
* AD7606 ADC driver
*
* Copyright 2011 Analog Devices Inc.
*
* Licensed under the GPL-2.
*/
#ifndef IIO_ADC_AD7606_H_
#define IIO_ADC_AD7606_H_
/*
* TODO: struct ad7606_platform_data needs to go into include/linux/iio
*/
/**
* struct ad7606_platform_data - platform/board specifc information
* @default_os: default oversampling value {0, 2, 4, 8, 16, 32, 64}
* @default_range: default range +/-{5000, 10000} mVolt
* @gpio_convst: number of gpio connected to the CONVST pin
* @gpio_reset: gpio connected to the RESET pin, if not used set to -1
* @gpio_range: gpio connected to the RANGE pin, if not used set to -1
* @gpio_os0: gpio connected to the OS0 pin, if not used set to -1
* @gpio_os1: gpio connected to the OS1 pin, if not used set to -1
* @gpio_os2: gpio connected to the OS2 pin, if not used set to -1
* @gpio_frstdata: gpio connected to the FRSTDAT pin, if not used set to -1
* @gpio_stby: gpio connected to the STBY pin, if not used set to -1
*/
struct ad7606_platform_data {
unsigned default_os;
unsigned default_range;
unsigned gpio_convst;
unsigned gpio_reset;
unsigned gpio_range;
unsigned gpio_os0;
unsigned gpio_os1;
unsigned gpio_os2;
unsigned gpio_frstdata;
unsigned gpio_stby;
};
/**
* struct ad7606_chip_info - chip specifc information
* @name: indentification string for chip
* @bits: accuracy of the adc in bits
* @bits: output coding [s]igned or [u]nsigned
* @int_vref_mv: the internal reference voltage
* @num_channels: number of physical inputs on chip
*/
struct ad7606_chip_info {
char name[10];
u8 bits;
char sign;
u16 int_vref_mv;
unsigned num_channels;
};
/**
* struct ad7606_state - driver instance specific data
*/
struct ad7606_state {
struct iio_dev *indio_dev;
struct device *dev;
const struct ad7606_chip_info *chip_info;
struct ad7606_platform_data *pdata;
struct regulator *reg;
struct work_struct poll_work;
wait_queue_head_t wq_data_avail;
atomic_t protect_ring;
size_t d_size;
const struct ad7606_bus_ops *bops;
int irq;
unsigned id;
unsigned range;
unsigned oversampling;
bool done;
bool have_frstdata;
bool have_os;
bool have_stby;
bool have_reset;
bool have_range;
void __iomem *base_address;
/*
* DMA (thus cache coherency maintenance) requires the
* transfer buffers to live in their own cache lines.
*/
unsigned short data[8] ____cacheline_aligned;
};
struct ad7606_bus_ops {
/* more methods added in future? */
int (*read_block)(struct device *, int, void *);
};
void ad7606_suspend(struct ad7606_state *st);
void ad7606_resume(struct ad7606_state *st);
struct ad7606_state *ad7606_probe(struct device *dev, int irq,
void __iomem *base_address, unsigned id,
const struct ad7606_bus_ops *bops);
int ad7606_remove(struct ad7606_state *st);
int ad7606_reset(struct ad7606_state *st);
enum ad7606_supported_device_ids {
ID_AD7606_8,
ID_AD7606_6,
ID_AD7606_4
};
int ad7606_scan_from_ring(struct ad7606_state *st, unsigned ch);
int ad7606_register_ring_funcs_and_init(struct iio_dev *indio_dev);
void ad7606_ring_cleanup(struct iio_dev *indio_dev);
#endif /* IIO_ADC_AD7606_H_ */
This diff is collapsed.
/*
* AD7606 Parallel Interface ADC driver
*
* Copyright 2011 Analog Devices Inc.
*
* Licensed under the GPL-2.
*/
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/types.h>
#include <linux/err.h>
#include <linux/io.h>
#include "ad7606.h"
static int ad7606_par16_read_block(struct device *dev,
int count, void *buf)
{
struct platform_device *pdev = to_platform_device(dev);
struct ad7606_state *st = platform_get_drvdata(pdev);
insw((unsigned long) st->base_address, buf, count);
return 0;
}
static const struct ad7606_bus_ops ad7606_par16_bops = {
.read_block = ad7606_par16_read_block,
};
static int ad7606_par8_read_block(struct device *dev,
int count, void *buf)
{
struct platform_device *pdev = to_platform_device(dev);
struct ad7606_state *st = platform_get_drvdata(pdev);
insb((unsigned long) st->base_address, buf, count * 2);
return 0;
}
static const struct ad7606_bus_ops ad7606_par8_bops = {
.read_block = ad7606_par8_read_block,
};
static int __devinit ad7606_par_probe(struct platform_device *pdev)
{
struct resource *res;
struct ad7606_state *st;
void __iomem *addr;
resource_size_t remap_size;
int ret, irq;
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "no irq\n");
return -ENODEV;
}
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
remap_size = resource_size(res);
/* Request the regions */
if (!request_mem_region(res->start, remap_size, "iio-ad7606")) {
ret = -EBUSY;
goto out1;
}
addr = ioremap(res->start, remap_size);
if (!addr) {
ret = -ENOMEM;
goto out1;
}
st = ad7606_probe(&pdev->dev, irq, addr,
platform_get_device_id(pdev)->driver_data,
remap_size > 1 ? &ad7606_par16_bops :
&ad7606_par8_bops);
if (IS_ERR(st)) {
ret = PTR_ERR(st);
goto out2;
}
platform_set_drvdata(pdev, st);
return 0;
out2:
iounmap(addr);
out1:
release_mem_region(res->start, remap_size);
return ret;
}
static int __devexit ad7606_par_remove(struct platform_device *pdev)
{
struct ad7606_state *st = platform_get_drvdata(pdev);
struct resource *res;
ad7606_remove(st);
iounmap(st->base_address);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
release_mem_region(res->start, resource_size(res));
platform_set_drvdata(pdev, NULL);
return 0;
}
#ifdef CONFIG_PM
static int ad7606_par_suspend(struct device *dev)
{
struct ad7606_state *st = dev_get_drvdata(dev);
ad7606_suspend(st);
return 0;
}
static int ad7606_par_resume(struct device *dev)
{
struct ad7606_state *st = dev_get_drvdata(dev);
ad7606_resume(st);
return 0;
}
static const struct dev_pm_ops ad7606_pm_ops = {
.suspend = ad7606_par_suspend,
.resume = ad7606_par_resume,
};
#define AD7606_PAR_PM_OPS (&ad7606_pm_ops)
#else
#define AD7606_PAR_PM_OPS NULL
#endif /* CONFIG_PM */
static struct platform_device_id ad7606_driver_ids[] = {
{
.name = "ad7606-8",
.driver_data = ID_AD7606_8,
}, {
.name = "ad7606-6",
.driver_data = ID_AD7606_6,
}, {
.name = "ad7606-4",
.driver_data = ID_AD7606_4,
},
{ }
};
MODULE_DEVICE_TABLE(platform, ad7606_driver_ids);
static struct platform_driver ad7606_driver = {
.probe = ad7606_par_probe,
.remove = __devexit_p(ad7606_par_remove),
.id_table = ad7606_driver_ids,
.driver = {
.name = "ad7606",
.owner = THIS_MODULE,
.pm = AD7606_PAR_PM_OPS,
},
};
static int __init ad7606_init(void)
{
return platform_driver_register(&ad7606_driver);
}
static void __exit ad7606_cleanup(void)
{
platform_driver_unregister(&ad7606_driver);
}
module_init(ad7606_init);
module_exit(ad7606_cleanup);
MODULE_AUTHOR("Michael Hennerich <hennerich@blackfin.uclinux.org>");
MODULE_DESCRIPTION("Analog Devices AD7606 ADC");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:ad7606_par");
/*
* Copyright 2011 Analog Devices Inc.
*
* Licensed under the GPL-2.
*
*/
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <linux/workqueue.h>
#include <linux/device.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/sysfs.h>
#include "../iio.h"
#include "../ring_generic.h"
#include "../ring_sw.h"
#include "../trigger.h"
#include "../sysfs.h"
#include "ad7606.h"
static IIO_SCAN_EL_C(in0, 0, 0, NULL);
static IIO_SCAN_EL_C(in1, 1, 0, NULL);
static IIO_SCAN_EL_C(in2, 2, 0, NULL);
static IIO_SCAN_EL_C(in3, 3, 0, NULL);
static IIO_SCAN_EL_C(in4, 4, 0, NULL);
static IIO_SCAN_EL_C(in5, 5, 0, NULL);
static IIO_SCAN_EL_C(in6, 6, 0, NULL);
static IIO_SCAN_EL_C(in7, 7, 0, NULL);
static ssize_t ad7606_show_type(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct iio_ring_buffer *ring = dev_get_drvdata(dev);
struct iio_dev *indio_dev = ring->indio_dev;
struct ad7606_state *st = indio_dev->dev_data;
return sprintf(buf, "%c%d/%d\n", st->chip_info->sign,
st->chip_info->bits, st->chip_info->bits);
}
static IIO_DEVICE_ATTR(in_type, S_IRUGO, ad7606_show_type, NULL, 0);
static struct attribute *ad7606_scan_el_attrs[] = {
&iio_scan_el_in0.dev_attr.attr,
&iio_const_attr_in0_index.dev_attr.attr,
&iio_scan_el_in1.dev_attr.attr,
&iio_const_attr_in1_index.dev_attr.attr,
&iio_scan_el_in2.dev_attr.attr,
&iio_const_attr_in2_index.dev_attr.attr,
&iio_scan_el_in3.dev_attr.attr,
&iio_const_attr_in3_index.dev_attr.attr,
&iio_scan_el_in4.dev_attr.attr,
&iio_const_attr_in4_index.dev_attr.attr,
&iio_scan_el_in5.dev_attr.attr,
&iio_const_attr_in5_index.dev_attr.attr,
&iio_scan_el_in6.dev_attr.attr,
&iio_const_attr_in6_index.dev_attr.attr,
&iio_scan_el_in7.dev_attr.attr,
&iio_const_attr_in7_index.dev_attr.attr,
&iio_dev_attr_in_type.dev_attr.attr,
NULL,
};
static mode_t ad7606_scan_el_attr_is_visible(struct kobject *kobj,
struct attribute *attr, int n)
{
struct device *dev = container_of(kobj, struct device, kobj);
struct iio_ring_buffer *ring = dev_get_drvdata(dev);
struct iio_dev *indio_dev = ring->indio_dev;
struct ad7606_state *st = indio_dev->dev_data;
mode_t mode = attr->mode;
if (st->chip_info->num_channels <= 6 &&
(attr == &iio_scan_el_in7.dev_attr.attr ||
attr == &iio_const_attr_in7_index.dev_attr.attr ||
attr == &iio_scan_el_in6.dev_attr.attr ||
attr == &iio_const_attr_in6_index.dev_attr.attr))
mode = 0;
else if (st->chip_info->num_channels <= 4 &&
(attr == &iio_scan_el_in5.dev_attr.attr ||
attr == &iio_const_attr_in5_index.dev_attr.attr ||
attr == &iio_scan_el_in4.dev_attr.attr ||
attr == &iio_const_attr_in4_index.dev_attr.attr))
mode = 0;
return mode;
}
static struct attribute_group ad7606_scan_el_group = {
.name = "scan_elements",
.attrs = ad7606_scan_el_attrs,
.is_visible = ad7606_scan_el_attr_is_visible,
};
int ad7606_scan_from_ring(struct ad7606_state *st, unsigned ch)
{
struct iio_ring_buffer *ring = st->indio_dev->ring;
int ret;
u16 *ring_data;
ring_data = kmalloc(ring->access.get_bytes_per_datum(ring), GFP_KERNEL);
if (ring_data == NULL) {
ret = -ENOMEM;
goto error_ret;
}
ret = ring->access.read_last(ring, (u8 *) ring_data);
if (ret)
goto error_free_ring_data;
ret = ring_data[ch];
error_free_ring_data:
kfree(ring_data);
error_ret:
return ret;
}
/**
* ad7606_ring_preenable() setup the parameters of the ring before enabling
*
* The complex nature of the setting of the nuber of bytes per datum is due
* to this driver currently ensuring that the timestamp is stored at an 8
* byte boundary.
**/
static int ad7606_ring_preenable(struct iio_dev *indio_dev)
{
struct ad7606_state *st = indio_dev->dev_data;
struct iio_ring_buffer *ring = indio_dev->ring;
size_t d_size;
d_size = st->chip_info->num_channels *
st->chip_info->bits / 8 + sizeof(s64);
if (d_size % sizeof(s64))
d_size += sizeof(s64) - (d_size % sizeof(s64));
if (ring->access.set_bytes_per_datum)
ring->access.set_bytes_per_datum(ring, d_size);
st->d_size = d_size;
return 0;
}
/**
* ad7606_poll_func_th() th of trigger launched polling to ring buffer
*
**/
static void ad7606_poll_func_th(struct iio_dev *indio_dev, s64 time)
{
struct ad7606_state *st = indio_dev->dev_data;
gpio_set_value(st->pdata->gpio_convst, 1);
return;
}
/**
* ad7606_poll_bh_to_ring() bh of trigger launched polling to ring buffer
* @work_s: the work struct through which this was scheduled
*
* Currently there is no option in this driver to disable the saving of
* timestamps within the ring.
* I think the one copy of this at a time was to avoid problems if the
* trigger was set far too high and the reads then locked up the computer.
**/
static void ad7606_poll_bh_to_ring(struct work_struct *work_s)
{
struct ad7606_state *st = container_of(work_s, struct ad7606_state,
poll_work);
struct iio_dev *indio_dev = st->indio_dev;
struct iio_sw_ring_buffer *sw_ring = iio_to_sw_ring(indio_dev->ring);
struct iio_ring_buffer *ring = indio_dev->ring;
s64 time_ns;
__u8 *buf;
int ret;
/* Ensure only one copy of this function running at a time */
if (atomic_inc_return(&st->protect_ring) > 1)
return;
buf = kzalloc(st->d_size, GFP_KERNEL);
if (buf == NULL)
return;
if (st->have_frstdata) {
ret = st->bops->read_block(st->dev, 1, buf);
if (ret)
goto done;
if (!gpio_get_value(st->pdata->gpio_frstdata)) {
/* This should never happen. However
* some signal glitch caused by bad PCB desgin or
* electrostatic discharge, could cause an extra read
* or clock. This allows recovery.
*/
ad7606_reset(st);
goto done;
}
ret = st->bops->read_block(st->dev,
st->chip_info->num_channels - 1, buf + 2);
if (ret)
goto done;
} else {
ret = st->bops->read_block(st->dev,
st->chip_info->num_channels, buf);
if (ret)
goto done;
}
time_ns = iio_get_time_ns();
memcpy(buf + st->d_size - sizeof(s64), &time_ns, sizeof(time_ns));
ring->access.store_to(&sw_ring->buf, buf, time_ns);
done:
gpio_set_value(st->pdata->gpio_convst, 0);
kfree(buf);
atomic_dec(&st->protect_ring);
}
int ad7606_register_ring_funcs_and_init(struct iio_dev *indio_dev)
{
struct ad7606_state *st = indio_dev->dev_data;
int ret;
indio_dev->ring = iio_sw_rb_allocate(indio_dev);
if (!indio_dev->ring) {
ret = -ENOMEM;
goto error_ret;
}
/* Effectively select the ring buffer implementation */
iio_ring_sw_register_funcs(&indio_dev->ring->access);
ret = iio_alloc_pollfunc(indio_dev, NULL, &ad7606_poll_func_th);
if (ret)
goto error_deallocate_sw_rb;
/* Ring buffer functions - here trigger setup related */
indio_dev->ring->preenable = &ad7606_ring_preenable;
indio_dev->ring->postenable = &iio_triggered_ring_postenable;
indio_dev->ring->predisable = &iio_triggered_ring_predisable;
indio_dev->ring->scan_el_attrs = &ad7606_scan_el_group;
INIT_WORK(&st->poll_work, &ad7606_poll_bh_to_ring);
/* Flag that polled ring buffering is possible */
indio_dev->modes |= INDIO_RING_TRIGGERED;
return 0;
error_deallocate_sw_rb:
iio_sw_rb_free(indio_dev->ring);
error_ret:
return ret;
}
void ad7606_ring_cleanup(struct iio_dev *indio_dev)
{
if (indio_dev->trig) {
iio_put_trigger(indio_dev->trig);
iio_trigger_dettach_poll_func(indio_dev->trig,
indio_dev->pollfunc);
}
kfree(indio_dev->pollfunc);
iio_sw_rb_free(indio_dev->ring);
}
/*
* AD7606 SPI ADC driver
*
* Copyright 2011 Analog Devices Inc.
*
* Licensed under the GPL-2.
*/
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/types.h>
#include <linux/err.h>
#include "ad7606.h"
#define MAX_SPI_FREQ_HZ 23500000 /* VDRIVE above 4.75 V */
static int ad7606_spi_read_block(struct device *dev,
int count, void *buf)
{
struct spi_device *spi = to_spi_device(dev);
int i, ret;
unsigned short *data = buf;
ret = spi_read(spi, (u8 *)buf, count * 2);
if (ret < 0) {
dev_err(&spi->dev, "SPI read error\n");
return ret;
}
for (i = 0; i < count; i++)
data[i] = be16_to_cpu(data[i]);
return 0;
}
static const struct ad7606_bus_ops ad7606_spi_bops = {
.read_block = ad7606_spi_read_block,
};
static int __devinit ad7606_spi_probe(struct spi_device *spi)
{
struct ad7606_state *st;
st = ad7606_probe(&spi->dev, spi->irq, NULL,
spi_get_device_id(spi)->driver_data,
&ad7606_spi_bops);
if (IS_ERR(st))
return PTR_ERR(st);
spi_set_drvdata(spi, st);
return 0;
}
static int __devexit ad7606_spi_remove(struct spi_device *spi)
{
struct ad7606_state *st = dev_get_drvdata(&spi->dev);
return ad7606_remove(st);
}
#ifdef CONFIG_PM
static int ad7606_spi_suspend(struct device *dev)
{
struct ad7606_state *st = dev_get_drvdata(dev);
ad7606_suspend(st);
return 0;
}
static int ad7606_spi_resume(struct device *dev)
{
struct ad7606_state *st = dev_get_drvdata(dev);
ad7606_resume(st);
return 0;
}
static const struct dev_pm_ops ad7606_pm_ops = {
.suspend = ad7606_spi_suspend,
.resume = ad7606_spi_resume,
};
#define AD7606_SPI_PM_OPS (&ad7606_pm_ops)
#else
#define AD7606_SPI_PM_OPS NULL
#endif
static const struct spi_device_id ad7606_id[] = {
{"ad7606-8", ID_AD7606_8},
{"ad7606-6", ID_AD7606_6},
{"ad7606-4", ID_AD7606_4},
{}
};
static struct spi_driver ad7606_driver = {
.driver = {
.name = "ad7606",
.bus = &spi_bus_type,
.owner = THIS_MODULE,
.pm = AD7606_SPI_PM_OPS,
},
.probe = ad7606_spi_probe,
.remove = __devexit_p(ad7606_spi_remove),
.id_table = ad7606_id,
};
static int __init ad7606_spi_init(void)
{
return spi_register_driver(&ad7606_driver);
}
module_init(ad7606_spi_init);
static void __exit ad7606_spi_exit(void)
{
spi_unregister_driver(&ad7606_driver);
}
module_exit(ad7606_spi_exit);
MODULE_AUTHOR("Michael Hennerich <hennerich@blackfin.uclinux.org>");
MODULE_DESCRIPTION("Analog Devices AD7606 ADC");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("spi:ad7606_spi");
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