The presence of a USB port in modern microcontrollers opens up wide possibilities for the independent production of various devices controlled from a computer. In practice, however, it turns out that the USB libraries supplied by the manufacturer need to be improved. If you are interested in the experience of similar refinement for two popular families of MCs - welcome under cat.
Formulation of the problem
So, we want to make a device that exchanges messages of arbitrary length with a computer via a USB port. The easiest way to do this is to use a USB character device (CDC) class, also known as a 'virtual serial port'. Then on the host system to which you connect your device, a serial port will automatically be created, through which you can communicate with the device, working with it as with a regular file. In practice, however, it turns out that some of the necessary functions for this in the manufacturer’s USB stack are either not implemented at all or are implemented with errors. We will start by looking at the STM32 microcontrollers (the first case) and end with another popular family - Texas Instruments Tiva C (the second case). Both families have the ARM Cortex M4 architecture.
STM32 - just add code
STM microcontrollers usually have a rich functionality at a very affordable price. The manufacturer supplies a wide range of libraries for all occasions. Among them there are libraries for USB support, and a library for working with other peripherals on the chip. Recently, all of these libraries have been combined into one mega-package called the STM32Cube. At the same time, however, they did not particularly care about compatibility and changed everything that they could change, including the names of the fields in the structures that describe the configuration of the I / O ports, while the very name of the structure remained the same. It is interesting that there is also a third variant of examples and libraries, which can be found on the site
stm32f4-discovery.com . However, the author of this option likes to rename files borrowed from the STM in order to perpetuate its initials, which also does not add compatibility with all other code. Considering all the above, I decided to take as a basis the last pre-cubic version of the libraries supplied by STM. Now they can be found in the compiler distribution kit (I use IAR). In order not to search for a long time, the libraries are included in the project, which you can take from the link below. For experiments, I used the STM32F4DISCOVERY board
www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF252419 . If you have another board and the code did not work right away, the case is most likely in the frequency of the external crystal oscillator. Although libraries abound with all sorts of macros, and in the latest version of the libraries a macro for external clock frequency appeared among them, this parameter is still written in the code as a number without any comments, it seems that the developers did not lose their form and did not forget to read the manual. You can find this number — the clock frequency in megahertz — in the system_stm32f4xx.c file in the definition of the macro PLL_M.
')
So, we take as a basis a ready-made example that shifts the data from USB to the serial port of the microcontroller and back. We will not need a serial port, and we will simply transfer the data from the input stream to the output stream, that is, we will implement the echo. With the help of PuTTY we make sure that it works. But this is not enough. To exchange data with the device, we need to send a lot more than one character at a time. We write a test program on python, which sends packages of random length and reads the answer. And here we are in for a surprise. The test works, but not for long, after which another reading attempt either hangs forever or ends on timeout if it is set. The study of the problem using the debugger shows that the MC did send all the data, and the last parcel was 64 bytes long. What happened?
The USB stack on the host system has a multilayer structure. At the driver level, the data was received, but remained in his cache. The driver sends the cached data to the application when new data arrives and replaces the old data, or when the driver finds out that new data is not yet expected. From where can he get this knowledge? USB bus transfers data packets. In our case, the maximum packet size is just 64 bytes. If in the next data packet came less, then new data can not wait, and this is a signal in order to transfer to the application all the data received. And if the data came exactly 64 bytes? In this case, the protocol provides for sending a zero-length packet (ZLP), which is the stream interruption signal. Having received it, the driver realizes that new data should not be expected yet. In our case, he did not receive it because the developers of the USB stack for STM32 simply did not know anything about ZLP.
The second problem that the developers of the USB stack undeservedly paid attention to - what to do with the data that was received via USB, if they have nowhere to go, because input buffer overflowed. By and large, they were not at all worried about the problem of the input buffer - they assumed that all the data obtained was immediately processed, which, of course, could not always be fulfilled. In the USB protocol, in case the data cannot be received, a NAK response is provided - a negative acknowledgment. After such a response, the host simply sends the data again. If we want to avoid an overflow of the input buffer, we need, if there is no space for a full packet (64 bytes), to transfer the channel to the NAK state, which ensures that the NAK automatically responds to all incoming packets.
Tiva C - Layer Cake with Bugs
For the experiments, the EK-TM4C123GXL board was taken
www.ti.com/tool/ek-tm4c123gxl . To compile, you need the TivaWare library package
www.ti.com/tool/sw-ek-tm4c123gxl . The study of libraries shows that the developers did not ignore either the ZLP or the problem of buffering - there are annular buffers in the input and output channels. However, the automatic test gives the same result - the data exchange suddenly stops. Using the debugger, it turns out that this time the data is stuck in the circular transfer buffer, and the size of the last packet, and therefore the ZLP, is not a problem.
It is possible to identify the problem only by carefully studying the sources of the libraries. It turns out that in order to send ZLP it is necessary to set a special flag, which is not set by default. Perhaps this circumstance prompted other developers to add code that sends the ZLP in another place - at a lower level of the USB stack, and without the checkbox. This change introduced a bug that caused the transfer to stop. The problem arises as follows. The transmitter receives the next packet when the transmission of the previous one ends, or if the previous one was not, and the application has added data to the transmission buffer. The code that initiates the transfer receives a notification of the completion of the transfer of the previous packet from the lower level of the USB stack. The problem is that if the lower level of the stack has initiated the transfer of the ZLP, then it does not send a completion notification, because He initiated the transfer himself. The upper level does not start data transmission while the transmitter is busy transmitting the ZLP packet, and does not start the transfer after it is completed, because it does not receive notifications - the transmission process stops. To fix the problem is very simple - you need to remove the lower level code that sends the ZLP and give it to the upper level of the stack. The second problem that needs to be solved is that the procedure that starts the transfer can be called both from the context of the interrupt handler (when the transfer is completed) and from the application context by adding data to the transfer buffer. To serialize calls to this procedure from different contexts, you need to disable interrupts for the duration of its execution.
Source
It lies here
github.com/olegv142/stm32tivc_usb_cdc .
In the stm and ti folders there are 2 test projects each - usb_cdc_echo and usb_cdc_api. The first simply sends all the data back, the second implements the packet protocol, which you can easily adapt to your needs. In the tools folder - test python scripts.