summaryrefslogtreecommitdiff
path: root/firmware/target/arm/imx233/sansa-fuzeplus/button-fuzeplus.c
blob: 2ded86a96744d417d037e4906b8559cc00162a26 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2011 by Amaury Pouly
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include "button-target.h"
#include "system.h"
#include "system-target.h"
#include "pinctrl-imx233.h"
#include "i2c-imx233.h"
#include "synaptics-rmi.h"
#include "lcd.h"
#include "string.h"
#include "usb.h"
#include "button-imx233.h"
#include "touchpad.h"
#include "stdio.h"
#include "font.h"

struct imx233_button_map_t imx233_button_map[] =
{
    IMX233_BUTTON(VOL_DOWN, GPIO(1, 30), "vol_down", INVERTED),
    IMX233_BUTTON(POWER, PSWITCH(1), "power"),
    IMX233_BUTTON(VOL_UP, PSWITCH(3), "vol_up"),
    IMX233_BUTTON_(END, END(), "")
};

#ifndef BOOTLOADER
/**
 * RMI API
 */

#define RMI_I2C_ADDR    0x40

static unsigned char dev_ctl_reg; /* cached value of control register */

/* NOTE:
 * RMI over i2c supports some special aliases on page 0x2 but this driver don't
 * use them */

struct rmi_xfer_t;
typedef void (*rmi_xfer_cb_t)(struct rmi_xfer_t *xfer);

/* Represent a typical RMI transaction: a first transfer to select the page
 * and a second transfer to read/write registers. The API takes care of annoying
 * details and will simply call the callback at the end of the transfer. */
struct rmi_xfer_t
{
    struct imx233_i2c_xfer_t xfer_page; /* first transfer: page select */
    struct imx233_i2c_xfer_t xfer_rw; /* second transfer: read/write */
    uint8_t sel_page[2]; /* write command to select page */
    uint8_t sel_reg; /* write command to select register */
    volatile enum imx233_i2c_error_t status; /* transfer status */
    rmi_xfer_cb_t callback; /* callback */
};

/* Synchronous transfer: add a semaphore to block */
struct rmi_xfer_sync_t
{
    struct rmi_xfer_t xfer;
    struct semaphore sema; /* semaphore for completion */
};

/* callback for first transfer: record error if any */
static void rmi_i2c_first_callback(struct imx233_i2c_xfer_t *xfer, enum imx233_i2c_error_t status)
{
    struct rmi_xfer_t *rxfer = container_of(xfer, struct rmi_xfer_t, xfer_page);
    /* record status */
    rxfer->status = status;
}

/* callback for first transfer: handle error and callback */
static void rmi_i2c_second_callback(struct imx233_i2c_xfer_t *xfer, enum imx233_i2c_error_t status)
{
    struct rmi_xfer_t *rxfer = container_of(xfer, struct rmi_xfer_t, xfer_rw);
    /* record status, only if not skipping (ie the error was in first transfer) */
    if(status != I2C_SKIP)
        rxfer->status = status;
    /* callback */
    if(rxfer->callback)
        rxfer->callback(rxfer);
}

/* build a rmi transaction to read/write registers; do NOT cross page boundary ! */
static void rmi_build_xfer(struct rmi_xfer_t *xfer, bool read, int address,
    int byte_count, unsigned char *buffer, rmi_xfer_cb_t callback)
{
    /* first transfer: change page */
    xfer->xfer_page.next = &xfer->xfer_rw;
    xfer->xfer_page.fast_mode = true;
    xfer->xfer_page.dev_addr = RMI_I2C_ADDR;
    xfer->xfer_page.mode = I2C_WRITE;
    xfer->xfer_page.count[0] = 2;
    xfer->xfer_page.data[0] = &xfer->sel_page;
    xfer->xfer_page.count[1] = 0;
    xfer->xfer_page.tmo_ms = 1000;
    xfer->xfer_page.callback = &rmi_i2c_first_callback;
    /* second transfer: read/write */
    xfer->xfer_rw.next = NULL;
    xfer->xfer_rw.fast_mode = true;
    xfer->xfer_rw.dev_addr = RMI_I2C_ADDR;
    xfer->xfer_rw.mode = read ? I2C_READ : I2C_WRITE;
    xfer->xfer_rw.count[0] = 1;
    xfer->xfer_rw.data[0] = &xfer->sel_reg;
    xfer->xfer_rw.count[1] = byte_count;
    xfer->xfer_rw.data[1] = buffer;
    xfer->xfer_rw.tmo_ms = 1000;
    xfer->xfer_rw.callback = &rmi_i2c_second_callback;
    /* general things */
    xfer->callback = callback;
    xfer->sel_page[0] = RMI_PAGE_SELECT;
    xfer->sel_page[1] = address >> 8;
    xfer->sel_reg = address & 0xff;
}

