Close
0%
0%

USB Crank-Stick

wheel based game controller

Similar projects worth following
I want to build a peculiar game controller, one with a crank!
I want to make a "physical" controller, something that requires a bit of strength and ample moves. This is how the idea of a joystick based on a crank was born.The goal is to read the position of a crank and send appropriate USB joystick or keyboard commands.

The Idea

Why build such a controller?

When I was younger, I was very fond of an Apple2 game called Sabotage, this is a very basic game but I still enjoy playing it from times to times.

To make this game more fun, I want to do what Nintendo did with the Wii : make an average game more interesting by using a special controller. I want to make a "physical" controller, something that requires a bit of strength and ample moves. I later learned this was more or less the definition of Extertainment or Exergaming and that Nintendo had released a bike controller in the 90's.

This controller should also work for games like Tetris and or FrozenBubble, etc.

I looked at similar projects that would involve "spinning a device" or more generally moving and I found one involving a unicycle! Too challenging for me ;-) Also, after developing my prototype, I found another project with a similar idea : Exercise machine as a usb controller 

Alternate use for people suffering from cerebral palsy

Jomie from Hackaday suggested me to enter this project for the UCPLA Hackaday challenge. That is something I had not thought about, but it is true that for some people who struggle to do precise moves, a crank may be easier to interact with than a joystick. I would be happy if my project could be used in such a way.

The goal

Make existing retro games more fun with this controller!
I want to build at least 2 of those controllers so I can play versus games with friends. This project is also an excuse to learn new things such as designing a my own PCB, 3D printing and laser cutting.
I want to provide 2 ways of building such a controller, a simple one, for people who don't have access to a laser cuter and expensive boards, and a nicer one with higher range electronic and laser cut parts.
The goal is also to document this project enough so that it can be build by someone else and used by people suffering from cerebral palsy.

The technical details

Sabotage, the game I initially built this controller for, is played with D and F keys  for left and right and any other key for fire. It means my device must be able to act as a keyboard, or more generally as a HID device, so I will be able to use it for other games.
How to act as a HID device? Some Arduino board can act as HID devices, but it is not always easy. For example if you want to use the Arduino Uno you need to reflash the Atmega8u2 to be detected as a HID device. Teensy boards are more appropriate for this task, and as a matter of fact I had a Teensy 3.1 lying around, so I used it for this project. It could also probably be done with a Teensy 2 or a STM32 blue Pill board which are much cheaper.
Then the other major part of the project is being able to read the position of a crank and feed it to the Teensy board.

  • 1 × Teensy3.1 for V1
  • 1 × PS/2 mouse for V1
  • 1 × 3.3v-5v level shifter for V1
  • 1 × crank for V1
  • 3 × arcade buttons with led

