Need help with the MCP2210 on Linux with libusb

nsaspook

Joined Aug 27, 2009
13,081
Nice! An interval of 14ms for each transaction is acceptable, but I'm guessing that the MCP2210 is a tad slow. The CP2130 was better in that respect. That slowness might be problematic in my use case, if I am going to use it to replace the CP2130 in one of my USB test switches. I will need to take five measurements from an LTC2312CTS8-12 ADC, and the total of all those transactions, plus reading 5 GPIOs, must be well within an interval of 50ms!

By the way, what bitrate are you using?
3MHz is the max SCK I use. A lot of that time is spent switching modes between three types of SPI devices with auto-CS. If you do multi-byte SPI transactions to one device, the SPI transfer speed is pretty good but there is a lot of dead-time inside the MCP2210. So if a SPI device has a FIFO/Stream mode for burst data, use it.
PXL_20211227_212126290.jpg
Test setup, need to clean the scope screen. :eek:
PXL_20211227_211528926.jpg
24 byte SPI transfer from IMU, 1.3ms

PXL_20211227_211714675.jpg
Byte inter-gap spacing, 66us


PXL_20211227_212117952.jpg
Per byte timing, ~3us per byte
 

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
In my case, I cannot exceed 1.5Mb/s due to the isolators in the SPI lines. By my calculations, 1.3ms for 24 bytes is just too much, but it does make sense if you are sending one byte at a time. Each packet will be encapsulated with significant overhead (basically, USB signalling and protocol content).

In the other hand, there are timings like pre-assert, inter-byte and post-deassert delays. In the CP2130, those can be incremented in 10us units, but by default such delays are set to zero.
 