/** IMPORTANT NOTE
 *
 * All transfers are built using rmi_build_xfer which constructs a transaction
 * consisting in a page select and register read/writes. Since transactions are
 * executed "atomically" and are queued, it is safe to call transfers functions
 * concurrently. However only asynchronous transfers can be used in IRQ context.
 * In all cases, make sure the the rmi_xfer_t structure lives at least until the
 * completion of the transfer (callback).
 */

/* queue transfer to change sleep mode, return true if transfer was queued
 * and false if ignored because requested mode is already the current one.
 * call must provide a transfer structure that must exist until completion */
static bool rmi_set_sleep_mode_async(struct rmi_xfer_t *xfer, uint8_t *buf,
    unsigned char sleep_mode, rmi_xfer_cb_t callback)
{
    /* avoid any race with concurrent changes to the mode */
    unsigned long cpsr = disable_irq_save();
    /* valid value different from the actual one */
    if((dev_ctl_reg & RMI_SLEEP_MODE_BM) != sleep_mode)
    {
        /* change cached version */
        dev_ctl_reg &= ~RMI_SLEEP_MODE_BM;
        dev_ctl_reg |= sleep_mode;
        *buf = dev_ctl_reg;
        restore_irq(cpsr);
        /* build transfer and kick */
        rmi_build_xfer(xfer, false, RMI_DEVICE_CONTROL, 1, buf, callback);
        imx233_i2c_transfer(&xfer->xfer_page);
        return true;
    }
    else
    {
        restore_irq(cpsr);
        return false;
    }
}

/* synchronous callback: release semaphore */
static void rmi_i2c_sync_callback(struct rmi_xfer_t *xfer)
{
    struct rmi_xfer_sync_t *sxfer = (void *)xfer;
    semaphore_release(&sxfer->sema);
}

/* synchronous read/write */
static void rmi_rw(bool read, int address, int byte_count, unsigned char *buffer)
{
    struct rmi_xfer_sync_t xfer;
    rmi_build_xfer(&xfer.xfer, read, address, byte_count, buffer, rmi_i2c_sync_callback);
    semaphore_init(&xfer.sema, 1, 0);
    /* kick and wait */
    imx233_i2c_transfer(&xfer.xfer.xfer_page);
    semaphore_wait(&xfer.sema, TIMEOUT_BLOCK);
    if(xfer.xfer.status != I2C_SUCCESS)
        panicf("rmi: i2c err %d", xfer.xfer.status);
}

/* read registers synchronously */
static void rmi_read(int address, int byte_count, unsigned char *buffer)
{
    rmi_rw(true, address, byte_count, buffer);
}

/* read single register synchronously */
static int rmi_read_single(int address)
{
    unsigned char c;
    rmi_rw(true, address, 1, &c);
    return c;
}

/* write single register synchronously */
static void rmi_write_single(int address, unsigned char byte)
{
    return rmi_rw(false, address, 1, &byte);
}

/* synchronously change sleep mode, this is a nop if current mode is the same as requested */
static void rmi_set_sleep_mode(unsigned char sleep_mode)
{
    struct rmi_xfer_sync_t xfer;
    uint8_t buf;
    semaphore_init(&xfer.sema, 1, 0);
    /* kick asynchronous transfer and only wait if mode was actually changed */
    if(rmi_set_sleep_mode_async(&xfer.xfer, &buf, sleep_mode, &rmi_i2c_sync_callback))
    {
        semaphore_wait(&xfer.sema, TIMEOUT_BLOCK);
        if(xfer.xfer.status != I2C_SUCCESS)
            panicf("rmi: i2c err %d", xfer.xfer.status);
    }
}

static void rmi_init(void)
{
    /* cache control register */
    dev_ctl_reg = rmi_read_single(RMI_DEVICE_CONTROL);
}


