The Guncon 3 is a light gun for the Play Station 3 that was bundled with Time Crisis 4. It is a USB device with two joysticks, 9 buttons and an IR LED based pointer (similar to the Wiimote). It is only compatible with the PS3 games Time Crisis 4 (TC4), Time Crisis 4: Razing Storm and Deadstorm Pirates. While it is only supported by a small number of games I found it to be a very good and accurate light gun, better than my AimTrak and with more buttons. The only problem is the lack of support for any other device - there are no drivers for Windows, Linux, etc. I hoped to fix this problem by creating a Linux kernel module to support the Guncon 3.

Table of Contents

The Guncon 3 USB device

Guncon-3
Guncon 3 taken by Chinpokomon5 [CC BY-SA 3.0 or GFDL], via Wikimedia Commons

When plugged in to a computer the Guncon 3 appears as a USB Hub with an attached device with the following device descriptor and configuration descriptor.

Device Descriptor
bLength 18
bDescriptorType DEVICE (0x01)
bcdUSB 1.10 (0x0110)
bDeviceClass Defined in Interface (0x00)
bDeviceSubClass Defined in Interface (0x00)
bDeviceProtocol Defined in Interface (0x00)
bMaxPacketSize0 8
idVendor 0x0b9a
idProduct 0x0800
bcdDevice 80.00 (0x8000)
iManufacturer None (0)
iProduct None (0)
iSerialNumber None (0)
bNumConfigurations 1
Configuration Descriptor
Length 9
bDescriptorType CONFIGURATION (0x02)
wTotalLength 32
bNumInterfaces 1
bConfigurationValue 1
iConfiguration None (0)
bmAttributes.Reserved 0
bmAttributes.RemoteWakeup RemoteWakeup Not Supported (0b0)
bmAttributes.SelfPowered Bus Powered (0b0)
bMaxPower 100mA (0x32)
Interface Descriptor
bLength 9
bDescriptorType INTERFACE (0x04)
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass Vendor Specific (0xff)
bInterfaceSubClass Unknown (0x00)
bInterfaceProtocol Unknown (0x00)
iInterface None (0)
Endpoint Descriptor
bLength 7
bDescriptorType ENDPOINT (0x05)
bEndpointAddress 2 OUT (0b00000010)
bmAttributes.TransferType Interrupt (0b11)
wMaxPacketSize.PacketSize 8
wMaxPacketSize.Transactions One transaction per microframe if HS (0b00)
Interval 16
Endpoint Descriptor
bLength 7
bDescriptorType ENDPOINT (0x05)
bEndpointAddress 2 IN (0b10000010)
bmAttributes.TransferType Interrupt (0b11)
wMaxPacketSize.PacketSize 15
wMaxPacketSize.Transactions One transaction per microframe if HS (0b00)
Interval 4

Reversing

I was hoping that the USB config and endpoint descriptors would provide useful information, however there isn’t much apart from a few clues. We can see that there are 2 Interrupt endpoints on the USB device, one input and one output. If you are not familiar with the specifics of the USB protocol then all you really need to know is that USB devices can have endpoints of different types. The endpoints are unidirectional and are used to send data to and from the device. The interrupt endpoints must be polled by the driver and always send/receive a fixed amount of data. So this device has input (relative to the host) endpoint that will send a 15 byte packet, and an output endpoint that will receive 8 an 8 byte packet.

Connecting the device to my PC and polling the input endpoint did not return any data, so at that point I tried to send some random data to the output endpoint (I used a Python script with pyusb to send and receive data). After sending 8 bytes to the Guncon data started appearing on the input endpoint (yay!). It seems that the Guncon requires a setup packet to initialise, however the data that gets sent back from Guncon appears to be scrambled in some way (boo!). The data packets being sent from the Guncon appear to repeat, for example there will be the same pair of packets sent many many times in a row:

56 C8 30 71 97 4B 3F F4 27 46 F1 40 98 EF F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
56 C7 30 71 97 4B 3F F4 27 46 F1 40 7D 29 F4
BE B4 CC 4C 53 9B D8 EE 5B 72 FC 7D 6C E7 F3
...

When you move a joystick or press a button you can see all of bytes in the output data changing and then changing back when the button is released. This suggests that the data is scrambled in some way, possibly encrypted. I determined that I could go no further with only the data from a PC, so I got a Play Station 3 and Time Crisis 4.