Last edited:

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
This is interesting. I was analyzing hid.c at libusb/hidapi on GitHub, and I've came across this code:
C:
int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length)
{
    int res;
    int report_number;
    int skipped_report_id = 0;

    if (!data || (length ==0)) {
        return -1;
    }

    report_number = data[0];

    if (report_number == 0x0) {
        data++;
        length--;
        skipped_report_id = 1;
    }


    if (dev->output_endpoint <= 0) {
        /* No interrupt out endpoint. Use the Control Endpoint */
        res = libusb_control_transfer(dev->device_handle,
            LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT,
            0x09/*HID Set_Report*/,
            (2/*HID output*/ << 8) | report_number,
            dev->interface,
            (unsigned char *)data, length,
            1000/*timeout millis*/);

        if (res < 0)
            return -1;

        if (skipped_report_id)
            length++;

        return length;
    }
    else {
        /* Use the interrupt out endpoint */
        int actual_length;
        res = libusb_interrupt_transfer(dev->device_handle,
            dev->output_endpoint,
            (unsigned char*)data,
            length,
            &actual_length, 1000);

        if (res < 0)
            return -1;

        if (skipped_report_id)
            actual_length++;

        return actual_length;
    }
}
Although a tad convoluted, it is interesting nonetheless. They are using interrupt transfers if the device has an OUT endpoint, and control transfers otherwise. The variable "dev->output_endpoint" is set by the initialization procedure, which, I assume it is called once the device is opened.
C:
static int hidapi_initialize_device(hid_device *dev, const struct libusb_interface_descriptor *intf_desc)
{
    int i =0;
    int res = 0;
    struct libusb_device_descriptor desc;
    libusb_get_device_descriptor(libusb_get_device(dev->device_handle), &desc);

#ifdef DETACH_KERNEL_DRIVER
    /* Detach the kernel driver, but only if the
       device is managed by the kernel */
    dev->is_driver_detached = 0;
    if (libusb_kernel_driver_active(dev->device_handle, intf_desc->bInterfaceNumber) == 1) {
        res = libusb_detach_kernel_driver(dev->device_handle, intf_desc->bInterfaceNumber);
        if (res < 0) {
            libusb_close(dev->device_handle);
            LOG("Unable to detach Kernel Driver\n");
            return 0;
        }
        else {
            dev->is_driver_detached = 1;
            LOG("Driver successfully detached from kernel.\n");
        }
    }
#endif
    res = libusb_claim_interface(dev->device_handle, intf_desc->bInterfaceNumber);
    if (res < 0) {
        LOG("can't claim interface %d: %d\n", intf_desc->bInterfaceNumber, res);
        return 0;
    }

    /* Store off the string descriptor indexes */
    dev->manufacturer_index = desc.iManufacturer;
    dev->product_index      = desc.iProduct;
    dev->serial_index       = desc.iSerialNumber;

    /* Store off the interface number */
    dev->interface = intf_desc->bInterfaceNumber;

    dev->input_endpoint = 0;
    dev->input_ep_max_packet_size = 0;
    dev->output_endpoint = 0;

    /* Find the INPUT and OUTPUT endpoints. An
       OUTPUT endpoint is not required. */
    for (i = 0; i < intf_desc->bNumEndpoints; i++) {
        const struct libusb_endpoint_descriptor *ep
            = &intf_desc->endpoint[i];

        /* Determine the type and direction of this
           endpoint. */
        int is_interrupt =
            (ep->bmAttributes & LIBUSB_TRANSFER_TYPE_MASK)
              == LIBUSB_TRANSFER_TYPE_INTERRUPT;
        int is_output =
            (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK)
              == LIBUSB_ENDPOINT_OUT;
        int is_input =
            (ep->bEndpointAddress & LIBUSB_ENDPOINT_DIR_MASK)
              == LIBUSB_ENDPOINT_IN;

        /* Decide whether to use it for input or output. */
        if (dev->input_endpoint == 0 &&
            is_interrupt && is_input) {
            /* Use this endpoint for INPUT */
            dev->input_endpoint = ep->bEndpointAddress;
            dev->input_ep_max_packet_size = ep->wMaxPacketSize;
        }
        if (dev->output_endpoint == 0 &&
            is_interrupt && is_output) {
            /* Use this endpoint for OUTPUT */
            dev->output_endpoint = ep->bEndpointAddress;
        }
    }

    pthread_create(&dev->thread, NULL, read_thread, dev);

    /* Wait here for the read thread to be initialized. */
    pthread_barrier_wait(&dev->barrier);
    return 1;
}
However, hid_read() is another story. It simply gets what was obtained by hid_write(). That is the convoluted bit.
C:
/* Helper function, to simplify hid_read().
   This should be called with dev->mutex locked. */
static int return_data(hid_device *dev, unsigned char *data, size_t length)
{
    /* Copy the data out of the linked list item (rpt) into the
       return buffer (data), and delete the liked list item. */
    struct input_report *rpt = dev->input_reports;
    size_t len = (length < rpt->len)? length: rpt->len;
    if (len > 0)
        memcpy(data, rpt->data, len);
    dev->input_reports = rpt->next;
    free(rpt->data);
    free(rpt);
    return len;
}

static void cleanup_mutex(void *param)
{
    hid_device *dev = param;
    pthread_mutex_unlock(&dev->mutex);
}


