November 4, 2023

Let's learn USB! -- Papoon - a second look

I took a look at Papoon about a week ago and gave it when the examples distributed with it did not build. I turned my attention then to TinyUSB and decided that it was even less promising for my purposes.

Papoon is written in C++. This would not be my first choice, but beggars cannot be choosers. When I try to build the first example distributed with papoon, it pulls in the standard crt0.o file that is part of g++. This thinks that, since there is a main() function, this is going to run as some kind of program in a linux system, and expects things like _exit(), _sbrk(), and other standard linux system calls to be provided.

What I decided to do, was to rename main() to something else. I chose cxx_main(). Once this is done, the C++ compiler does not get the itch to drag in the standard startup code.

The next trick is to learn how to call C++ from C (and vice versa). The way to set up a function that can be called from C is like so:

extern "C" void
papoon_main ( void )
{
    cxx_main();
}
Now I can call "papoon_main()" from my C code and it in turn calls cxx_main() for me. Of course I could just make cxx_main() C callable in this way, but I like to have as many layers of isolation and protection as possible between my nice C code and this angry world of C++. In turn, I would like to like to call my C printf function from C++. This can be done by simply adding a prototype like this to the C++ code.
extern "C" void printf ( const char *, ... );

If you are curious, you may wonder just what this extern "C" business does. I peek at a disassembly of the generated C++ code and I see the following labels on the code for cxx_main(). Similar and even more wild and crazy labels (such as _ZN17stm32f10_12357_xx6UsbDev18serial_number_initEv) are generated by C++ for other functions.

_Z8cxx_mainv:
Who would have guessed? These things are hidden from the typical C++ programmer, who lives happily in a higher level world. The embedded programmer ends up having to be aware of details like this. But the extern "C" construct handles all this for us, forcing C++ to use the sane function names we might expect.

From previous experience with other C++ embedded situations (namely libmaple for the STM32F103), I know that C++ can generate lists of initializer (and destructor) functions that it expects the startup code to call before calling main. I made an attempt to check for these sorts of things, and did not find any generated by the papoon code, but I won't say that I am fully confident.

Add papoon to a STMF103 project of my own

I spent some time copying the first example file from the papoon distribution along with (to my surprise only 3 other) necessary C++ files, I put these in the papoon directory for this project on my Github: So I have these four source files:
example.cxx
usb_dev_cdc_acm.cxx
usb_dev.cxx
usb_mcu_init.cxx
I work up a Makefile and keep typing "make" and copying header files until suddenly it stops complaining and just compiles everything. The minimum set of header files thus obtained are these 8:
bin_to_hex.hxx
regbits.hxx
stm32f103xb_tim.hxx
usb_dev.hxx
core_cm3.hxx
stm32f103xb.hxx
usb_dev_cdc_acm.hxx
usb_mcu_init.hxx
All told, 7070 lines of C++ code, which is certainly non-trivial. From my code, I just call:
papoon_main ();

Give it a try

It all compiles after some fiddling with Makefiles and compiler options. So I load it into a blue pill board and see what happens. I see this on my linux system logs when I plug it in:
Nov  4 11:00:05 trona kernel: usb 3-1.2: new full-speed USB device number 11 using ehci-pci
Nov  4 11:00:06 trona kernel: usb 3-1.2: device descriptor read/64, error -32
Nov  4 11:00:06 trona kernel: usb 3-1.2: device descriptor read/64, error -32
Nov  4 11:00:06 trona kernel: usb 3-1.2: new full-speed USB device number 12 using ehci-pci
Nov  4 11:00:06 trona kernel: usb 3-1.2: device descriptor read/64, error -32
Nov  4 11:00:06 trona kernel: usb 3-1.2: device descriptor read/64, error -32
Nov  4 11:00:06 trona kernel: usb 3-1-port2: attempt power cycle
Nov  4 11:00:07 trona kernel: usb 3-1.2: new full-speed USB device number 13 using ehci-pci
Nov  4 11:00:07 trona kernel: usb 3-1.2: device not accepting address 13, error -32
Nov  4 11:00:07 trona kernel: usb 3-1.2: new full-speed USB device number 14 using ehci-pci
Nov  4 11:00:08 trona kernel: usb 3-1.2: device not accepting address 14, error -32
So, it doesn't work. As an experiment I grab a factory fresh blue pill, connect it to a USB cable and plug it in. I get exactly the same messages.

Proper initialization

