rsta2 / circle

A C++ bare metal environment for Raspberry Pi with USB (32 and 64 bit)
https://circle-rpi.readthedocs.io
GNU General Public License v3.0
1.83k stars 242 forks source link

USB CDC/ACM gadget #405

Closed sebastienNEC closed 6 months ago

sebastienNEC commented 10 months ago

Hello, It would be quite handy to have the raspberry pi appear as /dev/ttyACMx so as to send over USB e.g. the log messages of circle. This way, debugging would be possible without a serial/usb adapter. I'll start working on such an implementation, as usbcdcgadget(endpoint).h/cpp. Unless this is in the works already by someone else ?

rsta2 commented 10 months ago

This would be a great new feature. Feel free to develop it, if you like.

sebastienNEC commented 10 months ago

I am facing the issue that linux issues class-specific control messages on EP0, which circle reacts to by STALLing the communication on line 142 of dwusbgadgetendpoint0.cpp

The control message is a SET_LINE_CODING one: bmRequestType = 0b100001 bRequest = 0h20 wValue = 0 wIndex = 0 wLength = 7

I don't actually need this information, so how can I just "swallow" for now this control message in dwusbgadgetendpoint0.cpp ?

Thank you !

rsta2 commented 10 months ago

The following patch should ignore all other OUT requests:

diff --git a/lib/usb/gadget/dwusbgadgetendpoint0.cpp b/lib/usb/gadget/dwusbgadgetendpoint0.cpp
index 58cb6cf..570012e 100644
--- a/lib/usb/gadget/dwusbgadgetendpoint0.cpp
+++ b/lib/usb/gadget/dwusbgadgetendpoint0.cpp
@@ -139,8 +139,27 @@ void CDWUSBGadgetEndpoint0::OnControlMessage (void)
            break;

        default:
-           Stall (TRUE);
-           BeginTransfer (TransferSetupOut, m_OutBuffer, sizeof (TSetupData));
+           // Ignore all other OUT requests
+           if (pSetupData->wLength)
+           {
+               m_State = StateOutDataPhase;
+
+               // EP0 can transfer only up to 127 bytes at once. Therefore we split greater
+               // transfers into multiple transfers, with up to max. packet size each.
+               assert (pSetupData->wLength <= sizeof m_OutBuffer);
+               m_nBytesLeft = pSetupData->wLength;
+               m_pBufPtr = m_OutBuffer;
+
+               BeginTransfer (TransferDataOut, m_pBufPtr,
+                        m_nBytesLeft <= m_nMaxPacketSize
+                          ? m_nBytesLeft : m_nMaxPacketSize);
+           }
+           else
+           {
+               m_State = StateInStatusPhase;
+
+               BeginTransfer (TransferDataIn, nullptr, 0);
+           }
            break;
        }
    }

This is untested. I hope, this is sufficient for now. Later we'll need a method, so that the gadget class driver can receive class specific requests, but currently I'm busy with other tasks (RPi 5).

sebastienNEC commented 10 months ago

Thanks you this works ! I'll clean up the code and send the new usbcdcgadget* files. Good luck with the Raspi5 - they changed everything with the auxiliary chip for the low-speed interfaces...

rsta2 commented 10 months ago

Good. I'm looking forward to your CDC gadget files. Thanks! Yes, it's a challenge (RPi 5).

sebastienNEC commented 10 months ago

Here are the files usbcdcgadget.zip

sebastienNEC commented 10 months ago

The API is different from the MIDI case. I made the Gadget class a CDevice, overriding the Write() method, so that it would be suitable for CLogger::SetNewTarget() And for the reading part, I am not overriding Read() but providing a callback mechanism with RegisterReceiveHandler() If that makes sense ?

One subtelty/hack in the code: Linux needs 3 endpoints to recognize a CDC/ACM device as such. The regular bulk In/Out endpoints, plus an interrupt Notif endpoint. Circle does not support (yet?) interrupt endpoints, but fortunately linux does not seem to use this endpoint, so we can get away with simply not instantiating it.

sebastienNEC commented 10 months ago

Usage e.g.:

CSerialDevice* pSerial = new CSerialDevice(CInterruptSystem::Get()); pSerial->Initialize(115200);

CLogger* pLogger = new CLogger(LogDebug, CTimer::Get()); pLogger->Initialize(pSerial);

void usbCDCReceiveHandler(void *pBuffer, unsigned nLength) { pSerial->Write(pBuffer, nLength); }