/**
 * Touchpad API
 */

/* we emulate a 3x3 grid, this gives the button mapping */
int button_mapping[3][3] =
{
    {BUTTON_BOTTOMLEFT, BUTTON_LEFT, BUTTON_BACK},
    {BUTTON_DOWN, BUTTON_SELECT, BUTTON_UP},
    {BUTTON_BOTTOMRIGHT, BUTTON_RIGHT, BUTTON_PLAYPAUSE},
};

/* timeout before lowering touchpad power from lack of activity */
#define ACTIVITY_TMO        (5 * HZ)
#define TOUCHPAD_WIDTH      3010
#define TOUCHPAD_HEIGHT     1975
#define DEADZONE_MULTIPLIER 2 /* deadzone multiplier */

/* power level when touchpad is active: experiments show that "low power" reduce
 * power consumption and hardly makes a difference in quality. */
#define ACTIVE_POWER_LEVEL  RMI_SLEEP_MODE_LOW_POWER

static int touchpad_btns = 0; /* button bitmap for the touchpad */
static unsigned last_activity = 0; /* tick of the last touchpad activity */
static bool t_enable = true; /* is touchpad enabled? */
static int deadzone; /* deadzone size */
static struct timeout activity_tmo; /* activity timeout */

/* Ignore deadzone function. If outside of the pad, project to border. */
static int find_button_no_deadzone(int x, int y)
{
    /* compute grid coordinate */
    int gx = MAX(MIN(x * 3 / TOUCHPAD_WIDTH, 2), 0);
    int gy = MAX(MIN(y * 3 / TOUCHPAD_HEIGHT, 2), 0);

    return button_mapping[gx][gy];
}

static int find_button(int x, int y)
{
    /* find button ignoring deadzones */
    int btn = find_button_no_deadzone(x, y);
    /* To check if we are in a deadzone, we try to shift the coordinates
     * and see if we get the same button. Not that we do not want to apply
     * the deadzone in the borders ! The code works even in the borders because
     * the find_button_no_deadzone() project out-of-bound coordinates to the
     * borders */
    if(find_button_no_deadzone(x + deadzone, y) != btn ||
            find_button_no_deadzone(x - deadzone, y) != btn ||
            find_button_no_deadzone(x, y + deadzone) != btn ||
            find_button_no_deadzone(x, y - deadzone) != btn)
        return 0;
    return btn;
}

void touchpad_set_deadzone(int touchpad_deadzone)
{
    deadzone = touchpad_deadzone * DEADZONE_MULTIPLIER;
}

static int touchpad_read_device(void)
{
    return touchpad_btns;
}

/* i2c transfer only used for irq processing
 * NOTE we use two sets of transfers because we setup one in the callback of the
 * other, using one would be unsafe */
static struct rmi_xfer_t rmi_irq_xfer[2];
static uint8_t rmi_irq_buf; /* buffer to hold irq status register and sleep mode */
static union
{
    unsigned char data[10];
    struct
    {
        struct rmi_2d_absolute_data_t absolute;
        struct rmi_2d_relative_data_t relative;
        struct rmi_2d_gesture_data_t gesture;
    }s;
}rmi_irq_data; /* buffer to hold touchpad data */

static void rmi_attn_cb(int bank, int pin, intptr_t user);

/* callback for i2c transfer to change power level after irq */
static void rmi_power_irq_cb(struct rmi_xfer_t *xfer)
{
    /* we do not recover from error for now */
    if(xfer->status != I2C_SUCCESS)
        panicf("rmi: clear i2c err %d", xfer->status);
    /* now that interrupt is cleared, we can renable attention irq */
    imx233_pinctrl_setup_irq(0, 27, true, true, false, &rmi_attn_cb, 0);
}

/* callback for i2c transfer to read/clear interrupt status register */
static void rmi_clear_irq_cb(struct rmi_xfer_t *xfer)
{
    /* we do not recover from error for now */
    if(xfer->status != I2C_SUCCESS)
        panicf("rmi: clear i2c err %d", xfer->status);
    /* at this point, we might have processed an event and the touchpad still be
     * in very low power mode because of some previous inactivity; if it's the case,
     * schedule another transfer to switch to a higher power mode before accepting the
     * next event */
    /* kick asynchronous transfer and only wait if mode was actually changed */
    if(!rmi_set_sleep_mode_async(&rmi_irq_xfer[0], &rmi_irq_buf, ACTIVE_POWER_LEVEL,
            &rmi_power_irq_cb))
        /* now that interrupt is cleared, we can renable attention irq */
        imx233_pinctrl_setup_irq(0, 27, true, true, false, &rmi_attn_cb, 0);
}

