| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * comedi/drivers/ni_usb6501.c |
| * Comedi driver for National Instruments USB-6501 |
| * |
| * COMEDI - Linux Control and Measurement Device Interface |
| * Copyright (C) 2014 Luca Ellero <luca.ellero@brickedbrain.com> |
| */ |
| |
| /* |
| * Driver: ni_usb6501 |
| * Description: National Instruments USB-6501 module |
| * Devices: [National Instruments] USB-6501 (ni_usb6501) |
| * Author: Luca Ellero <luca.ellero@brickedbrain.com> |
| * Updated: 8 Sep 2014 |
| * Status: works |
| * |
| * |
| * Configuration Options: |
| * none |
| */ |
| |
| /* |
| * NI-6501 - USB PROTOCOL DESCRIPTION |
| * |
| * Every command is composed by two USB packets: |
| * - request (out) |
| * - response (in) |
| * |
| * Every packet is at least 12 bytes long, here is the meaning of |
| * every field (all values are hex): |
| * |
| * byte 0 is always 00 |
| * byte 1 is always 01 |
| * byte 2 is always 00 |
| * byte 3 is the total packet length |
| * |
| * byte 4 is always 00 |
| * byte 5 is the total packet length - 4 |
| * byte 6 is always 01 |
| * byte 7 is the command |
| * |
| * byte 8 is 02 (request) or 00 (response) |
| * byte 9 is 00 (response) or 10 (port request) or 20 (counter request) |
| * byte 10 is always 00 |
| * byte 11 is 00 (request) or 02 (response) |
| * |
| * PORT PACKETS |
| * |
| * CMD: 0xE READ_PORT |
| * REQ: 00 01 00 10 00 0C 01 0E 02 10 00 00 00 03 <PORT> 00 |
| * RES: 00 01 00 10 00 0C 01 00 00 00 00 02 00 03 <BMAP> 00 |
| * |
| * CMD: 0xF WRITE_PORT |
| * REQ: 00 01 00 14 00 10 01 0F 02 10 00 00 00 03 <PORT> 00 03 <BMAP> 00 00 |
| * RES: 00 01 00 0C 00 08 01 00 00 00 00 02 |
| * |
| * CMD: 0x12 SET_PORT_DIR (0 = input, 1 = output) |
| * REQ: 00 01 00 18 00 14 01 12 02 10 00 00 |
| * 00 05 <PORT 0> <PORT 1> <PORT 2> 00 05 00 00 00 00 00 |
| * RES: 00 01 00 0C 00 08 01 00 00 00 00 02 |
| * |
| * COUNTER PACKETS |
| * |
| * CMD 0x9: START_COUNTER |
| * REQ: 00 01 00 0C 00 08 01 09 02 20 00 00 |
| * RES: 00 01 00 0C 00 08 01 00 00 00 00 02 |
| * |
| * CMD 0xC: STOP_COUNTER |
| * REQ: 00 01 00 0C 00 08 01 0C 02 20 00 00 |
| * RES: 00 01 00 0C 00 08 01 00 00 00 00 02 |
| * |
| * CMD 0xE: READ_COUNTER |
| * REQ: 00 01 00 0C 00 08 01 0E 02 20 00 00 |
| * RES: 00 01 00 10 00 0C 01 00 00 00 00 02 <u32 counter value, Big Endian> |
| * |
| * CMD 0xF: WRITE_COUNTER |
| * REQ: 00 01 00 10 00 0C 01 0F 02 20 00 00 <u32 counter value, Big Endian> |
| * RES: 00 01 00 0C 00 08 01 00 00 00 00 02 |
| * |
| * |
| * Please visit https://www.brickedbrain.com if you need |
| * additional information or have any questions. |
| * |
| */ |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| |
| #include "../comedi_usb.h" |
| |
| #define NI6501_TIMEOUT 1000 |
| |
| /* Port request packets */ |
| static const u8 READ_PORT_REQUEST[] = {0x00, 0x01, 0x00, 0x10, |
| 0x00, 0x0C, 0x01, 0x0E, |
| 0x02, 0x10, 0x00, 0x00, |
| 0x00, 0x03, 0x00, 0x00}; |
| |
| static const u8 WRITE_PORT_REQUEST[] = {0x00, 0x01, 0x00, 0x14, |
| 0x00, 0x10, 0x01, 0x0F, |
| 0x02, 0x10, 0x00, 0x00, |
| 0x00, 0x03, 0x00, 0x00, |
| 0x03, 0x00, 0x00, 0x00}; |
| |
| static const u8 SET_PORT_DIR_REQUEST[] = {0x00, 0x01, 0x00, 0x18, |
| 0x00, 0x14, 0x01, 0x12, |
| 0x02, 0x10, 0x00, 0x00, |
| 0x00, 0x05, 0x00, 0x00, |
| 0x00, 0x00, 0x05, 0x00, |
| 0x00, 0x00, 0x00, 0x00}; |
| |
| /* Counter request packets */ |
| static const u8 START_COUNTER_REQUEST[] = {0x00, 0x01, 0x00, 0x0C, |
| 0x00, 0x08, 0x01, 0x09, |
| 0x02, 0x20, 0x00, 0x00}; |
| |
| static const u8 STOP_COUNTER_REQUEST[] = {0x00, 0x01, 0x00, 0x0C, |
| 0x00, 0x08, 0x01, 0x0C, |
| 0x02, 0x20, 0x00, 0x00}; |
| |
| static const u8 READ_COUNTER_REQUEST[] = {0x00, 0x01, 0x00, 0x0C, |
| 0x00, 0x08, 0x01, 0x0E, |
| 0x02, 0x20, 0x00, 0x00}; |
| |
| static const u8 WRITE_COUNTER_REQUEST[] = {0x00, 0x01, 0x00, 0x10, |
| 0x00, 0x0C, 0x01, 0x0F, |
| 0x02, 0x20, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00}; |
| |
| /* Response packets */ |
| static const u8 GENERIC_RESPONSE[] = {0x00, 0x01, 0x00, 0x0C, |
| 0x00, 0x08, 0x01, 0x00, |
| 0x00, 0x00, 0x00, 0x02}; |
| |
| static const u8 READ_PORT_RESPONSE[] = {0x00, 0x01, 0x00, 0x10, |
| 0x00, 0x0C, 0x01, 0x00, |
| 0x00, 0x00, 0x00, 0x02, |
| 0x00, 0x03, 0x00, 0x00}; |
| |
| static const u8 READ_COUNTER_RESPONSE[] = {0x00, 0x01, 0x00, 0x10, |
| 0x00, 0x0C, 0x01, 0x00, |
| 0x00, 0x00, 0x00, 0x02, |
| 0x00, 0x00, 0x00, 0x00}; |
| |
| /* Largest supported packets */ |
| static const size_t TX_MAX_SIZE = sizeof(SET_PORT_DIR_REQUEST); |
| static const size_t RX_MAX_SIZE = sizeof(READ_PORT_RESPONSE); |
| |
| enum commands { |
| READ_PORT, |
| WRITE_PORT, |
| SET_PORT_DIR, |
| START_COUNTER, |
| STOP_COUNTER, |
| READ_COUNTER, |
| WRITE_COUNTER |
| }; |
| |
| struct ni6501_private { |
| struct usb_endpoint_descriptor *ep_rx; |
| struct usb_endpoint_descriptor *ep_tx; |
| struct mutex mut; |
| u8 *usb_rx_buf; |
| u8 *usb_tx_buf; |
| }; |
| |
| static int ni6501_port_command(struct comedi_device *dev, int command, |
| unsigned int val, u8 *bitmap) |
| { |
| struct usb_device *usb = comedi_to_usb_dev(dev); |
| struct ni6501_private *devpriv = dev->private; |
| int request_size, response_size; |
| u8 *tx = devpriv->usb_tx_buf; |
| int ret; |
| |
| if (command != SET_PORT_DIR && !bitmap) |
| return -EINVAL; |
| |
| mutex_lock(&devpriv->mut); |
| |
| switch (command) { |
| case READ_PORT: |
| request_size = sizeof(READ_PORT_REQUEST); |
| response_size = sizeof(READ_PORT_RESPONSE); |
| memcpy(tx, READ_PORT_REQUEST, request_size); |
| tx[14] = val & 0xff; |
| break; |
| case WRITE_PORT: |
| request_size = sizeof(WRITE_PORT_REQUEST); |
| response_size = sizeof(GENERIC_RESPONSE); |
| memcpy(tx, WRITE_PORT_REQUEST, request_size); |
| tx[14] = val & 0xff; |
| tx[17] = *bitmap; |
| break; |
| case SET_PORT_DIR: |
| request_size = sizeof(SET_PORT_DIR_REQUEST); |
| response_size = sizeof(GENERIC_RESPONSE); |
| memcpy(tx, SET_PORT_DIR_REQUEST, request_size); |
| tx[14] = val & 0xff; |
| tx[15] = (val >> 8) & 0xff; |
| tx[16] = (val >> 16) & 0xff; |
| break; |
| default: |
| ret = -EINVAL; |
| goto end; |
| } |
| |
| ret = usb_bulk_msg(usb, |
| usb_sndbulkpipe(usb, |
| devpriv->ep_tx->bEndpointAddress), |
| devpriv->usb_tx_buf, |
| request_size, |
| NULL, |
| NI6501_TIMEOUT); |
| if (ret) |
| goto end; |
| |
| ret = usb_bulk_msg(usb, |
| usb_rcvbulkpipe(usb, |
| devpriv->ep_rx->bEndpointAddress), |
| devpriv->usb_rx_buf, |
| response_size, |
| NULL, |
| NI6501_TIMEOUT); |
| if (ret) |
| goto end; |
| |
| /* Check if results are valid */ |
| |
| if (command == READ_PORT) { |
| *bitmap = devpriv->usb_rx_buf[14]; |
| /* mask bitmap for comparing */ |
| devpriv->usb_rx_buf[14] = 0x00; |
| |
| if (memcmp(devpriv->usb_rx_buf, READ_PORT_RESPONSE, |
| sizeof(READ_PORT_RESPONSE))) { |
| ret = -EINVAL; |
| } |
| } else if (memcmp(devpriv->usb_rx_buf, GENERIC_RESPONSE, |
| sizeof(GENERIC_RESPONSE))) { |
| ret = -EINVAL; |
| } |
| end: |
| mutex_unlock(&devpriv->mut); |
| |
| return ret; |
| } |
| |
| static int ni6501_counter_command(struct comedi_device *dev, int command, |
| u32 *val) |
| { |
| struct usb_device *usb = comedi_to_usb_dev(dev); |
| struct ni6501_private *devpriv = dev->private; |
| int request_size, response_size; |
| u8 *tx = devpriv->usb_tx_buf; |
| int ret; |
| |
| if ((command == READ_COUNTER || command == WRITE_COUNTER) && !val) |
| return -EINVAL; |
| |
| mutex_lock(&devpriv->mut); |
| |
| switch (command) { |
| case START_COUNTER: |
| request_size = sizeof(START_COUNTER_REQUEST); |
| response_size = sizeof(GENERIC_RESPONSE); |
| memcpy(tx, START_COUNTER_REQUEST, request_size); |
| break; |
| case STOP_COUNTER: |
| request_size = sizeof(STOP_COUNTER_REQUEST); |
| response_size = sizeof(GENERIC_RESPONSE); |
| memcpy(tx, STOP_COUNTER_REQUEST, request_size); |
| break; |
| case READ_COUNTER: |
| request_size = sizeof(READ_COUNTER_REQUEST); |
| response_size = sizeof(READ_COUNTER_RESPONSE); |
| memcpy(tx, READ_COUNTER_REQUEST, request_size); |
| break; |
| case WRITE_COUNTER: |
| request_size = sizeof(WRITE_COUNTER_REQUEST); |
| response_size = sizeof(GENERIC_RESPONSE); |
| memcpy(tx, WRITE_COUNTER_REQUEST, request_size); |
| /* Setup tx packet: bytes 12,13,14,15 hold the */ |
| /* u32 counter value (Big Endian) */ |
| *((__be32 *)&tx[12]) = cpu_to_be32(*val); |
| break; |
| default: |
| ret = -EINVAL; |
| goto end; |
| } |
| |
| ret = usb_bulk_msg(usb, |
| usb_sndbulkpipe(usb, |
| devpriv->ep_tx->bEndpointAddress), |
| devpriv->usb_tx_buf, |
| request_size, |
| NULL, |
| NI6501_TIMEOUT); |
| if (ret) |
| goto end; |
| |
| ret = usb_bulk_msg(usb, |
| usb_rcvbulkpipe(usb, |
| devpriv->ep_rx->bEndpointAddress), |
| devpriv->usb_rx_buf, |
| response_size, |
| NULL, |
| NI6501_TIMEOUT); |
| if (ret) |
| goto end; |
| |
| /* Check if results are valid */ |
| |
| if (command == READ_COUNTER) { |
| int i; |
| |
| /* Read counter value: bytes 12,13,14,15 of rx packet */ |
| /* hold the u32 counter value (Big Endian) */ |
| *val = be32_to_cpu(*((__be32 *)&devpriv->usb_rx_buf[12])); |
| |
| /* mask counter value for comparing */ |
| for (i = 12; i < sizeof(READ_COUNTER_RESPONSE); ++i) |
| devpriv->usb_rx_buf[i] = 0x00; |
| |
| if (memcmp(devpriv->usb_rx_buf, READ_COUNTER_RESPONSE, |
| sizeof(READ_COUNTER_RESPONSE))) { |
| ret = -EINVAL; |
| } |
| } else if (memcmp(devpriv->usb_rx_buf, GENERIC_RESPONSE, |
| sizeof(GENERIC_RESPONSE))) { |
| ret = -EINVAL; |
| } |
| end: |
| mutex_unlock(&devpriv->mut); |
| |
| return ret; |
| } |
| |
| static int ni6501_dio_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| int ret; |
| |
| ret = comedi_dio_insn_config(dev, s, insn, data, 0); |
| if (ret) |
| return ret; |
| |
| ret = ni6501_port_command(dev, SET_PORT_DIR, s->io_bits, NULL); |
| if (ret) |
| return ret; |
| |
| return insn->n; |
| } |
| |
| static int ni6501_dio_insn_bits(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| unsigned int mask; |
| int ret; |
| u8 port; |
| u8 bitmap; |
| |
| mask = comedi_dio_update_state(s, data); |
| |
| for (port = 0; port < 3; port++) { |
| if (mask & (0xFF << port * 8)) { |
| bitmap = (s->state >> port * 8) & 0xFF; |
| ret = ni6501_port_command(dev, WRITE_PORT, |
| port, &bitmap); |
| if (ret) |
| return ret; |
| } |
| } |
| |
| data[1] = 0; |
| |
| for (port = 0; port < 3; port++) { |
| ret = ni6501_port_command(dev, READ_PORT, port, &bitmap); |
| if (ret) |
| return ret; |
| data[1] |= bitmap << port * 8; |
| } |
| |
| return insn->n; |
| } |
| |
| static int ni6501_cnt_insn_config(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| int ret; |
| u32 val = 0; |
| |
| switch (data[0]) { |
| case INSN_CONFIG_ARM: |
| ret = ni6501_counter_command(dev, START_COUNTER, NULL); |
| break; |
| case INSN_CONFIG_DISARM: |
| ret = ni6501_counter_command(dev, STOP_COUNTER, NULL); |
| break; |
| case INSN_CONFIG_RESET: |
| ret = ni6501_counter_command(dev, STOP_COUNTER, NULL); |
| if (ret) |
| break; |
| ret = ni6501_counter_command(dev, WRITE_COUNTER, &val); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return ret ? ret : insn->n; |
| } |
| |
| static int ni6501_cnt_insn_read(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| int ret; |
| u32 val; |
| unsigned int i; |
| |
| for (i = 0; i < insn->n; i++) { |
| ret = ni6501_counter_command(dev, READ_COUNTER, &val); |
| if (ret) |
| return ret; |
| data[i] = val; |
| } |
| |
| return insn->n; |
| } |
| |
| static int ni6501_cnt_insn_write(struct comedi_device *dev, |
| struct comedi_subdevice *s, |
| struct comedi_insn *insn, |
| unsigned int *data) |
| { |
| int ret; |
| |
| if (insn->n) { |
| u32 val = data[insn->n - 1]; |
| |
| ret = ni6501_counter_command(dev, WRITE_COUNTER, &val); |
| if (ret) |
| return ret; |
| } |
| |
| return insn->n; |
| } |
| |
| static int ni6501_alloc_usb_buffers(struct comedi_device *dev) |
| { |
| struct ni6501_private *devpriv = dev->private; |
| size_t size; |
| |
| size = usb_endpoint_maxp(devpriv->ep_rx); |
| devpriv->usb_rx_buf = kzalloc(size, GFP_KERNEL); |
| if (!devpriv->usb_rx_buf) |
| return -ENOMEM; |
| |
| size = usb_endpoint_maxp(devpriv->ep_tx); |
| devpriv->usb_tx_buf = kzalloc(size, GFP_KERNEL); |
| if (!devpriv->usb_tx_buf) |
| return -ENOMEM; |
| |
| return 0; |
| } |
| |
| static int ni6501_find_endpoints(struct comedi_device *dev) |
| { |
| struct usb_interface *intf = comedi_to_usb_interface(dev); |
| struct ni6501_private *devpriv = dev->private; |
| struct usb_host_interface *iface_desc = intf->cur_altsetting; |
| struct usb_endpoint_descriptor *ep_desc; |
| int i; |
| |
| if (iface_desc->desc.bNumEndpoints != 2) { |
| dev_err(dev->class_dev, "Wrong number of endpoints\n"); |
| return -ENODEV; |
| } |
| |
| for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) { |
| ep_desc = &iface_desc->endpoint[i].desc; |
| |
| if (usb_endpoint_is_bulk_in(ep_desc)) { |
| if (!devpriv->ep_rx) |
| devpriv->ep_rx = ep_desc; |
| continue; |
| } |
| |
| if (usb_endpoint_is_bulk_out(ep_desc)) { |
| if (!devpriv->ep_tx) |
| devpriv->ep_tx = ep_desc; |
| continue; |
| } |
| } |
| |
| if (!devpriv->ep_rx || !devpriv->ep_tx) |
| return -ENODEV; |
| |
| if (usb_endpoint_maxp(devpriv->ep_rx) < RX_MAX_SIZE) |
| return -ENODEV; |
| |
| if (usb_endpoint_maxp(devpriv->ep_tx) < TX_MAX_SIZE) |
| return -ENODEV; |
| |
| return 0; |
| } |
| |
| static int ni6501_auto_attach(struct comedi_device *dev, |
| unsigned long context) |
| { |
| struct usb_interface *intf = comedi_to_usb_interface(dev); |
| struct ni6501_private *devpriv; |
| struct comedi_subdevice *s; |
| int ret; |
| |
| devpriv = comedi_alloc_devpriv(dev, sizeof(*devpriv)); |
| if (!devpriv) |
| return -ENOMEM; |
| |
| mutex_init(&devpriv->mut); |
| usb_set_intfdata(intf, devpriv); |
| |
| ret = ni6501_find_endpoints(dev); |
| if (ret) |
| return ret; |
| |
| ret = ni6501_alloc_usb_buffers(dev); |
| if (ret) |
| return ret; |
| |
| ret = comedi_alloc_subdevices(dev, 2); |
| if (ret) |
| return ret; |
| |
| /* Digital Input/Output subdevice */ |
| s = &dev->subdevices[0]; |
| s->type = COMEDI_SUBD_DIO; |
| s->subdev_flags = SDF_READABLE | SDF_WRITABLE; |
| s->n_chan = 24; |
| s->maxdata = 1; |
| s->range_table = &range_digital; |
| s->insn_bits = ni6501_dio_insn_bits; |
| s->insn_config = ni6501_dio_insn_config; |
| |
| /* Counter subdevice */ |
| s = &dev->subdevices[1]; |
| s->type = COMEDI_SUBD_COUNTER; |
| s->subdev_flags = SDF_READABLE | SDF_WRITABLE | SDF_LSAMPL; |
| s->n_chan = 1; |
| s->maxdata = 0xffffffff; |
| s->insn_read = ni6501_cnt_insn_read; |
| s->insn_write = ni6501_cnt_insn_write; |
| s->insn_config = ni6501_cnt_insn_config; |
| |
| return 0; |
| } |
| |
| static void ni6501_detach(struct comedi_device *dev) |
| { |
| struct usb_interface *intf = comedi_to_usb_interface(dev); |
| struct ni6501_private *devpriv = dev->private; |
| |
| if (!devpriv) |
| return; |
| |
| mutex_destroy(&devpriv->mut); |
| |
| usb_set_intfdata(intf, NULL); |
| |
| kfree(devpriv->usb_rx_buf); |
| kfree(devpriv->usb_tx_buf); |
| } |
| |
| static struct comedi_driver ni6501_driver = { |
| .module = THIS_MODULE, |
| .driver_name = "ni6501", |
| .auto_attach = ni6501_auto_attach, |
| .detach = ni6501_detach, |
| }; |
| |
| static int ni6501_usb_probe(struct usb_interface *intf, |
| const struct usb_device_id *id) |
| { |
| return comedi_usb_auto_config(intf, &ni6501_driver, id->driver_info); |
| } |
| |
| static const struct usb_device_id ni6501_usb_table[] = { |
| { USB_DEVICE(0x3923, 0x718a) }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(usb, ni6501_usb_table); |
| |
| static struct usb_driver ni6501_usb_driver = { |
| .name = "ni6501", |
| .id_table = ni6501_usb_table, |
| .probe = ni6501_usb_probe, |
| .disconnect = comedi_usb_auto_unconfig, |
| }; |
| module_comedi_usb_driver(ni6501_driver, ni6501_usb_driver); |
| |
| MODULE_AUTHOR("Luca Ellero"); |
| MODULE_DESCRIPTION("Comedi driver for National Instruments USB-6501"); |
| MODULE_LICENSE("GPL"); |