int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds)
{
#if 0
    int transferred;
    int res = libusb_interrupt_transfer(dev->device_handle, dev->input_endpoint, data, length, &transferred, 5000);
    LOG("transferred: %d\n", transferred);
    return transferred;
#endif
    /* by initialising this variable right here, GCC gives a compilation warning/error: */
    /* error: variable ‘bytes_read’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Werror=clobbered] */
    int bytes_read; /* = -1; */

    pthread_mutex_lock(&dev->mutex);
    pthread_cleanup_push(&cleanup_mutex, dev);

    bytes_read = -1;

    /* There's an input report queued up. Return it. */
    if (dev->input_reports) {
        /* Return the first one */
        bytes_read = return_data(dev, data, length);
        goto ret;
    }

    if (dev->shutdown_thread) {
        /* This means the device has been disconnected.
           An error code of -1 should be returned. */
        bytes_read = -1;
        goto ret;
    }

    if (milliseconds == -1) {
        /* Blocking */
        while (!dev->input_reports && !dev->shutdown_thread) {
            pthread_cond_wait(&dev->condition, &dev->mutex);
        }
        if (dev->input_reports) {
            bytes_read = return_data(dev, data, length);
        }
    }
    else if (milliseconds > 0) {
        /* Non-blocking, but called with timeout. */
        int res;
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        ts.tv_sec += milliseconds / 1000;
        ts.tv_nsec += (milliseconds % 1000) * 1000000;
        if (ts.tv_nsec >= 1000000000L) {
            ts.tv_sec++;
            ts.tv_nsec -= 1000000000L;
        }

        while (!dev->input_reports && !dev->shutdown_thread) {
            res = pthread_cond_timedwait(&dev->condition, &dev->mutex, &ts);
            if (res == 0) {
                if (dev->input_reports) {
                    bytes_read = return_data(dev, data, length);
                    break;
                }

                /* If we're here, there was a spurious wake up
                   or the read thread was shutdown. Run the
                   loop again (ie: don't break). */
            }
            else if (res == ETIMEDOUT) {
                /* Timed out. */
                bytes_read = 0;
                break;
            }
            else {
                /* Error. */
                bytes_read = -1;
                break;
            }
        }
    }
    else {
        /* Purely non-blocking */
        bytes_read = 0;
    }

ret:
    pthread_mutex_unlock(&dev->mutex);
    pthread_cleanup_pop(0);

    return bytes_read;
}

int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length)
{
    return hid_read_timeout(dev, data, length, dev->blocking ? -1 : 0);
}

int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
{
    dev->blocking = !nonblock;

    return 0;
}
Another issue that I've had is that the kernel driver is not reattached once the code is done. However, there is no reason for this behavior, since the kernel driver is being reattached on closure.
C:
void HID_API_EXPORT hid_close(hid_device *dev)
{
    if (!dev)
        return;

    /* Cause read_thread() to stop. */
    dev->shutdown_thread = 1;
    libusb_cancel_transfer(dev->transfer);

    /* Wait for read_thread() to end. */
    pthread_join(dev->thread, NULL);

    /* Clean up the Transfer objects allocated in read_thread(). */
    free(dev->transfer->buffer);
    libusb_free_transfer(dev->transfer);

    /* release the interface */
    libusb_release_interface(dev->device_handle, dev->interface);

    /* reattach the kernel driver if it was detached */
#ifdef DETACH_KERNEL_DRIVER
    if (dev->is_driver_detached) {
        int res = libusb_attach_kernel_driver(dev->device_handle, dev->interface);
        if (res < 0)
            LOG("Failed to reattach the driver to kernel.\n");
    }
#endif

    /* Close the handle */
    libusb_close(dev->device_handle);

    /* Clear out the queue of received reports. */
    pthread_mutex_lock(&dev->mutex);
    while (dev->input_reports) {
        return_data(dev, NULL, 0);
    }
    pthread_mutex_unlock(&dev->mutex);

    free_hid_device(dev);
}
 

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
As an update, I'm developing a class to interface with the MCP2210. Attached is a start of it. This will be much more complex.

Kind regards, Samuel Lourenço
 

Attachments

Last edited:

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
I've been procrastinating the making of the C++ class for the MCP2210, but today I've made a breakthrough. I've implemented a hidTransfer() function that writes and reads. Attached is the source code of a blinker (knight rider) program that used this implementation.

Indeed, I'm better off without using libusb/hidapi. I'll just use libusb to interface the MCP2210 directly, as I'm doing now. This implementation is well-behaved, as it reattached the kernel driver if it was attached before opening the device. However, it won't try to attach the kernel driver if it wasn't attached in the first place.

I'm using the same philosophy behind the C++ class for the CP2130, where I plan to keep hidTransfer() and interruptTransfer() public. Although they will not be used directly by the caller program (especially interruptTransfer()), they might be needed to debug something or to implement something out of scope of the class. In fact, the class for the CP2130 provided both controlTransfer() and bulkTransfer() because not every function was implemented at first.

