USBTMC Callback functions

mborgerson

Well-known member
My USBTMC (USB Test and Measurement Class) code has progressed to the point where I'm optimizing the data transfer functions that move data packets from the connected devices to the host. The USBTMC specification mandates request/response model where the host requests data packets of a specific size and the connected device returns data packets of exactly that size. I've got test code that does that, and can request and receive 8KB packets at about 4MB/second.

The T4.X USB stack (with help from the EHCI) takes about 360 microSeconds to break the 8KB input transfer into 16 512-byte sub-transfers, assign them to slots in the 125-microSecond microframes and move them down the wire. That's an effective instantaneous transfer rate of about 22MB/second. That rate is more than adequate to handle my goal of connecting 4 devices, each sending at about 4MB/second.

However, attaining that rate in the USBTMC request/response environment requires that I know when one 8KB response packet has been received at the host so that it can request the next packet. The device at the other end needs to know when the transmission of the previous packet is complete, so that it can set up the read of the next request and prepare the next response packet. The device also needs to know, as soon as possible, when a request has been received so that it can prepare and send the response ASAP.

Fortunately, you can set up the USBTMC send and receive endpoints to execute a callback function for each read and write transfer. The callback function has a single parameter: a pointer to the transfer structure that has just been completed. From this pointer, your callback function can derive the size of the transfer and the location of the data sent or received.

Unfortunately, my USBTMC driver is written as C++ class. That means that I have to use an external transfer function to pass the callback data into my class member functions. I'm doing this the old-fashioned way, without the new lightweight callback functions:

Code:
// These functions are outside the class
void etx_event(transfer_t *t){
  usbtmc1.tx_event(t);  // call the tx member function for this instance of the USBTMC class
}

void erx_event(transfer_t *t){
  usbtmc1.rx_event(t); // call the rx member function for this instance of the USBTMC class
}

//The USB stack is set up to use these callbacks in the initial configuration of the class:

void USBTMCDev::configure(void) {
    usb_config_rx(USBTMC_RX_ENDPOINT, USBTMC_RX_SIZE_480, 0, erx_event); // 0: no zlength packets
    usb_config_tx(USBTMC_TX_ENDPOINT, USBTMC_TX_SIZE_480, 0, etx_event);  
}

That works for the single USBTMC instance I'm testing now. But I want to be able to connect up to four separate devices to my T4.1 host. It's no problem to generate four different instantiations of the USBTMC class. They will all use the same member function code, but each will have its own set of private variables. At this point, I'm trying to decide whether to have each instance have internal send and receive buffers (adding about 17KB to the instance size) or keeping the instance sizes smaller by passing pointers to the send and receive buffer in a begin() function. The calling sketch will then have to allocate buffers. I'm inclined to keep the buffers inside the driver instance, as it simplifies the sketch code. The same 17KB of DTCM per device gets used in each case.

The issue is that each device instance has to provide a way for the sketch that will collect the data to have access to the buffer used to send the data. Since the USB EHCI will push the data down the wire with DMA, I'm using two 8KB buffers in ping-pong fashion so that the sketch can add data to one buffer while the USB stack is transmitting the other. Managing those two buffers is another reason I need to know when the data transmission has completed, so that I can swap buffers.

That still leaves the problem of connecting each instance of the driver to the appropriate callback functions. The driver for the multiple hardware UART Serial ports does this with a separate C++ driver wrapper function for each UART, which then uses a single instance of HardwareSerial class to do the grunt work. HardwareSerial1 just defines forwarding IRQ handlers and allocates the buffers.

The Hardware Serial case is complicated by the fact that the subclass for each UART has to use different registers to manage the port. I have a simpler case with the USBTMC driver. Each USBTMC device will have only one instance of the driver. When the device sends the data to the host, it will attach a header which tells the host which device sent the packet.

One thing I intend to try is to define four sets of forwarding functions, and have the begin tell the driver instance which one to use:

Code:
// These functions are outside the class
void etx_event1(transfer_t *t){
  usbtmc1.tx_event(t);  // call the tx member function for this instance of the USBTMC class
}
void erx_event1(transfer_t *t){
  usbtmc1.rx_event(t); // call the rx member function for this instance of the USBTMC class
}


void etx_event2(transfer_t *t){
  usbtmc1.tx_event(t);  // call the tx member function for this instance of the USBTMC class
}
void erx_event2(transfer_t *t){
  usbtmc1.rx_event(t); // call the rx member function for this instance of the USBTMC class
}
 etc, etc.

In the sketch you would have:

usbtmc1.begin(etx_event1, erx_event1);
usbtmc2.begin(etx_event2, erx_event2);

In the Driver, begin() uses the input event handler to configure transmit and receive endpoints:

void USBTMCDev::begin(void (*erx_event)(transfer_t *completed_transfer),
                            *etx_event)(transfer_t *completed_transfer) )
{
    usb_config_rx(USBTMC_RX_ENDPOINT, USBTMC_RX_SIZE_480, 0, erx_event); // 0: no zlength packets
    usb_config_tx(USBTMC_TX_ENDPOINT, USBTMC_TX_SIZE_480, 0, etx_event);  
}

I've not yet tried this option, so there may be syntax errors.

Are there other ways to handle this problem? Any suggestions or critiques would be greatly appreciated.

If I get a working code set, I will post the driver source. It's not too long--only about 350 lines with lots of comments and debug code to flip various pins for oscilloscope analysis. Posting runnable code is complicated by the fact that it relies on modified versions of usb_desc.c and usb_desc.h in the cores\Teensy4 folder. Asking someone to modify their core files is a bit like asking someone to jump into a swimming pool with a few deadly snakes lurking in the depths. If you're going to do that, you'd better have good instructions on how to drain and refill the pool afterwards--without the snakes. That just gets you to the point where you can compile the device code. Using it means including a modified USBHost_t36 host driver. That pool has not only snakes, but some nasty alligators!
 
The technique will be the same whether you use std::function or the new lightweight function type. It’s because there’s a need to “impedance match” C functions to C++ function objects.

See what @luni does here:

Basically, there’s some static state and slots are searched for an empty spot.
 
Back
Top