CUSBCDCGadget* pUSB = new CUSBCDCGadget(CInterruptSystem::Get()); pUSB->Initialize(); pUSB->RegisterReceiveHandler(usbCDCReceiveHandler); pUSB->UpdatePlugAndPlay(); pLogger->SetNewTarget(pUSB);

while (true) { pUSB->UpdatePlugAndPlay(); }

rsta2 commented 10 months ago

Thank you for this valuable contribution! I will test it next weekend and give you feedback.

rsta2 commented 10 months ago

Thanks again for this driver! I pushed your sources (with small modifications) to the branch serial-cdc-gadget in the Circle repo and added a test program in test/usb-serial-cdc-gadget/. Your driver works already well. I tested it successfully with Linux 6.6.1. Nevertheless I would like to have some modifications:

Some more notes:

Please let me know, if you want to implement the modifications above, or if I should get active on by myself here!

sebastienNEC commented 9 months ago

Hello Rene, thank you for these comments, they make sense. I think it's better if you implement them yourself since you are more familiar with the rest of the USB stack. In particular the integration with CUSBSerialCDCDevice, which is at the moment specific to the USB Host use-case (e.g. it calls GetHost()). I wonder also whether some modifications are necesarry with CKernelOptions::GetLogDevice(). CLogger will get created before the USB device is fully initialized, so some caching should probably happen ?

rsta2 commented 9 months ago

Hi Sebastien, I will implement it, but because of other tasks it will take some time. The API device should be CUSBSerialDevice (not CUSBSerialCDCDevice, my mistake), but it still needs some work to separate the logic from the existing host class driver from the interface code. I have to think about the log caching. There is already a backlog in CLogger. Perhaps there should be an additional boolean option for CLogger::SetNewTarget(), which simply sends the backlog contents to the new target, when it is set. Thanks again!

sebastienNEC commented 9 months ago

Thank you, and there is no rush at all for these modifications, of course !

rsta2 commented 6 months ago

After a long while I have now applied the the suggested modifications and merged the USB CDC/ACM gadget support to the develop branch. It will be part of the next release. The branch serial-cdc-gadget has been removed. This gadget can be tested with the test/usb-serial-cdc-gadget for example.

rsta2 commented 6 months ago

The USB CDC/ACM gadget support has been released with Circle 46.

sebastienNEC commented 6 months ago

Hello Rene, In usbcdcgadget.cpp line 98: is there any reason why you set the bInterval of the notif interrupt descriptor to 11 instead of 255 like in my original code ? This seems to crash circle (at least when plugged into my linux) with: 00:00:10.25 dwusbgadgetendpoint.cpp(270): assertion failed: nXferSize <= m_nTransferLength Thank you !

rsta2 commented 6 months ago

Hi Sebastien, yes, there was a reason for this modification. With the original value 255 I get the following message from Linux 6.7.7 (2nd line):

[ 1368.378210] usb 1-4: new high-speed USB device number 9 using xhci_hcd
[ 1368.520037] usb 1-4: config 1 interface 0 altsetting 0 endpoint 0x81 has an invalid bInterval 255, changing to 11
[ 1368.521564] usb 1-4: New USB device found, idVendor=cafe, idProduct=8002, bcdDevice= 1.00
[ 1368.521579] usb 1-4: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[ 1368.521584] usb 1-4: Product: CDC Gadget
[ 1368.521588] usb 1-4: Manufacturer: Circle
[ 1368.688660] cdc_acm 1-4:1.0: ttyACM0: USB ACM device
[ 1368.688764] usbcore: registered new interface driver cdc_acm
[ 1368.688778] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters

Looking into the USB 2.0 spec. this value can have a maximum of 16 for high-speed interrupt endpoints. I changed the value to 11, because my Linux does it anyway, and I had no problems with that.

So what to do? Unfortunately I have currently no idea, why this assertion fails, and I cannot reproduce this here. Perhaps we should reset the value to 255, because it works for both of us. Remember that interrupt endpoints are currently not really supported by Circle. This can be a problem.

sebastienNEC commented 6 months ago

Ooops Rene, I must apologize: I have retested this on my end and 11 works fine actually. I am also getting the same linux warning as you, so 11 is probably a good value indeed. Sorry !

rsta2 commented 6 months ago

No problem, it's good, that it is working now. Thanks for re-testing.