088ebb5fac
- Added register names to reduce usage of magic numbers - Added function to control max charging current, needed for USB - Corrected comment about axp173, since FiiO M3K has an axp192 Change-Id: I6604ce8d44e5a2ee84061cf042d17ccc4734ac57
503 lines
14 KiB
C
503 lines
14 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* 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 "axp173.h"
|
|
#include "gpio-x1000.h"
|
|
#include "i2c-x1000.h"
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
|
|
#ifndef BOOTLOADER
|
|
# include "lcd.h"
|
|
# 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 = AXP192_REG_GPIOSTATE1;
|
|
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 AXP192 GPIO: set it to input state */
|
|
i2c_reg_write1(AXP173_BUS, AXP173_ADDR, AXP192_REG_GPIO2FUNCTION, 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
|