NXP USB and libUSB now working

31 August 2023
Development of firmware to use USB directly rather than relying on an RS232 adapter chip has been an on-and-off project of mine for years and it was late-2020 that came the breakthrough of getting an NXP LPC11U24 microcontroller to enumerate under Linux, having in parallel tried to get USB operational with Microchip PIC16F1454 microcontrollers. Most likley due to circumstances at the time this felt like a good point to give it all a rest and as with a lot of my projects back then it got left at the way-side. Almost three years later it was finally time to have another look at the firmware and turn it into something that is actually useful, namely getting the microcontroller to communicate with libUSB and pass data from a host desktop system.

The hardware

In total three revisions of the development PCB were made, two based on LPC11U24 chips and one based on a LPC11U12. The first revision needed rework since the programming interface was wired up for full JTAG rather than SWD (Serial Wire Debugging) but ultimatelely it was abandoned because even though the reference manual (document code UM10462) mentions low-speed mode the chip only supports full-speed mode, and full-speed requires an external oscillator. While the second revision had no apparent design problems only one of the three reflow-soldered boards was ever operational and for reasons and for better or worse a cut-down third revision was made rather than fabricating more of the former — as for why this was back in the days of pre-vaccine Covid-19 and I don't look back at anything going through my mind at that time. I might be able to rework one or both of the non-functional revision-two boards into something operational but for now the three working boards are sufficent for my needs.

Functioning development PCBs

Unlike the others the third revision is open-source alongside its programming adapter, as is the breakout board for the PIC16F1454 chip. Looking back I prefer the second-generation NXP board to the third-generation board but for reasons long forgotten did not use it much in practice. Getting anywhere with a Microchip PIC16F1454 turned out to be too much of a headache and although some of the information obtained reading its data-sheet was useful in trying to get NXP chips to work, the trouble of getting it to work instead of either using the NXP or an RS2323-USB adapter chip does not seem worth the effort.

Development infrastructure

Back when I first tried ARM firmware development rather than using off-the-shelf adapters I developed my own ARM programming interface infrastructure for reasons of cost and personal convenience. The 20-pin IDC connector that the Olimex programming tool used was far too big, with the Cortex-M4 breakout boards being the only circuits to date big enough for a programming interface of that size. As far as I could tell the standard interface for JTAG SWD (Serial Wire Debugging) seemed to be 10-pin mini-IDC receptacles that were grossly overpriced for what they were and felt a bit too fragile, so I created an adapter that used standard-sized 10-pin IDC. For instances when space really mattered I also developed my own interface standard based on six-pin MicroMatch Value connectors since they were both compact and a lot cheaper than IDC, and a refined version of this ‘standard” was open-sourced in the hope others might find it useful.

Programming hardware

These days reasonably up-to-date versions of all the required software needed to create and flash firmware is available from SlackBuilds, and the two articles written back in 2018 on using OpenOCD and the GCC tool-chain are still valid. By far the most common error is permission issues with OpenOCD leading to LIBUSB_ERROR_ACCESS and although the “proper” solution is to setup a udev rule for some reason this did not work for me on the system I was using so instead resorted to setting /usr/bin/openoc to SUID root — normally a security hazard but acceptable on systems no-one else uses.

The firmware

The root cause of trouble developing the firmware was due to an undocumented quirk in how the DEVCMDSTAT was handled, which contains the seven-bit USB device address and an enable bit — all eight of these bits have to be written in one go rather than using boolean logic to set the address followed by the enable bit, something that took me months to find out. Once this bit was cracked getting as far as the device successfully enumerating within Linux was fairly quick but this being late-2020 I did not have the energy to push ahead with getting data transfers with libUSB operational. The firmware itself did not need much to get data transactions working and it was useful revisiting the code with a clearer mind having set it aside.

Changes to the code

Some of the changes to the code are for technical correctness such as using the EPSKIP register rather than clearing end-point active flags directly, but I do wonder the extent the hardware actually tolerates supposedly incorrect usage as things were working fine previously. The one remaining deviation from the flow-charts given in the reference manual was not enabling any interrupt on NAK functionality, as there is no way to distinguish interrupts caused by NAKs from other end-point events but enabling them was causing problems I was unable to solve, so in the end simply left them disabled because there would be nothing that could be done about them in either case. There is a lot of stuff that only gets discovered via experimentation and one of those things is the way Linux requests descriptors twice — once asking for only the first few bytes and then a second time to get the whole payload — so I expect this sill not be the past time I dig through the control endpoint code. The future may prove otherwise but I think a lot of the state of control transactions is redundant — for instance the only standard request I am aware of that has an outbound data stage is SET_DESCRIPTOR and from what I have read it is de-facto optional as most devices have no practical use for it.

The new bits

Compared to control transactions, sending and receiving data via interrupt transactions is surprisingly simple — queue up buffers with either data, or in the case of device-to-host endpoints set the stall bits to indicate none is available if that is the case, and wait for the interrupts when something has been done with them. There is none of this sending a zero-sized buffer to indicate an ACK, with the in and out directions of data endpoints being fully independent of each other. During the development there were times when it seemed something was going wrong with repeated control transfers, which could be an issue with libUSB although edge cases in the firmware is more likely, but the data endpoints remained rock solid.

Data-sheets vs. reality

While the technical documentation is certainly among the better ones I have read over the years it also contains several errors which I suspect slipped in because the content was cut-n-pasted from internal information in individual pieces of silicon, and I have issues about these errors never being corrected. The most egregious error is talking about non-existent low speed support but another is Table 204 in section 11.6.6 which describes the USB end-point Skip register lists bits 0 to 29 as ones that are associated with endpoints which is clearly incorrect as the end-point table only has twenty rows. So far I have not had most of the pit-falls listed as special cases in section 11.7.5 so I do wonder if the hardware is actually doing some of this stuff for me. A difficulty here is differences between the actual USB protocol specifications and how the hardware presents an interface to it.

Open vs. closed source branches

For this firmware a private fork was made which underwent heavy refactoring and only minimalist changes were back-ported to the existing open-source release, which was done because the latter is intended as a bare-bones get-things-working version aimed at those trying to figure out the basics rather than a complete off-the-shelf library for production use. In contrast the private fork has other changes that are general software engineering and only tangential to getting USB working — an example is end-point buffer allocation where the public version uses an algorithm to allocate a single 64-byte buffers and calculates the required data pointers, whereas the private version uses a manually-constructed lookup table. The refactoring work done on my private branch makes the code harder to follow for those who just want to see some signs of life, and the latter is why the debugging messages that are emitted via RS232 were overheauled in the public fork whereas they were mostly stripped out of the private one.

Remarks

Back in 2018 and 2019 I had a stack of data-sheet printouts for various ARM-based microcontrollers documenting features I wanted to figure out and at some point USB got added to the pile, and it would be years rather than months before USB as the one remaining thing would be cleared from that pile. But cleared is what has finally happened and for a long time this was to be my final frontier of firmware development. Getting things like USB working on other chipsets such as the ST Microelectronics STM32L4R5 M4-Cortex is still on my bucket-list but for now there is no practical purpose figuring out alternative chipsets. Firmware interests me but since most of my professional work is low-level software development I prefer other things on then hobby side, hence my interest in hard-wired electronics.