/* callback for i2c transfer to read touchpad data registers */
static void rmi_data_irq_cb(struct rmi_xfer_t *xfer)
{
    /* we do not recover from error for now */
    if(xfer->status != I2C_SUCCESS)
        panicf("rmi: data i2c err %d", xfer->status);
    /* now that we have the data, setup another transfer to clear interrupt */
    rmi_build_xfer(&rmi_irq_xfer[1], true, RMI_INTERRUPT_REQUEST, 1,
        &rmi_irq_buf, &rmi_clear_irq_cb);
    /* kick transfer */
    imx233_i2c_transfer(&rmi_irq_xfer[1].xfer_page);
    /* now process touchpad data */
    int absolute_x = rmi_irq_data.s.absolute.x_msb << 8 | rmi_irq_data.s.absolute.x_lsb;
    int absolute_y = rmi_irq_data.s.absolute.y_msb << 8 | rmi_irq_data.s.absolute.y_lsb;
    int nr_fingers = rmi_irq_data.s.absolute.misc & 7;
    if(nr_fingers == 1)
        touchpad_btns = find_button(absolute_x, absolute_y);
    else
        touchpad_btns = 0;
}

/* touchpad attention line interrupt */
static void rmi_attn_cb(int bank, int pin, intptr_t user)
{
    (void) bank;
    (void) pin;
    (void) user;
   /* build transfer to read data registers */
    rmi_build_xfer(&rmi_irq_xfer[0], true, RMI_DATA_REGISTER(0),
        sizeof(rmi_irq_data.data), rmi_irq_data.data, &rmi_data_irq_cb);
    /* kick transfer */
    imx233_i2c_transfer(&rmi_irq_xfer[0].xfer_page);
    /* update last activity */
    last_activity = current_tick;
}

void touchpad_enable_device(bool en)
{
    t_enable = en;
    rmi_set_sleep_mode(en ? ACTIVE_POWER_LEVEL : RMI_SLEEP_MODE_SENSOR_SLEEP);
}

void touchpad_set_sensitivity(int level)
{
    /* handle negative values as well ! */
    rmi_write_single(RMI_2D_SENSITIVITY_ADJ, (unsigned char)(int8_t)level);
}

/* transfer used by the activity timeout to change power level */
static struct rmi_xfer_t rmi_tmo_xfer;
static uint8_t rmi_tmo_buf;

/* activity timeout: lower power level after some inactivity */
static int activity_monitor(struct timeout *tmo)
{
    (void) tmo;
    if(TIME_AFTER(current_tick, last_activity + ACTIVITY_TMO))
    {
        /* don't change power mode if touchpad is disabled, it's already in sleep mode */
        if(t_enable)
            rmi_set_sleep_mode_async(&rmi_tmo_xfer, &rmi_tmo_buf,
                RMI_SLEEP_MODE_VERY_LOW_POWER, NULL);
    }

    return HZ; /* next check in 1 second */
}

