From 4c60bc9e681865fcfc149775a1ed7ccd2613d5bf Mon Sep 17 00:00:00 2001 From: Aidan MacDonald Date: Sun, 23 May 2021 17:30:58 +0100 Subject: New port: Shanling Q1 native - Audio playback works - Touchscreen and buttons work - Bootloader works and is capable of dual boot - Plugins are working - Cabbiev2 theme has been ported - Stable for general usage Thanks to Marc Aarts for porting Cabbiev2 and plugin bitmaps. There's a few minor known issues: - Bootloader must be installed manually using 'usbboot' as there is no support in jztool yet. - Keymaps may be lacking, need further testing and feedback. - Some plugins may not be fully adapted to the screen size and could benefit from further tweaking. - LCD shows abnormal effects under some circumstances: for example, after viewing a mostly black screen an afterimage appears briefly when going back to a brightly-lit screen. Sudden power-off without proper shutdown of the backlight causes a "dissolving" effect. - CW2015 battery reporting driver is buggy, and disabled for now. Battery reporting is currently voltage-based using the AXP192. Change-Id: I635e83f02a880192c5a82cb0861ad3a61c137c3a --- firmware/drivers/audio/es9218.c | 226 ++++++++++++++++++++++++++++++++++++++++ firmware/drivers/cw2015.c | 191 +++++++++++++++++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 firmware/drivers/audio/es9218.c create mode 100644 firmware/drivers/cw2015.c (limited to 'firmware/drivers') diff --git a/firmware/drivers/audio/es9218.c b/firmware/drivers/audio/es9218.c new file mode 100644 index 0000000000..76d387221a --- /dev/null +++ b/firmware/drivers/audio/es9218.c @@ -0,0 +1,226 @@ +/*************************************************************************** + * __________ __ ___. + * 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 "audiohw.h" +#include "system.h" +#include "i2c-async.h" + +struct es9218_state { + enum es9218_clock_gear clk_gear; + uint32_t fsr; + uint32_t nco; +}; + +static struct es9218_state es9218; + +void es9218_open(void) +{ + /* Enable power supply */ + es9218_set_power_pin(1); + + /* "Wiggle" reset pin to get the internal oscillator to stabilize. + * This should also work if using an external powered oscillator, + * although in that case it's unnecessary to do this dance. */ + es9218_set_reset_pin(1); + udelay(75); + es9218_set_reset_pin(0); + udelay(50); + es9218_set_reset_pin(1); + mdelay(2); + + /* Initialize driver state */ + es9218.clk_gear = ES9218_CLK_GEAR_1; + es9218.fsr = 0; + es9218.nco = 0; +} + +void es9218_close(void) +{ + /* Turn off power supply */ + es9218_set_power_pin(0); + es9218_set_reset_pin(0); +} + +static void recalc_nco(void) +{ + /* nco * CLK * + * fsr = --------- * + * 2**32 */ + + uint32_t clk = es9218_get_mclk(); + clk >>= (int)es9218.clk_gear; + + uint64_t nco64 = es9218.fsr; + nco64 <<= 32; + nco64 /= clk; + + /* let's just ignore overflow... */ + uint32_t nco = nco64; + if(nco != es9218.nco) { + es9218.nco = nco; + + /* registers must be written in this order */ + es9218_write(ES9218_REG_PROG_NCO_BIT0_7, (nco >> 0) & 0xff); + es9218_write(ES9218_REG_PROG_NCO_BIT8_15, (nco >> 8) & 0xff); + es9218_write(ES9218_REG_PROG_NCO_BIT16_23, (nco >> 16) & 0xff); + es9218_write(ES9218_REG_PROG_NCO_BIT24_31, (nco >> 24) & 0xff); + } +} + +void es9218_set_clock_gear(enum es9218_clock_gear gear) +{ + if(gear != es9218.clk_gear) { + es9218.clk_gear = gear; + es9218_update(ES9218_REG_SYSTEM, 0x0c, (uint8_t)(gear & 3) << 2); + recalc_nco(); + } +} + +void es9218_set_nco_frequency(uint32_t fsr) +{ + if(fsr != es9218.fsr) { + es9218.fsr = fsr; + recalc_nco(); + } +} + +void es9218_recompute_nco(void) +{ + recalc_nco(); +} + +void es9218_set_amp_mode(enum es9218_amp_mode mode) +{ + es9218_update(ES9218_REG_AMP_CONFIG, 0x03, (uint8_t)mode & 3); +} + +void es9218_set_amp_powered(bool en) +{ + /* this doesn't seem to be necessary..? */ + es9218_update(ES9218_REG_ANALOG_CTRL, 0x40, en ? 0x40 : 0x00); +} + +void es9218_set_iface_role(enum es9218_iface_role role) +{ + /* asrc is used to lock onto the incoming audio frequency and is + * only used in aysnchronous slave mode. In synchronous operation, + * including master mode, it can be disabled to save power. */ + int asrc_en = (role == ES9218_IFACE_ROLE_SLAVE ? 1 : 0); + int master_mode = (role == ES9218_IFACE_ROLE_MASTER ? 1 : 0); + + es9218_update(ES9218_REG_MASTER_MODE_CONFIG, 1 << 7, master_mode << 7); + es9218_update(ES9218_REG_GENERAL_CONFIG, 1 << 7, asrc_en << 7); +} + +void es9218_set_iface_format(enum es9218_iface_format fmt, + enum es9218_iface_bits bits) +{ + uint8_t val = 0; + val |= ((uint8_t)bits & 3) << 6; + val |= ((uint8_t)fmt & 3) << 4; + /* keep low 4 bits zero -> use normal I2S mode, disable DSD mode */ + es9218_write(ES9218_REG_INPUT_SEL, val); +} + +static int dig_vol_to_hw(int x) +{ + x = MIN(x, ES9218_DIG_VOLUME_MAX); + x = MAX(x, ES9218_DIG_VOLUME_MIN); + return 0xff - (x - ES9218_DIG_VOLUME_MIN) / ES9218_DIG_VOLUME_STEP; +} + +static int amp_vol_to_hw(int x) +{ + x = MIN(x, ES9218_AMP_VOLUME_MAX); + x = MAX(x, ES9218_AMP_VOLUME_MIN); + return 24 - (x - ES9218_AMP_VOLUME_MIN) / ES9218_AMP_VOLUME_STEP; +} + +void es9218_set_dig_volume(int vol_l, int vol_r) +{ + es9218_write(ES9218_REG_VOLUME_LEFT, dig_vol_to_hw(vol_l)); + es9218_write(ES9218_REG_VOLUME_RIGHT, dig_vol_to_hw(vol_r)); +} + +void es9218_set_amp_volume(int vol) +{ + es9218_update(ES9218_REG_ANALOG_VOL, 0x1f, amp_vol_to_hw(vol)); +} + +void es9218_mute(bool en) +{ + es9218_update(ES9218_REG_FILTER_SYS_MUTE, 1, en ? 1 : 0); +} + +void es9218_set_filter(enum es9218_filter_type filt) +{ + es9218_update(ES9218_REG_FILTER_SYS_MUTE, 0xe0, ((int)filt & 7) << 5); +} + +void es9218_set_automute_time(int time) +{ + if(time < 0) time = 0; + if(time > 255) time = 255; + es9218_write(ES9218_REG_AUTOMUTE_TIME, time); +} + +void es9218_set_automute_level(int dB) +{ + es9218_update(ES9218_REG_AUTOMUTE_LEVEL, 0x7f, dB); +} + +void es9218_set_automute_fast_mode(bool en) +{ + es9218_update(ES9218_REG_MIX_AUTOMUTE, 0x10, en ? 0x10 : 0x00); +} + +void es9218_set_dpll_bandwidth(int knob) +{ + es9218_update(ES9218_REG_ASRC_DPLL_BANDWIDTH, 0xf0, (knob & 0xf) << 4); +} + +void es9218_set_thd_compensation(bool en) +{ + es9218_update(ES9218_REG_THD_COMP_BYPASS, 0x40, en ? 0x40 : 0); +} + +void es9218_set_thd_coeffs(uint16_t c2, uint16_t c3) +{ + es9218_write(ES9218_REG_THD_COMP_C2_LO, c2 & 0xff); + es9218_write(ES9218_REG_THD_COMP_C2_HI, (c2 >> 8) & 0xff); + es9218_write(ES9218_REG_THD_COMP_C3_LO, c3 & 0xff); + es9218_write(ES9218_REG_THD_COMP_C3_HI, (c3 >> 8) & 0xff); +} + +int es9218_read(int reg) +{ + return i2c_reg_read1(ES9218_BUS, ES9218_ADDR, reg); +} + +void es9218_write(int reg, uint8_t val) +{ + i2c_reg_write1(ES9218_BUS, ES9218_ADDR, reg, val); +} + +void es9218_update(int reg, uint8_t msk, uint8_t val) +{ + i2c_reg_modify1(ES9218_BUS, ES9218_ADDR, reg, msk, val, NULL); +} diff --git a/firmware/drivers/cw2015.c b/firmware/drivers/cw2015.c new file mode 100644 index 0000000000..705ca16e22 --- /dev/null +++ b/firmware/drivers/cw2015.c @@ -0,0 +1,191 @@ +/*************************************************************************** + * __________ __ ___. + * 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 "cw2015.h" +#include "i2c-async.h" +#include +#include "system.h" + +/* Headers for the debug menu */ +#ifndef BOOTLOADER +# include "action.h" +# include "list.h" +# include +#endif + +/* Battery profile info is an opaque blob. According to this, + * https://lore.kernel.org/linux-pm/20200503154855.duwj2djgqfiyleq5@earth.universe/T/#u + * the blob only comes from Cellwise testing a physical battery and cannot be + * obtained any other way. It's specific to a given battery so each target has + * its own profile. + * + * Profile data seems to be retained on the chip so it's not a hard requirement + * to define this. Provided you don't lose power in the meantime, it should be + * enough to just boot the OF, then boot Rockbox and read out the battery info + * from the CW2015 debug screen. + */ +#if defined(SHANLING_Q1) +static const uint8_t device_batinfo[CW2015_SIZE_BATINFO] = { + 0x15, 0x7E, 0x61, 0x59, 0x57, 0x55, 0x56, 0x4C, + 0x4E, 0x4D, 0x50, 0x4C, 0x45, 0x3A, 0x2D, 0x27, + 0x22, 0x1E, 0x19, 0x1E, 0x2A, 0x3C, 0x48, 0x45, + 0x1D, 0x94, 0x08, 0xF6, 0x15, 0x29, 0x48, 0x51, + 0x5D, 0x60, 0x63, 0x66, 0x45, 0x1D, 0x83, 0x38, + 0x09, 0x43, 0x16, 0x42, 0x76, 0x98, 0xA5, 0x1B, + 0x41, 0x76, 0x99, 0xBF, 0x80, 0xC0, 0xEF, 0xCB, + 0x2F, 0x00, 0x64, 0xA5, 0xB5, 0x0E, 0x30, 0x29, +}; +#else +# define NO_BATINFO +#endif + +static uint8_t chip_batinfo[CW2015_SIZE_BATINFO]; + +/* TODO: Finish implementing this + * + * Although this chip might give a better battery estimate than voltage does, + * the mainline linux driver has a lot of weird hacks due to oddities like the + * SoC getting stuck during charging, and from limited testing it seems this + * may occur for the Q1 too. + */ + +static int cw2015_read_bat_info(uint8_t* data) +{ + for(int i = 0; i < CW2015_SIZE_BATINFO; ++i) { + int r = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_BATINFO + i); + if(r < 0) + return r; + + data[i] = r & 0xff; + } + + return 0; +} + +void cw2015_init(void) +{ + /* mdelay(100); */ + int rc = cw2015_read_bat_info(&chip_batinfo[0]); + if(rc < 0) + memset(chip_batinfo, 0, sizeof(chip_batinfo)); +} + +int cw2015_get_vcell(void) +{ + int vcell_msb = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_VCELL); + int vcell_lsb = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_VCELL+1); + + if(vcell_msb < 0 || vcell_lsb < 0) + return -1; + + /* 14 bits, resolution 305 uV */ + int v_raw = ((vcell_msb & 0x3f) << 8) | vcell_lsb; + return v_raw * 61 / 200; +} + +int cw2015_get_soc(void) +{ + int soc_msb = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_SOC); + + if(soc_msb < 0) + return -1; + + /* MSB is the state of charge in percentage. + * the LSB contains fractional information not useful to Rockbox. */ + return soc_msb & 0xff; +} + +int cw2015_get_rrt(void) +{ + int rrt_msb = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_RRT_ALERT); + int rrt_lsb = i2c_reg_read1(CW2015_BUS, CW2015_ADDR, CW2015_REG_RRT_ALERT+1); + + if(rrt_msb < 0 || rrt_lsb < 0) + return -1; + + /* 13 bits, resolution 1 minute */ + return ((rrt_msb & 0x1f) << 8) | rrt_lsb; +} + +const uint8_t* cw2015_get_bat_info(void) +{ + return &chip_batinfo[0]; +} + +#ifndef BOOTLOADER +enum { + CW2015_DEBUG_VCELL = 0, + CW2015_DEBUG_SOC, + CW2015_DEBUG_RRT, + CW2015_DEBUG_BATINFO, + CW2015_DEBUG_BATINFO_LAST = CW2015_DEBUG_BATINFO + 7, + CW2015_DEBUG_NUM_ENTRIES, +}; + +static int cw2015_debug_menu_cb(int action, struct gui_synclist* lists) +{ + (void)lists; + + if(action == ACTION_NONE) + action = ACTION_REDRAW; + + return action; +} + +static const char* cw2015_debug_menu_get_name(int item, void* data, + char* buf, size_t buflen) +{ + (void)data; + + /* hexdump of battery info */ + if(item >= CW2015_DEBUG_BATINFO && item <= CW2015_DEBUG_BATINFO_LAST) { + int i = item - CW2015_DEBUG_BATINFO; + const uint8_t* batinfo = cw2015_get_bat_info(); + snprintf(buf, buflen, "BatInfo%d: %02x %02x %02x %02x %02x %02x %02x %02x", i, + batinfo[8*i + 0], batinfo[8*i + 1], batinfo[8*i + 2], batinfo[8*i + 3], + batinfo[8*i + 4], batinfo[8*i + 5], batinfo[8*i + 6], batinfo[8*i + 7]); + return buf; + } + + switch(item) { + case CW2015_DEBUG_VCELL: + snprintf(buf, buflen, "VCell: %d mV", cw2015_get_vcell()); + return buf; + case CW2015_DEBUG_SOC: + snprintf(buf, buflen, "SOC: %d%%", cw2015_get_soc()); + return buf; + case CW2015_DEBUG_RRT: + snprintf(buf, buflen, "Runtime: %d min", cw2015_get_rrt()); + return buf; + default: + return "---"; + } +} + +bool cw2015_debug_menu(void) +{ + struct simplelist_info info; + simplelist_info_init(&info, "CW2015 debug", CW2015_DEBUG_NUM_ENTRIES, NULL); + info.action_callback = cw2015_debug_menu_cb; + info.get_name = cw2015_debug_menu_get_name; + return simplelist_show_list(&info); +} +#endif -- cgit v1.2.3