December 20, 2020

USB - run some code

Some time ago, I cloned this repository from Github:
git clone https://github.com/rogerclarkmelbourne/Arduino_STM32.git
Buried inside here, is the directory Arduino_STM32/STM32F4/cores/maple/libmaple/usbF4, which is what I have been studying, for better or worse. This is code from ST Micro, taken from who knows what exactly. It came from them, then was part of libmaple, then that morphed into Arduino_STM32, or something like that.

I have been studying this code, but got the idea, "how hard would it be". So I pulled this directory into my "Hydra" project and spent about 3 hours fussing with it and creating a Makefile and got it to build and run, which was less trouble and suffering than I expected.

I did some cleanup. For whatever reason (i.e. signficant brain-damage) the ST code sequesters C source in an "src" directory and related include file in a "inc" directory. I did away with that nonsense and put both files together in one directory. I also shortened long directory names, and when I was done, I had files in 3 directories: driver, library, and vcp. The library also had things neatly split into a Core and Classes directory, with the code of interest to us in the Classes/cdc directory. I did away with all of that, deleting all the other Classes and putting the core and cdc files (and include files as well) into the directory "library". Too much "organization" is actually a bad thing.

Some of their nomenclature is a complete mystery. Who knows what DCD is supposed to stand for, but it ends up everywhere. Vcp and cdc are straightforward. Vcp stands for "virtual com port", and that is exactly what we want to end up with. Cdc is "communications device class" and is one of the standard USB classes that you can go read about. Just why "CDC" doesn't do the job by itself and why we need VCP to go along with it, is another mystery. Just what the API is between the various components of this 3 part epoxy is pretty vague, but it may all make sense someday.

At first glance this doesn't seem like much code, but when you actually count lines, there are 6413 in driver, 3136 in library, and 4081 in vcp for a total of almost 14,000 lines which is nothing trivial at all.

Running this

The idea is to run this code, and add printf statements to let it tell me how it operates. It is one thing to study the source code itself, and another to observe it in operation.

So, when I fire this up on my STM32F411 board and my linux system sees it, linux likes what it sees and hooks up a driver and calls this /dev/ttyACM0. I can put together a little loop to send a hello message once a second, run picocom on /dev/ttyACM0 and I see output.

Reading

There is a call to read bytes, VCPGetBytes ( buf, len );. I call this in a loop and discover two things. One is that if there is nothing waiting to read, it returns immediately with a zero count. When there are bytes to read, it returns them. So far, using picocom, it only ever returns one byte at a time, which makes sense. This would work, if you don't mind the non-blocking nature of this call. I do mind, and set about right away arranging a callback scheme. I dig into VCPGetBytes() and figure out how to have the USB code just call my callback when it has data. I add the call, usb_hookup ( usb_handler ); to specify the function I want to be called, and this works just fine; much better in fact to my mind.

Writing

There is a call to write bytes, VCP_DataTx ( buf, len );. I add usb_puts() and usb_printf() on top of that as a nicer interface and continue with testing. This all works fine. However, when my little board boots, picocom exits with the message "FATAL: read zero bytes from port". This pretty much makes sense, because the device actually goes away for a while. It takes me a while to move my mouse around and start it up again, and I miss the first few messages. I have experimented with this a bit and there seems to be some variation in what happens. I always miss the first message. I sometimes miss all the messages before when I actually start picocom. Sometimes however, I start picocom and find all but the first messages waiting. This only seems to happen when I actually power cycle my little board, but not always, and generally not.

This merits some investigation. It seems like it should be possible (up to a point) to buffer pending output so we always get everything written to USB when we connect picocom. It turns out that like reads, writes simply deposit data into a circular buffer and then return immediately. I see no evidence that this gets reinitialized during a USB reset (which is what I suspected).

So, why do we always loose the first bit of console output? My test runs with a 1 second delay. The first message is lost, but the second comes through fine. Well, when the first message is sent, it goes into the buffer and the code performs the manipulations to start the transfer on endpoint 9, but it is simply not ready. We still have not finished with setup and enumeration.

Circular buffers

This is just a side note. The code uses a simple and surprising way to handle wraparound in the circular buffer. The buffers are 2048 bytes in size, which is a power of 2. So we can set a size mask as (SIZE-1). To calculate how many bytes are in the buffer we can do (in-out) & MASK, which is rather neat.
Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org