< Back | Github repository | Project page on Hackaday.io

Can this thing work as an XInput gamepad?

I've been tinkering around with the idea of emulating an Xbox controller instead of a regular "classic" HID gamepad using the V-USB library. My conclusions? I made something but not everything (also notice the V-USB library can work only as low-speed devices, and the original controller is a full-speed one!).

Here are my notes about what I tried and what happened (tl;dr: I have a pseudofunctional low-speed xbox controller with limited button input and so many questions about in which point exactly I can't make it work as desired and why :D).

Full disclosure: I'm not an expert and most of the things I've used here were also learned for this specific project, so keep that in mind!

Files and references

There are some modifications I made to the V-USB library for this mini-project. Since these are minor changes for testing and figuring things out (and some times they doesn't even work!) the code is not uploaded anywhere, but all the details are in the sections below.

There's also an utils folder in the original nesmini_usb_adapter repo with some aditional info used here, like:

Setting up the library

The V-USB library can work with custom full USB descriptors (instead of using the regular classic HID ones that we can see in most of the keyboard/mouse/gamepad projects). You just need to set the proper length and provide the vaild descriptors (on usbconfig.h):

#define USB_CFG_DESCR_PROPS_DEVICE                  USB_PROP_LENGTH(18) // descriptor device for xbox controller
#define USB_CFG_DESCR_PROPS_CONFIGURATION           USB_PROP_LENGTH(153) // descriptor config for xbox controller
		

And, according to the docs, the default way to set the static descriptor info (that means our device will always read this at the beginning and we won't change that behaviour "dinamically" in our code - like a button with a "turn this gamepad into an xbox controller" label or something like that) is by providing a couple of PROGMEN names:

PROGMEM const char usbDescriptorDevice[] = {
	0x12,
	...
}
PROGMEM const char usbDescriptorConfiguration[] = {
	0x09,
	...
}
		

With this setup we can just "declare" a valid USB device with all the Xbox Controller settings. How does it work? What's the information encoded there? This post about the USB data of a controller helped me a lot. It talks about the different parts of the descriptors (from the joysticks to the external microphone attachment) and also about how the button status is encoded in a 20 bytes package that is sent using an interrupt endpoint.

Problem: The max data payload size for low-speed devices is 8 bytes (and the max size for a full-speed one is 64 bytes, so a "real device" will obviously skip this issue) and the V-USB library implements this "limitation" (actually it's not a "limitation" but what the regular implementation says). What can we do about it?

Low-speed devices with >8 bytes on interrupt endpoints. Is that even possible?

Short answer: yes, in theory. I've tried it! (but without success :_ D)

First of all, the "limitation" of 8 bytes on low-speed devices is basically "a rule" (or "a standard", or whatever you wanna call it). You can send more info if you want (you'll need to format the packages in a proper way, ensure the timmings are correct, etc) but the important question will be the following:

Will the destination machine work fine with a non-standard implementation like this one? ("hey, I'm expecting an 8 bytes package but you just send 15, what's going on? I don't know what to do!"). I've read about OS's ignoring most of this size-errors, but since we're running away from the basic conventions there's no guarantee that this will work on the future.

If I connect my attiny85 acting as an xbox controller I got the following from a dmesg:

[ 9428.802551] usb 1-1.1.3: new low-speed USB device number 10 using xhci_hcd
[ 9429.960560] usb 1-1.1.3: config 1 interface 0 altsetting 0 endpoint 0x81 has invalid maxpacket 32, setting to 8
[ 9429.960566] usb 1-1.1.3: config 1 interface 0 altsetting 0 endpoint 0x1 has invalid maxpacket 32, setting to 8
[ 9429.960572] usb 1-1.1.3: config 1 interface 1 altsetting 0 endpoint 0x82 has invalid maxpacket 32, setting to 8
[ 9429.960575] usb 1-1.1.3: config 1 interface 1 altsetting 0 endpoint 0x2 has invalid maxpacket 32, setting to 8
[ 9429.960579] usb 1-1.1.3: config 1 interface 1 altsetting 0 endpoint 0x83 has invalid maxpacket 32, setting to 8
[ 9429.960582] usb 1-1.1.3: config 1 interface 1 altsetting 0 endpoint 0x3 has invalid maxpacket 32, setting to 8
[ 9429.960586] usb 1-1.1.3: config 1 interface 2 altsetting 0 endpoint 0x84 has invalid maxpacket 32, setting to 8
[ 9430.738129] usb 1-1.1.3: New USB device found, idVendor=045e, idProduct=028e, bcdDevice= 1.14
[ 9430.738135] usb 1-1.1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 9430.738139] usb 1-1.1.3: Product: attiny85 raw xbox controller
[ 9430.738142] usb 1-1.1.3: Manufacturer: albertgonzalez.coffee
[ 9431.015711] input: Microsoft X-Box 360 pad as /devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1.1/1-1.1.3/1-1.1.3:1.0/input/input32
		

The device works fine, and the computer recognize the descriptors, names, etc. BUT notice the conflict: the original Xbox configuration is trying to set a max-package size (32 bytes) that exceeds what we should expect for a low-speed device (8 bytes). The problem doesn't seem to go further, but the info message is there since the drivers doesn't expect that.

Anyway, just for the sake of curiosity, and aiming from the best, let's try to have a valid-and-working xbox controller low-speed device. How can we send that 20 bytes report?

First approach: split the data in three packages: 8 + 8 + 4 bytes

The way V-USB handles the interrupts (two different endpoints can be configured and the data can be prepared by reaching usbSetInterrupt() or usbSetInterrupt3()) allows only packages with a max-size of 8 bytes (actually 11); not ONLY for the standard (which makes totally sense because, well, you know, standards) but because the way it uses it's internal data structures makes impossible (without some fine tunning we'll discuss later) to encode a length bigger than 11 bytes.

My first approach was to split the 20 bytes report into three 8/4 bytes blocks. Something like this:

while(1) {
	// do stuff and update, if required, reportBuffer with the new 20 bytes info

	usbPoll();

	if (usbInterruptIsReady() && updateReportBuffer) { // updateReportBuffer is true only if there's a change on the buttons 

		usbSetInterrupt((void *)&reportBuffer + reportBufferOffset, (reportBufferOffset > 8 ? 4 : 8));
		reportBufferOffset += 8;
		if (reportBufferOffset > 16) {
			reportBufferOffset = 0;
			updateReportBuffer = 0;
		}

	}
}
		

Once I detect a change on the report (to avoid sending the same data over and over) the first 8 bytes of the reportBuffer are sent. After that, the offset is increased and we proceed with the next block. This is done three times with two blocks of 8 bytes and one last 4 bytes package.

This seems to work fine on Linux (with the xboxdrv drivers for userspace) and it can send info of any button, trigger or joystick on the pad (the full 20 bytes report). The i2c (S)NES Mini controller can be added too and everything seems to work pretty fine and quick, even for a low-speed device acting as an Xinput! (I used the jtest-gdk utility to view all the buttons and axis).

But it doesn't work on Windows :(

On my Windows 7 computer the device is labeled as a valid Xbox controller too, but the pressed keys seems to be detected only once in a while and for small periods of time. I'm assuming the driver expects a full 20 bytes package, not three different blocks (they're not "combined" in the end in a single one like the driver on Linux is probably doing, so this explain the weird button behaviour) so this strategy won't work...

Second approach: Sending more than 8 bytes on an interrupt on V-USB, some theory

My second attempt was to modify the V-USB library to send more than 8 bytes in an interrupt package despite the fact that this is something non-standard.

I have almost zero "real" experience in C (maybe a couple of mini-projects here and there...), my ASM is kinda rusty (I did something in college back in the days. I think... and I wrote a Game Boy game later on; and that's it!) and my USB protocol knowledge is a big "Quick, go and fetch some info from USB in a nutshell!" sentence on my notebook so, what could possibly go wrong?

There's a USB_BUFSIZE definition in usbdrv.h that sets the max buffer size for the interrupt data package. This package is made of:

PID (1 byte) + DATA (up to 8 bytes on low-speed) + CRC16 (2 bytes)
		

That makes a "regular" USB_BUFSIZE size of 11 bytes (more info about the package structure on the USB 2.0 specs docs). For the record, the SYNC byte is added later, so it doesn't appear here until sending the info (but the length variable is increased by 1 before the sending process!).

In theory, by modifiying this buffer size the whole thing should work (again, remember, by ignoring the standard) but there's something else!

Acording to this post from the old V-USB forums the internal field used by V-USB to store the package size (a len value that should be between 1 and 8) is also used as a signal for some USBPID_NAK stuff (again, go and check the Protocol Layer from the USB specs to learn more about PIDs).

Basically the len field can have the USBPID_NAK value when the package is not a data package but a handshake one. To make this distinction, the usbGenericSetInterrupt() function (among other parts of the code) checks for the bit 4 on the len field. Since the max value for len will be (in theory) 8, that means a max value of b0000 1000 (0x08). If the next bit is set (b0001 0000 - 0x10) then we know for sure we're dealing with something that it's not a data package.

(that's also the reason a max size of 11 data bytes is also valid: 11 data bytes means 1 PID + 11 DATA + 2 CRC16 AND + 1 SYNC when sending = 15 bytes, so size will be b0000 1111, BUT when adding another one, changing the size to b0001 0000 will use that bit 4 for the size, breaking the whole thing).

But what happens when we increase our max size up to 20? (b0001 0100 - 0x14). Yup, that's it: that 0x10 bit no longer works as expected, since the size itself uses it, not only the USBPID_NAK!.

According to the solution by the user in the forum, this can be changed by replacing that bit by a higher one. Since the USBPID_NAK value uses both bit 4 and 6, and since our max size will only use up to bit 4, replacing all the "bit checks" for Interrupt3 should work.

This part was some kind of a DenverCoder9 problem for me, since the few lines of the post (without valid links to the source code, sadly) were the only reference about the issue. At first I didn't now anything about the protocol packages ("why there's 8 bytes max but the BUFSIZE is 11?", "why NAK == 0x5A?", etc) but after reading some docs and some trial-and-error I think I finally got it:

Modifying the V-USB and sending packages > 8 bytes

Following the example of the post, I changed the Interrupt3 methods so they use bit 6 instead of bit 4 for the NAK check. This means:

After reading the code, comparing my notes with THE POST (the only practical reference to this code I had) and some trial and error, I think these are, at least, the first steps to make it work. Failing at replacing some of those parts will usually cause a deadlock or malfunctioning on the device (interrupts are never "ready" and things like that).

Debug, how?

So, does it works?

Noep :_ D

(or, at least, not completely)

I've been trying different things (like using a completely different bit for that NAK check) but none of them seems to work. I manage different hypothesis:

BUT sending a SINGLE 4 bytes package (that contains the single buttons information) seems to work fine on both Linux and Windows (not the ideal result, I know).

And now, what?

Honestly, I don't know :_ D

I'm thinking on trying all this stuff in another avr microcontroller to see how it works, and maybe I'll come back to the code in the future and give it a closer look with a different perspective. Meanwhile I'm going to leave my notes here for future references and I'll update them with any progress I made.

Links and references


< Back | Last update: 2021/05/24