void touchpad_init(void)
{
    /* Synaptics TouchPad information:
     * - product id: 1533
     * - nr function: 1 (0x10 = 2D touchpad)
     * 2D Touchpad information (function 0x10)
     * - nr data sources: 3
     * - standard layout
     * - extra data registers: 7
     * - nr sensors: 1
     * 2D Touchpad Sensor #0 information:
     * - has relative data: yes
     * - has palm detect: yes
     * - has multi finger: yes
     * - has enhanced gesture: yes
     * - has scroller: no
     * - has 2D scrollers: no
     * - Maximum X: 3009
     * - Maxumum Y: 1974
     * - Resolution: 82
     */

    imx233_pinctrl_acquire(0, 26, "touchpad power");
    imx233_pinctrl_set_function(0, 26, PINCTRL_FUNCTION_GPIO);
    imx233_pinctrl_enable_gpio(0, 26, false);
    imx233_pinctrl_set_drive(0, 26, PINCTRL_DRIVE_8mA);

    /* use a timer to monitor touchpad activity and manage power level */
    last_activity = current_tick;
    timeout_register(&activity_tmo, activity_monitor, HZ, 0);

    rmi_init();

    char product_id[RMI_PRODUCT_ID_LEN];
    rmi_read(RMI_PRODUCT_ID, RMI_PRODUCT_ID_LEN, product_id);
    /* The OF adjust the sensitivity based on product_id[1] compared to 2.
     * Since it doesn't seem to work great, just hardcode the sensitivity to
     * some reasonable value for now. */
    rmi_write_single(RMI_2D_SENSITIVITY_ADJ, 13);

    rmi_write_single(RMI_2D_GESTURE_SETTINGS,
        RMI_2D_GESTURE_PRESS_TIME_300MS |
        RMI_2D_GESTURE_FLICK_DIST_4MM << RMI_2D_GESTURE_FLICK_DIST_BP |
        RMI_2D_GESTURE_FLICK_TIME_700MS << RMI_2D_GESTURE_FLICK_TIME_BP);

    /* we don't know in which mode the touchpad start so use a sane default */
    rmi_set_sleep_mode(ACTIVE_POWER_LEVEL);
    /* enable interrupt */
    imx233_pinctrl_acquire(0, 27, "touchpad int");
    imx233_pinctrl_set_function(0, 27, PINCTRL_FUNCTION_GPIO);
    imx233_pinctrl_enable_gpio(0, 27, false);
    imx233_pinctrl_setup_irq(0, 27, true, true, false, &rmi_attn_cb, 0);
}

/**
 * Debug screen
 */