Anyway, enjoy the blinker program!
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
I've coded a small program to find valid bit rates, that is, those that are accepted directly by the MCP2230. The sweep goes from 20Mbps to almost zero, because it allows to find such bit rate values much faster. It uses a behavior that the MCP2230 has, because the chip selects the next smallest bit rate when configured with a given bit rate, if it does not conform (to whatever specification).

However, it is possible to find false positives. Those are simply discarded by verifying if the bit rate value actually read corresponds to the value given (the true or false positive). If not, the value was a false positive.
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Here is the most recent version of the MCP2210 library, which is still very lacking and under development. I've decided to make interruptTransfer() private, because I saw no advantage on keeping it accessible. The MCP2210 only seems to accept one HID response fetch per HID request, and any more will lead to errors. Thus, the use of interruptTransfer() by the caller is not a good practice.

Any debugging will be done through hidTransfer(), as the MCP2210 is prepared to accept unknown commands. That function always ensures the correct use of interruptTransfer().
 

Attachments

Last edited:

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Meanwhile, I've made more progress. Attached is a program that tests almost every function that was done so far. It should pass every test case. That is how I test my set and get functions.

As a note, I find the MCP2210 a bit sluggish. The CP2130 had far more tests, and it was able to complete them quite snappily. I hope this is not an issue in the future.
 

Attachments

nsaspook

Joined Aug 27, 2009
13,081
Meanwhile, I've made more progress. Attached is a program that tests almost every function that was done so far. It should pass every test case. That is how I test my set and get functions.

As a note, I find the MCP2210 a bit sluggish. The CP2130 had far more tests, and it was able to complete them quite snappily. I hope this is not an issue in the future.
It's not a speed daemon but it gets the job done.
PXL_20220301_204957256.jpgPXL_20220301_204752610.jpg
RPi 2 USB demo connected to my MCP2210 I/O board with IMU.

 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
How many packets are you sending per cycle? Are you able to do some probing with Wireshark?

By the way, this is what I'm getting. It is acceptable, but the CP2130 was much faster. I think it was because the packets were much smaller.

Screenshot_20220302_231709.png
 

nsaspook

Joined Aug 27, 2009
13,081
It's a simple transfer that sleeps for 20,000us after the device reads. The max IMU update is 12.5Hz.
C:
#define BMX160_R                        0b10000000
#define BMX160_DATA_LEN                 31
#define BMX160_DATA_REG                 0x04

bmx160_get(BMX160_DATA_LEN, BMX160_DATA_REG);

/*
 * read SPI data from BMX160 register
 */
uint8_t bmx160_get(uint8_t nbytes, uint8_t addr)
{
    cbufs();  // clear buffers
    // BMX160 config
    S->buf[0] = 0x42; // transfer SPI data command
    S->buf[1] = nbytes; // no. of SPI bytes to transfer
    S->buf[4] = addr | BMX160_R; //device address, read
    S->res = SendUSBCmd(S->handle, S->buf, S->rbuf);
    while (S->rbuf[3] == SPI_STATUS_STARTED_NO_DATA_TO_RECEIVE || S->rbuf[3] == SPI_STATUS_SUCCESSFUL) {
        S->res = SendUSBCmd(S->handle, S->buf, S->rbuf);
    }
    return S->rbuf[5];
}
PXL_20220303_003004560.jpgPXL_20220303_003026706.jpg

PXL_20220303_003852317.jpg
Wireshark usbmon.
 

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Here is the latest iteration of the class, plus the test program. I'm amazed that the program is very slow when writing 255 bytes to the EEPROM, and reading them back. Overhead, perhaps?

Whoever made the decision of making the MCP2210 an HID device, has made a very bad decision. And the mandatory 64 bytes per interrupt transfer is a big no no. It makes a whole lot of garbage just to send a couple of bytes.
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
I've decided to simplify things. Essentially, hidTransfer() now always returns a vector with a size of 64 bytes. Thus, I don't have to check for errors when getting data. However, and as before, it is up to the client program to check for errors, because the data returned will be bogus in case of some error during transfer.
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Here is a new version. As far as it goes, it is able to read descriptors. I haven't tried to write any descriptors yet, because I wish to test this on a "guinea pig" other than my original evaluation board.