View all 6 components

  • Second prototype : JoyWheel with laser cut enclosure

    PixJuan08/22/2020 at 06:10 0 comments

    I had plans for a V2 which would be more easily reproducible, it involved:

    • USB mouse optical sensor, easier to find a than a PS2 wheel mouse
    • Teensy4, to have both a USB client port for the PC and a USB host port for the mouse
    • Laser cut, because it is easier than to depend on a specific crank.

    Before the lockdown, I went to Edulab to make a laser cut enclosure for my new prototype. They helped me design the parts with Inkscape and boxes.py and I used their awsome laser cutter.

    It worked quite well for a first time with laser cutting, even if I am not 100% happy with the result since I had to sand all the "fingers" which were joining 2 parts. Hopefully my Dremel tool made the work much less tedious. I must have done something wrong, I will try to fix this in another iteration. Also, a few holes are missing from the SVG files because they were done empirically afterward.

    I assembled the enclosure and added 4 pieces of wood in the corners to make it stronger. In the next iteration, I will probably use 2 layers of wood to have something more rigid.

    I also used hand cut balsa for a few things like the mouse sensor support because it's much easier to adjust this way than to use a laser cutter, and it is the kind of parts that completely depends on the mouse  you are using.

    I used a "Lazy Susan" type ball bearing to make the wheel turn. There may be simpler designs but I was not very confident to insert a classic ball bearing into a piece of wood.

    Another challenge was the handle on the disc, I didn't know what kind of handle to use, or if I could buy a 'ready to use' one. I glued some laser cut rings together and use some "assembling screws" I found at my local hardware store.
    It's not perfect but it's not too bad, I'm open to suggestions on how to improve it.

    Concerning the code, it looks very much like the first version, except I replaced PS2 functions with USB host code, and I also used a lib to debounce the signal read from the buttons.
    I also added a potentiometer to change the sensitivity of the wheel, as it makes it easier to experiment with to find the right value.

    This is how it looks like fully assembled
    I'm going to publish the SVG files and the code on a Github repo soon.

  • Buttons, switches and breakout boards

    PixJuan08/10/2020 at 15:55 0 comments

    I was thinking that buttons and switches would be straightforward, but I had a few problems. I don't want to recompile my firmware each time I need to change a setting, I could change the settings with commands sent through the serial port, but I would need a terminal running on the PC and won't be able to run games in full screen, so I added switches for :   

    • resolution (how much I need to turn the crank to simulate a key press)
    • key press length
    • keymap( which events are assigned to the buttons and the crank)

    I did a basic mode, where each switch was used to choose between 2 values for the settings, but that was not enough and also, it was not over-enginneered ;-)
    So instead I chose to use the 3 arcade buttons as a 3 bit value between 0 and 5. As I use buttons with leds, the led state is used to display the bit state.
    How this value is interpreted depends on the switches positions.
    When all the switches are off, the device is in normal playing state and the buttons send keyboard events. If any of the switch is on, the device is in settings mode and the buttons alter the settings

    Exemples:

    Switches : [X]Resolution [ ]Length [ ]Keymap
    Buttons  : [ ]4      [X]2   [ ]1
    ->setting = 2 for Resolution
    
    Switches : [ ]Resolution [ ]Length [X]Keymap
    Buttons  : [X]4      [ ]2   [X]1
    ->setting 5 (4+1) for Keymap 
    
    Switches : [ ]Resolution [ ]Length [ ]Keymap
    Buttons  : [ ]4      [ ]2   [X]1
    -> game mode, 1 button pressed and sending a keyboard event
    

     It's a detail but I had a bad surprise with the switches. I first tried with regular ones which where behaving correctly, but then I tried fancy ones with leds which needed pulldown resistors. There was no way to use the integrated pull up resistors of the Teensy, otherwise I couldn't light the leds.

    For the arcade buttons, I was a bit too optimistic concerning the size of the cable lugs and had to make some additional room for them. Also, as I was working on a Sunday and wanted to get things done I did with what I had at hand. I used jumper cables to connect the buttons where I should have used PCB terminal block connectors. It was a bad idea to use jumper cables for anything else than testing, they tend to unplug and it takes time to debug, I ended up gluing them.


    I had to connect a lot of things to the Teensy : 3 buttons, 3 leds, 3 switches and a PS/2 mouse. So I had to make a breakout board.
    If you look at the pictures, you'll notice that I soldered row headers upside down on the Teensy I can't remember why but I'm sure I had a good reason at that time ;-)
    It didn't matter much since I was not designing my own PCB, just using a prototype board.
    I don't know if it is worth designing my own PCB for such a simple task, but I will probably do it anyway to learn how to use Kicad.

  • Trouble with the arrows

    PixJuan08/06/2020 at 08:25 0 comments

    So, I got my CrankStick working with Sabotage on my emulator, but I want to use it with other games.
    A game controller is exepected to behave like a usb joypad(on my TODO list) or to behave like a keyboard and send arrows key codes + Alt/Ctrl.
    I'm already sending 'D' and 'F' key codes, so it should be easy to send 🡸 and 🡺, right?
    But what 8 bit values should I send to say 'left arrow' ? I just have to RTFM to know how it works
    On the Arduino Keyboard lib page, it mentions that   "Not every possible ASCII character, particularly the non-printing ones, can be sent with the Keyboard library" so I just followed a link and found this :

      KEY_UP_ARROW 0xDA1
      KEY_DOWN_ARROW 0xD9
      KEY_LEFT_ARROW 0xD8
      KEY_RIGHT_ARROW 0xD7 

    Easy, I just have to use that code,no ? Well... it doesn't seem to work.
    So what should you do when reading the documentation doesn't work? Look at the code!
    I looked at Keyboard.cpp in the Arduino IDE directory

    size_t Keyboard_::press(uint8_t k) {
        uint8_t i;
        if (k >= 136) {            // it's a non-printing key (not a modifier)
            k = k - 136;
            ...
            _keyReport.keys[i] = k;
            ...
            sendReport(&_keyReport);

    Sending 0xDA to Keyboard.press() will change it to 0xDA - 136 = 0x52 before sending it to the low level function,
    which according to USB HID standard is "up arrow". That should work, but it doesn't...

    I also wanted to send 'Enter'(0x28 in USB keycode) and noticed in was in _asciimap, so I send the corresponding value and it worked.
    I tried to modify the _asciimap variable to send arrow keycodes with unmapped characters but it still didn't work!
    Ok Fine, I just wrote some obscenity in Keyboard.cpp to see if it was compiled at all... It turned out this file wasn't compiled at all in my case. Ok, fair enough that's not something you're supposed to play with and anyway it wasn't a proper solution
    to modify this file.

    DuckDuckGo didn't provide me any obvious solution, so I used the strings command on most of the binary files in my Arduino directory, still no luck I couldn't find ReleaseAll, which was the most unique function name of the lib. I found the 'release' keyword in usb_keyboard.c but though it was for the usb host support.
    So what to do when you can't find the information neither in the documentation nor in the source code? Disassemble it ;-)
    On Linux, Arduino projects are built in the /tmp/arduinoXXX dir, They are built as elf files and then converted to bin/hex
    to be sent through the serial port. So I loaded my elf file in Ghidra and noticed that the real function called in my code was usb_keyboard_press_keycode() which is in fact present usb_keyboard.c

    // Input can be:
    //     32 - 127     ASCII direct (U+0020 to U+007F) <-- uses layout
    //    128 - 0xC1FF  Unicode direct (U+0080 to U+C1FF) <-- uses layout
    // 0xC200 - 0xDFFF  Unicode UTF8 packed (U+0080 to U+07FF) <-- uses layout
    // 0xE000 - 0xE0FF  Modifier key (bitmap, 8 keys, shift/ctrl/alt/gui)
    // 0xE200 - 0xE2FF  System key (HID usage code, within usage page 1)
    // 0xE400 - 0xE7FF  Media/Consumer key (HID usage code, within usage page 12)
    // 0xF000 - 0xFFFF  Normal key (HID usage code, within usage page 7)
    
    void usb_keyboard_press_keycode(uint16_t n)

    Great! I have a documentation explaining me clearly what to send to the function so I just send KEY_LEFT (defined as  80  | 0xF000) it should work...Sill not working!

    Looking at the compiled code I noticed that the parameter was stored in r0 and truncated to 8 bits before being used!

            000007b8 c8 b2           uxtb       r0,r1
            000007ba 23 60           str        r3,[r4,#0x0]=>encoder0Pos 
            000007bc 2a 60           str        r2,[r5,#0x0]=>keyJustPressed
            000007be 01 f0 6d ff     bl         usb_keyboard_press_keycode     

     
    So Finally I directly called usb_keyboard_press_keycode() with the same keycode and it just worked!

    After all this, I searched again on the web and found a page with some useful information but it was not obvious I had to use "the micro manager way" to accomplish what I was trying to do.

    I struggled a lot but it's ok, because it reminded me just how powerful Ghidra...

    Read more »

  • Now Working with Mame!

    PixJuan07/29/2020 at 04:50 0 comments

    To test my device, I was just using a text editor on my Linux box to see if some characters are printed when I turn the crank. As it was working well enough I started Mame to test it with a game and... Segfault :-(
      I tried it on my Debian, on a Linux Mint, with the latest version of Mame and it still segfaults!
      What's wrong? I played Sabotage many times on Mame, why is Mame segfaulting now? It segfaults on startup when I start Mame with my device plugged in, while without it is working fine. If I am plugging my joystick after I launch Mame, it doesn't always segfault... but i doesn't work very well.

    I had 2 distinct problems :

    • Using the wrong function

    My code was based on the examples included in the Arduino so I suppose it I didn't make any mistakes. The code is pretty basic I just used Keyboard.write()
    In Mame my device seems to be working just a bit, like a one in twenty pulse. So I though I could just increase the length of the key presses... and that's when I noticed I was using the wrong function! Keyboard.write() is designed to spit chars as fast as possible. This is perfect if you want to make a rubber ducky style device, not if you want to make a game controller. So the functions I am supposed to use are Keyboard.press() and Keyboard.release(). It's working much better now!

    • Bug in Mame

      Concerning Mame, I opened a ticket and waited for the developers to fix it. I suppose it is not a high priority problem and quite a difficult one to reproduce if you don't have a Teensy. I finally found the cause of the bug was that the Teensy USB "stack" declares itself as a joystick with 33 axes and 143 buttons !
    I did a quickfix by myself but it didn't get accepted, which is ok because I can not guarantee it doesn't break something else. After that, one of the dev provided a fix that works for me.

  • First Prototype

    PixJuan01/15/2020 at 04:48 0 comments

    I used wood, a crank and shelf braces. The size of the main board is 70cm x 40 cm, probably a bit too big. I used 20mm thick pine wood  and also a bit of plywood for the thinner parts.
    I won't give the complete build instructions, the pictures are self explanatory, and also this is just the prototype, the final project will be laser cut and I will publish the files here.

    The only tricky part was to get the mouse wheel to touch the crank with just the right pressure. At first the contact was orthogonal, which was a bad idea, because if the wheel is not exactly placed there is too much pressure on it and it falls. So instead,

    I just positioned the wheel like I would have for a regular gear. I still use the plastic base of the mouse, I just saw off as much as I could of it.

  • Good old PS/2 mouse

    PixJuan12/14/2019 at 02:32 0 comments

    As I don't have a good oscilloscope, debugging the misbehaviour of my quadratic encoder code is quite a pain.   So I thought about using a PS/2 mouse because it is a serial-like protocol which is easy to implement, there are examples on the Arduino playground.
     First, even if I keep a lot of stuff, I struggled a bit to find a PS/2 wheel-mouse in my attic but I finally found one!
    It was quite fast to get something working on my Arduino UNO. The basic script doesn't support the wheel, but I found another one which does.


      So I just plugged the PS/2 mouse on my Teensy 3.1 and... it failed!
      The Teensy is 5V tolerant, but my PS/2 mouse isn't, so I fixed the problem by adding a 3.3v<->5.5v level shifter between the PS/2 mouse and my Teensy.
      It is working great so far, it's not missing pulses anymore.
      Later on, I had my "facepalm moment" when I realised the Teensy3.1 support can act as a usb host  if you plug a USB OTG converter into it, so I could have used a USB mouse instead of a vintage PS/2 one! But then I realised that in this configuration I would have needed another board to act as a USB HID, so my struggle to find a PS/2 mouse was not vain.

    This PS/2 mouse solution is working really well, but I ordered a Teensy4 to be able to use a USB mouse in a future version.

  • Quadratic encoder

    PixJuan12/04/2019 at 21:03 0 comments

    How to measure the rotation of the crank? In a computer mouse, the wheel can measure rotation. I had a broken mouse with a wheel that used a quadratic encoder.

    It was a USB mouse and the Teensy3.1 doesn't have a USB host port, so I decided to only take the quadratic encoder and read it with the Teensy.
    I kept the wheel itself as it was well adapted to the task.

      The mouse wheel was working correctly when I was using it from the mouse but when I was reading it from the Teensy, it was very erratic. So I added capacitors and resistors to filter bounces and glitches:

    It is working under normal conditions but when the wheel turns too fast, it misses some pulses. I was using code from the Arduino playground to read the encoder. Then I found out that there was examples for reading quadratic on a Teensy using hardware interruptions. So I tried this code but it failed even more! It's not skipping pulses any more but it was only going forward. I suppose the problem lies in the filtering I did. I am too lazy to unsolder everything and as this part is more or less working, I'll work on other parts of the projects and come back to this later.

View all 7 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates