ipodnano3g: power, RTC, backlight, battery and audio

Fills in the stubs the Nano 3G port was left with, taking each from how
the original firmware drives the same hardware.

Testing evidence: firmware/target/arm/s5l8702/ipodnano3g/TESTING.md.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Change-Id: Iea5314769502f5941a176600869591756c5e3233
This commit is contained in:
Andrew Rice 2026-09-19 07:52:18 +10:00
parent e4c010be98
commit 76f8925d23
18 changed files with 286 additions and 129 deletions

View file

@ -426,7 +426,7 @@ static const char graphic_numeric[] = "graphic,numeric";
# define DEFAULT_REC_MIC_GAIN 0
# define DEFAULT_REC_LEFT_GAIN 0
# define DEFAULT_REC_RIGHT_GAIN 0
# elif defined(HAVE_WM8975)
# elif defined(HAVE_WM8975) || defined(HAVE_WM1870)
# define DEFAULT_REC_MIC_GAIN 16
# define DEFAULT_REC_LEFT_GAIN 0
# define DEFAULT_REC_RIGHT_GAIN 0

View file

@ -575,7 +575,7 @@ drivers/audio/wm8740.c
drivers/audio/wm8751.c
#elif defined(HAVE_WM8978)
drivers/audio/wm8978.c
#elif defined(HAVE_WM8975)
#elif defined(HAVE_WM8975) || defined(HAVE_WM1870)
drivers/audio/wm8975.c
#elif defined(HAVE_WM8985)
drivers/audio/wm8985.c
@ -1647,7 +1647,7 @@ target/arm/s5l8702/ipodnano3g/serial-nano3g.c
#endif
#ifndef BOOTLOADER
target/arm/s5l8702/ipodnano3g/audio-nano3g.c
target/arm/s5l8702/ipodnano3g/cscodec-nano3g.c // TODO: -> wmcodec-nano3g.c
target/arm/s5l8702/ipodnano3g/wmcodec-nano3g.c
#endif
#endif /* IPOD_NANO3G */

View file

@ -25,6 +25,12 @@
*
****************************************************************************/
/*
* Wolfson WM8975 codec driver. It also covers the WM1870 fitted to the iPod
* Nano 3G (HAVE_WM1870), which answers the same registers and adds a few
* above them; the differences are behind that define.
*/
#include "logf.h"
#include "system.h"
#include "kernel.h"
@ -34,6 +40,9 @@
#include "wmcodec.h"
#include "audiohw.h"
#ifdef HAVE_WM1870
#include "pcm_sampr.h"
#endif
static unsigned short wm8975_regs[WM8975_NUM_REGISTERS] =
{
@ -126,7 +135,35 @@ void audiohw_postinit(void)
wm8975_write(PWRMGMT1, wm8975_regs[PWRMGMT1]);
audiohw_mute(false);
}
#else /* !IPOD_NANO2G */
#elif defined(HAVE_WM1870)
/* The WM1870, set up as the original firmware does it. It answers the
* WM8975's registers and a few above them. */
void audiohw_preinit(void)
{
wm8975_write(RESET, RESET_RESET);
wm8975_write(PWRMGMT1, PWRMGMT1_VMIDSEL_50K | PWRMGMT1_VREF);
wm8975_write(AINTFCE, AINTFCE_MS | AINTFCE_LRP_I2S_RLO
| AINTFCE_IWL_16BIT | AINTFCE_FORMAT_I2S);
wm8975_write(PWRMGMT2, PWRMGMT2_DACL | PWRMGMT2_DACR
| PWRMGMT2_LOUT1 | PWRMGMT2_ROUT1);
wm8975_write(LOUTMIX1, LOUTMIX1_LD2LO | LOUTMIX1_LI2LOVOL(2)
| LOUTMIX1_LMIXSEL_LADCIN);
wm8975_write(ROUTMIX2, ROUTMIX2_RD2RO | ROUTMIX2_RI2ROVOL(2));
wm8975_write(ROUTMIX1, ROUTMIX1_RMIXSEL_RADCIN);
wmcodec_write(WM1870_R67, 0x008);
wm8975_write(ADDCTRL2, ADDCTRL2_OUT3SW_MONOOUT | ADDCTRL2_LRCM);
wm8975_write(ADDCTRL3, ADDCTRL3_VROI);
}
void audiohw_postinit(void)
{
audiohw_mute(false);
}
#else /* !IPOD_NANO2G && !HAVE_WM1870 */
void audiohw_preinit(void)
{
/* POWER UP SEQUENCE */
@ -240,7 +277,15 @@ void audiohw_close(void)
/* Note: Disable output before calling this function */
void audiohw_set_frequency(int fsel)
{
#ifdef HAVE_WM1870
/* The rate codes depend on the MCLK, so the target supplies them */
unsigned short sampctrl = wmcodec_sampctrl(hw_freq_sampr[fsel]);
if (sampctrl)
wm8975_write(SAMPCTRL, sampctrl);
#else
(void)fsel;
#endif
}
#ifdef HAVE_RECORDING

View file

@ -169,7 +169,7 @@ struct sound_settings_info
#include "wm8751.h"
#elif defined(HAVE_WM8978)
#include "wm8978.h"
#elif defined(HAVE_WM8975)
#elif defined(HAVE_WM8975) || defined(HAVE_WM1870)
#include "wm8975.h"
#elif defined(HAVE_WM8985)
#include "wm8985.h"

View file

@ -138,22 +138,17 @@
#define HAVE_MORSE_INPUT
/* define this if you have a real-time clock */
// TODO
//#define CONFIG_RTC RTC_NANO3G
#define CONFIG_RTC 0
#define CONFIG_RTC RTC_NANO3G
/* Define if the device can wake from an RTC alarm */
//#define HAVE_RTC_ALARM
#define CONFIG_LCD LCD_IPOD6GNANO3G4G
// TODO
#if 0
/* Define the type of audio codec */
#define HAVE_WM8975
#endif
// XXX: dummy for preliminary build, WRONG CODEC!!!
#define HAVE_CS42L55
/* Define the type of audio codec. The Nano 3G has a Wolfson WM1870 at I2C
* address 0x34, which the original firmware drives as a WM8975 plus a few
* registers above the WM8975's range, so wm8975.c covers it. */
#define HAVE_WM1870
#define HAVE_PCM_DMA_ADDRESS

View file

@ -313,4 +313,7 @@ void audiohw_enable_lineout(bool enable);
#define WM8975_88200HZ 0x7f
#define WM8975_96000HZ 0x5d
/* The WM1870 (HAVE_WM1870) answers these registers and a few above them */
#define WM1870_R67 0x43
#endif /* _WM8975_H */

View file

@ -20,3 +20,9 @@
void wmcodec_write(int reg, int data);
#ifdef HAVE_WM1870
/* SAMPCTRL for a sample rate, which depends on the MCLK the target feeds
* the codec. Zero if the target cannot make that rate. */
unsigned short wmcodec_sampctrl(unsigned long rate);
#endif

View file

@ -425,8 +425,13 @@ bool headphones_inserted(void)
#if CONFIG_CPU==S5L8701
return ((PDAT14 & (1 << 5)) != 0);
#elif CONFIG_CPU==S5L8702 || CONFIG_CPU==S5L8720
#ifdef IPOD_NANO3G
/* GPIO 7.0, high with headphones in (measured) */
return (PDAT(7) & (1 << 0)) != 0;
#else
return ((PDAT10 & (1 << 6)) != 0);
#endif
#endif
}
#endif

View file

@ -21,3 +21,13 @@ Tested on the 4GB unit from DFU: host sees "Nano 3G NAND", 2113537 x
2048-byte sectors, write protect on. `nandcheck.py collect` reads it in
seven minutes on Linux and Windows, no ECC failures or timeouts, and the
archive passes the host FTL suite.
## Power, RTC, backlight, battery, audio
Tested on the 4GB unit: 44.1 kHz WAV plays with elapsed time running;
battery reads 4077 mV, charging true/false correctly with a cell below
full; a time set in Rockbox survives a reboot; backlight and brightness
work; "pause on headphone unplug" pauses on disconnect.
Not tested: sample rates other than 44.1 kHz, and the charge-complete
status bits (never seen set).

View file

@ -27,17 +27,51 @@
#include "pmu-target.h"
#include "kernel.h"
/* Only the battery input is known: the original firmware selects it with
* 0x24 and converts with 2500 mV + raw * 2000 mV / 1023 (measured: 4062 mV
* on battery, 4110 mV charging). The USB data and accessory inputs are not
* identified yet and read as 0. */
static const struct pmu_adc_channel adc_channels[] =
{
[ADC_BATTERY] =
{
.name = "Battery",
.mux = 0x24,
.samples = 4,
.offset_mv = 2500,
.span_mv = 2000,
},
[ADC_USBDATA] =
{
.name = "USB data",
},
[ADC_ACCESSORY] =
{
.name = "Accessory",
},
};
unsigned short adc_read_millivolts(int channel)
{
const struct pmu_adc_channel *ch = &adc_channels[channel];
if (!ch->samples)
return 0;
return pmu_adc_raw2mv(ch, pmu_read_adc(ch));
}
/* Returns battery voltage [millivolts] */
unsigned int adc_read_battery_voltage(void)
{
return 0;
return adc_read_millivolts(ADC_BATTERY);
}
/* API functions */
unsigned short adc_read(int channel)
{
(void) channel;
return 0;
const struct pmu_adc_channel *ch = &adc_channels[channel];
return ch->samples ? pmu_read_adc(ch) : 0;
}
int adc_read_accessory_resistor(void)
@ -53,8 +87,7 @@ unsigned int adc_read_usbdata_voltage(bool dp)
const char *adc_name(int channel)
{
(void) channel;
return "";
return adc_channels[channel].name;
}
void adc_init(void)

View file

@ -30,7 +30,7 @@
#include "lcd-s5l8702.h"
#endif
// TODO: test
/* The original firmware writes the level to bits 0..4 as well */
void backlight_hw_brightness(int brightness)
{
pmu_write(D1671_REG_LEDCTL,
@ -43,8 +43,11 @@ void backlight_hw_on(void)
if (!lcd_active())
lcd_awake();
#endif
/* As the original firmware turns it on: bits 7 and 6 set,
* bit 5 clear. Bit 6's meaning is unknown. */
pmu_write(D1671_REG_LEDCTL,
(pmu_read(D1671_REG_LEDCTL) | D1671_LEDCTL_ENABLE));
(pmu_read(D1671_REG_LEDCTL) & ~0x20)
| D1671_LEDCTL_ENABLE | D1671_LEDCTL_UNKNOWN);
}
void backlight_hw_off(void)

View file

@ -1,67 +0,0 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* S5L8702-specific code for Cirrus codecs
*
* Copyright (c) 2010 Michael Sparmann
*
* 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 "system.h"
#include "audiohw.h"
#include "i2c-s5l8702.h"
#include "s5l87xx.h"
#include "cscodec.h"
void audiohw_init(void)
{
#ifdef HAVE_CS42L55
audiohw_preinit();
#endif
}
unsigned char cscodec_read(int reg)
{
// TODO: not tested
unsigned char data;
i2c_read(0, 0x94, reg, 1, &data);
return data;
}
void cscodec_write(int reg, unsigned char data)
{
// XXX: not tested
i2c_write(0, 0x94, reg, 1, &data);
}
void cscodec_power(bool state)
{
(void)state; //TODO: Figure out which LDO this is
}
void cscodec_reset(bool state)
{
// XXX: not tested
if (state) PDAT(3) &= ~8;
else PDAT(3) |= 8;
}
void cscodec_clock(bool state)
{
// XXX: not tested
if (state) CLKCON3 &= ~0xffff;
else CLKCON3 |= 0x8000;
}

View file

@ -112,16 +112,26 @@ void pmu_set_cpu_voltage(bool high)
#endif
#if (CONFIG_RTC == RTC_NANO3G)
/* The RTC, as the original firmware uses it:
* registers 0x40..0x45 hold seconds, minutes, hours, day, month and year
* (since 2000), in binary. Bit 6 of the seconds register is set whenever
* the time is written and masked off when it is read. The buffer holds the
* six values in that order. */
void pmu_read_rtc(unsigned char* buffer)
{
// TODO
(void) buffer;
pmu_read_multiple(D1671_REG_RTCSEC, 6, buffer);
buffer[0] &= ~D1671_RTCSEC_SET;
}
void pmu_write_rtc(unsigned char* buffer)
{
// TODO
(void) buffer;
int i;
/* One register at a time, seconds first, as the original firmware
* does */
for (i = 0; i < 6; i++)
pmu_write(D1671_REG_RTCSEC + i,
buffer[i] | (i == 0 ? D1671_RTCSEC_SET : 0));
}
#endif
@ -132,7 +142,10 @@ void pmu_set_usblimit(bool fast_charge)
}
/*
* ADC
* ADC, as the original firmware drives it: write the channel's
* input selection to register 0x30 with bit 3 set to start a conversion,
* wait for bit 3 to clear, and read the 10-bit result from 0x32 (bits 9..2)
* and 0x31 (bits 1..0). 0x30 idles at 0x20.
*/
static struct mutex pmu_adc_mutex;
@ -140,21 +153,31 @@ static struct mutex pmu_adc_mutex;
unsigned short pmu_adc_raw2mv(
const struct pmu_adc_channel *ch, unsigned short raw)
{
// TODO
(void) ch;
(void) raw;
return 0;
return ch->offset_mv + raw * ch->span_mv / 1023;
}
/* returns raw value */
/* returns raw value, averaged over ch->samples conversions */
unsigned short pmu_read_adc(const struct pmu_adc_channel *ch)
{
// TODO
int raw = 0;
unsigned int sum = 0;
int i, tries;
mutex_lock(&pmu_adc_mutex);
(void) ch;
for (i = 0; i < ch->samples; i++)
{
pmu_write(D1671_REG_ADCCTL, ch->mux | D1671_ADCCTL_START);
for (tries = 0; tries < 20; tries++)
{
udelay(50);
if (!(pmu_read(D1671_REG_ADCCTL) & D1671_ADCCTL_START))
break;
}
sum += (pmu_read(D1671_REG_ADCHI) << 2)
| (pmu_read(D1671_REG_ADCLO) & 3);
}
pmu_write(D1671_REG_ADCCTL, D1671_ADCCTL_IDLE);
mutex_unlock(&pmu_adc_mutex);
return raw;
return ch->samples ? sum / ch->samples : 0;
}
/*

View file

@ -37,6 +37,9 @@ enum d1671_regs {
D1671_REG_SYSCTRLA = 0x08,
D1671_REG_LEDCTL = 0x20,
D1671_REG_CHCTL = 0x21, // TBC
D1671_REG_ADCCTL = 0x30,
D1671_REG_ADCLO = 0x31,
D1671_REG_ADCHI = 0x32,
D1671_REG_RTCSEC = 0x40, // TBC
};
@ -87,13 +90,26 @@ enum d1671_reg_chctl {
D1671_CHCTL_FASTCHRG = 0x01, /* 100/500mA USB limit */
};
enum d1671_reg_rtcsec {
/* set when the time is written, masked off when it is read */
D1671_RTCSEC_SET = 0x40,
};
enum d1671_reg_adcctl {
D1671_ADCCTL_START = 0x08,
D1671_ADCCTL_IDLE = 0x20, /* register's value at rest */
};
/* GPIO for external PMU interrupt */
#define GPIO_EINT_PMU 0x7b
struct pmu_adc_channel
{
const char *name;
// TODO
uint8_t mux; /* register 0x30 value selecting the input */
uint8_t samples; /* conversions averaged, 0 = not readable */
unsigned short offset_mv; /* millivolts at raw 0 */
unsigned short span_mv; /* millivolts from raw 0 to raw 1023 */
};
void pmu_preinit(void);

View file

@ -58,9 +58,19 @@ unsigned int power_input_status(void)
return status;
}
/* As the original firmware decides it: the charger is
* enabled (CHCTL bits 1..6), not suspended (SYSCTRLA bit 2), and not done:
* STATUSB bits 1..2 clear. Measured: they read 0 while a 4.1 V cell
* charges from USB, so they are taken as the charge-complete indication;
* they have not been seen set. */
bool charging_state(void)
{
// TODO
if (!(power_input_status() & POWER_INPUT_CHARGER))
return false;
if (!(pmu_read(D1671_REG_CHCTL) & 0x7e))
return false;
if (pmu_read(D1671_REG_SYSCTRLA) & 0x04)
return false;
return !(pmu_read(D1671_REG_STATUSB) & 0x06);
}
#endif /* CONFIG_CHARGING */

View file

@ -24,6 +24,8 @@
#include "pmu-target.h"
#include "timefuncs.h"
/* The PMU keeps seconds, minutes, hours, day, month and year (since 2000)
* in binary, and no weekday; see pmu_read_rtc() */
void rtc_init(void)
{
@ -31,20 +33,16 @@ void rtc_init(void)
int rtc_read_datetime(struct tm *tm)
{
unsigned int i;
unsigned char buf[7];
unsigned char buf[6];
pmu_read_rtc(buf);
for (i = 0; i < sizeof(buf); i++)
buf[i] = BCD2DEC(buf[i]);
tm->tm_sec = buf[0];
tm->tm_min = buf[1];
tm->tm_hour = buf[2];
tm->tm_mday = buf[4];
tm->tm_mon = buf[5] - 1;
tm->tm_year = buf[6] + 100;
tm->tm_mday = buf[3];
tm->tm_mon = buf[4] - 1;
tm->tm_year = buf[5] + 100;
tm->tm_yday = 0; /* Not implemented for now */
set_day_of_week(tm);
@ -53,19 +51,14 @@ int rtc_read_datetime(struct tm *tm)
int rtc_write_datetime(const struct tm *tm)
{
unsigned int i;
unsigned char buf[7];
unsigned char buf[6];
buf[0] = tm->tm_sec;
buf[1] = tm->tm_min;
buf[2] = tm->tm_hour;
buf[3] = tm->tm_wday;
buf[4] = tm->tm_mday;
buf[5] = tm->tm_mon + 1;
buf[6] = tm->tm_year - 100;
for (i = 0; i < sizeof(buf); i++)
buf[i] = DEC2BCD(buf[i]);
buf[3] = tm->tm_mday;
buf[4] = tm->tm_mon + 1;
buf[5] = tm->tm_year - 100;
pmu_write_rtc(buf);
return 0;

View file

@ -0,0 +1,65 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
*
* iPod Nano 3G code for the Wolfson codec (WM1870)
*
* Copyright (c) 2010 Michael Sparmann
* Copyright (C) 2026 by Andrew Rice
*
* 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 "system.h"
#include "audiohw.h"
#include "i2c-s5l8702.h"
#include "wmcodec.h"
/* The PCM driver sets the codec up (audiohw_preinit()) and controls its
* MCLK */
void audiohw_init(void)
{
}
/* The codec runs from a 12 MHz MCLK here, so its rate codes are the USB
* mode ones, with a BCLK divider, as the original firmware sets them. */
unsigned short wmcodec_sampctrl(unsigned long rate)
{
static const struct
{
unsigned long rate;
unsigned short sampctrl;
} rates[] =
{
{ SAMPR_8, 0x10d }, { SAMPR_11, 0x133 }, { SAMPR_12, 0x111 },
{ SAMPR_16, 0x115 }, { SAMPR_22, 0x137 }, { SAMPR_24, 0x139 },
{ SAMPR_32, 0x119 }, { SAMPR_44, 0x123 }, { SAMPR_48, 0x081 },
{ SAMPR_88, 0x0bf }, { SAMPR_96, 0x09d },
};
unsigned int i;
for (i = 0; i < ARRAYLEN(rates); i++)
if (rates[i].rate == rate)
return rates[i].sampctrl;
return 0;
}
/* 7-bit register and 9-bit value, as two bytes to address 0x34 on I2C
* bus 0. The codec does not acknowledge reads. */
void wmcodec_write(int reg, int data)
{
unsigned char d = data & 0xff;
i2c_write(0, 0x34, (reg << 1) | ((data >> 8) & 1), 1, &d);
}

View file

@ -80,6 +80,18 @@ static struct dmac_ch_cfg dma_play_ch_cfg = {
#define CHUNK_MAX_BYTES (LLI_MAX_BYTES * 1)
#define WATERMARK_BYTES (PCM_WATERMARK * 4)
/* I2S0 TX setup and start command. The Nano 3G uses the values Apple's
* firmware writes for its WM1870, which is the I2S clock master; the
* ipod6g values have TXCON bits 3-4 and TXCOM bit 3 set as well, and with
* them I2S0 never takes the codec's BCLK/LRCK. */
#ifdef IPOD_NANO3G
#define I2STXCON_SETUP 0x0b100001
#define I2STXCOM_START 0x6
#else
#define I2STXCON_SETUP 0xb100019
#define I2STXCOM_START 0xe
#endif
static volatile int locked = 0;
static unsigned char dblbuf[2][WATERMARK_BYTES] CACHEALIGN_ATTR;
static int active_dblbuf;
@ -152,7 +164,7 @@ static void sink_dma_start(const void* addr, size_t size)
sink_dma_stop();
pcm_remaining = size;
I2STXCOM = 0xe;
I2STXCOM = I2STXCOM_START;
dma_play_callback((void*)addr);
}
@ -162,9 +174,16 @@ static void sink_dma_start(const void* addr, size_t size)
/* set the configured PCM frequency */
static void sink_set_freq(uint16_t freq)
{
#ifdef HAVE_CS42L55
static uint16_t last_clkcon3l = 0;
uint16_t clkcon3l;
#else
/* Not a valid setting, so the first call always programs MCLK. The
* CS42L55 driver starts MCLK itself; the WM8975 driver does not. */
static uint16_t last_clkcon3l = 0xffff;
#endif
uint16_t clkcon3l = 0; /* OSC0 -> 12 MHz */
#ifdef HAVE_CS42L55
/* For unknown reasons, s5l8702 I2S controller does not synchronize
* with CS42L55 at 32000 Hz. To fix it, the CODEC is configured with
* a sample rate of 48000 Hz and MCLK is decreased 1/3 to 8 Mhz,
@ -175,9 +194,7 @@ static void sink_set_freq(uint16_t freq)
freq = HW_FREQ_48;
clkcon3l = 0x3028; /* PLL2 / 3 / 9 -> 8 MHz */
}
else {
clkcon3l = 0; /* OSC0 -> 12 MHz */
}
#endif
/* configure MCLK */
/* TODO: maybe all CLKCON management should be moved to
@ -201,7 +218,7 @@ static void sink_dma_init(void)
dmac_ch_init(&dma_play_ch, &dma_play_ch_cfg);
I2STXCON = 0xb100019;
I2STXCON = I2STXCON_SETUP;
I2SCLKCON = 1;
audiohw_preinit();