I used a Beagle USB 12 protocol analyser and hooked the Guncon up to the PS3 and started Time Crisis 4 (TC4), before starting TC4 the PS3 reports the Guncon as an unknown USB device - the game must provide a driver, because as soon as the game starts you can see the USB enumeration and setup packets, along with the magic initialisation packet sent to the output endpoint. In all cases I have observed the initialisation packet starts with 0x01, in this case the packet was 01 12 6F 32 24 60 17 21. I then observed the same behaviour as when it was connected to the PC, apparently scrambled data packets.

It did not seem likely that I would be able to decipher the scrambling based only on the observations of the data packets, so I took the next logic step - disassembly. You need to get a copy of the decrypted .elf file from your copy of TC4, and I cannot explain how to do this - there are a number of guides available online for decrypting EBOOT.BIN. Once I had the TC4 elf file I could load it up in IDA Pro 64Bit, I am using 6.8 (which was the latest version when I was doing this, 6.9 is out now…) An extra plug-in called PowerPC AltiVec Extension is required to decode the extra AltiVec instructions used in the CellBE processor.

I used a IDA script to analyse the TC4 elf (analyze_self.idc from kakaroto/ps3ida which will find the TOC address (which you can set in IDA under Options->General->Analyse->Processor specific analysis options) and it will find the function imports and exports (which is very useful for understanding the USB driver functions).

Note: The Cell Processor is a 64-bit RISC PowerPC with 32x64bit general purpose registers. TC4 was compiled with GCC 4.1.1, this is useful to note because most of the time when pointers are referenced there will be some pretty dubious ASM generated, addi, clrldi and stw will be used instead of stwu and relative addressing. When a pointer to a buffer is accessed, uint8_t *buffer; and you wish to write to an offset you may write something like: buffer[4] = 0x10; in C, which could be compiled to

# r4=0x10, r5=buffer
stw    r4, 4(r5)      # *(buffer+4) = 0x10; 

However GCC will assume that the address could overflow and will truncate it with clrldi.

#r4=0x10, r5=buffer
addi    r6, r5, 4   # temp = buffer + 4;
clrldi  r6, r6, 32  # ensure the address is with-in 32 bits
stw     r4, 0(r6)   # *(buffer+4) = 0x10;

This pattern appears quite a bit in the code we will look at, and it makes everything look a bit messy.

The way the USB driver seems to work with the PS3 is that a few callbacks are define for probing, attaching and detaching devices - probe is called when a new device is plugged in and tests to see if the new device is supported, attach and detach are called to setup or teardown a supported device. Most of the other USB functions take a callback argument and pass around a pointer to a struct that describe the device.

To find the various functions in the TC4 I code guessed that there might be some debugging messages still in the code (they were left there to help us…), searching in the Strings Window (View->Open Subviews->Strings) for "guncon" reveals a fair number of references - had there not been any strings there is still the Vendor and Product ID (0x0b9a and 0x0800), which appear in the probe function. The interesting strings are Guncon 3: probe ... and Guncon 3: attach_done..., the attach_done string appears in a subroutine that I called guncon_attached and starts at $0787A08.

The guncon_attached function takes 3 arguments, int32_t x, int32_t y, GUNCON_UNIT_t* unit - I do not understand exactly what the first 2 arguments are but the last argument is a pointer to a struct that describes the connection to the Guncon 3 which I called GUNCON_UNIT_t.

This is the current state of the reversed GUNCON_UNIT_t, not all of the fields are complete but it was sufficient to work out the communication functions.

GUNCON_UNIT_t
00000000field_0: .byte ?
00000001field_1: .byte ?
00000002field_2: .byte ?
00000002
00000003state?: .byte ?
00000003
00000004dev_id: .long ?
00000004
00000008cpipe: .long ?
00000008
0000000Cipipe: .long ?
0000000C
00000010opipe: .long ?
00000010
00000014field_14: .long ?
00000018send_buffer: .quad ?
00000018
00000020send_buffer_related: .quad ?
00000020
00000028buffer: .long ?
00000028
0000002Cbuffer_offset: .long ?
00000030 .byte ?
00000031 .byte ?
00000032 .byte ?
00000033 .byte ?
00000034 .byte ?
00000035 .byte ?
00000036field_36: .byte ?
00000037GUNCON_UNIT_t ends

The guncon_attached function is passed a GUNCON_UNIT_t struct that has been partially initialised with the dev_id and pipes set up, the function clears the buffer and sets the send_buffer/_related with a key of some sorts, I don’t fully understand the method used to generate the key but the buffer send_buffer_related appears to be derived from send_buffer (hence the name). At the end of the function the 8 byte send_buffer_related is sent to the Guncon using the InterruptTransfer function.

The InterruptTransfer function sets up a callback to a subroutine at $07880E4 that I called guncon_recv. The callback has the same arguments as all the USB callbacks (int32_t x, int32_t y, GUNCON_UNIT_t*), and it simply sets up the output polling. Another InterruptTransfer call is made with a pointer to the receive buffer and a callback to a subroutine at $0787CC4 which I called guncon_DecodeGunData - this is where the really interesting bits happen.

The receive buffer is a ring buffer that appears to defined as uint8_t buffer[32][16]. The buffer_offset points to the current offset in the buffer for the data to be written, the Guncon only sends 15 bytes but the buffer is 16 byte aligned for speed reasons - each of the buffer elements can be stored in two registers which speeds up processing.

The guncon_DecodeGunData function has 4 parts, error testing/housekeeping, check summing, decrypting and setting up the next interrupt poll. The housekeeping and set up for the next call are not very interesting and I won’t cover them. However, the checksum and decrypt are quite interesting. The checksum code starts at $787D40 and I have written a rough C port of it.

int checksum(uint8_t *data, uint8_t expected) 
{
    uint8_t checksum, temp_sum;
    temp_sum = ((data[14] ^ data[13]) + data[12] + data[11] - data[10] - data[9]) ^ data[8];
    checksum = (((data[7] ^ temp_sum) - data[6] - data[5]) ^ data[4]) + data[3] + data[2] - data[1];
    if (checksum != expected) {
        GUNCON_DEBUG("checksum mismatch: %02x != %02x\n", checksum, expected);
        return -1;
    }
    return 0;
}

This is the commented version of the IDA disassembly:


0000000000787D40 checksum:                               # CODE XREF: guncon_DecodeGunData+2Cj
0000000000787D40 n = r26                                 # this block appears to calculate a checksum of the values returned
0000000000787D40                 addi      r27, r5, GUNCON_UNIT_t.buffer
0000000000787D44                 li        n, 0          # r26 = 0;
0000000000787D48                 clrldi    r28, r27, 32
0000000000787D4C                 addi      r5, r5, GUNCON_UNIT_t.send_buffer
0000000000787D50                 addi      r3, r28, GUNCON_BUFFER.buffer_data
0000000000787D54                 clrldi    r27, r3, 32   # buffer_base
0000000000787D58                 lwz       r7, GUNCON_BUFFER.buffer_offset(r28) # buffer_offset
0000000000787D5C                 clrlslwi  r30, r7, 27,4 # r30 = (r7 * 16) % (31 << 4);
0000000000787D60                 add       r6, r30, r27
0000000000787D64                 clrldi    r30, r6, 32   # data = buffer[buffer_offset]
0000000000787D68                 ld        r3, guncon_recv_buffer.field_b(r30) # data[1]
0000000000787D6C                 ld        r6, guncon_recv_buffer(r30) # data[0]
0000000000787D70                 srdi      r31, r3, 8    # b >> 8
0000000000787D74                 srdi      r10, r3, 16   # b >> 16
0000000000787D78                 srdi      r9, r3, 24    # b >> 24
0000000000787D7C                 xor       r12, r31, r10 # (b >> 8) ^ (b >> 16)
0000000000787D80                 srdi      r11, r3, 32   # b >> 32
0000000000787D84                 add       r4, r12, r9   # ((b >> 8) ^ (b >> 16)) + (b >> 24)
0000000000787D88                 srdi      r8, r3, 40    # b >> 40
0000000000787D8C                 add       r0, r4, r11   # ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32)
0000000000787D90                 srdi      r7, r3, 48    # b >> 48
0000000000787D94                 subf      r31, r8, r0   # ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40)
0000000000787D98                 srdi      r9, r3, 56    # b >> 56
0000000000787D9C                 subf      r12, r7, r31  # ((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40) - (b >> 48)
0000000000787DA0                 srdi      r11, r6, 8    # a >> 8
0000000000787DA4                 xor       r10, r12, r9  # (((b >> 8) ^ (b >> 16)) + (b >> 24) + (b >> 32) - (b >> 40) - (b >> 48)) ^ (b >> 56)
0000000000787DA8                 srdi      r8, r6, 16    # a >> 16
0000000000787DAC                 xor       r4, r6, r10   # a ^ b_sum
0000000000787DB0                 srdi      r7, r6, 24    # a >> 24
0000000000787DB4                 subf      r3, r11, r4   # (a ^ b_sum) - (a >> 8)
0000000000787DB8                 srdi      r31, r6, 32   # a >> 32
0000000000787DBC                 subf      r0, r8, r3    # (a ^ b_sum) - (a >> 8) - (a >> 16)
0000000000787DC0                 srdi      r9, r6, 40    # a >> 40
0000000000787DC4                 xor       r12, r0, r7   # ((a ^ b_sum) - (a >> 8) - (a >> 16)) ^ (a >> 24)
0000000000787DC8                 srdi      r11, r6, 48   # a >> 48
0000000000787DCC                 add       r10, r12, r31 # a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32)
0000000000787DD0                 add       r4, r10, r9   # a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32) + (a >> 40)
0000000000787DD4                 subf      r3, r11, r4   # a ^ b_sum - (a >> 8) - (a >> 16) ^ (a >> 24) + (a >> 32) + (a >> 40) - (a >> 48)
0000000000787DD8                 clrldi    r10, r3, 56   # r3 & 0xff
0000000000787DDC
0000000000787DDC decode:                                 # CODE XREF: guncon_DecodeGunData+228j
0000000000787DDC                 slwi      r4, n, 3      # r4 = r26 * 8;
0000000000787DDC                                         # r26 is incremented later, the first time it's 0
0000000000787DDC                                         # this loop wil run twice, probably decoding each quad of the data from the gun
0000000000787DE0                 clrldi    r31, r5, 32   # r31 = send_buffer
0000000000787DE4                 add       r8, r4, r31   # send_buffer[n]
0000000000787DE8                 clrldi    r11, r8, 32
0000000000787DEC                 lbz       r0, 7(r11)    # r11 = send_buffer[n][7]
0000000000787DF0                 cmpw      cr1, r0, r10  # compare checksum
0000000000787DF4                 bne       cr1, invalid_data # checksum mismatch

You can see that the see the address masking I mentioned before a few times in this snippet. The checksum is calculated and compared the last byte in send_buffer (aka key), if the checksum does not match then there is some retry logic and if that fails a new send_buffer/key is generated and sent to the Guncon. I don’t know if this a standard checksum algorithm or if it is something Namco came up with? The next part of the function deals with the decryption of the data from the Guncon.

There is a decryption table at $1009AA90 that is used to decrypt the data sent from the Guncon - this is probably the main reason no other games or drivers can use this lightgun (they don’t have the decryption algorithm), which is a really big shame because it’s an excellent light gun! Again I will show my recoded C version and then the relevant disassembly.

The key is used in combination with the last byte of the data to compute an offset in the KEY_TABLE that is used as the starting point to decrypt the rest of the data. The 15th byte in the data is padding used so that the checksum will work out, the 1st of the buffer is not part of the data so this leaves 13 bytes for joystick state data. Only bytes 13 to 1 can be decoded, and only those are decoded by the function. The operation performed on each byte is determined by the byte in KEY_TABLE, it is either added, subtracted, or XORed with a key byte and the byte from KEY_TABLE.

int Guncon 3_decode(uint8_t *data, const unsigned char *key) {
    int32_t x, y, key_index;
    uint8_t bkey, keyr, key_offset, byte;
    
    key_offset = (((((key[1] ^ key[2]) - key[3] - key[4]) ^ key[5]) + key[6] - key[7]) ^ data[15]) + (unsigned char)0x26;

    key_index = 4;

    // byte E is part of the key offset
    // byte D is ignored, probably a padding byte - to make the checksum workout
    for (x = 13; x >= 1; x--) {
        byte = data[x];
        for (y = 0; y < 3; ++y) { 
            key_offset--;

            bkey = KEY_TABLE[key_offset + 0x41];
            keyr = key[key_index];
            if (--key_index == 0)
                key_index = 7;

            // bkey & 3 selects the mode operation for this byte
            if ((bkey & 3) == 0)
                byte =(byte - bkey) - keyr;
            else if ((bkey & 3) == 1)
                byte = ((byte + bkey) + keyr);
            else
                byte = ((byte ^ bkey) ^ keyr);
        }
        data[x] = byte;
    }
    return 0;
}

And now for the disassembly…


0000000000787DDC decode:                                 # CODE XREF: guncon_DecodeGunData+228j
0000000000787DDC                 slwi      r4, n, 3      # r4 = r26 * 8;
0000000000787DDC                                         # r26 is incremented later, the first time it's 0
0000000000787DDC                                         # this loop wil run twice, probably decoding each quad of the data from the gun
0000000000787DE0                 clrldi    r31, r5, 32   # r31 = send_buffer
0000000000787DE4                 add       r8, r4, r31   # send_buffer[n]
0000000000787DE8                 clrldi    r11, r8, 32
0000000000787DEC                 lbz       r0, 7(r11)    # r11 = send_buffer[n][7]
0000000000787DF0                 cmpw      cr1, r0, r10  # compare checksum
0000000000787DF4                 bne       cr1, invalid_data # checksum mismatch
0000000000787DF8                 ld        r6, 0(r11)    # send_buffer as quad
0000000000787DFC                 lbz       r7, 15(r30)   # last byte of buffer
0000000000787E00                 srdi      r0, r6, 48
0000000000787E04                 srdi      r12, r6, 40
0000000000787E08                 srdi      r10, r6, 32
0000000000787E0C                 xor       r9, r0, r12
0000000000787E10                 srdi      r31, r6, 24
0000000000787E14                 subf      r11, r10, r9
0000000000787E18                 srdi      r5, r6, 16
0000000000787E1C                 subf      r4, r31, r11
0000000000787E20                 lwz       r31, guncon_table_p # guncon_table+0x41
0000000000787E24                 .drop r31 # 0x1009AAD11009A910
0000000000787E24                 srdi      r8, r6, 8
0000000000787E28                 xor       r3, r4, r5
0000000000787E2C                 add       r12, r3, r8
0000000000787E30                 li        r3, 5
0000000000787E34                 subf      r0, r6, r12
0000000000787E38                 li        r12, 13       # loop_counter = 13 // there are 15 bytes in the buffer, the last byte is a checksum and
0000000000787E38                                         #                   // the first byte is a counter and the 14th byte is ignored...
0000000000787E3C                 xor       r10, r0, r7
0000000000787E40                 rotrdi    r7, r6, 16    # rotate right 16
0000000000787E44                 addi      r9, r10, 38   # key_hash + 0x26 (0x26 is the minimum offset if key_hash is 0)
0000000000787E48                 clrldi    r11, r9, 56   #            & 0xff // this has a wrap around
0000000000787E4C                 add       r5, r11, r31  # key_table_[key_hash]
0000000000787E50                 clrldi    r6, r5, 32    # restrict to 32bit address space
0000000000787E54                 b         decode_start  # buffer[r12]
0000000000787E58 # ---------------------------------------------------------------------------
0000000000787E58
0000000000787E58 decode_write:                           # CODE XREF: guncon_DecodeGunData+1CCj
0000000000787E58 byte = r10                              # r12--
0000000000787E58                 addi      r12, r12, -1
0000000000787E5C                 clrldi    r5, r31, 32
0000000000787E60                 cmpdi     cr1, r12, 0   # if r12 == 0
0000000000787E64                 stb       byte, 0(r5)   # buffer[r12] = decoded
0000000000787E68                 beq       cr1, decode_end
0000000000787E6C
0000000000787E6C decode_start:                           # CODE XREF: guncon_DecodeGunData+190j
0000000000787E6C                 add       r31, r12, r30 # buffer[r12]
0000000000787E70                 .drop r31
0000000000787E70                 li        r5, 4
0000000000787E74                 clrldi    r8, r31, 32
0000000000787E78                 lbz       byte, 0(r8)   # buffer[i]
0000000000787E7C
0000000000787E7C inner_loop:                             # CODE XREF: guncon_DecodeGunData+21Cj
0000000000787E7C                                         # guncon_DecodeGunData+29Cj ...
0000000000787E7C                 addi      r5, r5, -1    # r5-- // first loop r5 = 3
0000000000787E80                 addi      r0, r6, -1    # key_ptr-- // KEY_TABLE offset is decremented first, before the loop test it made
0000000000787E80                                         # for (int y = 4; y > 0; --y, --key_ptr)
0000000000787E84                 cmpdi     cr7, r5, 0
0000000000787E88                 li        r4, 7
0000000000787E8C                 li        r11, 48
0000000000787E90                 beq       cr7, decode_write # if r5 == 0
0000000000787E94                 clrldi    r6, r0, 32
0000000000787E98                 lbz       r9, 0(r6)     # *key_ptr; // KEY_TABLE[key_offset]
0000000000787E9C                 addi      r0, r3, -1
0000000000787EA0                 cmpdi     cr6, r0, 0
0000000000787EA4                 mtspr   CTR, r0
0000000000787EA8                 beq       cr6, change_rbits_skip # if r3 == 0
0000000000787EAC                 li        r4, 0
0000000000787EB0                 li        r11, 56
0000000000787EB4
0000000000787EB4 change_rbits_skip:                      # CODE XREF: guncon_DecodeGunData+1E4j
0000000000787EB4 key = r7                                # clear top 62 bits...
0000000000787EB4                 clrldi    r3, r9, 62
0000000000787EB8                 rldcl     key, key, r11,0 # rotate right (16 or 8 bits)
0000000000787EBC                 cmpdi     r3, 0
0000000000787EC0                 mfspr   r11, CTR
0000000000787EC4                 cmpdi     cr6, r3, 1
0000000000787EC8 bkey = r9
0000000000787EC8                 subf      r8, bkey, byte # r8 = byte - bkey
0000000000787ECC                 add       r3, r4, r11
0000000000787ED0                 beq       key_0         # byte = r8 - key
0000000000787ED4                 xor       r0, byte, bkey # r0 = byte ^ bkey
0000000000787ED8                 beq       cr6, key_1    # byte = byte + bkey + key
0000000000787EDC                 xor       byte, r0, key # byte = (byte ^ bkey) ^ key
0000000000787EE0 byte = r10
0000000000787EE0                 b         inner_loop    # r5-- // first loop r5 = 3
0000000000787EE4 # ---------------------------------------------------------------------------
0000000000787EE4
0000000000787EE4 invalid_data:                           # CODE XREF: guncon_DecodeGunData+130j
0000000000787EE4                 addi      n, n, 1       # Resend the init packet when 'n' reaches 2
0000000000787EE8                 cmpdi     cr7, n, 2
0000000000787EEC                 bne       cr7, decode   # r4 = r26 * 8;
0000000000787EEC                                         # r26 is incremented later, the first time it's 0
0000000000787EEC                                         # this loop wil run twice, probably decoding each quad of the data from the gun

#    TRUNCATED FOR BREVITY
#    This section retries the interrupt request
# ---------------------------------------------------------------------------
0000000000787F5C
0000000000787F5C key_0:                                  # CODE XREF: guncon_DecodeGunData+20Cj
0000000000787F5C key = r7                                # byte = r8 - key
0000000000787F5C byte = r10
0000000000787F5C bkey = r9
0000000000787F5C                 subf      byte, key, r8
0000000000787F60                 b         inner_loop    # r5-- // first loop r5 = 3
0000000000787F64 # ---------------------------------------------------------------------------
0000000000787F64
0000000000787F64 key_1:                                  # CODE XREF: guncon_DecodeGunData+214j
0000000000787F64                 add       r4, byte, bkey # byte = byte + bkey + key
0000000000787F68                 add       byte, r4, key
0000000000787F6C                 b         inner_loop    # r5-- // first loop r5 = 3
0000000000787F70 # ---------------------------------------------------------------------------
0000000000787F70
0000000000787F70 decode_end:                             # CODE XREF: guncon_DecodeGunData+1A4j
0000000000787F70                 lwz       r11, GUNCON_BUFFER.buffer_offset(r28)
0000000000787F74                 cmpdi     cr6, r26, 0   # r26 is the outer loop counter
0000000000787F78                 addi      r12, r11, 1
0000000000787F7C                 clrlslwi  r0, r11, 27,4
0000000000787F80                 clrlslwi  r10, r12, 27,4
0000000000787F84                 add       r8, r0, r27
0000000000787F88                 add       r9, r10, r27
0000000000787F8C                 clrldi    r31, r8, 32   # buffer[i % 31]
0000000000787F90                 lbz       r7, 0(r31)
0000000000787F94                 clrldi    r26, r12, 32
0000000000787F98                 clrldi    r30, r9, 32   # buffer[i+1 % 31]
0000000000787F9C                 addi      r6, r7, 1
0000000000787FA0                 stb       r6, 0(r30)
0000000000787FA4                 stw       r26, GUNCON_BUFFER.buffer_offset(r28) # buffer_offset++;
0000000000787FA8                 bne       cr6, new_send_buffer # if the first decode failed, then resend the sendbuffer
# Code after here sends the request for next interrupt request

Deciphering the code for the PS3 USB driver took me a little while, but most of the time was spent looking up PowerPC instructions like clrlslwi. This was the first time that I have looked at PowerPC ASM, but it isn’t too dissimilar to some other things I have worked on so picking it up wasn’t too tricky. There may be some mistakes in the comments for ASM as most of the comments are still from the first pass I did in IDA. However, the C versions of the functions have been tested and do work.

Driver for Guncon 3

Source code available: beardypig/guncon3 (provided as-is and possibly not in a full usable state)

Once the decryption algorithm was reversed I could decrypt all the information coming from the Guncon. To write the driver I had to look at the decoded data coming from the Guncon and work out what was what, which is quite easily done by pressing the buttons one at a time :) By watching the data when the buttons are pressed in turn we can easily see which bit is which for the buttons.

