From 3ec66893e377b088c1284d2d23adb2aeea6d7965 Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sat, 27 Feb 2021 22:08:58 +0000 Subject: New port: FiiO M3K on bare metal Change-Id: I7517e7d5459e129dcfc9465c6fbd708619888fbe --- .../mips/ingenic_x1000/fiiom3k/button-fiiom3k.c | 503 +++++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c (limited to 'firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c') diff --git a/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c b/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c new file mode 100644 index 0000000000..db5ece10b0 --- /dev/null +++ b/firmware/target/mips/ingenic_x1000/fiiom3k/button-fiiom3k.c @@ -0,0 +1,503 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2021 Aidan MacDonald + * + * 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.h" +#include "kernel.h" +#include "backlight.h" +#include "panic.h" +#include "lcd.h" +#include "gpio-x1000.h" +#include "i2c-x1000.h" +#include +#include + +#ifndef BOOTLOADER +# include "font.h" +#endif + +#define FT_RST_PIN (1 << 15) +#define FT_INT_PIN (1 << 12) +#define ft_interrupt GPIOB12 + +/* Touch event types */ +#define EVENT_NONE (-1) +#define EVENT_PRESS 0 +#define EVENT_RELEASE 1 +#define EVENT_CONTACT 2 + +/* FSM states */ +#define STATE_IDLE 0 +#define STATE_PRESS 1 +#define STATE_REPORT 2 +#define STATE_SCROLL_PRESS 3 +#define STATE_SCROLLING 4 + +/* Assume there's no active touch if no event is reported in this time */ +#define AUTORELEASE_TIME (10000 * OST_TICKS_PER_US) + +/* If there's no significant motion on the scrollbar for this time, + * then report it as a button press instead */ +#define SCROLL_PRESS_TIME (100000 * OST_TICKS_PER_US) + +/* If a press on the scrollbar moves more than this during SCROLL_PRESS_TIME, + * then we enter scrolling mode. */ +#define MIN_SCROLL_THRESH 15 + +/* If OST tick a is after OST tick b, then returns the number of ticks + * in the interval between a and b; otherwise undefined. */ +#define TICKS_SINCE(a, b) ((a) - (b)) + +/* Number of touch samples to smooth before reading */ +#define TOUCH_SAMPLES 3 + +static struct ft_driver { + int i2c_cookie; + i2c_descriptor i2c_desc; + uint8_t raw_data[6]; + bool active; + + /* Number of pixels squared which must be moved before + * a scrollbar pulse is generated */ + int scroll_thresh_sqr; +} ftd; + +static struct ft_state_machine { + /* Current button state, used by button_read_device() */ + int buttons; + + /* FSM state */ + int state; + + /* Time of the last touch event, as 32-bit OST timestamp. The kernel + * tick is simply too low-resolution to work reliably, especially as + * we handle touchpad events asynchronously. */ + uint32_t last_event_t; + + /* Time of entering the SCROLL_PRESS state, used to differentiate + * between a press, hold, or scrolling motion */ + uint32_t scroll_press_t; + + /* Number of CONTACT events sampled in the PRESS state. + * Must reach TOUCH_SAMPLES before we move forward. */ + int samples; + + /* Filter for smoothing touch points */ + int sum_x, sum_y; + + /* Position of the original touch */ + int orig_x, orig_y; + + /* Current touch position */ + int cur_x, cur_y; +} fsm; + +static int touch_to_button(int x, int y) +{ + if(x == 900) { + /* Right strip */ + if(y == 80) + return BUTTON_BACK; + else if(y == 240) + return BUTTON_RIGHT; + else + return 0; + } else if(x < 80) { + /* Left strip */ + if(y < 80) + return BUTTON_MENU; + else if(y > 190) + return BUTTON_LEFT; + else + return 0; + } else { + /* Middle strip */ + if(y < 100) + return BUTTON_UP; + else if(y > 220) + return BUTTON_DOWN; + else + return BUTTON_SELECT; + } +} + +static bool ft_accum_touch(uint32_t t, int tx, int ty) +{ + /* Record event time */ + fsm.last_event_t = t; + + if(fsm.samples < TOUCH_SAMPLES) { + /* Continue "priming" the filter */ + fsm.sum_x += tx; + fsm.sum_y += ty; + fsm.samples += 1; + + /* Return if filter is not ready */ + if(fsm.samples < TOUCH_SAMPLES) + return false; + } else { + /* Update filter */ + fsm.sum_x += tx - fsm.sum_x / TOUCH_SAMPLES; + fsm.sum_y += ty - fsm.sum_y / TOUCH_SAMPLES; + } + + /* Filter is ready, so read the point */ + fsm.cur_x = fsm.sum_x / TOUCH_SAMPLES; + fsm.cur_y = fsm.sum_y / TOUCH_SAMPLES; + return true; +} + +static void ft_go_idle(void) +{ + /* Null out the touch state */ + fsm.buttons = 0; + fsm.samples = 0; + fsm.sum_x = fsm.sum_y = 0; + fsm.state = STATE_IDLE; +} + +static void ft_start_report(void) +{ + /* Report the button bit */ + fsm.buttons = touch_to_button(fsm.cur_x, fsm.cur_y); + fsm.orig_x = fsm.cur_x; + fsm.orig_y = fsm.cur_y; + fsm.state = STATE_REPORT; +} + +static void ft_start_report_or_scroll(void) +{ + ft_start_report(); + + /* If the press occurs on the scrollbar, then we need to + * wait an additional delay before reporting it in case + * this is the beginning of a scrolling motion */ + if(fsm.buttons & (BUTTON_UP|BUTTON_DOWN|BUTTON_SELECT)) { + fsm.buttons = 0; + fsm.scroll_press_t = __ost_read32(); + fsm.state = STATE_SCROLL_PRESS; + } +} + +static void ft_step_state(uint32_t t, int evt, int tx, int ty) +{ + /* Generate a release event automatically in case we missed it */ + if(evt == EVENT_NONE) { + if(TICKS_SINCE(t, fsm.last_event_t) >= AUTORELEASE_TIME) { + evt = EVENT_RELEASE; + tx = fsm.cur_x; + ty = fsm.cur_y; + } + } + + switch(fsm.state) { + case STATE_IDLE: { + if(evt == EVENT_PRESS || evt == EVENT_CONTACT) { + /* Move to REPORT or PRESS state */ + if(ft_accum_touch(t, tx, ty)) + ft_start_report_or_scroll(); + else + fsm.state = STATE_PRESS; + } + } break; + + case STATE_PRESS: { + if(evt == EVENT_RELEASE) { + /* Ignore if the number of samples is too low */ + ft_go_idle(); + } else if(evt == EVENT_PRESS || evt == EVENT_CONTACT) { + /* Accumulate the touch position in the filter */ + if(ft_accum_touch(t, tx, ty)) + ft_start_report_or_scroll(); + } + } break; + + case STATE_REPORT: { + if(evt == EVENT_RELEASE) + ft_go_idle(); + else if(evt == EVENT_PRESS || evt == EVENT_CONTACT) + ft_accum_touch(t, tx, ty); + } break; + + case STATE_SCROLL_PRESS: { + if(evt == EVENT_RELEASE) { + /* This _should_ synthesize a button press. + * + * - ft_start_report() will set the button bit based on the + * current touch position and enter the REPORT state, which + * will automatically hold the bit high + * + * - The next button_read_device() will see the button bit + * and report it back to Rockbox, then step the FSM with + * EVENT_NONE. + * + * - The EVENT_NONE stepping will eventually autogenerate a + * RELEASE event and restore the button state back to 0 + * + * - There's a small logic hole in the REPORT state which + * could cause it to miss an immediately repeated PRESS + * that occurs before the autorelease timeout kicks in. + * FIXME: We might want to special-case that. + */ + ft_start_report(); + break; + } + + if(evt == EVENT_PRESS || evt == EVENT_CONTACT) + ft_accum_touch(t, tx, ty); + + int dx = fsm.cur_x - fsm.orig_x; + int dy = fsm.cur_y - fsm.orig_y; + int dp = (dx*dx) + (dy*dy); + if(dp >= MIN_SCROLL_THRESH*MIN_SCROLL_THRESH) { + /* Significant motion: enter SCROLLING state */ + fsm.state = STATE_SCROLLING; + } else if(TICKS_SINCE(t, fsm.scroll_press_t) >= SCROLL_PRESS_TIME) { + /* No significant motion: report it as a press */ + fsm.cur_x = fsm.orig_x; + fsm.cur_y = fsm.orig_y; + ft_start_report(); + } + } break; + + case STATE_SCROLLING: { + if(evt == EVENT_RELEASE) { + ft_go_idle(); + break; + } + + if(evt == EVENT_PRESS || evt == EVENT_CONTACT) + ft_accum_touch(t, tx, ty); + + int dx = fsm.cur_x - fsm.orig_x; + int dy = fsm.cur_y - fsm.orig_y; + int dp = (dx*dx) + (dy*dy); + if(dp >= ftd.scroll_thresh_sqr) { + if(dy < 0) { + queue_post(&button_queue, BUTTON_SCROLL_BACK, 0); + } else { + queue_post(&button_queue, BUTTON_SCROLL_FWD, 0); + } + + /* Poke the backlight */ + backlight_on(); + buttonlight_on(); + + fsm.orig_x = fsm.cur_x; + fsm.orig_y = fsm.cur_y; + } + } break; + + default: + panicf("ft6x06: unhandled state"); + break; + } +} + +static void ft_i2c_callback(int status, i2c_descriptor* desc) +{ + (void)desc; + if(status != I2C_STATUS_OK) + return; + + /* The panel is oriented such that its X axis is vertical, + * so swap the axes for reporting */ + int evt = ftd.raw_data[1] >> 6; + int ty = ftd.raw_data[2] | ((ftd.raw_data[1] & 0xf) << 8); + int tx = ftd.raw_data[4] | ((ftd.raw_data[3] & 0xf) << 8); + + /* TODO: convert the touch positions to linear positions. + * + * Points reported by the touch controller are distorted and non-linear, + * ideally we'd like to correct these values. There's more precision in + * the middle of the touchpad than on the edges, so scrolling feels slow + * in the middle and faster near the edge. + */ + + ft_step_state(__ost_read32(), evt, tx, ty); +} + +void ft_interrupt(void) +{ + /* We don't care if this fails */ + i2c_async_queue(FT6x06_BUS, TIMEOUT_NOBLOCK, I2C_Q_ONCE, + ftd.i2c_cookie, &ftd.i2c_desc); +} + +static void ft_init(void) +{ + /* Initialize the driver state */ + ftd.i2c_cookie = i2c_async_reserve_cookies(FT6x06_BUS, 1); + ftd.i2c_desc.slave_addr = FT6x06_ADDR; + ftd.i2c_desc.bus_cond = I2C_START | I2C_STOP; + ftd.i2c_desc.tran_mode = I2C_READ; + ftd.i2c_desc.buffer[0] = &ftd.raw_data[5]; + ftd.i2c_desc.count[0] = 1; + ftd.i2c_desc.buffer[1] = &ftd.raw_data[0]; + ftd.i2c_desc.count[1] = 5; + ftd.i2c_desc.callback = ft_i2c_callback; + ftd.i2c_desc.arg = 0; + ftd.i2c_desc.next = NULL; + ftd.raw_data[5] = 0x02; + ftd.active = true; + touchpad_set_sensitivity(DEFAULT_TOUCHPAD_SENSITIVITY_SETTING); + + /* Initialize the state machine */ + fsm.buttons = 0; + fsm.state = STATE_IDLE; + fsm.last_event_t = 0; + fsm.scroll_press_t = 0; + fsm.samples = 0; + fsm.sum_x = fsm.sum_y = 0; + fsm.orig_x = fsm.orig_y = 0; + fsm.cur_x = fsm.cur_y = 0; + + /* Bring up I2C bus */ + i2c_x1000_set_freq(FT6x06_BUS, I2C_FREQ_400K); + + /* Reset chip */ + gpio_config(GPIO_B, FT_RST_PIN|FT_INT_PIN, GPIO_OUTPUT(0)); + mdelay(5); + gpio_out_level(GPIO_B, FT_RST_PIN, 1); + gpio_config(GPIO_B, FT_INT_PIN, GPIO_IRQ_EDGE(0)); + gpio_enable_irq(GPIO_B, FT_INT_PIN); +} + +void touchpad_set_sensitivity(int level) +{ + int pixels = 40; + pixels -= level; + ftd.scroll_thresh_sqr = pixels * pixels; +} + +void touchpad_enable_device(bool en) +{ + i2c_reg_write1(FT6x06_BUS, FT6x06_ADDR, 0xa5, en ? 0 : 3); + ftd.active = en; +} + +/* Value of headphone detect register */ +static uint8_t hp_detect_reg = 0x00; + +/* Interval to poll the register */ +#define HPD_POLL_TIME (HZ/2) + +static int hp_detect_tmo_cb(struct timeout* tmo) +{ + i2c_descriptor* d = (i2c_descriptor*)tmo->data; + i2c_async_queue(AXP173_BUS, TIMEOUT_NOBLOCK, I2C_Q_ADD, 0, d); + return HPD_POLL_TIME; +} + +static void hp_detect_init(void) +{ + static struct timeout tmo; + static const uint8_t gpio_reg = 0x94; + static i2c_descriptor desc = { + .slave_addr = AXP173_ADDR, + .bus_cond = I2C_START | I2C_STOP, + .tran_mode = I2C_READ, + .buffer[0] = (void*)&gpio_reg, + .count[0] = 1, + .buffer[1] = &hp_detect_reg, + .count[1] = 1, + .callback = NULL, + .arg = 0, + .next = NULL, + }; + + /* Headphone detect is wired to an undocumented GPIO on the AXP173. + * This sets it to input mode so we can see the pin state. */ + i2c_reg_write1(AXP173_BUS, AXP173_ADDR, 0x93, 0x01); + + /* Get an initial reading before startup */ + int r = i2c_reg_read1(AXP173_BUS, AXP173_ADDR, gpio_reg); + if(r >= 0) + hp_detect_reg = r; + + /* Poll the register every second */ + timeout_register(&tmo, &hp_detect_tmo_cb, HPD_POLL_TIME, (intptr_t)&desc); +} + +/* Rockbox interface */ +void button_init_device(void) +{ + /* Configure physical button GPIOs */ + gpio_config(GPIO_A, (1 << 17) | (1 << 19), GPIO_INPUT); + gpio_config(GPIO_B, (1 << 28) | (1 << 31), GPIO_INPUT); + + /* Initialize touchpad */ + ft_init(); + + /* Set up headphone detect polling */ + hp_detect_init(); +} + +int button_read_device(void) +{ + int r = fsm.buttons; + ft_step_state(__ost_read32(), EVENT_NONE, 0, 0); + + /* Read GPIOs for physical buttons */ + uint32_t a = REG_GPIO_PIN(GPIO_A); + uint32_t b = REG_GPIO_PIN(GPIO_B); + + /* All buttons are active low */ + if((a & (1 << 17)) == 0) r |= BUTTON_PLAY; + if((a & (1 << 19)) == 0) r |= BUTTON_VOL_UP; + if((b & (1 << 28)) == 0) r |= BUTTON_VOL_DOWN; + if((b & (1 << 31)) == 0) r |= BUTTON_POWER; + + return r; +} + +bool headphones_inserted() +{ + return hp_detect_reg & 0x40 ? true : false; +} + +#ifndef BOOTLOADER +static int getbtn(void) +{ + int btn; + do { + btn = button_get_w_tmo(1); + } while(btn & (BUTTON_REL|BUTTON_REPEAT)); + return btn; +} + +bool dbg_fiiom3k_touchpad(void) +{ + static const char* fsm_statenames[] = { + "IDLE", "PRESS", "REPORT", "SCROLL_PRESS", "SCROLLING" + }; + + do { + int line = 0; + lcd_clear_display(); + lcd_putsf(0, line++, "state: %s", fsm_statenames[fsm.state]); + lcd_putsf(0, line++, "button: %08x", fsm.buttons); + lcd_putsf(0, line++, "pos x: %4d orig x: %4d", fsm.cur_x, fsm.orig_x); + lcd_putsf(0, line++, "pos y: %4d orig y: %4d", fsm.cur_y, fsm.orig_y); + lcd_update(); + } while(getbtn() != BUTTON_POWER); + return false; +} +#endif -- cgit v1.2.3