bool button_debug_screen(void)
{
    char product_id[RMI_PRODUCT_ID_LEN];
    rmi_read(RMI_PRODUCT_ID, RMI_PRODUCT_ID_LEN, product_id);
    uint8_t product_info[RMI_PRODUCT_INFO_LEN];
    rmi_read(RMI_PRODUCT_INFO, RMI_PRODUCT_INFO_LEN, product_info);
    char product_info_str[RMI_PRODUCT_INFO_LEN * 2 + 1];
    for(int i = 0; i < RMI_PRODUCT_INFO_LEN; i++)
        snprintf(product_info_str + 2 * i, 3, "%02x", product_info[i]);
    int x_max = rmi_read_single(RMI_2D_SENSOR_XMAX_MSB(0)) << 8 | rmi_read_single(RMI_2D_SENSOR_XMAX_LSB(0));
    int y_max = rmi_read_single(RMI_2D_SENSOR_YMAX_MSB(0)) << 8 | rmi_read_single(RMI_2D_SENSOR_YMAX_LSB(0));
    int sensor_resol = rmi_read_single(RMI_2D_SENSOR_RESOLUTION(0));
    int min_dist = rmi_read_single(RMI_2D_MIN_DIST);
    int gesture_settings = rmi_read_single(RMI_2D_GESTURE_SETTINGS);
    int volkeys_delay_counter = 0;
    union
    {
        unsigned char data;
        signed char value;
    }sensitivity;
    rmi_read(RMI_2D_SENSITIVITY_ADJ, 1, &sensitivity.data);

    /* Device to screen */
    int zone_w = LCD_WIDTH;
    int zone_h = (zone_w * y_max + x_max - 1) / x_max;
    int zone_x = 0;
    int zone_y = LCD_HEIGHT - zone_h;
    #define DX2SX(x) (((x) * zone_w ) / x_max)
    #define DY2SY(y) (zone_h - ((y) * zone_h ) / y_max)
    struct viewport report_vp;
    memset(&report_vp, 0, sizeof(report_vp));
    report_vp.x = zone_x;
    report_vp.y = zone_y;
    report_vp.width = zone_w;
    report_vp.height = zone_h;
    struct viewport gesture_vp;
    memset(&gesture_vp, 0, sizeof(gesture_vp));
    gesture_vp.x = LCD_WIDTH / 2;
    gesture_vp.y = zone_y - 80;
    gesture_vp.width = LCD_WIDTH / 2;
    gesture_vp.height = 80;
    /* remember tick of last gestures */
    #define GESTURE_TMO     HZ / 2
    int tick_last_tap = current_tick - GESTURE_TMO;
    int tick_last_doubletap = current_tick - GESTURE_TMO;
    int tick_last_taphold = current_tick - GESTURE_TMO;
    int tick_last_flick = current_tick - GESTURE_TMO;
    int flick_x = 0, flick_y = 0;

    /* BUG the data register are usually read by the IRQ already and it is
     * important to not read them again, otherwise we could miss some events
     * (most notable gestures). However, we only read registers when the
     * touchpad is active so the data might be outdated if touchpad is
     * inactive. We should implement a continuous reading mode for the debug
     * screen. */

    lcd_setfont(FONT_SYSFIXED);
    while(1)
    {
        /* call button_get() to avoid an overflow in the button queue */
        button_get(false);

        unsigned char sleep_mode = rmi_read_single(RMI_DEVICE_CONTROL) & RMI_SLEEP_MODE_BM;
        lcd_set_viewport(NULL);
        lcd_clear_display();
        int btns = button_read_device();
        lcd_putsf(0, 0, "button bitmap: %x", btns);
        lcd_putsf(0, 1, "RMI: id=%s info=%s", product_id, product_info_str);
        lcd_putsf(0, 2, "xmax=%d ymax=%d res=%d", x_max, y_max, sensor_resol);
        lcd_putsf(0, 3, "attn=%d ctl=%x",
            imx233_pinctrl_get_gpio(0, 27) ? 0 : 1,
            rmi_read_single(RMI_DEVICE_CONTROL));
        lcd_putsf(0, 4, "sensi: %d min_dist: %d", (int)sensitivity.value, min_dist);
        lcd_putsf(0, 5, "gesture: %x", gesture_settings);

        union
        {
            unsigned char data[10];
            struct
            {
                struct rmi_2d_absolute_data_t absolute;
                struct rmi_2d_relative_data_t relative;
                struct rmi_2d_gesture_data_t gesture;
            }s;
        }u;

        /* Disable IRQs when reading to avoid reading incorrect data */
        unsigned long cpsr = disable_irq_save();
        memcpy(&u, &rmi_irq_data, sizeof(u));
        restore_irq(cpsr);

        int absolute_x = u.s.absolute.x_msb << 8 | u.s.absolute.x_lsb;
        int absolute_y = u.s.absolute.y_msb << 8 | u.s.absolute.y_lsb;
        int nr_fingers = u.s.absolute.misc & 7;
        bool gesture = (u.s.absolute.misc & 8) == 8;
        int palm_width = u.s.absolute.misc >> 4;

        lcd_putsf(0, 6, "abs: %d %d %d", absolute_x, absolute_y, (int)u.s.absolute.z);
        lcd_putsf(0, 7, "rel: %d %d", (int)u.s.relative.x, (int)u.s.relative.y);
        lcd_putsf(0, 8, "gesture: %x %x", u.s.gesture.misc, u.s.gesture.flick);
        lcd_putsf(0, 9, "misc: w=%d g=%d f=%d", palm_width, gesture, nr_fingers);
        lcd_putsf(0, 10, "sleep_mode: %d", sleep_mode);
        lcd_putsf(0, 11, "deadzone: %d", deadzone);
        /* display virtual touchpad with deadzones */
        lcd_set_viewport(&report_vp);
        lcd_set_drawinfo(DRMODE_SOLID, LCD_RGBPACK(0xff, 0xff, 0), LCD_BLACK);
        for(int i = 0; i < 3; i++)
            for(int j = 0; j < 3; j++)
            {
                int x = j * x_max / 3;
                if(j != 0)
                    x += deadzone;
                int x2 = (j + 1) * x_max / 3;
                if(j != 2)
                    x2 -= deadzone;
                int y = i * y_max / 3;
                if(i != 0)
                    y += deadzone;
                int y2 = (i + 1) * y_max / 3;
                if(i != 2)
                    y2 -= deadzone;
                x = DX2SX(x); x2 = DX2SX(x2); y = DY2SY(y); y2 = DY2SY(y2);
                lcd_drawrect(x, y2, x2 - x + 1, y - y2 + 1);
            }
        lcd_set_drawinfo(DRMODE_SOLID, LCD_RGBPACK(0xff, 0, 0), LCD_BLACK);
        lcd_drawrect(0, 0, zone_w, zone_h);
        /* put a done at the reported position of the finger
         * also display relative motion by a line as reported by the device */
        if(nr_fingers == 1)
        {
            lcd_set_drawinfo(DRMODE_SOLID, LCD_RGBPACK(0, 0, 0xff), LCD_BLACK);
            lcd_drawline(DX2SX(absolute_x) - u.s.relative.x,
                DY2SY(absolute_y) + u.s.relative.y,
                DX2SX(absolute_x), DY2SY(absolute_y));
            lcd_set_drawinfo(DRMODE_SOLID, LCD_RGBPACK(0, 0xff, 0), LCD_BLACK);
            lcd_fillrect(DX2SX(absolute_x) - 1, DY2SY(absolute_y) - 1, 3, 3);
        }
        lcd_set_viewport(&gesture_vp);
        lcd_set_drawinfo(DRMODE_SOLID, LCD_RGBPACK(0xff, 0xff, 0), LCD_BLACK);
        if(u.s.gesture.misc & RMI_2D_GEST_MISC_CONFIRMED)
        {
            switch(u.s.gesture.misc & RMI_2D_GEST_MISC_TAP_CODE_BM)
            {
                case RMI_2D_GEST_MISC_NO_TAP: break;
                case RMI_2D_GEST_MISC_SINGLE_TAP:
                    tick_last_tap = current_tick;
                    break;
                case RMI_2D_GEST_MISC_DOUBLE_TAP:
                    tick_last_doubletap = current_tick;
                    break;
                case RMI_2D_GEST_MISC_TAP_AND_HOLD:
                    tick_last_taphold = current_tick;
                    break;
                default: break;
            }

            if(u.s.gesture.misc & RMI_2D_GEST_MISC_FLICK)
            {
                tick_last_flick = current_tick;
                flick_x = u.s.gesture.flick & RMI_2D_GEST_FLICK_X_BM;
                flick_y = (u.s.gesture.flick & RMI_2D_GEST_FLICK_Y_BM) >> RMI_2D_GEST_FLICK_Y_BP;
                #define SIGN4EXT(a) \
                    if(a & 8) a = -((a ^ 0xf) + 1);
                SIGN4EXT(flick_x);
                SIGN4EXT(flick_y);
            }

            if(TIME_BEFORE(current_tick, tick_last_tap + GESTURE_TMO))
                lcd_putsf(0, 0, "TAP!");
            if(TIME_BEFORE(current_tick, tick_last_doubletap + GESTURE_TMO))
                lcd_putsf(0, 1, "DOUBLE TAP!");
            if(TIME_BEFORE(current_tick, tick_last_taphold + GESTURE_TMO))
                lcd_putsf(0, 2, "TAP & HOLD!");
            if(TIME_BEFORE(current_tick, tick_last_flick + GESTURE_TMO))
            {
                lcd_putsf(0, 3, "FLICK!");
                int center_x = (LCD_WIDTH * 2) / 3;
                int center_y = 40;
                lcd_drawline(center_x, center_y, center_x + flick_x * 5, center_y - flick_y * 5);
            }
        }
        lcd_update();
        if(btns & BUTTON_POWER)
            break;
        if(btns & (BUTTON_VOL_DOWN|BUTTON_VOL_UP))
        {
            volkeys_delay_counter++;
            if(volkeys_delay_counter == 15)
            {
                if(btns & BUTTON_VOL_UP)
                    if(sleep_mode > RMI_SLEEP_MODE_FORCE_FULLY_AWAKE)
                        sleep_mode--;
                if(btns & BUTTON_VOL_DOWN)
                    if(sleep_mode < RMI_SLEEP_MODE_SENSOR_SLEEP)
                        sleep_mode++;
                rmi_set_sleep_mode(sleep_mode);
                volkeys_delay_counter = 0;
            }
        }
    }

    lcd_set_viewport(NULL);
    lcd_setfont(FONT_UI);
    return true;
}

#else /* BOOTLOADER */
int touchpad_read_device(void)
{
    return 0;
}
#endif

/**
 * Button API
 */
void button_init_device(void)
{
#ifndef BOOTLOADER
    touchpad_init();
#endif
    /* generic */
    imx233_button_init();
}

int button_read_device(void)
{
    return imx233_button_read(touchpad_filter(touchpad_read_device()));
}