BTN_TRIGGER = (data[1] & 0x20));
BTN_A1 = (data[0] & 0x04));    
BTN_A2 = (data[0] & 0x02));
BTN_B1 = (data[1] & 0x04));    
BTN_B2 = (data[1] & 0x02));
BTN_C1 = (data[1] & 0x80));    
BTN_C2 = (data[0] & 0x08));
BTN_JOY_A = (data[2] & 0x80)); 
BTN_JOY_B = (data[2] & 0x40));

The Joystick axis are also easy to work out, moving the joysticks one axis at a time shows that there is only 1 byte the changes. The unsigned bytes center to 127 and go from 0 to 255, this means that each axis for the joystick is a signed char.

JOY_A_X = (int8_t)data[11]
JOY_A_Y = (int8_t)data[12];
JOY_B_X = (int8_t)data[9];
JOY_B_Y = (int8_t)data[10];

The last piece of the puzzle is the aiming, pointing the gun around at the IR LEDs sees the remaining 6 bytes changing. With some trial and error I found there are 3 shorts, two of which correspond roughly to left/right and up/down movement of the gun and the other corresponds roughly to the distance from the sensors to the guns. I say roughly because there are maximum and minimum values, and it depends on the size of your TV and how far apart the sensors are. There is a need for some calibration as the IR LEDs can be placed at different distances, the extra value sent by the Guncon is probably used to adjust the other values and as a reference for the calibration.

AIM_X = ((short)data[4] << 8) | (short)data[3];
AIM_Y, ((short)data[6] << 8) | (short)data[5];
AIM_Z = (data[8] << 8) + data[7]

jstest-gtk
Output from jstest-gtk with the Guncon 3

I really wanted to use this light gun in MAME for all the old arcade shooting games, so I wrote a kernel module so that it will work as a joystick. I haven’t really done much kernel programming before and nothing to do with the joydev module, I based my module heavily on other already existing modules including the xpad and guncon2 modules. There are known bugs and omissions, you may have noticed there was not information about how the key was generated - I am currently using a fixed key that works with the two Guncon 3 guns that I have.

There also appears to be a strange bug where 100s of button presses being generated when the joysticks are held in a specific position, I have yet to track it down. It doesn’t appear to be generated by the Guncon, as the raw data remains stable and I believe there is a bug in my kernel module code :)

There is also no way to calibrate the aiming for the gun and it is not perfect, but should work OK if you are able to position the IR LEDs as far apart as they go and use the gun at a 2-3m distance from the LEDs. I plan continue updating the kernel module for the Guncon 3 and I hope that other people will be interested enough to help out :)

This information is provided for educational and interoperability purposes.