Close

USB, what you want to (and all you need is to get your boogie down)

A project log for HP 82240B IR Receiver and Printer Interface

This is an IR receiver and interface for adapting a commodity receipt printer to be 82240B-compatible.

ziggurat29ziggurat29 07/09/2017 at 17:370 Comments

Summary:

Despite having peripheral libraries, implementing the USB CDC interface was even more painful than the UART serial. But now the deed is done.

I'm now moving on to processing the incoming printer data stream, and rasterizing output to be printed.

Deets:

The USB CDC implementation is provided by what ST calls 'middleware'. USB is a byzantine protocol, and the on-chip peripheral IP has a myriad of registers to understand if you were attempt to hand-roll it. So, yeah, I had a good reason for taking the easy way out. But, alas, the implementation of this component was worse than the UART from before (well, worse to my tastes, at least). Amongst the problems I had with it were:

  1. the USBD_CDC_ItfTypeDef is a C style 'interface': a struct of function pointers. These functions are implemented by you (with skeleton generated from ST32CubeMX) and include things like 'initialize', 'deinitialize', 'control packet', and 'received data'. But notably absent is 'transmit complete'. So, you cannot know when a transmission you have started is completed so that you can continue with subsequent transactions. The sample projects use a timer to poll for completion periodically. Polling? Yuck! Fer real?
  2. there is a TX buffer declared in the generated code, but it's usage seems odd because the generated code seems to not use it at all. It is referenced only once, with a length of 0, and all subsequent transfers bind the caller supplied buffer. Hmm.
  3. the RX callback function seems odd in that it seems to assume that you can take all the data in the buffer you bind. There is a length param that is a pointer, so presumably you can alter it, but doing so would run counter to the caveat communicated in the @note (which implies you must take it all before returning from the method or there will be lost data). Hmm.

The first item is especially problematic because there is no way to add the TxComplete notification without editing the generated code. This means if I need to re-run STM32CubeMX (and I do all the time), my changes will be overwritten. Yuck.

OK, I simply have to have these changes -- I'm not doing this polling thing. That means I will have to re-apply the changes when I re-run the tool, so obviously I have backed up copies so I can re-apply them, though this is tedious. Additionally, the nature of the changes I have made are such that if they were to be overwritten, that the result will still build successfully, but it will not run correctly (the callbacks will never be called). Since this is a debugging time waster (you will one day forget to manually apply the changes), I have added a do-nothing API in the area with those changes that gets called early in main. It is a waste of code space, but at least it will cause linkage to fail if I do not re-apply the required changes. And obviously I have also left a comment at the call site to the effect of 'if this fails to build, you need to apply the xxx changes at yyy'.

The second item is problematic for several reasons; one, if the buffer is not going to be used, then why does it need to be defined at all (wasted RAM), and two, by binding the caller supplied buffer in the public API CDC_Transmit_FS(), there is now an assumption that the pointer will remain valid long after the call returns, to be used later during subsequent transmission activities (ostensibly at ISR time). This precludes common use cases like this:

void ShowMeTheMoney ( float fSmackers )
{
    char achBuffer[128];
    int nLen;
    //need linker flag "-u _printf_float" and it will incur ~12k code penalty
    nLen = sprintf ( achBuffer, "You have $%0.2f smackers\r\n", fSmackers );
    CDC_Transmit_FS ( achBuffer, nLen );
}

because the buffer must be of long (and indeterminate!) duration outside this function's scope. So you'll either have to define your own long-lived buffer and copy into it (and arbitrated it as a shared resource, etc.) or some other scheme to work around the nature of the API.

The third item is worrisome; I'll have to study the implementation a bit more to see if there are actual buffer overflow exposures, but in the meantime, I made the internal transfer buffers (which I don't like) 64 bytes, which I believe is the largest size transfer that can occur in one packet in USB full-speed.

Another little thing is that the USBD_CDC_HandleTypeDef in usb_cdc.h has a buffer defined as CDC_DATA_HS_MAX_PACKET_SIZE in size. However, this chip doesn't do high-speed, so we waste 448 bytes. I changes this to CDC_DATA_FS_MAX_PACKET_SIZE to reclaim that memory.

At length, I made the desired changes and provided the higher-level implementation, so now the API for the USB CDC is:

//transmit data via USB CDC implementation; returns bytes enqueued
size_t USBCDC_transmit ( const void* pv, size_t nLen )

//read data received from USB CDC implementation; returns bytes dequeued
size_t USBCDC_receive ( void* pv, const size_t nLen )

//maximum that can be read from the USB CDC
size_t USBCDC_receiveAvailable ( void )

//maximum that can be pushed into the USB CDC
size_t USBCDC_transmitFree ( void )

//optional callbacks you can implement to get notified of xmit/recv
void USBCDC_DataAvailable ( void );
void USBCDC_TransmitEmpty ( void );
I.e., a form consistent as with UART2. Now I feel clean again...

Next:

Now I think I have finally gotten all the low-level components in place, and have bandaged over warts therein. Now I need to do some higher-level stuff, including:

my new new new idea is having various operational modes on this interface device. This morning it occurred to me that the interface board could be useful to someone even without a physical printer, e.g. as an HP IR to serial converter, supporting both UART and USB CDC. This mode would be simpler -- essentially 'data pass-through', maybe with some additional options (e.g. translate the special purpose CR (0x04) into a normal CR (0x0d)). More thinking needs to go into this, of course.

Discussions