Kernel debouncing for PS/2 mice

Recently my trackball has been playing up. The switch contacts have gone flaky, and do not reliably make continuous contact for as long as the button is pressed. This makes simple operations like moving and resizing windows or selecting sections of text somewhere between a pain in the arse and outright impossible.

Resizing windows is perhaps the worst, because the dodgy switch sometimes manages to send an obscure sequence of clicks while I'm trying to drag the corner, a sequence which I have been entirely unable to replicate by deliberate action and which has the bizarre and useless effect of causing the window to instantly shrink to a titchy little object not much bigger than the mouse pointer itself, which disappears into the background clutter of whatever is behind it and leaves me wondering where the fuck it's gone. It also tends to respond to attempts to drag it bigger again by perversely becoming even smaller, so I end up closing it and starting again.

So I started looking around for information on debouncing mouse buttons in Linux, and found fuck loads of people asking about it but nothing by way of satisfactory answers. The closest I did find was a patched version of xf86-input-evdev for Arch Linux, derived from this evdev patch by Matt Whitlock submitted to in 2012 but never incorporated into X officially.

Unfortunately that is of no use to me because evdev smells of wee. When I junked Ubuntu and went back to straight Debian, the installation's X configuration used evdev by default, and it did some horribly shitty thing which made it useless. Either X wouldn't start at all or it did start but nothing would make the mouse work, I can't remember which, but it was shit. So I looked for what was different in the config from what I'd had before, ripped out all the evdev shit, set it up to use xserver-xorg-input-kbd and xserver-xorg-input-mouse like it always used to, and we were all as merry as lambkins on the lea. It's been working fine ever since so I've never had the slightest inclination or motivation to find out why evdev didn't work, especially since fixing X problems is a giant pain in the arse since without X up you can't use google, but it does now mean that I can't use this patch.

But in any case I was really after a solution that would not involve shutting X down and starting it up again, and that basically meant a kernel module, since removing and reinserting a kernel mouse driver module is trivially done on the fly without disturbing X. Also they tend to be simpler to code and the documentation is better. Of course there is also the possibility of locking the whole system up by getting shit wrong, but the answer there is to not get shit that wrong.

So here is a patch for the psmouse driver in kernel 3.14. When a button is released, it does not report the event straight away; instead it hangs on to it for a certain time, and if a press event is received for that button within that time, it decides that the release was unintentional and drops both events on the floor. (This is the same logic as Matt Whitlock's X patch but in a different implementation and done in the kernel.) It keeps separate jiffy counts for each button so they are all debounced independently (or as near as doesn't matter). The debounce delay, which is in jiffies, can be configured at module load time by the parameter debounce_delay, or on the fly using /sys/module/psmouse/parameters/debounce_delay.

Since it is impossible to decide at the time of receiving it whether a release event is spurious or not, and it can only be done by waiting to see what follows or doesn't follow it, setting too long a debounce delay can lead to the release event being deferred long enough to be noticeable. It requires a bit of experimentation to find a value which is long enough to be effective but short enough to avoid "mouse not letting go of things" problems. Unfortunately until someone works out how to make semiconductors incorporating a thiotimoline structure we're more or less stuck with this.

Setting too long a value can also make it impossible to double-click, so it may be necessary to extend the double-click time, for instance by:

# cat > /etc/X11/Xresources/doubleclick *multiClickTime: 400 ^D

followed by

$ xrdb -merge /etc/X11/Xresources/doubleclick

to apply the change without restarting X.

Download the patch: psmouse-3.14-debounce.patch.gz