My question now is if I need to fill every unused byte with 0xFF after the descriptor data. Originally, the manufacturer and product descriptors are returned with 0xFF on every byte after the descriptor data. The CP2130 would return 0x00, or null characters, after the used characters. Basically, I'm planning to write the descriptors padded with 0x00, which the current code does (the data is internally padded with 0x00 inside hidTransfer(), if the input vector is short).

Questions, questions...
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Here is an update. I've implemented GPIO related functions, as well as more NVRAM functions. However, I haven't tested the functions that write the NVRAM. I'm still waiting for my test board to arrive. There is much more to do, but I'm getting there.
 

Attachments

nsaspook

Joined Aug 27, 2009
13,081
Here is an update. I've implemented GPIO related functions, as well as more NVRAM functions. However, I haven't tested the functions that write the NVRAM. I'm still waiting for my test board to arrive. There is much more to do, but I'm getting there.
The interrupt related GPIO functions can be critical for efficient I/O code for SPI connected input triggers.
3.4 External Interrupt Pin (GP6) Event Status The External Interrupt pin event status command is used by the USB host to query the external interrupt events recorded by the MCP2210. In order to have the MCP2210 record the number of external interrupt events, GP6 must be configured to have its dedicated function active.
mcp.pngtic.png
For my board I use the TIC12400 interrupt out pin (24) as a input to MCP2210 GPIO 6 in FUNC2 pin (14) to trigger the event counter. This saves SPI I/O to the actual TIC12400 device until there is a change in the input switch status.
C:
// GPIO CHECK with USB only I/O
/*
 * when connected to the TIC12400 interrupt pin it shows a switch has changed state
 */
bool get_MCP2210_ext_interrupt(void)
{
#ifdef EXT_INT_DPRINT
    static uint32_t counts = 0;
#endif

    cbufs();
    S->buf[0] = 0x12; // Get (VM) the Current Number of Events From the Interrupt Pin, GPIO 6 FUNC2
    S->buf[1] = 0x00; // reads, then resets the event counter
    S->res = SendUSBCmd(S->handle, S->buf, S->rbuf);
    if (S->rbuf[4] || S->rbuf[5]) {
#ifdef EXT_INT_DPRINT
        printf("\r\nrbuf4 %x: rbuf5 %x: counts %i\n", S->rbuf[4], S->rbuf[5], ++counts);
#endif
        return true;
    }
    return false;
}

// MAIN CODE LOOP


            /*
             * check for change in MCP2210 interrupt counter
             */
            if (get_MCP2210_ext_interrupt()) {
                /*
                 * handle the MC33966 chip MCP2210 SPI setting
                 */
                setup_mc33996_transfer(3);
                /*
                 * send data to the output ports
                 */
                mc33996_set(mc33996_control, led_pattern[k & 0x0f], led_pattern[j & 0x0f]);

                /*
                 * handle the TIC12400 chip MCP2210 SPI setting
                 */
                setup_tic12400_transfer(); // CS 5 and mode 1
                /*
                 * read 24 switch inputs
                 */
                tic12400_read_sw(0, 0);
                /*
                 * look for switch 0 changes for led speeds
                 */
                do_switch_state();
                printf("tic12400 switch value %X , status %X \n", tic12400_value, tic12400_status);
                setup_bmx160_transfer(BMX160_DATA_LEN); // byte transfer, address and data registers
                k++;
                j--;
            } else {

            }
 

Attachments

Thread Starter

bloguetronica

Joined Apr 27, 2007
1,541
Well, the interrupt counter is in the plans. I have to implement many other things beyond that, like access to individual GPIOs, SPI transfers, NVRAM password access and setting (that will be for last). These is still much to do before I can release version 1.0.0.
 
Top