/* comedi/drivers/adv_pci1724.c This is a driver for the Advantech PCI-1724U card. Author: Frank Mori Hess Copyright (C) 2013 GnuBIO Inc COMEDI - Linux Control and Measurement Device Interface Copyright (C) 1997-8 David A. Schleef 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ************************************************************************/ /* Driver: adv_1724 Description: Advantech PCI-1724U Author: Frank Mori Hess Status: works Updated: 2013-02-09 Devices: [Advantech] PCI-1724U (adv_pci1724) Subdevice 0 is the analog output. Subdevice 1 is the offset calibration for the analog output. Subdevice 2 is the gain calibration for the analog output. The calibration offset and gains have quite a large effect on the analog output, so it is possible to adjust the analog output to have an output range significantly different from the board's nominal output ranges. For a calibrated +/- 10V range, the analog output's offset will be set somewhere near mid-range (0x2000) and its gain will be near maximum (0x3fff). There is really no difference between the board's documented 0-20mA versus 4-20mA output ranges. To pick one or the other is simply a matter of adjusting the offset and gain calibration until the board outputs in the desired range. Configuration options: None Manual configuration of comedi devices is not supported by this driver; supported PCI devices are configured as comedi devices automatically. */ #include #include "../comedidev.h" #define PCI_VENDOR_ID_ADVANTECH 0x13fe #define NUM_AO_CHANNELS 32 /* register offsets */ enum board_registers { DAC_CONTROL_REG = 0x0, SYNC_OUTPUT_REG = 0x4, EEPROM_CONTROL_REG = 0x8, SYNC_OUTPUT_TRIGGER_REG = 0xc, BOARD_ID_REG = 0x10 }; /* bit definitions for registers */ enum dac_control_contents { DAC_DATA_MASK = 0x3fff, DAC_DESTINATION_MASK = 0xc000, DAC_NORMAL_MODE = 0xc000, DAC_OFFSET_MODE = 0x8000, DAC_GAIN_MODE = 0x4000, DAC_CHANNEL_SELECT_MASK = 0xf0000, DAC_GROUP_SELECT_MASK = 0xf00000 }; static uint32_t dac_data_bits(uint16_t dac_data) { return dac_data & DAC_DATA_MASK; } static uint32_t dac_channel_select_bits(unsigned channel) { return (channel << 16) & DAC_CHANNEL_SELECT_MASK; } static uint32_t dac_group_select_bits(unsigned group) { return (1 << (20 + group)) & DAC_GROUP_SELECT_MASK; } static uint32_t dac_channel_and_group_select_bits(unsigned comedi_channel) { return dac_channel_select_bits(comedi_channel % 8) | dac_group_select_bits(comedi_channel / 8); } enum sync_output_contents { SYNC_MODE = 0x1, DAC_BUSY = 0x2, /* dac state machine is not ready */ }; enum sync_output_trigger_contents { SYNC_TRIGGER_BITS = 0x0 /* any value works */ }; enum board_id_contents { BOARD_ID_MASK = 0xf }; static const struct comedi_lrange ao_ranges_1724 = { 4, { BIP_RANGE(10), RANGE_mA(0, 20), RANGE_mA(4, 20), RANGE_unitless(0, 1) } }; static const struct comedi_lrange *const ao_range_list_1724[NUM_AO_CHANNELS] = { [0 ... NUM_AO_CHANNELS - 1] = &ao_ranges_1724, }; /* this structure is for data unique to this hardware driver. */ struct adv_pci1724_private { int ao_value[NUM_AO_CHANNELS]; int offset_value[NUM_AO_CHANNELS]; int gain_value[NUM_AO_CHANNELS]; }; static int wait_for_dac_idle(struct comedi_device *dev) { static const int timeout = 10000; int i; for (i = 0; i < timeout; ++i) { if ((inl(dev->iobase + SYNC_OUTPUT_REG) & DAC_BUSY) == 0) break; udelay(1); } if (i == timeout) { comedi_error(dev, "Timed out waiting for dac to become idle."); return -EIO; } return 0; } static int set_dac(struct comedi_device *dev, unsigned mode, unsigned channel, unsigned data) { int retval; unsigned control_bits; retval = wait_for_dac_idle(dev); if (retval < 0) return retval; control_bits = mode; control_bits |= dac_channel_and_group_select_bits(channel); control_bits |= dac_data_bits(data); outl(control_bits, dev->iobase + DAC_CONTROL_REG); return 0; } static int ao_winsn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; int channel = CR_CHAN(insn->chanspec); int retval; int i; /* turn off synchronous mode */ outl(0, dev->iobase + SYNC_OUTPUT_REG); for (i = 0; i < insn->n; ++i) { retval = set_dac(dev, DAC_NORMAL_MODE, channel, data[i]); if (retval < 0) return retval; devpriv->ao_value[channel] = data[i]; } return insn->n; } static int ao_readback_insn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; int channel = CR_CHAN(insn->chanspec); int i; if (devpriv->ao_value[channel] < 0) { comedi_error(dev, "Cannot read back channels which have not yet been written to."); return -EIO; } for (i = 0; i < insn->n; i++) data[i] = devpriv->ao_value[channel]; return insn->n; } static int offset_write_insn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; int channel = CR_CHAN(insn->chanspec); int retval; int i; /* turn off synchronous mode */ outl(0, dev->iobase + SYNC_OUTPUT_REG); for (i = 0; i < insn->n; ++i) { retval = set_dac(dev, DAC_OFFSET_MODE, channel, data[i]); if (retval < 0) return retval; devpriv->offset_value[channel] = data[i]; } return insn->n; } static int offset_read_insn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; unsigned int channel = CR_CHAN(insn->chanspec); int i; if (devpriv->offset_value[channel] < 0) { comedi_error(dev, "Cannot read back channels which have not yet been written to."); return -EIO; } for (i = 0; i < insn->n; i++) data[i] = devpriv->offset_value[channel]; return insn->n; } static int gain_write_insn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; int channel = CR_CHAN(insn->chanspec); int retval; int i; /* turn off synchronous mode */ outl(0, dev->iobase + SYNC_OUTPUT_REG); for (i = 0; i < insn->n; ++i) { retval = set_dac(dev, DAC_GAIN_MODE, channel, data[i]); if (retval < 0) return retval; devpriv->gain_value[channel] = data[i]; } return insn->n; } static int gain_read_insn(struct comedi_device *dev, struct comedi_subdevice *s, struct comedi_insn *insn, unsigned int *data) { struct adv_pci1724_private *devpriv = dev->private; unsigned int channel = CR_CHAN(insn->chanspec); int i; if (devpriv->gain_value[channel] < 0) { comedi_error(dev, "Cannot read back channels which have not yet been written to."); return -EIO; } for (i = 0; i < insn->n; i++) data[i] = devpriv->gain_value[channel]; return insn->n; } /* Allocate and initialize the subdevice structures. */ static int setup_subdevices(struct comedi_device *dev) { struct comedi_subdevice *s; int ret; ret = comedi_alloc_subdevices(dev, 3); if (ret) return ret; /* analog output subdevice */ s = &dev->subdevices[0]; s->type = COMEDI_SUBD_AO; s->subdev_flags = SDF_READABLE | SDF_WRITABLE | SDF_GROUND; s->n_chan = NUM_AO_CHANNELS; s->maxdata = 0x3fff; s->range_table_list = ao_range_list_1724; s->insn_read = ao_readback_insn; s->insn_write = ao_winsn; /* offset calibration */ s = &dev->subdevices[1]; s->type = COMEDI_SUBD_CALIB; s->subdev_flags = SDF_READABLE | SDF_WRITABLE | SDF_INTERNAL; s->n_chan = NUM_AO_CHANNELS; s->insn_read = offset_read_insn; s->insn_write = offset_write_insn; s->maxdata = 0x3fff; /* gain calibration */ s = &dev->subdevices[2]; s->type = COMEDI_SUBD_CALIB; s->subdev_flags = SDF_READABLE | SDF_WRITABLE | SDF_INTERNAL; s->n_chan = NUM_AO_CHANNELS; s->insn_read = gain_read_insn; s->insn_write = gain_write_insn; s->maxdata = 0x3fff; return 0; } static int adv_pci1724_auto_attach(struct comedi_device *dev, unsigned long context_unused) { struct pci_dev *pcidev = comedi_to_pci_dev(dev); struct adv_pci1724_private *devpriv; int i; int retval; unsigned int board_id; devpriv = kzalloc(sizeof(*devpriv), GFP_KERNEL); if (!devpriv) return -ENOMEM; dev->private = devpriv; /* init software copies of output values to indicate we don't know * what the output value is since it has never been written. */ for (i = 0; i < NUM_AO_CHANNELS; ++i) { devpriv->ao_value[i] = -1; devpriv->offset_value[i] = -1; devpriv->gain_value[i] = -1; } retval = comedi_pci_enable(dev); if (retval) return retval; dev->iobase = pci_resource_start(pcidev, 2); board_id = inl(dev->iobase + BOARD_ID_REG) & BOARD_ID_MASK; dev_info(dev->class_dev, "board id: %d\n", board_id); retval = setup_subdevices(dev); if (retval < 0) return retval; dev_info(dev->class_dev, "%s (pci %s) attached, board id: %u\n", dev->board_name, pci_name(pcidev), board_id); return 0; } static struct comedi_driver adv_pci1724_driver = { .driver_name = "adv_pci1724", .module = THIS_MODULE, .auto_attach = adv_pci1724_auto_attach, .detach = comedi_pci_disable, }; static int adv_pci1724_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) { return comedi_pci_auto_config(dev, &adv_pci1724_driver, id->driver_data); } static DEFINE_PCI_DEVICE_TABLE(adv_pci1724_pci_table) = { { PCI_DEVICE(PCI_VENDOR_ID_ADVANTECH, 0x1724) }, { 0 } }; MODULE_DEVICE_TABLE(pci, adv_pci1724_pci_table); static struct pci_driver adv_pci1724_pci_driver = { .name = "adv_pci1724", .id_table = adv_pci1724_pci_table, .probe = adv_pci1724_pci_probe, .remove = comedi_pci_auto_unconfig, }; module_comedi_pci_driver(adv_pci1724_driver, adv_pci1724_pci_driver); MODULE_AUTHOR("Frank Mori Hess "); MODULE_DESCRIPTION("Advantech PCI-1724U Comedi driver"); MODULE_LICENSE("GPL");