--- linux-source-3.14-orig/drivers/input/mouse/psmouse.h 2014-07-31 22:51:43.000000000 +0100 +++ linux-source-3.14/drivers/input/mouse/psmouse.h 2015-04-11 13:12:56.176368107 +0100 @@ -36,6 +36,11 @@ PSMOUSE_FULL_PACKET } psmouse_ret_t; +struct psmouse_button_states { + int button; + long ts; +}; + struct psmouse { void *private; struct input_dev *dev; @@ -62,6 +67,9 @@ unsigned int resolution; unsigned int resetafter; unsigned int resync_time; + unsigned int debounce_delay; + struct psmouse_button_states *debounce_button_list; + struct delayed_work debounce_work; bool smartscroll; /* Logitech only */ psmouse_ret_t (*protocol_handler)(struct psmouse *psmouse); --- linux-source-3.14-orig/drivers/input/mouse/psmouse-base.c 2014-07-31 22:51:43.000000000 +0100 +++ linux-source-3.14/drivers/input/mouse/psmouse-base.c 2015-04-11 13:12:52.580384638 +0100 @@ -73,6 +73,10 @@ module_param_named(resync_time, psmouse_resync_time, uint, 0644); MODULE_PARM_DESC(resync_time, "How long can mouse stay idle before forcing resync (in seconds, 0 = never)."); +static unsigned int psmouse_debounce_delay = 0; +module_param_named(debounce_delay, psmouse_debounce_delay, uint, 0644); +MODULE_PARM_DESC(debounce_delay, "Ignore button release if button press received before delay (in jiffies)."); + PSMOUSE_DEFINE_ATTR(protocol, S_IWUSR | S_IRUGO, NULL, psmouse_attr_show_protocol, psmouse_attr_set_protocol); @@ -88,6 +92,9 @@ PSMOUSE_DEFINE_ATTR(resync_time, S_IWUSR | S_IRUGO, (void *) offsetof(struct psmouse, resync_time), psmouse_show_int_attr, psmouse_set_int_attr); +PSMOUSE_DEFINE_ATTR(debounce_delay, S_IWUSR | S_IRUGO, + (void *) offsetof(struct psmouse, debounce_delay), + psmouse_show_int_attr, psmouse_set_int_attr); static struct attribute *psmouse_attributes[] = { &psmouse_attr_protocol.dattr.attr, @@ -95,6 +102,7 @@ &psmouse_attr_resolution.dattr.attr, &psmouse_attr_resetafter.dattr.attr, &psmouse_attr_resync_time.dattr.attr, + &psmouse_attr_debounce_delay.dattr.attr, NULL }; @@ -124,6 +132,94 @@ }; /* + * psmouse_return_button_state() returns a pointer to the struct psmouse_button_states + * corresponding to the given button. If there is no such struct, it writes one. + * This relies on the protocol detection setting no fewer bits in psmouse->dev->keybit + * than the packet processing reports distinct buttons, otherwise insufficient memory + * will have been allocated for psmouse->debounce_button_list. + */ + +static struct psmouse_button_states *psmouse_return_button_state(struct psmouse *psmouse, int button) { + struct psmouse_button_states *n; + + if (!(n = psmouse->debounce_button_list)) return(NULL); + for (; n->button; n++) if (n->button == button) break; + if (!n->button) { + n->button = button; + n->ts = 0; + } + return(n); +} + +/* + * psmouse_debounce() sends deferred release events + */ + +static void psmouse_debounce(struct work_struct *work) { + struct psmouse_button_states *n; + int x, b; + unsigned long j; + struct psmouse *psmouse = container_of(work, struct psmouse,; + + j = (jiffies & LONG_MAX) - psmouse->debounce_delay; + x = b = 0; + for (n = psmouse->debounce_button_list; n && n->button; n++) if (n->ts > 0) { + if (n->ts <= j) { + input_report_key(psmouse->dev, n->button, 0); + n->ts = 0; + b = 1; + } else { + x = 1; /* happens if an unexpired event exists */ + } + } + if (b) input_sync(psmouse->dev); + + /* Set another queue entry if any unexpired events remain */ + /* Shouldn't happen, but belt and braces... */ + if (x) psmouse_queue_work(psmouse, &psmouse->debounce_work, psmouse->debounce_delay); +} + +/* + * psmouse_input_report_button_debounced() decides whether or not + * to report button events, using debounce_delay + */ + +void psmouse_input_report_button_debounced (struct psmouse *psmouse, int code, int value) { + struct psmouse_button_states *current_button_state; + + /* If no debounce delay set, just report as normal */ + if (!psmouse->debounce_delay) { + input_report_key(psmouse->dev, code, value); + return; + } + + /* If button state table lookup fails, report as normal */ + if (!(current_button_state = psmouse_return_button_state(psmouse, code))) { + input_report_key(psmouse->dev, code, value); + return; + } + + /* Make sure no deferred events go off while we're doing things */ + if (!cancel_delayed_work(&psmouse->debounce_work)) flush_workqueue(kpsmoused_wq); + + if (!value) { /* Release event */ + if (current_button_state->ts < 0) current_button_state->ts = jiffies & LONG_MAX; + } else { /* Press event */ + /* Do we have a waiting release event? If not, report this event */ + if (!current_button_state->ts) { + input_report_key(psmouse->dev, code, 1); + } + /* Cancel any waiting release events and record pressed state */ + current_button_state->ts = -1; + } + + /* (Re)queue deferred events */ + psmouse_queue_work(psmouse, &psmouse->debounce_work, psmouse->debounce_delay); + + return; +} + +/* * psmouse_process_byte() analyzes the PS/2 data stream and reports * relevant events to the input module once full packet has arrived. */ @@ -162,8 +258,8 @@ case 0x00: case 0xC0: input_report_rel(dev, REL_WHEEL, (int) (packet[3] & 8) - (int) (packet[3] & 7)); - input_report_key(dev, BTN_SIDE, (packet[3] >> 4) & 1); - input_report_key(dev, BTN_EXTRA, (packet[3] >> 5) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_SIDE, (packet[3] >> 4) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_EXTRA, (packet[3] >> 5) & 1); break; } } @@ -173,15 +269,15 @@ */ if (psmouse->type == PSMOUSE_GENPS) { - input_report_key(dev, BTN_SIDE, (packet[0] >> 6) & 1); - input_report_key(dev, BTN_EXTRA, (packet[0] >> 7) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_SIDE, (packet[0] >> 6) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_EXTRA, (packet[0] >> 7) & 1); } /* * Extra button on ThinkingMouse */ if (psmouse->type == PSMOUSE_THINKPS) { - input_report_key(dev, BTN_EXTRA, (packet[0] >> 3) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_EXTRA, (packet[0] >> 3) & 1); /* Without this bit of weirdness moving up gives wildly high Y changes. */ packet[1] |= (packet[0] & 0x40) << 1; } @@ -191,7 +287,7 @@ * byte. */ if (psmouse->type == PSMOUSE_CORTRON) { - input_report_key(dev, BTN_SIDE, (packet[0] >> 3) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_SIDE, (packet[0] >> 3) & 1); packet[0] |= 0x08; } @@ -199,9 +295,9 @@ * Generic PS/2 Mouse */ - input_report_key(dev, BTN_LEFT, packet[0] & 1); - input_report_key(dev, BTN_MIDDLE, (packet[0] >> 2) & 1); - input_report_key(dev, BTN_RIGHT, (packet[0] >> 1) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_LEFT, packet[0] & 1); + psmouse_input_report_button_debounced(psmouse, BTN_MIDDLE, (packet[0] >> 2) & 1); + psmouse_input_report_button_debounced(psmouse, BTN_RIGHT, (packet[0] >> 1) & 1); input_report_rel(dev, REL_X, packet[1] ? (int) packet[1] - (int) ((packet[0] << 4) & 0x100) : 0); input_report_rel(dev, REL_Y, packet[2] ? (int) ((packet[0] << 3) & 0x100) - (int) packet[2] : 0); @@ -1337,6 +1433,7 @@ serio_close(serio); serio_set_drvdata(serio, NULL); input_unregister_device(psmouse->dev); + if (psmouse->debounce_button_list) kfree(psmouse->debounce_button_list); kfree(psmouse); if (parent) @@ -1350,6 +1447,8 @@ { const struct psmouse_protocol *selected_proto; struct input_dev *input_dev = psmouse->dev; + int n, x; + unsigned long k; input_dev->dev.parent = &psmouse->ps2dev.serio->dev; @@ -1372,6 +1471,18 @@ psmouse->ignore_parity = selected_proto->ignore_parity; + /* Count buttons and allocate space for debounce list */ + if (psmouse->debounce_button_list) kfree(psmouse->debounce_button_list); + n = 1; + for (x = 0; x < BITS_TO_LONGS(KEY_CNT); x++) { + k = psmouse->dev->keybit[x]; + while (k) { + if (k & 1) n++; + k >>= 1; + } + } + psmouse->debounce_button_list = kzalloc(n * sizeof(struct psmouse_button_states), GFP_KERNEL); + /* * If mouse's packet size is 3 there is no point in polling the * device in hopes to detect protocol reset - we won't get less @@ -1451,6 +1562,9 @@ psmouse->resetafter = psmouse_resetafter; psmouse->resync_time = parent ? 0 : psmouse_resync_time; psmouse->smartscroll = psmouse_smartscroll; + psmouse->debounce_delay = psmouse_debounce_delay; + psmouse->debounce_button_list = NULL; + INIT_DELAYED_WORK(&psmouse->debounce_work, psmouse_debounce); psmouse_switch_protocol(psmouse, NULL); @@ -1493,6 +1607,7 @@ serio_set_drvdata(serio, NULL); err_free: input_free_device(input_dev); + if (psmouse->debounce_button_list) kfree(psmouse->debounce_button_list); kfree(psmouse); retval = error;

It has to be said that further motivation for writing this patch, over and above the need to have the buttons working properly again, arose from becoming pissed off at some of the comments I had seen made in reply to other people having the same problem while I was looking to see if there was a ready-made solution...

"Buy a new one." - Fuck off. A mouse may be cheap, but that's by the by. There's too much of this replace-rather-than-repair shit these days by far. It wastes resources and it creates unnecessary work. Stuff has to be dug up for raw materials, processed, taken to the mouse factory, made into mice, taken away from the mouse factory, put on a ship, taken half way round the world, and sold, plus a load more steps if you look with greater granularity. All these steps involve consumption of materials and/or energy, and all of them involve making actual people do work which would not otherwise need to be done. And all that can be avoided by me sitting here for a wee whiley using a trivial amount of energy (system load compiling vs. system load doing whatever else I might have done instead - might even come out negative) to do something which can't be counted as work since nobody is making me do it and I enjoy it. OK it's a drop in the ocean but at least I can put it online so other people can also avoid creating waste.

Other advantages to this method are that of getting things working right now, instead of pissing around waiting for a mouse to come through the post, and that as far as my trackball is concerned buying a new one isn't an option in the first place, see below...

"Repair it." In the general case, yes, indeed, fine, great. I've got the bits and in the general case it's not a big job. However my trackball is a Frankenstinian abomination consisting of the body and buttons of a Trust Ami Track Dual Scroll and the optical pickup and ball of some Logitech thing thrust together in bestial union, because that is the only way I can get a trackball which has optical pickup and is a comfortable shape and has buttons and scroll wheels where I want them to be. Persuading the two components to interrupt their coitus so that I can replace the microswitches is not something I fancy doing, given how much of a cunt it was to get everything to line up properly when I hacked it together in the first place. Not to mention that simply clearing the papers and boxes and envelopes off the cable so I can pick the thing up to take it apart at all would probably take a good 15 minutes.

"It's not worth patching the official X source just for one person's problem." True, but not relevant, because it isn't just one person's problem. It's fucking loads of people's problem. Googling for it brings up a whole slew of different people asking about it, and not just the same bloody thread scraped onto a hundred different bollocks sites like you usually get these days, so there must be plenty of people having it. Not to mention that from the look of some of those results, at least some writers of Windows mouse drivers have thought it worthwhile to include it. So bugger that.

"The cost of writing software to do it would be greater than the cost of buying a new mouse." For fuck's sake don't be so FUCKING STUPID. What the fuck is going on that someone who has the mental capacity to program computers can still come out with such moronic bullshit as that? The cost of writing software to do it is FUCK ALL. I know this because I've just done it. How in all the caked shit in a thousand tramps' underpants does this div manage to write code when he doesn't know the difference between x > 0 and x < 0 for fuck's sake? Or was he actually planning to shoplift a new mouse but just didn't want to say so in public? I suppose they are small enough to be easily nickable but it's hardly worth the risk just for a fucking mouse. It's probably the answer though since it does make a little more sense than saying that zero is greater than a positive number, though not much.

And finally, the one that not only takes the fucking biscuit but spunks all over it and makes you eat it - "Don't use software to solve hardware problems." FOR FUCK'S SAKE THAT'S WHAT IT'S FUCKING FOR YOU STEAMING GREAT DIV. Leaving aside that the actual kernel part of the Linux kernel is really quite small and most of the size of it is modular components to deal with the various quirks and foibles of different hardware devices that all do the same thing but in slightly different ways - the whole point of computational capability existing in the first place is to deal with problems arising from the non-computational, physical world. That's why brains evolved. That's why some of those brains then invented computers. And that is what those computers then do: use software to circumvent hardware-based difficulties. They use it to provide communications facilities because those provided by the standard human hardware don't work beyond a few yards or so. They use it to simulate the operation of engines with various parameters being experimentally varied because designing them to the same standard but doing all the experiments in hardware would take bleeding forever. They use it to store and reproduce the sounds of musical instruments and the movements of actors because physical hardware limitations mean you can't have the band or the cast giving individual performances in the living rooms of everyone who wants to hear or see them. They even - and predominantly - use it to shift and shuffle truly stupendous amounts of utter bullshit, because humans are so mindlessly addicted to making up imaginary bollocks about money shit and then pretending it is worth more money even though it doesn't fucking exist that the built-in human computational hardware doesn't have the capacity to process all of it. Using software to deal with hardware is what the whole fucking thing is all about. If we didn't use software to deal with hardware it wouldn't be a case of we'd never have come out of the caves, it'd be more like we never even thought of going into the caves in the first place. Chicken's tits.

Anyway, my trackball is working.

Back to Pigeon's Nest

Be kind to pigeons

Valid HTML 4.01!