I look at the papoon sources a bit and start to get an idea.
static const uint32_t   PERIPH_BASE         = 0x40000000U;

static const uint32_t   APB1PERIPH_BASE     = PERIPH_BASE              ,
                        APB2PERIPH_BASE     = PERIPH_BASE + 0x00010000U,
                        AHBPERIPH_BASE      = PERIPH_BASE + 0x00020000U;
The above code may well create initialized data. For a system like the STM32F103, these values will be in ROM, but will need to be copied into RAM. Maybe. These particular values, being constants, could simply be left in ROM, but without some experimenting I don't really know how the compiler handles this.

Whatever the case, I know full well that there are two things that I am not yet doing that I should be doing to launch even C code, never mind C++, namely:

I have code to do this in another project, so I copy it over, splice it in and add some print statements, just because I am curious. I see this:
STM32 usb_papoon demo starting
37 bytes of BSS cleared
86 bytes of Data initialized
Aha! So there definitely was a need to do this, and better yet, now I see this in my linux logs when I plug in the board:
Nov  4 12:35:42 trona kernel: usb 3-1.2: new full-speed USB device number 15 using ehci-pci
Nov  4 12:35:47 trona kernel: usb 3-1.2: New USB device found, idVendor=0483, idProduct=5740, bcdDevice= 2.00
Nov  4 12:35:47 trona kernel: usb 3-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Nov  4 12:35:47 trona kernel: usb 3-1.2: Product: STM32 Virtual COM Port
Nov  4 12:35:47 trona kernel: usb 3-1.2: Manufacturer: STMicroelectronics
Nov  4 12:35:47 trona kernel: usb 3-1.2: SerialNumber: 8717472853518850066fff48
Nov  4 12:35:47 trona mtp-probe[53888]: checking bus 3, device 15: "/sys/devices/pci0000:00/0000:00:1a.0/usb3/3-1/3-1.2"
Nov  4 12:35:47 trona mtp-probe[53888]: bus: 3, device: 15 was not an MTP device
Nov  4 12:35:47 trona kernel: cdc_acm 3-1.2:1.0: ttyACM0: USB ACM device
Nov  4 12:35:47 trona kernel: usbcore: registered new interface driver cdc_acm
Nov  4 12:35:47 trona kernel: cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
There is one more code change I need to do before poking at this with picocom. The file "example.cxx" has these lines:
	if (recv_len = usb_dev.recv(UsbDevCdcAcm::CDC_ENDPOINT_OUT, recv_buf)) {
            // process data received from host -- populate send_buf and set send_len

            while (!usb_dev.send(UsbDevCdcAcm::CDC_ENDPOINT_IN, send_buf, send_len))
                usb_dev.poll();

	}
The comment tells me that I should copy the recv_buf to the send_buf, or do something. As it is, it will receive data, but never respond. So I make it look like this:
	if (recv_len = usb_dev.recv(UsbDevCdcAcm::CDC_ENDPOINT_OUT, recv_buf)) {

            printf ( "Papoon recv %d\n", recv_len );
            // process data received from host -- populate send_buf and set send_len
            for ( i=0; i

I flash the new code.  I need to unplug and replug the make linux realize that something
has happened (here is where the disconnect circuit in the Maple board would be handy).
Now I can use "picocom /dev/ttyACN0" and it just works!.

I can even use Ctrl-A, Ctrl-X to exit picocom. The pill doesn't know anything happened (or doesn't seem to) and if I start picocom again, I am right back in business!

What is interesting is that usb_recv gets single characters in the pill. No waiting for full lines to accumulate or anything of the sort.

Conclusion

The main thing (believe it or not) I wanted to get out of this was to find out if an unmodified blue pill with the 10K resistor on USBDP would behave correctly. Based on this test, yes it does! I don't need to fuss around under my microscope adding a new resistor. This would have been a challenge (perhaps requiring me to order a supply of 1.5K surface mount resistors). Now I cannot say that a blue pill with the improper 10K resistor will work properly on all systems, but it certainly does on mine, at least on the port I am testing with (one on the front of my computer.

I try it on a different port. This one is the hub built into my monitor and connected to a USB on the back of my computer. It works fine there also.

Of course there was much more learned here, but we needed to know if a "stock" blue pill with the wrong 10K resistor would work. If not, we would have been trying to debug new code on faulty hardware.


Feedback? Questions? Drop me a line!

Tom's Computer Info / [email protected]