Skip to content

Instantly share code, notes, and snippets.

@not-fl3
Last active September 9, 2021 01:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save not-fl3/2125b08f90d6e298bc94d48bcbf19d32 to your computer and use it in GitHub Desktop.
Save not-fl3/2125b08f90d6e298bc94d48bcbf19d32 to your computer and use it in GitHub Desktop.

An introduction to device drivers in linux and gamepad implementation.

There are two gamepad drivers, joydev and evedev.

Very inaccurate definitions of what driver is and how drivers operates: In linux, driver is a program runned with lots of priviliges, that creates a file in /dev and implement handlers for read/write requests on this file. That means that you can communicate with any device with text editor!

js0Me, reading data from /dev/input/js0 while pushing random buttons on the gamepad

Joydev

Joydev is an old, legacy driver. It still works, still is present on most systems. But it lacks certain features like OS-level gamepad calibration and do not support some crazy joysticks.

hots

The good thing about joydev - it is crazy simple.

Some docs from linux kernel: https://www.kernel.org/doc/Documentation/input/joystick-api.txt

Basically there is a file, "/dev/input/js0". And to read joystick events - just open the file and read some bytes out of it, with normal reading procedure.

Bytes will represent a sturct like this:

#[derive(Debug, Clone, Copy)]
#[repr(C)]
struct JsEvent {
    time: u32,
    value: i16,
    type_: u8,
    number: u8,
}
let mut f = File::open("/dev/input/js0").unwrap();
loop {
    let mut b: [u8; std::mem::size_of::<JsEvent>()] = [0; std::mem::size_of::<JsEvent>()];
    f.read_exact(&mut b).unwrap();
    let event: JsEvent = unsafe { std::mem::transmute(b) };
    
    if (event.type_ & JsEventType::Axis as u8) != 0 {
        let value = event.value as f32 / std::u16::MAX as f32 * 2.;
        println!("Axis number {}: changed value to {}", event.number, value);
    }
    if (event.type_ & JsEventType::Button as u8) != 0 {
        println!("Digital button number {}: changed value to {}", event.number, event.value != 0);
    }
}

So this is the all the code needed to get some data out of the joystick. Nice!

Evdev

Evdev clearly is less joy to use.
Evdev creates an event file in /dev/input/event*.

The only article I found, and a really good one: https://ourmachinery.com/post/gamepad-implementation-on-linux/

Main difference and a challenge introduced by this /dev/input/event* files - normal reading procedures are not enough anymore.

In linux, there are 3 syscalls to work with files. read, write and ioctl. read/write are the "normal" one. In rust we usually call them with some nice functions std::fs. ioctl is a weird one, introduced when read/write became not sufficient for linux.

Itresting side note: there were an alternative! Plan9, more pure unix-like system, managed to avoid ioctl weirdness and used just read/write. http://catb.org/~esr/writings/taoup/html/plan9.html. Plan9 is really cool! Not only device drivers, but all the system's services operates as files with just read/write interface. You can do things like cat /dev/screen > screenshot to capture a screenshot, how cool is that?

Anyways, back to linux.

libc ioctl declaration

pub unsafe extern "C" fn ioctl(
    fd: c_int, 
    request: c_ulong,
     ...
) -> c_int

C docs: https://man7.org/linux/man-pages/man2/ioctl.2.html

request is a device specific number and, technically, any device driver may use any notation for request and accept any amount of arguments as ....

However there is a common notation for input/output devices. In linux kernel there are tons of macros to build a request ID and drivers follow this notation. If we boil down those C macros to a nice rust's const fn we will get something like this:

/// Encode an ioctl command.
pub const fn ioc(dir: u64, type_: u64, nr: u64, size: u64) -> u64 {
    (((dir as u32) << DIRSHIFT)
        | ((type_ as u32) << TYPESHIFT)
        | ((nr as u32) << NRSHIFT) 
        | ((size as u32) << SIZESHIFT)) as u64
}

And most ioctl call will look like

libc::ioctl(file_descriptor, ioc(dir, type_, nr, size), &mut data);

dir/type_/nr/size are real parameters describing ioctl command (I have no clues why they use smart macroses instead of 4 u8 function arguments, legacy, I guess?).

On top of this there are lots of macros like EVIOCGABS. Check them out here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/input.h#n129

#define EVIOCGABS(abs)		_IOR('E', 0x40 + (abs), struct input_absinfo)	/* get abs value/limits */

They look like this, and boiled down to const fn will look something like this:

pub const fn eviocgabs(abs: u32) -> u64 {
    ioc(READ | WRITE, b'E', 0x40 + abs, std::mem::size_of::<input_absinfo>() as _)
}

This eviocgabs is going to be used later like this:

libc::ioctl(fd, eviocgabs(code), &mut axis_data);

It is an ioctl call with a request for some absolute value, and the argument is a pointer to a result data.

And, finally, back to the gamepad implementation. To get our axis/buttons state we need to do 3 steps:

  • go through all /dev/input/event* and figure who is the gamepad
  • ask the gamepad about axises/buttons metadata
  • and finally get the data itself

Now I am mostly based on GLFW https://github.com/glfw/glfw/blob/master/src/linux_joystick.c implementation (the best docs on evdev's gamepad I found)

For the first step - just try to open all the /dev/input/event* files, and whoever will successefully open - probably is some sort of an input device.

For the second step we will need a few ioctl call to get axis/button maps. And device who will not give that map - is not a gamepad.

let mut key_bits: [u8; (KEY_CNT as usize + 7) / 8] = [0; (KEY_CNT as usize + 7) / 8];

if libc::ioctl(fd, eviocgbit(std::mem::size_of(key_bits)), key_bits.as_mut_ptr()) < 0 {
  panic!();
}

This is the way to get key's binding, for example. There are lots of ioctl calls and metadata to acquire, but thats the idea - feed some pointers to ioctl, get all the metadata, validate that this is a gamepad and we know what are the axis.

And the third, last step - back to normal reading routing, pretty much like joydev. In my implementation I already used a lot of libc stuff to work with ioctl, so this time it would be libc::read instad of std::fs::read, but this is just implementation details, it is certainly doable with std::fs::read.

loop {
    let mut event = InputEvent::default();

    if libc::read(
        self.fd,
        &mut event as *mut _ as *mut _,
        std::mem::size_of_val(&e),
    ) < 0
    {
        // handle disconnect
        return;
    }
    if event.type_ == EV_KEY as _ {
        println!("Digital button number {}: changed value to {}", event.code, event.value != 0);
    }
    if event.type_ == EV_ABS as _ {
        let info = ..; // thing we got from ioctl while joystick initialisation 

        let value = value / (info.maximum - info.minimum); // not exactly correct formula, but thats the idea
        println!("Axis number {}: changed value to {}", event.code, value);
    }
}

Intresting that code in event supposed to be a gamepad's button code, defined in https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h#L379 But in reality it is not. Some gampeads just assigns random codes to random buttons. What most libraries do instead (SDL, GLFW) - during gamepad initialisation they assign a number to each of a presented button. And than they use a hardcoded, per-gamepad map https://github.com/gabomdq/SDL_GameControllerDB to go from this 0..buttons_count identifier to a real button, like BTN_A/BTN_B.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment