blob: cc18e25103ca5976dd914de4b2021db28c10b6e6 [file] [log] [blame]
/*
* serial2002.c
* Comedi driver for serial connected hardware
*
* COMEDI - Linux Control and Measurement Device Interface
* Copyright (C) 2002 Anders Blomdell <anders.blomdell@control.lth.se>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
/*
* Driver: serial2002
* Description: Driver for serial connected hardware
* Devices:
* Author: Anders Blomdell
* Updated: Fri, 7 Jun 2002 12:56:45 -0700
* Status: in development
*/
#include <linux/module.h>
#include "../comedidev.h"
#include <linux/delay.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/ktime.h>
#include <linux/termios.h>
#include <asm/ioctls.h>
#include <linux/serial.h>
#include <linux/poll.h>
struct serial2002_range_table_t {
/* HACK... */
int length;
struct comedi_krange range;
};
struct serial2002_private {
int port; /* /dev/ttyS<port> */
int speed; /* baudrate */
struct file *tty;
unsigned int ao_readback[32];
unsigned char digital_in_mapping[32];
unsigned char digital_out_mapping[32];
unsigned char analog_in_mapping[32];
unsigned char analog_out_mapping[32];
unsigned char encoder_in_mapping[32];
struct serial2002_range_table_t in_range[32], out_range[32];
};
struct serial_data {
enum { is_invalid, is_digital, is_channel } kind;
int index;
unsigned long value;
};
/*
* The configuration serial_data.value read from the device is
* a bitmask that defines specific options of a channel:
*
* 4:0 - the channel to configure
* 7:5 - the kind of channel
* 9:8 - the command used to configure the channel
*
* The remaining bits vary in use depending on the command:
*
* BITS 15:10 - the channel bits (maxdata)
* MIN/MAX 12:10 - the units multiplier for the scale
* 13 - the sign of the scale
* 33:14 - the base value for the range
*/
#define S2002_CFG_CHAN(x) ((x) & 0x1f)
#define S2002_CFG_KIND(x) (((x) >> 5) & 0x7)
#define S2002_CFG_KIND_INVALID 0
#define S2002_CFG_KIND_DIGITAL_IN 1
#define S2002_CFG_KIND_DIGITAL_OUT 2
#define S2002_CFG_KIND_ANALOG_IN 3
#define S2002_CFG_KIND_ANALOG_OUT 4
#define S2002_CFG_KIND_ENCODER_IN 5
#define S2002_CFG_CMD(x) (((x) >> 8) & 0x3)
#define S2002_CFG_CMD_BITS 0
#define S2002_CFG_CMD_MIN 1
#define S2002_CFG_CMD_MAX 2
#define S2002_CFG_BITS(x) (((x) >> 10) & 0x3f)
#define S2002_CFG_UNITS(x) (((x) >> 10) & 0x7)
#define S2002_CFG_SIGN(x) (((x) >> 13) & 0x1)
#define S2002_CFG_BASE(x) (((x) >> 14) & 0xfffff)
static long serial2002_tty_ioctl(struct file *f, unsigned int op,
unsigned long param)
{
if (f->f_op->unlocked_ioctl)
return f->f_op->unlocked_ioctl(f, op, param);
return -ENOTTY;
}
static int serial2002_tty_write(struct file *f, unsigned char *buf, int count)
{
loff_t pos = 0;
return kernel_write(f, buf, count, &pos);
}
static void serial2002_tty_read_poll_wait(struct file *f, int timeout)
{
struct poll_wqueues table;
ktime_t start, now;
start = ktime_get();
poll_initwait(&table);
while (1) {
long elapsed;
int mask;
mask = f->f_op->poll(f, &table.pt);
if (mask & (POLLRDNORM | POLLRDBAND | POLLIN |
POLLHUP | POLLERR)) {
break;
}
now = ktime_get();
elapsed = ktime_us_delta(now, start);
if (elapsed > timeout)
break;
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout(((timeout - elapsed) * HZ) / 10000);
}
poll_freewait(&table);
}
static int serial2002_tty_read(struct file *f, int timeout)
{
unsigned char ch;
int result;
loff_t pos = 0;
result = -1;
if (!IS_ERR(f)) {
if (f->f_op->poll) {
serial2002_tty_read_poll_wait(f, timeout);
if (kernel_read(f, &ch, 1, &pos) == 1)
result = ch;
} else {
/* Device does not support poll, busy wait */
int retries = 0;
while (1) {
retries++;
if (retries >= timeout)
break;
if (kernel_read(f, &ch, 1, &pos) == 1) {
result = ch;
break;
}
usleep_range(100, 1000);
}
}
}
return result;
}
static void serial2002_tty_setspeed(struct file *f, int speed)
{
struct termios termios;
struct serial_struct serial;
mm_segment_t oldfs;
oldfs = get_fs();
set_fs(KERNEL_DS);
/* Set speed */
serial2002_tty_ioctl(f, TCGETS, (unsigned long)&termios);
termios.c_iflag = 0;
termios.c_oflag = 0;
termios.c_lflag = 0;
termios.c_cflag = CLOCAL | CS8 | CREAD;
termios.c_cc[VMIN] = 0;
termios.c_cc[VTIME] = 0;
switch (speed) {
case 2400:
termios.c_cflag |= B2400;
break;
case 4800:
termios.c_cflag |= B4800;
break;
case 9600:
termios.c_cflag |= B9600;
break;
case 19200:
termios.c_cflag |= B19200;
break;
case 38400:
termios.c_cflag |= B38400;
break;
case 57600:
termios.c_cflag |= B57600;
break;
case 115200:
termios.c_cflag |= B115200;
break;
default:
termios.c_cflag |= B9600;
break;
}
serial2002_tty_ioctl(f, TCSETS, (unsigned long)&termios);
/* Set low latency */
serial2002_tty_ioctl(f, TIOCGSERIAL, (unsigned long)&serial);
serial.flags |= ASYNC_LOW_LATENCY;
serial2002_tty_ioctl(f, TIOCSSERIAL, (unsigned long)&serial);
set_fs(oldfs);
}
static void serial2002_poll_digital(struct file *f, int channel)
{
char cmd;
cmd = 0x40 | (channel & 0x1f);
serial2002_tty_write(f, &cmd, 1);
}
static void serial2002_poll_channel(struct file *f, int channel)
{
char cmd;
cmd = 0x60 | (channel & 0x1f);
serial2002_tty_write(f, &cmd, 1);
}
static struct serial_data serial2002_read(struct file *f, int timeout)
{
struct serial_data result;
int length;
result.kind = is_invalid;
result.index = 0;
result.value = 0;
length = 0;
while (1) {
int data = serial2002_tty_read(f, timeout);
length++;
if (data < 0) {
break;
} else if (data & 0x80) {
result.value = (result.value << 7) | (data & 0x7f);
} else {
if (length == 1) {
switch ((data >> 5) & 0x03) {
case 0:
result.value = 0;
result.kind = is_digital;
break;
case 1:
result.value = 1;
result.kind = is_digital;
break;
}
} else {
result.value =
(result.value << 2) | ((data & 0x60) >> 5);
result.kind = is_channel;
}
result.index = data & 0x1f;
break;
}
}
return result;
}
static void serial2002_write(struct file *f, struct serial_data data)
{
if (data.kind == is_digital) {
unsigned char ch =
((data.value << 5) & 0x20) | (data.index & 0x1f);
serial2002_tty_write(f, &ch, 1);
} else {
unsigned char ch[6];
int i = 0;
if (data.value >= (1L << 30)) {
ch[i] = 0x80 | ((data.value >> 30) & 0x03);
i++;
}
if (data.value >= (1L << 23)) {
ch[i] = 0x80 | ((data.value >> 23) & 0x7f);
i++;
}
if (data.value >= (1L << 16)) {
ch[i] = 0x80 | ((data.value >> 16) & 0x7f);
i++;
}
if (data.value >= (1L << 9)) {
ch[i] = 0x80 | ((data.value >> 9) & 0x7f);
i++;
}
ch[i] = 0x80 | ((data.value >> 2) & 0x7f);
i++;
ch[i] = ((data.value << 5) & 0x60) | (data.index & 0x1f);
i++;
serial2002_tty_write(f, ch, i);
}
}
struct config_t {
short int kind;
short int bits;
int min;
int max;
};
static int serial2002_setup_subdevice(struct comedi_subdevice *s,
struct config_t *cfg,
struct serial2002_range_table_t *range,
unsigned char *mapping,
int kind)
{
const struct comedi_lrange **range_table_list = NULL;
unsigned int *maxdata_list;
int j, chan;
for (chan = 0, j = 0; j < 32; j++) {
if (cfg[j].kind == kind)
chan++;
}
s->n_chan = chan;
s->maxdata = 0;
kfree(s->maxdata_list);
maxdata_list = kmalloc_array(s->n_chan, sizeof(unsigned int),
GFP_KERNEL);
if (!maxdata_list)
return -ENOMEM;
s->maxdata_list = maxdata_list;
kfree(s->range_table_list);
s->range_table = NULL;
s->range_table_list = NULL;
if (kind == 1 || kind == 2) {
s->range_table = &range_digital;
} else if (range) {
range_table_list = kmalloc_array(s->n_chan, sizeof(*range),
GFP_KERNEL);
if (!range_table_list)
return -ENOMEM;
s->range_table_list = range_table_list;
}
for (chan = 0, j = 0; j < 32; j++) {
if (cfg[j].kind == kind) {
if (mapping)
mapping[chan] = j;
if (range && range_table_list) {
range[j].length = 1;
range[j].range.min = cfg[j].min;
range[j].range.max = cfg[j].max;
range_table_list[chan] =
(const struct comedi_lrange *)&range[j];
}
if (cfg[j].bits < 32)
maxdata_list[chan] = (1u << cfg[j].bits) - 1;
else
maxdata_list[chan] = 0xffffffff;
chan++;
}
}
return 0;
}
static int serial2002_setup_subdevs(struct comedi_device *dev)
{
struct serial2002_private *devpriv = dev->private;
struct config_t *di_cfg;
struct config_t *do_cfg;
struct config_t *ai_cfg;
struct config_t *ao_cfg;
struct config_t *cfg;
struct comedi_subdevice *s;
int result = 0;
int i;
/* Allocate the temporary structs to hold the configuration data */
di_cfg = kcalloc(32, sizeof(*cfg), GFP_KERNEL);
do_cfg = kcalloc(32, sizeof(*cfg), GFP_KERNEL);
ai_cfg = kcalloc(32, sizeof(*cfg), GFP_KERNEL);
ao_cfg = kcalloc(32, sizeof(*cfg), GFP_KERNEL);
if (!di_cfg || !do_cfg || !ai_cfg || !ao_cfg) {
result = -ENOMEM;
goto err_alloc_configs;
}
/* Read the configuration from the connected device */
serial2002_tty_setspeed(devpriv->tty, devpriv->speed);
serial2002_poll_channel(devpriv->tty, 31);
while (1) {
struct serial_data data = serial2002_read(devpriv->tty, 1000);
int kind = S2002_CFG_KIND(data.value);
int channel = S2002_CFG_CHAN(data.value);
int range = S2002_CFG_BASE(data.value);
int cmd = S2002_CFG_CMD(data.value);
if (data.kind != is_channel || data.index != 31 ||
kind == S2002_CFG_KIND_INVALID)
break;
switch (kind) {
case S2002_CFG_KIND_DIGITAL_IN:
cfg = di_cfg;
break;
case S2002_CFG_KIND_DIGITAL_OUT:
cfg = do_cfg;
break;
case S2002_CFG_KIND_ANALOG_IN:
cfg = ai_cfg;
break;
case S2002_CFG_KIND_ANALOG_OUT:
cfg = ao_cfg;
break;
case S2002_CFG_KIND_ENCODER_IN:
cfg = ai_cfg;
break;
default:
cfg = NULL;
break;
}
if (!cfg)
continue; /* unknown kind, skip it */
cfg[channel].kind = kind;
switch (cmd) {
case S2002_CFG_CMD_BITS:
cfg[channel].bits = S2002_CFG_BITS(data.value);
break;
case S2002_CFG_CMD_MIN:
case S2002_CFG_CMD_MAX:
switch (S2002_CFG_UNITS(data.value)) {
case 0:
range *= 1000000;
break;
case 1:
range *= 1000;
break;
case 2:
range *= 1;
break;
}
if (S2002_CFG_SIGN(data.value))
range = -range;
if (cmd == S2002_CFG_CMD_MIN)
cfg[channel].min = range;
else
cfg[channel].max = range;
break;
}
}
/* Fill in subdevice data */
for (i = 0; i <= 4; i++) {
unsigned char *mapping = NULL;
struct serial2002_range_table_t *range = NULL;
int kind = 0;
s = &dev->subdevices[i];
switch (i) {
case 0:
cfg = di_cfg;
mapping = devpriv->digital_in_mapping;
kind = S2002_CFG_KIND_DIGITAL_IN;
break;
case 1:
cfg = do_cfg;
mapping = devpriv->digital_out_mapping;
kind = S2002_CFG_KIND_DIGITAL_OUT;
break;
case 2:
cfg = ai_cfg;
mapping = devpriv->analog_in_mapping;
range = devpriv->in_range;
kind = S2002_CFG_KIND_ANALOG_IN;
break;
case 3:
cfg = ao_cfg;
mapping = devpriv->analog_out_mapping;
range = devpriv->out_range;
kind = S2002_CFG_KIND_ANALOG_OUT;
break;
case 4:
cfg = ai_cfg;
mapping = devpriv->encoder_in_mapping;
range = devpriv->in_range;
kind = S2002_CFG_KIND_ENCODER_IN;
break;
}
if (serial2002_setup_subdevice(s, cfg, range, mapping, kind))
break; /* err handled below */
}
if (i <= 4) {
/*
* Failed to allocate maxdata_list or range_table_list
* for a subdevice that needed it.
*/
result = -ENOMEM;
for (i = 0; i <= 4; i++) {
s = &dev->subdevices[i];
kfree(s->maxdata_list);
s->maxdata_list = NULL;
kfree(s->range_table_list);
s->range_table_list = NULL;
}
}
err_alloc_configs:
kfree(di_cfg);
kfree(do_cfg);
kfree(ai_cfg);
kfree(ao_cfg);
if (result) {
if (devpriv->tty) {
filp_close(devpriv->tty, NULL);
devpriv->tty = NULL;
}
}
return result;
}
static int serial2002_open(struct comedi_device *dev)
{
struct serial2002_private *devpriv = dev->private;
int result;
char port[20];
sprintf(port, "/dev/ttyS%d", devpriv->port);
devpriv->tty = filp_open(port, O_RDWR, 0);
if (IS_ERR(devpriv->tty)) {
result = (int)PTR_ERR(devpriv->tty);
dev_err(dev->class_dev, "file open error = %d\n", result);
} else {
result = serial2002_setup_subdevs(dev);
}
return result;
}
static void serial2002_close(struct comedi_device *dev)
{
struct serial2002_private *devpriv = dev->private;
if (!IS_ERR(devpriv->tty) && devpriv->tty)
filp_close(devpriv->tty, NULL);
}
static int serial2002_di_insn_read(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan;
chan = devpriv->digital_in_mapping[CR_CHAN(insn->chanspec)];
for (n = 0; n < insn->n; n++) {
struct serial_data read;
serial2002_poll_digital(devpriv->tty, chan);
while (1) {
read = serial2002_read(devpriv->tty, 1000);
if (read.kind != is_digital || read.index == chan)
break;
}
data[n] = read.value;
}
return n;
}
static int serial2002_do_insn_write(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan;
chan = devpriv->digital_out_mapping[CR_CHAN(insn->chanspec)];
for (n = 0; n < insn->n; n++) {
struct serial_data write;
write.kind = is_digital;
write.index = chan;
write.value = data[n];
serial2002_write(devpriv->tty, write);
}
return n;
}
static int serial2002_ai_insn_read(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan;
chan = devpriv->analog_in_mapping[CR_CHAN(insn->chanspec)];
for (n = 0; n < insn->n; n++) {
struct serial_data read;
serial2002_poll_channel(devpriv->tty, chan);
while (1) {
read = serial2002_read(devpriv->tty, 1000);
if (read.kind != is_channel || read.index == chan)
break;
}
data[n] = read.value;
}
return n;
}
static int serial2002_ao_insn_write(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan;
chan = devpriv->analog_out_mapping[CR_CHAN(insn->chanspec)];
for (n = 0; n < insn->n; n++) {
struct serial_data write;
write.kind = is_channel;
write.index = chan;
write.value = data[n];
serial2002_write(devpriv->tty, write);
devpriv->ao_readback[chan] = data[n];
}
return n;
}
static int serial2002_ao_insn_read(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan = CR_CHAN(insn->chanspec);
for (n = 0; n < insn->n; n++)
data[n] = devpriv->ao_readback[chan];
return n;
}
static int serial2002_encoder_insn_read(struct comedi_device *dev,
struct comedi_subdevice *s,
struct comedi_insn *insn,
unsigned int *data)
{
struct serial2002_private *devpriv = dev->private;
int n;
int chan;
chan = devpriv->encoder_in_mapping[CR_CHAN(insn->chanspec)];
for (n = 0; n < insn->n; n++) {
struct serial_data read;
serial2002_poll_channel(devpriv->tty, chan);
while (1) {
read = serial2002_read(devpriv->tty, 1000);
if (read.kind != is_channel || read.index == chan)
break;
}
data[n] = read.value;
}
return n;
}
static int serial2002_attach(struct comedi_device *dev,
struct comedi_devconfig *it)
{
struct serial2002_private *devpriv;
struct comedi_subdevice *s;
int ret;
devpriv = comedi_alloc_devpriv(dev, sizeof(*devpriv));
if (!devpriv)
return -ENOMEM;
devpriv->port = it->options[0];
devpriv->speed = it->options[1];
ret = comedi_alloc_subdevices(dev, 5);
if (ret)
return ret;
/* digital input subdevice */
s = &dev->subdevices[0];
s->type = COMEDI_SUBD_DI;
s->subdev_flags = SDF_READABLE;
s->n_chan = 0;
s->maxdata = 1;
s->range_table = &range_digital;
s->insn_read = serial2002_di_insn_read;
/* digital output subdevice */
s = &dev->subdevices[1];
s->type = COMEDI_SUBD_DO;
s->subdev_flags = SDF_WRITABLE;
s->n_chan = 0;
s->maxdata = 1;
s->range_table = &range_digital;
s->insn_write = serial2002_do_insn_write;
/* analog input subdevice */
s = &dev->subdevices[2];
s->type = COMEDI_SUBD_AI;
s->subdev_flags = SDF_READABLE | SDF_GROUND;
s->n_chan = 0;
s->maxdata = 1;
s->range_table = NULL;
s->insn_read = serial2002_ai_insn_read;
/* analog output subdevice */
s = &dev->subdevices[3];
s->type = COMEDI_SUBD_AO;
s->subdev_flags = SDF_WRITABLE;
s->n_chan = 0;
s->maxdata = 1;
s->range_table = NULL;
s->insn_write = serial2002_ao_insn_write;
s->insn_read = serial2002_ao_insn_read;
/* encoder input subdevice */
s = &dev->subdevices[4];
s->type = COMEDI_SUBD_COUNTER;
s->subdev_flags = SDF_READABLE | SDF_LSAMPL;
s->n_chan = 0;
s->maxdata = 1;
s->range_table = NULL;
s->insn_read = serial2002_encoder_insn_read;
dev->open = serial2002_open;
dev->close = serial2002_close;
return 0;
}
static void serial2002_detach(struct comedi_device *dev)
{
struct comedi_subdevice *s;
int i;
for (i = 0; i < dev->n_subdevices; i++) {
s = &dev->subdevices[i];
kfree(s->maxdata_list);
kfree(s->range_table_list);
}
}
static struct comedi_driver serial2002_driver = {
.driver_name = "serial2002",
.module = THIS_MODULE,
.attach = serial2002_attach,
.detach = serial2002_detach,
};
module_comedi_driver(serial2002_driver);
MODULE_AUTHOR("Comedi http://www.comedi.org");
MODULE_DESCRIPTION("Comedi low-level driver");
MODULE_LICENSE("GPL");