rsta2 / circle

A C++ bare metal environment for Raspberry Pi with USB (32 and 64 bit)

Home Page:https://circle-rpi.readthedocs.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

USB CDC/ACM gadget

sebastienNEC opened this issue · comments

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 ?

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

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 !

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).

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...

Here are the files
usbcdcgadget.zip

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.

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();
}

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:

  • To be compatible with CSerialDevice, I would prefer to have a Read() method, instead of the callback mechanism.

  • Currently there is a problem with self-powered gadgets. The application does not notice, when the device is unplugged and cannot react on it. This can only be handled, when the gadget class is not directly derived from CDevice, but a separate API device class (e.g. CUSBCDCSerialDevice) is used, which has a device name (e.g. "ttyACM0") and which is deleted in CUSBCDCGadget::OnSuspend(). The application can register a device removed handler for this device and can react, if an unplugging occurs.

  • Ideally this new API device should support the method SetOptions() (as CSerialDevice is doing) with the serial option SERIAL_OPTION_ONLCR (translate NL to NL+CR on output). This should be the default setting too, because it is also in CSerialDevice.

Some more notes:

  • I modified the USB device ID in the device descriptor to USB_GADGET_DEVICE_ID_BASE + 1, because USB_GADGET_DEVICE_ID_BASE is already used for the USB MIDI gadget driver. Using the same vendor/device ID combination for different gadgets might cause problems (e.g. with Windows hosts), when the host remembers a previously attached gadget.

  • Currently I have not the time to implement interrupt endpoints in the USB gadget library. We will see, if problems will occur, when the gadget driver is used with other operating systems like Windows.

  • Another task is adding the propagation of received class-specific requests on EP0 to the gadget class driver, to be able to handle it. Currently the patch is active, which simply ignores such requests. This should be also a task for later.

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

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 ?

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