From 43eb5abcbd84d1b47890a6826869c8202b6ae01f Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 20:58:09 -0230 Subject: [PATCH 01/31] irqchip: add S5L8740 GPIO EIC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N31 GPIO interrupts go through an EIC at 0x39700000, then into the PL192 VIC EXT lines. Group is gpio >> 5. Chain only the parents listed in DT; wiring every EXT0..6 hung boot. GPIO 86 (PMIC nIRQ) is group 2 on VIC EXT3. Vol± stay on SoC GPIO and do not need this chip. Tested on iPod nano 7G. --- drivers/irqchip/Kconfig | 9 + drivers/irqchip/Makefile | 1 + drivers/irqchip/irq-s5l8740-eic.c | 349 ++++++++++++++++++++++++++++++ 3 files changed, 359 insertions(+) create mode 100755 drivers/irqchip/irq-s5l8740-eic.c diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig index c11b9965c4ad9b..cb191cf0679d74 100644 --- a/drivers/irqchip/Kconfig +++ b/drivers/irqchip/Kconfig @@ -56,6 +56,15 @@ config ARM_NVIC select IRQ_DOMAIN_HIERARCHY select GENERIC_IRQ_CHIP +config S5L8740_EIC + bool "S5L8740 GPIO External Interrupt Controller" + depends on OF && ARM_VIC + select IRQ_DOMAIN + help + GPIO EIC at 0x39700000 on Apple S5L8740 (iPod nano 7G). + Groups map to PL192 VIC EXT lines. Chain only the parents + listed in the device tree. + config ARM_VIC bool select IRQ_DOMAIN diff --git a/drivers/irqchip/Makefile b/drivers/irqchip/Makefile index 25e9ad29b8c4a5..7819656cb715d4 100644 --- a/drivers/irqchip/Makefile +++ b/drivers/irqchip/Makefile @@ -38,6 +38,7 @@ obj-$(CONFIG_PARTITION_PERCPU) += irq-partition-percpu.o obj-$(CONFIG_HISILICON_IRQ_MBIGEN) += irq-mbigen.o obj-$(CONFIG_ARM_NVIC) += irq-nvic.o obj-$(CONFIG_ARM_VIC) += irq-vic.o +obj-$(CONFIG_S5L8740_EIC) += irq-s5l8740-eic.o obj-$(CONFIG_ARMADA_370_XP_IRQ) += irq-armada-370-xp.o obj-$(CONFIG_ATMEL_AIC_IRQ) += irq-atmel-aic-common.o irq-atmel-aic.o obj-$(CONFIG_ATMEL_AIC5_IRQ) += irq-atmel-aic-common.o irq-atmel-aic5.o diff --git a/drivers/irqchip/irq-s5l8740-eic.c b/drivers/irqchip/irq-s5l8740-eic.c new file mode 100755 index 00000000000000..a791c71b4cf050 --- /dev/null +++ b/drivers/irqchip/irq-s5l8740-eic.c @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 GPIO External Interrupt Controller (EIC) @ 0x39700000 + * + * Topology (CONFIRMED_N31): + * GPIO → EIC → EXT line → PL192 VIC @ 0x38E00000 → CPU + * + * Registers (group g = 0..6): + * +0x80+4*g INTLEVEL 0=low, 1=high + * +0xA0+4*g INTSTAT W1C (edge) / status + * +0xC0+4*g INTEN + * +0xE0+4*g INTTYPE 0=edge, 1=level + * + * GPIO → (group, bit): group = gpio >> 5, bit = gpio & 31 + * Group g is parented to VIC EXT irq g (Nimbus GPIO38 → g=1 bit6 → VIC1). + * + * RetailOS: sub_7D490 (level), sub_40641C (type+enable+ack). + * + * #interrupt-cells = <2> via irq_domain_xlate_twocell (hwirq, flags). + * + * Chaining: only chain parents that appear in DT interrupts. The safe N31 + * config is a single parent — EXT1 (interrupts = <1>) for Nimbus GPIO38 / + * group1 — until multi-EXT chaining is HW-proven. Omitting interrupts = + * MMIO helper only (s5l8740_eic_enable_gpio / gpio_to_irq). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define EIC_NGROUPS 7 +#define EIC_GPIOS (EIC_NGROUPS * 32) + +#define EIC_INTLEVEL(g) (0x80 + 4 * (g)) +#define EIC_INTSTAT(g) (0xa0 + 4 * (g)) +#define EIC_INTEN(g) (0xc0 + 4 * (g)) +#define EIC_INTTYPE(g) (0xe0 + 4 * (g)) + +struct s5l8740_eic { + void __iomem *base; + struct irq_domain *domain; + int parent_irq[EIC_NGROUPS]; +}; + +static struct s5l8740_eic *s5l8740_eic_global; + +static void eic_mask(struct irq_data *d) +{ + struct s5l8740_eic *eic = irq_data_get_irq_chip_data(d); + unsigned int gpio = irqd_to_hwirq(d); + u32 g = gpio >> 5, bit = gpio & 31; + u32 en; + + en = readl(eic->base + EIC_INTEN(g)); + writel(en & ~BIT(bit), eic->base + EIC_INTEN(g)); +} + +static void eic_unmask(struct irq_data *d) +{ + struct s5l8740_eic *eic = irq_data_get_irq_chip_data(d); + unsigned int gpio = irqd_to_hwirq(d); + u32 g = gpio >> 5, bit = gpio & 31; + u32 en; + + /* ack sticky */ + writel(BIT(bit), eic->base + EIC_INTSTAT(g)); + en = readl(eic->base + EIC_INTEN(g)); + writel(en | BIT(bit), eic->base + EIC_INTEN(g)); +} + +/* + * Hardware INTLEVEL is one polarity. gpio-keys always requests EDGE_BOTH + * (IRQF_TRIGGER_RISING|FALLING) → -EINVAL without this. Flip polarity on + * each ack so press (falling, idle-high) and release (rising) both fire. + */ +static void eic_ack(struct irq_data *d) +{ + struct s5l8740_eic *eic = irq_data_get_irq_chip_data(d); + unsigned int gpio = irqd_to_hwirq(d); + u32 g = gpio >> 5, bit = gpio & 31; + + writel(BIT(bit), eic->base + EIC_INTSTAT(g)); + + if ((irqd_get_trigger_type(d) & IRQ_TYPE_SENSE_MASK) == + IRQ_TYPE_EDGE_BOTH) { + u32 level = readl(eic->base + EIC_INTLEVEL(g)); + + writel(level ^ BIT(bit), eic->base + EIC_INTLEVEL(g)); + } +} + +static int eic_set_type(struct irq_data *d, unsigned int type) +{ + struct s5l8740_eic *eic = irq_data_get_irq_chip_data(d); + unsigned int gpio = irqd_to_hwirq(d); + u32 g = gpio >> 5, bit = gpio & 31; + u32 level, itype; + unsigned int sense = type & IRQ_TYPE_SENSE_MASK; + + level = readl(eic->base + EIC_INTLEVEL(g)); + itype = readl(eic->base + EIC_INTTYPE(g)); + + switch (sense) { + case IRQ_TYPE_LEVEL_LOW: + level &= ~BIT(bit); /* 0 = low */ + itype |= BIT(bit); /* 1 = level */ + irq_set_handler_locked(d, handle_level_irq); + break; + case IRQ_TYPE_LEVEL_HIGH: + level |= BIT(bit); + itype |= BIT(bit); + irq_set_handler_locked(d, handle_level_irq); + break; + case IRQ_TYPE_EDGE_FALLING: + level &= ~BIT(bit); + itype &= ~BIT(bit); /* 0 = edge */ + irq_set_handler_locked(d, handle_edge_irq); + break; + case IRQ_TYPE_EDGE_RISING: + level |= BIT(bit); + itype &= ~BIT(bit); + irq_set_handler_locked(d, handle_edge_irq); + break; + case IRQ_TYPE_EDGE_BOTH: + /* Idle-high active-low keys: first event is falling. */ + level &= ~BIT(bit); + itype &= ~BIT(bit); + irq_set_handler_locked(d, handle_edge_irq); + break; + default: + return -EINVAL; + } + + writel(level, eic->base + EIC_INTLEVEL(g)); + writel(itype, eic->base + EIC_INTTYPE(g)); + return 0; +} + +static struct irq_chip s5l8740_eic_chip = { + .name = "s5l8740-eic", + .irq_ack = eic_ack, + .irq_mask = eic_mask, + .irq_unmask = eic_unmask, + .irq_set_type = eic_set_type, + .flags = IRQCHIP_MASK_ON_SUSPEND | IRQCHIP_SKIP_SET_WAKE | + IRQCHIP_SET_TYPE_MASKED, +}; + +static void eic_chained_handler(struct irq_desc *desc) +{ + struct s5l8740_eic *eic = irq_desc_get_handler_data(desc); + struct irq_chip *chip = irq_desc_get_chip(desc); + unsigned int g, parent = irq_desc_get_irq(desc); + u32 stat, en, pending; + unsigned int bit; + + chained_irq_enter(chip, desc); + + for (g = 0; g < EIC_NGROUPS; g++) { + if (eic->parent_irq[g] != parent) + continue; + stat = readl(eic->base + EIC_INTSTAT(g)); + en = readl(eic->base + EIC_INTEN(g)); + pending = stat & en; + for_each_set_bit(bit, (unsigned long *)&pending, 32) { + unsigned int virq = irq_find_mapping(eic->domain, + (g << 5) | bit); + static unsigned hits; + + if (hits < 8) { + hits++; + pr_debug("EIC hit g%u b%u virq=%u\n", g, bit, + virq); + } + if (virq) + generic_handle_irq(virq); + /* W1C ack */ + writel(BIT(bit), eic->base + EIC_INTSTAT(g)); + } + } + + chained_irq_exit(chip, desc); +} + +static int eic_domain_map(struct irq_domain *d, unsigned int irq, + irq_hw_number_t hwirq) +{ + irq_set_chip_and_handler(irq, &s5l8740_eic_chip, handle_level_irq); + irq_set_chip_data(irq, d->host_data); + irq_set_probe(irq); + return 0; +} + +static const struct irq_domain_ops eic_domain_ops = { + .map = eic_domain_map, + .xlate = irq_domain_xlate_twocell, +}; + +/* Export for early consumers (nimbus) before domain lookup */ +int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type) +{ + struct s5l8740_eic *eic = s5l8740_eic_global; + u32 g, bit; + u32 level, itype, en; + + if (!eic || gpio >= EIC_GPIOS) + return -ENODEV; + + g = gpio >> 5; + bit = gpio & 31; + + level = readl(eic->base + EIC_INTLEVEL(g)); + itype = readl(eic->base + EIC_INTTYPE(g)); + + if (irq_type & IRQ_TYPE_LEVEL_HIGH) + level |= BIT(bit); + else + level &= ~BIT(bit); /* default active-low */ + + if (irq_type & (IRQ_TYPE_EDGE_RISING | IRQ_TYPE_EDGE_FALLING)) + itype &= ~BIT(bit); + else + itype |= BIT(bit); /* level */ + + writel(level, eic->base + EIC_INTLEVEL(g)); + writel(itype, eic->base + EIC_INTTYPE(g)); + writel(BIT(bit), eic->base + EIC_INTSTAT(g)); + en = readl(eic->base + EIC_INTEN(g)); + writel(en | BIT(bit), eic->base + EIC_INTEN(g)); + return 0; +} +EXPORT_SYMBOL_GPL(s5l8740_eic_enable_gpio); + +/** + * s5l8740_eic_gpio_to_irq - create/return Linux IRQ for an SoC GPIO line + * @gpio: SoC GPIO number (group = gpio>>5, bit = gpio&31) + */ +int s5l8740_eic_gpio_to_irq(unsigned int gpio) +{ + struct s5l8740_eic *eic = s5l8740_eic_global; + int virq; + + if (!eic || !eic->domain || gpio >= EIC_GPIOS) + return -ENODEV; + + virq = irq_create_mapping(eic->domain, gpio); + if (!virq) + return -EINVAL; + return virq; +} +EXPORT_SYMBOL_GPL(s5l8740_eic_gpio_to_irq); + +static int s5l8740_eic_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_eic *eic; + int g, nirq, ngrp, i, ret, chained = 0; + + eic = devm_kzalloc(dev, sizeof(*eic), GFP_KERNEL); + if (!eic) + return -ENOMEM; + + eic->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(eic->base)) + return PTR_ERR(eic->base); + + /* Mask all, clear status (same as SEC pinmux_223C) */ + for (g = 0; g < EIC_NGROUPS; g++) { + writel(0, eic->base + EIC_INTEN(g)); + writel(0xffffffff, eic->base + EIC_INTSTAT(g)); + writel(0, eic->base + EIC_INTLEVEL(g)); + writel(0, eic->base + EIC_INTTYPE(g)); + eic->parent_irq[g] = -1; + } + + eic->domain = irq_domain_add_linear(dev->of_node, EIC_GPIOS, + &eic_domain_ops, eic); + if (!eic->domain) + return -ENOMEM; + + nirq = of_irq_count(dev->of_node); + if (nirq < 0) + nirq = 0; + if (nirq > EIC_NGROUPS) + nirq = EIC_NGROUPS; + + ngrp = of_property_count_u32_elems(dev->of_node, "apple,eic-groups"); + if (ngrp < 0) + ngrp = 0; + + /* + * Chain only the VIC EXTn parents listed in DT. Map each to an EIC + * group via apple,eic-groups (parallel to interrupts). GPIO 86 is + * group 2 (86>>5) on VIC EXT3 = 3 — do not chain every EXT0..6 + * (that hung boot). + */ + for (i = 0; i < nirq; i++) { + u32 group = i; + + ret = platform_get_irq(pdev, i); + if (ret < 0) + continue; + if (ngrp == nirq) + of_property_read_u32_index(dev->of_node, + "apple,eic-groups", i, + &group); + if (group >= EIC_NGROUPS) + continue; + eic->parent_irq[group] = ret; + irq_set_chained_handler_and_data(ret, eic_chained_handler, eic); + chained++; + dev_info(dev, "EIC group%u <- VIC irq %d\n", group, ret); + } + + s5l8740_eic_global = eic; + platform_set_drvdata(pdev, eic); + dev_info(dev, + "EIC @%pR groups=%d dt_irqs=%d chained_parents=%d (prefer EXT1-only)\n", + platform_get_resource(pdev, IORESOURCE_MEM, 0), EIC_NGROUPS, + nirq, chained); + return 0; +} + +static const struct of_device_id s5l8740_eic_of_match[] = { + { .compatible = "apple,s5l8740-eic" }, + { .compatible = "samsung,s5l8740-eic" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_eic_of_match); + +static struct platform_driver s5l8740_eic_driver = { + .probe = s5l8740_eic_probe, + .driver = { + .name = "s5l8740-eic", + .of_match_table = s5l8740_eic_of_match, + }, +}; +builtin_platform_driver(s5l8740_eic_driver); + +MODULE_DESCRIPTION("S5L8740 GPIO EIC (External Interrupt Controller)"); +MODULE_LICENSE("GPL"); From 970ebf6553127e29218ecb68d481bd2d5b9b4dda Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 20:58:21 -0230 Subject: [PATCH 02/31] gpio: add S5L8740 banked GPIO and GPIOCMD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RetailOS programs pads with a word at 0x3CF001E0: (bank << 16) | (pin << 8) | cmd DIN is bank+0x04, DIR is bank+0x14. The 2-line bcm6345 hack on 0x3CF000A4 is not enough for Vol± or for EIC to_irq. apple,skip-sec-pinmux leaves U-Boot/SEC leftovers alone and only GPIOCMDs the IIS0 pair plus GPIO 86. Replaying the full SEC table from Linux broke buttons and USB on glass. gpio-keys-polled on GPIO 40/41 issues mode 0xFFFE and the pads go quiet. This driver polls DIN itself. Tested on iPod nano 7G: Vol+ / Vol- report KEY_VOLUMEUP/DOWN. --- drivers/gpio/Kconfig | 11 +- drivers/gpio/Makefile | 1 + drivers/gpio/gpio-s5l8740.c | 538 ++++++++++++++++++++++++++++++ drivers/gpio/pinmux_table.inc | 25 ++ drivers/irqchip/irq-s5l8740-eic.c | 0 5 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 drivers/gpio/gpio-s5l8740.c create mode 100644 drivers/gpio/pinmux_table.inc mode change 100755 => 100644 drivers/irqchip/irq-s5l8740-eic.c diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig index 4622176f88cd09..b2a230c6d59f79 100644 --- a/drivers/gpio/Kconfig +++ b/drivers/gpio/Kconfig @@ -1333,7 +1333,16 @@ config GPIO_CS5535 If unsure, say N. -config GPIO_D1830 +config GPIO_S5L8740 + bool "Samsung/Apple S5L8740 banked GPIO" + depends on OF && GPIOLIB + select GPIO_GENERIC + help + Banked GPIO at 0x3CF00000 plus the GPIOCMD latch at +0x1E0. + Used on iPod nano 7G for Vol± and as the parent for EIC + to_irq. Do not use gpio-keys-polled on the Vol pads; that + path issues GPIOCMD 0xFFFE and the DIN lines stop moving. + tristate "Dialog Semiconductor D1830 PMIC GPIO (read-only, I2C)" depends on I2C depends on GPIOLIB diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile index 9062f4de56f2c5..5e4c5013566ad5 100644 --- a/drivers/gpio/Makefile +++ b/drivers/gpio/Makefile @@ -51,6 +51,7 @@ obj-$(CONFIG_GPIO_SNPS_CREG) += gpio-creg-snps.o obj-$(CONFIG_GPIO_CROS_EC) += gpio-cros-ec.o obj-$(CONFIG_GPIO_CRYSTAL_COVE) += gpio-crystalcove.o obj-$(CONFIG_GPIO_CS5535) += gpio-cs5535.o +obj-$(CONFIG_GPIO_S5L8740) += gpio-s5l8740.o obj-$(CONFIG_GPIO_D1830) += gpio-d1830.o obj-$(CONFIG_GPIO_DA9052) += gpio-da9052.o obj-$(CONFIG_GPIO_DA9055) += gpio-da9055.o diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c new file mode 100644 index 00000000000000..d4f4ccb5d2f793 --- /dev/null +++ b/drivers/gpio/gpio-s5l8740.c @@ -0,0 +1,538 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * GPIO driver for Samsung/Apple S5L8740 (iPod nano 7G / N31) + * + * Banked MMIO @ 0x3CF00000 (RetailOS / Rockbox): + * bank_base = base + 32 * (gpio >> 3) + * DIN = bank + 0x04 + * DOUT = bank + 0x08 + * DIR = bank + 0x14 (bit set on pinmux/mode!=1; cleared on mode 0xFFFE) + * + * GPIOCMD latch @ 0x3CF001E0 (sub_43D38C): + * word = (bank << 16) | (pin << 8) | cmd + * mode==1: cmd = val ? 15 : 14 (drive high / low) + * mode==0xFFFE: clear DIR bit, cmd=0 + * else: set DIR bit, cmd=(u8)mode (pinmux / EN-enable mode 0) + * + * IRQ: gc.to_irq maps through the sibling EIC (apple,eic / apple,s5l8740-eic) + * after s5l8740_eic_enable_gpio(offset, IRQ_TYPE_LEVEL_LOW). Enough for + * gpio-keys once EIC parent chaining is proven; hierarchical irqchip optional. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define S5L8740_GPIO_BANK_STRIDE 32 +#define S5L8740_GPIO_DIN_OFF 0x04 +#define S5L8740_GPIO_DOUT_OFF 0x08 +#define S5L8740_GPIO_DIR_OFF 0x14 +#define S5L8740_GPIOCMD_OFF 0x1e0 +#define S5L8740_GPIO_DEFAULT_NGPIO 128 /* BT host-wake is GPIO 119 */ + +#define S5L8740_CMD_OUT_LOW 14 +#define S5L8740_CMD_OUT_HIGH 15 + +/* From irq-s5l8740-eic.c */ +int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); + +/* + * IpodSec sub_223C / sub_47CC — packed pinmux word: + * [31:24] bank, [23:16] pin, [15] pull?, [12] bit→+0x14?, [8] bit→+0x10, + * [4] bit→+0x0C, [3:0] nibble into bank+0 function field. + * Table extracted from bootloader VA 0x22004C6C (121 words). + */ +#include "pinmux_table.inc" + +static void s5l8740_pinmux_apply_word(void __iomem *gpio_base, u32 a1) +{ + unsigned int bank = (a1 >> 24) & 0xff; + unsigned int pin = (a1 >> 16) & 0xff; + void __iomem *base = gpio_base + 32u * bank; + u32 v; + + v = readl(base + 0x00); + writel(((a1 & 0xfu) << (4u * pin)) | (v & ~(15u << (4u * pin))), + base + 0x00); + + v = readl(base + 0x14); + writel((((a1 >> 12) & 1u) << pin) | (v & ~BIT(pin)), base + 0x14); + + v = readl(base + 0x0c); + writel((((a1 >> 4) & 1u) << pin) | (v & ~BIT(pin)), base + 0x0c); + + v = readl(base + 0x10); + writel((((a1 >> 8) & 1u) << pin) | (v & ~BIT(pin)), base + 0x10); +} + +static void s5l8740_pinmux_223C(struct device *dev, void __iomem *gpio_base) +{ + unsigned int i; + void __iomem *eic; + + s5l8740_pinmux_apply_word(gpio_base, 0x0C03000Fu); + for (i = 0; i < ARRAY_SIZE(k_pinmux_table); i++) + s5l8740_pinmux_apply_word(gpio_base, k_pinmux_table[i]); + + /* EIC mask-all (SEC sub_223C @0x39700080…E0) */ + eic = ioremap(0x39700000ul, 0x100); + if (eic) { + for (i = 0; i <= 6; i++) { + writel(0, eic + 0x80 + 4 * i); + writel(0xffffffffu, eic + 0xa0 + 4 * i); + writel(0, eic + 0xc0 + 4 * i); + writel(0, eic + 0xe0 + 4 * i); + } + iounmap(eic); + } + + s5l8740_pinmux_apply_word(gpio_base, 0x0C041100u); + /* SEC busy(~0x1F4); approximate with short delay */ + udelay(500); + s5l8740_pinmux_apply_word(gpio_base, 0x0C040000u); + + writel(1377685u, gpio_base + 0x380); + writel(1, gpio_base + 0x388); + writel(1, gpio_base + 0x3f4); + writel(1, gpio_base + 0x3e0); + + dev_info(dev, "SEC pinmux sub_223C applied (%u table words)\n", + (unsigned int)ARRAY_SIZE(k_pinmux_table)); +} + +struct s5l8740_gpio { + void __iomem *base; + void __iomem *gpiocmd; + struct gpio_chip gc; + struct irq_domain *eic_domain; + struct timer_list din_timer; + struct work_struct poweroff_work; + struct input_dev *input; + u8 last40, last41, last86; + bool din_inited; +}; + +static struct s5l8740_gpio *s5l8740_n31; + +void (*d1830_n31_din_nirq_hook)(void); +EXPORT_SYMBOL_GPL(d1830_n31_din_nirq_hook); + +void s5l8740_n31_report_key(unsigned int code, int pressed) +{ + if (!s5l8740_n31 || !s5l8740_n31->input) + return; + input_report_key(s5l8740_n31->input, code, pressed); + input_sync(s5l8740_n31->input); +} +EXPORT_SYMBOL_GPL(s5l8740_n31_report_key); + +static void __iomem *s5l8740_bank(struct s5l8740_gpio *sg, unsigned int offset) +{ + return sg->base + S5L8740_GPIO_BANK_STRIDE * (offset >> 3); +} + +/* RetailOS sub_43D38C(gpio, mode, val) */ +static void s5l8740_gpiocmd_mode(struct s5l8740_gpio *sg, unsigned int gpio, + u16 mode, int val) +{ + void __iomem *bank = s5l8740_bank(sg, gpio); + u32 pin = gpio & 7; + u32 dir; + u8 cmd; + + if (gpio == 200) + return; + + if (mode == 1) { + cmd = val ? S5L8740_CMD_OUT_HIGH : S5L8740_CMD_OUT_LOW; + } else if (mode == 0xFFFE) { + dir = readl(bank + S5L8740_GPIO_DIR_OFF); + writel(dir & ~BIT(pin), bank + S5L8740_GPIO_DIR_OFF); + cmd = 0; + } else { + cmd = (u8)mode; + dir = readl(bank + S5L8740_GPIO_DIR_OFF); + writel(dir | BIT(pin), bank + S5L8740_GPIO_DIR_OFF); + } + + writel(((gpio >> 3) << 16) | (pin << 8) | cmd, sg->gpiocmd); +} + +static int s5l8740_gpio_get(struct gpio_chip *gc, unsigned int offset) +{ + struct s5l8740_gpio *sg = gpiochip_get_data(gc); + u32 din = readl(s5l8740_bank(sg, offset) + S5L8740_GPIO_DIN_OFF); + + return !!(din & BIT(offset & 7)); +} + +static void s5l8740_gpio_set(struct gpio_chip *gc, unsigned int offset, int value) +{ + struct s5l8740_gpio *sg = gpiochip_get_data(gc); + + /* mode==1 — cmd 14/15 only (DIR untouched per sub_43D38C) */ + s5l8740_gpiocmd_mode(sg, offset, 1, value); +} + +static void __maybe_unused s5l8740_button_as_input(struct s5l8740_gpio *sg, + unsigned int gpio, + u32 pinmux_word) +{ + void __iomem *bank = s5l8740_bank(sg, gpio); + u32 pin = gpio & 7; + u32 v; + + /* SEC PCON/PUNB. Bit8 (PUNC/+0x10) is 0 in the table = pull-down; + * Vol± are pull-up active-low, so force +0x0c and +0x10. */ + s5l8740_pinmux_apply_word(sg->base, pinmux_word | 0x00000100u); + v = readl(bank + 0x0c); + writel(v | BIT(pin), bank + 0x0c); + v = readl(bank + 0x10); + writel(v | BIT(pin), bank + 0x10); + s5l8740_gpiocmd_mode(sg, gpio, 0xFFFE, 0); +} + +static int s5l8740_gpio_direction_input(struct gpio_chip *gc, unsigned int offset) +{ + struct s5l8740_gpio *sg = gpiochip_get_data(gc); + + /* Vol± / nIRQ: do not re-pinmux or GPIOCMD 0xFFFE — last image did + * that and DIN never moved. Leave SEC / U-Boot pad state. + */ + if (offset == 40 || offset == 41 || offset == 86) + return 0; + s5l8740_gpiocmd_mode(sg, offset, 0xFFFE, 0); + return 0; +} + +static int s5l8740_gpio_direction_output(struct gpio_chip *gc, unsigned int offset, + int value) +{ + s5l8740_gpio_set(gc, offset, value); + return 0; +} + +static int s5l8740_gpio_get_direction(struct gpio_chip *gc, unsigned int offset) +{ + struct s5l8740_gpio *sg = gpiochip_get_data(gc); + u32 dir = readl(s5l8740_bank(sg, offset) + S5L8740_GPIO_DIR_OFF); + + if (dir & BIT(offset & 7)) + return GPIO_LINE_DIRECTION_OUT; + return GPIO_LINE_DIRECTION_IN; +} + +static int s5l8740_gpio_to_irq(struct gpio_chip *gc, unsigned int offset) +{ + struct s5l8740_gpio *sg = gpiochip_get_data(gc); + int ret, virq; + + if (!sg->eic_domain) + return -ENXIO; + + ret = s5l8740_eic_enable_gpio(offset, IRQ_TYPE_LEVEL_LOW); + if (ret) + return ret; + + virq = irq_create_mapping(sg->eic_domain, offset); + if (!virq) + return -EINVAL; + return virq; +} + +static u8 s5l8740_din_bit(struct s5l8740_gpio *sg, unsigned int gpio) +{ + u32 din = readl(s5l8740_bank(sg, gpio) + S5L8740_GPIO_DIN_OFF); + + return !!(din & BIT(gpio & 7)); +} + +/* + * Pinmux ownership (SEC 223C table + every OSOS 43D38C immediate): + * SEC nibbles are only 0 / 2 / 4 / 14. No IIC/IIS-specific nibble. + * OSOS never GPIOCMDs IIC0/IIC1 — IIC1 works from SEC/WTF leftover. + * OSOS 5714EE is UART pairs func2: (4,5)(78,79)(66,67)(83,84). + * OSOS 20690 is SPI2: 87/5, 88/3, 89/3, 90/3. + * Nimbus: 14 EN, 39 RST, 38 IRQ. Vol 40/41. nIRQ 86. + * IIS0: OSOS BCB60 GPIOCMD 7 and 20 only (mode 3=on, 2=off). + * IIS1/IIS2: no 43D38C. Do not treat 21-22/49-54/57-63 as IIS. + * IIC0/IIC1: no named SCL/SDA GPIO; no PUNB/PUNC. Clock IIC1 = + * PWRCON1 bit 6 (SEC 2308). Do not invent IIC pulls. + * Do not replay 20690 or 5714EE in GATE0. Do not 0xFFFE IIC/Vol/86. + */ +static void s5l8740_log_pad(struct s5l8740_gpio *sg, unsigned int gpio, + char *out, size_t n) +{ + void __iomem *b = s5l8740_bank(sg, gpio); + u32 pin = gpio & 7; + u32 pcon = readl(b), din = readl(b + 0x04); + u32 dir = readl(b + S5L8740_GPIO_DIR_OFF); + u32 punb = readl(b + 0x0c), punc = readl(b + 0x10); + + snprintf(out, n, "%u:n%x/d%u/i%u/b%u/c%u", gpio, + (pcon >> (4 * pin)) & 0xf, !!(dir & BIT(pin)), + !!(din & BIT(pin)), !!(punb & BIT(pin)), !!(punc & BIT(pin))); +} + +static void s5l8740_log_pads(struct s5l8740_gpio *sg, const char *tag, + const char *what, const unsigned int *gpios, + unsigned int n) +{ + char buf[160]; + unsigned int i, off = 0; + + off = snprintf(buf, sizeof(buf), "n31-btn pinmux %s %s", tag, what); + for (i = 0; i < n && off < sizeof(buf) - 36; i++) { + char p[36]; + + s5l8740_log_pad(sg, gpios[i], p, sizeof(p)); + off += snprintf(buf + off, sizeof(buf) - off, " %s", p); + } + dev_err(sg->gc.parent, "%s\n", buf); +} + +static void s5l8740_log_pinmux_map(struct s5l8740_gpio *sg, const char *tag) +{ + static const unsigned int keys[] = { 40, 41, 86 }; + static const unsigned int spi2[] = { 87, 88, 89, 90 }; + static const unsigned int nim[] = { 14, 38, 39 }; + static const unsigned int spi0[] = { 0, 1, 2, 3 }; + static const unsigned int uart0[] = { 4, 5 }; + static const unsigned int uart1[] = { 78, 79 }; + static const unsigned int uart2[] = { 66, 67 }; + static const unsigned int uart3p[] = { 83, 84 }; + static const unsigned int iis0[] = { 7, 20 }; + + s5l8740_log_pads(sg, tag, "key", keys, ARRAY_SIZE(keys)); + s5l8740_log_pads(sg, tag, "spi2", spi2, ARRAY_SIZE(spi2)); + s5l8740_log_pads(sg, tag, "nim", nim, ARRAY_SIZE(nim)); + s5l8740_log_pads(sg, tag, "spi0", spi0, ARRAY_SIZE(spi0)); + s5l8740_log_pads(sg, tag, "uartA", uart0, ARRAY_SIZE(uart0)); + s5l8740_log_pads(sg, tag, "uartB", uart1, ARRAY_SIZE(uart1)); + s5l8740_log_pads(sg, tag, "uartC", uart2, ARRAY_SIZE(uart2)); + s5l8740_log_pads(sg, tag, "uartD", uart3p, ARRAY_SIZE(uart3p)); + s5l8740_log_pads(sg, tag, "iis0", iis0, ARRAY_SIZE(iis0)); +} + +static void s5l8740_sec_gpio86(struct s5l8740_gpio *sg) +{ + s5l8740_log_pinmux_map(sg, "before-SEC"); + s5l8740_pinmux_apply_word(sg->base, 0x0A061010u); + /* OSOS BCB60 IIS0 on: 43D38C(20,3) 43D38C(7,3). No IIC GPIOCMD. */ + s5l8740_gpiocmd_mode(sg, 20, 3, 0); + s5l8740_gpiocmd_mode(sg, 7, 3, 0); + s5l8740_log_pinmux_map(sg, "after-SEC-86-iis0"); +} + +int s5l8740_n31_din86(void) +{ + if (!s5l8740_n31) + return -1; + return s5l8740_din_bit(s5l8740_n31, 86); +} +EXPORT_SYMBOL_GPL(s5l8740_n31_din86); + +static void s5l8740_poweroff_work(struct work_struct *work) +{ + if (pm_power_off) + pm_power_off(); +} + +static void s5l8740_key_edge(struct s5l8740_gpio *sg, unsigned int code, + u8 now, u8 *last, const char *name) +{ + if (now == *last) + return; + if (sg->input) { + /* Active-low pad: 0 = pressed */ + input_report_key(sg->input, code, now ? 0 : 1); + input_sync(sg->input); + } + dev_err(sg->gc.parent, "n31-btn %s %s din=%u\n", + name, now ? "release" : "PRESS", now); + *last = now; +} + +static void s5l8740_din_timer(struct timer_list *t) +{ + struct s5l8740_gpio *sg = container_of(t, struct s5l8740_gpio, din_timer); + u8 v40 = s5l8740_din_bit(sg, 40); + u8 v41 = s5l8740_din_bit(sg, 41); + u8 v86 = s5l8740_din_bit(sg, 86); + + if (!sg->din_inited) { + sg->last40 = v40; + sg->last41 = v41; + sg->last86 = v86; + sg->din_inited = true; + } else { + /* OSOS GPIOButtonManager: only GPIO 40/41. Home/Play/Sleep + * are PMIC bits; GPIO 86 is the nIRQ doorbell into d1830. + */ + s5l8740_key_edge(sg, KEY_VOLUMEUP, v40, &sg->last40, "VOL+"); + s5l8740_key_edge(sg, KEY_VOLUMEDOWN, v41, &sg->last41, "VOL-"); + if (v86 != sg->last86) { + dev_dbg(sg->gc.parent, "n31-btn NIRQ86 %u->%u\n", + sg->last86, v86); + sg->last86 = v86; + if (d1830_n31_din_nirq_hook) + d1830_n31_din_nirq_hook(); + } + } + + mod_timer(&sg->din_timer, jiffies + msecs_to_jiffies(50)); +} + +static struct irq_domain *s5l8740_gpio_find_eic_domain(struct device *dev) +{ + struct device_node *np = dev->of_node; + struct device_node *eic_np = NULL; + struct irq_domain *domain = NULL; + + /* Preferred: DT phandle apple,eic = <&eic> on the gpio node */ + if (np) + eic_np = of_parse_phandle(np, "apple,eic", 0); + + if (!eic_np && np) + eic_np = of_parse_phandle(np, "interrupt-parent", 0); + + if (!eic_np && np) + eic_np = of_irq_find_parent(np); + + if (!eic_np) + eic_np = of_find_compatible_node(NULL, NULL, "apple,s5l8740-eic"); + + if (!eic_np) + eic_np = of_find_compatible_node(NULL, NULL, "samsung,s5l8740-eic"); + + if (eic_np) { + domain = irq_find_host(eic_np); + of_node_put(eic_np); + } + + return domain; +} + +static int s5l8740_gpio_probe(struct platform_device *pdev) +{ + struct s5l8740_gpio *sg; + struct device *dev = &pdev->dev; + struct resource *res; + u32 ngpios = S5L8740_GPIO_DEFAULT_NGPIO; + int ret; + + sg = devm_kzalloc(dev, sizeof(*sg), GFP_KERNEL); + if (!sg) + return -ENOMEM; + + sg->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(sg->base)) + return PTR_ERR(sg->base); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (res && resource_size(res) > S5L8740_GPIOCMD_OFF) + sg->gpiocmd = sg->base + S5L8740_GPIOCMD_OFF; + else + sg->gpiocmd = devm_ioremap(dev, 0x3cf001e0, 4); + if (!sg->gpiocmd) + return -ENOMEM; + + of_property_read_u32(dev->of_node, "ngpios", &ngpios); + + sg->eic_domain = s5l8740_gpio_find_eic_domain(dev); + if (!sg->eic_domain) + dev_warn(dev, "EIC irq domain not found — to_irq unavailable\n"); + + sg->gc.label = dev_name(dev); + sg->gc.parent = dev; + sg->gc.owner = THIS_MODULE; + sg->gc.base = -1; + sg->gc.ngpio = ngpios; + sg->gc.get = s5l8740_gpio_get; + sg->gc.set = s5l8740_gpio_set; + sg->gc.direction_input = s5l8740_gpio_direction_input; + sg->gc.direction_output = s5l8740_gpio_direction_output; + sg->gc.get_direction = s5l8740_gpio_get_direction; + sg->gc.to_irq = s5l8740_gpio_to_irq; + /* Do not set gc.irq.* without a full gpio irqchip — to_irq alone. */ + + ret = devm_gpiochip_add_data(dev, &sg->gc, sg); + if (ret) { + dev_err(dev, "gpiochip_add failed: %d\n", ret); + return ret; + } + + /* Re-apply SEC pinmux so WTF/U-Boot leftovers match cold-boot */ + if (!of_property_read_bool(dev->of_node, "apple,skip-sec-pinmux")) + s5l8740_pinmux_223C(dev, sg->base); + else + s5l8740_sec_gpio86(sg); + + /* Vol± stay as SEC/U-Boot left them. GPIO 86 got its SEC word only. */ + sg->input = devm_input_allocate_device(dev); + if (sg->input) { + sg->input->name = "n31-buttons"; + sg->input->phys = "s5l8740/gpio"; + sg->input->dev.parent = dev; + sg->input->id.bustype = BUS_HOST; + input_set_capability(sg->input, EV_KEY, KEY_VOLUMEUP); + input_set_capability(sg->input, EV_KEY, KEY_VOLUMEDOWN); + input_set_capability(sg->input, EV_KEY, KEY_POWER); + input_set_capability(sg->input, EV_KEY, KEY_HOMEPAGE); + input_set_capability(sg->input, EV_KEY, KEY_PLAYPAUSE); + if (input_register_device(sg->input)) + sg->input = NULL; + } + + INIT_WORK(&sg->poweroff_work, s5l8740_poweroff_work); + timer_setup(&sg->din_timer, s5l8740_din_timer, 0); + mod_timer(&sg->din_timer, jiffies + msecs_to_jiffies(50)); + s5l8740_n31 = sg; + platform_set_drvdata(pdev, sg); + + dev_info(dev, "S5L8740 GPIO @%pR ngpios=%u (GPIOCMD @+0x1E0) eic=%s\n", + res, ngpios, sg->eic_domain ? "yes" : "no"); + return 0; +} + +static void s5l8740_gpio_remove(struct platform_device *pdev) +{ + struct s5l8740_gpio *sg = platform_get_drvdata(pdev); + + if (!sg) + return; + s5l8740_n31 = NULL; + timer_delete_sync(&sg->din_timer); + cancel_work_sync(&sg->poweroff_work); +} + +static const struct of_device_id s5l8740_gpio_of_match[] = { + { .compatible = "apple,s5l8740-gpio" }, + { .compatible = "samsung,s5l8740-gpio" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_gpio_of_match); + +static struct platform_driver s5l8740_gpio_driver = { + .probe = s5l8740_gpio_probe, + .remove = s5l8740_gpio_remove, + .driver = { + .name = "gpio-s5l8740", + .of_match_table = s5l8740_gpio_of_match, + }, +}; +module_platform_driver(s5l8740_gpio_driver); + +MODULE_DESCRIPTION("Samsung/Apple S5L8740 banked GPIO + GPIOCMD + EIC to_irq"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpio/pinmux_table.inc b/drivers/gpio/pinmux_table.inc new file mode 100644 index 00000000000000..da7a05477637f6 --- /dev/null +++ b/drivers/gpio/pinmux_table.inc @@ -0,0 +1,25 @@ +/* extracted from N31.bootloader.dec.bin @ VA 0x22004C6C — IpodSec sub_223C */ +static const u32 k_pinmux_table[] = { + 0x00001004u, 0x00011002u, 0x00021002u, 0x00031002u, 0x0004100Eu, 0x0005100Eu, + 0x00061002u, 0x00071002u, /* GPIO 6/7 func2 DIR — not buttons (LCD/UART pads) */ + 0x01000000u, 0x01010000u, 0x01021002u, 0x01031002u, /* GPIO 11 = last: func2 DIR */ + 0x01040000u, 0x01050000u, 0x0106000Eu, 0x0107100Eu, 0x02000000u, 0x02010000u, + 0x02020000u, 0x02030000u, 0x02041002u, 0x02051002u, 0x02061002u, 0x02070000u, + 0x03000000u, 0x03010000u, 0x03020000u, 0x03030000u, 0x03040000u, 0x03050000u, + 0x03060000u, 0x03070000u, 0x04000000u, 0x04010000u, 0x04020000u, 0x04030000u, + 0x04040000u, 0x04050000u, 0x04061010u, 0x0407100Eu, 0x05001010u, 0x05011010u, + 0x05021002u, 0x05030000u, 0x05040000u, 0x05050000u, 0x05060000u, 0x05070000u, + 0x06000000u, 0x06011002u, 0x06021002u, 0x06031002u, 0x06041002u, 0x06051002u, + 0x06061002u, 0x06070000u, 0x07000000u, 0x07011002u, 0x07021002u, 0x07031002u, + 0x07041002u, 0x07051002u, 0x07061002u, 0x07071002u, 0x08001002u, 0x08010000u, + 0x08020000u, 0x08030000u, 0x08041010u, 0x08050000u, 0x08060000u, 0x08070000u, + 0x09000000u, 0x09010000u, 0x09020000u, 0x09030000u, 0x09040000u, 0x09050000u, + 0x09060000u, 0x09070000u, 0x0A000000u, 0x0A010000u, 0x0A020000u, 0x0A031002u, + 0x0A041002u, 0x0A051010u, 0x0A061010u, 0x0A070100u, 0x0B00000Eu, 0x0B01000Eu, + 0x0B020000u, 0x0B030000u, 0x0B040000u, 0x0B050000u, 0x0B060000u, 0x0B070000u, + 0x0C000000u, 0x0C010000u, 0x0C020000u, 0x0C03000Eu, 0x0C041010u, 0x0C050000u, + 0x0C060000u, 0x0C070000u, 0x0D000000u, 0x0D010000u, 0x0D020000u, 0x0D030000u, + 0x0D040000u, 0x0D050000u, 0x0D060000u, 0x0D070000u, 0x0E000000u, 0x0E010000u, + 0x0E020000u, 0x0E030000u, 0x0E040000u, 0x0E050000u, 0x0E060000u, 0x0E070000u, + 0x00000000u, +}; diff --git a/drivers/irqchip/irq-s5l8740-eic.c b/drivers/irqchip/irq-s5l8740-eic.c old mode 100755 new mode 100644 From 7ac2340e32ece3154d34453453c70ccea659e0d0 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 20:58:33 -0230 Subject: [PATCH 03/31] i2c: s5l8702: complete xfers on IICCON IRQPEND N31 IIC1 never sets the +0x20 INT word. Byte done is IICCON bit 4 (S3C IRQPEND). The old ISR read +0x20, returned IRQ_NONE, and left SCL stretched. Clear IRQPEND to resume the bus. Do not treat STAT bit 0 as NAK; that aborted every PMIC@0x73 transfer with -EIO even when VIC 22 fired. On reads, the first IRQPEND is address-complete and DS still holds the address byte. Skip that one, then take one IRQPEND per RX byte. PIO mode stays available but is not required. IIC1 at 0x3C900000 is the PMIC bus. Tested on iPod nano 7G: D1830 @0x73 and LIS3 @0x18 complete. --- drivers/i2c/busses/i2c-s5l8702.c | 826 ++++++++++++++++++++++--------- 1 file changed, 595 insertions(+), 231 deletions(-) diff --git a/drivers/i2c/busses/i2c-s5l8702.c b/drivers/i2c/busses/i2c-s5l8702.c index 1c8000aa839dfd..a413adece55797 100644 --- a/drivers/i2c/busses/i2c-s5l8702.c +++ b/drivers/i2c/busses/i2c-s5l8702.c @@ -1,83 +1,116 @@ // SPDX-License-Identifier: GPL-2.0 /* - * S5L8702 I2C controller driver + * S5L8702 / S5L8740 I2C — IRQ master (S3C2410-compatible CON/STAT) + * + * Glass 2026-08-22: "IRQ timeout INT=0 STAT=30 CON=181 or 1B1" + * + * +0x20 INT is always 0. Completion is IICCON bit4 IRQPEND (S3C INTPEND). + * CON=0x1B1 = 0x1A1|IRQPEND — byte done, ISR used to read +0x20, return + * IRQ_NONE, leave SCL stretched. Old "BUSHOLD" *set* bit4, which is the + * hold bit — inverted. Clear IRQPEND to resume; set IRQEN (bit5) for VIC. + * + * Glass 2026-08-22: VIC IRQ works (virq 38 = hwirq 22). STAT bit0 as + * S3C LASTBIT/NAK aborted every xfer with -EIO (-5) including PMIC@0x73. + * Ignore bit0; writes still complete on IRQPEND. + * + * Reads: address IRQPEND is not a data byte (DS still holds addr8). + * clock_rx_byte() held IRQPEND *clear* and wiped the next byte-done, + * so DS stayed 0x31/0xe7. One IRQPEND per RX byte, then read DS. + * SEC 4AC4 / Rockbox also treat INT 0x100 as byte-ready. Not PIO. */ - +#include +#include +#include +#include +#include #include -#include #include +#include +#include #include #include #include #include #include +#include + +#define S5L8702_I2C_CON 0x0 +#define S5L8702_I2C_STAT 0x4 +#define S5L8702_I2C_ADD 0x8 +#define S5L8702_I2C_DS 0xc +#define S5L8702_I2C_BUSY 0x10 +#define S5L8702_I2C_UNK14 0x14 +#define S5L8702_I2C_UNK18 0x18 +#define S5L8702_I2C_INT 0x20 +#define S5L8702_I2C_UNK28 0x28 + +/* S3C2410 IICCON low byte + SEC bit8 */ +#define S5L8702_I2C_CON_SCALE(x) ((x) & 0xf) +#define S5L8702_I2C_CON_IRQPEND BIT(4) /* W0C to resume SCL */ +#define S5L8702_I2C_CON_IRQEN BIT(5) +#define S5L8702_I2C_CON_TXDIV_512 BIT(6) +#define S5L8702_I2C_CON_ACKEN BIT(7) +#define S5L8702_I2C_CON_SEC_BIT8 BIT(8) + +#define S5L8702_I2C_CON_IDLE (S5L8702_I2C_CON_SEC_BIT8 | \ + S5L8702_I2C_CON_ACKEN | \ + S5L8702_I2C_CON_IRQEN | \ + S5L8702_I2C_CON_SCALE(1)) /* 0x1A1 */ + +#define S5L8702_I2C_STAT_LASTBIT BIT(0) +#define S5L8702_I2C_STAT_TXRXEN BIT(4) +#define S5L8702_I2C_STAT_START BIT(5) +#define S5L8702_I2C_STAT_TX BIT(6) +#define S5L8702_I2C_STAT_MASTER BIT(7) +#define S5L8702_I2C_STAT_MASTER_TX (S5L8702_I2C_STAT_MASTER | \ + S5L8702_I2C_STAT_TX | \ + S5L8702_I2C_STAT_TXRXEN) /* 0xD0 */ +#define S5L8702_I2C_STAT_MASTER_RX (S5L8702_I2C_STAT_MASTER | \ + S5L8702_I2C_STAT_TXRXEN) /* 0x90 */ + +#define S5L8702_I2C_INT_ALL 0x3f00 +#define S5L8702_I2C_INT_BYTE BIT(8) /* SEC 4AC4 / Rockbox STA2 */ +#define S5L8702_I2C_INT_STOP BIT(13) + +/* SEC 1C8C canned STAT: v4=0x80 read / 0xC0 write, then |0x10 / |0x30 */ +#define S5L8702_I2C_STAT_SEC_RX 0x80 +#define S5L8702_I2C_STAT_SEC_TX 0xC0 +#define S5L8702_I2C_STAT_SEC_SOE 0x10 +#define S5L8702_I2C_STAT_SEC_GO 0x30 /* SOE|BB */ + +#define S5L8702_I2C_XFER_TIMEOUT (msecs_to_jiffies(200)) +#define S5L8702_I2C_BUSY_LOOPS 10000 -#define S5L8702_I2C_CON 0x0 /* Control register */ -#define S5L8702_I2C_STAT 0x4 /* Control/status register */ -#define S5L8702_I2C_ADD 0x8 /* Bus address register */ -#define S5L8702_I2C_DS 0xc /* Transmit/receive data shift register */ -#define S5L8702_I2C_BUSY 0x10 -#define S5L8702_I2C_UNK14 0x14 -#define S5L8702_I2C_UNK18 0x18 -#define S5L8702_I2C_INT 0x20 /* Interrupt status register */ -#define S5L8702_I2C_UNK28 0x28 - -#define S5L8702_I2C_CON_CK_REG(x) ((x) & 0xf) -#define S5L8702_I2C_CON_BUSHOLD BIT(4) -#define S5L8702_I2C_CON_CKSEL16 (0 << 6) -#define S5L8702_I2C_CON_CKSEL512 BIT(6) -#define S5L8702_I2C_CON_ACKGEN BIT(7) -#define S5L8702_I2C_CON_INTEN_BUSHOLD BIT(8) -#define S5L8702_I2C_CON_INTEN_TIMEOUT BIT(9) -#define S5L8702_I2C_CON_INTEN_RX BIT(10) -#define S5L8702_I2C_CON_INTEN_TX BIT(11) -#define S5L8702_I2C_CON_INTEN_START BIT(12) -#define S5L8702_I2C_CON_INTEN_STOP BIT(13) -#define S5L8702_I2C_CON_INTEN_ALL (0x3f00) - -#define S5L8702_I2C_STAT_LRB BIT(0) -// The missing bits probably match S5L8700X datasheet ADDR_ZERO, AAS, LBA -#define S5L8702_I2C_STAT_SOE BIT(4) // Serial Output Enable -#define S5L8702_I2C_STAT_BB BIT(5) -#define S5L8702_I2C_STAT_TX BIT(6) -#define S5L8702_I2C_STAT_MASTER BIT(7) - -#define S5L8702_I2C_INT_BUSHOLD BIT(8) -#define S5L8702_I2C_INT_TIMEOUT BIT(9) -#define S5L8702_I2C_INT_RX BIT(10) -#define S5L8702_I2C_INT_TX BIT(11) -#define S5L8702_I2C_INT_START BIT(12) -#define S5L8702_I2C_INT_STOP BIT(13) -#define S5L8702_I2C_INT_ALL (0x3f00) - -#define S5L8702_I2C_XFER_TIMEOUT (msecs_to_jiffies(100)) - -#define S5L8702_I2C_BUSY_LOOPS 5000 - -/* i2c controller state */ enum s5l8702_i2c_state { STATE_IDLE, STATE_START, STATE_READ, - STATE_PREPARE_READ, STATE_WRITE, - STATE_STOP }; struct s5l8702_i2c_dev { struct device *dev; void __iomem *regs; int irq; - bool write_busy_poll; + spinlock_t lock; enum s5l8702_i2c_state state; - struct i2c_msg *msg; + struct i2c_msg *msg; unsigned int msg_pos; unsigned int nmsgs; int msg_ret; unsigned int iiccon; - unsigned int pending_irq; + bool timeout_logged; + bool start_logged; + bool isr_logged; + bool polled_logged; + bool stat_logged; + bool rd_logged; + bool drop_pend; /* READ: skip address-phase IRQPEND (DS still addr8) */ + u8 rx_retry; struct completion msg_complete; struct i2c_adapter adapter; + struct clk_bulk_data *clks; + int num_clks; }; static inline u32 s5l8702_i2c_readl(struct s5l8702_i2c_dev *i2c_dev, u32 reg) @@ -85,179 +118,497 @@ static inline u32 s5l8702_i2c_readl(struct s5l8702_i2c_dev *i2c_dev, u32 reg) return readl(i2c_dev->regs + reg); } -static void s5l8702_i2c_writel(struct s5l8702_i2c_dev *i2c_dev, u32 reg, u32 val) +static void s5l8702_i2c_write_raw(struct s5l8702_i2c_dev *i2c_dev, u32 reg, + u32 val) { - if (i2c_dev->write_busy_poll) { - unsigned int n = S5L8702_I2C_BUSY_LOOPS; - - while (readl(i2c_dev->regs + S5L8702_I2C_BUSY) && --n) - cpu_relax(); - } writel(val, i2c_dev->regs + reg); } -static void s5l8702_i2c_state_machine(struct s5l8702_i2c_dev *i2c_dev) { - uint32_t stat; +static void s5l8702_i2c_wait_rdy(struct s5l8702_i2c_dev *i2c_dev) +{ + unsigned int n = S5L8702_I2C_BUSY_LOOPS; - switch ( i2c_dev->state ) - { + while (s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_BUSY) && --n) + cpu_relax(); +} - case STATE_START: - i2c_dev->pending_irq = S5L8702_I2C_INT_BUSHOLD; - if (i2c_dev->msg->flags & I2C_M_RD) { - stat = S5L8702_I2C_STAT_SOE | S5L8702_I2C_STAT_MASTER; - } else { - stat = S5L8702_I2C_STAT_SOE | S5L8702_I2C_STAT_TX | S5L8702_I2C_STAT_MASTER; - } - i2c_dev->iiccon &= ~S5L8702_I2C_CON_BUSHOLD; - i2c_dev->iiccon |= S5L8702_I2C_CON_ACKGEN; - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, stat); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_DS, i2c_8bit_addr_from_msg(i2c_dev->msg)); - stat |= S5L8702_I2C_STAT_BB; - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, stat); - if (i2c_dev->msg->flags & I2C_M_RD) { - i2c_dev->state = STATE_PREPARE_READ; - } - else { - i2c_dev->state = STATE_WRITE; - } - break; +static void s5l8702_i2c_writel(struct s5l8702_i2c_dev *i2c_dev, u32 reg, u32 val) +{ + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, reg, val); +} - case STATE_WRITE: // Write - if ( s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & S5L8702_I2C_STAT_LRB ) { // Did we receive ACK? - i2c_dev->msg_ret = -EIO; - goto generate_stop; - } - if ( i2c_dev->msg_pos == i2c_dev->msg->len ) { // is the end of the msg - goto generate_stop; - } - i2c_dev->pending_irq = S5L8702_I2C_INT_BUSHOLD; - i2c_dev->iiccon |= S5L8702_I2C_CON_BUSHOLD; - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_DS, i2c_dev->msg->buf[i2c_dev->msg_pos++]); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); - break; +/* Clear IRQPEND so SCL runs. Never set bit4 — that stretches the bus. */ +static void s5l8702_i2c_resume(struct s5l8702_i2c_dev *i2c_dev) +{ + u32 con = i2c_dev->iiccon & ~S5L8702_I2C_CON_IRQPEND; + unsigned int n = 10000; + + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, con); + /* Wait until this IRQPEND drops so the next service is a new byte. */ + while ((s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON) & + S5L8702_I2C_CON_IRQPEND) && --n) + cpu_relax(); +} - case STATE_READ: // Read - i2c_dev->msg->buf[i2c_dev->msg_pos++] = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_DS); - fallthrough; - case STATE_PREPARE_READ: // Prepare read - if ( !i2c_dev->msg_pos && (s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & S5L8702_I2C_STAT_LRB) ) { - i2c_dev->msg_ret = -EIO; - goto generate_stop; - } +static void s5l8702_i2c_state_machine(struct s5l8702_i2c_dev *i2c_dev); - if ( i2c_dev->msg_pos == i2c_dev->msg->len ) { // is the end of the msg - goto generate_stop; - } +static void s5l8702_i2c_finish(struct s5l8702_i2c_dev *i2c_dev) +{ + i2c_dev->state = STATE_IDLE; + complete(&i2c_dev->msg_complete); +} - if ( (i2c_dev->msg->len - i2c_dev->msg_pos) == 1 ) { // last byte of msg NACK - i2c_dev->iiccon &= ~S5L8702_I2C_CON_ACKGEN; - } - i2c_dev->iiccon |= S5L8702_I2C_CON_BUSHOLD; - i2c_dev->pending_irq = S5L8702_I2C_INT_BUSHOLD; - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); - i2c_dev->state = STATE_READ; - break; +static void s5l8702_i2c_stop(struct s5l8702_i2c_dev *i2c_dev) +{ + u32 mode = (i2c_dev->msg->flags & I2C_M_RD) ? + S5L8702_I2C_STAT_SEC_RX : S5L8702_I2C_STAT_SEC_TX; + + /* SEC 1C8C stop: INT=0x2000, STAT = v4|0x10 (0x90 read / 0xD0 + * write). Do not read-modify current STAT — that left 0xe1 + * (MASTER|TX|START|bit0) in DS on the next read. + */ + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_INT, 0x2000); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, + mode | S5L8702_I2C_STAT_SEC_SOE); + i2c_dev->iiccon = S5L8702_I2C_CON_IDLE; + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); + i2c_dev->nmsgs = 0; + s5l8702_i2c_finish(i2c_dev); +} - case STATE_STOP: // Generate Stop -generate_stop: - i2c_dev->pending_irq = S5L8702_I2C_INT_STOP; +static void s5l8702_i2c_state_machine(struct s5l8702_i2c_dev *i2c_dev) +{ + u32 stat; + + switch (i2c_dev->state) { + case STATE_START: + dev_dbg(i2c_dev->dev, "IIC START 7bit=0x%02x DS=0x%02x %s\n", + i2c_dev->msg->addr, + i2c_8bit_addr_from_msg(i2c_dev->msg), + (i2c_dev->msg->flags & I2C_M_RD) ? "RD" : "WR"); + /* Write: MASTER_TX|START = 0xF0. Read: MASTER_RX|START = 0xB0 + * (same 0xB0 glass saw). INT +0x20 stays 0 — IRQPEND only. + */ + if (i2c_dev->msg->flags & I2C_M_RD) + stat = S5L8702_I2C_STAT_MASTER_RX; + else + stat = S5L8702_I2C_STAT_MASTER_TX; + i2c_dev->iiccon = S5L8702_I2C_CON_IDLE; + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, stat); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, + i2c_8bit_addr_from_msg(i2c_dev->msg)); + ndelay(50); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, + stat | S5L8702_I2C_STAT_START); + i2c_dev->state = (i2c_dev->msg->flags & I2C_M_RD) ? + STATE_READ : STATE_WRITE; + /* NACK the sole RX byte before the address IRQ is dropped. */ + if ((i2c_dev->msg->flags & I2C_M_RD) && + i2c_dev->msg->len == 1) + i2c_dev->iiccon &= ~S5L8702_I2C_CON_ACKEN; + /* + * Glass #73: first RD IRQPEND is address complete, DS=addr8. + * Only skipping when IRQPEND was already set ate that IRQ + * as data (DS=31) and wedged the next xfer (-110). + */ + if (i2c_dev->msg->flags & I2C_M_RD) + i2c_dev->drop_pend = true; + break; - if (i2c_dev->msg->flags & I2C_M_RD) { - stat &= ~S5L8702_I2C_STAT_BB; + case STATE_WRITE: { + u32 st = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT); + + /* + * Do not abort on STAT bit0. S3C LASTBIT=NAK, but on this + * block it is 1 whenever IRQPEND fires — PMIC@0x73 (CONFIRMED) + * and LIS3 both returned -EIO (-5) after a working VIC IRQ. + */ + dev_dbg(i2c_dev->dev, + "IIC WR STAT=%08x CON=%08x bit0=%u\n", + st, s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON), + !!(st & S5L8702_I2C_STAT_LASTBIT)); + if (i2c_dev->msg_pos == i2c_dev->msg->len) { + s5l8702_i2c_stop(i2c_dev); + break; } - else { - stat &= ~( S5L8702_I2C_STAT_BB | S5L8702_I2C_STAT_TX ); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, + i2c_dev->msg->buf[i2c_dev->msg_pos++]); + ndelay(50); + break; + } + + case STATE_READ: { + u32 ds, st; + u8 addr8 = i2c_8bit_addr_from_msg(i2c_dev->msg); + + /* + * This IRQPEND is a completed RX byte. The address-phase + * IRQPEND is dropped in service_pend (DS still addr8). + * Do not hold IRQPEND clear — that wiped the data-ready + * flag and left DS=0x31/0xe7 on glass. + */ + if (i2c_dev->msg_pos >= i2c_dev->msg->len) { + s5l8702_i2c_stop(i2c_dev); + break; } - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, stat); - i2c_dev->iiccon &= ~S5L8702_I2C_CON_ACKGEN; - i2c_dev->iiccon |= S5L8702_I2C_CON_BUSHOLD; - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); - i2c_dev->nmsgs--; - i2c_dev->msg++; - i2c_dev->msg_pos = 0; - - // If we have an error or we processed all messages then we are done - if (i2c_dev->msg_ret || (i2c_dev->nmsgs == 0)) { - i2c_dev->state = STATE_IDLE; - } else { - i2c_dev->state = STATE_START; + ds = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_DS) & 0xff; + st = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & 0xff; + if (!i2c_dev->rd_logged) { + i2c_dev->rd_logged = true; + dev_info(i2c_dev->dev, + "IIC RD DS=%02x STAT=%02x addr8=%02x%s\n", + ds, st, addr8, + (ds == addr8) ? " (still addr)" : ""); } + i2c_dev->msg->buf[i2c_dev->msg_pos++] = (u8)ds; + if (i2c_dev->msg_pos >= i2c_dev->msg->len) + s5l8702_i2c_stop(i2c_dev); + else if (i2c_dev->msg_pos + 1 == i2c_dev->msg->len) + i2c_dev->iiccon &= ~S5L8702_I2C_CON_ACKEN; break; - - case STATE_IDLE: // We are done - i2c_dev->pending_irq = 0; - complete(&i2c_dev->msg_complete); + } + + case STATE_IDLE: break; } } -static irqreturn_t s5l8702_i2c_isr(int this_irq, void *data) -{ - struct s5l8702_i2c_dev *i2c_dev = data; - u32 val; - - val = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_INT); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_INT, val); +#define S5L8740_VIC0_PHYS 0x38E00000ul +#define S5L8740_VIC_SIZE 0x2000 - dev_dbg(i2c_dev->dev, "%s state=0x%04x msg_ret=0x%04x pending_irq=0x%04x val=0x%04x", - __func__, i2c_dev->state, i2c_dev->msg_ret, i2c_dev->pending_irq, val); +static void __iomem *s5l_vic; - i2c_dev->pending_irq &= ~val; +static void s5l8702_dump_vic(struct s5l8702_i2c_dev *i2c_dev) +{ + void __iomem *v0, *v1; + u32 r0, s0, e0, r1, s1, e1; + + if (!s5l_vic) + s5l_vic = ioremap(S5L8740_VIC0_PHYS, S5L8740_VIC_SIZE); + if (!s5l_vic) + return; + v0 = s5l_vic; + v1 = s5l_vic + 0x1000; + r0 = readl(v0 + 0x08); + s0 = readl(v0 + 0x00); + e0 = readl(v0 + 0x10); + r1 = readl(v1 + 0x08); + s1 = readl(v1 + 0x00); + e1 = readl(v1 + 0x10); + dev_err(i2c_dev->dev, + "VIC0 raw=%08x stat=%08x en=%08x VIC1 raw=%08x stat=%08x en=%08x\n", + r0, s0, e0, r1, s1, e1); + if (r0) + dev_err(i2c_dev->dev, "VIC0 pending bit %u (DT IIC is 21/22)\n", + ffs(r0) - 1); + if (r1) + dev_err(i2c_dev->dev, "VIC1 pending bit %u\n", ffs(r1) - 1); +} - // [TODO] Is this the best way due to us getting other interrupts? - if (!i2c_dev->pending_irq) { - s5l8702_i2c_state_machine(i2c_dev); +/* Caller holds i2c_dev->lock. */ +static bool s5l8702_i2c_service_pend(struct s5l8702_i2c_dev *i2c_dev) +{ + u32 con = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON); + u32 extra = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_INT); + + if (!(con & S5L8702_I2C_CON_IRQPEND) && + !(extra & (S5L8702_I2C_INT_ALL | S5L8702_I2C_INT_BYTE | + S5L8702_I2C_INT_STOP))) + return false; + if (extra & (S5L8702_I2C_INT_ALL | S5L8702_I2C_INT_BYTE | + S5L8702_I2C_INT_STOP)) + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_INT, extra); + if (i2c_dev->drop_pend) { + i2c_dev->drop_pend = false; + s5l8702_i2c_resume(i2c_dev); + return true; } + s5l8702_i2c_state_machine(i2c_dev); + /* One S3C-style out_ack per IRQ. Do not ack inside each state. */ + s5l8702_i2c_resume(i2c_dev); + return true; +} +static irqreturn_t s5l8702_i2c_isr(int this_irq, void *data) +{ + struct s5l8702_i2c_dev *i2c_dev = data; + bool handled; + + spin_lock(&i2c_dev->lock); + handled = s5l8702_i2c_service_pend(i2c_dev); + spin_unlock(&i2c_dev->lock); + if (!handled) + return IRQ_NONE; + if (!i2c_dev->isr_logged) { + struct irq_data *d = irq_get_irq_data(this_irq); + + i2c_dev->isr_logged = true; + dev_dbg(i2c_dev->dev, "IIC ISR fired irq=%d hwirq=%lu\n", + this_irq, d ? d->hwirq : 0); + } return IRQ_HANDLED; } -static int s5l8702_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], - int num) +static int s5l8702_i2c_init(struct s5l8702_i2c_dev *i2c_dev); + +/* Drop leftover IRQPEND/INT so the next START's first pend is a new byte. */ +static void s5l8702_i2c_drain_pend(struct s5l8702_i2c_dev *i2c_dev) { - unsigned long time_left; - struct s5l8702_i2c_dev *i2c_dev = i2c_get_adapdata(adap); + unsigned int n = 8; + + while (n--) { + u32 con = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON); + u32 extra = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_INT); + + if (!(con & S5L8702_I2C_CON_IRQPEND) && + !(extra & S5L8702_I2C_INT_ALL)) + break; + if (extra & S5L8702_I2C_INT_ALL) + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_INT, extra); + s5l8702_i2c_resume(i2c_dev); + } +} - dev_dbg(i2c_dev->dev, "%s start", __func__); +/* One START/STOP, like SEC 1C8C. IRQPEND completion for write and read. */ +static int s5l8702_i2c_xfer_one(struct s5l8702_i2c_dev *i2c_dev, + struct i2c_msg *msg) +{ + unsigned long flags, deadline; - // [TODO] implement clocks this is equivalent to set controller active and clear interrupts - // but we are missing clock enable and disable + reinit_completion(&i2c_dev->msg_complete); s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_UNK14, 1); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_INT, S5L8702_I2C_INT_ALL); - - int i; + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_INT, S5L8702_I2C_INT_ALL); + + spin_lock_irqsave(&i2c_dev->lock, flags); + s5l8702_i2c_drain_pend(i2c_dev); + i2c_dev->drop_pend = false; + i2c_dev->rx_retry = 0; + i2c_dev->msg = msg; + i2c_dev->nmsgs = 1; + i2c_dev->msg_ret = 0; + i2c_dev->msg_pos = 0; + i2c_dev->state = STATE_START; + s5l8702_i2c_state_machine(i2c_dev); + spin_unlock_irqrestore(&i2c_dev->lock, flags); + + /* + * Completion is IICCON IRQPEND (S3C bit4). Glass: VIC 22 can fire + * once ("IIC ISR fired") then go silent for later bytes — ISR-only + * wait became -110 on every PMIC/LIS3 xfer. Service the same + * IRQPEND bit from this thread when the ISR does not. Same lock as + * the ISR, so one IRQPEND is one step. Not the old +0x20 PIO path. + */ + deadline = jiffies + S5L8702_I2C_XFER_TIMEOUT; + while (!try_wait_for_completion(&i2c_dev->msg_complete)) { + bool serviced; + + if (time_after(jiffies, deadline)) { + if (!i2c_dev->timeout_logged) { + i2c_dev->timeout_logged = true; + dev_err(i2c_dev->dev, + "IIC IRQ timeout 7bit=0x%02x DS=0x%02x INT=%08x STAT=%08x CON=%08x state=%d isr=%d\n", + i2c_dev->msg ? i2c_dev->msg->addr : 0, + i2c_dev->msg ? i2c_8bit_addr_from_msg(i2c_dev->msg) : 0, + s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_INT), + s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT), + s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON), + i2c_dev->state, + i2c_dev->isr_logged); + s5l8702_dump_vic(i2c_dev); + } + spin_lock_irqsave(&i2c_dev->lock, flags); + i2c_dev->state = STATE_IDLE; + s5l8702_i2c_init(i2c_dev); + spin_unlock_irqrestore(&i2c_dev->lock, flags); + return -ETIMEDOUT; + } - for (i = 0; i < num; i++) { - dev_dbg(i2c_dev->dev, "%s addr=0x%04x flags=0x%04x len=%u buf=%02x", - __func__, msgs[i].addr, msgs[i].flags, msgs[i].len, msgs[i].buf[0]); + spin_lock_irqsave(&i2c_dev->lock, flags); + serviced = s5l8702_i2c_service_pend(i2c_dev); + spin_unlock_irqrestore(&i2c_dev->lock, flags); + if (serviced) { + if (!i2c_dev->polled_logged && !i2c_dev->isr_logged) { + i2c_dev->polled_logged = true; + dev_dbg(i2c_dev->dev, + "IIC IRQPEND polled (VIC irq=%d missed)\n", + i2c_dev->irq); + s5l8702_dump_vic(i2c_dev); + } + continue; + } + cpu_relax(); } + return i2c_dev->msg_ret; +} - i2c_dev->msg = msgs; - i2c_dev->nmsgs = num; - i2c_dev->msg_ret = 0; - i2c_dev->msg_pos = 0; - i2c_dev->state = STATE_START; +/* + * emcore/umsboot s5l87xx_i2c_recv_byte — the RX kick this IP needs. + * After the address IRQPEND, rewrite CON to 0xB7 (ACK) or 0x37 (NACK), + * wait for bit4, then read DS. Clearing bit4 (our write resume) does + * not clock a payload byte — glass #74 still had DS=addr8. + * Writes stay on the IRQPEND path. Not samsung,pio-mode. + */ +static int s5l8702_i2c_wait_con_pend(struct s5l8702_i2c_dev *i2c_dev) +{ + unsigned int i; - s5l8702_i2c_state_machine(i2c_dev); + for (i = 0; i < 200000; i++) { + if (s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_CON) & + S5L8702_I2C_CON_IRQPEND) + return 0; + cpu_relax(); + } + return -ETIMEDOUT; +} - time_left = wait_for_completion_timeout(&i2c_dev->msg_complete, - S5L8702_I2C_XFER_TIMEOUT); +static int s5l8702_i2c_xfer_read(struct s5l8702_i2c_dev *i2c_dev, + struct i2c_msg *msg) +{ + unsigned int i; + int ret; + u8 addr8 = i2c_8bit_addr_from_msg(msg); + static bool rd_ok_logged; + + disable_irq(i2c_dev->irq); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, addr8); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, 0xb0); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0xb7); + ret = s5l8702_i2c_wait_con_pend(i2c_dev); + if (ret) + goto out; + + for (i = 0; i < msg->len; i++) { + u8 ds; + u32 st; + bool ack = (i + 1 < msg->len); + + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, + ack ? 0xb7 : 0x37); + ret = s5l8702_i2c_wait_con_pend(i2c_dev); + if (ret) + goto out; + ds = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_DS) & 0xff; + st = s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & 0xff; + msg->buf[i] = ds; + if (!rd_ok_logged) { + rd_ok_logged = true; + dev_info(i2c_dev->dev, + "IIC RD DS=%02x STAT=%02x addr8=%02x%s\n", + ds, st, addr8, + (ds == addr8) ? " (still addr)" : ""); + } + } - dev_dbg(i2c_dev->dev, "%s done time_left=0x%04lx msg_ret=0x%04x", __func__, time_left, i2c_dev->msg_ret); - if (time_left == 0) - return -ETIMEDOUT; +out: + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, 0x90); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0xb7); + for (i = 0; i < 200000; i++) { + if (!(s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & + S5L8702_I2C_STAT_START)) + break; + cpu_relax(); + } + i2c_dev->iiccon = S5L8702_I2C_CON_IDLE; + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); + enable_irq(i2c_dev->irq); + if (ret) + dev_err_once(i2c_dev->dev, "IIC emcore-RX timeout\n"); + return ret; +} - return i2c_dev->msg_ret ? : num; +/* emcore i2c_send: DS then CON=0xB7, wait bit4. Same kick as RX. */ +static int s5l8702_i2c_xfer_write(struct s5l8702_i2c_dev *i2c_dev, + struct i2c_msg *msg) +{ + unsigned int i; + int ret; + u8 addr8 = i2c_8bit_addr_from_msg(msg); + + disable_irq(i2c_dev->irq); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, addr8); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, 0xf0); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0xb7); + ret = s5l8702_i2c_wait_con_pend(i2c_dev); + if (ret) + goto out; + for (i = 0; i < msg->len; i++) { + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, msg->buf[i]); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0xb7); + ret = s5l8702_i2c_wait_con_pend(i2c_dev); + if (ret) + goto out; + } +out: + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, 0xd0); + s5l8702_i2c_wait_rdy(i2c_dev); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0xb7); + for (i = 0; i < 200000; i++) { + if (!(s5l8702_i2c_readl(i2c_dev, S5L8702_I2C_STAT) & + S5L8702_I2C_STAT_START)) + break; + cpu_relax(); + } + i2c_dev->iiccon = S5L8702_I2C_CON_IDLE; + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); + enable_irq(i2c_dev->irq); + if (ret) + dev_err_once(i2c_dev->dev, "IIC emcore-TX timeout\n"); + return ret; } -static u32 s5l8702_i2c_func(struct i2c_adapter *adap) +static int s5l8702_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], + int num) { struct s5l8702_i2c_dev *i2c_dev = i2c_get_adapdata(adap); + int i, ret; + static bool split_logged; + + /* + * SEC 3F40 = 3F60 write of the register, STOP, then 1C8C READ. + * Linux smbus_read_byte_data is two msgs in one xfer (Sr). + * Chaining the READ START inside the WRITE's IRQ left STAT=0xe1 + * and every r1–r12 read returned that byte. + */ + if (!split_logged) { + split_logged = true; + dev_dbg(i2c_dev->dev, + "IIC one-msg xfer (SEC 1C8C STOP, IRQPEND write+read)\n"); + } + for (i = 0; i < num; i++) { + if (msgs[i].flags & I2C_M_RD) + ret = s5l8702_i2c_xfer_read(i2c_dev, &msgs[i]); + else + ret = s5l8702_i2c_xfer_write(i2c_dev, &msgs[i]); + if (ret) + return ret; + if (i + 1 < num) + udelay(100); + } + return num; +} - dev_dbg(i2c_dev->dev, "%s", __func__); - +static u32 s5l8702_i2c_func(struct i2c_adapter *adap) +{ return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; } @@ -266,69 +617,91 @@ static const struct i2c_algorithm s5l8702_i2c_algo = { .functionality = s5l8702_i2c_func, }; -static int s5l8702_i2c_init(struct s5l8702_i2c_dev *i2c_dev) { - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_ADD, 0x40); // [TODO] Get slave address from DT +#define S5L87XX_CLK_BASE 0x3C500000u +#define S5L87XX_PWRCON1 (S5L87XX_CLK_BASE + 0x4C) - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_UNK14, 1); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_UNK18, 0); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, S5L8702_I2C_STAT_MASTER); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, 0); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, 0); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_DS, 0x40); // [TODO] Get slave address from DT - - // [TODO] calculate divisors from freq in DT - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_CON, - S5L8702_I2C_CON_INTEN_BUSHOLD | S5L8702_I2C_CON_ACKGEN | - S5L8702_I2C_CON_CKSEL512 | S5L8702_I2C_CON_CK_REG(0)); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_STAT, S5L8702_I2C_STAT_SOE); - s5l8702_i2c_writel(i2c_dev, S5L8702_I2C_UNK28, 0); - - i2c_dev->iiccon = S5L8702_I2C_CON_INTEN_STOP | S5L8702_I2C_CON_INTEN_BUSHOLD | - S5L8702_I2C_CON_CKSEL512 | S5L8702_I2C_CON_CK_REG(0); +static void s5l8702_i2c_ungate(struct s5l8702_i2c_dev *i2c_dev, + resource_size_t base) +{ + void __iomem *pwr; + u32 val, mask; + + if (base == 0x3C600000) + mask = BIT(4); + else if (base == 0x3C900000) + mask = BIT(6); + else + return; + + pwr = ioremap(S5L87XX_PWRCON1, 4); + if (!pwr) + return; + val = readl(pwr) & ~mask; + writel(val, pwr); + iounmap(pwr); + udelay(50); +} +static int s5l8702_i2c_init(struct s5l8702_i2c_dev *i2c_dev) +{ + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_ADD, 0x40); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_UNK14, 1); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_UNK18, 0); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, 0); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, 0); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_DS, 0x40); + i2c_dev->iiccon = S5L8702_I2C_CON_IDLE; + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_CON, i2c_dev->iiccon); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_STAT, S5L8702_I2C_STAT_TXRXEN); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_UNK28, 0); + s5l8702_i2c_write_raw(i2c_dev, S5L8702_I2C_INT, S5L8702_I2C_INT_ALL); return 0; } static int s5l8702_i2c_probe(struct platform_device *pdev) { - dev_dbg(&pdev->dev, "%s", __func__); struct s5l8702_i2c_dev *i2c_dev; - int ret; struct i2c_adapter *adap; + struct resource *res; + int ret; i2c_dev = devm_kzalloc(&pdev->dev, sizeof(*i2c_dev), GFP_KERNEL); if (!i2c_dev) return -ENOMEM; platform_set_drvdata(pdev, i2c_dev); i2c_dev->dev = &pdev->dev; + spin_lock_init(&i2c_dev->lock); i2c_dev->regs = devm_platform_get_and_ioremap_resource(pdev, 0, NULL); if (IS_ERR(i2c_dev->regs)) return PTR_ERR(i2c_dev->regs); - i2c_dev->write_busy_poll = of_property_read_bool(pdev->dev.of_node, - "samsung,write-busy-poll"); + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (res) + s5l8702_i2c_ungate(i2c_dev, res->start); - ret = s5l8702_i2c_init(i2c_dev); - if (ret) { - dev_err(&pdev->dev, "Could initialize I2C controller\n"); - goto err; + ret = devm_clk_bulk_get_all(&pdev->dev, &i2c_dev->clks); + if (ret > 0) { + i2c_dev->num_clks = ret; + ret = clk_bulk_prepare_enable(i2c_dev->num_clks, i2c_dev->clks); + if (ret) + return ret; } + s5l8702_i2c_init(i2c_dev); + i2c_dev->irq = platform_get_irq(pdev, 0); - if (i2c_dev->irq < 0) { - ret = i2c_dev->irq; - goto err; - } + if (i2c_dev->irq < 0) + return i2c_dev->irq; - ret = devm_request_irq(&pdev->dev, i2c_dev->irq, s5l8702_i2c_isr, IRQF_SHARED, - dev_name(&pdev->dev), i2c_dev); - if (ret) { - dev_err(&pdev->dev, "Could not request IRQ\n"); - goto err; - } + ret = devm_request_irq(&pdev->dev, i2c_dev->irq, s5l8702_i2c_isr, 0, + dev_name(&pdev->dev), i2c_dev); + if (ret) + return ret; init_completion(&i2c_dev->msg_complete); + dev_info(&pdev->dev, "IIC IRQ INTPEND irq=%d CON=0x%03lx\n", + i2c_dev->irq, (unsigned long)S5L8702_I2C_CON_IDLE); adap = &i2c_dev->adapter; i2c_set_adapdata(adap, i2c_dev); @@ -340,29 +713,20 @@ static int s5l8702_i2c_probe(struct platform_device *pdev) adap->dev.parent = &pdev->dev; adap->dev.of_node = pdev->dev.of_node; - ret = devm_i2c_add_adapter(&pdev->dev, adap); - if (ret) - goto err; - - return 0; - -err: - return ret; + return devm_i2c_add_adapter(&pdev->dev, adap); } -#ifdef CONFIG_OF static const struct of_device_id s5l8702_i2c_of_match[] = { { .compatible = "samsung,s5l8702-i2c" }, - {}, + { } }; MODULE_DEVICE_TABLE(of, s5l8702_i2c_of_match); -#endif static struct platform_driver s5l8702_i2c_driver = { - .probe = s5l8702_i2c_probe, - .driver = { - .name = "i2c-s5l8702", - .of_match_table = of_match_ptr(s5l8702_i2c_of_match), + .probe = s5l8702_i2c_probe, + .driver = { + .name = "i2c-s5l8702", + .of_match_table = s5l8702_i2c_of_match, }, }; module_platform_driver(s5l8702_i2c_driver); From 8b53e1d4cef4181b3fb560e7ff57578761eb0cbe Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 20:58:34 -0230 Subject: [PATCH 04/31] gpio: d1830: nIRQ keys, power-off, no rail writes at probe Home / Sleep / Play are PMIC bits (regs 7 and 8), not SoC GPIO. OSOS unmasks them and uses GPIO 86 as an active-low nIRQ into the EIC. gpio-keys-polled on these lines hammers I2C and was already disabled in the N31 DTS. Register an input device and a threaded nIRQ. Keep a slow poll so a missed EIC edge still shows up. Do not write register 13 at probe. Bit 0 is the power-off latch and it cuts Vbat. The old default rail sequence did that. Rail bring-up stays behind dlg,apply-sec-rails, which N31 must not set. pm_power_off writes reg 13 bit 0. Sleep held ~2s still uses that path. Tested on iPod nano 7G: Home, Sleep, Play, and Sleep-hold poweroff. --- drivers/gpio/gpio-d1830.c | 917 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 890 insertions(+), 27 deletions(-) diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index d15d0ef5600bec..ce5c85d60c8ecc 100644 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -1,17 +1,58 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * GPIO Driver for Dialog Semiconductor D1830 PMIC + * GPIO driver for Dialog Semiconductor D1830 PMIC * - * This driver exposes specific bits of PMIC registers as GPIO lines. - * It is read-only and intended for button polling via gpio-keys-polled. + * Exposes selected PMIC register bits as GPIO lines for gpio-keys-polled, + * implements machine power-off via SEC-observed reg 13 bit0, and registers + * a power_supply battery using the OSOS ADC path (not ACPI — this SoC + * has none; power_supply is the Linux equivalent). + * + * OSOS RetailOS 1.0.2: + * 439A98(1) → 2C778(channel 3, 5 samples) → 3477C / 347E4 / 3484C + * start: reg48 = (reg48 & 0xF0) | (ch & 0xF) | 0x10 + * data: 10-bit = (reg50 << 2) | reg49 + * 158C82(3/5) writes bitfields in 87/88 — not the ADC. Keep poweroff + * unchanged. Do not enable dlg,apply-sec-rails from here. * * Copyright (C) 2026 Vencislav Atanasov */ - -#include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include + +#define D1830_REG_POWEROFF 13 +#define D1830_POWEROFF_BIT BIT(0) +#define D1830_REG_ADC_CFG 48 +#define D1830_REG_ADC_LOW 49 +#define D1830_REG_ADC_HIGH 50 +#define D1830_ADC_CH_VBAT 3 /* OSOS 439A98 case 1 */ +#define D1830_ADC_START 0x10 +#define D1830_ADC_SAMPLES 5 +#define D1830_ADC_FS_MV 6000 /* 10-bit, 6 V FS (emcore/Apple) */ +#define D1830_DESIGN_UAH 200000 /* nano 7 pack, 200 mAh */ +#define D1830_DESIGN_MIN_UV 3300000 +#define D1830_DESIGN_MAX_UV 4200000 + +int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); +void s5l8740_n31_report_key(unsigned int code, int pressed); +int s5l8740_n31_din86(void); +extern void (*d1830_n31_din_nirq_hook)(void); + +/* Provisional Li-ion empty/full for capacity % (OPEN scale) */ +#define D1830_MV_EMPTY 3300 +#define D1830_MV_FULL 4200 struct d1830_gpio_map { u8 reg; @@ -23,8 +64,27 @@ struct d1830_gpio { struct gpio_chip gpio_chip; struct d1830_gpio_map *map; int num_gpios; + struct power_supply *psy; + struct power_supply *usb_psy; + struct input_dev *input; + u8 last_home, last_sleep, last_play; + u8 sleep_hold; + int last_r5, last_r6, last_r7, last_r8; + bool keys_inited; + bool lsb_logged; + int last_mv; + u16 last_adc; + u8 last_r48, last_r49, last_r50; + unsigned long last_adc_jiffies; + int psy_ticks; + struct delayed_work trace; + int trace_r[12]; + int trace_din; + bool trace_inited; }; +static struct i2c_client *d1830_poweroff_client; + static int d1830_gpio_get_direction(struct gpio_chip *chip, unsigned int offset) { return GPIO_LINE_DIRECTION_IN; @@ -36,19 +96,17 @@ static int d1830_gpio_get(struct gpio_chip *chip, unsigned int offset) struct d1830_gpio_map *entry; int ret; - if (offset >= gpio_dev->num_gpios) { + if (offset >= gpio_dev->num_gpios) return -EINVAL; - } entry = &gpio_dev->map[offset]; - ret = i2c_smbus_read_byte_data(gpio_dev->client, entry->reg); if (ret < 0) { - dev_err(&gpio_dev->client->dev, - "Failed to read reg 0x%02x: %d\n", entry->reg, ret); + dev_err_ratelimited(&gpio_dev->client->dev, + "Failed to read reg 0x%02x: %d\n", + entry->reg, ret); return 0; } - return !!(ret & BIT(entry->bit)); } @@ -57,14 +115,14 @@ static int d1830_gpio_direction_input(struct gpio_chip *chip, unsigned int offse return 0; } -static int d1830_gpio_direction_output(struct gpio_chip *chip, unsigned int offset, int value) +static int d1830_gpio_direction_output(struct gpio_chip *chip, unsigned int offset, + int value) { return -ENOTSUPP; } static void d1830_gpio_set(struct gpio_chip *chip, unsigned int offset, int value) { - // read-only } static int d1830_gpio_parse_dt(struct d1830_gpio *gpio_dev) @@ -78,38 +136,683 @@ static int d1830_gpio_parse_dt(struct d1830_gpio *gpio_dev) size = of_property_count_u32_elems(np, "dlg,gpio-map"); if (size <= 0 || size % 2) { - dev_err(dev, "Invalid or missing 'dlg,gpio-map' property " - "(size=%d)\n", size); + dev_err(dev, "Invalid or missing 'dlg,gpio-map' (size=%d)\n", size); return -EINVAL; } gpio_dev->num_gpios = size / 2; - gpio_dev->map = devm_kcalloc(dev, gpio_dev->num_gpios, - sizeof(*gpio_dev->map), GFP_KERNEL); + sizeof(*gpio_dev->map), GFP_KERNEL); if (!gpio_dev->map) return -ENOMEM; for (i = 0; i < gpio_dev->num_gpios; i++) { u32 reg, bit; - of_property_read_u32_index(np, "dlg,gpio-map", - i * 2, ®); - of_property_read_u32_index(np, "dlg,gpio-map", - i * 2 + 1, &bit); - + of_property_read_u32_index(np, "dlg,gpio-map", i * 2, ®); + of_property_read_u32_index(np, "dlg,gpio-map", i * 2 + 1, &bit); if (reg > 0xff || bit > 7) { dev_err(dev, "GPIO %d: reg=0x%x bit=%u out of range\n", i, reg, bit); return -EINVAL; } - gpio_dev->map[i].reg = (u8)reg; gpio_dev->map[i].bit = (u8)bit; + } + return 0; +} + +static void d1830_cut_power(struct i2c_client *client) +{ + int v, ret; + u8 out; - dev_dbg(dev, "GPIO %d -> reg 0x%02x bit %u\n", i, reg, bit); + if (!client) + return; + + dev_emerg(&client->dev, "PMIC poweroff: reg %u |= 0x%02lx\n", + D1830_REG_POWEROFF, D1830_POWEROFF_BIT); + + v = i2c_smbus_read_byte_data(client, D1830_REG_POWEROFF); + out = (v < 0) ? (u8)D1830_POWEROFF_BIT : (u8)(v | D1830_POWEROFF_BIT); + + ret = i2c_smbus_write_byte_data(client, D1830_REG_POWEROFF, out); + if (ret) { + dev_emerg(&client->dev, "PMIC poweroff write failed (%d); retry raw 1\n", + ret); + i2c_smbus_write_byte_data(client, D1830_REG_POWEROFF, + (u8)D1830_POWEROFF_BIT); } + mdelay(100); + while (1) + cpu_relax(); +} + +static void d1830_pm_power_off(void) +{ + d1830_cut_power(d1830_poweroff_client); +} + +static ssize_t do_poweroff_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev); + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + d1830_cut_power(client); + return count; +} +static DEVICE_ATTR_WO(do_poweroff); + +/* + * Chain from RE (do not invert without new ARM): + * user key → PMIC status r5-r8 (sub_26520, read-only in OSOS) + * → SoC GPIO 86 DIN 0 = asserted (sub_42BBEC; EFBB4 loops while DIN==0) + * → EIC g2 b22 INTLEVEL=0 INTTYPE=1 (40641C(86,1); 7D490 set=high/clear=low) + * → VIC EXT3 + * SEC sub_27F4 is IIC1 + rail/hibernate RMW. No PMIC MCU image, no write of + * r5-r8. OSOS 4118BC never writes 5-8. Linux must not replay 27F4 (reg13). + */ +static void d1830_dump_irq_chain(struct i2c_client *client, const char *tag) +{ + void __iomem *eic, *gpio; + u8 i; + int v[12]; + u32 din, pcon, dir, level, stat, en, itype; + + for (i = 0; i < 12; i++) + v[i] = i2c_smbus_read_byte_data(client, i + 1); + dev_dbg(&client->dev, + "n31-pmic %s r1-8=%02x %02x %02x %02x %02x %02x %02x %02x r9-12=%02x %02x %02x %02x\n", + tag, + v[0] < 0 ? 0 : v[0], v[1] < 0 ? 0 : v[1], + v[2] < 0 ? 0 : v[2], v[3] < 0 ? 0 : v[3], + v[4] < 0 ? 0 : v[4], v[5] < 0 ? 0 : v[5], + v[6] < 0 ? 0 : v[6], v[7] < 0 ? 0 : v[7], + v[8] < 0 ? 0 : v[8], v[9] < 0 ? 0 : v[9], + v[10] < 0 ? 0 : v[10], v[11] < 0 ? 0 : v[11]); + + /* GPIO 86: bank 10, pin 6. EIC group 2, bit 22. */ + gpio = ioremap(0x3cf00000ul + 32u * 10u, 32); + eic = ioremap(0x39700000ul, 0x100); + if (gpio && eic) { + din = readl(gpio + 0x04); + pcon = readl(gpio); + dir = readl(gpio + 0x14); + level = readl(eic + 0x80 + 8); + stat = readl(eic + 0xa0 + 8); + en = readl(eic + 0xc0 + 8); + itype = readl(eic + 0xe0 + 8); + dev_dbg(&client->dev, + "n31-pmic %s gpio86 din=%u dir=%u pcon=%08x eic g2 L=%08x S=%08x E=%08x T=%08x bit22 L=%u S=%u E=%u T=%u irq=%d\n", + tag, !!(din & BIT(6)), !!(dir & BIT(6)), pcon, + level, stat, en, itype, + !!(level & BIT(22)), !!(stat & BIT(22)), + !!(en & BIT(22)), !!(itype & BIT(22)), + client->irq); + } + if (gpio) + iounmap(gpio); + if (eic) + iounmap(eic); + + { + int r14 = i2c_smbus_read_byte_data(client, 14); + int r41 = i2c_smbus_read_byte_data(client, 41); + int r42 = i2c_smbus_read_byte_data(client, 42); + int r43 = i2c_smbus_read_byte_data(client, 43); + int r60 = i2c_smbus_read_byte_data(client, 60); + + dev_dbg(&client->dev, + "n31-pmic %s r14=%02x r41=%02x r42=%02x r43=%02x r60=%02x (SEC want 14=20 41=(x&ec)|10 42=(x&c0)|14 43=(x&f0)|01 60=01)\n", + tag, + r14 < 0 ? 0 : r14, r41 < 0 ? 0 : r41, + r42 < 0 ? 0 : r42, r43 < 0 ? 0 : r43, + r60 < 0 ? 0 : r60); + } +} + +/* + * OSOS BatteryLevel 0x588150 (not PCFPowerMgr, not SEC): + * 51686C → 174288(0x891DB48, 0x1c1f40ee) + * 174288 sbfx+1 per bit → write ~bytes to regs 9-12 + * then 5D308(gpio 0x56, level=0, type=1) and EFBB4 + * SEC 27F4 never writes 9-12. Linux DFU skips OSOS, so replay this mask. + * 158C82(3/5) in the same task is ADC regs 87/88 — not nIRQ. + */ +static void d1830_osos_nirq_mask(struct i2c_client *client) +{ + static const u8 regs[] = { 9, 10, 11, 12 }; + static const u8 vals[] = { 0x11, 0xbf, 0xe0, 0xe3 }; + int i, ret, before[4]; + + for (i = 0; i < 4; i++) + before[i] = i2c_smbus_read_byte_data(client, regs[i]); + for (i = 0; i < 4; i++) { + ret = i2c_smbus_write_byte_data(client, regs[i], vals[i]); + if (ret) + dev_err(&client->dev, + "n31-pmic OSOS r%u=0x%02x write %d\n", + regs[i], vals[i], ret); + } + dev_dbg(&client->dev, + "n31-pmic OSOS 174288 mask r9-12 %02x %02x %02x %02x -> 11 bf e0 e3\n", + before[0] < 0 ? 0 : before[0], before[1] < 0 ? 0 : before[1], + before[2] < 0 ? 0 : before[2], before[3] < 0 ? 0 : before[3]); +} + +static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev); + +/* GPIO 86 DIN edge from n31-btn poll — I2C read even if EIC missed the line. */ +void d1830_n31_din_nirq(void) +{ + struct i2c_client *client = d1830_poweroff_client; + struct d1830_gpio *gpio_dev; + + if (!client) + return; + gpio_dev = i2c_get_clientdata(client); + if (gpio_dev) + d1830_btn_poll_once(gpio_dev); +} + +static irqreturn_t d1830_irq_thread(int irq, void *data) +{ + struct d1830_gpio *gpio_dev = data; + static unsigned hits; + + hits++; + if (hits <= 8 || (hits & 0x3f) == 0) { + dev_dbg(&gpio_dev->client->dev, + "n31-pmic nIRQ irq=%d hit=%u (GPIO86 EIC g2 b22)\n", + irq, hits); + if (hits == 1) + d1830_dump_irq_chain(gpio_dev->client, "nirq1"); + } + d1830_btn_poll_once(gpio_dev); + return IRQ_HANDLED; +} + +static void d1830_key_active_low(struct d1830_gpio *gpio_dev, unsigned int code, + u8 now_bit, u8 *last, const char *name) +{ + bool pressed, was; + + if (now_bit == *last) + return; + /* OSOS sub_4195D8(id, bit==0): pressed when the status bit is clear. */ + pressed = !now_bit; + was = !(*last); + *last = now_bit; + if (pressed == was) + return; + dev_dbg(&gpio_dev->client->dev, + "n31-btn %s %s (bit=%u, 0=pressed OSOS)\n", + name, pressed ? "PRESS" : "release", now_bit); + s5l8740_n31_report_key(code, pressed); + if (gpio_dev->input) { + input_report_key(gpio_dev->input, code, pressed); + input_sync(gpio_dev->input); + } +} + +static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) +{ + struct i2c_client *client = gpio_dev->client; + int r5, r6, r7, r8; + u8 home, sleep, play; + + r5 = i2c_smbus_read_byte_data(client, 5); + r6 = i2c_smbus_read_byte_data(client, 6); + r7 = i2c_smbus_read_byte_data(client, 7); + r8 = i2c_smbus_read_byte_data(client, 8); + if (r7 < 0) + return; + + /* OSOS sub_26520: Home=r7b4, Sleep=r7b5, Play=r8b1. */ + home = !!(r7 & BIT(4)); + sleep = !!(r7 & BIT(5)); + play = (r8 >= 0) ? !!(r8 & BIT(1)) : 1; + + if (!gpio_dev->keys_inited) { + gpio_dev->last_home = home; + gpio_dev->last_sleep = sleep; + gpio_dev->last_play = play; + gpio_dev->last_r5 = r5 < 0 ? 0 : r5; + gpio_dev->last_r6 = r6 < 0 ? 0 : r6; + gpio_dev->last_r7 = r7; + gpio_dev->last_r8 = r8 < 0 ? 0 : r8; + gpio_dev->keys_inited = true; + dev_dbg(&client->dev, + "n31-pmic idle r5=0x%02x r6=0x%02x r7=0x%02x r8=0x%02x home=%u sleep=%u play=%u (OSOS active-low)\n", + r5 < 0 ? 0 : r5, r6 < 0 ? 0 : r6, r7, + r8 < 0 ? 0 : r8, home, sleep, play); + return; + } + + if (r5 >= 0 && r5 != gpio_dev->last_r5) + dev_dbg(&client->dev, "n31-pmic r5 0x%02x->0x%02x xor=0x%02x\n", + gpio_dev->last_r5, r5, gpio_dev->last_r5 ^ r5); + if (r6 >= 0 && r6 != gpio_dev->last_r6) + dev_dbg(&client->dev, "n31-pmic r6 0x%02x->0x%02x xor=0x%02x\n", + gpio_dev->last_r6, r6, gpio_dev->last_r6 ^ r6); + if (r7 != gpio_dev->last_r7) + dev_dbg(&client->dev, "n31-pmic r7 0x%02x->0x%02x xor=0x%02x\n", + gpio_dev->last_r7, r7, gpio_dev->last_r7 ^ r7); + if (r8 >= 0 && r8 != gpio_dev->last_r8) + dev_dbg(&client->dev, "n31-pmic r8 0x%02x->0x%02x xor=0x%02x\n", + gpio_dev->last_r8, r8, gpio_dev->last_r8 ^ r8); + + d1830_key_active_low(gpio_dev, KEY_HOMEPAGE, home, + &gpio_dev->last_home, "HOME"); + d1830_key_active_low(gpio_dev, KEY_PLAYPAUSE, play, + &gpio_dev->last_play, "PLAY"); + + if (!sleep) { + if (gpio_dev->last_sleep) { + dev_dbg(&client->dev, + "n31-btn SLEEP PRESS r7=0x%02x (bit5 1->0)\n", + r7); + s5l8740_n31_report_key(KEY_POWER, 1); + if (gpio_dev->input) { + input_report_key(gpio_dev->input, KEY_POWER, 1); + input_sync(gpio_dev->input); + } + } + /* Hold Sleep across 5 polls (~500ms) before cutting power. + * One noisy I2C byte must not hibernate. */ + if (gpio_dev->sleep_hold < 5) + gpio_dev->sleep_hold++; + if (gpio_dev->sleep_hold == 5) { + dev_warn(&client->dev, + "n31-btn SLEEP held — poweroff\n"); + d1830_cut_power(client); + } + } else { + if (!gpio_dev->last_sleep) { + dev_dbg(&client->dev, + "n31-btn SLEEP release r7=0x%02x\n", r7); + s5l8740_n31_report_key(KEY_POWER, 0); + if (gpio_dev->input) { + input_report_key(gpio_dev->input, KEY_POWER, 0); + input_sync(gpio_dev->input); + } + } + gpio_dev->sleep_hold = 0; + } + gpio_dev->last_sleep = sleep; + if (r5 >= 0) + gpio_dev->last_r5 = r5; + if (r6 >= 0) + gpio_dev->last_r6 = r6; + gpio_dev->last_r7 = r7; + if (r8 >= 0) + gpio_dev->last_r8 = r8; +} + +/* Split the silent-PMIC case: do r5-r8 bits move when Home/Play/Sleep + * are pressed, and does GPIO 86 DIN follow? Not a product poll. + */ +static void d1830_trace_work(struct work_struct *work) +{ + struct d1830_gpio *gpio_dev = container_of(to_delayed_work(work), + struct d1830_gpio, trace); + + d1830_btn_poll_once(gpio_dev); + if (gpio_dev->psy && ++gpio_dev->psy_ticks >= 100) { + gpio_dev->psy_ticks = 0; + power_supply_changed(gpio_dev->psy); + if (gpio_dev->usb_psy) + power_supply_changed(gpio_dev->usb_psy); + } + schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(100)); +} + +/* + * OSOS 3477C + 347E4 + 2C778(3, 5). Channel 3 is VBAT. 10-bit sample + * averaged 5×. Scale: 10-bit × 6 mV (6 V FS). 439A98 then >>2; that + * is logged, not used for µV. No writes to 87/88 (158C82 bitfields). + */ +static int d1830_adc_once(struct d1830_gpio *gpio_dev, int *adc, + u8 *r48, u8 *r49, u8 *r50) +{ + struct i2c_client *client = gpio_dev->client; + int cfg, hi, lo, i; + + cfg = i2c_smbus_read_byte_data(client, D1830_REG_ADC_CFG); + if (cfg < 0) + return cfg; + if (cfg & D1830_ADC_START) { + usleep_range(1000, 1500); + cfg = i2c_smbus_read_byte_data(client, D1830_REG_ADC_CFG); + if (cfg < 0) + return cfg; + if (cfg & D1830_ADC_START) + return -EBUSY; + } + cfg = i2c_smbus_write_byte_data(client, D1830_REG_ADC_CFG, + (cfg & 0xF0) | D1830_ADC_CH_VBAT | + D1830_ADC_START); + if (cfg) + return cfg; + + for (i = 0; i < 5; i++) { + usleep_range(1000, 1500); + cfg = i2c_smbus_read_byte_data(client, D1830_REG_ADC_CFG); + if (cfg < 0) + return cfg; + if (cfg & D1830_ADC_START) + break; + } + + hi = i2c_smbus_read_byte_data(client, D1830_REG_ADC_HIGH); + lo = i2c_smbus_read_byte_data(client, D1830_REG_ADC_LOW); + if (hi < 0) + return hi; + if (lo < 0) + return lo; + *r48 = (u8)cfg; + *r49 = (u8)lo; + *r50 = (u8)hi; + /* 347E4: (reg50 << 2) | reg49. Mask 10-bit; low nibble is 2 LSBs. */ + *adc = ((hi << 2) | lo) & 0x3ff; + return 0; +} + +static int d1830_adc_to_mv(int adc) +{ + return (adc * D1830_ADC_FS_MV) / 1023; +} + +static int d1830_read_vbat(struct d1830_gpio *gpio_dev, int *mv) +{ + int i, ret, adc, sum = 0, n = 0; + u8 r48 = 0, r49 = 0, r50 = 0; + + if (gpio_dev->last_adc_jiffies && + time_before(jiffies, gpio_dev->last_adc_jiffies + HZ / 2) && + gpio_dev->last_mv > 0) { + *mv = gpio_dev->last_mv; + return 0; + } + + for (i = 0; i < D1830_ADC_SAMPLES; i++) { + ret = d1830_adc_once(gpio_dev, &adc, &r48, &r49, &r50); + if (ret) { + if (n) + break; + return ret; + } + sum += adc; + n++; + } + if (!n) + return -EIO; + adc = sum / n; + *mv = d1830_adc_to_mv(adc); + gpio_dev->last_adc = (u16)adc; + gpio_dev->last_mv = *mv; + gpio_dev->last_r48 = r48; + gpio_dev->last_r49 = r49; + gpio_dev->last_r50 = r50; + gpio_dev->last_adc_jiffies = jiffies; + + if (!gpio_dev->lsb_logged) { + dev_dbg(&gpio_dev->client->dev, + "n31-bat OSOS 2C778 ch3 adc=%u r48=%02x r49=%02x r50=%02x mv=%d osos>>2=%u\n", + adc, r48, r49, r50, *mv, adc >> 2); + gpio_dev->lsb_logged = true; + } + return 0; +} + +static int d1830_get_vbat_uV(struct d1830_gpio *gpio_dev, int *val) +{ + int mv, ret; + + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) + return ret; + *val = mv * 1000; + return 0; +} + +static int d1830_get_capacity(struct d1830_gpio *gpio_dev, int *val) +{ + int mv, pct, ret; + + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) + return ret; + if (mv <= D1830_MV_EMPTY) + pct = 0; + else if (mv >= D1830_MV_FULL) + pct = 100; + else + pct = ((mv - D1830_MV_EMPTY) * 100) / + (D1830_MV_FULL - D1830_MV_EMPTY); + *val = clamp_val(pct, 0, 100); + return 0; +} + +static ssize_t vbat_raw_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct d1830_gpio *gpio_dev = i2c_get_clientdata(to_i2c_client(dev)); + int mv, ret; + + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) + return ret; + return sysfs_emit(buf, + "r48=%02x r49=%02x r50=%02x adc=%u mv=%d\n", + gpio_dev->last_r48, gpio_dev->last_r49, + gpio_dev->last_r50, gpio_dev->last_adc, mv); +} +static DEVICE_ATTR_RO(vbat_raw); + +static int d1830_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct d1830_gpio *gpio_dev = power_supply_get_drvdata(psy); + int mv, pct, ret; + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return d1830_get_vbat_uV(gpio_dev, &val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = D1830_DESIGN_MIN_UV; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = D1830_DESIGN_MAX_UV; + return 0; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = D1830_DESIGN_UAH; + return 0; + case POWER_SUPPLY_PROP_CAPACITY: + return d1830_get_capacity(gpio_dev, &val->intval); + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + ret = d1830_get_capacity(gpio_dev, &pct); + if (ret) + return ret; + if (pct <= 5) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else if (pct <= 15) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (pct >= 95) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (pct >= 80) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + return 0; + case POWER_SUPPLY_PROP_STATUS: + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) { + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + return 0; + } + /* USB gadget is the only supply we have; no charge-bit RE. */ + if (mv >= 4150) + val->intval = POWER_SUPPLY_STATUS_FULL; + else + val->intval = POWER_SUPPLY_STATUS_DISCHARGING; + return 0; + case POWER_SUPPLY_PROP_HEALTH: + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + return 0; + } + if (mv < 3000) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (mv > 4300) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return 0; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + return 0; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + return 0; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + return 0; + default: + return -EINVAL; + } +} + +static enum power_supply_property d1830_psy_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_SCOPE, +}; + +static int d1830_usb_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + /* Gadget host is the only path this image runs. */ + val->intval = 1; + return 0; + case POWER_SUPPLY_PROP_USB_TYPE: + val->intval = POWER_SUPPLY_USB_TYPE_SDP; + return 0; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + return 0; + default: + return -EINVAL; + } +} + +static enum power_supply_property d1830_usb_props[] = { + POWER_SUPPLY_PROP_ONLINE, + POWER_SUPPLY_PROP_USB_TYPE, + POWER_SUPPLY_PROP_SCOPE, +}; + +static int d1830_rmw(struct i2c_client *client, u8 reg, u8 clear, u8 set) +{ + int v = i2c_smbus_read_byte_data(client, reg); + + if (v < 0) + return v; + return i2c_smbus_write_byte_data(client, reg, (u8)((v & ~clear) | set)); +} + +/* + * IpodSec PMIC rail / charge bring-up: + * sub_23EC — regs 20–23,26,16,17,19,35 (charge/rail-ish) + * sub_27F4 — IIC1 init already done by i2c driver; apply safe RMW sequence + * (skip hibernate Stpr cookie / fatal halt paths). + */ +static int d1830_sec_rail_seq(struct i2c_client *client) +{ + struct device *dev = &client->dev; + int ret, v; + u8 b; + + /* --- sub_23EC (rail/charge) --- */ + /* Reg20 ← (delay-derived) & 0x1F: use 0x10 as safe mid rail enable-ish */ + ret = i2c_smbus_write_byte_data(client, 20, 0x10); + if (ret) + dev_warn(dev, "rail reg20: %d\n", ret); + + v = i2c_smbus_read_byte_data(client, 35); + if (v >= 0) { + b = (u8)(v & 0xFC); + ret = i2c_smbus_write_byte_data(client, 35, b); + if (ret) + dev_warn(dev, "rail reg35: %d\n", ret); + } + + /* Regs 21–23 same pattern as 20 in SEC loop — use 0x10 */ + i2c_smbus_write_byte_data(client, 21, 0x10); + i2c_smbus_write_byte_data(client, 22, 0x10); + i2c_smbus_write_byte_data(client, 23, 0x10); + + /* Reg26 ← 0xB2 (-78) twice in SEC */ + i2c_smbus_write_byte_data(client, 26, 0xB2); + i2c_smbus_write_byte_data(client, 26, 0xB2); + + v = i2c_smbus_read_byte_data(client, 16); + if (v >= 0) { + b = (u8)((v & 0x2F) | 0x10); + /* optional |0x20 path when a1 set — keep base */ + i2c_smbus_write_byte_data(client, 16, b); + } + + v = i2c_smbus_read_byte_data(client, 17); + if (v >= 0) + i2c_smbus_write_byte_data(client, 17, (u8)(v | 0x07)); + + v = i2c_smbus_read_byte_data(client, 19); + if (v >= 0) + i2c_smbus_write_byte_data(client, 19, (u8)(v | 0x02)); + + /* --- sub_27F4 safe subset (non-fatal) --- */ + i2c_smbus_write_byte_data(client, 2, 0x80); + i2c_smbus_write_byte_data(client, 73, 0x00); + i2c_smbus_write_byte_data(client, 1, 0x00); + /* clear 4-byte cookie @96 without hibernate SPI (SEC writes 4 bytes) */ + if (i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)) { + u8 z[4] = { 0, 0, 0, 0 }; + + i2c_smbus_write_i2c_block_data(client, 96, 4, z); + } else { + i2c_smbus_write_byte_data(client, 96, 0); + } + /* NEVER write reg 13 here — bit0 is D1830_POWEROFF_BIT (cuts Vbat). */ + d1830_rmw(client, 48, 0, 0x40); /* |= 0x40 */ + d1830_rmw(client, 89, 0x1C, 0); /* &= 0xE3 */ + i2c_smbus_write_byte_data(client, 60, 0x01); + d1830_rmw(client, 41, 0x13, 0x10); /* (x & 0xEC) | 0x10 */ + d1830_rmw(client, 42, 0x3F, 0x14); /* (x & 0xC0) | 0x14 */ + d1830_rmw(client, 43, 0x0F, 0x01); /* (x & 0xF0) | 0x01 */ + i2c_smbus_write_byte_data(client, 14, 0x20); + /* 36/37 depend on ADC helper — leave unread defaults */ + d1830_rmw(client, 38, 0x01, 0); /* clear bit0 */ + /* do not RMW reg 13 — poweroff register */ + + dev_info(dev, "SEC PMIC rail seq applied (sub_23EC + sub_27F4 safe)\n"); return 0; } @@ -135,6 +838,31 @@ static int d1830_gpio_probe(struct i2c_client *client) if (ret) return ret; + /* Opt-in only. Default probe is GPIO + VBAT reads — no rail writes. + * The old default seq wrote reg 13 = 0x01 (POWEROFF bit) at boot. + */ + if (of_property_read_bool(dev->of_node, "dlg,apply-sec-rails")) + d1830_sec_rail_seq(client); + else + dev_info(dev, "d1830 gpio-only (rail seq off; set dlg,apply-sec-rails to enable)\n"); + + { + static const u8 dump_regs[] = { + 1, 2, 3, 5, 13, 14, 16, 17, 19, 20, 21, 22, 23, + 26, 35, 36, 37, 41, 48, 49, 50, 96, 110, 111 + }; + int i, v; + + dev_dbg(dev, "PMIC identity dump @0x%02x:\n", client->addr); + for (i = 0; i < ARRAY_SIZE(dump_regs); i++) { + v = i2c_smbus_read_byte_data(client, dump_regs[i]); + if (v < 0) + dev_dbg(dev, " reg 0x%02u: ERR %d\n", dump_regs[i], v); + else + dev_dbg(dev, " reg 0x%02u = 0x%02x\n", dump_regs[i], v); + } + } + gpio_dev->gpio_chip.label = dev_name(dev); gpio_dev->gpio_chip.parent = dev; gpio_dev->gpio_chip.owner = THIS_MODULE; @@ -153,11 +881,145 @@ static int d1830_gpio_probe(struct i2c_client *client) return ret; } + ret = device_create_file(dev, &dev_attr_do_poweroff); + if (ret) + dev_warn(dev, "sysfs do_poweroff unavailable: %d\n", ret); + + ret = device_create_file(dev, &dev_attr_vbat_raw); + if (ret) + dev_warn(dev, "sysfs vbat_raw unavailable: %d\n", ret); + + d1830_poweroff_client = client; + if (!pm_power_off) { + pm_power_off = d1830_pm_power_off; + dev_info(dev, "registered pm_power_off (SEC reg %u bit0)\n", + D1830_REG_POWEROFF); + } else { + dev_warn(dev, "pm_power_off already set — sysfs do_poweroff only\n"); + } + + /* OSOS 9-12 mask only. No 27F4 tail, no 1-4 writeback, no IIC1 peek. */ + d1830_dump_irq_chain(client, "sec-left"); + d1830_osos_nirq_mask(client); + d1830_dump_irq_chain(client, "osos-mask"); + + if (client->irq > 0) { + struct irq_data *d = irq_get_irq_data(client->irq); + + if (d) + s5l8740_eic_enable_gpio(d->hwirq, IRQ_TYPE_LEVEL_LOW); + ret = devm_request_threaded_irq(dev, client->irq, NULL, + d1830_irq_thread, + IRQF_ONESHOT | IRQF_TRIGGER_LOW, + "d1830-nirq", gpio_dev); + if (ret) + dev_err(dev, "PMIC nIRQ %d failed: %d\n", + client->irq, ret); + else + dev_info(dev, + "PMIC nIRQ virq=%d hwirq=%lu LEVEL_LOW (OSOS 40641C type=1, EFBB4 DIN=0)\n", + client->irq, d ? d->hwirq : 0); + } else { + dev_err(dev, "no PMIC nIRQ in DT (of_irq did not map GPIO 86)\n"); + } + d1830_dump_irq_chain(client, "irq-on"); + + { + int v = i2c_smbus_read_byte_data(client, 7); + + dev_info(dev, + "PMIC 7bit=0x%02x wire WR=0x%02x RD=0x%02x reg7 %s (%d)\n", + client->addr, client->addr << 1, + (client->addr << 1) | 1, + v < 0 ? "read fail" : "ok", v); + } + + { + struct power_supply_config psy_cfg = { + .drv_data = gpio_dev, + .of_node = dev->of_node, + }; + struct power_supply_desc *desc; + int mv; + + desc = devm_kzalloc(dev, sizeof(*desc), GFP_KERNEL); + if (desc) { + desc->name = "d1830-battery"; + desc->type = POWER_SUPPLY_TYPE_BATTERY; + desc->properties = d1830_psy_props; + desc->num_properties = ARRAY_SIZE(d1830_psy_props); + desc->get_property = d1830_psy_get_property; + gpio_dev->psy = devm_power_supply_register(dev, desc, &psy_cfg); + if (IS_ERR(gpio_dev->psy)) { + dev_warn(dev, "battery psy unavailable: %ld\n", + PTR_ERR(gpio_dev->psy)); + gpio_dev->psy = NULL; + } else if (!d1830_read_vbat(gpio_dev, &mv)) { + dev_info(dev, + "battery psy OSOS ch3 10-bit*6 mV=%d (design %u mAh)\n", + mv, D1830_DESIGN_UAH / 1000); + } + } + + desc = devm_kzalloc(dev, sizeof(*desc), GFP_KERNEL); + if (desc) { + desc->name = "d1830-usb"; + desc->type = POWER_SUPPLY_TYPE_USB; + desc->properties = d1830_usb_props; + desc->num_properties = ARRAY_SIZE(d1830_usb_props); + desc->get_property = d1830_usb_get_property; + desc->usb_types = BIT(POWER_SUPPLY_USB_TYPE_SDP); + gpio_dev->usb_psy = devm_power_supply_register(dev, desc, + &psy_cfg); + if (IS_ERR(gpio_dev->usb_psy)) { + dev_warn(dev, "usb psy unavailable: %ld\n", + PTR_ERR(gpio_dev->usb_psy)); + gpio_dev->usb_psy = NULL; + } + } + } + dev_info(dev, "Registered %u read-only GPIOs using Dialog D1830 driver\n", - gpio_dev->num_gpios); + gpio_dev->num_gpios); + + gpio_dev->input = devm_input_allocate_device(dev); + if (gpio_dev->input) { + gpio_dev->input->name = "n31-pmic-buttons"; + gpio_dev->input->phys = "d1830/gpio"; + gpio_dev->input->dev.parent = dev; + gpio_dev->input->id.bustype = BUS_I2C; + input_set_capability(gpio_dev->input, EV_KEY, KEY_HOMEPAGE); + input_set_capability(gpio_dev->input, EV_KEY, KEY_POWER); + input_set_capability(gpio_dev->input, EV_KEY, KEY_PLAYPAUSE); + if (input_register_device(gpio_dev->input)) + gpio_dev->input = NULL; + } + + /* Idle snapshot plus 100ms poll. nIRQ (GPIO 86) still calls + * d1830_n31_din_nirq; poll covers a missed EIC edge so Home / + * Sleep / Play show on n31-btn. Trace prints only on change. + */ + d1830_btn_poll_once(gpio_dev); + INIT_DELAYED_WORK(&gpio_dev->trace, d1830_trace_work); + schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(100)); + d1830_n31_din_nirq_hook = d1830_n31_din_nirq; return 0; } +static void d1830_gpio_remove(struct i2c_client *client) +{ + struct d1830_gpio *gpio_dev = i2c_get_clientdata(client); + + if (gpio_dev) + cancel_delayed_work_sync(&gpio_dev->trace); + device_remove_file(&client->dev, &dev_attr_vbat_raw); + device_remove_file(&client->dev, &dev_attr_do_poweroff); + if (pm_power_off == d1830_pm_power_off) + pm_power_off = NULL; + d1830_n31_din_nirq_hook = NULL; + d1830_poweroff_client = NULL; +} + static const struct of_device_id d1830_gpio_of_match[] = { { .compatible = "dlg,d1830-gpio" }, { } @@ -176,11 +1038,12 @@ static struct i2c_driver d1830_gpio_driver = { .of_match_table = d1830_gpio_of_match, }, .probe = d1830_gpio_probe, + .remove = d1830_gpio_remove, .id_table = d1830_gpio_id, }; module_i2c_driver(d1830_gpio_driver); MODULE_AUTHOR("Vencislav Atanasov "); -MODULE_DESCRIPTION("Dialog Semiconductor D1830 PMIC read-only GPIO driver"); +MODULE_DESCRIPTION("Dialog Semiconductor D1830 PMIC GPIO + poweroff + battery"); MODULE_LICENSE("GPL v2"); From 1b91f19ab3f41c88e9bd07ad2c557ecf709215ab Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 20:58:45 -0230 Subject: [PATCH 05/31] phy: s5l8702-usb2: use the s5l87xx sequence on N31 The 8702 analog-stage ramp (CTRL1/CTRL2 walk) is for nano 3G. On nano 7G it drops the Lightning link while DWC2 still probes. Windows then sees VID_0000&PID_0002. N31 matches U-Boot s5l87xx-otg-phy: drop D+ (DCTL SFTDISCON), clear PCGCCTL, then PWR/RSTCON/MODE/CLK. Compatible strings apple,s5l87xx-otgphy and apple,s5l8740-otgphy select that path. apple,s5l8702-otgphy keeps the old ramp for N46. Tested on iPod nano 7G: gadget enumerates after DFU. --- drivers/phy/samsung/phy-s5l8702-usb2.c | 138 +++++++++++++++++++++++-- 1 file changed, 129 insertions(+), 9 deletions(-) diff --git a/drivers/phy/samsung/phy-s5l8702-usb2.c b/drivers/phy/samsung/phy-s5l8702-usb2.c index 2c2056e61041d7..cedcbf228e3cf7 100644 --- a/drivers/phy/samsung/phy-s5l8702-usb2.c +++ b/drivers/phy/samsung/phy-s5l8702-usb2.c @@ -1,30 +1,51 @@ // SPDX-License-Identifier: GPL-2.0+ /* - * Apple/Samsung S5L8702 USB OTG PHY. + * Apple/Samsung S5L8702 / S5L87xx USB OTG PHY. + * + * S5L8702 (Nano 3G) uses the analog stage ramp sequence. + * S5L8723/8740 (Nano 6G/7G) use the shorter Freemyipod/U-Boot + * s5l87xx sequence — N31 must NOT run the 8702 dance or the + * Lightning-facing link dies while DWC2 still loads. */ #include #include +#include +#include #include #include #include #include #include +#include + +enum s5l_usbphy_kind { + S5L_USBPHY_8702 = 0, + S5L_USBPHY_87XX = 1, +}; struct s5l8702_usbphy { struct device *dev; struct phy *phy; void __iomem *base; + enum s5l_usbphy_kind kind; }; #define S5L8702_OTGPHY_PWR 0x000 #define S5L8702_OTGPHY_CLK 0x004 #define S5L8702_OTGPHY_RSTCON 0x008 #define S5L8702_OTGPHY_BIAS 0x018 +#define S5L8702_OTGPHY_MODE 0x01c /* s5l87xx unkcon */ #define S5L8702_OTGPHY_INTFCON 0x030 #define S5L8702_OTGPHY_CTRL1 0x040 #define S5L8702_OTGPHY_CTRL2 0x044 #define S5L8702_OTGPHY_ENABLE 0x100 +/* DWC2 PCGCCTL — clear USB suspend before PHY on (U-Boot does this) */ +#define S5L87XX_OTG_PCGCCTL 0x38400e00 +/* DWC2 DCTL — drop U-Boot D+ before PHY reset or Windows sees 0000:0002 */ +#define S5L87XX_OTG_DCTL 0x38400804 +#define S5L87XX_DCTL_SFTDISCON BIT(1) + static int s5l8702_usbphy_phy_init(struct phy *phy) { return 0; @@ -35,9 +56,76 @@ static int s5l8702_usbphy_phy_exit(struct phy *phy) return 0; } -static int s5l8702_usbphy_phy_power_on(struct phy *phy) +static void s5l87xx_clear_suspend(struct s5l8702_usbphy *usbphy) +{ + void __iomem *pcgc; + + pcgc = ioremap(S5L87XX_OTG_PCGCCTL, 4); + if (!pcgc) { + dev_warn(usbphy->dev, "PCGCCTL ioremap failed\n"); + return; + } + writel(0, pcgc); + iounmap(pcgc); +} + +static void s5l87xx_soft_disconnect(struct s5l8702_usbphy *usbphy) +{ + void __iomem *dctl; + u32 val; + + dctl = ioremap(S5L87XX_OTG_DCTL, 4); + if (!dctl) { + dev_warn(usbphy->dev, "DCTL ioremap failed\n"); + return; + } + val = readl(dctl) | S5L87XX_DCTL_SFTDISCON; + writel(val, dctl); + iounmap(dctl); +} + +/* + * Matches upstream/u-boot arch/arm/mach-s5l87xx/s5l87xx-otg-phy.c + * and is the sequence that already proves DFU gadget on N31. + */ +static int s5l87xx_usbphy_power_on(struct s5l8702_usbphy *usbphy) +{ + void __iomem *b = usbphy->base; + + /* U-Boot DFU leaves D+ pulled up. Drop it before PHY reset. */ + s5l87xx_soft_disconnect(usbphy); + s5l87xx_clear_suspend(usbphy); + mdelay(10); + + writel(0, b + S5L8702_OTGPHY_PWR); + mdelay(10); + writel(1, b + S5L8702_OTGPHY_RSTCON); + mdelay(10); + writel(0, b + S5L8702_OTGPHY_RSTCON); + mdelay(10); + writel(6, b + S5L8702_OTGPHY_MODE); + writel(1, b + S5L8702_OTGPHY_CLK); /* con @ +0x04 */ + /* U-Boot waits ~400ms for PLL lock */ + mdelay(400); + + dev_info(usbphy->dev, "s5l87xx OTG PHY on (N31/N20 path)\n"); + return 0; +} + +static int s5l87xx_usbphy_power_off(struct s5l8702_usbphy *usbphy) +{ + void __iomem *b = usbphy->base; + + writel(0xff, b + S5L8702_OTGPHY_PWR); + mdelay(10); + writel(0xff, b + S5L8702_OTGPHY_RSTCON); + mdelay(10); + writel(4, b + S5L8702_OTGPHY_MODE); + return 0; +} + +static int s5l8702_usbphy_power_on_legacy(struct s5l8702_usbphy *usbphy) { - struct s5l8702_usbphy *usbphy = phy_get_drvdata(phy); void __iomem *b = usbphy->base; writel(0x000, b + S5L8702_OTGPHY_PWR); @@ -45,13 +133,11 @@ static int s5l8702_usbphy_phy_power_on(struct phy *phy) writel(0x400, b + S5L8702_OTGPHY_BIAS); writel(0x007, b + S5L8702_OTGPHY_RSTCON); - /* Analog stage 1 ramp */ writel(0x300, b + S5L8702_OTGPHY_CTRL1); writel(0x340, b + S5L8702_OTGPHY_CTRL1); writel(0x346, b + S5L8702_OTGPHY_CTRL1); writel(0x347, b + S5L8702_OTGPHY_CTRL1); - /* Analog stage 2 ramp */ writel(0x0c00, b + S5L8702_OTGPHY_CTRL2); writel(0x0fc0, b + S5L8702_OTGPHY_CTRL2); writel(0x0fe0, b + S5L8702_OTGPHY_CTRL2); @@ -65,14 +151,12 @@ static int s5l8702_usbphy_phy_power_on(struct phy *phy) writel(0x000, b + S5L8702_OTGPHY_INTFCON); writel(0x000, b + S5L8702_OTGPHY_BIAS); - /* Let the PLL lock before the controller starts poking the core. */ mdelay(40); return 0; } -static int s5l8702_usbphy_phy_power_off(struct phy *phy) +static int s5l8702_usbphy_power_off_legacy(struct s5l8702_usbphy *usbphy) { - struct s5l8702_usbphy *usbphy = phy_get_drvdata(phy); void __iomem *b = usbphy->base; writel(0x0, b + S5L8702_OTGPHY_CTRL2); @@ -82,6 +166,24 @@ static int s5l8702_usbphy_phy_power_off(struct phy *phy) return 0; } +static int s5l8702_usbphy_phy_power_on(struct phy *phy) +{ + struct s5l8702_usbphy *usbphy = phy_get_drvdata(phy); + + if (usbphy->kind == S5L_USBPHY_87XX) + return s5l87xx_usbphy_power_on(usbphy); + return s5l8702_usbphy_power_on_legacy(usbphy); +} + +static int s5l8702_usbphy_phy_power_off(struct phy *phy) +{ + struct s5l8702_usbphy *usbphy = phy_get_drvdata(phy); + + if (usbphy->kind == S5L_USBPHY_87XX) + return s5l87xx_usbphy_power_off(usbphy); + return s5l8702_usbphy_power_off_legacy(usbphy); +} + static const struct phy_ops s5l8702_usbphy_phy_ops = { .init = s5l8702_usbphy_phy_init, .exit = s5l8702_usbphy_phy_exit, @@ -95,6 +197,7 @@ static int s5l8702_usbphy_probe(struct platform_device *pdev) struct s5l8702_usbphy *usbphy; struct phy_provider *phy_provider; struct device *dev = &pdev->dev; + const struct of_device_id *match; int ret; usbphy = devm_kzalloc(dev, sizeof(*usbphy), GFP_KERNEL); @@ -103,6 +206,12 @@ static int s5l8702_usbphy_probe(struct platform_device *pdev) usbphy->dev = dev; dev_set_drvdata(dev, usbphy); + match = of_match_device(dev->driver->of_match_table, dev); + if (match && match->data) + usbphy->kind = (uintptr_t)match->data; + else + usbphy->kind = S5L_USBPHY_8702; + usbphy->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(usbphy->base)) return PTR_ERR(usbphy->base); @@ -118,6 +227,9 @@ static int s5l8702_usbphy_probe(struct platform_device *pdev) phy_provider = devm_of_phy_provider_register(&pdev->dev, of_phy_simple_xlate); + dev_info(dev, "USB OTG PHY kind=%s\n", + usbphy->kind == S5L_USBPHY_87XX ? "s5l87xx" : "s5l8702"); + return PTR_ERR_OR_ZERO(phy_provider); } @@ -126,7 +238,12 @@ static void s5l8702_usbphy_remove(struct platform_device *pdev) } static const struct of_device_id s5l8702_usbphy_of_match[] = { - { .compatible = "apple,s5l8702-otgphy", }, + { .compatible = "apple,s5l8702-otgphy", + .data = (void *)(uintptr_t)S5L_USBPHY_8702 }, + { .compatible = "apple,s5l87xx-otgphy", + .data = (void *)(uintptr_t)S5L_USBPHY_87XX }, + { .compatible = "apple,s5l8740-otgphy", + .data = (void *)(uintptr_t)S5L_USBPHY_87XX }, { }, }; MODULE_DEVICE_TABLE(of, s5l8702_usbphy_of_match); @@ -140,3 +257,6 @@ static struct platform_driver s5l8702_usbphy_driver = { } }; module_platform_driver(s5l8702_usbphy_driver); + +MODULE_DESCRIPTION("Apple/Samsung S5L8702/S5L87xx USB OTG PHY"); +MODULE_LICENSE("GPL"); From 16480b710b78ee8f76c497319b38d5a3bef67c3f Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 21:03:08 -0230 Subject: [PATCH 06/31] usb: dwc2/rndis: N31 FIFO split and Windows inbox RNDIS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N46 and N20 keep apple,s5l87xx-usb → dwc2_set_s5l8702_params (slave PIO, session_valid_gintmsk_quirk). N31 uses apple,s5l8740-usb → dwc2_set_s5l87xx_params. That setter follows RetailOS sub_1B543A: NP=32, first dedicated IN capped at 512 words, buffer DMA, GAHBCFG INCR8. The 8702 quirk masks USBRST/EP0 until SessReqInt, which does not arrive after the 87xx PHY reset, so GET_DESCRIPTOR dies as 0000:0002. RNDIS already leaves cdc_filter=0 until SET. Apply the Windows inbox bits: IAD class/subclass/protocol on the control iface (usbccgp / rndiscmp.inf want EF/04/01 there), INIT reports media connected, LINK_SPEED stays non-zero. Tested on Windows 10/11 against 1d6b:0106 from our gadget: Remote NDIS Compatible Device, SSH and telnet to 192.168.7.2. --- drivers/usb/dwc2/params.c | 135 ++++++++++++++++++++++++++ drivers/usb/gadget/function/f_rndis.c | 6 ++ drivers/usb/gadget/function/rndis.c | 19 ++-- 3 files changed, 153 insertions(+), 7 deletions(-) diff --git a/drivers/usb/dwc2/params.c b/drivers/usb/dwc2/params.c index cc32d0ebea941a..587271202917f3 100644 --- a/drivers/usb/dwc2/params.c +++ b/drivers/usb/dwc2/params.c @@ -128,6 +128,139 @@ static void dwc2_set_s5l8702_params(struct dwc2_hsotg *hsotg) p->session_valid_gintmsk_quirk = true; } +static void dwc2_set_s5l87xx_params(struct dwc2_hsotg *hsotg) +{ + struct dwc2_core_params *p = &hsotg->params; + struct dwc2_hw_params *hw = &hsotg->hw_params; + unsigned n_ep, n_fifo, dirs, ctrl, v1, v3, i, t; + unsigned total, rx, np, left, first, rest, share, stubs; + u32 hwcfg4; + + p->speed = DWC2_SPEED_PARAM_HIGH; + p->otg_caps.hnp_support = true; + p->otg_caps.srp_support = true; + p->phy_utmi_width = 16; + p->g_dma = true; + /* + * RetailOS sub_1B5254 writes GAHBCFG=0x2B (DMA+INCR8) and + * never sets DCFG_DESCDMA. Descriptor-DMA IN is a common + * DWC2 TX-only failure when the ROM used buffer DMA. + */ + p->g_dma_desc = false; + p->ahbcfg = GAHBCFG_HBSTLEN_INCR8 << GAHBCFG_HBSTLEN_SHIFT; + /* + * N31 glass 2026-08-22: quirk-on left USBRST/EP0 masked until + * SessReqInt. That IRQ does not arrive after PHY reset, so Windows + * GET_DESCRIPTOR dies as VID_0000&PID_0002 Code 43. Nano3 gating + * stays on dwc2_set_s5l8702_params only. + */ + p->session_valid_gintmsk_quirk = false; + + total = hw->total_fifo_size; + dev_info(hsotg->dev, + "s5l87xx GHWCFG3[31:16] fifo_words=%u (total DFIFO)\n", + total); + dev_info(hsotg->dev, + "s5l87xx dwc2 num_dev_ep=%u perio_in=%u in_eps=%u dyn=%u ded=%u\n", + hw->num_dev_ep, hw->num_dev_perio_in_ep, hw->num_dev_in_eps, + hw->enable_dynamic_fifo, hw->en_multiple_tx_fifo); + + /* RetailOS sub_1B543A — only when dynamic FIFO is set. */ + if (!hw->enable_dynamic_fifo) + return; + + n_ep = hw->num_dev_ep; + if (n_ep + 1 > 9) + n_ep = 8; + dirs = hw->dev_ep_dirs; + v1 = 0; + v3 = 0; + for (i = 1; i < n_ep; i++) { + t = (dirs >> (2 * i)) & 3; + if (t == 0) { + v1++; + v3++; + } else if (t == 2) { + v1++; + } else if (t == 1) { + v3++; + } + } + + hwcfg4 = dwc2_readl(hsotg, GHWCFG4); + ctrl = (hwcfg4 & GHWCFG4_NUM_DEV_MODE_CTRL_EP_MASK) >> + GHWCFG4_NUM_DEV_MODE_CTRL_EP_SHIFT; + rx = 4 * ctrl + 2 * v1 + 272; + if (hw->rx_fifo_size && rx > hw->rx_fifo_size) + rx = hw->rx_fifo_size; + np = hw->en_multiple_tx_fifo ? 32 : 256; + if (rx + np > total) { + if (total > rx + 16) + np = total - rx - 16; + else + np = 16; + } + p->g_rx_fifo_size = rx; + p->g_np_tx_fifo_size = np; + /* check_params CHECK_RANGE uses these as max. */ + if (hw->rx_fifo_size < rx) + hw->rx_fifo_size = rx; + if (hw->dev_nperio_tx_fifo_size < np) + hw->dev_nperio_tx_fifo_size = np; + left = (total > rx + np) ? total - rx - np : 0; + + n_fifo = hw->num_dev_in_eps; + if (n_fifo < 1) + n_fifo = 1; + if (n_fifo > 15) + n_fifo = 15; + if (!v3) + v3 = n_fifo; + if (v3 > n_fifo) + v3 = n_fifo; + + stubs = 16 * (n_fifo - v3); + if (left > stubs) + left -= stubs; + else + stubs = 0; + + first = left; + if (first > 512) + first = 512; + /* HS bulk MP=512 B needs a dedicated FIFO >= 128 words. */ + if (first && first < 128) + first = (left >= 128) ? 128 : left; + memset(p->g_tx_fifo_size, 0, sizeof(p->g_tx_fifo_size)); + p->g_tx_fifo_size[1] = first ? first : 16; + rest = (left > first) ? left - first : 0; + if (v3 > 1) { + share = rest / (v3 - 1); + if (share < 128 && rest >= (unsigned)(128 * (v3 - 1))) + share = 128; + if (!share) + share = 16; + for (i = 2; i <= v3; i++) { + unsigned sz = (i == v3) ? rest : share; + + if (!sz) + sz = 16; + p->g_tx_fifo_size[i] = sz; + if (i != v3 && rest >= share) + rest -= share; + } + } + for (i = v3 + 1; i <= n_fifo; i++) + p->g_tx_fifo_size[i] = 16; + for (i = 1; i <= n_fifo; i++) + if (p->g_tx_fifo_size[i] > hw->g_tx_fifo_size[i]) + hw->g_tx_fifo_size[i] = p->g_tx_fifo_size[i]; + + dev_info(hsotg->dev, + "s5l87xx retailos fifo rx=%u np=%u in1=%u n=%u v1=%u v3=%u ctrl=%u\n", + rx, np, p->g_tx_fifo_size[1], n_fifo, v1, v3, ctrl); +} + static void dwc2_set_socfpga_agilex_params(struct dwc2_hsotg *hsotg) { struct dwc2_core_params *p = &hsotg->params; @@ -346,6 +479,8 @@ const struct of_device_id dwc2_of_match_table[] = { .data = dwc2_set_s3c6400_params }, { .compatible = "apple,s5l87xx-usb", .data = dwc2_set_s5l8702_params }, + { .compatible = "apple,s5l8740-usb", + .data = dwc2_set_s5l87xx_params }, { .compatible = "amlogic,meson8-usb", .data = dwc2_set_amlogic_params }, { .compatible = "amlogic,meson8b-usb", diff --git a/drivers/usb/gadget/function/f_rndis.c b/drivers/usb/gadget/function/f_rndis.c index 7cec19d65fb534..050627f792975b 100644 --- a/drivers/usb/gadget/function/f_rndis.c +++ b/drivers/usb/gadget/function/f_rndis.c @@ -585,6 +585,9 @@ static int rndis_set_alt(struct usb_function *f, unsigned intf, unsigned alt) rndis_set_param_dev(rndis->params, net, &rndis->port.cdc_filter); + rndis_set_param_medium(rndis->params, RNDIS_MEDIUM_802_3, + gether_bitrate(cdev->gadget) / 100); + rndis_signal_connect(rndis->params); } else goto fail; @@ -680,6 +683,9 @@ rndis_bind(struct usb_configuration *c, struct usb_function *f) rndis_iad_descriptor.bFunctionClass = rndis_opts->class; rndis_iad_descriptor.bFunctionSubClass = rndis_opts->subclass; rndis_iad_descriptor.bFunctionProtocol = rndis_opts->protocol; + rndis_control_intf.bInterfaceClass = rndis_opts->class; + rndis_control_intf.bInterfaceSubClass = rndis_opts->subclass; + rndis_control_intf.bInterfaceProtocol = rndis_opts->protocol; /* * in drivers/usb/gadget/configfs.c:configfs_composite_bind() diff --git a/drivers/usb/gadget/function/rndis.c b/drivers/usb/gadget/function/rndis.c index afd75d72412c9f..6a4810658767f8 100644 --- a/drivers/usb/gadget/function/rndis.c +++ b/drivers/usb/gadget/function/rndis.c @@ -256,10 +256,7 @@ static int gen_ndis_query_resp(struct rndis_params *params, u32 OID, u8 *buf, case RNDIS_OID_GEN_LINK_SPEED: if (rndis_debug > 1) pr_debug("%s: RNDIS_OID_GEN_LINK_SPEED\n", __func__); - if (params->media_state == RNDIS_MEDIA_STATE_DISCONNECTED) - *outbuf = cpu_to_le32(0); - else - *outbuf = cpu_to_le32(params->speed); + *outbuf = cpu_to_le32(params->speed); retval = 0; break; @@ -325,7 +322,7 @@ static int gen_ndis_query_resp(struct rndis_params *params, u32 OID, u8 *buf, case RNDIS_OID_GEN_MEDIA_CONNECT_STATUS: if (rndis_debug > 1) pr_debug("%s: RNDIS_OID_GEN_MEDIA_CONNECT_STATUS\n", __func__); - *outbuf = cpu_to_le32(params->media_state); + *outbuf = cpu_to_le32(RNDIS_MEDIA_STATE_CONNECTED); retval = 0; break; @@ -813,8 +810,16 @@ int rndis_msg_parser(struct rndis_params *params, u8 *buf) case RNDIS_MSG_INIT: pr_debug("%s: RNDIS_MSG_INIT\n", __func__); - params->state = RNDIS_INITIALIZED; - return rndis_init_response(params, (rndis_init_msg_type *)buf); + { + int ret; + + params->state = RNDIS_INITIALIZED; + params->media_state = RNDIS_MEDIA_STATE_CONNECTED; + ret = rndis_init_response(params, + (rndis_init_msg_type *)buf); + rndis_signal_connect(params); + return ret; + } case RNDIS_MSG_HALT: pr_debug("%s: RNDIS_MSG_HALT\n", From 790b8b84c5636341c27714fc14b5816076020de0 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 21:04:00 -0230 Subject: [PATCH 07/31] clk: s5l8702: ungate PWRCON on N31 without remuxing SYS N31 boots with WTF/U-Boot leftovers. clk_disable_unused then writes SET_TO_DISABLE gates and peripherals drop. Clear the known PWRCON banks and the CG16 enable bits in the divider regs, and mark the published gates CLK_IS_CRITICAL | CLK_IGNORE_UNUSED. Never write CLKCON+0x00/+0x04 (SYS PLL / DRAM) or +0x50 (fatal latch). The ungate walk is limited to samsung,s5l8740 so N46 keeps the old probe. Tested on iPod nano 7G: I2C, GPIO, LCDIF, and DWC2 stay clocked after late init. --- drivers/clk/clk-s5l8702.c | 76 +++++++++++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/drivers/clk/clk-s5l8702.c b/drivers/clk/clk-s5l8702.c index e003018011e972..7e33a5f50ed332 100644 --- a/drivers/clk/clk-s5l8702.c +++ b/drivers/clk/clk-s5l8702.c @@ -1,16 +1,29 @@ // SPDX-License-Identifier: GPL-2.0 /* - * S5L8702 Clockgates driver + * S5L8702 / S5L8740 Clockgates + * + * Bring-up policy: ungate documented PWRCON banks so peripherals stay + * alive without a Linux consumer. CCF also marks the published gates + * CLK_IS_CRITICAL | CLK_IGNORE_UNUSED so clk_disable_unused cannot + * write those bits later. + * + * Never remux SYS PLL (+0x00/+0x04) — that kills live DRAM. + * Never write CLKCON+0x50 — that is the fatal/WDT latch (0xA5). */ #include +#include #include #include -#include #include #include #include +#define CLKCON_PWRCON0 0x48 +#define CLKCON_PWRCON1 0x4c +#define CLKCON_PWRCON2 0x58 +#define CLKCON_PWRCON4 0x6c + struct s5l8702_clk_data { void __iomem *regs; struct clk_hw_onecell_data *hw_data; @@ -39,6 +52,36 @@ static const struct s5l8702_clk_gate s5l8702_gates[] = { [CLK_PRNG] = GATE("prng", NULL, 0x4c, 0), }; +/* + * SET_TO_DISABLE: bit clear = clock running. Write 0 to the known PWRCON + * banks so every AHB/APB gate is on even without a driver. CG16 enable + * bits (RetailOS 41CBD8) are the high halves of the divider regs — clear + * those bits only; leave the divider fields WTF/U-Boot programmed. + */ +static void s5l8740_ungate_all(struct device *dev, void __iomem *regs) +{ + dev_info(dev, + "PWRCON before: +48=%08x +4c=%08x +58=%08x +6c=%08x SYS+00=%08x\n", + readl(regs + CLKCON_PWRCON0), readl(regs + CLKCON_PWRCON1), + readl(regs + CLKCON_PWRCON2), readl(regs + CLKCON_PWRCON4), + readl(regs + 0x00)); + + writel(0, regs + CLKCON_PWRCON0); + writel(0, regs + CLKCON_PWRCON1); + writel(0, regs + CLKCON_PWRCON2); + writel(0, regs + CLKCON_PWRCON4); + + writel(readl(regs + 0x08) & ~0x80008000u, regs + 0x08); + writel(readl(regs + 0x0c) & ~0x80008000u, regs + 0x0c); + writel(readl(regs + 0x10) & ~0x8000u, regs + 0x10); + writel(readl(regs + 0x14) & ~0x80008000u, regs + 0x14); + + dev_info(dev, + "PWRCON after ungate-all: +48=%08x +4c=%08x +58=%08x +6c=%08x\n", + readl(regs + CLKCON_PWRCON0), readl(regs + CLKCON_PWRCON1), + readl(regs + CLKCON_PWRCON2), readl(regs + CLKCON_PWRCON4)); +} + static int s5l8702_clk_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -46,23 +89,25 @@ static int s5l8702_clk_probe(struct platform_device *pdev) struct clk_hw_onecell_data *hw_data; int i, ret; size_t num_clks; + const unsigned long gate_flags = + CLK_IGNORE_UNUSED | CLK_IS_CRITICAL; num_clks = ARRAY_SIZE(s5l8702_gates); clk_data = devm_kzalloc(dev, sizeof(*clk_data), GFP_KERNEL); - if (!clk_data) { + if (!clk_data) return -ENOMEM; - } clk_data->regs = devm_platform_ioremap_resource(pdev, 0); - if (IS_ERR(clk_data->regs)) { + if (IS_ERR(clk_data->regs)) return PTR_ERR(clk_data->regs); - } + + if (of_machine_is_compatible("samsung,s5l8740")) + s5l8740_ungate_all(dev, clk_data->regs); hw_data = devm_kzalloc(dev, struct_size(hw_data, hws, num_clks), GFP_KERNEL); - if (!hw_data) { + if (!hw_data) return -ENOMEM; - } hw_data->num = num_clks; @@ -71,22 +116,23 @@ static int s5l8702_clk_probe(struct platform_device *pdev) for (i = 0; i < num_clks; i++) { const struct s5l8702_clk_gate *clk_gate = &s5l8702_gates[i]; - hw_data->hws[i] = devm_clk_hw_register_gate(dev, clk_gate->name, clk_gate->parent_name, 0, - clk_data->regs + clk_gate->reg, clk_gate->bit, CLK_GATE_SET_TO_DISABLE, &clk_data->lock); + hw_data->hws[i] = devm_clk_hw_register_gate(dev, clk_gate->name, + clk_gate->parent_name, gate_flags, + clk_data->regs + clk_gate->reg, clk_gate->bit, + CLK_GATE_SET_TO_DISABLE, &clk_data->lock); - if (IS_ERR(hw_data->hws[i])) { + if (IS_ERR(hw_data->hws[i])) return PTR_ERR(hw_data->hws[i]); - } } clk_data->hw_data = hw_data; ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, hw_data); - if (ret) { + if (ret) return ret; - } - dev_info(dev, "Registered %d clockgate(s)", num_clks); + dev_info(dev, "Registered %zu clockgate(s), unused left enabled\n", + num_clks); return 0; } From cdc4deed45403ae570bbb6ffcc5ea901b30bd139 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 21:04:00 -0230 Subject: [PATCH 08/31] video: S5L8740 backlight and TinyDRM LCDIF handoff Backlight is a separate MMIO block at 0x3E000000. Level is 1..62 at +0x08. This driver does not touch LCDIF CON or PHTIME. TinyDRM was blitting fb->width * fb->height as a linear buffer. fbcon pitch is wider than 240, so the panel tore. Walk each row with pitches[0]/4. Log CON/PHTIME at probe and leave the values U-Boot wrote. Tested on iPod nano 7G: tty0 shell and backlight at 62. --- drivers/gpu/drm/tiny/s5l8740.c | 41 ++++--- drivers/video/backlight/Kconfig | 8 ++ drivers/video/backlight/Makefile | 1 + drivers/video/backlight/backlight-s5l8740.c | 122 ++++++++++++++++++++ 4 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 drivers/video/backlight/backlight-s5l8740.c diff --git a/drivers/gpu/drm/tiny/s5l8740.c b/drivers/gpu/drm/tiny/s5l8740.c index 696bda7ee58a65..f2c6256c2797b2 100644 --- a/drivers/gpu/drm/tiny/s5l8740.c +++ b/drivers/gpu/drm/tiny/s5l8740.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only #include +#include #include #include @@ -38,7 +39,7 @@ #define S5L8740_LCD_STATUS_BUSY 0x10 -#define S5L8740_LCD_TIMEOUT_US 1 +#define S5L8740_LCD_TIMEOUT_US 100000 #define WIDTH 240 #define HEIGHT 432 @@ -108,22 +109,29 @@ static void s5l8740_primary_plane_helper_atomic_update(struct drm_plane *plane, if (!drm_dev_enter(dev, &idx)) goto out_drm_gem_fb_end_cpu_access; - unsigned int count = fb->width * fb->height; - int *src = shadow_plane_state->data[0].vaddr; + unsigned int x, y, pitch_px; + u32 *src = shadow_plane_state->data[0].vaddr; - for (int i = 0; i < count; i++) { - int ret; - u32 status; + pitch_px = fb->pitches[0] / 4; + if (!pitch_px) + pitch_px = fb->width; - ret = readl_poll_timeout_atomic(sdev->lcdif + S5L8740_LCD_STATUS, status, - !(status & S5L8740_LCD_STATUS_BUSY), 0, S5L8740_LCD_TIMEOUT_US); + for (y = 0; y < fb->height; y++) { + const u32 *row = src + y * pitch_px; - if (unlikely(ret)) { - drm_warn(dev, "S5L8740_LCD_STATUS_BUSY timeout\n"); - goto out_drm_dev_exit; - } + for (x = 0; x < fb->width; x++) { + int ret; + u32 status; - s5l8740_lcd_writel(sdev, S5L8740_LCD_WDATA, src[i]); + ret = readl_poll_timeout_atomic(sdev->lcdif + S5L8740_LCD_STATUS, status, + !(status & S5L8740_LCD_STATUS_BUSY), 0, + S5L8740_LCD_TIMEOUT_US); + if (unlikely(ret)) { + drm_warn_once(dev, "S5L8740_LCD_STATUS_BUSY timeout\n"); + goto out_drm_dev_exit; + } + s5l8740_lcd_writel(sdev, S5L8740_LCD_WDATA, row[x]); + } } out_drm_dev_exit: @@ -242,6 +250,13 @@ static int s5l8740_probe(struct platform_device *pdev) drm_dbg(dev, "using I/O memory framebuffer at %pr\n", res); sdev->lcdif = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(sdev->lcdif)) + return PTR_ERR(sdev->lcdif); + + /* U-Boot already programmed CON/PHTIME. Do not rewrite them. */ + drm_info(dev, "LCDIF handoff CON=%08x PHTIME=%08x (untouched)\n", + readl(sdev->lcdif + S5L8740_LCD_CON), + readl(sdev->lcdif + S5L8740_LCD_PHTIME)); /* * Modesetting diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig index 3614a5d29c716e..7d476c673f6223 100644 --- a/drivers/video/backlight/Kconfig +++ b/drivers/video/backlight/Kconfig @@ -492,4 +492,12 @@ config BACKLIGHT_LED endif # BACKLIGHT_CLASS_DEVICE + +config BACKLIGHT_S5L8740 + tristate "Samsung/Apple S5L8740 backlight" + depends on BACKLIGHT_CLASS_DEVICE + help + LCD backlight MMIO at 0x3E000000 for iPod nano 7G (N31). + Does not touch LCDIF CON/PHTIME. + endmenu diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile index 8fc98f760a8ad4..ae1ebfffc9b89d 100644 --- a/drivers/video/backlight/Makefile +++ b/drivers/video/backlight/Makefile @@ -60,3 +60,4 @@ obj-$(CONFIG_BACKLIGHT_WM831X) += wm831x_bl.o obj-$(CONFIG_BACKLIGHT_ARCXCNN) += arcxcnn_bl.o obj-$(CONFIG_BACKLIGHT_RAVE_SP) += rave-sp-backlight.o obj-$(CONFIG_BACKLIGHT_LED) += led_bl.o +obj-$(CONFIG_BACKLIGHT_S5L8740) += backlight-s5l8740.o diff --git a/drivers/video/backlight/backlight-s5l8740.c b/drivers/video/backlight/backlight-s5l8740.c new file mode 100644 index 00000000000000..9d8017b26be77d --- /dev/null +++ b/drivers/video/backlight/backlight-s5l8740.c @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Platform backlight for Samsung/Apple S5L8740 (iPod nano 7G / N31) + * + * MMIO block @ 0x3E000000 (LCD AUX / backlight): + * +0x04 enable — bit0 + * +0x08 level — low 8 bits hold 1..62; bit0 also acts as enable + * + * Init (matches U-Boot / panel bring-up): write 62 to +0x08, then set bit0 + * on +0x04 and +0x08. Brightness 0 = off; userspace 1..max → HW 1..62. + * + * Kconfig fragment (wire Makefile / Kconfig separately): + * config BACKLIGHT_S5L8740 + * tristate "Samsung/Apple S5L8740 backlight" + * depends on BACKLIGHT_CLASS_DEVICE && (ARCH_S5L8740 || COMPILE_TEST) + * default y if ARCH_S5L8740 + * help + * LCD backlight at 0x3E000000 for iPod nano 7G (N31). + */ +#include +#include +#include +#include +#include +#include + +#define S5L8740_BL_ENABLE_OFF 0x04 +#define S5L8740_BL_LEVEL_OFF 0x08 +#define S5L8740_BL_MAX 62 + +struct s5l8740_bl { + void __iomem *base; + struct backlight_device *bd; +}; + +static void s5l8740_bl_hw_set(struct s5l8740_bl *bl, int level) +{ + u32 en, lvl; + + if (level <= 0) { + en = readl(bl->base + S5L8740_BL_ENABLE_OFF); + writel(en & ~BIT(0), bl->base + S5L8740_BL_ENABLE_OFF); + lvl = readl(bl->base + S5L8740_BL_LEVEL_OFF); + writel(lvl & ~BIT(0), bl->base + S5L8740_BL_LEVEL_OFF); + return; + } + + if (level > S5L8740_BL_MAX) + level = S5L8740_BL_MAX; + + writel((u32)level, bl->base + S5L8740_BL_LEVEL_OFF); + en = readl(bl->base + S5L8740_BL_ENABLE_OFF); + writel(en | BIT(0), bl->base + S5L8740_BL_ENABLE_OFF); + lvl = readl(bl->base + S5L8740_BL_LEVEL_OFF); + writel(lvl | BIT(0), bl->base + S5L8740_BL_LEVEL_OFF); +} + +static int s5l8740_bl_update_status(struct backlight_device *bd) +{ + struct s5l8740_bl *bl = bl_get_data(bd); + int brightness = backlight_get_brightness(bd); + + s5l8740_bl_hw_set(bl, brightness); + return 0; +} + +static const struct backlight_ops s5l8740_bl_ops = { + .update_status = s5l8740_bl_update_status, +}; + +static int s5l8740_bl_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_bl *bl; + struct backlight_properties props = { }; + struct backlight_device *bd; + + bl = devm_kzalloc(dev, sizeof(*bl), GFP_KERNEL); + if (!bl) + return -ENOMEM; + + bl->base = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(bl->base)) + return PTR_ERR(bl->base); + + /* Full brightness + enables (U-Boot path) */ + s5l8740_bl_hw_set(bl, S5L8740_BL_MAX); + + props.type = BACKLIGHT_RAW; + props.max_brightness = S5L8740_BL_MAX; + props.brightness = S5L8740_BL_MAX; + + bd = devm_backlight_device_register(dev, "s5l8740-backlight", dev, bl, + &s5l8740_bl_ops, &props); + if (IS_ERR(bd)) + return PTR_ERR(bd); + + bl->bd = bd; + platform_set_drvdata(pdev, bl); + dev_info(dev, "S5L8740 backlight @%pR max=%u\n", + platform_get_resource(pdev, IORESOURCE_MEM, 0), S5L8740_BL_MAX); + return 0; +} + +static const struct of_device_id s5l8740_bl_of_match[] = { + { .compatible = "apple,s5l8740-backlight" }, + { .compatible = "samsung,s5l8740-backlight" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_bl_of_match); + +static struct platform_driver s5l8740_bl_driver = { + .probe = s5l8740_bl_probe, + .driver = { + .name = "backlight-s5l8740", + .of_match_table = s5l8740_bl_of_match, + }, +}; +module_platform_driver(s5l8740_bl_driver); + +MODULE_DESCRIPTION("Samsung/Apple S5L8740 LCD backlight"); +MODULE_LICENSE("GPL"); From dd9bb19d17ad8c45394afdcce6543d999b8d6b65 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 21:04:01 -0230 Subject: [PATCH 09/31] arm: dts/defconfig: wire N31 EIC, GPIO, PMIC nIRQ, 87xx PHY Replace the 2-line bcm6345 GPIO hack with the banked s5l8740 GPIO and the EIC. Home/Sleep/Play come from D1830 nIRQ on GPIO 86; do not poll those bits over I2C. gpio-keys-polled stays disabled so it cannot GPIOCMD 0xFFFE the Vol pads. USB uses apple,s5l8740-usb / apple,s5l8740-otgphy so N31 does not inherit the nano3 PHY ramp or the 8702 DWC2 quirk. syscon-reboot is disabled: 0x100000 arms the watchdog. No I2S, PL080, CS42, or dlg,apply-sec-rails. Those are not ready. Tested on iPod nano 7G with apple_n31_defconfig. --- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 65 +++++++++++++++++++---- arch/arm/configs/apple_n31_defconfig | 7 ++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 54d0e34aab25d8..83d2801dcc0cdd 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -9,6 +9,7 @@ #include #include #include +#include / { #address-cells = <1>; @@ -75,6 +76,18 @@ #interrupt-cells = <1>; }; + /* GPIO → EIC → VIC EXTn. EXT1 = Vol 40/41; EXT3 = PMIC GPIO86. */ + eic: interrupt-controller@39700000 { + compatible = "apple,s5l8740-eic", "samsung,s5l8740-eic"; + reg = <0x39700000 0x1000>; + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&vic0>; + interrupts = <1>, <3>; + apple,eic-groups = <1>, <2>; + status = "okay"; + }; + timer: timer@3c700000 { compatible = "samsung,s5l8720-timer"; reg = <0x3c700000 0x20000>; @@ -129,14 +142,14 @@ }; usbphy: usbphy@3c400000 { - compatible = "apple,s5l8720-otgphy"; + compatible = "apple,s5l8740-otgphy", "apple,s5l87xx-otgphy"; reg = <0x3c400000 0x100>; status = "okay"; #phy-cells = <0>; }; usbotg_hs: usb@38400000 { - compatible = "apple,s5l87xx-usb"; + compatible = "apple,s5l8740-usb", "apple,s5l87xx-usb"; reg = <0x38400000 0x40000>; interrupt-parent = <&vic0>; interrupts = <19>; @@ -149,6 +162,7 @@ g-tx-fifo-size = <256 256 512 512 512 768 768>; status = "okay"; dr_mode = "peripheral"; + maximum-speed = "high-speed"; }; lcdif: lcdif@38300000 { @@ -156,6 +170,14 @@ reg = <0x38300000 0x10000>; }; + /* MMIO backlight only. Never LCDIF CON/PHTIME. */ + backlight: backlight@3e000000 { + compatible = "apple,s5l8740-backlight", "samsung,s5l8740-backlight"; + reg = <0x3e000000 0x100>; + default-brightness = <62>; + status = "okay"; + }; + wdt: watchdog@3c800000 { compatible = "apple,s5l8740-syscon", "syscon", "simple-mfd"; reg = <0x3c800000 0x8>; @@ -164,16 +186,31 @@ compatible = "syscon-reboot"; offset = <0x0>; value = <0x100000>; + /* 0x100000 arms the WDT. Leave off until reboot is proven. */ + status = "disabled"; }; }; - gpio5: gpio@3cf00000 { + /* 2-line hack kept disabled; Vol± use full banked GPIO. */ + gpio5: gpio-hack@3cf000a4 { compatible = "brcm,bcm6345-gpio"; reg-names = "dat"; reg = <0x3cf000a4 0x4>; #gpio-cells = <2>; gpio-controller; ngpios = <2>; + status = "disabled"; + }; + + gpio: gpio@3cf00000 { + compatible = "apple,s5l8740-gpio", "samsung,s5l8740-gpio"; + reg = <0x3cf00000 0x400>; + apple,eic = <&eic>; + apple,skip-sec-pinmux; + #gpio-cells = <2>; + gpio-controller; + ngpios = <128>; + status = "okay"; }; i2c0: i2c@3c600000 { @@ -205,9 +242,12 @@ reg = <0x73>; gpio-controller; #gpio-cells = <2>; - dlg,gpio-map = <0x07 4>, /* Home button */ - <0x07 5>, /* Power button */ - <0x08 1>; /* Play/pause button */ + dlg,gpio-map = <0x07 4>, /* Home OSOS ID 1 */ + <0x07 5>, /* Sleep OSOS ID 9 */ + <0x08 1>; /* Play OSOS ID 6 */ + /* SoC GPIO 86 → EIC group2 → VIC EXT3 */ + interrupt-parent = <&eic>; + interrupts = <86 IRQ_TYPE_LEVEL_LOW>; }; }; @@ -240,12 +280,15 @@ }; }; + /* Home/Sleep/Play come from gpio-d1830 + nIRQ, not this node. + * gpio-keys-polled on Vol± issues GPIOCMD 0xFFFE and the pads + * go quiet. gpio-s5l8740 polls DIN and reports the keys. + */ gpio-keys { compatible = "gpio-keys-polled"; + status = "disabled"; poll-interval = <50>; - /* these hammer the i2c bus and the driver breaks - at some point, disable them for now */ button-home { label = "Home Button"; gpios = <&d1830 0 GPIO_ACTIVE_LOW>; @@ -269,14 +312,16 @@ button-volup { label = "Volume Up"; - gpios = <&gpio5 0 GPIO_ACTIVE_LOW>; + gpios = <&gpio 40 GPIO_ACTIVE_LOW>; linux,code = ; + status = "disabled"; }; button-voldown { label = "Volume Down"; - gpios = <&gpio5 1 GPIO_ACTIVE_LOW>; + gpios = <&gpio 41 GPIO_ACTIVE_LOW>; linux,code = ; + status = "disabled"; }; }; }; diff --git a/arch/arm/configs/apple_n31_defconfig b/arch/arm/configs/apple_n31_defconfig index 2d3160bd4812e9..fe3fbfe9785fbc 100644 --- a/arch/arm/configs/apple_n31_defconfig +++ b/arch/arm/configs/apple_n31_defconfig @@ -14,7 +14,7 @@ CONFIG_USB_DWC2=y CONFIG_USB_GADGET=y CONFIG_USB_SNP_UDC_PLAT=y CONFIG_USB_ETH=y -CONFIG_USB_ETH_RNDIS=n +CONFIG_USB_ETH_RNDIS=y CONFIG_NET=y CONFIG_UNIX=y CONFIG_PACKET=y @@ -62,3 +62,8 @@ CONFIG_CRYPTO_USER_API_HASH=y CONFIG_CRYPTO_DEV_S5L8702_PRNG=y CONFIG_CRYPTO_USER_API_RNG=y CONFIG_CLK_S5L8702=y +CONFIG_PHY_S5L8702_USB2=y +CONFIG_S5L8740_EIC=y +CONFIG_GPIO_S5L8740=y +CONFIG_BACKLIGHT_CLASS_DEVICE=y +CONFIG_BACKLIGHT_S5L8740=y From b1386a07b7a40f3709cb5afb9c044e699aa4d286 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sun, 23 Aug 2026 21:04:01 -0230 Subject: [PATCH 10/31] arm: s5l87xx: disarm the SEC watchdog early WTF and U-Boot leave CON/CNT armed at 0x3C800000. A bigger zImage loses the race and resets in the middle of decompress or early device probe. Write CON=0 then CNT=0, twice. Never CLKCON+0x50. Tested on iPod nano 7G: kernel #90 reaches tty0. --- arch/arm/mach-s5l87xx/s5l87xx.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/arch/arm/mach-s5l87xx/s5l87xx.c b/arch/arm/mach-s5l87xx/s5l87xx.c index cb9cf8120a71d1..2dafd3ba98bba7 100644 --- a/arch/arm/mach-s5l87xx/s5l87xx.c +++ b/arch/arm/mach-s5l87xx/s5l87xx.c @@ -7,9 +7,35 @@ */ #include +#include +#include #include #include + +#define S5L8740_WDT_PHYS 0x3c800000ul + +/* + * WTF/U-Boot leave the SEC watchdog armed. A bigger zImage loses the race. + * CON=0 then CNT=0. Never CLKCON+0x50. + */ +static int __init s5l87xx_wdt_disarm(void) +{ + void __iomem *wdt = ioremap(S5L8740_WDT_PHYS, 8); + + if (!wdt) + return 0; + writel(0, wdt); + writel(0, wdt + 4); + writel(0, wdt); + writel(0, wdt + 4); + pr_info("s5l87xx: WDT disarmed con=%08x cnt=%08x\n", + readl(wdt), readl(wdt + 4)); + iounmap(wdt); + return 0; +} +early_initcall(s5l87xx_wdt_disarm); + /* * Map the debug UART into a fixed virtual address so earlyprintk works * across the MMU transition. The physical base is 0x3CC00000; UART3 From 37cee365781ae9a904ba11f0830ffa4c85fe2cf2 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Tue, 25 Aug 2026 10:58:49 -0230 Subject: [PATCH 11/31] N31: FMSS/FTL modules, nimbus, audio, and expanded glass bring-up Land the in-tree module set used on device (#90+): FMSS CS/META weave scan and FTL helper, Nimbus touch, PL080 DMA, CS42L81/I2S audio stubs, Tristar mux, plus DTS/defconfig wiring and RNDIS/gadget tweaks from the lab tree. Keep FMSS/FTL as loadable modules (CONFIG_FMSS_S5L8740=m). --- arch/arm/boot/dts/samsung/s5l8702-n46.dts | 2 +- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 151 +- .../configs/apple_n31_bootminimal_defconfig | 41 + arch/arm/configs/apple_n31_defconfig | 99 +- arch/arm/configs/apple_n31_nodrm_defconfig | 19 + arch/arm/mach-s5l87xx/s5l87xx.c | 24 +- drivers/bluetooth/Kconfig | 6 + drivers/bluetooth/Makefile | 1 + drivers/bluetooth/bcm2078-bt.c | 593 ++ drivers/clk/clk-s5l8702.c | 3 +- drivers/clk/n31-early-bringup.c | 66 + drivers/dma/Kconfig | 8 + drivers/dma/Makefile | 1 + drivers/dma/dma-s5l8740-pl080.c | 1105 ++++ drivers/gpio/Kconfig | 21 +- drivers/gpio/Makefile | 2 +- drivers/gpio/gpio-d1830.c | 26 +- drivers/gpu/drm/tiny/s5l8740.c | 18 +- drivers/input/touchscreen/Kconfig | 4 + drivers/input/touchscreen/Makefile | 1 + drivers/input/touchscreen/apple-nimbus.c | 2130 +++++++ drivers/irqchip/Kconfig | 16 +- drivers/irqchip/Makefile | 2 +- drivers/irqchip/irq-vic.c | 14 +- drivers/misc/Kconfig | 30 + drivers/misc/Makefile | 4 + drivers/misc/apple-tristar-cbtl1609.c | 351 ++ drivers/misc/fmss-s5l8740-api.h | 27 + drivers/misc/fmss-s5l8740.c | 5177 +++++++++++++++++ drivers/misc/fmss-seq-read.h | 185 + drivers/misc/ftl-s5l8740.c | 738 +++ drivers/misc/lis3lv02d/lis3lv02d_i2c.c | 34 +- drivers/misc/s5l8740-iis2-mmio.c | 105 + drivers/misc/whimory-ftl.h | 116 + drivers/spi/spi-s5l8702.c | 545 +- drivers/usb/dwc2/gadget.c | 18 +- drivers/usb/dwc2/params.c | 21 +- drivers/usb/gadget/function/f_rndis.c | 7 +- drivers/usb/gadget/function/rndis.c | 3 + drivers/usb/misc/Kconfig | 1 + drivers/video/backlight/Kconfig | 10 +- drivers/video/backlight/Makefile | 2 +- freemyipod/initramfs/rcS | 20 + .../dt-bindings/clock/samsung,s5l8702-clock.h | 74 +- include/linux/n31-glass-mark.h | 14 + sound/soc/apple/Kconfig | 21 + sound/soc/apple/Makefile | 3 + sound/soc/apple/cs42l81-spi.c | 863 +++ sound/soc/apple/nano7-audio.c | 98 + sound/soc/apple/s5l8740-i2s.c | 961 +++ 50 files changed, 13590 insertions(+), 191 deletions(-) create mode 100755 arch/arm/configs/apple_n31_bootminimal_defconfig create mode 100755 arch/arm/configs/apple_n31_nodrm_defconfig create mode 100755 drivers/bluetooth/bcm2078-bt.c mode change 100644 => 100755 drivers/clk/clk-s5l8702.c create mode 100755 drivers/clk/n31-early-bringup.c create mode 100755 drivers/dma/dma-s5l8740-pl080.c mode change 100644 => 100755 drivers/gpio/gpio-d1830.c create mode 100755 drivers/input/touchscreen/apple-nimbus.c create mode 100755 drivers/misc/apple-tristar-cbtl1609.c create mode 100755 drivers/misc/fmss-s5l8740-api.h create mode 100755 drivers/misc/fmss-s5l8740.c create mode 100755 drivers/misc/fmss-seq-read.h create mode 100755 drivers/misc/ftl-s5l8740.c create mode 100755 drivers/misc/s5l8740-iis2-mmio.c create mode 100755 drivers/misc/whimory-ftl.h mode change 100644 => 100755 drivers/spi/spi-s5l8702.c create mode 100755 freemyipod/initramfs/rcS mode change 100644 => 100755 include/dt-bindings/clock/samsung,s5l8702-clock.h create mode 100755 include/linux/n31-glass-mark.h create mode 100755 sound/soc/apple/cs42l81-spi.c create mode 100755 sound/soc/apple/nano7-audio.c create mode 100755 sound/soc/apple/s5l8740-i2s.c diff --git a/arch/arm/boot/dts/samsung/s5l8702-n46.dts b/arch/arm/boot/dts/samsung/s5l8702-n46.dts index 1e1a38e7efca8e..be304daf434a85 100644 --- a/arch/arm/boot/dts/samsung/s5l8702-n46.dts +++ b/arch/arm/boot/dts/samsung/s5l8702-n46.dts @@ -91,7 +91,7 @@ }; usbotg: usb@38400000 { - compatible = "apple,s5l87xx-usb"; + compatible = "apple,s5l8702-usb", "apple,s5l87xx-usb"; reg = <0x38400000 0x40000>; interrupt-parent = <&vic0>; interrupts = <19>; diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 83d2801dcc0cdd..b21ab743daa2b9 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -23,6 +23,8 @@ }; chosen { + /* U-Boot CONFIG_BOOTARGS overrides this. g_ether is built-in (0525:a4a2). */ + bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=-1 clk_ignore_unused init=/init"; stdout-path = "serial0"; }; @@ -142,6 +144,7 @@ }; usbphy: usbphy@3c400000 { + /* N31 = s5l87xx PHY. Do NOT use apple,s5l8702-otgphy (Nano3 ramp). */ compatible = "apple,s5l8740-otgphy", "apple,s5l87xx-otgphy"; reg = <0x3c400000 0x100>; status = "okay"; @@ -157,11 +160,14 @@ phy-names = "usb2-phy"; clocks = <&nclk>; clock-names = "otg"; + /* Glass: GHWCFG3 DFIFO=2080 words, ded=1, in_eps=6. + * SoC setter applies RetailOS sub_1B543A (NP=32, first + * IN cap 512). These DT values are overwritten. */ g-rx-fifo-size = <256>; - g-np-tx-fifo-size = <32>; - g-tx-fifo-size = <256 256 512 512 512 768 768>; + g-np-tx-fifo-size = <256>; status = "okay"; dr_mode = "peripheral"; + /* DMA on (g_dma=true) — HS was an IRQ storm only in slave PIO. */ maximum-speed = "high-speed"; }; @@ -170,7 +176,8 @@ reg = <0x38300000 0x10000>; }; - /* MMIO backlight only. Never LCDIF CON/PHTIME. */ + /* MMIO backlight only. Never LCDIF CON/PHTIME. U-Boot already + * wrote 62 @ +0x08; this class device owns brightness after probe. */ backlight: backlight@3e000000 { compatible = "apple,s5l8740-backlight", "samsung,s5l8740-backlight"; reg = <0x3e000000 0x100>; @@ -186,12 +193,12 @@ compatible = "syscon-reboot"; offset = <0x0>; value = <0x100000>; - /* 0x100000 arms the WDT. Leave off until reboot is proven. */ + /* 0x100000 is the WDT arm/reset poke — do not probe until poweroff is proven. */ status = "disabled"; }; }; - /* 2-line hack kept disabled; Vol± use full banked GPIO. */ + /* 2-line hack kept disabled; Vol± use full banked GPIO + EIC. */ gpio5: gpio-hack@3cf000a4 { compatible = "brcm,bcm6345-gpio"; reg-names = "dat"; @@ -215,11 +222,25 @@ i2c0: i2c@3c600000 { compatible = "samsung,s5l8702-i2c"; + #address-cells = <1>; + #size-cells = <0>; reg = <0x3c600000 0x100>; clock-frequency = <100000>; /* UPDATE ME */ interrupt-parent = <&vic0>; interrupts = <21>; status = "okay"; + + /* + * CBTL1609A1 — public 0x34/0x35 is 8-bit; Linux 7-bit is 0x1a. + * RetailOS writes no Dx mux map. U-Boot DFU already routes + * Lightning USB; do not invent apple,init-sequence. Probe + * skips I2C ACK (reads still return the address byte). + */ + tristar: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + reg = <0x1a>; + status = "okay"; + }; }; i2c1: i2c@3c900000 { @@ -234,20 +255,31 @@ lis3dc: lis331dlh@18 { compatible = "st,lis3lv02d"; + /* Linux DT 7-bit. Wire 8-bit is 0x30 write / 0x31 read. */ reg = <0x18>; + status = "okay"; }; d1830: pmic@73 { compatible = "dlg,d1830-gpio"; + /* Linux DT 7-bit. SEC sub_1C8C(115). Wire 8-bit 0xE6/0xE7. */ reg = <0x73>; gpio-controller; #gpio-cells = <2>; + /* OSOS sub_26520 packs PMIC regs 5-8; sub_3B3100 + sub_FFA*: + * ID 1 Home = reg7 bit4 (status bit 16) + * ID 9 Sleep = reg7 bit5 (status bit 17) + * ID 6 Play = reg8 bit1 (status bit 19) + * Key path is active-low: sub_4195D8(id, bit==0). + * GPIOButtonManager has only GPIO 40/41 — not these. + */ dlg,gpio-map = <0x07 4>, /* Home OSOS ID 1 */ <0x07 5>, /* Sleep OSOS ID 9 */ - <0x08 1>; /* Play OSOS ID 6 */ + <0x08 1>; /* Play OSOS ID 6 (side, between Vol±) */ /* SoC GPIO 86 → EIC group2 → VIC EXT3 */ interrupt-parent = <&eic>; interrupts = <86 IRQ_TYPE_LEVEL_LOW>; + monitored-battery = <&battery>; }; }; @@ -263,6 +295,7 @@ reg = <0x38000000 0x100>; clocks = <&clkctrl CLK_SHA1>; clock-names = "sha1"; + status = "okay"; }; aes: aes@38c00000 { @@ -270,6 +303,7 @@ reg = <0x38c00000 0x100>; clocks = <&clkctrl CLK_AES>; clock-names = "aes"; + status = "okay"; }; prng: prng@3c100000 { @@ -277,17 +311,97 @@ reg = <0x3c100000 0x100>; clocks = <&clkctrl CLK_PRNG>; clock-names = "prng"; + status = "okay"; + }; + + /* + * SPI0 @0x3C300000 — CS42L81 / 338S1146 control (RetailOS). + * Panel pixels are LCDIF@383, not this bus. Pads 0–3 are the + * SEC/gpio-s5l8740 SPI0 group (PCON 0x2222 in the SPI driver). + */ + spi0: spi@3c300000 { + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", + "samsung,s5l8702-spi"; + reg = <0x3c300000 0x100>; + /* CLKCON already ungate-all; SPI driver also clears PWRCON1 SPI0. */ + #address-cells = <1>; + #size-cells = <0>; + status = "okay"; + + cs42l81: codec@0 { + compatible = "cirrus,cs42l81", "apple,338s1146"; + reg = <0>; + spi-max-frequency = <1000000>; + #sound-dai-cells = <0>; + status = "okay"; + }; + }; + + /* + * SPI2 @0x3D200000 — TI 343S0538 Nimbus. Pads 87/5, 88/3, 89/3, + * 90/3 (OSOS 20690). EN 14, RST 39, IRQ 38. No clocks= — CCF + * only publishes SHA/AES/PRNG; ungate-all + SPI PWRCON poke. + */ + spi2: spi@3d200000 { + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", + "samsung,s5l8702-spi"; + reg = <0x3d200000 0x100>; + #address-cells = <1>; + #size-cells = <0>; + status = "okay"; + + nimbus: touchscreen@0 { + compatible = "apple,nimbus"; + reg = <0>; + spi-max-frequency = <1000000>; + enable-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; + reset-gpios = <&gpio 39 GPIO_ACTIVE_LOW>; + attn-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; + /* GPIO38 → EIC group1 → VIC EXT1 */ + interrupts-extended = <&eic 38 IRQ_TYPE_LEVEL_LOW>; + status = "okay"; + }; + }; + + /* PL080 pair from OSOS. Peri 12/13 = IIS0. Not 0x384 (DWC2). */ + dmac: dma-controller@38200000 { + compatible = "apple,s5l8740-pl080", "arm,pl080"; + reg = <0x38200000 0x1000>, <0x38700000 0x1000>; + interrupt-parent = <&vic0>; + interrupts = <16>, <17>; + #dma-cells = <2>; + status = "okay"; + }; + + i2s0: i2s@3ca00000 { + compatible = "apple,s5l8740-i2s", "samsung,s5l8740-i2s"; + reg = <0x3ca00000 0x1000>; + dmas = <&dmac 10 0>, <&dmac 11 0>; + dma-names = "tx", "rx"; + #sound-dai-cells = <0>; + status = "okay"; + }; + + nano7_audio: audio { + compatible = "apple,n31-audio"; + apple,cpu = <&i2s0>; + apple,codec = <&cs42l81>; + status = "okay"; }; }; - /* Home/Sleep/Play come from gpio-d1830 + nIRQ, not this node. - * gpio-keys-polled on Vol± issues GPIOCMD 0xFFFE and the pads - * go quiet. gpio-s5l8740 polls DIN and reports the keys. - */ + /* Design pack only. Voltage comes from D1830 OSOS ADC, not this node. */ + battery: battery { + compatible = "simple-battery"; + voltage-min-design-microvolt = <3300000>; + voltage-max-design-microvolt = <4200000>; + energy-full-design-microwatt-hours = <740000>; + charge-full-design-microamp-hours = <200000>; + }; + gpio-keys { - compatible = "gpio-keys-polled"; + compatible = "gpio-keys"; status = "disabled"; - poll-interval = <50>; button-home { label = "Home Button"; @@ -309,19 +423,28 @@ linux,code = ; status = "disabled"; }; + }; + + /* Disabled so gpio-keys-polled cannot direction_input/0xFFFE Vol pads. + * gpio-s5l8740 polls DIN itself and reports KEY_VOLUMEUP/DOWN. + */ + gpio-keys-vol { + compatible = "gpio-keys-polled"; + status = "disabled"; + poll-interval = <50>; button-volup { + /* OSOS id 8, GPIO 0x28. Glass: Vol+. */ label = "Volume Up"; gpios = <&gpio 40 GPIO_ACTIVE_LOW>; linux,code = ; - status = "disabled"; }; button-voldown { + /* OSOS id 7, GPIO 0x29. Glass: Vol-. */ label = "Volume Down"; gpios = <&gpio 41 GPIO_ACTIVE_LOW>; linux,code = ; - status = "disabled"; }; }; }; diff --git a/arch/arm/configs/apple_n31_bootminimal_defconfig b/arch/arm/configs/apple_n31_bootminimal_defconfig new file mode 100755 index 00000000000000..53377f4837839f --- /dev/null +++ b/arch/arm/configs/apple_n31_bootminimal_defconfig @@ -0,0 +1,41 @@ +# N31 boot-minimal — LCD + timer + clk only (bisect ~500ms OSOS reset) +CONFIG_ARCH_S5L87XX=y +CONFIG_CPU_S5L8740=y +CONFIG_CMDLINE="console=tty0 fbcon=font:MINI4x6 vt.global_cursor_default=0 loglevel=8 panic=-1 nohlt" +CONFIG_CMDLINE_FORCE=y +CONFIG_PRINTK_TIME=y +CONFIG_S5L8720_TIMER=y +CONFIG_SERIAL_SAMSUNG=y +# CONFIG_SERIAL_SAMSUNG_CONSOLE is not set +CONFIG_VFP=y +CONFIG_TMPFS=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_DRM=y +CONFIG_TINYDRM_S5L8740=y +CONFIG_FB=y +CONFIG_FRAMEBUFFER_CONSOLE=y +CONFIG_DRM_FBDEV_EMULATION=y +CONFIG_FONTS=y +CONFIG_FONT_8x8=y +CONFIG_FONT_MINI_4x6=y +CONFIG_CLK_S5L8702=y +CONFIG_ARM_VIC=y +CONFIG_BACKLIGHT_CLASS_DEVICE=y +CONFIG_BACKLIGHT_S5L8740=y +CONFIG_BLK_DEV_INITRD=y +# No panic→reboot (was masking oops before fbcon ready) +# CONFIG_POWER_RESET is not set +# CONFIG_POWER_RESET_SYSCON is not set +# Peripheral drivers compiled out — DT disabled alone was not enough +# CONFIG_USB is not set +# CONFIG_NET is not set +# CONFIG_I2C is not set +# CONFIG_SPI is not set +# CONFIG_CRYPTO is not set +# CONFIG_GPIOLIB is not set +# CONFIG_GPIO_S5L8740 is not set +# CONFIG_S5L8740_EIC is not set +# CONFIG_INPUT is not set +# CONFIG_SND is not set +# CONFIG_DMA_ENGINE is not set diff --git a/arch/arm/configs/apple_n31_defconfig b/arch/arm/configs/apple_n31_defconfig index fe3fbfe9785fbc..a5a586905ab45d 100644 --- a/arch/arm/configs/apple_n31_defconfig +++ b/arch/arm/configs/apple_n31_defconfig @@ -1,6 +1,7 @@ CONFIG_ARCH_S5L87XX=y CONFIG_CPU_S5L8740=y -CONFIG_CMDLINE="console=tty0 console=ttySAC0 earlyprintk nohlt" +CONFIG_CMDLINE="console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=-1 clk_ignore_unused init=/init" +CONFIG_DEBUG_USER=y CONFIG_EARLY_PRINTK=y CONFIG_PRINTK_TIME=y CONFIG_DEBUG_KERNEL=y @@ -9,16 +10,39 @@ CONFIG_DEBUG_S3C_UART3=y CONFIG_S5L8720_TIMER=y CONFIG_SERIAL_SAMSUNG=y CONFIG_SERIAL_SAMSUNG_CONSOLE=y -CONFIG_PHY_S5L8720_USB2=y -CONFIG_USB_DWC2=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +# CONFIG_MODVERSIONS is not set +# CONFIG_MODULE_COMPRESS is not set +CONFIG_DEBUG_FS=y +CONFIG_DYNAMIC_DEBUG=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_USB_DWC2=m +CONFIG_USB_DWC2_PERIPHERAL=y CONFIG_USB_GADGET=y -CONFIG_USB_SNP_UDC_PLAT=y -CONFIG_USB_ETH=y -CONFIG_USB_ETH_RNDIS=y +# CONFIG_USB_SNP_UDC_PLAT is not set +# CONFIG_USB_ETH is not set +CONFIG_USB_CONFIGFS=y +CONFIG_USB_CONFIGFS_ACM=y +CONFIG_USB_CONFIGFS_RNDIS=y +CONFIG_USB_U_SERIAL=y +CONFIG_USB_F_ACM=y +CONFIG_USB_F_RNDIS=y +CONFIG_USB_LIBCOMPOSITE=y +CONFIG_CONFIGFS_FS=y +CONFIG_PHY_S5L8702_USB2=y +CONFIG_APPLE_TRISTAR_CBTL1609=m +CONFIG_UNIX98_PTYS=y +CONFIG_DEVPTS_FS=y CONFIG_NET=y +CONFIG_NETDEVICES=y +CONFIG_NET_CORE=y +# CONFIG_ETHERNET is not set CONFIG_UNIX=y CONFIG_PACKET=y CONFIG_INET=y +# CONFIG_IPV6 is not set CONFIG_IP_PNP=y # CONFIG_NFS_FS=y # CONFIG_NFS_V4=y @@ -33,27 +57,34 @@ CONFIG_TINYDRM_S5L8740=y CONFIG_FB=y CONFIG_FRAMEBUFFER_CONSOLE=y CONFIG_DRM_FBDEV_EMULATION=y -CONFIG_FRAMEBUFFER_CONSOLE_ROTATION=y -CONFIG_LOGO=y +CONFIG_FONTS=y +CONFIG_FONT_8x8=y +CONFIG_FONT_MINI_4x6=y +# CONFIG_FRAMEBUFFER_CONSOLE_ROTATION is not set +# CONFIG_LOGO is not set CONFIG_BLK_DEV_INITRD=y CONFIG_REGMAP=y CONFIG_REGMAP_MMIO=y CONFIG_MFD_SYSCON=y CONFIG_POWER_RESET=y CONFIG_POWER_RESET_SYSCON=y +CONFIG_POWER_SUPPLY=y +CONFIG_BACKLIGHT_CLASS_DEVICE=y +CONFIG_BACKLIGHT_S5L8740=y CONFIG_GPIOLIB=y CONFIG_GPIOLIB_FASTPATH_LIMIT=512 CONFIG_OF_GPIO=y CONFIG_GPIO_CDEV=y CONFIG_GPIO_CDEV_V1=y -CONFIG_GPIO_D1830=y -CONFIG_GPIO_GENERIC=y -CONFIG_GPIO_GENERIC_PLATFORM=y +CONFIG_GPIO_D1830=m +CONFIG_S5L8740_EIC=y +CONFIG_GPIO_S5L8740=y +CONFIG_KEYBOARD_GPIO=y CONFIG_KEYBOARD_GPIO_POLLED=y CONFIG_INPUT_EVDEV=y -CONFIG_I2C_S5L8702=y +CONFIG_I2C_S5L8702=m CONFIG_I2C_CHARDEV=y -CONFIG_SENSORS_LIS3_I2C=y +CONFIG_SENSORS_LIS3_I2C=m CONFIG_CRYPTO=y CONFIG_CRYPTO_DEV_S5L8702_AES=y CONFIG_CRYPTO_USER_API_SKCIPHER=y @@ -62,8 +93,40 @@ CONFIG_CRYPTO_USER_API_HASH=y CONFIG_CRYPTO_DEV_S5L8702_PRNG=y CONFIG_CRYPTO_USER_API_RNG=y CONFIG_CLK_S5L8702=y -CONFIG_PHY_S5L8702_USB2=y -CONFIG_S5L8740_EIC=y -CONFIG_GPIO_S5L8740=y -CONFIG_BACKLIGHT_CLASS_DEVICE=y -CONFIG_BACKLIGHT_S5L8740=y +CONFIG_SPI=y +CONFIG_SPI_S5L8702=m +CONFIG_INPUT=y +CONFIG_INPUT_TOUCHSCREEN=y +CONFIG_TOUCHSCREEN_APPLE_NIMBUS=m +CONFIG_FW_LOADER=y +CONFIG_SOUND=y +CONFIG_SND=y +CONFIG_SND_SOC=y +CONFIG_SND_SOC_GENERIC_DMAENGINE_PCM=y +CONFIG_SND_DMAENGINE_PCM=y +CONFIG_SND_SOC_APPLE_CS42L81_SPI=m +CONFIG_SND_SOC_APPLE_S5L8740_I2S=m +CONFIG_SND_SOC_APPLE_NANO7=m +CONFIG_DMADEVICES=y +CONFIG_DMA_ENGINE=y +CONFIG_DMA_VIRTUAL_CHANNELS=y +CONFIG_S5L8740_PL080=m +# Block + buffer_head must be built into vmlinux so FAT/VFAT modules +# can resolve mark_buffer_dirty / __bread / etc. (FAT selects BUFFER_HEAD; +# a stale O-tree with CONFIG_BUFFER_HEAD=y but no fs/buffer.o breaks vfat.ko.) +CONFIG_BLOCK=y +CONFIG_BUFFER_HEAD=y +CONFIG_NLS=y +CONFIG_FAT_FS=m +CONFIG_VFAT_FS=m +CONFIG_MSDOS_FS=m +CONFIG_NLS_CODEPAGE_437=m +CONFIG_NLS_ISO8859_1=m +CONFIG_NLS_UTF8=m +CONFIG_MSDOS_PARTITION=y +CONFIG_EFI_PARTITION=y +CONFIG_SND_OSSEMUL=y +CONFIG_SND_MIXER_OSS=y +CONFIG_SND_PCM_OSS=y +CONFIG_FMSS_S5L8740=m +CONFIG_FTL_S5L8740=m diff --git a/arch/arm/configs/apple_n31_nodrm_defconfig b/arch/arm/configs/apple_n31_nodrm_defconfig new file mode 100755 index 00000000000000..39831181bbf2cd --- /dev/null +++ b/arch/arm/configs/apple_n31_nodrm_defconfig @@ -0,0 +1,19 @@ +# N31 nodrm bisect — no LCD drivers, no initramfs, BL-off witness +CONFIG_ARCH_S5L87XX=y +CONFIG_CPU_S5L8740=y +CONFIG_CMDLINE="panic=-1 loglevel=8 nohlt" +CONFIG_CMDLINE_FORCE=y +CONFIG_PRINTK_TIME=y +CONFIG_S5L8720_TIMER=y +CONFIG_VFP=y +CONFIG_TMPFS=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CLK_S5L8702=y +CONFIG_ARM_VIC=y +CONFIG_N31_EARLY_BL_OFF=y +# CONFIG_DRM is not set +# CONFIG_FB is not set +# CONFIG_BLK_DEV_INITRD is not set +# CONFIG_SERIAL_SAMSUNG is not set +# CONFIG_POWER_RESET is not set diff --git a/arch/arm/mach-s5l87xx/s5l87xx.c b/arch/arm/mach-s5l87xx/s5l87xx.c index 2dafd3ba98bba7..dfa612a7a43325 100644 --- a/arch/arm/mach-s5l87xx/s5l87xx.c +++ b/arch/arm/mach-s5l87xx/s5l87xx.c @@ -6,18 +6,20 @@ * S5L8740 (Cortex-A5, iPod nano 7g) */ +#include #include #include +#include +#include #include #include #include - #define S5L8740_WDT_PHYS 0x3c800000ul /* * WTF/U-Boot leave the SEC watchdog armed. A bigger zImage loses the race. - * CON=0 then CNT=0. Never CLKCON+0x50. + * Stage0 sequence only — never CLKCON+0x50 (fatal latch). */ static int __init s5l87xx_wdt_disarm(void) { @@ -36,6 +38,24 @@ static int __init s5l87xx_wdt_disarm(void) } early_initcall(s5l87xx_wdt_disarm); +/* Print before Run /init so glass shows whether rootfs actually has PID 1. */ +static int __init n31_init_witness(void) +{ + struct path path; + int err = kern_path("/init", LOOKUP_FOLLOW, &path); + + if (err) { + pr_err("n31: /init missing (%d) — initramfs not in rootfs\n", err); + return 0; + } + pr_info("n31: /init present mode=%o size=%lld\n", + path.dentry->d_inode->i_mode, + (long long)i_size_read(path.dentry->d_inode)); + path_put(&path); + return 0; +} +late_initcall(n31_init_witness); + /* * Map the debug UART into a fixed virtual address so earlyprintk works * across the MMU transition. The physical base is 0x3CC00000; UART3 diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig index 7771edf54fb3f1..b881199eeb7457 100644 --- a/drivers/bluetooth/Kconfig +++ b/drivers/bluetooth/Kconfig @@ -518,3 +518,9 @@ config BT_INTEL_PCIE Say Y here to compiler support for Intel Bluetooth PCIe device into the kernel or say M to compile it as module (btintel_pcie) endmenu + +config BT_BCM2078_N31 + tristate "Broadcom BCM2078 power companion (N31)" + depends on BT && OF && HAS_IOMEM + help + Power/GPIO + Vincent patchram HCD companion for BCM2078 on N31. diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile index 81856512ddd030..2c6727dd91b8fc 100644 --- a/drivers/bluetooth/Makefile +++ b/drivers/bluetooth/Makefile @@ -53,3 +53,4 @@ hci_uart-$(CONFIG_BT_HCIUART_AG6XX) += hci_ag6xx.o hci_uart-$(CONFIG_BT_HCIUART_MRVL) += hci_mrvl.o hci_uart-$(CONFIG_BT_HCIUART_AML) += hci_aml.o hci_uart-objs := $(hci_uart-y) +obj-$(CONFIG_BT_BCM2078_N31) += bcm2078-bt.o diff --git a/drivers/bluetooth/bcm2078-bt.c b/drivers/bluetooth/bcm2078-bt.c new file mode 100755 index 00000000000000..ccf868662f086c --- /dev/null +++ b/drivers/bluetooth/bcm2078-bt.c @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * BCM2078 companion — N31 + * + * Power: RetailOS sub_43D38C mode-2 / 0xFFFE on GPIOs 0x61/0x62/0x77. + * UART1 HCI @ 0x3DB00000 / 115200 (BT Uart RxLoop). + * Patchram: stream Vincent HCD (Write_RAM 0xFC4C ×146 + Launch_RAM 0xFC4E). + * FM: HCI vendor 0xFC15 cookbook from RetailOS BroadcomFM FIFO (sub_4290C4). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BCM_GPIO_PHYS 0x3cf00000UL +#define BCM_GPIOCMD_OFF 0x1e0 +#define BCM_UART1_PHYS 0x3db00000UL +#define BCM_UART_STATUS 0x10 +#define BCM_UART_TX 0x20 +#define BCM_UART_RX 0x24 +#define BCM_UART_TXFULL 0x20 /* STATUS bit — refine on HW if needed */ +#define BCM_UART_RXRDY 0x01 + +#define BCM_GPIO_A 0x61 +#define BCM_GPIO_B 0x62 +#define BCM_GPIO_C 0x77 +#define BCM_GPIO_NOP 0xC8 +#define BCM_MODE_POWER 2 +#define BCM_MODE_CLEAR 0xFFFE + +#define BCM_FW_NAME "brcm/BCM2076B1.hcd" +#define HCI_OP_FC15 0xFC15 + +struct bcm2078_bt { + struct device *dev; + void __iomem *gpio; + void __iomem *gpiocmd; + void __iomem *uart; + struct clk *uart_clk; + bool powered; + bool patched; + bool fw_present; + struct mutex lock; +}; + +/* ---------- GPIO (RetailOS sub_43D38C) ---------- */ + +static void bcm_43D38C(struct bcm2078_bt *bt, unsigned int gpio, u16 mode, int val) +{ + void __iomem *bank; + u32 pin, dir; + u8 cmd; + + if (!bt->gpio || !bt->gpiocmd || gpio == BCM_GPIO_NOP) + return; + bank = bt->gpio + 32 * (gpio >> 3); + pin = gpio & 7; + if (mode == 1) { + cmd = val ? 15 : 14; + } else if (mode == BCM_MODE_CLEAR) { + dir = readl(bank + 0x14); + writel(dir & ~BIT(pin), bank + 0x14); + cmd = 0; + } else { + cmd = (u8)mode; + dir = readl(bank + 0x14); + writel(dir | BIT(pin), bank + 0x14); + } + writel(((gpio >> 3) << 16) | (pin << 8) | cmd, bt->gpiocmd); +} + +static void bcm_power_pins_on(struct bcm2078_bt *bt) +{ + bcm_43D38C(bt, BCM_GPIO_NOP, 0, 0); + bcm_43D38C(bt, BCM_GPIO_A, BCM_MODE_POWER, 0); + bcm_43D38C(bt, BCM_GPIO_B, BCM_MODE_POWER, 0); + bcm_43D38C(bt, BCM_GPIO_C, BCM_MODE_POWER, 0); +} + +static void bcm_power_pins_off(struct bcm2078_bt *bt) +{ + bcm_43D38C(bt, BCM_GPIO_NOP, BCM_MODE_CLEAR, 0); + bcm_43D38C(bt, BCM_GPIO_A, BCM_MODE_CLEAR, 0); + bcm_43D38C(bt, BCM_GPIO_B, BCM_MODE_CLEAR, 0); + bcm_43D38C(bt, BCM_GPIO_C, BCM_MODE_CLEAR, 0); +} + +/* ---------- UART1 H4 HCI ---------- */ + +static int bcm_uart_tx(struct bcm2078_bt *bt, const u8 *buf, size_t len) +{ + size_t i; + unsigned guard; + + for (i = 0; i < len; i++) { + guard = 200000; + while (guard-- && (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_TXFULL)) + cpu_relax(); + writel(buf[i], bt->uart + BCM_UART_TX); + } + return 0; +} + +static size_t bcm_uart_rx(struct bcm2078_bt *bt, u8 *buf, size_t maxlen, + unsigned timeout_ms) +{ + size_t n = 0; + unsigned long deadline = jiffies + msecs_to_jiffies(timeout_ms); + + while (n < maxlen && time_before(jiffies, deadline)) { + if (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_RXRDY) + buf[n++] = (u8)readl(bt->uart + BCM_UART_RX); + else + cpu_relax(); + } + return n; +} + +static void bcm_uart_drain(struct bcm2078_bt *bt) +{ + unsigned guard = 10000; + + while (guard-- && (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_RXRDY)) + (void)readl(bt->uart + BCM_UART_RX); +} + +static int bcm_hci_cmd(struct bcm2078_bt *bt, u16 opcode, const u8 *plen_payload, + u8 plen, u8 *evt, size_t evt_max, size_t *evt_n) +{ + u8 hdr[4]; + size_t n; + + hdr[0] = 0x01; /* H4 CMD */ + hdr[1] = opcode & 0xff; + hdr[2] = opcode >> 8; + hdr[3] = plen; + bcm_uart_drain(bt); + bcm_uart_tx(bt, hdr, 4); + if (plen && plen_payload) + bcm_uart_tx(bt, plen_payload, plen); + n = bcm_uart_rx(bt, evt, evt_max, 500); + if (evt_n) + *evt_n = n; + return n > 0 ? 0 : -ETIMEDOUT; +} + +static int bcm_hci_reset(struct bcm2078_bt *bt) +{ + u8 evt[32]; + size_t n; + int ret; + + ret = bcm_hci_cmd(bt, 0x0c03, NULL, 0, evt, sizeof(evt), &n); + dev_info(bt->dev, "HCI Reset → %d RX %zu:%*ph\n", ret, n, (int)n, evt); + return ret; +} + +/* ---------- Patchram (Vincent HCD) ---------- */ + +static int bcm_load_hcd(struct bcm2078_bt *bt) +{ + const struct firmware *fw; + const u8 *p, *end; + u8 evt[64]; + size_t n; + unsigned cmds = 0; + int ret; + + ret = request_firmware(&fw, BCM_FW_NAME, bt->dev); + if (ret) { + dev_err(bt->dev, "firmware %s: %d\n", BCM_FW_NAME, ret); + return ret; + } + + p = fw->data; + end = p + fw->size; + while (p + 4 <= end) { + u8 type = p[0]; + u16 opcode; + u8 plen; + + if (type != 0x01) { + dev_err(bt->dev, "HCD bad type %02x @+%zx\n", + type, p - fw->data); + ret = -EINVAL; + break; + } + opcode = p[1] | (p[2] << 8); + plen = p[3]; + if (p + 4 + plen > end) { + ret = -EINVAL; + break; + } + bcm_uart_drain(bt); + bcm_uart_tx(bt, p, 4 + plen); + n = bcm_uart_rx(bt, evt, sizeof(evt), 1000); + cmds++; + if (n < 2 || evt[0] != 0x04) { + dev_warn(bt->dev, + "patch cmd#%u op=%04x plen=%u RX %zu:%*ph\n", + cmds, opcode, plen, n, (int)n, evt); + } + p += 4 + plen; + /* Launch_RAM is last */ + if (opcode == 0xfc4e) + break; + } + release_firmware(fw); + bt->patched = (ret == 0); + dev_info(bt->dev, "patchram %s — %u HCI cmds\n", + ret ? "FAIL" : "OK", cmds); + return ret; +} + +/* ---------- FM 0xFC15 cookbook (RetailOS BroadcomFM) ---------- */ + +static int bcm_fc15(struct bcm2078_bt *bt, const u8 *payload, u8 plen) +{ + u8 evt[64]; + size_t n; + int ret; + + ret = bcm_hci_cmd(bt, HCI_OP_FC15, payload, plen, evt, sizeof(evt), &n); + dev_dbg(bt->dev, "FC15 plen=%u → RX %zu:%*ph\n", plen, n, (int)n, evt); + return ret; +} + +/* Encode FIFO-style write8: plen=3, reg, 0x00, val */ +static int bcm_fm_w8(struct bcm2078_bt *bt, u8 reg, u8 val) +{ + u8 p[3] = { reg, 0x00, val }; + + return bcm_fc15(bt, p, 3); +} + +static int bcm_fm_w16(struct bcm2078_bt *bt, u8 reg, u16 val) +{ + u8 p[4] = { reg, 0x00, val & 0xff, val >> 8 }; + + return bcm_fc15(bt, p, 4); +} + +static int bcm_fm_power_on(struct bcm2078_bt *bt) +{ + int ret; + /* DD136 + DD334(0) + DD458 + DD2FC(33,20) */ + ret = bcm_fm_w8(bt, 0x00, 0x03); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0x14, 0x0c); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0x02, 0x02); + if (ret) + return ret; + ret = bcm_fm_w16(bt, 0x05, 0x0001); + if (ret) + return ret; + { + u8 rd[3] = { 0x4d, 0x01, 0x01 }; /* read status */ + + bcm_fc15(bt, rd, 3); + } + { + /* RSSI=33 noise=20 — DD2FC */ + u8 p[11] = { + 0xf9, 0x00, 0x21, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x00, 0x00 + }; + + ret = bcm_fc15(bt, p, 11); + } + dev_info(bt->dev, "FM power ON (0xFC15 cookbook)%s\n", + ret ? " FAIL" : ""); + return ret; +} + +static int bcm_fm_power_off(struct bcm2078_bt *bt) +{ + int ret = bcm_fm_w8(bt, 0x00, 0x00); /* DD118: 00 00 00 */ + + dev_info(bt->dev, "FM power OFF%s\n", ret ? " FAIL" : ""); + return ret; +} + +static int bcm_fm_tune_khz(struct bcm2078_bt *bt, unsigned int khz) +{ + u16 enc; + int ret; + + /* Band: >=87500 → 2 else 3 */ + ret = bcm_fm_w8(bt, 0x01, khz >= 87500 ? 2 : 3); + if (ret) + return ret; + /* Pre-tune 56DB66: reg 0x10 = 0x1203 */ + ret = bcm_fm_w16(bt, 0x10, 0x1203); + if (ret) + return ret; + enc = (u16)((khz + 1536) & 0xffff); + ret = bcm_fm_w16(bt, 0x0a, enc); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0x09, 0x01); /* unmute */ + dev_info(bt->dev, "FM tune %u kHz enc=%04x%s\n", + khz, enc, ret ? " FAIL" : ""); + return ret; +} + +static int bcm_fm_seek(struct bcm2078_bt *bt, int up, u8 rssi) +{ + u8 flags = 0x70 | (up ? 0x80 : 0); + int ret; + + ret = bcm_fm_w8(bt, 0x07, flags); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0x08, rssi ? rssi : 33); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0xde, 0x01); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0xfc, 0x00); + if (ret) + return ret; + ret = bcm_fm_w8(bt, 0x09, 0x02); + dev_info(bt->dev, "FM seek %s rssi=%u%s\n", + up ? "up" : "down", rssi ? rssi : 33, ret ? " FAIL" : ""); + return ret; +} + +/* ---------- Power / bring-up ---------- */ + +static int bcm_power_on(struct bcm2078_bt *bt) +{ + if (bt->uart_clk) + clk_prepare_enable(bt->uart_clk); + bcm_power_pins_on(bt); + msleep(150); + bt->powered = true; + bcm_hci_reset(bt); + return 0; +} + +static void bcm_power_off(struct bcm2078_bt *bt) +{ + if (bt->patched) + bcm_fm_power_off(bt); + bcm_power_pins_off(bt); + if (bt->uart_clk) + clk_disable_unprepare(bt->uart_clk); + bt->powered = false; + bt->patched = false; +} + +/* ---------- sysfs ---------- */ + +static ssize_t power_on_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", bt->powered ? 1 : 0); +} + +static ssize_t power_on_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + unsigned int on; + + if (kstrtouint(buf, 0, &on)) + return -EINVAL; + mutex_lock(&bt->lock); + if (on) + bcm_power_on(bt); + else + bcm_power_off(bt); + mutex_unlock(&bt->lock); + return count; +} +static DEVICE_ATTR_RW(power_on); + +static ssize_t patchram_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + int ret; + + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + ret = bcm_load_hcd(bt); + mutex_unlock(&bt->lock); + return ret ? ret : count; +} + +static ssize_t patchram_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", bt->patched ? 1 : 0); +} +static DEVICE_ATTR_RW(patchram); + +static ssize_t fm_power_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + unsigned int on; + int ret; + + if (kstrtouint(buf, 0, &on)) + return -EINVAL; + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + if (!bt->patched) + bcm_load_hcd(bt); + ret = on ? bcm_fm_power_on(bt) : bcm_fm_power_off(bt); + mutex_unlock(&bt->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(fm_power); + +static ssize_t fm_tune_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + unsigned int khz; + int ret; + + /* accept kHz or MHz*10 (e.g. 991 for 99.1) */ + if (kstrtouint(buf, 0, &khz)) + return -EINVAL; + if (khz < 1000) + khz *= 100; /* 991 → 99100 */ + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + if (!bt->patched) + bcm_load_hcd(bt); + ret = bcm_fm_tune_khz(bt, khz); + mutex_unlock(&bt->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(fm_tune); + +static ssize_t fm_seek_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + int up = 1; + int ret; + + if (buf[0] == 'd' || buf[0] == '0' || buf[0] == '-') + up = 0; + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + if (!bt->patched) + bcm_load_hcd(bt); + ret = bcm_fm_seek(bt, up, 33); + mutex_unlock(&bt->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(fm_seek); + +static ssize_t patchram_info_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + return sysfs_emit(buf, + "hcd=/lib/firmware/%s\n" + "hci=Write_RAM(0xFC4C)x146+Launch_RAM(0xFC4E)\n" + "load=echo 1 > patchram\n" + "fm=echo 1 > fm_power; echo 99100 > fm_tune; echo up > fm_seek\n" + "fc15=RetailOS BroadcomFM FIFO cookbook\n", + BCM_FW_NAME); +} +static DEVICE_ATTR_RO(patchram_info); + +static struct attribute *bcm_attrs[] = { + &dev_attr_power_on.attr, + &dev_attr_patchram.attr, + &dev_attr_patchram_info.attr, + &dev_attr_fm_power.attr, + &dev_attr_fm_tune.attr, + &dev_attr_fm_seek.attr, + NULL, +}; +ATTRIBUTE_GROUPS(bcm); + +static int bcm2078_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct bcm2078_bt *bt; + const struct firmware *fw; + + bt = devm_kzalloc(dev, sizeof(*bt), GFP_KERNEL); + if (!bt) + return -ENOMEM; + bt->dev = dev; + mutex_init(&bt->lock); + platform_set_drvdata(pdev, bt); + + bt->gpio = devm_ioremap(dev, BCM_GPIO_PHYS, 0x400); + if (!bt->gpio) + return -ENOMEM; + bt->gpiocmd = bt->gpio + BCM_GPIOCMD_OFF; + + bt->uart = devm_ioremap(dev, BCM_UART1_PHYS, 0x40); + if (!bt->uart) + return -ENOMEM; + + bt->uart_clk = devm_clk_get_optional(dev->parent ? dev->parent : dev, + "uart"); + if (IS_ERR(bt->uart_clk)) + bt->uart_clk = NULL; + + if (request_firmware(&fw, BCM_FW_NAME, dev) == 0) { + bt->fw_present = fw->size > 0; + release_firmware(fw); + } + + if (sysfs_create_groups(&dev->kobj, bcm_groups)) + dev_warn(dev, "sysfs groups failed\n"); + + /* + * Do NOT power UART or load patchram at probe — boot-time HCI + raw GPIO + * poke (0x61/0x62/0x77) correlated with reset back to RetailOS. + * Use sysfs power_on / patchram or /bin/n31-bt-up after init is up. + */ + dev_info(dev, + "BCM2078 deferred — echo 1 > power_on; echo 1 > patchram (fw=%s)\n", + bt->fw_present ? BCM_FW_NAME : "missing"); + return 0; +} + +static void bcm2078_remove(struct platform_device *pdev) +{ + struct bcm2078_bt *bt = platform_get_drvdata(pdev); + + sysfs_remove_groups(&pdev->dev.kobj, bcm_groups); + bcm_power_off(bt); +} + +static const struct of_device_id bcm2078_of_match[] = { + { .compatible = "brcm,bcm2078" }, + { .compatible = "brcm,bcm4329-bt" }, + { } +}; +MODULE_DEVICE_TABLE(of, bcm2078_of_match); + +static struct platform_driver bcm2078_driver = { + .probe = bcm2078_probe, + .remove = bcm2078_remove, + .driver = { + .name = "bcm2078-bt", + .of_match_table = bcm2078_of_match, + }, +}; + +static int __init bcm2078_init(void) +{ + struct device_node *np; + + for_each_compatible_node(np, NULL, "brcm,bcm2078") + if (of_device_is_available(np)) + of_platform_device_create(np, NULL, NULL); + for_each_compatible_node(np, NULL, "brcm,bcm4329-bt") + if (of_device_is_available(np)) + of_platform_device_create(np, NULL, NULL); + return platform_driver_register(&bcm2078_driver); +} +module_init(bcm2078_init); + +static void __exit bcm2078_exit(void) +{ + platform_driver_unregister(&bcm2078_driver); +} +module_exit(bcm2078_exit); + +MODULE_DESCRIPTION("BCM2078 HCI patchram + FM 0xFC15 (N31)"); +MODULE_LICENSE("GPL"); +MODULE_FIRMWARE(BCM_FW_NAME); diff --git a/drivers/clk/clk-s5l8702.c b/drivers/clk/clk-s5l8702.c old mode 100644 new mode 100755 index 7e33a5f50ed332..ab6cc6c7e2bdac --- a/drivers/clk/clk-s5l8702.c +++ b/drivers/clk/clk-s5l8702.c @@ -102,8 +102,7 @@ static int s5l8702_clk_probe(struct platform_device *pdev) if (IS_ERR(clk_data->regs)) return PTR_ERR(clk_data->regs); - if (of_machine_is_compatible("samsung,s5l8740")) - s5l8740_ungate_all(dev, clk_data->regs); + s5l8740_ungate_all(dev, clk_data->regs); hw_data = devm_kzalloc(dev, struct_size(hw_data, hws, num_clks), GFP_KERNEL); if (!hw_data) diff --git a/drivers/clk/n31-early-bringup.c b/drivers/clk/n31-early-bringup.c new file mode 100755 index 00000000000000..729062132d7deb --- /dev/null +++ b/drivers/clk/n31-early-bringup.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * N31 earliest bring-up hooks (before any platform drivers): + * 1) SEC WDT @0x3C800000 disarm (stage0 sequence + clear enable bits) + * 2) CLKCON+0x50 fatal latch clear + * 3) Optional glass witness: backlight OFF @0x3E000000 (U-Boot leaves BL on) + */ +#include +#include +#include + +#define N31_WDT_PHYS 0x3c800000ul +#define N31_BL_PHYS 0x3e000000ul +#define N31_CLKCON_PHYS 0x3c500000ul + +static void n31_wdt_disarm_full(void) +{ + void __iomem *wdt, *fatal; + u32 wcon, wcnt; + + wdt = ioremap(N31_WDT_PHYS, 8); + if (!wdt) + return; + + wcon = readl(wdt); + wcnt = readl(wdt + 4); + writel(0, wdt + 4); + writel(0, wdt); + writel(0, wdt + 4); + writel(0, wdt); + + pr_alert("N31>> WDT disarm con=%08x cnt=%08x now=%08x/%08x\n", + wcon, wcnt, readl(wdt), readl(wdt + 4)); + iounmap(wdt); + + fatal = ioremap(N31_CLKCON_PHYS + 0x50, 4); + if (fatal) { + u32 v = readl(fatal); + + if (v) + pr_alert("N31>> CLKCON+0x50 was %08x — clearing\n", v); + writel(0, fatal); + iounmap(fatal); + } +} + +static void n31_bl_off_witness(void) +{ + void __iomem *aux; + + aux = ioremap(N31_BL_PHYS, 0x10); + if (!aux) + return; + writel(readl(aux + 0x04) & ~1u, aux + 0x04); + writel(readl(aux + 0x08) & ~1u, aux + 0x08); + pr_alert("N31>> BL OFF witness (expect screen black)\n"); + iounmap(aux); +} + +static int __init n31_early_bringup(void) +{ + n31_wdt_disarm_full(); + n31_bl_off_witness(); + return 0; +} +early_initcall(n31_early_bringup); diff --git a/drivers/dma/Kconfig b/drivers/dma/Kconfig index 8afea2e2336027..8e76a88c6f1aa1 100644 --- a/drivers/dma/Kconfig +++ b/drivers/dma/Kconfig @@ -787,3 +787,11 @@ config DMA_ENGINE_RAID bool endif + +config S5L8740_PL080 + tristate "S5L8740 PL080 DMAC probe (N31)" + depends on ARM + select DMA_ENGINE + select DMA_VIRTUAL_CHANNELS + help + PL080 dmaengine at 0x38200000 / 0x38700000 for iPod nano 7G. diff --git a/drivers/dma/Makefile b/drivers/dma/Makefile index 19ba465011a6d5..bb14d5472c6d27 100644 --- a/drivers/dma/Makefile +++ b/drivers/dma/Makefile @@ -90,3 +90,4 @@ obj-y += qcom/ obj-y += stm32/ obj-y += ti/ obj-y += xilinx/ +obj-$(CONFIG_S5L8740_PL080) += dma-s5l8740-pl080.o diff --git a/drivers/dma/dma-s5l8740-pl080.c b/drivers/dma/dma-s5l8740-pl080.c new file mode 100755 index 00000000000000..c0ae6d3c0a5212 --- /dev/null +++ b/drivers/dma/dma-s5l8740-pl080.c @@ -0,0 +1,1105 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 dual PL080 DMA (N31) + * + * Bases: 0x38200000 / 0x38700000 (OSOS pair). Not 0x384 (DWC OTG). + * + * DT #dma-cells = <2>: + * Quirks (PL080 + I²S on S5L8740/N31): + * Burst: M2P dest=1 beat (fixed IIS FIFO @+0x10); src≈4 beats (half FIFO). + * Peri: glass IIS0 TX/RX = 10/11 (Rockbox 0xA); OSOS table 12/13 never TCs. + * Cache: PL080 not coherent — dma_sync in start(); no CTL_PROT_CACHE on slave. + * LLI: dma_alloc_coherent, 16-byte aligned chain; misaligned LLI hangs engine. + * terminate_all: CFG disable + bounded ENBLD poll — never spin on BUSY (amba-pl08x). + * SG: multi-element builds LLI chain; contiguous buffers preferred (CMA). + * AHB: M2P src=mem on AHB2 (ahb_s=1), dst=FIFO on AHB1 (ahb_d=0). + * FIFO: S3C64xx-style ~64 deep — src burst 8 (m2p_src_burst=2), dst=1. + * PL080S: CONTROL2 @+0x114 holds count (OSOS B424C), not CTL low bits. + * Cache: ARM1176 32-byte lines — LLI/buffer 32-byte aligned; sync in start(). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "virt-dma.h" + +#define PL080_INT_STATUS 0x00 +#define PL080_INT_TC_STATUS 0x04 +#define PL080_INT_TC_CLEAR 0x08 +#define PL080_INT_ERR_STATUS 0x0c +#define PL080_INT_ERR_CLEAR 0x10 +#define PL080_RAW_TC 0x14 +#define PL080_RAW_ERR 0x18 +#define PL080_ENBLD_CHNS 0x1c +#define PL080_SOFT_BREQ 0x20 +#define PL080_SOFT_SREQ 0x24 +#define PL080_CONFIG 0x30 +#define PL080_CONFIG_EN BIT(0) +#define PL080_SYNC 0x34 + +#define PL080_Cx_SRC(i) (0x100 + (i) * 0x20) +#define PL080_Cx_DST(i) (0x104 + (i) * 0x20) +#define PL080_Cx_LLI(i) (0x108 + (i) * 0x20) +#define PL080_Cx_CTL(i) (0x10c + (i) * 0x20) +#define PL080_Cx_CFG(i) (0x110 + (i) * 0x20) +/* OSOS B424C writes transfer count here (PL080S CONTROL2). Not in DDI0196. */ +#define PL080S_Cx_CONTROL2(i) (0x114 + (i) * 0x20) + +#define PL080_CH_COUNT 8 +#define PL080_MAX_XFER_WORDS 0xfff + +#define CTL_SRC_AI BIT(26) +#define CTL_DST_AI BIT(27) +#define CTL_TC_IRQ BIT(31) +#define CTL_WIDTH_SHIFT 18 +#define CTL_DBSIZE_SHIFT 15 +#define CTL_SBSIZE_SHIFT 12 + +#define CFG_ENABLE BIT(0) +#define CFG_SRC_PERI_SHIFT 1 +#define CFG_DST_PERI_SHIFT 6 +#define CFG_FLOW_SHIFT 11 +#define CFG_IE BIT(14) /* unmask error IRQ */ +#define CFG_ITC BIT(15) /* unmask TC IRQ */ +#define FLOW_M2P 0x1 +#define FLOW_P2M 0x2 +#define CTL_PROT_PRIV BIT(28) +#define CTL_PROT_BUFF BIT(29) +#define CTL_PROT_CACHE BIT(30) + +/* Glass: peri 12 Active+c2 stuck. peri 10 SRC walks. Rockbox IIS0 TX=0xA. */ +static int force_peri = 10; +module_param(force_peri, int, 0644); +MODULE_PARM_DESC(force_peri, "override DT DMA peri id (-1 = use DT)"); +static int force_mem; +module_param(force_mem, int, 0644); +MODULE_PARM_DESC(force_mem, "1 = M2M flow + soft req, dest still FIFO"); +static int force_flow = -1; +module_param(force_flow, int, 0644); +MODULE_PARM_DESC(force_flow, "PL080 FlowCntrl -1=auto M2P, 0=M2M+soft, 1=M2P, 5=M2P-peri"); +/* DDI0196 CxControl bits 24/25: 0=AHB1, 1=AHB2. Kitra memcpy uses AHB1. */ +/* M2P: AHB2→memory, AHB1→APB FIFO (Samsung PL080S topology). */ +static int ahb_s = 1; +module_param(ahb_s, int, 0644); +MODULE_PARM_DESC(ahb_s, "source AHB master (0=AHB1/periph-side, 1=AHB2/mem)"); +static int ahb_d; +module_param(ahb_d, int, 0644); +MODULE_PARM_DESC(ahb_d, "dest AHB master (0=AHB1/periph, 1=AHB2/mem)"); +/* 1=16-bit S16 LE (Rockbox pcm / OSOS BCB60 16-bit). 2=32-bit packed LR. */ +static int xfer_width = 1; +module_param(xfer_width, int, 0644); +MODULE_PARM_DESC(xfer_width, "PL080 src/dst width 0=8 1=16 2=32"); +/* M2P: dest burst 1; src 8 beats (~half 64-entry IIS FIFO). */ +static int m2p_src_burst = 2; /* enc: 2=8 beats */ +module_param(m2p_src_burst, int, 0644); +MODULE_PARM_DESC(m2p_src_burst, "M2P SBSIZE enc (default 2=8 beats)"); +static int m2p_dst_burst; /* 0=1 beat — do not burst into IIS TX FIFO */ +module_param(m2p_dst_burst, int, 0644); +MODULE_PARM_DESC(m2p_dst_burst, "M2P DBSIZE enc (default 0=1 beat)"); +static int force_eng = -1; +module_param(force_eng, int, 0644); +MODULE_PARM_DESC(force_eng, "PL080 engine 0/1 for xlate (-1 = either)"); + +#define PL080_LLI_ALIGN 32 +#define PL080_TERM_POLL_US 10 +#define PL080_TERM_POLL_MAX 10 + +struct pl080_lli { + __le32 src; + __le32 dst; + __le32 lli; + __le32 ctrl; +} __aligned(PL080_LLI_ALIGN); + +static size_t s5l_pl080_lli_size(unsigned int nlli) +{ + return ALIGN(nlli * sizeof(struct pl080_lli), PL080_LLI_ALIGN); +} + +static dma_addr_t s5l_pl080_lli_pa(dma_addr_t base, unsigned int idx) +{ + return base + idx * sizeof(struct pl080_lli); +} + +static struct pl080_lli *s5l_pl080_lli_alloc(struct device *dev, + unsigned int nlli, + dma_addr_t *phys) +{ + size_t bytes = s5l_pl080_lli_size(nlli); + struct pl080_lli *lli; + + lli = dma_alloc_coherent(dev, bytes, phys, GFP_NOWAIT); + if (!lli) + return NULL; + if (*phys & (PL080_LLI_ALIGN - 1)) + dev_warn(dev, "LLI phys misaligned pa=%pad (need %u)\n", + &*phys, PL080_LLI_ALIGN); + return lli; +} +struct s5l_pl080_chan { + struct virt_dma_chan vc; + struct s5l_pl080 *host; + void __iomem *base; + u8 id; + u8 peri; + u8 src_burst; + u8 dst_burst; + enum dma_transfer_direction dir; + dma_addr_t fifo_addr; + struct s5l_pl080_desc *running; +}; + +struct s5l_pl080_desc { + struct virt_dma_desc vd; + struct pl080_lli *lli; + dma_addr_t lli_phys; + unsigned int nlli; + u32 cfg; + bool cyclic; + dma_addr_t buf_addr; + size_t buf_len; +}; + +struct s5l_pl080; + +struct dma_chan *s5l_pl080_request_slave(struct device *consumer, + unsigned int idx); + +struct s5l_pl080 { + struct device *dev; + void __iomem *base[2]; + struct clk *clk[2]; + struct dma_device ddev; + struct s5l_pl080_chan chans[PL080_CH_COUNT * 2]; + spinlock_t lock; + void *dummy_cpu; + dma_addr_t dummy_dma; + struct task_struct *pump; +}; + +static int s5l_pl080_need_soft(void) +{ + /* Flow 0/4: M2M or M2P under DMA control — drive with SOFT_BREQ. */ + return force_mem || force_flow == 0 || force_flow == 4; +} + +static unsigned int s5l_pl080_burst_enc(unsigned int maxburst) +{ + if (maxburst <= 1) + return 0; + if (maxburst <= 4) + return 1; + if (maxburst <= 8) + return 2; + if (maxburst <= 16) + return 3; + if (maxburst <= 32) + return 4; + if (maxburst <= 64) + return 5; + if (maxburst <= 128) + return 6; + return 7; +} + +static int s5l_pl080_pump(void *data) +{ + struct s5l_pl080 *pl = data; + unsigned int i, n; + + while (!kthread_should_stop()) { + if (!s5l_pl080_need_soft()) { + usleep_range(20000, 40000); + continue; + } + n = 0; + for (i = 0; i < PL080_CH_COUNT * 2; i++) { + struct s5l_pl080_chan *ch = &pl->chans[i]; + + if (!ch->base || !ch->running) + continue; + writel(BIT(ch->id % PL080_CH_COUNT), + ch->base + PL080_SOFT_BREQ); + n++; + } + if (!n) + usleep_range(2000, 4000); + else + cond_resched(); + } + return 0; +} + +static struct s5l_pl080_chan *to_s5l_chan(struct dma_chan *c) +{ + return container_of(c, struct s5l_pl080_chan, vc.chan); +} + +static struct s5l_pl080_desc *to_s5l_desc(struct virt_dma_desc *vd) +{ + return container_of(vd, struct s5l_pl080_desc, vd); +} + +static unsigned int s5l_pl080_unit(void) +{ + unsigned int w = xfer_width & 7; + + if (w > 2) + w = 1; + return 1u << w; +} + +static u32 s5l_pl080_build_ctl(struct s5l_pl080_chan *ch, u32 words, + bool src_inc, bool dst_inc, bool irq) +{ + unsigned int w = xfer_width & 7; + unsigned int sb, db; + u32 ctl; + + if (w > 2) + w = 1; + if (ch && (ch->dir == DMA_MEM_TO_DEV || ch->dir == DMA_DEV_TO_MEM)) { + sb = ch->src_burst; + db = ch->dst_burst; + ctl = CTL_PROT_PRIV | CTL_PROT_BUFF; + } else { + /* M2M selftest / memcpy: Rockbox pcm-s5l8702 8/4 */ + sb = 2; + db = 1; + ctl = CTL_PROT_PRIV | CTL_PROT_BUFF | CTL_PROT_CACHE; + } + ctl |= words | (w << CTL_WIDTH_SHIFT) | (w << (CTL_WIDTH_SHIFT + 3)) | + (sb << CTL_SBSIZE_SHIFT) | (db << CTL_DBSIZE_SHIFT); + if (ahb_s) + ctl |= BIT(24); + if (ahb_d) + ctl |= BIT(25); + + if (src_inc) + ctl |= CTL_SRC_AI; + if (dst_inc) + ctl |= CTL_DST_AI; + if (irq) + ctl |= CTL_TC_IRQ; + return ctl; +} + +static void s5l_pl080_chan_disable(struct s5l_pl080_chan *ch) +{ + u8 id = ch->id % PL080_CH_COUNT; + void __iomem *b = ch->base; + unsigned int i; + u32 en; + + if (!b) + return; + writel(0, b + PL080_Cx_CFG(id)); + /* Never spin forever on BUSY — bounded poll then force-clear IRQs. */ + for (i = 0; i < PL080_TERM_POLL_MAX; i++) { + en = readl(b + PL080_ENBLD_CHNS); + if (!(en & BIT(id))) + break; + udelay(PL080_TERM_POLL_US); + } + if (en & BIT(id)) + dev_warn(ch->host->dev, + "ch%u still enabled en=0x%x after disable (no BUSY wait)\n", + ch->id, en); + writel(BIT(id), b + PL080_INT_TC_CLEAR); + writel(BIT(id), b + PL080_INT_ERR_CLEAR); +} + +static void s5l_pl080_sync_buffer(struct s5l_pl080_chan *ch, + struct s5l_pl080_desc *d) +{ + struct device *dev = ch->host->dev; + + if (!d->buf_len || !dev) + return; + if (ch->dir == DMA_MEM_TO_DEV) + dma_sync_single_for_device(dev, d->buf_addr, d->buf_len, + DMA_TO_DEVICE); + else if (ch->dir == DMA_DEV_TO_MEM) + dma_sync_single_for_device(dev, d->buf_addr, d->buf_len, + DMA_FROM_DEVICE); +} + +static void s5l_pl080_start(struct s5l_pl080_chan *ch, struct s5l_pl080_desc *d) +{ + void __iomem *b = ch->base; + u8 id = ch->id % PL080_CH_COUNT; + struct pl080_lli *first = d->lli; + + s5l_pl080_sync_buffer(ch, d); + s5l_pl080_chan_disable(ch); + writel(le32_to_cpu(first->src), b + PL080_Cx_SRC(id)); + writel(le32_to_cpu(first->dst), b + PL080_Cx_DST(id)); + /* Next LLI, not the first (already loaded into SRC/DST/CTL). */ + writel(le32_to_cpu(first->lli), b + PL080_Cx_LLI(id)); + writel(le32_to_cpu(first->ctrl), b + PL080_Cx_CTL(id)); + /* B424C: CONTROL2 = transfer count (v27 & 0x1FFFFFFF), not CTL. */ + writel(le32_to_cpu(first->ctrl) & 0x1fffffffu, + b + PL080S_Cx_CONTROL2(id)); + writel(d->cfg | CFG_ENABLE, b + PL080_Cx_CFG(id)); + /* M2M / force_flow 0|4: software request. M2P peri waits for IIS DRQ. */ + if (s5l_pl080_need_soft()) { + writel(BIT(id), b + PL080_SOFT_BREQ); + if (force_mem || force_flow == 0) + writel(BIT(id), b + PL080_SOFT_SREQ); + } + ch->running = d; + dev_info(ch->host->dev, + "ch%u start peri=%u cfg=0x%x nlli=%u src=0x%x dst=0x%x ctl=0x%x\n", + ch->id, ch->peri, (u32)(d->cfg | CFG_ENABLE), d->nlli, + le32_to_cpu(first->src), le32_to_cpu(first->dst), + le32_to_cpu(first->ctrl)); +} + +static void s5l_pl080_issue(struct dma_chan *c) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + struct virt_dma_desc *vd; + unsigned long flags; + + spin_lock_irqsave(&ch->vc.lock, flags); + if (vchan_issue_pending(&ch->vc) && !ch->running) { + vd = vchan_next_desc(&ch->vc); + if (vd) { + list_del(&vd->node); + s5l_pl080_start(ch, to_s5l_desc(vd)); + } + } + spin_unlock_irqrestore(&ch->vc.lock, flags); +} + +static enum dma_status s5l_pl080_tx_status(struct dma_chan *c, + dma_cookie_t cookie, + struct dma_tx_state *state) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + enum dma_status st = dma_cookie_status(c, cookie, state); + struct s5l_pl080_desc *d; + unsigned long flags; + u32 cur, start, end; + + if (!state || st == DMA_COMPLETE) + return st; + + spin_lock_irqsave(&ch->vc.lock, flags); + d = ch->running; + if (d && d->buf_len) { + u8 id = ch->id % PL080_CH_COUNT; + + cur = readl(ch->base + ((ch->dir == DMA_DEV_TO_MEM) ? + PL080_Cx_DST(id) : PL080_Cx_SRC(id))); + start = lower_32_bits(d->buf_addr); + end = start + d->buf_len; + if (cur >= start && cur < end) + state->residue = end - cur; + else + state->residue = d->buf_len; + } + spin_unlock_irqrestore(&ch->vc.lock, flags); + return st; +} + +static int s5l_pl080_alloc(struct dma_chan *c) +{ + return 0; +} + +static void s5l_pl080_free(struct dma_chan *c) +{ + vchan_free_chan_resources(&to_s5l_chan(c)->vc); +} + +static void s5l_pl080_desc_free(struct virt_dma_desc *vd) +{ + struct s5l_pl080_desc *d = to_s5l_desc(vd); + struct s5l_pl080_chan *ch = to_s5l_chan(vd->tx.chan); + + if (d->lli && !irqs_disabled() && !in_atomic()) + dma_free_coherent(ch->host->dev, s5l_pl080_lli_size(d->nlli), + d->lli, d->lli_phys); + kfree(d); +} + +static struct dma_async_tx_descriptor * +s5l_pl080_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl, + unsigned int sg_len, + enum dma_transfer_direction dir, + unsigned long flags, void *context) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + struct s5l_pl080_desc *d; + struct scatterlist *sg; + struct pl080_lli *lli; + dma_addr_t lli_phys, dev_addr; + unsigned int i, nlli = 0, total = 0; + u32 cfg; + + for_each_sg(sgl, sg, sg_len, i) + total += sg_dma_len(sg); + if (!total) + return NULL; + if (sg_len > 1) + dev_info(ch->host->dev, "prep_slave_sg sg_len=%u (LLI chain)\n", + sg_len); + + /* One LLI node per <= PL080_MAX_XFER_WORDS transfer units */ + nlli = DIV_ROUND_UP(total, PL080_MAX_XFER_WORDS * s5l_pl080_unit()); + if (nlli == 0) + return NULL; + + d = kzalloc(sizeof(*d), GFP_NOWAIT); + if (!d) + return NULL; + + lli = s5l_pl080_lli_alloc(ch->host->dev, nlli, &lli_phys); + if (!lli) { + kfree(d); + return NULL; + } + + cfg = 0; + dev_addr = ch->fifo_addr; + if (force_flow >= 0) { + cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_IE | CFG_ITC; + if (dir == DMA_MEM_TO_DEV) + cfg |= (ch->peri & 0x1f) << CFG_DST_PERI_SHIFT; + else + cfg |= (ch->peri & 0x1f) << CFG_SRC_PERI_SHIFT; + } else if (force_mem) { + cfg |= CFG_IE | CFG_ITC; + } else if (dir == DMA_MEM_TO_DEV) { + cfg |= (FLOW_M2P << CFG_FLOW_SHIFT) | + ((ch->peri & 0x1f) << CFG_DST_PERI_SHIFT) | + CFG_IE | CFG_ITC; + } else { + cfg |= (FLOW_P2M << CFG_FLOW_SHIFT) | + ((ch->peri & 0x1f) << CFG_SRC_PERI_SHIFT) | + CFG_IE | CFG_ITC; + } + + { + unsigned int idx = 0; + size_t remaining = total; + + for_each_sg(sgl, sg, sg_len, i) { + dma_addr_t addr = sg_dma_address(sg); + size_t sg_left = sg_dma_len(sg); + size_t sg_off = 0; + + while (sg_left && idx < nlli) { + size_t unit = s5l_pl080_unit(); + size_t chunk = min_t(size_t, sg_left, + PL080_MAX_XFER_WORDS * unit); + u32 words = chunk / unit; + + if (!words) + words = 1; + chunk = words * unit; + + if (dir == DMA_MEM_TO_DEV) { + lli[idx].src = cpu_to_le32( + lower_32_bits(addr + sg_off)); + lli[idx].dst = cpu_to_le32( + lower_32_bits(dev_addr)); + } else { + lli[idx].src = cpu_to_le32( + lower_32_bits(dev_addr)); + lli[idx].dst = cpu_to_le32( + lower_32_bits(addr + sg_off)); + } + + lli[idx].ctrl = cpu_to_le32(s5l_pl080_build_ctl( + ch, words, + dir == DMA_MEM_TO_DEV, + dir == DMA_DEV_TO_MEM, + idx == nlli - 1)); + + if (idx < nlli - 1) + lli[idx].lli = cpu_to_le32( + lower_32_bits(s5l_pl080_lli_pa( + lli_phys, idx + 1))); + else + lli[idx].lli = cpu_to_le32(0); + + sg_off += chunk; + sg_left -= chunk; + remaining -= chunk; + idx++; + } + } + } + + d->lli = lli; + d->lli_phys = lli_phys; + d->nlli = nlli; + d->cfg = cfg; + d->cyclic = false; + d->buf_addr = sg_dma_address(sgl); + d->buf_len = total; + return vchan_tx_prep(&ch->vc, &d->vd, flags); +} + +static struct dma_async_tx_descriptor * +s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, + size_t buf_len, size_t period_len, + enum dma_transfer_direction dir, + unsigned long flags) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + struct s5l_pl080_desc *d; + struct pl080_lli *lli; + dma_addr_t lli_phys, dev_addr; + unsigned int periods, per_period, nlli, p, idx; + u32 cfg; + + if (!buf_len || !period_len || buf_len % period_len) { + dev_info(ch->host->dev, + "cyclic reject len=%zu period=%zu\n", buf_len, period_len); + return NULL; + } + + periods = buf_len / period_len; + per_period = DIV_ROUND_UP(period_len, PL080_MAX_XFER_WORDS * s5l_pl080_unit()); + if (!per_period) + return NULL; + nlli = periods * per_period; + + d = kzalloc(sizeof(*d), GFP_NOWAIT); + if (!d) + return NULL; + + lli = s5l_pl080_lli_alloc(ch->host->dev, nlli, &lli_phys); + if (!lli) { + kfree(d); + return NULL; + } + + cfg = 0; + dev_addr = ch->fifo_addr; + if (force_flow >= 0) { + cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_IE | CFG_ITC; + if (dir == DMA_MEM_TO_DEV) + cfg |= (ch->peri & 0x1f) << CFG_DST_PERI_SHIFT; + else + cfg |= (ch->peri & 0x1f) << CFG_SRC_PERI_SHIFT; + } else if (force_mem) { + cfg |= CFG_IE | CFG_ITC; + } else if (dir == DMA_MEM_TO_DEV) { + cfg |= (FLOW_M2P << CFG_FLOW_SHIFT) | + ((ch->peri & 0x1f) << CFG_DST_PERI_SHIFT) | + CFG_IE | CFG_ITC; + } else { + cfg |= (FLOW_P2M << CFG_FLOW_SHIFT) | + ((ch->peri & 0x1f) << CFG_SRC_PERI_SHIFT) | + CFG_IE | CFG_ITC; + } + + idx = 0; + for (p = 0; p < periods; p++) { + size_t sg_off = 0; + size_t sg_left = period_len; + unsigned int chunk_i; + + for (chunk_i = 0; chunk_i < per_period && sg_left; chunk_i++) { + size_t unit = s5l_pl080_unit(); + size_t chunk = min_t(size_t, sg_left, + PL080_MAX_XFER_WORDS * unit); + u32 words = chunk / unit; + bool period_last; + + if (!words) + words = 1; + chunk = words * unit; + period_last = (chunk_i == per_period - 1) || + (sg_left <= chunk); + + if (dir == DMA_MEM_TO_DEV) { + lli[idx].src = cpu_to_le32(lower_32_bits( + buf_addr + p * period_len + sg_off)); + lli[idx].dst = cpu_to_le32( + lower_32_bits(dev_addr)); + } else { + lli[idx].src = cpu_to_le32( + lower_32_bits(dev_addr)); + lli[idx].dst = cpu_to_le32(lower_32_bits( + buf_addr + p * period_len + sg_off)); + } + + lli[idx].ctrl = cpu_to_le32(s5l_pl080_build_ctl( + ch, words, + dir == DMA_MEM_TO_DEV, + dir == DMA_DEV_TO_MEM, + period_last)); + + if (idx + 1 < nlli) + lli[idx].lli = cpu_to_le32(lower_32_bits( + s5l_pl080_lli_pa(lli_phys, idx + 1))); + else + lli[idx].lli = cpu_to_le32( + lower_32_bits(lli_phys)); + + sg_off += chunk; + sg_left -= chunk; + idx++; + } + } + if (idx) + lli[idx - 1].lli = cpu_to_le32(lower_32_bits(lli_phys)); + + d->lli = lli; + d->lli_phys = lli_phys; + d->nlli = nlli; + d->cfg = cfg; + d->cyclic = true; + d->buf_addr = buf_addr; + d->buf_len = buf_len; + dev_info(ch->host->dev, + "cyclic ok peri=%u nlli=%u periods=%u period=%zu fifo=0x%x\n", + ch->peri, nlli, periods, period_len, + (u32)lower_32_bits(dev_addr)); + return vchan_tx_prep(&ch->vc, &d->vd, flags); +} + +static int s5l_pl080_config(struct dma_chan *c, + struct dma_slave_config *cfg) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + + if (cfg->direction == DMA_MEM_TO_DEV) { + ch->fifo_addr = cfg->dst_addr; + ch->src_burst = cfg->src_maxburst ? + s5l_pl080_burst_enc(cfg->src_maxburst) : + clamp(m2p_src_burst, 0, 7); + ch->dst_burst = cfg->dst_maxburst ? + s5l_pl080_burst_enc(cfg->dst_maxburst) : + clamp(m2p_dst_burst, 0, 7); + } else { + ch->fifo_addr = cfg->src_addr; + ch->src_burst = cfg->src_maxburst ? + s5l_pl080_burst_enc(cfg->src_maxburst) : 0; + ch->dst_burst = cfg->dst_maxburst ? + s5l_pl080_burst_enc(cfg->dst_maxburst) : 1; + } + ch->dir = cfg->direction; + return 0; +} + +static int s5l_pl080_terminate(struct dma_chan *c) +{ + struct s5l_pl080_chan *ch = to_s5l_chan(c); + u8 id = ch->id % PL080_CH_COUNT; + unsigned long flags; + struct virt_dma_desc *vd; + + dev_info(ch->host->dev, + "term ch%u en=0x%x src=0x%x dst=0x%x lli=0x%x ctl=0x%x cfg=0x%x rawtc=0x%x rawerr=0x%x\n", + ch->id, readl(ch->base + PL080_ENBLD_CHNS), + readl(ch->base + PL080_Cx_SRC(id)), + readl(ch->base + PL080_Cx_DST(id)), + readl(ch->base + PL080_Cx_LLI(id)), + readl(ch->base + PL080_Cx_CTL(id)), + readl(ch->base + PL080_Cx_CFG(id)), + readl(ch->base + PL080_RAW_TC), + readl(ch->base + PL080_RAW_ERR)); + s5l_pl080_chan_disable(ch); + spin_lock_irqsave(&ch->vc.lock, flags); + if (ch->running) { + s5l_pl080_desc_free(&ch->running->vd); + ch->running = NULL; + } + while (!list_empty(&ch->vc.desc_submitted)) { + vd = list_first_entry(&ch->vc.desc_submitted, + struct virt_dma_desc, node); + list_del(&vd->node); + s5l_pl080_desc_free(vd); + } + spin_unlock_irqrestore(&ch->vc.lock, flags); + return 0; +} + +static irqreturn_t s5l_pl080_irq(int irq, void *data) +{ + struct s5l_pl080 *pl = data; + unsigned eng, i; + u32 tc, err; + + for (eng = 0; eng < 2; eng++) { + void __iomem *b = pl->base[eng]; + + if (!b) + continue; + tc = readl(b + PL080_INT_TC_STATUS); + err = readl(b + PL080_INT_ERR_STATUS); + if (tc || err) + dev_info_ratelimited(pl->dev, + "irq eng%u tc=0x%x err=0x%x\n", + eng, tc, err); + if (tc) + writel(tc, b + PL080_INT_TC_CLEAR); + if (err) + writel(err, b + PL080_INT_ERR_CLEAR); + for (i = 0; i < PL080_CH_COUNT; i++) { + if (!(tc & BIT(i))) + continue; + { + struct s5l_pl080_chan *ch = + &pl->chans[eng * PL080_CH_COUNT + i]; + struct s5l_pl080_desc *d; + unsigned long flags; + + spin_lock_irqsave(&ch->vc.lock, flags); + d = ch->running; + if (d && d->cyclic) { + vchan_cyclic_callback(&d->vd); + spin_unlock_irqrestore(&ch->vc.lock, + flags); + continue; + } + ch->running = NULL; + if (d) + vchan_cookie_complete(&d->vd); + { + struct virt_dma_desc *vd = + vchan_next_desc(&ch->vc); + if (vd) { + list_del(&vd->node); + s5l_pl080_start(ch, + to_s5l_desc(vd)); + } + } + spin_unlock_irqrestore(&ch->vc.lock, flags); + } + } + } + return IRQ_HANDLED; +} + +static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, + struct of_phandle_args *spec) +{ + struct s5l_pl080_chan *ch; + unsigned i, peri, eng_lo, eng_hi; + + if (!pl || !spec || spec->args_count < 1) + return NULL; + peri = spec->args[0] & 0x1f; + if (force_peri >= 0) + peri = force_peri & 0x1f; + if (force_eng >= 0) { + eng_lo = force_eng ? PL080_CH_COUNT : 0; + eng_hi = eng_lo + PL080_CH_COUNT; + } else { + eng_lo = 0; + eng_hi = PL080_CH_COUNT * 2; + } + for (i = eng_lo; i < eng_hi; i++) { + ch = &pl->chans[i]; + if (!ch->base || ch->vc.chan.client_count) + continue; + ch->peri = peri; + ch->src_burst = clamp(m2p_src_burst, 0, 7); + ch->dst_burst = clamp(m2p_dst_burst, 0, 7); + dev_info(pl->dev, "xlate DT peri=%u -> ch%u (eng%u) peri=%u\n", + spec->args[0] & 0x1f, i, i / PL080_CH_COUNT, ch->peri); + return dma_get_slave_channel(&ch->vc.chan); + } + return NULL; +} + +static struct dma_chan *s5l_pl080_xlate(struct of_phandle_args *spec, + struct of_dma *ofdma) +{ + return s5l_pl080_xlate_args(ofdma->of_dma_data, spec); +} + +/* + * Request PL080 slave channel from consumer DT dmas[] without creating + * the consumer-side dma:tx sysfs symlink (avoids sysfs_warn_dup spam). + */ +struct dma_chan *s5l_pl080_request_slave(struct device *consumer, + unsigned int idx) +{ + struct of_phandle_args spec; + struct platform_device *pdev; + struct s5l_pl080 *pl; + struct dma_chan *chan; + + if (!consumer || !consumer->of_node) + return ERR_PTR(-ENODEV); + if (of_parse_phandle_with_args(consumer->of_node, "dmas", "#dma-cells", + idx, &spec)) + return ERR_PTR(-ENODEV); + pdev = of_find_device_by_node(spec.np); + if (!pdev) + return ERR_PTR(-EPROBE_DEFER); + pl = platform_get_drvdata(pdev); + if (!pl) { + put_device(&pdev->dev); + return ERR_PTR(-EPROBE_DEFER); + } + chan = s5l_pl080_xlate_args(pl, &spec); + put_device(&pdev->dev); + if (!chan) + return ERR_PTR(-EBUSY); + return chan; +} +EXPORT_SYMBOL_GPL(s5l_pl080_request_slave); + +static ssize_t chregs_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct s5l_pl080 *pl = dev_get_drvdata(dev); + int n = 0, eng, i; + + if (!pl) + return -ENODEV; + for (eng = 0; eng < 2; eng++) { + void __iomem *b = pl->base[eng]; + + if (!b) + continue; + n += scnprintf(buf + n, PAGE_SIZE - n, + "eng%u en=0x%x rawtc=0x%x rawerr=0x%x cfg=0x%x\n", + eng, readl(b + PL080_ENBLD_CHNS), + readl(b + PL080_RAW_TC), + readl(b + PL080_RAW_ERR), + readl(b + PL080_CONFIG)); + for (i = 0; i < PL080_CH_COUNT; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, + " e%u ch%u src=%08x dst=%08x lli=%08x ctl=%08x cfg=%08x c2=%08x\n", + eng, i, + readl(b + PL080_Cx_SRC(i)), + readl(b + PL080_Cx_DST(i)), + readl(b + PL080_Cx_LLI(i)), + readl(b + PL080_Cx_CTL(i)), + readl(b + PL080_Cx_CFG(i)), + readl(b + PL080S_Cx_CONTROL2(i))); + } + return n; +} +static DEVICE_ATTR_RO(chregs); + +static int s5l_pl080_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l_pl080 *pl; + struct resource *res; + int irq, i, ret; + u32 id0; + + pl = devm_kzalloc(dev, sizeof(*pl), GFP_KERNEL); + if (!pl) + return -ENOMEM; + pl->dev = dev; + spin_lock_init(&pl->lock); + + for (i = 0; i < 2; i++) { + res = platform_get_resource(pdev, IORESOURCE_MEM, i); + if (!res) + continue; + pl->base[i] = devm_ioremap_resource(dev, res); + if (IS_ERR(pl->base[i])) + return PTR_ERR(pl->base[i]); + } + if (!pl->base[0]) + return -EINVAL; + + pl->clk[0] = devm_clk_get_optional(dev, "dmac0"); + pl->clk[1] = devm_clk_get_optional(dev, "dmac1"); + if (!IS_ERR_OR_NULL(pl->clk[0])) + clk_prepare_enable(pl->clk[0]); + if (!IS_ERR_OR_NULL(pl->clk[1])) + clk_prepare_enable(pl->clk[1]); + + id0 = readl(pl->base[0] + 0xfe0) & 0xff; + writel(PL080_CONFIG_EN, pl->base[0] + PL080_CONFIG); + writel(~0u, pl->base[0] + PL080_SYNC); + if (pl->base[1]) { + writel(PL080_CONFIG_EN, pl->base[1] + PL080_CONFIG); + writel(~0u, pl->base[1] + PL080_SYNC); + } + + /* DDI0196 M2M: try AHB1/AHB2 × 16/32-bit. dst0==pattern means the engine copies. */ + { + dma_addr_t sa, da; + u32 *s, *d; + int eng, as, ad, wid; + + s = dmam_alloc_coherent(dev, 64, &sa, GFP_KERNEL); + d = dmam_alloc_coherent(dev, 64, &da, GFP_KERNEL); + if (s && d) { + s[0] = 0xa5a5a5a5; + s[1] = 0x5a5a5a5a; + for (eng = 0; eng < 2; eng++) { + void __iomem *b = pl->base[eng]; + + if (!b) + continue; + dev_info(dev, "selftest eng%u id=%02x\n", + eng, readl(b + 0xfe0) & 0xff); + for (wid = 1; wid <= 2; wid++) { + for (as = 0; as <= 1; as++) { + for (ad = 0; ad <= 1; ad++) { + u32 ctl, words = (wid == 2) ? 4 : 8; + + d[0] = 0; + writel(0, b + PL080_Cx_CFG(0)); + writel(lower_32_bits(sa), + b + PL080_Cx_SRC(0)); + writel(lower_32_bits(da), + b + PL080_Cx_DST(0)); + writel(0, b + PL080_Cx_LLI(0)); + ctl = words | + (wid << CTL_WIDTH_SHIFT) | + (wid << (CTL_WIDTH_SHIFT + 3)) | + (2 << CTL_SBSIZE_SHIFT) | + (2 << CTL_DBSIZE_SHIFT) | + CTL_PROT_PRIV | + CTL_PROT_BUFF | + CTL_PROT_CACHE | + CTL_SRC_AI | CTL_DST_AI | + CTL_TC_IRQ; + if (as) + ctl |= BIT(24); + if (ad) + ctl |= BIT(25); + writel(ctl, b + PL080_Cx_CTL(0)); + writel(words, b + PL080S_Cx_CONTROL2(0)); + writel(CFG_ENABLE | CFG_IE | CFG_ITC, + b + PL080_Cx_CFG(0)); + writel(BIT(0), b + PL080_SOFT_BREQ); + writel(BIT(0), b + PL080_SOFT_SREQ); + udelay(50); + dev_info(dev, + "selftest e%u w%u s%d d%d tc=%x err=%x dst=%08x\n", + eng, wid, as, ad, + readl(b + PL080_RAW_TC), + readl(b + PL080_RAW_ERR), + d[0]); + writel(0, b + PL080_Cx_CFG(0)); + writel(~0u, b + PL080_INT_TC_CLEAR); + writel(~0u, b + PL080_INT_ERR_CLEAR); + } + } + } + } + } + } + + dma_cap_zero(pl->ddev.cap_mask); + dma_cap_set(DMA_SLAVE, pl->ddev.cap_mask); + dma_cap_set(DMA_CYCLIC, pl->ddev.cap_mask); + dma_cap_set(DMA_PRIVATE, pl->ddev.cap_mask); + pl->ddev.dev = dev; + pl->ddev.device_alloc_chan_resources = s5l_pl080_alloc; + pl->ddev.device_free_chan_resources = s5l_pl080_free; + pl->ddev.device_tx_status = s5l_pl080_tx_status; + pl->ddev.device_issue_pending = s5l_pl080_issue; + pl->ddev.device_prep_slave_sg = s5l_pl080_prep_slave_sg; + pl->ddev.device_prep_dma_cyclic = s5l_pl080_prep_dma_cyclic; + pl->ddev.device_config = s5l_pl080_config; + pl->ddev.device_terminate_all = s5l_pl080_terminate; + pl->ddev.src_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) | + BIT(DMA_SLAVE_BUSWIDTH_4_BYTES); + pl->ddev.dst_addr_widths = BIT(DMA_SLAVE_BUSWIDTH_2_BYTES) | + BIT(DMA_SLAVE_BUSWIDTH_4_BYTES); + pl->ddev.directions = BIT(DMA_MEM_TO_DEV) | BIT(DMA_DEV_TO_MEM); + pl->ddev.residue_granularity = DMA_RESIDUE_GRANULARITY_DESCRIPTOR; + INIT_LIST_HEAD(&pl->ddev.channels); + + for (i = 0; i < PL080_CH_COUNT * 2; i++) { + struct s5l_pl080_chan *ch = &pl->chans[i]; + + ch->host = pl; + ch->id = i; + ch->base = pl->base[i / PL080_CH_COUNT]; + if (!ch->base) + continue; + ch->vc.desc_free = s5l_pl080_desc_free; + vchan_init(&ch->vc, &pl->ddev); + } + + for (i = 0; i < 2; i++) { + irq = platform_get_irq_optional(pdev, i); + if (irq <= 0) + continue; + ret = devm_request_irq(dev, irq, s5l_pl080_irq, 0, + "s5l8740-pl080", pl); + if (ret) + dev_warn(dev, "IRQ %d: %d (poll mode)\n", irq, ret); + } + + ret = dma_async_device_register(&pl->ddev); + if (ret) + return ret; + + ret = of_dma_controller_register(dev->of_node, s5l_pl080_xlate, pl); + if (ret) + dev_warn(dev, "of_dma_controller_register: %d\n", ret); + + pl->dummy_cpu = dmam_alloc_coherent(dev, 4096, &pl->dummy_dma, + GFP_KERNEL); + if (!pl->dummy_cpu) + dev_warn(dev, "dummy DMA sink alloc failed\n"); + + pl->pump = kthread_run(s5l_pl080_pump, pl, "n31-pl080-pump"); + if (IS_ERR(pl->pump)) { + dev_warn(dev, "soft-req pump: %ld\n", PTR_ERR(pl->pump)); + pl->pump = NULL; + } + + platform_set_drvdata(pdev, pl); + ret = device_create_file(dev, &dev_attr_chregs); + if (ret) + dev_warn(dev, "chregs sysfs: %d\n", ret); + dev_info(dev, + "PL080 dmaengine @%pR id=%02x peri IIS0=10/11 (glass) OSOS=12/13\n", + platform_get_resource(pdev, IORESOURCE_MEM, 0), id0); + return 0; +} + +static void s5l_pl080_remove(struct platform_device *pdev) +{ + struct s5l_pl080 *pl = platform_get_drvdata(pdev); + + if (pl->pump) + kthread_stop(pl->pump); + device_remove_file(&pdev->dev, &dev_attr_chregs); + of_dma_controller_free(pdev->dev.of_node); + dma_async_device_unregister(&pl->ddev); +} + +static const struct of_device_id s5l_pl080_of_match[] = { + { .compatible = "apple,s5l8740-pl080" }, + { .compatible = "arm,pl080" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l_pl080_of_match); + +static struct platform_driver s5l_pl080_driver = { + .probe = s5l_pl080_probe, + .remove = s5l_pl080_remove, + .driver = { + .name = "s5l8740-pl080", + .of_match_table = s5l_pl080_of_match, + }, +}; +module_platform_driver(s5l_pl080_driver); + +MODULE_DESCRIPTION("S5L8740 PL080 dmaengine (N31)"); +MODULE_LICENSE("GPL"); diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig index b2a230c6d59f79..35dc7f47dc11c7 100644 --- a/drivers/gpio/Kconfig +++ b/drivers/gpio/Kconfig @@ -1333,16 +1333,7 @@ config GPIO_CS5535 If unsure, say N. -config GPIO_S5L8740 - bool "Samsung/Apple S5L8740 banked GPIO" - depends on OF && GPIOLIB - select GPIO_GENERIC - help - Banked GPIO at 0x3CF00000 plus the GPIOCMD latch at +0x1E0. - Used on iPod nano 7G for Vol± and as the parent for EIC - to_irq. Do not use gpio-keys-polled on the Vol pads; that - path issues GPIOCMD 0xFFFE and the DIN lines stop moving. - +config GPIO_D1830 tristate "Dialog Semiconductor D1830 PMIC GPIO (read-only, I2C)" depends on I2C depends on GPIOLIB @@ -1354,6 +1345,16 @@ config GPIO_S5L8740 together with gpio-keys-polled for physical buttons wired through the PMIC. +config GPIO_S5L8740 + tristate "Samsung/Apple S5L8740 SoC GPIO" + depends on ARCH_S5L87XX || COMPILE_TEST + depends on GPIOLIB + depends on OF + help + Banked GPIO controller at 0x3CF00000 for S5L8740 (iPod nano 7G). + DIN/DOUT only; PCON/pinmux is left to SEC/boot or a future + pinctrl driver (direction callbacks are soft no-ops). + config GPIO_DA9052 tristate "Dialog DA9052 GPIO" depends on PMIC_DA9052 diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile index 5e4c5013566ad5..80a7a18808c986 100644 --- a/drivers/gpio/Makefile +++ b/drivers/gpio/Makefile @@ -51,8 +51,8 @@ obj-$(CONFIG_GPIO_SNPS_CREG) += gpio-creg-snps.o obj-$(CONFIG_GPIO_CROS_EC) += gpio-cros-ec.o obj-$(CONFIG_GPIO_CRYSTAL_COVE) += gpio-crystalcove.o obj-$(CONFIG_GPIO_CS5535) += gpio-cs5535.o -obj-$(CONFIG_GPIO_S5L8740) += gpio-s5l8740.o obj-$(CONFIG_GPIO_D1830) += gpio-d1830.o +obj-$(CONFIG_GPIO_S5L8740) += gpio-s5l8740.o obj-$(CONFIG_GPIO_DA9052) += gpio-da9052.o obj-$(CONFIG_GPIO_DA9055) += gpio-da9055.o obj-$(CONFIG_GPIO_DAVINCI) += gpio-davinci.o diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c old mode 100644 new mode 100755 index ce5c85d60c8ecc..a078bffa76c750 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -473,8 +473,9 @@ static void d1830_trace_work(struct work_struct *work) /* * OSOS 3477C + 347E4 + 2C778(3, 5). Channel 3 is VBAT. 10-bit sample - * averaged 5×. Scale: 10-bit × 6 mV (6 V FS). 439A98 then >>2; that - * is logged, not used for µV. No writes to 87/88 (158C82 bitfields). + * averaged 5×. Scale is 10-bit * 6 V FS / 1023 (emcore / OSOS). + * 439A98 then >>2 — logged only. RetailOS UI cache at 0x891DB18 + * is still unmapped. No writes to 87/88. Never write reg 13 here. */ static int d1830_adc_once(struct d1830_gpio *gpio_dev, int *adc, u8 *r48, u8 *r49, u8 *r50) @@ -738,6 +739,27 @@ static int d1830_rmw(struct i2c_client *client, u8 reg, u8 clear, u8 set) return i2c_smbus_write_byte_data(client, reg, (u8)((v & ~clear) | set)); } +/* + * OSOS sub_20766(1) → 439B00(1) → 6644(4) → 7484(pmic, 9, on): + * RMW D1830 register 16 bit 5. Targeted Nimbus rail — not the SEC seq. + */ +int d1830_nimbus_rail(bool on) +{ + struct i2c_client *client = d1830_poweroff_client; + int before, ret; + + if (!client) + return -ENODEV; + before = i2c_smbus_read_byte_data(client, 16); + if (before < 0) + return before; + ret = d1830_rmw(client, 16, BIT(5), on ? BIT(5) : 0); + dev_info(&client->dev, "nimbus rail reg16 0x%02x -> bit5=%d ret=%d\n", + before, on, ret); + return ret; +} +EXPORT_SYMBOL_GPL(d1830_nimbus_rail); + /* * IpodSec PMIC rail / charge bring-up: * sub_23EC — regs 20–23,26,16,17,19,35 (charge/rail-ish) diff --git a/drivers/gpu/drm/tiny/s5l8740.c b/drivers/gpu/drm/tiny/s5l8740.c index f2c6256c2797b2..e0e470b987f246 100644 --- a/drivers/gpu/drm/tiny/s5l8740.c +++ b/drivers/gpu/drm/tiny/s5l8740.c @@ -39,6 +39,7 @@ #define S5L8740_LCD_STATUS_BUSY 0x10 +/* GATE0: 1us was too short (stride/FIFO); 100ms wait, pitch-aware blit */ #define S5L8740_LCD_TIMEOUT_US 100000 #define WIDTH 240 @@ -253,11 +254,26 @@ static int s5l8740_probe(struct platform_device *pdev) if (IS_ERR(sdev->lcdif)) return PTR_ERR(sdev->lcdif); - /* U-Boot already programmed CON/PHTIME. Do not rewrite them. */ + /* GATE0: log WTF handoff, never rewrite CON/PHTIME */ drm_info(dev, "LCDIF handoff CON=%08x PHTIME=%08x (untouched)\n", readl(sdev->lcdif + S5L8740_LCD_CON), readl(sdev->lcdif + S5L8740_LCD_PHTIME)); + /* CON first (stage0). Print so glass shows whether WDT is still live. */ + { + void __iomem *wdt = ioremap(0x3c800000, 8); + + if (wdt) { + writel(0, wdt); + writel(0, wdt + 4); + writel(0, wdt); + writel(0, wdt + 4); + drm_info(dev, "WDT CON=%08x CNT=%08x (disarmed)\n", + readl(wdt), readl(wdt + 4)); + iounmap(wdt); + } + } + /* * Modesetting */ diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig index 1a03de7fcfa66c..17e8df0fcb6b90 100644 --- a/drivers/input/touchscreen/Kconfig +++ b/drivers/input/touchscreen/Kconfig @@ -1391,3 +1391,7 @@ config TOUCHSCREEN_HIMAX_HX83112B module will be called himax_hx83112b. endif + +config TOUCHSCREEN_APPLE_NIMBUS + tristate "Apple Nimbus (iPod nano 7G) multitouch" + depends on SPI diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile index 82bc837ca01e2e..75ca270d9a7964 100644 --- a/drivers/input/touchscreen/Makefile +++ b/drivers/input/touchscreen/Makefile @@ -117,3 +117,4 @@ obj-$(CONFIG_TOUCHSCREEN_IQS5XX) += iqs5xx.o obj-$(CONFIG_TOUCHSCREEN_IQS7211) += iqs7211.o obj-$(CONFIG_TOUCHSCREEN_ZINITIX) += zinitix.o obj-$(CONFIG_TOUCHSCREEN_HIMAX_HX83112B) += himax_hx83112b.o +obj-$(CONFIG_TOUCHSCREEN_APPLE_NIMBUS) += apple-nimbus.o diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c new file mode 100755 index 00000000000000..1f92893c4c66b6 --- /dev/null +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -0,0 +1,2130 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Apple Nimbus / Grape multitouch — N31 SPI2 @ 0x3D200000 + * + * RetailOS 1.0.2 sequences (osos extracts): + * bring-up sub_1A5AC / 2075A / 20766 / 20690 / 11B70 + * teardown sub_1A878: IRQ off, RST, 20690(0), rail off, EN mode 1 + * (13A20 retries: 1A878 + sleep 50 + 1A5AC, max 3) + * grape.bin IS the app. SEC bootloader has no grape/Nimbus path. + * bootload cmd sub_20848(6593) = 19 C1 + (18 E1)* + * FW load 1A640 204E0: ARM at +0x400, size le32(+0x0c) + * rev 3: 422FFA GID-CBC IV=0 decrypt in place + * 273A0: 2D640(ARM) → 2D7A4(IsyS/cal +350 @ 0x400200) → + * 2D5B0 (34AD0 + poke 0x011F RequestCal) → 2D54C + * 2D7A4 window is this unit's NVRAM cal. sub_564 copies + * 1376B IsyS from A34(0x18)=0x2202FE18 into BSS; 43CFB4 + * returns that object. DFU Linux never ran sub_564; the + * 0x22xxxxxx window is Grape-internal. Use grape.bin +350. + * chunk pack via 35C1C→3B9D0 (18 E1 / 30 01 / …) + * status poll sub_3D5706: TX 1A A1 → rev16 status + * ping sub_182590 type 490 + * read sub_17E404 EA 01 01 + * report sub_187AB4 type 0x44 → MT-B + * 1703E8 10 failed 188FFC → 13A20(0) + 13A20(1) + * + * Firmware: request_firmware("apple/grape-nimbus.bin") — optional; without + * it we still bootload+ping (chip may already be programmed). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NIMBUS_MAGIC 0xEA +#define NIMBUS_PING_TYPE 490 +#define NIMBUS_BOOTLOAD_WORD 6593 /* 0x19C1 */ +#define NIMBUS_FRAME_LEN 16 +#define NIMBUS_READ_MAX 512 +#define NIMBUS_SLOTS 8 +#define NIMBUS_ABS_X_MAX 239 +#define NIMBUS_ABS_Y_MAX 431 +#define NIMBUS_SCALE_X_DIV 0x0B1D +#define NIMBUS_SCALE_Y_DIV 0x1482 + +#define NIMBUS_CHUNK_MAX 0x1FF0 /* 8176 — sub_2D640 */ +#define NIMBUS_HDR_LEN 16 +#define NIMBUS_FW_HDR_OFF 350 +#define NIMBUS_FW_HDR_LEN 0x200 +#define NIMBUS_ARM_OFFICIAL 0xe970 /* 8740 le32(+0x0c); 204E0 2D640 size */ +#define NIMBUS_ISYS_MAGIC 0x53797349u /* 'IsyS' — sub_564 */ +#define NIMBUS_ISYS_LEN 0x560 +#define NIMBUS_A34_BASE 0x2202fe00UL /* sub_A34(idx) = 0x2202FE00+idx */ + +#define NIMBUS_ACK_CHUNK 0x4BC1 /* 19393 */ +#define NIMBUS_ACK_34AD0 0x4AD1 /* 19153 */ +#define NIMBUS_POST_POKE 0x011F /* 287 */ + +#define NIMBUS_GPIO_EN 0x0E +#define NIMBUS_GPIO_RST 0x27 +#define NIMBUS_GPIO_IRQ 0x26 + +#define S5L8740_GPIO_PHYS 0x3cf00000UL +#define S5L8740_GPIOCMD_PHYS 0x3cf001e0UL +#define S5L8740_SPI2_PHYS 0x3d200000UL +#define SPI2_CTRL 0x00 +#define SPI2_SETUP 0x04 +#define SPI2_STATUS 0x08 +#define SPI2_PIN 0x0c +#define SPI2_TXDATA 0x10 +#define SPI2_RXDATA 0x20 +#define SPI2_CLKDIV 0x30 +#define SPI2_RXLIMIT 0x34 +#define SPI2_UNK4C 0x4c +#define SPI2_CTRL_FIFO_RST 0x0c +#define SPI2_CTRL_ENABLE 0x01 +#define SPI2_SETUP_11B70 0x403e /* 11B70(2, 0x1A, 0x2EE0, 1) */ +#define SPI2_CS_BIT BIT(1) + +#define NIMBUS_Z2_HDR_LEN 16 +#define NIMBUS_Z2_MAGIC_5A5A 0x5a5a0000u +#define NIMBUS_Z2_MAGIC_C3F5 0xc3f50000u +#define NIMBUS_Z2FW_MAGIC 0x5746325au /* apple_z2 "Z2FW" container */ + +#define NIMBUS_CS_BEGIN BIT(0) +#define NIMBUS_CS_END BIT(1) + +static int spi_clkdiv = 16; +module_param(spi_clkdiv, int, 0644); +MODULE_PARM_DESC(spi_clkdiv, "SPI2 CLKDIV (higher=slower; try 8-32 for FW download)"); +static int reset_hold_ms = 10; +module_param(reset_hold_ms, int, 0644); +MODULE_PARM_DESC(reset_hold_ms, "RST low ms before bootload"); +static int reset_release_ms = 100; +module_param(reset_release_ms, int, 0644); +MODULE_PARM_DESC(reset_release_ms, "ms after RST release before SPI FW"); +static int go_spi_setup; +module_param(go_spi_setup, int, 0644); +MODULE_PARM_DESC(go_spi_setup, "SPI2 SETUP override for 2D54C GO (0=11B70)"); +static int prepend_z2_hdr; +module_param(prepend_z2_hdr, int, 0644); +MODULE_PARM_DESC(prepend_z2_hdr, "0=none 1=5A5A+BE len+CRC32 2=c3f5 hdr"); +static int chunk_spi; +module_param(chunk_spi, int, 0644); +MODULE_PARM_DESC(chunk_spi, "1=spi_sync chunk xfers (apple_z2-style atomic CS)"); +static int quiet; +module_param(quiet, int, 0644); +MODULE_PARM_DESC(quiet, "1=minimal logs (auto after GO fail)"); +static int skip_download; +module_param(skip_download, int, 0644); +MODULE_PARM_DESC(skip_download, "1=bootload+ping only, no FW chunks"); + +static bool nimbus_verbose = true; + +#define nimbus_vinfo(n, fmt, ...) \ + do { \ + if (nimbus_verbose && !(n)->parked) \ + dev_info(&(n)->spi->dev, fmt, ##__VA_ARGS__); \ + } while (0) + +struct nimbus { + struct spi_device *spi; + struct input_dev *input; + struct gpio_desc *enable; + struct gpio_desc *reset; + struct gpio_desc *attn; + void __iomem *gpio_base; + void __iomem *gpiocmd; + void __iomem *spi2; + struct task_struct *thread; + struct mutex lock; + bool stopped; + bool fw_loaded; + bool fw_tried; + bool spi_ok; + bool use_irq; + bool blob16; /* S5L TXDATA is 8-bit; 16-bit writes fail 4BC1 */ + bool parked; /* give up after recycle budget — stop SPI spam */ + int irq; + unsigned int ping_fails; + unsigned int recycle_count; +}; + +/* From irq-s5l8740-eic.c */ +int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); +/* From gpio-d1830.c — OSOS 20766 / 6644(4) / reg16 bit5 */ +int d1830_nimbus_rail(bool on); + +static u16 nimbus_sum16(const u8 *buf, int len) +{ + u16 sum = 0; + + while (len-- > 0) + sum += *buf++; + return sum; +} + +static void nimbus_gpiocmd_mode(struct nimbus *n, unsigned int gpio, u16 mode, int val) +{ + void __iomem *bank; + u32 pin = gpio & 7; + u32 dir; + u8 cmd; + + if (!n->gpiocmd || !n->gpio_base) + return; + + bank = n->gpio_base + 32 * (gpio >> 3); + if (mode == 1) { + /* 43D38C: mode 1 also sets DIR, then GPIOCMD 14/15 */ + cmd = val ? 15 : 14; + dir = readl(bank + 0x14); + writel(dir | BIT(pin), bank + 0x14); + } else if (mode == 0xFFFE) { + dir = readl(bank + 0x14); + writel(dir & ~BIT(pin), bank + 0x14); + cmd = 0; + } else { + cmd = (u8)mode; + dir = readl(bank + 0x14); + writel(dir | BIT(pin), bank + 0x14); + } + writel(((gpio >> 3) << 16) | (pin << 8) | cmd, n->gpiocmd); +} + +/* sub_23CD0(gpio, on) — PUNC bit at bank+0x10 */ +static void nimbus_punc(struct nimbus *n, unsigned int gpio, bool set) +{ + void __iomem *bank; + u32 pin, punc; + + if (!n->gpio_base) + return; + bank = n->gpio_base + 32 * (gpio >> 3); + pin = gpio & 7; + punc = readl(bank + 0x10); + if (set) + punc |= BIT(pin); + else + punc &= ~BIT(pin); + writel(punc, bank + 0x10); +} + +/* sub_20690(a1) — SPI2 pads 0x57–0x5A */ +static void nimbus_spi2_pinmux(struct nimbus *n, bool on) +{ + if (on) { + nimbus_punc(n, 0x57, false); + nimbus_gpiocmd_mode(n, 0x57, 5, 0); + nimbus_gpiocmd_mode(n, 0x58, 3, 0); + nimbus_gpiocmd_mode(n, 0x59, 3, 0); + nimbus_gpiocmd_mode(n, 0x5A, 3, 0); + } else { + nimbus_gpiocmd_mode(n, 0x57, 0xFFFE, 0); + nimbus_punc(n, 0x57, true); + nimbus_gpiocmd_mode(n, 0x58, 1, 0); + nimbus_gpiocmd_mode(n, 0x59, 1, 0); + nimbus_gpiocmd_mode(n, 0x5A, 0xFFFE, 0); + } +} + +/* + * sub_1A878 — disable / retry power-cut: + * 20490(0), RST assert, 20690(0), 20766(0) rail off + EN mode 1. + */ +static void nimbus_power_down(struct nimbus *n) +{ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_IRQ, 0xFFFE, 0); + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); + nimbus_spi2_pinmux(n, false); + d1830_nimbus_rail(false); + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_EN, 1, 0); + dev_info(&n->spi->dev, "1A878 power-cut (RST hold, rail off, EN mode 1)\n"); +} + +/* + * sub_11B70(2, 0x1A, 0x2EE0, 1) after every 20690(1). + * 1A5AC always re-inits SPI2 here. Skipping it after 1A878 remux + * left an extra SCLK edge: 0x1f01/0x4879 came back as 0x0f80/0xa43c + * and shifted one more bit on each retry. + */ +static void nimbus_spi2_11b70(struct nimbus *n) +{ + u32 setup; + + if (!n->spi2) + return; + writel(0xf, n->spi2 + SPI2_STATUS); + writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, + n->spi2 + SPI2_CTRL); + writel(10, n->spi2 + 0x44); + writel(24, n->spi2 + 0x38); /* 24 * a4=1 */ + writel(255, n->spi2 + 0x40); + writel(144, n->spi2 + 0x3c); /* 3 * 24 * (1+1) */ + writel(clamp(spi_clkdiv, 1, 255), n->spi2 + SPI2_CLKDIV); + writel(SPI2_SETUP_11B70, n->spi2 + SPI2_SETUP); + writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, + n->spi2 + SPI2_CTRL); + writel(SPI2_CTRL_ENABLE, n->spi2 + SPI2_CTRL); + setup = readl(n->spi2 + SPI2_SETUP); + dev_info(&n->spi->dev, "11B70 SPI2 SETUP=0x%x CLKDIV=%u\n", + setup, readl(n->spi2 + SPI2_CLKDIV)); +} + +static void nimbus_spi2_cs(struct nimbus *n, bool assert) +{ + u32 pin; + + if (!n->spi2) + return; + pin = readl(n->spi2 + SPI2_PIN); + if (assert) + pin &= ~SPI2_CS_BIT; + else + pin |= SPI2_CS_BIT; + writel(pin, n->spi2 + SPI2_PIN); +} + +/* + * Tight 4043D0 PIO for 16-byte app frames. OSOS 11B70 leaves SETUP + * bit5 set so 40F770 takes the DMA path — continuous clocks. Linux + * per-byte spi_sync gaps are fine for the bootloader, not the app. + */ +static void nimbus_spi2_fifo_flush(struct nimbus *n) +{ + if (!n->spi2) + return; + writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, + n->spi2 + SPI2_CTRL); + writel(0xf, n->spi2 + SPI2_STATUS); +} + +static int nimbus_burst_ex(struct nimbus *n, const u8 *tx, u8 *rx, + unsigned int len, unsigned int cs_flags) +{ + unsigned int i, guard; + u32 st; + + if (!n->spi2) + return -ENODEV; + if (cs_flags & NIMBUS_CS_BEGIN) { + nimbus_spi2_cs(n, true); + ndelay(2000); + nimbus_spi2_fifo_flush(n); + writel(readl(n->spi2 + SPI2_SETUP) & ~BIT(0), n->spi2 + SPI2_SETUP); + writel(readl(n->spi2 + SPI2_STATUS) | 0x400000u, n->spi2 + SPI2_STATUS); + } + for (i = 0; i < len; i++) { + writel(1, n->spi2 + SPI2_RXLIMIT); + guard = 100000; + do { + st = readl(n->spi2 + SPI2_STATUS); + } while ((st & 0x7c0) != 0 && (st & 0x7c0) != 0x40 && --guard); + writel(tx[i], n->spi2 + SPI2_TXDATA); + writel(1, n->spi2 + SPI2_UNK4C); + guard = 100000; + do { + st = readl(n->spi2 + SPI2_STATUS); + } while (!(st & 0xf800) && --guard); + if (rx) + rx[i] = (u8)readl(n->spi2 + SPI2_RXDATA); + else + readl(n->spi2 + SPI2_RXDATA); + } + if (cs_flags & NIMBUS_CS_END) { + writel(readl(n->spi2 + SPI2_SETUP) & ~0x400001u, n->spi2 + SPI2_SETUP); + nimbus_spi2_cs(n, false); + } + return 0; +} + +static int nimbus_burst(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len) +{ + return nimbus_burst_ex(n, tx, rx, len, NIMBUS_CS_BEGIN | NIMBUS_CS_END); +} + +/* + * S5LBox §6.1: every 32-bit HBPP field is middle-endian because the + * part is a 16-bit SPI slave. Our packed buffer is already wire-byte + * order (18 E1 30 01 …); pair as BE u16 so TXDATA 0x18E1 clocks 18 then E1. + * If TXDATA is 8-bit-only, only the low byte leaves and 4BC1 fails — + * send_chunk then falls back to 8-bit PIO. + */ +static int nimbus_burst_u16_ex(struct nimbus *n, const u8 *tx, u8 *rx, + unsigned int len, unsigned int cs_flags) +{ + unsigned int i, guard; + u32 st; + + if (!n->spi2) + return -ENODEV; + if (len & 1) + return nimbus_burst_ex(n, tx, rx, len, cs_flags); + if (cs_flags & NIMBUS_CS_BEGIN) { + nimbus_spi2_cs(n, true); + ndelay(2000); + nimbus_spi2_fifo_flush(n); + writel(readl(n->spi2 + SPI2_SETUP) & ~BIT(0), n->spi2 + SPI2_SETUP); + writel(readl(n->spi2 + SPI2_STATUS) | 0x400000u, n->spi2 + SPI2_STATUS); + } + for (i = 0; i < len; i += 2) { + u16 w = ((u16)tx[i] << 8) | tx[i + 1]; + u16 r; + + writel(1, n->spi2 + SPI2_RXLIMIT); + guard = 100000; + do { + st = readl(n->spi2 + SPI2_STATUS); + } while ((st & 0x7c0) != 0 && (st & 0x7c0) != 0x40 && --guard); + writel(w, n->spi2 + SPI2_TXDATA); + writel(1, n->spi2 + SPI2_UNK4C); + guard = 100000; + do { + st = readl(n->spi2 + SPI2_STATUS); + } while (!(st & 0xf800) && --guard); + r = (u16)readl(n->spi2 + SPI2_RXDATA); + if (rx) { + rx[i] = (u8)(r >> 8); + rx[i + 1] = (u8)r; + } + } + if (cs_flags & NIMBUS_CS_END) { + writel(readl(n->spi2 + SPI2_SETUP) & ~0x400001u, n->spi2 + SPI2_SETUP); + nimbus_spi2_cs(n, false); + } + return 0; +} + +static int nimbus_burst_u16(struct nimbus *n, const u8 *tx, u8 *rx, + unsigned int len) +{ + return nimbus_burst_u16_ex(n, tx, rx, len, + NIMBUS_CS_BEGIN | NIMBUS_CS_END); +} + +static int nimbus_burst16(struct nimbus *n, const u8 *tx, u8 *rx) +{ + return nimbus_burst(n, tx, rx, NIMBUS_FRAME_LEN); +} + +static int nimbus_xfer(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len) +{ + u8 *drain = NULL; + int ret; + struct spi_transfer t = { + .tx_buf = tx, + .rx_buf = rx, + .len = len, + }; + struct spi_message m; + + /* + * s5l8702 pio_one skips RXDATA when rx==NULL. 2D640 is TX-only + * in OSOS (40F770 dest 0) but that path still drains the FIFO. + * Without a drain, 8 KiB chunks overflow RX and the payload + * after the 12-byte header is dropped — 4BC1 can still ACK. + */ + if (!rx) { + drain = kzalloc(len, GFP_KERNEL); + if (!drain) + return -ENOMEM; + t.rx_buf = drain; + } + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + ret = spi_sync(n->spi, &m); + kfree(drain); + return ret; +} + +/* sub_2C87E — bootloader opcode whitelist */ +static bool nimbus_opcode_known(u16 w); +static bool nimbus_looks_like_arm(const u8 *p, size_t n); + +static bool nimbus_opcode_known(u16 w) +{ + return w == 0x18e1 || w == 0x1aa1 || w == 0x1f01 || w == 0x19c1 || + w == 0x4879 || w == 0x4bc1 || w == 0x4969 || w == 0x4ad1; +} + +/* sub_26494 — 16↔16 1A A1 + 18 E1 pad; two rev16 words must be known */ +static int nimbus_probe_26494(struct nimbus *n, const char *tag) +{ + u8 tx[NIMBUS_FRAME_LEN]; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + unsigned int i; + u16 w0, w1; + int ret; + + tx[0] = 0x1a; + tx[1] = 0xa1; + for (i = 2; i < NIMBUS_FRAME_LEN; i += 2) { + tx[i] = 0x18; + tx[i + 1] = 0xe1; + } + ret = nimbus_xfer(n, tx, rx, NIMBUS_FRAME_LEN); + w0 = (u16)((rx[0] << 8) | rx[1]); + w1 = (u16)((rx[2] << 8) | rx[3]); + dev_info(&n->spi->dev, + "26494 %s ret=%d words 0x%04x 0x%04x known=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + tag, ret, w0, w1, nimbus_opcode_known(w0) && nimbus_opcode_known(w1), + rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); + if (ret) + return ret; + if (!nimbus_opcode_known(w0) || !nimbus_opcode_known(w1)) + return -EIO; + return 0; +} + +/* sub_3D5706 — TX 1A A1, RX 2, byteswap */ +static int nimbus_status_poll(struct nimbus *n, u16 *status) +{ + u8 tx[2] = { 0x1a, 0xa1 }; + u8 rx[2] = { 0 }; + int ret; + + ret = nimbus_xfer(n, tx, rx, 2); + if (ret) + return ret; + if (status) + *status = (u16)((rx[0] << 8) | rx[1]); /* __rev16 of LE word */ + return 0; +} + +static bool nimbus_fw_has_8740_hdr(const u8 *data, size_t size) +{ + return size >= 8 && data[0] == '8' && data[1] == '7' && + data[2] == '4' && data[3] == '0'; +} + +static bool nimbus_fw_has_z2fw_hdr(const u8 *data, size_t size) +{ + u32 magic; + + if (size < 8) + return false; + magic = get_unaligned_le32(data); + return magic == NIMBUS_Z2FW_MAGIC; +} + +static u32 nimbus_crc32_payload(const u8 *p, size_t len) +{ + return crc32_le(~0U, p, len) ^ ~0U; +} + +static void nimbus_fw_audit(struct nimbus *n, const u8 *body, size_t body_len, + const char *tag) +{ + u32 crc; + size_t pad; + + if (!body_len) + return; + crc = nimbus_crc32_payload(body, body_len); + pad = body_len & 3u; + dev_info(&n->spi->dev, + "FW audit %s %zuB arm=%d crc32=0x%08x pad=%zu\n", + tag, body_len, nimbus_looks_like_arm(body, body_len), crc, pad); + if (body_len >= 16) { + u32 m = get_unaligned_le32(body); + u32 ln_le = get_unaligned_le32(body + 4); + u32 ln_be = get_unaligned_be32(body + 4); + u32 c_le = get_unaligned_le32(body + 8); + + if (m == NIMBUS_Z2_MAGIC_5A5A || m == NIMBUS_Z2_MAGIC_C3F5) + dev_info(&n->spi->dev, + " z2-dl hdr magic=0x%08x len_le=%u len_be=%u crc=0x%08x\n", + m, ln_le, ln_be, c_le); + } +} + +static int nimbus_build_z2_dl_hdr(u8 *hdr, const u8 *payload, size_t len, + u32 magic) +{ + u32 crc = nimbus_crc32_payload(payload, len); + + put_unaligned_le32(magic, hdr); + put_unaligned_be32(len, hdr + 4); + put_unaligned_le32(crc, hdr + 8); + put_unaligned_le32(0, hdr + 12); + return 0; +} + +static u8 *nimbus_maybe_prepend_z2_hdr(struct nimbus *n, const u8 *body, + size_t body_len, size_t *out_len) +{ + u8 *buf; + u32 magic; + + if (!prepend_z2_hdr || body_len < 4) + return NULL; + magic = prepend_z2_hdr == 2 ? NIMBUS_Z2_MAGIC_C3F5 : NIMBUS_Z2_MAGIC_5A5A; + buf = kmalloc(NIMBUS_Z2_HDR_LEN + body_len + 3, GFP_KERNEL); + if (!buf) + return NULL; + nimbus_build_z2_dl_hdr(buf, body, body_len, magic); + memcpy(buf + NIMBUS_Z2_HDR_LEN, body, body_len); + *out_len = NIMBUS_Z2_HDR_LEN + body_len; + if (*out_len & 3) { + memset(buf + *out_len, 0, 4 - (*out_len & 3)); + *out_len = round_up(*out_len, 4); + } + dev_info(&n->spi->dev, + "prepended Z2 dl hdr magic=0x%08x total=%zu\n", magic, *out_len); + return buf; +} + +static int nimbus_wait_ack(struct nimbus *n, u16 expect, int retries) +{ + int i; + u16 st = 0; + + for (i = 0; i < retries; i++) { + if (nimbus_status_poll(n, &st) == 0 && st == expect) + return 0; + msleep(2); + } + dev_warn(&n->spi->dev, "ACK wait fail (want 0x%04x got 0x%04x)\n", + expect, st); + return -ETIMEDOUT; +} + +/* + * S5LBox §6.5 / iOS AppleMultitouchZ2SPI MemRead: + * 1C 73 + addr middle-endian + sum16(addr bytes) + * then 8-byte ATN 1A A1 18 E1×3; value at rx[2..5] middle-endian. + */ +static int nimbus_rdreg(struct nimbus *n, u32 addr, u32 *val) +{ + u8 tx[8] = { 0x1c, 0x73 }; + u8 rx[8] = { 0 }; + u8 atn_tx[8] = { 0x1a, 0xa1, 0x18, 0xe1, 0x18, 0xe1, 0x18, 0xe1 }; + u8 atn_rx[8] = { 0 }; + u16 csum; + int ret; + + tx[2] = (addr >> 8) & 0xff; + tx[3] = addr & 0xff; + tx[4] = (addr >> 24) & 0xff; + tx[5] = (addr >> 16) & 0xff; + csum = nimbus_sum16(tx + 2, 4); + tx[6] = (csum >> 8) & 0xff; + tx[7] = csum & 0xff; + + ret = nimbus_xfer(n, tx, rx, 8); + if (ret) + return ret; + ret = nimbus_xfer(n, atn_tx, atn_rx, 8); + if (ret) + return ret; + if (val) + *val = ((u32)atn_rx[2] << 8) | atn_rx[3] | + ((((u32)atn_rx[4] << 8) | atn_rx[5]) << 16); + dev_dbg(&n->spi->dev, + "RDREG 0x%08x = 0x%08x atn %02x %02x %02x %02x %02x %02x %02x %02x\n", + addr, val ? *val : 0, atn_rx[0], atn_rx[1], atn_rx[2], + atn_rx[3], atn_rx[4], atn_rx[5], atn_rx[6], atn_rx[7]); + return 0; +} + +/* iOS3 MT_SPI_Z2_WAKE_CMD — 16-byte frame, opcode 0xEE, LE16 csum 0x00EE */ +static int nimbus_hbpp_wake_ee(struct nimbus *n, const char *tag) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0xee }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + int ret; + + tx[14] = 0xee; + ret = nimbus_burst16(n, tx, rx); + dev_info(&n->spi->dev, + "HBPP 0xEE wake %s ret=%d rx %02x %02x %02x %02x %02x %02x\n", + tag, ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5]); + return ret; +} + +static void nimbus_peek(struct nimbus *n, const char *tag) +{ + static const u32 addrs[] = { + 0x00000000, 0x0000d208, 0x0000e970, 0x00400200, + 0x0040f7f4, 0x0040fffc, 0x1000300c, 0x10008ffc, + }; + unsigned int i; + + if (!nimbus_verbose) + return; + for (i = 0; i < ARRAY_SIZE(addrs); i++) { + u32 v = 0; + + if (nimbus_rdreg(n, addrs[i], &v) == 0) + nimbus_vinfo(n, "peek %s %08x=%08x\n", tag, addrs[i], v); + } +} + +/* sub_20848(6593) */ +static int nimbus_bootload_cmd(struct nimbus *n) +{ + u8 tx[NIMBUS_FRAME_LEN]; + u8 rx[NIMBUS_FRAME_LEN]; + unsigned int i; + int ret; + + tx[0] = (NIMBUS_BOOTLOAD_WORD >> 8) & 0xff; + tx[1] = NIMBUS_BOOTLOAD_WORD & 0xff; + for (i = 2; i < NIMBUS_FRAME_LEN; i += 2) { + tx[i] = 0x18; + tx[i + 1] = 0xe1; + } + memset(rx, 0, sizeof(rx)); + ret = nimbus_xfer(n, tx, rx, NIMBUS_FRAME_LEN); + dev_info(&n->spi->dev, + "bootload 6593 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); + return ret; +} + +/* + * sub_3B9D0 dword swizzle into chunk payload: b0 b1 b2 b3 → b1 b0 b3 b2 + * (distinct from the full u32 byte-reverse used at FW+350). + */ +static void nimbus_grape_swizzle32(u8 *dst, const u8 *src, unsigned int len) +{ + unsigned int i; + + for (i = 0; i + 3 < len; i += 4) { + dst[i] = src[i + 1]; + dst[i + 1] = src[i]; + dst[i + 2] = src[i + 3]; + dst[i + 3] = src[i + 2]; + } +} + +/* 273A0: full u32 reverse of the 512-byte cal window before 2D7A4. */ +static void nimbus_bswap32_words(u8 *p, unsigned int len) +{ + unsigned int i; + + for (i = 0; i + 3 < len; i += 4) { + u8 t0 = p[i], t1 = p[i + 1]; + + p[i] = p[i + 3]; + p[i + 1] = p[i + 2]; + p[i + 2] = t1; + p[i + 3] = t0; + } +} + +static u32 nimbus_sum32(const u8 *p, unsigned int len) +{ + u32 s = 0; + + while (len--) + s += *p++; + return s; +} + +static bool nimbus_cal_from_isys(struct nimbus *n, const u8 *blob, size_t len, + u8 *win) +{ + if (len < NIMBUS_FW_HDR_OFF + NIMBUS_FW_HDR_LEN) + return false; + memcpy(win, blob + NIMBUS_FW_HDR_OFF, NIMBUS_FW_HDR_LEN); + nimbus_bswap32_words(win, NIMBUS_FW_HDR_LEN); + dev_info(&n->spi->dev, + "cal +350 sum32=0x%08x head %02x %02x %02x %02x byte8=%u\n", + nimbus_sum32(win, NIMBUS_FW_HDR_LEN), + win[0], win[1], win[2], win[3], win[8]); + return true; +} + +static bool nimbus_try_isys_slot(struct nimbus *n, phys_addr_t slot, u8 *win) +{ + void __iomem *p, *src; + u32 magic, ptr; + u8 *tmp; + bool ok = false; + + p = ioremap(slot, 8); + if (!p) + return false; + magic = readl(p); + ptr = readl(p + 4); + iounmap(p); + dev_info(&n->spi->dev, "IsyS slot 0x%lx magic=0x%08x ptr=0x%08x\n", + (unsigned long)slot, magic, ptr); + if (magic != NIMBUS_ISYS_MAGIC || !ptr) + return false; + src = ioremap(ptr, NIMBUS_ISYS_LEN); + if (!src) + return false; + tmp = kmalloc(NIMBUS_ISYS_LEN, GFP_KERNEL); + if (!tmp) { + iounmap(src); + return false; + } + memcpy_fromio(tmp, src, NIMBUS_ISYS_LEN); + iounmap(src); + ok = nimbus_cal_from_isys(n, tmp, NIMBUS_ISYS_LEN, win); + kfree(tmp); + return ok; +} + +/* + * 2D7A4 payload = 43CFB4()+350, 512B, u32-reversed. + * RetailOS source is sub_564: A34(0x18) → copy 0x560 into BSS 0x8A8B510. + * A34 lives at Grape 0x2202FE18; ioremap of that is not AP RAM on DFU. + * 273A0 uses the grape image itself at +350. + */ +static int nimbus_load_cal_window(struct nimbus *n, u8 *win, + const u8 *fw, size_t fw_len) +{ + if (fw && nimbus_cal_from_isys(n, fw, fw_len, win)) { + dev_info(&n->spi->dev, + "2D7A4 cal from grape.bin +350 (skipped A34 0x22)\n"); + return 0; + } + + dev_warn(&n->spi->dev, + "no grape.bin +350 window — 2D7A4 zeros\n"); + return -ENOENT; +} + +/* + * sub_2D640 / 2D7A4 + trampoline sub_35C1C → sub_3B9D0: + * [0..1] 18 E1 + * [2..3] 30 01 + * [4..5] (len>>10), (len>>2) — len must be multiple of 4 + * [6..9] offset packed BYTE1,0,3,2 + * [10..11] sum16 of bytes [4..9] + * [12 .. 12+len) swizzled payload + * [12+len .. +4) sum32 of payload, stored BYTE1,0,3,2 + * SPI len = len + 16; ACK 0x4BC1 (retry ≤5). + */ +static int nimbus_send_chunk_ex(struct nimbus *n, const u8 *data, + unsigned int offset, unsigned int len, + unsigned int cs_flags) +{ + u8 *buf; + u16 hdr_sum; + u32 body_sum; + unsigned int i; + int ret, try; + + if (!len || len > NIMBUS_CHUNK_MAX || (len & 3)) + return -EINVAL; + + buf = kzalloc(len + NIMBUS_HDR_LEN, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + buf[0] = 0x18; + buf[1] = 0xe1; + buf[2] = 0x30; + buf[3] = 0x01; + buf[4] = (len >> 10) & 0xff; + buf[5] = (len >> 2) & 0xff; + buf[6] = (offset >> 8) & 0xff; + buf[7] = offset & 0xff; + buf[8] = (offset >> 24) & 0xff; + buf[9] = (offset >> 16) & 0xff; + hdr_sum = nimbus_sum16(buf + 4, 6); + buf[10] = (hdr_sum >> 8) & 0xff; + buf[11] = hdr_sum & 0xff; + + nimbus_grape_swizzle32(buf + 12, data, len); + + body_sum = 0; + for (i = 0; i < len; i++) + body_sum += buf[12 + i]; + buf[12 + len] = (body_sum >> 8) & 0xff; + buf[12 + len + 1] = body_sum & 0xff; + buf[12 + len + 2] = (body_sum >> 24) & 0xff; + buf[12 + len + 3] = (body_sum >> 16) & 0xff; + + /* Z2 SEND_BLOB: spi_sync keeps CS down for whole HBPP frame. */ + for (try = 0; try < 5; try++) { + if (chunk_spi) + ret = nimbus_xfer(n, buf, NULL, len + NIMBUS_HDR_LEN); + else if (n->blob16) + ret = nimbus_burst_u16_ex(n, buf, NULL, + len + NIMBUS_HDR_LEN, cs_flags); + else + ret = nimbus_burst_ex(n, buf, NULL, + len + NIMBUS_HDR_LEN, cs_flags); + if (ret) + continue; + if (nimbus_wait_ack(n, NIMBUS_ACK_CHUNK, 8) == 0) { + if (!offset) + dev_info(&n->spi->dev, + "chunk0 %u bytes ACK 0x4BC1 (%s)\n", + len, n->blob16 ? "u16" : "u8"); + kfree(buf); + return 0; + } + if (n->blob16 && try == 0) { + n->blob16 = false; + dev_info(&n->spi->dev, + "16-bit DATA no 4BC1 — falling back to 8-bit PIO\n"); + } + } + kfree(buf); + return -EIO; +} + +static int nimbus_send_chunk(struct nimbus *n, const u8 *data, + unsigned int offset, unsigned int len) +{ + return nimbus_send_chunk_ex(n, data, offset, len, + NIMBUS_CS_BEGIN | NIMBUS_CS_END); +} + +static int nimbus_send_blob(struct nimbus *n, const u8 *data, unsigned int len, + unsigned int dest_off) +{ + unsigned int off = 0; + u8 pad[4]; + + while (off < len) { + unsigned int chunk = min_t(unsigned int, len - off, NIMBUS_CHUNK_MAX); + int ret; + + /* RetailOS always transfers whole words */ + if (chunk & 3) + chunk &= ~3u; + if (!chunk) { + memset(pad, 0, sizeof(pad)); + memcpy(pad, data + off, len - off); + return nimbus_send_chunk(n, pad, dest_off + off, 4); + } + ret = nimbus_send_chunk(n, data + off, dest_off + off, chunk); + if (ret) + return ret; + off += chunk; + } + return 0; +} + +/* sub_34AD0(a1,a2,a3) — TX 1E 33 + 12-byte pack + sum16, expect ACK 0x4AD1 */ +static int nimbus_cmd_34ad0(struct nimbus *n, u32 a1, u32 a2, u32 a3) +{ + u8 tx[16]; + u8 rx[16]; + u8 body[12]; + u16 csum; + int ret; + + tx[0] = 0x1e; + tx[1] = 0x33; + + /* Packing from Hex-Rays sub_34AD0 */ + body[0] = (a1 >> 8) & 0xff; + body[1] = a1 & 0xff; + body[2] = (a1 >> 24) & 0xff; + body[3] = (a1 >> 16) & 0xff; + body[4] = (a3 >> 8) & 0xff; + body[5] = a3 & 0xff; + body[6] = (a3 >> 24) & 0xff; + body[7] = (a3 >> 16) & 0xff; + body[8] = (a2 >> 8) & 0xff; + body[9] = a2 & 0xff; + body[10] = (a2 >> 24) & 0xff; + body[11] = (a2 >> 16) & 0xff; + csum = nimbus_sum16(body, 12); + memcpy(tx + 2, body, 12); + tx[14] = (csum >> 8) & 0xff; + tx[15] = csum & 0xff; + + /* 34AD0: 40F770 16↔16 then 3D5706 == 0x4AD1 */ + ret = nimbus_xfer(n, tx, rx, 16); + if (ret) + return ret; + return nimbus_wait_ack(n, NIMBUS_ACK_34AD0, 8); +} + +/* sub_2D5B0 post-download */ +static int nimbus_post_download(struct nimbus *n) +{ + u8 tx[2], rx[2]; + u16 st = 0; + int ret, i; + + static const struct { + u32 a1, a2, a3; + } pokes[] = { + /* 2D5B0: ldr 0x1000300C, then +0x50 / +0x4C / -0x0C */ + { 0x1000300c, 5859, (u32)-1 }, + { 0x1000305c, 32, (u32)-1 }, + { 0x10003058, 6, (u32)-1 }, + { 0x10003000, 3, (u32)-1 }, + }; + + for (i = 0; i < ARRAY_SIZE(pokes); i++) { + ret = nimbus_cmd_34ad0(n, pokes[i].a1, pokes[i].a2, pokes[i].a3); + dev_info(&n->spi->dev, "34AD0[%d] %d\n", i, ret); + if (ret) + return ret; + } + + put_unaligned_le16(NIMBUS_POST_POKE, tx); + ret = nimbus_xfer(n, tx, rx, 2); + if (ret) + return ret; + msleep(65); + /* 2D5B0: 3D5706 success only — does not require 0x4BC1 */ + if (nimbus_status_poll(n, &st) == 0) { + dev_info(&n->spi->dev, "post-poke status 0x%04x\n", st); + return 0; + } + return -EIO; +} + +/* sub_2D54C — 12↔12: 1D 53 + two LE u32 + sum16 */ +static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) +{ + u8 tx[12] = { 0x1d, 0x53 }; + u8 rx[12] = { 0 }; + u16 csum; + u32 saved_setup = 0; + int ret; + + put_unaligned_le32(word0, tx + 2); + put_unaligned_le32(word1, tx + 6); + csum = nimbus_sum16(tx + 2, 8); + tx[10] = (csum >> 8) & 0xff; + tx[11] = csum & 0xff; + if (n->spi2) { + nimbus_spi2_fifo_flush(n); + if (go_spi_setup > 0) { + saved_setup = readl(n->spi2 + SPI2_SETUP); + writel((u32)go_spi_setup, n->spi2 + SPI2_SETUP); + dev_info(&n->spi->dev, "2D54C GO SETUP 0x%x (was 0x%x)\n", + go_spi_setup, saved_setup); + } + } + ret = nimbus_burst(n, tx, rx, 12); + if (saved_setup) + writel(saved_setup, n->spi2 + SPI2_SETUP); + dev_info(&n->spi->dev, + "2D54C %08x %08x ret=%d rx %02x %02x %02x %02x %02x %02x\n", + word0, word1, ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5]); + return ret; +} + +static void nimbus_drain(struct nimbus *n, unsigned int bytes) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0 }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + unsigned int nxf = bytes < NIMBUS_FRAME_LEN ? bytes : NIMBUS_FRAME_LEN; + + nimbus_burst(n, tx, rx, nxf); +} + +static int nimbus_cmd_2d54c(struct nimbus *n) +{ + u16 st = 0; + int ret; + + /* 273A0: 2D54C immediately after 2D5B0. No HBPP MemRead around go. */ + nimbus_drain(n, 16); + ret = nimbus_cmd_2d54c_raw(n, 0x00100018, 0x00000100); + if (!ret) { + msleep(40); + if (nimbus_status_poll(n, &st) == 0) + dev_info(&n->spi->dev, "post-2D54C status 0x%04x\n", st); + nimbus_drain(n, 16); + } + return ret; +} + +/* + * 273A0: 2D640(204E0 ARM, hdr+0x0c) → 2D7A4(BSS+350 @ 0x400200) → 2D5B0. + */ +static int nimbus_probe_z2_eb(struct nimbus *n) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0 }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + int ret; + + tx[0] = 0xeb; + tx[1] = 0x01; + put_unaligned_le16(0xeb + 1, tx + 14); + ret = nimbus_burst16(n, tx, rx); + dev_info(&n->spi->dev, + "z2-EB ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); + return (ret == 0 && rx[0] == 0xe1) ? 0 : -EIO; +} + +static int nimbus_probe_ea16(struct nimbus *n) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0 }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + u16 csum; + int ret; + + tx[0] = NIMBUS_MAGIC; + tx[1] = 0x01; + tx[2] = 0x01; + csum = nimbus_sum16(tx, 14); + put_unaligned_le16(csum, tx + 14); + ret = nimbus_burst16(n, tx, rx); + dev_info(&n->spi->dev, + "EA16 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); + return (ret == 0 && rx[0] == NIMBUS_MAGIC) ? 0 : -EIO; +} + +static int nimbus_probe_ping16(struct nimbus *n) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0 }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + u16 csum; + int ret; + struct spi_transfer t = { + .tx_buf = tx, + .rx_buf = rx, + .len = NIMBUS_FRAME_LEN, + .bits_per_word = 16, + }; + struct spi_message m; + + put_unaligned_le32(NIMBUS_PING_TYPE, tx); + csum = nimbus_sum16(tx, 14); + put_unaligned_le16(csum, tx + 14); + spi_message_init(&m); + spi_message_add_tail(&t, &m); + ret = spi_sync(n->spi, &m); + dev_info(&n->spi->dev, + "ping16 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x csum=%d\n", + ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7], + nimbus_sum16(rx, 14) == get_unaligned_le16(rx + 14)); + return ret; +} + +#define S5L8740_AES_PHYS 0x38c00000UL + +/* + * Touch FW GID decrypt (OSOS sub_422FFA / sub_204E0): + * MMIO @ 0x38C00000 AES, keysel=1 (GID), CBC IV=0, CFG=0xE|enc. + * 26CCC verifies with 16-byte encrypt; 204E0 decrypts full ARM @ +0x400 + * for 8740 rev 3 only. grape-nimbus.bin on DFU is usually pre-decrypted-cut. + * force_gid=1 tries 422FFA even when loading plaintext blob (bring-up). + */ +static int force_gid; +module_param(force_gid, int, 0644); +MODULE_PARM_DESC(force_gid, "1=422FFA decrypt attempt even without 8740 rev3 hdr"); +static int nimbus_422ffa_mmio(struct device *dev, u8 *buf, unsigned int len, + bool encrypt) +{ + struct device *aes_dev; + void __iomem *aes; + struct clk *clk = NULL; + dma_addr_t phys; + u32 irq = 0; + int ret; + + if (!len || (len & 15)) + return -EINVAL; + + aes_dev = bus_find_device_by_name(&platform_bus_type, NULL, + "38c00000.aes"); + if (!aes_dev) + aes_dev = dev; + + clk = clk_get(aes_dev, "aes"); + if (IS_ERR(clk)) + clk = NULL; + if (clk) { + ret = clk_prepare_enable(clk); + if (ret) { + clk_put(clk); + if (aes_dev != dev) + put_device(aes_dev); + return ret; + } + } + + aes = ioremap(S5L8740_AES_PHYS, 0x100); + if (!aes) { + ret = -ENOMEM; + goto out_clk; + } + + phys = dma_map_single(aes_dev, buf, len, DMA_BIDIRECTIONAL); + if (dma_mapping_error(aes_dev, phys)) { + ret = -ENOMEM; + goto out_io; + } + + writel(1, aes + 0x08); + { + unsigned int guard = 100000; + + while (readl(aes + 0x08) && --guard) + ; + } + writel(1, aes + 0x70); + writel(1, aes + 0x6c); + writel(~1u, aes + 0x88); + writel(1, aes + 0x00); + writel((encrypt ? 1u : 0u) | 0xeu, aes + 0x14); + writel(len, aes + 0x18); + writel(phys, aes + 0x28); + writel(len, aes + 0x2c); + writel(phys, aes + 0x20); + writel(len, aes + 0x24); + writel(phys, aes + 0x30); + writel(len, aes + 0x34); + writel(0, aes + 0x74); + writel(0, aes + 0x78); + writel(0, aes + 0x7c); + writel(0, aes + 0x80); + writel(7, aes + 0x0c); + writel(1, aes + 0x04); + ret = readl_poll_timeout(aes + 0x0c, irq, irq & 1, 2, 500000); + writel(0, aes + 0x00); + dma_unmap_single(aes_dev, phys, len, DMA_BIDIRECTIONAL); + if (ret) + dev_err(dev, "422FFA MMIO timeout IRQ=0x%x\n", irq); + +out_io: + iounmap(aes); +out_clk: + if (clk) { + clk_disable_unprepare(clk); + clk_put(clk); + } + if (aes_dev != dev) + put_device(aes_dev); + return ret; +} + +static int nimbus_gid_crypt(struct device *dev, u8 *buf, unsigned int len, + bool encrypt) +{ + struct crypto_skcipher *tfm; + struct skcipher_request *req; + struct scatterlist sg; + u8 key[AES_KEYSIZE_128] = { 0 }; + u8 iv[AES_BLOCK_SIZE] = { 0 }; + int ret; + + if (!len || (len & 15)) + return -EINVAL; + + tfm = crypto_alloc_skcipher("cbc(aes-gid)", 0, 0); + if (IS_ERR(tfm)) + return PTR_ERR(tfm); + ret = crypto_skcipher_setkey(tfm, key, sizeof(key)); + if (ret) + goto out_tfm; + req = skcipher_request_alloc(tfm, GFP_KERNEL); + if (!req) { + ret = -ENOMEM; + goto out_tfm; + } + sg_init_one(&sg, buf, len); + skcipher_request_set_callback(req, 0, NULL, NULL); + skcipher_request_set_crypt(req, &sg, &sg, len, iv); + ret = encrypt ? crypto_skcipher_encrypt(req) : crypto_skcipher_decrypt(req); + skcipher_request_free(req); +out_tfm: + crypto_free_skcipher(tfm); + if (ret) + dev_warn(dev, "cbc(aes-gid) %s %d\n", + encrypt ? "enc" : "dec", ret); + return ret; +} + +static bool nimbus_looks_like_arm(const u8 *p, size_t n) +{ + return n >= 4 && p[0] == 0x18 && p[1] == 0xf0 && + p[2] == 0x9f && p[3] == 0xe5; +} + +/* + * 204E0 sends le32(8740+0x0c)=0xe970. The decrypted cut is 0xecf0 and the + * extra 896 bytes are 0x53/0x43 fill. Downloading that fill to dest 0xe970 + * stomps SRAM just past the official image (likely BSS / bootloader workspace). + */ +static size_t nimbus_official_arm_len(const u8 *body, size_t len) +{ + size_t i; + + if (len <= NIMBUS_ARM_OFFICIAL || !nimbus_looks_like_arm(body, len)) + return len; + for (i = NIMBUS_ARM_OFFICIAL; i < len; i++) { + if (body[i] != 0x53 && body[i] != 0x43) + return len; + } + return NIMBUS_ARM_OFFICIAL; +} + +static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, + bool arm_at_zero) +{ + u8 *dec = NULL; + int ret; + const u8 *body = data; + size_t body_len = size; + bool apple_hdr = nimbus_fw_has_8740_hdr(data, size); + + (void)arm_at_zero; + + /* + * 1A640 NOR 8740 → 204E0. ARM at +0x400, size le32(+0x0c). + * Rev 3: 422FFA GID-CBC IV=0 decrypt in place (NOR gpfw). + * Rev 4: no decrypt; 204E0 then returns 33 so NOR drops it. + */ + if (apple_hdr) { + u32 hdr_sz; + u8 rev; + + if (size < 0x400 + 4) { + dev_warn(&n->spi->dev, + "FW too small (%zu) — skip download\n", size); + return -EINVAL; + } + hdr_sz = get_unaligned_le32(data + 0x0c); + /* Short NOR slice: take the ARM bytes we have. Never expand. */ + if (!hdr_sz || hdr_sz > size - 0x400) + hdr_sz = size - 0x400; + hdr_sz &= ~3u; + body = data + 0x400; + body_len = hdr_sz; + rev = data[7]; + /* + * Disk path (1A640) sends the raw file, no 204E0. + * NOR 204E0 rev≠3 returns 33 after writing outputs and + * the NOR path frees the buffer. Rev 4 plaintext is the + * disk-shaped image — send the whole 8740+ARM at dest 0. + */ + if (rev != 3) { + dev_info(&n->spi->dev, + "204E0 ARM-at-0 %zuB dest 0 (rev=%u hdr+0x0c=0x%x file=%zu)\n", + body_len, rev, get_unaligned_le32(data + 0x0c), + size); + if (force_gid && size >= 0x400 + 16) { + u8 *try = kmemdup(data + 0x400, min_t(size_t, body_len, size - 0x400), + GFP_KERNEL); + if (try && nimbus_422ffa_mmio(&n->spi->dev, try, + round_up(min_t(size_t, body_len, size - 0x400) & ~15u, 16), + false) == 0 && + nimbus_looks_like_arm(try, min_t(size_t, 16, body_len))) { + dev_info(&n->spi->dev, "force_gid 422FFA ARM ok\n"); + body = try; + body_len = min_t(size_t, body_len, size - 0x400); + dec = try; + } else { + kfree(try); + } + } + goto send; + } + dev_info(&n->spi->dev, + "204E0 ARM-at-0 %zu bytes rev=%u (hdr+0x0c=0x%x file=%zu)\n", + body_len, rev, get_unaligned_le32(data + 0x0c), size); + + if (rev == 3) { + u8 *probe; + bool verified = false; + + probe = kmemdup(data, 16, GFP_KERNEL); + if (!probe) + return -ENOMEM; + if (nimbus_gid_crypt(&n->spi->dev, probe, 16, true) == 0 && + !memcmp(probe, data + 0x40, 16)) { + verified = true; + dev_info(&n->spi->dev, "26CCC GID verify OK\n"); + } else { + memcpy(probe, data, 16); + if (nimbus_422ffa_mmio(&n->spi->dev, probe, 16, + true) == 0 && + !memcmp(probe, data + 0x40, 16)) { + verified = true; + dev_info(&n->spi->dev, + "26CCC 422FFA verify OK\n"); + } else { + dev_warn(&n->spi->dev, + "26CCC GID verify fail (sig %02x%02x%02x%02x got %02x%02x%02x%02x)\n", + data[0x40], data[0x41], + data[0x42], data[0x43], + probe[0], probe[1], + probe[2], probe[3]); + } + } + kfree(probe); + + dec = kmemdup(body, body_len, GFP_KERNEL); + if (!dec) + return -ENOMEM; + /* 204E0: one-shot 422FFA, not the Linux AES walk. */ + ret = nimbus_422ffa_mmio(&n->spi->dev, dec, body_len, + false); + if (ret || !nimbus_looks_like_arm(dec, body_len)) { + memcpy(dec, body, body_len); + ret = nimbus_gid_crypt(&n->spi->dev, dec, + body_len, false); + } + dev_info(&n->spi->dev, + "204E0 GID decrypt ret=%d arm=%d head %02x %02x %02x %02x ver=%d\n", + ret, nimbus_looks_like_arm(dec, body_len), + dec[0], dec[1], dec[2], dec[3], verified); + if (!ret && body_len > 0xd210) + dev_info(&n->spi->dev, + "ARM +0x54 %02x%02x%02x%02x +0x100 %02x%02x%02x%02x +0x1000 %02x%02x%02x%02x +0xD208 %02x%02x%02x%02x +0x20=%08x\n", + dec[0x54], dec[0x55], dec[0x56], dec[0x57], + dec[0x100], dec[0x101], dec[0x102], dec[0x103], + dec[0x1000], dec[0x1001], dec[0x1002], + dec[0x1003], + dec[0xd208], dec[0xd209], dec[0xd20a], + dec[0xd20b], + get_unaligned_le32(dec + 0x20)); + if (!ret) { + body = dec; + } else { + kfree(dec); + dec = NULL; + } + } + } else if (size < 4) { + dev_warn(&n->spi->dev, "FW empty (%zu) — skip download\n", size); + return -EINVAL; + } else { + dev_info(&n->spi->dev, "Grape FW download %zu bytes (no 8740)\n", + size); + } + +send: + { + size_t official = nimbus_official_arm_len(body, body_len); + size_t dl_len = body_len; + const u8 *dl_body = body; + u8 *z2_prep = NULL; + u8 *pad_buf = NULL; + u8 *win; + int try, cal; + + if (official < body_len) { + dev_info(&n->spi->dev, + "cap ARM %zu -> %zu (204E0 +0x0c; strip S/C fill)\n", + body_len, official); + body_len = official; + dl_len = body_len; + } + + z2_prep = nimbus_maybe_prepend_z2_hdr(n, body, body_len, &dl_len); + if (z2_prep) + dl_body = z2_prep; + else if (dl_len & 3) { + pad_buf = kmalloc(round_up(dl_len, 4), GFP_KERNEL); + if (!pad_buf) { + kfree(dec); + return -ENOMEM; + } + memcpy(pad_buf, dl_body, dl_len); + memset(pad_buf + dl_len, 0, round_up(dl_len, 4) - dl_len); + dl_len = round_up(dl_len, 4); + dl_body = pad_buf; + dev_info(&n->spi->dev, "FW padded to %zu (4-byte align)\n", + dl_len); + } + nimbus_fw_audit(n, dl_body, dl_len, "2D640"); + + win = kzalloc(NIMBUS_FW_HDR_LEN, GFP_KERNEL); + if (!win) { + kfree(z2_prep); + kfree(pad_buf); + kfree(dec); + return -ENOMEM; + } + /* 273A0: 43CFB4()+350 from the grape image, not A34 0x22. */ + cal = nimbus_load_cal_window(n, win, data, size); + + /* 20E94: 273A0 up to 3 times, no 1A878 between. */ + for (try = 0; try < 3; try++) { + ret = nimbus_send_blob(n, dl_body, dl_len, 0); + if (ret) { + dev_err(&n->spi->dev, + "2D640 try %d: %d\n", try, ret); + continue; + } + ret = nimbus_send_blob(n, win, NIMBUS_FW_HDR_LEN, + 0x400200); + if (ret) { + dev_err(&n->spi->dev, + "2D7A4 try %d: %d\n", try, ret); + continue; + } + dev_info(&n->spi->dev, + "2D7A4 512B %s @0x400200 ACK\n", + cal ? "zeros" : "grape+350"); + ret = nimbus_post_download(n); + if (ret) { + dev_warn(&n->spi->dev, + "2D5B0 try %d: %d\n", try, ret); + continue; + } + ret = nimbus_cmd_2d54c(n); + if (!ret) + break; + dev_warn(&n->spi->dev, "2D54C try %d: %d\n", try, ret); + } + kfree(win); + kfree(z2_prep); + kfree(pad_buf); + } + kfree(dec); + if (ret) + return ret; + n->fw_loaded = true; + return 0; +} + +static int nimbus_ping(struct nimbus *n, u16 *status_out) +{ + u8 tx[NIMBUS_FRAME_LEN] = { 0 }; + u8 rx[NIMBUS_FRAME_LEN] = { 0 }; + u16 csum, rx_csum; + int ret, tries; + + put_unaligned_le32(NIMBUS_PING_TYPE, tx); + csum = nimbus_sum16(tx, 14); + put_unaligned_le16(csum, tx + 14); + + /* 182590: up to 5 retries, sleep 1 between. Burst matches DMA. */ + for (tries = 0; tries < 6; tries++) { + ret = nimbus_burst16(n, tx, rx); + if (ret) + return ret; + + rx_csum = get_unaligned_le16(rx + 14); + if (!rx_csum && !nimbus_sum16(rx, 14)) { + dev_warn(&n->spi->dev, "ping rx all-zero (MISO dead)\n"); + return -EIO; + } + if (nimbus_sum16(rx, 14) == rx_csum) + break; + /* One dump per call; MultitouchTask rate-limits via ping_fails. */ + if (tries == 0 && n->ping_fails == 0) + dev_warn(&n->spi->dev, + "ping csum fail rx %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + rx[0], rx[1], rx[2], rx[3], rx[4], + rx[5], rx[6], rx[7], rx[8], rx[9], + rx[10], rx[11], rx[12], rx[13], + rx[14], rx[15]); + if (tries == 5) + return -EIO; + msleep(1); + } + + if (status_out) + *status_out = get_unaligned_le16(rx + 1); + return 0; +} + +static void nimbus_map_coords(s16 rawx, s16 rawy, int *x, int *y) +{ + int xx = (NIMBUS_ABS_X_MAX * ((int)rawx + 75)) / NIMBUS_SCALE_X_DIV; + int yy = (NIMBUS_ABS_Y_MAX * ((int)rawy + 75)) / NIMBUS_SCALE_Y_DIV; + + if (xx < 0) + xx = 0; + if (xx > NIMBUS_ABS_X_MAX) + xx = NIMBUS_ABS_X_MAX; + yy = NIMBUS_ABS_Y_MAX - yy; + if (yy < 0) + yy = 0; + if (yy > NIMBUS_ABS_Y_MAX) + yy = NIMBUS_ABS_Y_MAX; + *x = xx; + *y = yy; +} + +static void nimbus_parse_D(struct nimbus *n, const u8 *payload, unsigned int len) +{ + const u8 *rec; + u8 count, stride; + unsigned int off; + int i; + + if (len < 18 || payload[0] != 0x44) + return; + + off = payload[2]; + count = payload[16]; + stride = payload[17]; + if (!stride || off >= len) + return; + if (count > NIMBUS_SLOTS) + count = NIMBUS_SLOTS; + + rec = payload + off; + for (i = 0; i < count; i++) { + s16 rawx, rawy; + int x, y; + u8 tip; + + if (rec + stride > payload + len) + break; + rawx = (s16)get_unaligned_le16(rec + 4); + rawy = (s16)get_unaligned_le16(rec + 6); + tip = rec[1]; + nimbus_map_coords(rawx, rawy, &x, &y); + + input_mt_slot(n->input, i); + input_mt_report_slot_state(n->input, MT_TOOL_FINGER, tip != 0); + if (tip) { + input_report_abs(n->input, ABS_MT_POSITION_X, x); + input_report_abs(n->input, ABS_MT_POSITION_Y, y); + /* Visible proof on quiet console (fbcon won't scroll from MT) */ + pr_warn_ratelimited("nimbus touch slot%d tip=%u raw=%d,%d -> %d,%d\n", + i, tip, rawx, rawy, x, y); + } + rec += stride; + } + input_mt_sync_frame(n->input); + input_sync(n->input); +} + +static int nimbus_read_reports(struct nimbus *n, u16 ping_st) +{ + u8 *tx, *rx; + u16 csum; + unsigned int len = ping_st + 5; + int ret; + + /* 17E404(ping_status+5), cap 512 */ + if (len < NIMBUS_FRAME_LEN) + len = NIMBUS_FRAME_LEN; + if (len > NIMBUS_READ_MAX) + len = NIMBUS_READ_MAX; + + tx = kzalloc(NIMBUS_READ_MAX, GFP_KERNEL); + rx = kzalloc(NIMBUS_READ_MAX, GFP_KERNEL); + if (!tx || !rx) { + ret = -ENOMEM; + goto out; + } + + tx[0] = NIMBUS_MAGIC; + tx[1] = 0x01; + tx[2] = 0x01; + csum = nimbus_sum16(tx, 14); + /* 17E404 writes sum16(tx,14) at TX+(len-2) */ + put_unaligned_le16(csum, tx + len - 2); + + ret = nimbus_xfer(n, tx, rx, len); + if (ret) + goto out; + + if (rx[0] != NIMBUS_MAGIC) { + dev_info_once(&n->spi->dev, + "read magic fail rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], + rx[6], rx[7]); + ret = -EIO; + goto out; + } + + if (rx[2] && rx[2] != 2) { + unsigned int plen = rx[2]; + + if (plen >= 2 && (5 + (plen - 2)) <= len) + nimbus_parse_D(n, rx + 5, plen - 2); + } else if (len > 5 && rx[5] == 0x44) { + nimbus_parse_D(n, rx + 5, len - 5); + } + ret = 0; +out: + kfree(tx); + kfree(rx); + return ret; +} + +static void nimbus_dump_pad(struct nimbus *n, unsigned int gpio, const char *name) +{ + void __iomem *b; + u32 pin, pcon, din, dir; + + if (!nimbus_verbose || !n->gpio_base) + return; + b = n->gpio_base + 32 * (gpio >> 3); + pin = gpio & 7; + pcon = readl(b); + din = readl(b + 0x04); + dir = readl(b + 0x14); + dev_info(&n->spi->dev, + "pad %s gpio%u pcon=%x din=%u dir=%u dout=%u punb=%u punc=%u\n", + name, gpio, (pcon >> (4 * pin)) & 0xf, + !!(din & BIT(pin)), !!(dir & BIT(pin)), + !!(readl(b + 0x08) & BIT(pin)), + !!(readl(b + 0x0c) & BIT(pin)), + !!(readl(b + 0x10) & BIT(pin))); +} + +static void nimbus_gpio_por_reset(struct nimbus *n) +{ + /* Clear latched download mode from a prior failed attempt. */ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); + msleep(reset_hold_ms); + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 1); + msleep(reset_release_ms); + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); + msleep(5); + dev_info(&n->spi->dev, "POR RST %dms low / %dms high\n", + reset_hold_ms, reset_release_ms); +} + +static void nimbus_gpio_bringup(struct nimbus *n) +{ + int rail; + + /* GPIOCMD only — gpiod set_value fights polarity on RST. */ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); + msleep(5); + /* + * sub_20766(1): 439B00(1) rail first, 66A8(8)+sleep 3, then + * GPIOCMD EN mode 0 (not output-high). + */ + rail = d1830_nimbus_rail(true); + if (rail) + dev_warn(&n->spi->dev, "20766 PMIC rail: %d\n", rail); + else { + /* 66A8(8) is 345D40 thunk (0x220002B2), not an 8ms sleep */ + msleep(3); + } + /* 20766(1): EN mode 0, val 0. Do not cmd-15 the latch. */ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_EN, 0, 0); + nimbus_dump_pad(n, NIMBUS_GPIO_EN, "en-mode0"); + msleep(15); + /* 20690(1) after EN — OSOS order; required after 1A878 unmux */ + nimbus_spi2_pinmux(n, true); + msleep(5); + /* 1A5AC: 11B70 after remux, still in reset, before 20848 */ + nimbus_spi2_11b70(n); + nimbus_dump_pad(n, NIMBUS_GPIO_EN, "en"); + nimbus_dump_pad(n, NIMBUS_GPIO_RST, "rst"); + nimbus_dump_pad(n, NIMBUS_GPIO_IRQ, "irq"); + nimbus_dump_pad(n, 87, "spi2-87"); + nimbus_dump_pad(n, 88, "spi2-88"); + nimbus_dump_pad(n, 89, "spi2-89"); + nimbus_dump_pad(n, 90, "spi2-90"); +} + +static void nimbus_gpio_release_reset(struct nimbus *n) +{ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 1); + msleep(reset_release_ms); + nimbus_dump_pad(n, NIMBUS_GPIO_RST, "rst-rel"); +} + +/* sub_20490(1) — GPIOCMD input + EIC enable for GPIO 38 */ +static void nimbus_irq_enable(struct nimbus *n) +{ + nimbus_gpiocmd_mode(n, NIMBUS_GPIO_IRQ, 0, 0); + /* RetailOS: level, active-low → VIC EXT1 */ + if (s5l8740_eic_enable_gpio(NIMBUS_GPIO_IRQ, IRQ_TYPE_LEVEL_LOW) == 0) + dev_info(&n->spi->dev, "EIC enabled GPIO%d level-low\n", + NIMBUS_GPIO_IRQ); +} + +static int nimbus_1a5ac_and_download(struct nimbus *n, const u8 *data, + size_t size, const char *tag) +{ + int err; + + nimbus_gpio_por_reset(n); + nimbus_gpio_bringup(n); + err = nimbus_bootload_cmd(n); + if (err) + dev_warn(&n->spi->dev, "bootload %s: %d\n", tag, err); + msleep(15); + nimbus_gpio_release_reset(n); + if (skip_download) { + dev_info(&n->spi->dev, "skip_download — no 2D640\n"); + return 0; + } + if (nimbus_probe_26494(n, tag)) { + msleep(30); + if (nimbus_probe_26494(n, tag)) { + dev_warn(&n->spi->dev, "26494 %s failed\n", tag); + return -EIO; + } + } + err = nimbus_download_fw(n, data, size, false); + n->fw_tried = true; + return err; +} + +/* + * 1703E8: 10 failed pings → 13A20(0) then 13A20(1). Cap recycles so a + * stuck bootloader does not spam forever (GO ACKs but app never runs). + */ +#define NIMBUS_RECYCLE_MAX 3 + +static void nimbus_park(struct nimbus *n, const char *why) +{ + if (n->parked) + return; + n->parked = true; + n->stopped = true; + nimbus_verbose = false; + dev_warn(&n->spi->dev, + "nimbus parked (%s) — rmmod/insmod to retry\n", why); + nimbus_power_down(n); +} + +static void nimbus_recycle(struct nimbus *n) +{ + const struct firmware *fw = NULL; + u16 st = 0; + + if (n->parked) + return; + if (n->recycle_count >= NIMBUS_RECYCLE_MAX) { + nimbus_park(n, "recycle budget"); + return; + } + n->recycle_count++; + dev_info(&n->spi->dev, + "1703E8 10 ping fails — 13A20 recycle %u/%u\n", + n->recycle_count, NIMBUS_RECYCLE_MAX); + nimbus_power_down(n); + n->fw_loaded = false; + n->fw_tried = false; + n->spi_ok = false; + n->ping_fails = 0; + msleep(50); + if (request_firmware(&fw, "apple/grape-nimbus.bin", &n->spi->dev) || + !fw) { + dev_warn(&n->spi->dev, "recycle: no grape-nimbus.bin\n"); + nimbus_park(n, "no firmware"); + return; + } + nimbus_1a5ac_and_download(n, fw->data, fw->size, "recycle"); + release_firmware(fw); + msleep(2); + if (!nimbus_ping(n, &st)) { + n->spi_ok = true; + dev_info(&n->spi->dev, "recycle ping ok, status=0x%04x\n", st); + } else if (n->recycle_count >= NIMBUS_RECYCLE_MAX) { + nimbus_park(n, "still bootloader after GO"); + } +} + +static void nimbus_service(struct nimbus *n) +{ + u16 st = 0; + + if (n->parked) + return; + /* 188FFC: ping 490 (5 tries) then 17E404. Never 1A A1 after FW. */ + if (nimbus_ping(n, &st) == 0) { + n->spi_ok = true; + n->ping_fails = 0; + if (st) + nimbus_read_reports(n, st); + return; + } + n->ping_fails++; + if (nimbus_verbose && (n->ping_fails <= 3 || n->ping_fails == 10)) + dev_info(&n->spi->dev, + "188FFC ping still fail (%u) attn=%d\n", + n->ping_fails, + n->attn ? gpiod_get_value_cansleep(n->attn) : -1); +} + +static irqreturn_t nimbus_irq_thread(int irq, void *data) +{ + struct nimbus *n = data; + + mutex_lock(&n->lock); + if (!n->stopped) + nimbus_service(n); + mutex_unlock(&n->lock); + return IRQ_HANDLED; +} + +static void nimbus_try_firmware(struct nimbus *n) +{ + const struct firmware *fw = NULL; + int err; + + if (n->fw_tried || n->fw_loaded) + return; + /* Don't hammer SPI with 60KB download if ping already fails */ + if (!n->spi_ok) { + u16 st; + + if (nimbus_ping(n, &st)) + return; + n->spi_ok = true; + } + + err = request_firmware(&fw, "apple/grape-nimbus.bin", &n->spi->dev); + if (err || !fw) + return; + + n->fw_tried = true; + nimbus_download_fw(n, fw->data, fw->size, false); + release_firmware(fw); +} + +static int nimbus_poll_thread(void *data) +{ + struct nimbus *n = data; + int wait; + int fail_backoff_ms = 50; + + for (wait = 0; wait < 30 && !n->fw_loaded && !n->fw_tried && + !kthread_should_stop(); wait++) { + mutex_lock(&n->lock); + nimbus_try_firmware(n); + mutex_unlock(&n->lock); + if (!n->fw_loaded && !n->fw_tried) + msleep(1000); + } + if (!n->fw_loaded && !n->fw_tried) { + n->fw_tried = true; + dev_info(&n->spi->dev, + "no apple/grape-nimbus.bin — bootload+ping only\n"); + } + + while (!kthread_should_stop()) { + bool do_poll = true; + int attn = -1; + + /* + * 1703E8/188FFC runs every MultitouchTask loop, not only + * on attn. Gating on attn skipped ping if the app released + * GPIO 38 after 2D54C (bootloader holds it low). + */ + if (n->attn) + attn = gpiod_get_value_cansleep(n->attn); + do_poll = true; + + mutex_lock(&n->lock); + if (n->parked || n->stopped) { + mutex_unlock(&n->lock); + /* Parked: sleep until kthread_stop; no SPI traffic. */ + msleep(500); + continue; + } + if (do_poll) { + nimbus_service(n); + if (n->ping_fails >= 10) + nimbus_recycle(n); + fail_backoff_ms = n->spi_ok ? 50 : + min(fail_backoff_ms * 2, 2000); + } + mutex_unlock(&n->lock); + + if (!do_poll) + msleep(20); + else if (!n->spi_ok) + msleep(fail_backoff_ms); + else + msleep(50); + } + return 0; +} + +static int nimbus_probe(struct spi_device *spi) +{ + struct nimbus *n; + struct input_dev *input; + const struct firmware *fw = NULL; + u16 ping_st = 0; + int err; + + n = devm_kzalloc(&spi->dev, sizeof(*n), GFP_KERNEL); + if (!n) + return -ENOMEM; + n->spi = spi; + n->blob16 = false; + nimbus_verbose = !quiet; + mutex_init(&n->lock); + spi_set_drvdata(spi, n); + + n->gpio_base = devm_ioremap(&spi->dev, S5L8740_GPIO_PHYS, 0x400); + n->gpiocmd = devm_ioremap(&spi->dev, S5L8740_GPIOCMD_PHYS, 4); + n->spi2 = devm_ioremap(&spi->dev, S5L8740_SPI2_PHYS, 0x80); + if (!n->gpio_base || !n->gpiocmd || !n->spi2) + return -ENOMEM; + + n->enable = devm_gpiod_get_optional(&spi->dev, "enable", GPIOD_ASIS); + if (IS_ERR(n->enable)) + return PTR_ERR(n->enable); + n->reset = devm_gpiod_get_optional(&spi->dev, "reset", GPIOD_ASIS); + if (IS_ERR(n->reset)) + return PTR_ERR(n->reset); + n->attn = devm_gpiod_get_optional(&spi->dev, "attn", GPIOD_IN); + if (IS_ERR(n->attn)) + return PTR_ERR(n->attn); + + err = request_firmware(&fw, "apple/grape-nimbus.bin", &spi->dev); + if (err || !fw) + dev_warn(&spi->dev, "grape-nimbus.bin missing (%d)\n", err); + + { + int attempt; + + /* + * 13A20(1): first try is 1A5AC only. 1A878 + sleep 50 + * only after a failed 1A5AC, max 3. remove() already + * 1A878s on reload. + */ + for (attempt = 0; attempt < 3 && fw; attempt++) { + if (attempt) { + nimbus_power_down(n); + msleep(50); + } + /* 0xEE is iOS3 Z2-only; N31 wake is 19 C1 in reset. */ + mutex_lock(&n->lock); + nimbus_1a5ac_and_download(n, fw->data, fw->size, + attempt ? "retry" : "1A5AC"); + { + u16 st = 0; + + /* 20E94: sleep 2 after 273A0; 1703E8 pings next. */ + msleep(2); + err = nimbus_ping(n, &ping_st); + if (!err) { + n->spi_ok = true; + dev_info(&spi->dev, + "ping ok, status=0x%04x\n", + ping_st); + } else { + nimbus_peek(n, "post-go-fail"); + if (nimbus_status_poll(n, &st) == 0) + dev_info(&spi->dev, + "post-go status 0x%04x\n", + st); + } + } + mutex_unlock(&n->lock); + /* 20E94 does not 1A878 after a successful 273A0. */ + if (n->fw_loaded) + break; + } + } + if (fw) + release_firmware(fw); + dev_info(&spi->dev, "fw download attempted, fw_loaded=%d spi_ok=%d\n", + n->fw_loaded, n->spi_ok); + + if (n->fw_loaded || n->spi_ok) { + /* 1A5AC: 20490 after 20E94, then MultitouchTask 1703E8/188FFC. */ + nimbus_irq_enable(n); + n->irq = spi->irq; + if (n->irq > 0) { + err = devm_request_threaded_irq(&spi->dev, n->irq, NULL, + nimbus_irq_thread, + IRQF_ONESHOT | IRQF_TRIGGER_LOW, + "nimbus", n); + if (err) { + dev_warn(&spi->dev, + "VIC IRQ %d request failed %d — attn poll\n", + n->irq, err); + n->irq = -1; + } else { + n->use_irq = true; + dev_info(&spi->dev, + "IRQ-driven (VIC irq %d + EIC GPIO%d)\n", + n->irq, NIMBUS_GPIO_IRQ); + } + } + } else { + dev_info(&spi->dev, "SPI not talking — IRQ/poll parked\n"); + } + + input = devm_input_allocate_device(&spi->dev); + if (!input) + return -ENOMEM; + n->input = input; + input->name = "Apple Nimbus"; + input->phys = "nimbus/input0"; + input->id.bustype = BUS_SPI; + __set_bit(INPUT_PROP_DIRECT, input->propbit); + __set_bit(BTN_TOUCH, input->keybit); + input_set_abs_params(input, ABS_MT_POSITION_X, 0, NIMBUS_ABS_X_MAX, 0, 0); + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, NIMBUS_ABS_Y_MAX, 0, 0); + err = input_mt_init_slots(input, NIMBUS_SLOTS, INPUT_MT_DIRECT); + if (err) + return err; + err = input_register_device(input); + if (err) + return err; + + /* + * FW chunk ACK alone is not enough — RDREG still shows bootloader + * after GO. Only run MultitouchTask when ping works; otherwise park + * so we do not recycle forever while MtCl/cal is unfinished. + */ + if (n->spi_ok) { + if (ping_st) { + mutex_lock(&n->lock); + nimbus_read_reports(n, ping_st); + mutex_unlock(&n->lock); + } + n->thread = kthread_run(nimbus_poll_thread, n, "nimbus-poll"); + if (IS_ERR(n->thread)) { + dev_warn(&spi->dev, + "nimbus-poll kthread %ld — poll via IRQ only\n", + PTR_ERR(n->thread)); + n->thread = NULL; + } + } else if (n->fw_loaded) { + nimbus_park(n, "GO left chip in bootloader"); + } + + if (n->parked) + dev_info_once(&spi->dev, "Nimbus parked (touch offline)\n"); + else + dev_info(&spi->dev, "Nimbus up (attn=%d spi_ok=%d)\n", + !!n->attn, n->spi_ok); + return 0; +} + +static void nimbus_remove(struct spi_device *spi) +{ + struct nimbus *n = spi_get_drvdata(spi); + + n->stopped = true; + if (n->thread) + kthread_stop(n->thread); + nimbus_power_down(n); +} + +static const struct of_device_id nimbus_of_match[] = { + { .compatible = "apple,nimbus" }, + { } +}; +MODULE_DEVICE_TABLE(of, nimbus_of_match); + +static struct spi_driver nimbus_driver = { + .driver = { + .name = "apple-nimbus", + .of_match_table = nimbus_of_match, + }, + .probe = nimbus_probe, + .remove = nimbus_remove, +}; +module_spi_driver(nimbus_driver); + +MODULE_DESCRIPTION("Apple Nimbus/Grape multitouch (N31 SPI2, RetailOS-matched)"); +MODULE_LICENSE("GPL"); +MODULE_FIRMWARE("apple/grape-nimbus.bin"); diff --git a/drivers/irqchip/Kconfig b/drivers/irqchip/Kconfig index cb191cf0679d74..215047cfce1d48 100644 --- a/drivers/irqchip/Kconfig +++ b/drivers/irqchip/Kconfig @@ -56,15 +56,6 @@ config ARM_NVIC select IRQ_DOMAIN_HIERARCHY select GENERIC_IRQ_CHIP -config S5L8740_EIC - bool "S5L8740 GPIO External Interrupt Controller" - depends on OF && ARM_VIC - select IRQ_DOMAIN - help - GPIO EIC at 0x39700000 on Apple S5L8740 (iPod nano 7G). - Groups map to PL192 VIC EXT lines. Chain only the parents - listed in the device tree. - config ARM_VIC bool select IRQ_DOMAIN @@ -771,3 +762,10 @@ config SUNPLUS_SP7021_INTC the primary controller on C-Chip. endmenu + +config S5L8740_EIC + bool "Samsung/Apple S5L8740 GPIO EIC" + depends on ARM || COMPILE_TEST + select IRQ_DOMAIN + help + External interrupt controller at 0x39700000 feeding PL192 VIC EXTn. diff --git a/drivers/irqchip/Makefile b/drivers/irqchip/Makefile index 7819656cb715d4..41becb0b032e73 100644 --- a/drivers/irqchip/Makefile +++ b/drivers/irqchip/Makefile @@ -38,7 +38,6 @@ obj-$(CONFIG_PARTITION_PERCPU) += irq-partition-percpu.o obj-$(CONFIG_HISILICON_IRQ_MBIGEN) += irq-mbigen.o obj-$(CONFIG_ARM_NVIC) += irq-nvic.o obj-$(CONFIG_ARM_VIC) += irq-vic.o -obj-$(CONFIG_S5L8740_EIC) += irq-s5l8740-eic.o obj-$(CONFIG_ARMADA_370_XP_IRQ) += irq-armada-370-xp.o obj-$(CONFIG_ATMEL_AIC_IRQ) += irq-atmel-aic-common.o irq-atmel-aic.o obj-$(CONFIG_ATMEL_AIC5_IRQ) += irq-atmel-aic-common.o irq-atmel-aic5.o @@ -130,3 +129,4 @@ obj-$(CONFIG_IRQ_IDT3243X) += irq-idt3243x.o obj-$(CONFIG_APPLE_AIC) += irq-apple-aic.o obj-$(CONFIG_MCHP_EIC) += irq-mchp-eic.o obj-$(CONFIG_SUNPLUS_SP7021_INTC) += irq-sp7021-intc.o +obj-$(CONFIG_S5L8740_EIC) += irq-s5l8740-eic.o diff --git a/drivers/irqchip/irq-vic.c b/drivers/irqchip/irq-vic.c index ea93e7236c4ac9..b4f707c38f47f3 100644 --- a/drivers/irqchip/irq-vic.c +++ b/drivers/irqchip/irq-vic.c @@ -89,9 +89,14 @@ static void vic_init2(void __iomem *base) { int i; - for (i = 0; i < 16; i++) { - void __iomem *reg = base + VIC_VECT_CNTL0 + (i * 4); - writel(VIC_VECT_CNTL_ENABLE | i, reg); + /* + * PL190 has 16 vectored slots; PL192 has 32. Linux used to program + * only 0..15, so sources 16..31 (N31 IIC is family 21/22) kept the + * bootloader VECTADDR. Zero every slot, then enable all 32. + */ + for (i = 0; i < 32; i++) { + writel(0, base + VIC_VECT_ADDR0 + (i * 4)); + writel(VIC_VECT_CNTL_ENABLE | i, base + VIC_VECT_CNTL0 + (i * 4)); } writel(32, base + VIC_PL190_DEF_VECT_ADDR); @@ -391,6 +396,9 @@ static void __init vic_clear_interrupts(void __iomem *base) value = readl(base + VIC_PL190_VECT_ADDR); writel(value, base + VIC_PL190_VECT_ADDR); } + /* PL192 EOI is +0xF00. U-Boot/SEC may have taken a vector without EOI. */ + for (i = 0; i < 32; i++) + writel(0, base + VIC_PL192_VECT_ADDR); } /* diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 56bc72c7ce4a99..8e9d6ba2684da6 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -650,3 +650,33 @@ source "drivers/misc/pvpanic/Kconfig" source "drivers/misc/mchp_pci1xxxx/Kconfig" source "drivers/misc/keba/Kconfig" endmenu + +config FMSS_S5L8740 + tristate "S5L8740 FMSS NAND (N31 WhimoryPPN CS path)" + depends on HAS_IOMEM + help + Flash Memory Subsystem driver for iPod nano 7G (S5L8740). + Provides CS DMA page reads, META weave/LBA scan, and sysfs + hooks used by the FTL bring-up path. + +config FTL_S5L8740 + tristate "S5L8740 FTL helper (N31)" + depends on FMSS_S5L8740 + help + Higher-level FTL helper layered on FMSS_S5L8740 for N31 + restore / LBA mapping experiments. + +config APPLE_TRISTAR_CBTL1609 + tristate "Apple Lightning Tristar (CBTL1609A1) mux" + depends on I2C + help + I2C driver for NXP CBTL1609A1 Lightning charge/data mux (Tristar) + on iPod nano 7G. Lives under misc because N31 is gadget-only + (CONFIG_USB host is off, so drivers/usb/misc is invisible). + +config S5L8740_IIS2_MMIO + tristate "S5L8740 IIS2 FM MMIO hook (N31)" + depends on HAS_IOMEM + help + IIS2 @0x3D400000 FM digital RX hook — MMIO regs sysfs only. + Capture PCM register model still OPEN per N31 RE. diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index 545aad06d08856..f4df4f82c213b6 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -75,3 +75,7 @@ lan966x-pci-objs := lan966x_pci.o lan966x-pci-objs += lan966x_pci.dtbo.o obj-$(CONFIG_MCHP_LAN966X_PCI) += lan966x-pci.o obj-y += keba/ +obj-$(CONFIG_APPLE_TRISTAR_CBTL1609) += apple-tristar-cbtl1609.o +obj-$(CONFIG_S5L8740_IIS2_MMIO) += s5l8740-iis2-mmio.o +obj-$(CONFIG_FMSS_S5L8740) += fmss-s5l8740.o +obj-$(CONFIG_FTL_S5L8740) += ftl-s5l8740.o diff --git a/drivers/misc/apple-tristar-cbtl1609.c b/drivers/misc/apple-tristar-cbtl1609.c new file mode 100755 index 00000000000000..e37d9d2e91a905 --- /dev/null +++ b/drivers/misc/apple-tristar-cbtl1609.c @@ -0,0 +1,351 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Apple Lightning Tristar mux — NXP CBTL1609A1 (iPod nano 7G / N31) + * + * Public “0x34 write / 0x35 read” is 8-bit; Linux 7-bit address is 0x1a. + * THS7383 Dx/ACCx pin tables are public (nyansatan); CBTL1609 I2C indices + * that program them are still OPEN in public docs. RetailOS RE shows + * **zero** Dx/mux register writes — dump is flat until accessory attaches. + * Only apple,init-sequence from DT may write. UDC soft reconnect is done + * from initramfs via sysfs udc soft_connect. + */ +#include +#include +#include +#include +#include + +#define TRISTAR_DUMP_LEN 0x40 + +struct apple_tristar { + struct i2c_client *client; + u8 last_dump[TRISTAR_DUMP_LEN]; + u8 read_reg; + u8 read_val; + bool dump_ok; + bool dump_flat; +}; + +static int tristar_read_reg(struct apple_tristar *ts, u8 reg, u8 *val) +{ + int ret = i2c_smbus_read_byte_data(ts->client, reg); + + if (ret < 0) + return ret; + *val = (u8)ret; + return 0; +} + +static int tristar_write_reg(struct apple_tristar *ts, u8 reg, u8 val) +{ + return i2c_smbus_write_byte_data(ts->client, reg, val); +} + +static bool tristar_dump_is_flat(const u8 *dump, size_t len) +{ + size_t i; + + for (i = 1; i < len; i++) { + if (dump[i] != dump[0]) + return false; + } + return true; +} + +static int tristar_dump(struct apple_tristar *ts) +{ + int i, ret; + u8 v; + + for (i = 0; i < TRISTAR_DUMP_LEN; i++) { + ret = tristar_read_reg(ts, i, &v); + if (ret) { + dev_warn(&ts->client->dev, + "read 0x%02x failed: %d\n", i, ret); + ts->dump_ok = false; + return ret; + } + ts->last_dump[i] = v; + } + + ts->dump_ok = true; + ts->dump_flat = tristar_dump_is_flat(ts->last_dump, TRISTAR_DUMP_LEN); + + dev_dbg(&ts->client->dev, + "CBTL1609 dump[0..0x3f] on %s:\n", + ts->client->adapter->name); + dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump); + dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 16); + dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 32); + dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 48); + return 0; +} + +static int tristar_apply_init_sequence(struct apple_tristar *ts) +{ + struct device *dev = &ts->client->dev; + struct device_node *np = dev->of_node; + int n, i, ret; + u32 reg, val; + + if (!np) + return 0; + + n = of_property_count_u32_elems(np, "apple,init-sequence"); + if (n <= 0) + return 0; + if (n % 2) { + dev_err(dev, "apple,init-sequence must be reg,val pairs\n"); + return -EINVAL; + } + + for (i = 0; i < n; i += 2) { + of_property_read_u32_index(np, "apple,init-sequence", i, ®); + of_property_read_u32_index(np, "apple,init-sequence", i + 1, &val); + ret = tristar_write_reg(ts, (u8)reg, (u8)val); + if (ret) { + dev_err(dev, "init write 0x%02x=0x%02x failed: %d\n", + reg, val, ret); + return ret; + } + dev_dbg(dev, "init 0x%02x <= 0x%02x\n", reg, val); + udelay(100); + } + return 0; +} + +/* + * Mode heuristic from dump only — no invented mux map. + * Flat dump → unknown; non-flat → "active" (register diversity seen). + */ +static const char *tristar_mode_name(struct apple_tristar *ts) +{ + if (!ts->dump_ok) + return "unknown"; + if (ts->dump_flat) + return "unknown"; + return "active"; +} + +static ssize_t dump_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + int i, n = 0; + + if (tristar_dump(ts)) + return -EIO; + for (i = 0; i < TRISTAR_DUMP_LEN; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, "%02x%s", + ts->last_dump[i], + (i + 1) % 16 ? " " : "\n"); + return n; +} +static DEVICE_ATTR_RO(dump); + +static ssize_t poke_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + unsigned int reg, val; + int ret; + + if (sscanf(buf, "%x %x", ®, &val) != 2) + return -EINVAL; + if (reg > 0xff || val > 0xff) + return -EINVAL; + ret = tristar_write_reg(ts, reg, val); + if (ret) + return ret; + dev_info(dev, "poke 0x%02x <= 0x%02x\n", reg, val); + return count; +} +static DEVICE_ATTR_WO(poke); + +static ssize_t mode_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + + if (tristar_dump(ts)) + return sysfs_emit(buf, "unknown\n"); + return sysfs_emit(buf, "%s\n", tristar_mode_name(ts)); +} +static DEVICE_ATTR_RO(mode); + +static ssize_t read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + unsigned int reg; + u8 val; + int ret; + + if (kstrtouint(buf, 0, ®) || reg > 0xff) + return -EINVAL; + ret = tristar_read_reg(ts, (u8)reg, &val); + if (ret) + return ret; + ts->read_reg = (u8)reg; + ts->read_val = val; + return count; +} + +static ssize_t read_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + + return sysfs_emit(buf, "0x%02x\n", ts->read_val); +} +static DEVICE_ATTR_RW(read); + +static ssize_t value_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + + return sysfs_emit(buf, "0x%02x (reg 0x%02x)\n", + ts->read_val, ts->read_reg); +} +static DEVICE_ATTR_RO(value); + +static ssize_t verify_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + + if (tristar_dump(ts)) + return sysfs_emit(buf, "FAIL read\n"); + if (ts->dump_flat) + return sysfs_emit(buf, "FAIL flat 0x%02x\n", ts->last_dump[0]); + return sysfs_emit(buf, "STATUS_OK non-flat\n"); +} +static DEVICE_ATTR_RO(verify); + +static ssize_t poll_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + u8 prior[TRISTAR_DUMP_LEN]; + unsigned int i, deltas = 0; + int ret; + + memcpy(prior, ts->last_dump, sizeof(prior)); + ret = tristar_dump(ts); + if (ret) + return ret; + + for (i = 0; i < TRISTAR_DUMP_LEN; i++) { + if (prior[i] != ts->last_dump[i]) + deltas++; + } + + dev_info(dev, "Tristar poll: flat=%d deltas=%u mode=%s (mux map OPEN)\n", + ts->dump_flat, deltas, tristar_mode_name(ts)); + return count; +} + +static ssize_t poll_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + + return sysfs_emit(buf, + "flat=%d last_ok=%d — echo 1 > poll to re-dump\n", + ts->dump_flat, ts->dump_ok); +} +static DEVICE_ATTR_RW(poll); + +static struct attribute *tristar_attrs[] = { + &dev_attr_dump.attr, + &dev_attr_poke.attr, + &dev_attr_mode.attr, + &dev_attr_read.attr, + &dev_attr_value.attr, + &dev_attr_verify.attr, + &dev_attr_poll.attr, + NULL, +}; +ATTRIBUTE_GROUPS(tristar); + +static int apple_tristar_probe(struct i2c_client *client) +{ + struct apple_tristar *ts; + struct device *dev = &client->dev; + int ret; + u8 id0 = 0xff; + + ts = devm_kzalloc(dev, sizeof(*ts), GFP_KERNEL); + if (!ts) + return -ENOMEM; + ts->client = client; + i2c_set_clientdata(client, ts); + + /* + * I2C RX still returns the address byte (DS=0x31/0xe1). Do not + * require a read ACK at probe — that either fails the bind or + * storms IIC0. U-Boot DFU already routed Lightning USB. Only + * apple,init-sequence may write; RetailOS has no Dx mux map. + */ + if (of_property_read_bool(dev->of_node, "apple,require-ack")) { + ret = tristar_read_reg(ts, 0x00, &id0); + if (ret) { + dev_err(dev, + "Tristar no ACK at 7-bit 0x%02x on %s (err=%d)\n", + client->addr, client->adapter->name, ret); + return -ENODEV; + } + dev_info(dev, + "Lightning Tristar ACK @7bit=0x%02x on %s reg0=0x%02x\n", + client->addr, client->adapter->name, id0); + } else { + dev_info(dev, + "Tristar bound @7bit=0x%02x on %s (skip ACK)\n", + client->addr, client->adapter->name); + } + + /* Full 64-byte dump deferred to sysfs (poll/dump) — avoid boot I2C storm */ + + /* DT-only init — never invent mux register writes in driver */ + tristar_apply_init_sequence(ts); + + ret = sysfs_create_groups(&dev->kobj, tristar_groups); + if (ret) + dev_warn(dev, "sysfs groups failed: %d\n", ret); + + return 0; +} + +static void apple_tristar_remove(struct i2c_client *client) +{ + sysfs_remove_groups(&client->dev.kobj, tristar_groups); +} + +static const struct of_device_id apple_tristar_of_match[] = { + { .compatible = "apple,tristar-cbtl1609" }, + { .compatible = "nxp,cbtl1609a1" }, + { }, +}; +MODULE_DEVICE_TABLE(of, apple_tristar_of_match); + +static const struct i2c_device_id apple_tristar_id[] = { + { "tristar-cbtl1609" }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, apple_tristar_id); + +static struct i2c_driver apple_tristar_driver = { + .driver = { + .name = "apple-tristar", + .of_match_table = apple_tristar_of_match, + }, + .probe = apple_tristar_probe, + .remove = apple_tristar_remove, + .id_table = apple_tristar_id, +}; +module_i2c_driver(apple_tristar_driver); + +MODULE_DESCRIPTION("Apple Lightning Tristar / NXP CBTL1609A1 mux"); +MODULE_AUTHOR("Hydrogenuine / FreeMyiPod N31 bring-up"); +MODULE_LICENSE("GPL"); diff --git a/drivers/misc/fmss-s5l8740-api.h b/drivers/misc/fmss-s5l8740-api.h new file mode 100755 index 00000000000000..f4775c434c7611 --- /dev/null +++ b/drivers/misc/fmss-s5l8740-api.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * S5L8740 FMSS → FTL block layer export (Whimory read path). + * Consumed by ftl-s5l8740.ko; implemented by fmss-s5l8740.ko. + * + * Logical disk: 4096-byte sectors, LPN = sector >> 2 (4 sectors / 16 KiB page). + * Build the dense L2V via sysfs l2v_build (or fmss_ftl_build_map); sector 0 is + * served from a carved *UOKJIHC BPB when present. Unmapped sectors return -ENOENT. + */ +#ifndef FMSS_S5L8740_API_H +#define FMSS_S5L8740_API_H + +#include +#include + +/* Apple RetailOS FAT32 on N31 (4096-byte logical sectors). Override via module param. */ +#define FMSS_FTL_SECTOR_SIZE 4096U +#define FMSS_FTL_SECTORS_PER_LPN 4U +#define FMSS_FTL_DEFAULT_CAPACITY 3856968U + +bool fmss_ftl_present(void); +struct device *fmss_ftl_device(void); +unsigned int fmss_ftl_lpn_count(void); +int fmss_ftl_build_map(unsigned int max_lpn); +int fmss_ftl_read_sector(u64 logical_sector, void *buf); + +#endif /* FMSS_S5L8740_API_H */ diff --git a/drivers/misc/fmss-s5l8740.c b/drivers/misc/fmss-s5l8740.c new file mode 100755 index 00000000000000..86e539067d96d6 --- /dev/null +++ b/drivers/misc/fmss-s5l8740.c @@ -0,0 +1,5177 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 FMSS/FMC NAND peek — N31 + * + * Stage A: dump controller regs and issue OSOS READ ID (cmd 0x90). + * Stage B: PPN FIL page read (sub_50D960 / 12ECCC). 3 x 1024 PIO. + * Does not write NAND array data. Do not call the 10453C reset + * sequence from probe — peek first. Do not port 50DC34 (program) + * or 4ED258 (erase). + * + * Cookbook (RetailOS 1.0.2): + * 41C738: *(0x38A00008) = cmd + * 4F11F4: READ ID — FMCTRL0 CE bit, cmd 0x90, addr via 106594, + * 8 bytes via 41AE38 / D622C @ +0x80 + * 50D960: page read — cmd 0x0A, addr, cmd 0x37, 4F1CE8 ready, + * cmd 0x7A, 3 x (optional 53-byte parity + 1024 data) + * D6388: PIO data port is +0x80 (write path; unused here) + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fmss-seq-read.h" +#include "fmss-s5l8740-api.h" +#include "whimory-ftl.h" + +#define FMSS_PHYS 0x38A00000ul +#define FMSS_SIZE 0x1000 + +#define FMCTRL0 0x00 +#define FMCTRL1 0x04 +#define FMCMD 0x08 +#define FMADDR 0x0c +#define FMCE 0x14 +#define FMUNK18 0x18 +#define FMUNK24 0x24 +#define FMUNK28 0x28 +#define FMCYCLES 0x2c +#define FMLEN 0x30 +#define FMUNK38 0x38 +#define FMSTAT48 0x48 +#define NANDSTAT 0x4c +#define FMDATA 0x80 +#define FMSEQ 0xc00 +#define FMSEQBASE 0xc04 +#define FMSEQSTAT 0xc08 +#define FMSEQIRQ 0xc0c +#define FMGEN0 0xd00 +#define FMGEN1 0xd04 +#define FMGEN2 0xd08 +#define FMGEN3 0xd0c +#define FMGEN4 0xd10 +#define FMGEN5 0xd14 +#define FMUNK81C 0x81c + +#define FMSS_DMA_STATUS_LEN 512 +#define FMSS_DMA_CMDLIST_LEN 256 +#define FMSS_DMA_SPARE_LEN 256 +#define FMSS_SECTOR_LEN 4096 + +#define FMSS_MAX_CHUNKS 16 +#define FMSS_CHUNK 1024 +#define FMSS_PAGE_LEN (FMSS_MAX_CHUNKS * FMSS_CHUNK) +#define FMSS_PARAM_LEN 0x200 + +/* + * PPN address packing — OSOS 5173CA / Sogeti PPNVFL: + * page | (block << page_bits) | (cau << (page_bits+block_bits)) | (slc << ...) + * Param page: page_bits=7, block_bits=12, cau_bits=1. + * VFL context lives in the last ~5% of each CAU, SLC page 0 + * (spare type 0x20, index 0xFFFF). SFTL context spare type 0x1F. + */ +#define FMSS_PAGE_BITS 7 +#define FMSS_BLOCK_BITS 12 +#define FMSS_CAU_BITS 1 +#define FMSS_BLOCKS_PER_CAU 2088 +#define FMSS_NUM_CE 2 +#define FMSS_NUM_CAU 2 +#define FMSS_VFL_TAIL 128 +#define FMSS_VFL_HDR_LEN 512 +#define FMSS_BTOC_PAGE 127 +#define FMSS_VBAS_PER_PAGE 4 /* s_g_vbas_per_page: 16 KiB / 4096 */ +#define FMSS_LPN_INDEX_MAX 512 +#define FMSS_GREP_MAX_BLOCKS 64 +#define FMSS_VFL_MAP_MAX 512 +#define FMSS_L2V_DEFAULT_BLOCKS 256 +/* Map is keyed by page LPN (YaFTL); early LBAs also in early_lba_map. */ +#define FMSS_L2V_DEFAULT_MAX_LPN \ + ((FMSS_FTL_DEFAULT_CAPACITY / FMSS_FTL_SECTORS_PER_LPN) + 64) + +/* Classic Whimory mount (freemyipod) adapted for N31 PPN geometry. */ +#define WMR_PAGES_PER_BLOCK 128u +#define WMR_BLOCK_MAP_MAX 16384u +#define WMR_MOUNT_MAX_BLOCKS 64u +#define WMR_FTLCTRL_MAX 3u + +/* Packed L2V entry: valid|ce[1:0]|cau[1:0]|sec[1:0]|block[11:0]|page[6:0] + * sec = 4K index within the NAND page (SFTL VBA). 0x3 = “use LBA%4”. + */ +#define L2V_VALID BIT(31) +#define L2V_CE_SHIFT 29 +#define L2V_CAU_SHIFT 27 +#define L2V_SEC_SHIFT 19 +#define L2V_SEC_MASK 0x3u +#define L2V_SEC_FROM_LBA 0x3u /* sentinel: derive sec from lba%4 */ +#define L2V_BLOCK_SHIFT 7 +#define L2V_PAGE_MASK 0x7fu +#define L2V_BLOCK_MASK 0xfffu + +struct fmss_vfl_map { + unsigned int cau; + unsigned int virt; + unsigned int phys; +}; + +struct fmss_lpn_map { + unsigned int lpn; + unsigned int ce; + unsigned int cau; + unsigned int block; + unsigned int page; +}; + +static char vfl_log[PAGE_SIZE]; +static unsigned int vfl_log_len; +static char sector_log[512]; +static unsigned int sector_log_len; +static unsigned int lpn_scan_blocks = 512; +module_param(lpn_scan_blocks, uint, 0644); +MODULE_PARM_DESC(lpn_scan_blocks, "max CAU blocks to scan for lpn_read (default 512)"); + +static unsigned int l2v_scan_blocks = FMSS_L2V_DEFAULT_BLOCKS; +module_param(l2v_scan_blocks, uint, 0644); +MODULE_PARM_DESC(l2v_scan_blocks, + "default block count per CE/CAU for l2v_build (default 256)"); + +static unsigned int grep_max_blocks = 64; +module_param(grep_max_blocks, uint, 0644); +MODULE_PARM_DESC(grep_max_blocks, "max blocks per ftl_grep call (default 64)"); + +static int quiet = 1; +module_param(quiet, int, 0644); +MODULE_PARM_DESC(quiet, "1=minimal logs, skip ECC diag (faster FTL scans, default on)"); + +#define fmss_info(fmt, ...) \ + do { if (!quiet) pr_info("s5l8740-fmss: " fmt, ##__VA_ARGS__); } while (0) + +#define fmss_dev_info(dev, fmt, ...) \ + do { if (!quiet) dev_info(dev, fmt, ##__VA_ARGS__); } while (0) + +static unsigned int vfl_build_blocks = 32; +module_param(vfl_build_blocks, uint, 0644); +MODULE_PARM_DESC(vfl_build_blocks, "max blocks per CAU for vfl_build (default 32, tail only)"); + +/* Tiny list for sysfs lpn_index (debug); dense map is authoritative. */ +static struct fmss_lpn_map lpn_index[FMSS_LPN_INDEX_MAX]; +static unsigned int lpn_index_count; + +/* Dense LPN → physical page map (vzalloc). */ +static u32 *l2v_map; +static unsigned int l2v_map_size; +static unsigned int l2v_mapped; +static unsigned int l2v_max_lpn; +static unsigned int l2v_btoc_hits; +static unsigned int l2v_bmap_hits; +static unsigned int l2v_meta_hits; + +/* Carved Apple FAT boot (*UOKJIHC); sector 0 served from here. */ +static bool boot_carve_valid; +static unsigned int boot_carve_ce; +static unsigned int boot_carve_cau; +static unsigned int boot_carve_block; +static unsigned int boot_carve_page; +static unsigned int boot_carve_off; +/* From carved BPB; reserved area must not be served from a poisoned L2V[0]. */ +static unsigned int boot_reserved_sects = 32; +static unsigned int boot_data_start = 1916; +static unsigned int boot_fat_sects = 942; /* FATSz32 from live BPB */ + +/* + * Optional cached aligned boot page (page must have BPB at offset 0). + * Defaults 0 = rediscover via BTOC(0,1) + aligned BPB. Do NOT cache the + * mid-page *UOKJIHC @ blk63/pg88/off7816 — that is a file copy, not LBA0. + */ +static unsigned int boot_carve_ce_param; +static unsigned int boot_carve_cau_param; +static unsigned int boot_carve_block_param; /* 0 = scan */ +static unsigned int boot_carve_page_param; +static unsigned int boot_carve_off_param; /* must be 0 for a real boot page */ +module_param_named(boot_carve_ce, boot_carve_ce_param, uint, 0644); +module_param_named(boot_carve_cau, boot_carve_cau_param, uint, 0644); +module_param_named(boot_carve_block, boot_carve_block_param, uint, 0644); +module_param_named(boot_carve_page, boot_carve_page_param, uint, 0644); +module_param_named(boot_carve_off, boot_carve_off_param, uint, 0644); +MODULE_PARM_DESC(boot_carve_block, + "cached aligned boot block (0=BTOC hunt for LPN0)"); + +/* Default l2v_build width when sysfs omits NBLOCKS (user blocks to scan). */ +static unsigned int l2v_auto_blocks = 512; +module_param(l2v_auto_blocks, uint, 0644); +MODULE_PARM_DESC(l2v_auto_blocks, + "default l2v_build block span per CE/CAU (0=carve/boot hunt only)"); + +/* Root-dir physical page when LPN(DataStart) is otherwise unmapped. */ +static bool root_dir_valid; +static unsigned int root_dir_ce; +static unsigned int root_dir_cau; +static unsigned int root_dir_block; +static unsigned int root_dir_page; +static unsigned int root_dir_lpn; + +static struct fmss_vfl_map vfl_map[FMSS_VFL_MAP_MAX]; +static unsigned int vfl_map_count; +static unsigned int vfl_ctx_cau[FMSS_NUM_CAU]; +static unsigned int vfl_ctx_block[FMSS_NUM_CAU]; + +/* Classic Whimory FTL block map + mount status (Stage C). */ +static u16 *wmr_block_map; +static unsigned int wmr_block_map_n; +static unsigned int wmr_dis_hits; +static unsigned int wmr_vfl_hits; +static unsigned int wmr_ftlctrl_hits; +static unsigned int wmr_bmap_pages; +static unsigned int wmr_l2v_filled; +static int wmr_mount_ret; +static u16 wmr_ftlctrl[WMR_FTLCTRL_MAX]; +static unsigned int wmr_ftlctrl_n; +static unsigned int wmr_dis_ce, wmr_dis_cau, wmr_dis_block, wmr_dis_page; + +static u32 fmss_ppn_addr(unsigned int cau, unsigned int block, + unsigned int page, unsigned int slc) +{ + return page + | (block << FMSS_PAGE_BITS) + | (cau << (FMSS_PAGE_BITS + FMSS_BLOCK_BITS)) + | ((slc ? 1u : 0u) << (FMSS_PAGE_BITS + FMSS_BLOCK_BITS + FMSS_CAU_BITS)); +} + +struct fmss_n31 { + void __iomem *base; + struct mutex lock; + u8 last_id[8]; + int last_ce; + u8 last_page[FMSS_PAGE_LEN]; + u8 last_spare[64]; + unsigned int last_spare_len; + u8 last_parity[FMSS_MAX_CHUNKS][64]; + unsigned int last_parity_len[FMSS_MAX_CHUNKS]; + u32 last_page_addr; + int last_page_ce; + int last_page_ret; + int last_page_chunk; + unsigned int last_page_len; + u8 last_param[FMSS_PARAM_LEN]; + int last_param_ce; + int last_param_ret; + u32 last_stat48; + u32 last_nandstat; + unsigned int pages_since_reset; + int dma_ok; + int dma_mapped; + struct device *dev; + void *seq; + void *cmdl; + void *data; + void *spare; + void *stbuf; + dma_addr_t seq_dma; + dma_addr_t cmdl_dma; + dma_addr_t data_dma; + dma_addr_t spare_dma; + dma_addr_t stbuf_dma; + u32 last_dma_c0c; + u32 last_dma_d00; + u32 last_dma_c00; + int irq; + struct completion cs_irq; + u32 last_vic_raw; + u32 last_vic_en; +}; + +static struct fmss_n31 *fmss_dev; + +/* 12ED9C: PPN >= 0x10500 uses 4 address cycles. */ +static unsigned int addr_cycles = 4; +module_param(addr_cycles, uint, 0644); +MODULE_PARM_DESC(addr_cycles, "FMSS address cycles (OSOS 8D102EC, default 4)"); + +/* 12ECCC calls 50D960(..., 1): hardware eats 53-byte parity before each 1K. */ +static bool with_parity = true; +module_param(with_parity, bool, 0644); +MODULE_PARM_DESC(with_parity, "Issue 50D960 parity transfer (default Y)"); + +static bool use_dma; +module_param(use_dma, bool, 0644); +MODULE_PARM_DESC(use_dma, "Use FMSS DMA page read when dma_ok (default N — DMA spare path still OPEN on N31)"); + +/* 4EC6F4: MEMORY[0x8980CA4] = 0xFF000, OR'd into FMCTRL0 for ID/param/features. */ +static unsigned int ctrl0_or = 0xFF000; +module_param(ctrl0_or, uint, 0644); +MODULE_PARM_DESC(ctrl0_or, "FMCTRL0 extra bits for ID/param/features (default 0xFF000)"); + +/* + * FFE70 timing row 171 MHz: CA4 = 0x20011000. 50D960 uses this, not 0xFF000. + * 0xFF000 for array reads returns zeros and wedges PPN. + */ +static unsigned int page_ctrl0_or = 0x20011000; +module_param(page_ctrl0_or, uint, 0644); +MODULE_PARM_DESC(page_ctrl0_or, "FMCTRL0 extra bits for 50D960 page read (171MHz timing)"); + +/* 50D960 wedges PPN after many reads; re-run 10453C+0xFF+power-state. */ +static unsigned int reset_every = 6; +module_param(reset_every, uint, 0644); +MODULE_PARM_DESC(reset_every, "nand_reset after this many page_read calls (0=off)"); + +/* 50D960 uses 3; full PPN page is 16 x 1K data (+64 spare not in this PIO). */ +static unsigned int page_chunks = 16; +module_param(page_chunks, uint, 0644); +MODULE_PARM_DESC(page_chunks, "1K PIO chunks per page_read (16=full 16KiB data)"); + +static unsigned int spare_len = 16; +module_param(spare_len, uint, 0644); +MODULE_PARM_DESC(spare_len, "extra PIO bytes after data (OSOS meta is 16)"); + +/* + * 4EDDDC: D14 = (v40 ? 8D102F0 : 8D102EC) - 1. + * v40 = (8D10300 != 1) — PPN multi-cycle path uses 8, so D14=7. + */ +static unsigned int dma_d14 = 7; +module_param(dma_d14, uint, 0644); +MODULE_PARM_DESC(dma_d14, "FMSS D14 address-cycles-1 for DMA (default 7 = PPN v40)"); + +/* One 4096-byte host sector first (oracle G1); raise to 4 for full 16K page. */ +static unsigned int dma_nsect = 1; +module_param(dma_nsect, uint, 0644); +MODULE_PARM_DESC(dma_nsect, "DMA span (# logical LBAs) per CS read (default 1)"); + +/* + * PPN physical page = N × (4096 DATA + 16 META) records (N=2 or 4). + * 5172A0: qword.lo = (rec*span) | ((rec*slot) << 16); qword.hi = encoded_ppn. + */ +#define FMSS_PPN_REC 4112u /* 4096 + 16 */ +static unsigned int dma_slot; +module_param(dma_slot, uint, 0644); +MODULE_PARM_DESC(dma_slot, "starting logical slot within PPN page (0..3, default 0)"); + +static unsigned int dma_rec = FMSS_PPN_REC; +module_param(dma_rec, uint, 0644); +MODULE_PARM_DESC(dma_rec, "PPN on-flash record bytes (default 4112 = 4096+16)"); + +/* + * 4EDDDC v40 packing with qword=(0,ppn): addr in cmdlist[3]. + * 1 = force addr into cmdlist[2] (experiment). Default 0. + */ +static unsigned int dma_row_in_lo; +module_param(dma_row_in_lo, uint, 0644); +MODULE_PARM_DESC(dma_row_in_lo, "1=addr in cmdlist[2]; 0=v40 high dword in [3] (default)"); + +static unsigned int dma_c6c = 16; +module_param(dma_c6c, uint, 0644); +MODULE_PARM_DESC(dma_c6c, "value written to +0xC6C before DMA kick (default 16)"); + +/* D39EC: MEMORY[C00] = 65525 = 0xFFF5 (NOT 0xFFFD). */ +static unsigned int dma_kick = 0xfff5; +module_param(dma_kick, uint, 0644); +MODULE_PARM_DESC(dma_kick, "FMSEQ (C00) kick value (OSOS D39EC = 0xFFF5)"); + +static bool dma_dry; +module_param(dma_dry, bool, 0644); +MODULE_PARM_DESC(dma_dry, "program CS regs/descriptors but do not write C00 (default N)"); + +/* + * D39EC: if 0x8982448 then C6C=0 + pulse C60; else C6C=16. + * Pulse never raised C64 on glass. Default matches the else path. + */ +static bool dma_pulse; +module_param(dma_pulse, bool, 0644); +MODULE_PARM_DESC(dma_pulse, "pulse +0xC60 before DMA kick (D39EC if-path)"); + +/* + * Seq blob[0..31] is CS ops. CPU-poking C0C=0xFF is W1C of FMSEQIRQ and + * made wait_cs see 0xFF&0xD==0xD. Leave off unless debugging C10/C58/C4C. + */ +static bool dma_preamble; +module_param(dma_preamble, bool, 0644); +MODULE_PARM_DESC(dma_preamble, "CPU-write C10/C58/C4C from seq header (not C0C)"); + +/* OSOS D39B8: interrupt 54 = VIC1 hwirq 22 = Linux irq 70 on this GATE0 map. */ +static int dma_irq = 70; +module_param(dma_irq, int, 0644); +MODULE_PARM_DESC(dma_irq, "Linux IRQ for FMSS CS (OSOS 54 / VIC1 22, default 70)"); + +static u32 fmss_ctrl0(unsigned int ce) +{ + return ctrl0_or | (2u * (1u << ce)) | 1u; +} + +static u32 fmss_page_ctrl0(unsigned int ce) +{ + return page_ctrl0_or | (2u * (1u << ce)) | 1u; +} + +/* 1858DC: poll FMSTAT48 bit(s), then W1C. */ +static int fmss_wait48_n(struct fmss_n31 *f, u32 mask, unsigned int loops) +{ + unsigned int i; + u32 st; + + for (i = 0; i < loops; i++) { + st = readl(f->base + FMSTAT48); + if (st & mask) { + if (mask == 0x800000u) + writel(32, f->base + FMCTRL1); + writel(mask, f->base + FMSTAT48); + return 0; + } + udelay(1); + } + f->last_stat48 = readl(f->base + FMSTAT48); + f->last_nandstat = readl(f->base + NANDSTAT); + writel(mask, f->base + FMSTAT48); + return -ETIMEDOUT; +} + +static int fmss_wait48(struct fmss_n31 *f, u32 mask) +{ + return fmss_wait48_n(f, mask, 20000); +} + +static int fmss_cmd(struct fmss_n31 *f, u8 cmd) +{ + writel(cmd, f->base + FMCMD); + return fmss_wait48(f, 2); +} + +/* D622C: drain PIO FIFO at +0x80. Must read +0x80 first (OSOS + live ID). */ +static int fmss_pio_read(struct fmss_n31 *f, void *dst, unsigned int len) +{ + u8 *p = dst; + unsigned int n = 0, words = len >> 2, spins = 0; + u32 word; + + writel(3, f->base + FMLEN); + writel(1u << 8, f->base + FMCE); + writel(readl(f->base + FMCTRL0) & 0xFEFFFBFF, f->base + FMCTRL0); + writel(0, f->base + FMUNK28); + writel(480, f->base + FMCTRL1); + + while (n < words && spins < 20000u * (words + 1)) { + word = readl(f->base + FMDATA); + if ((u8)readl(f->base + FMUNK28) == (u8)(4 * n + 4)) { + memcpy(p + 4 * n, &word, 4); + n++; + } + spins++; + cpu_relax(); + } + writel(0x100000, f->base + FMSTAT48); + writel(32, f->base + FMCTRL1); + return n == words ? 0 : -ETIMEDOUT; +} + +/* 41AE38: NAND→controller beat then D622C PIO. len <= M2_BYTES_PER_SECTOR. */ +static int fmss_data_in(struct fmss_n31 *f, void *dst, unsigned int len) +{ + if (len < 1 || len > 0x400) + return -EINVAL; + writel(len - 1, f->base + FMLEN); + writel(1, f->base + FMCE); + writel(0, f->base + FMUNK24); + writel(34, f->base + FMCTRL1); + if (fmss_wait48(f, 8)) { + pr_info("s5l8740-fmss: data_in wait48(8) timeout len=%u st=%08x\n", + len, f->last_stat48); + return -ETIMEDOUT; + } + return fmss_pio_read(f, dst, len); +} + +/* 41DA70: wait NANDSTAT ready after 0x77/0x7D. */ +static int fmss_wait_status(struct fmss_n31 *f, unsigned int loops, u8 *nandstat) +{ + int ret; + + writel(0x800000u, f->base + FMSTAT48); + writel(234, f->base + FMCTRL1); + ret = fmss_wait48_n(f, 0x800000u, loops); + if (nandstat) + *nandstat = (u8)readl(f->base + NANDSTAT); + writel(0x1000000u, f->base + FMSTAT48); + return ret; +} + +static int fmss_addr_n(struct fmss_n31 *f, u32 addr, unsigned int cycles) +{ + if (cycles < 1 || cycles > 8) + cycles = 1; + writel(cycles - 1, f->base + FMCYCLES); + writel(addr, f->base + FMADDR); + writel(1, f->base + FMCTRL1); + return fmss_wait48(f, 4); +} + +static int fmss_addr1(struct fmss_n31 *f, u32 addr) +{ + return fmss_addr_n(f, addr, 1); +} + +/* + * D6388 + 112A7C: PIO write to +0x80 then kick. Used only for SET FEATURES + * (NAND device registers), never for array program (50DC34). + */ +static int fmss_pio_write(struct fmss_n31 *f, const void *src, unsigned int len) +{ + const u32 *p = src; + unsigned int i, words; + + if (len < 4 || (len & 3) || len > 0x400) + return -EINVAL; + words = len >> 2; + writel(readl(f->base + FMCTRL0) | 0x02000000, f->base + FMCTRL0); + writel(readl(f->base + FMCTRL0) & 0xFEFFFBFF, f->base + FMCTRL0); + writel(1, f->base + FMCE); + writel(0, f->base + FMUNK24); + writel(736, f->base + FMCTRL1); + for (i = 0; i < words; i++) + writel(p[i], f->base + FMDATA); + writel(0x100000, f->base + FMSTAT48); + writel(32, f->base + FMCTRL1); + writel(readl(f->base + FMCTRL0) & ~0x02000000u, f->base + FMCTRL0); + + writel(len - 1, f->base + FMLEN); + writel(256, f->base + FMCE); + writel(65764, f->base + FMCTRL1); + return fmss_wait48(f, 8); +} + +/* 1303B4: PPN SET FEATURES (cmd 0xEF). */ +static int fmss_set_feature(struct fmss_n31 *f, unsigned int ce, u16 feat, u32 val) +{ + u8 st = 0; + u32 feat_word = feat; + int ret = 0; + + if (ce > 7) + return -EINVAL; + writel((2u * (1u << ce)) | 0xFF001u, f->base + FMCTRL0); + if (fmss_cmd(f, 0xef) || fmss_addr_n(f, feat_word, 2) || + fmss_pio_write(f, &val, 4)) + ret = -ETIMEDOUT; + fmss_cmd(f, 0xe7); + fmss_cmd(f, 0x77); + fmss_cmd(f, 0x7d); + if (fmss_wait_status(f, 100000, &st)) + ret = -ETIMEDOUT; + fmss_cmd(f, 0x77); + writel(1, f->base + FMCTRL0); + fmss_info("set_feature ce=%u feat=0x%04x val=0x%08x ret=%d nandstat=%02x\n", + ce, feat, val, ret, st); + return ret; +} + +/* 41CEDC: PPN GET FEATURES (cmd 0xEE). */ +static u8 last_feat[16]; +static u16 last_feat_id; +static int last_feat_ce = -1; +static int last_feat_ret = -1; + +static int fmss_get_feature(struct fmss_n31 *f, unsigned int ce, u16 feat, + void *dst, unsigned int len) +{ + u8 st = 0; + u32 feat_word = feat; + u32 saved; + int ret = 0; + + if (ce > 7 || len < 4 || len > 0x200) + return -EINVAL; + memset(dst, 0, len); + saved = readl(f->base + FMCTRL0); + writel((2u * (1u << ce)) | 0xFF001u, f->base + FMCTRL0); + if (fmss_cmd(f, 0xee) || fmss_addr_n(f, feat_word, 2)) + ret = -ETIMEDOUT; + fmss_cmd(f, 0xe7); + fmss_cmd(f, 0x77); + fmss_cmd(f, 0x7d); + if (fmss_wait_status(f, 100000, &st)) + ret = -ETIMEDOUT; + else { + fmss_cmd(f, 0x7a); + if (fmss_data_in(f, dst, len)) + ret = -ETIMEDOUT; + fmss_cmd(f, 0x77); + } + writel(saved, f->base + FMCTRL0); + fmss_info("get_feature ce=%u feat=0x%04x ret=%d nandstat=%02x %02x %02x %02x %02x\n", + ce, feat, ret, st, + ((u8 *)dst)[0], ((u8 *)dst)[1], ((u8 *)dst)[2], ((u8 *)dst)[3]); + return ret; +} + +/* sub_4F11F4(ce, addr=0, buf): READ ID, no 10453C reset. */ +static int fmss_read_id(struct fmss_n31 *f, unsigned int ce) +{ + u8 *dst = f->last_id; + + if (ce > 7) + return -EINVAL; + + memset(dst, 0, 8); + /* Drop leftover ready bits so 1858DC does not return immediately. */ + writel(0x0e, f->base + FMSTAT48); + + writel(fmss_ctrl0(ce), f->base + FMCTRL0); + fmss_cmd(f, 0x90); + + if (fmss_addr1(f, 0)) + pr_info("s5l8740-fmss: wait48(4) after addr timed out st=%08x\n", + readl(f->base + FMSTAT48)); + + if (fmss_data_in(f, dst, 8)) { + f->last_ce = (int)ce; + return -ETIMEDOUT; + } + writel(1, f->base + FMCTRL0); + + f->last_ce = (int)ce; + return 0; +} + +/* 4F1CE8: cmd 0x77/0x7D then wait NAND ready (STAT48 bit 0x800000). */ +static int fmss_wait_ready(struct fmss_n31 *f) +{ + int ret; + + fmss_cmd(f, 0x77); + fmss_cmd(f, 0x7d); + writel(1, f->base + FMCE); + writel(234, f->base + FMCTRL1); + ret = fmss_wait48_n(f, 0x800000u, 200000); + writel(32, f->base + FMCTRL1); + writel(0x800000u, f->base + FMSTAT48); + if (ret) + pr_info("s5l8740-fmss: ready timeout NANDSTAT=%08x STAT48=%08x\n", + f->last_nandstat, f->last_stat48); + return ret; +} + +static bool ecc_before_drain = true; +module_param(ecc_before_drain, bool, 0644); +MODULE_PARM_DESC(ecc_before_drain, + "Run OSOS 4EB458 ECC/descramble before PIO drain (default Y)"); + +/* + * 0 = diagnostic sub_50D960: FMLEN=52 FMCE=16 CTRL1=34 then data FMCE=1 + * 1 = production-seq style: FMLEN=15 FMCE=0x102 CTRL1=0x1E2, data FMCE=0x201 +0x18=2 + */ +static unsigned int xfer_style; +module_param(xfer_style, uint, 0644); +MODULE_PARM_DESC(xfer_style, + "0=50D960 parity52 (default), 1=DMA-seq-like meta15/CTRL1=0x1E2"); + +static unsigned int ecc_op = 0x1e1; +module_param(ecc_op, uint, 0644); +MODULE_PARM_DESC(ecc_op, + "low bits OR'd into +0x804 for 4EB458 (default 0x1E1; encode uses 0x1E2)"); + +/* OSOS 50D960 uses 16; sweep if UECC persists. */ +static unsigned int parity_fmce = 16; +module_param(parity_fmce, uint, 0644); +MODULE_PARM_DESC(parity_fmce, "FMCE value for 52-byte parity beat (default 16)"); + +static unsigned int data_fmce = 1; +module_param(data_fmce, uint, 0644); +MODULE_PARM_DESC(data_fmce, "FMCE value for 1024-byte data beat (default 1)"); + +static unsigned int xfer_ctrl1 = 34; +module_param(xfer_ctrl1, uint, 0644); +MODULE_PARM_DESC(xfer_ctrl1, "FMCTRL1 for parity/data beats (default 34)"); + +/* + * OSOS sub_4EB458(a1=0, len=1024): kick FMSS ECC/descramble engine. + * Must run AFTER parity+data transfer waits, BEFORE FIFO drain (sub_D622C). + * + * Exact RetailOS order — do NOT clear +0x810 before kick (preload stays + * 0x3f1f73af). Poll bit0 of +0x810 for completion; if preload already has + * bit0, treat as synchronous after the +0x804 write (first read). + * + * Returns 0 OK, 1 clean/erased page, -ETIMEDOUT / -EIO on hard fail. + */ +static int fmss_ecc_chunk(struct fmss_n31 *f, unsigned int seed_a1) +{ + u32 ecc, hist, st; + unsigned int t; + + writel(1, f->base + 0x81c); + writel(0x04000400u, f->base + 0x818); + writel(1, f->base + 0x80c); + writel(0x3f1f73afu, f->base + 0x810); + writel(0x80000000u, f->base + 0x820); + writel(0x02000000u, f->base + 0x808); + writel(seed_a1 | (1024u << 16) | (ecc_op & 0xfffu), f->base + 0x804); + + /* Kick may clear bit0; poll until set again (or already set). */ + for (t = 0; t < 20000; t++) { + st = readl(f->base + 0x810); + if (st & 1u) + break; + udelay(1); + } + if (t >= 20000) { + pr_info("s5l8740-fmss: 4EB458 timeout st=%08x\n", st); + return -ETIMEDOUT; + } + ecc = readl(f->base + 0x80c); + hist = readl(f->base + 0x818); + writel(1, f->base + 0x810); + if (!quiet) + fmss_info("4EB458 ecc=0x%08x hist=0x%08x t=%u st=%08x op=%x\n", + ecc, hist, t, st, ecc_op); + if (ecc & 1u) + return -EIO; + if (ecc & 2u) + return 1; + return 0; +} + +/* + * sub_50D960(ce, page_addr, buf, with_parity). + * Per 1KiB chunk: parity beat → data xfer wait → 4EB458 → PIO drain. + * Linux previously drained before ECC — that left DATA pages whitened. + */ +static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) +{ + int i, ret = -EIO, ecc_ret; + u8 *dst = f->last_page; + unsigned int cycles = addr_cycles; + unsigned int chunks = page_chunks; + + if (ce > 7) + return -EINVAL; + if (cycles < 1 || cycles > 8) + cycles = 4; + if (chunks < 1 || chunks > FMSS_MAX_CHUNKS) + chunks = 3; + + memset(dst, 0, FMSS_PAGE_LEN); + f->last_page_ce = (int)ce; + f->last_page_addr = addr; + f->last_page_chunk = -1; + f->last_page_len = 0; + + writel(7, f->base + FMUNK38); + writel(fmss_page_ctrl0(ce), f->base + FMCTRL0); + writel(0x0FF00FFE, f->base + FMSTAT48); + writel(1, f->base + FMUNK81C); + fmss_cmd(f, 0x0a); + + writel(cycles - 1, f->base + FMCYCLES); + writel(addr, f->base + FMADDR); + writel(1, f->base + FMCTRL1); + if (fmss_wait48(f, 4)) { + pr_info("s5l8740-fmss: addr cycle timeout ce=%u addr=%08x st=%08x nand=%08x\n", + ce, addr, f->last_stat48, f->last_nandstat); + goto fail_ctrl0; + } + + if (fmss_cmd(f, 0x37)) { + pr_info("s5l8740-fmss: cmd 0x37 timeout ce=%u addr=%08x st=%08x\n", + ce, addr, f->last_stat48); + goto fail_ctrl0; + } + + if (fmss_wait_ready(f)) { + pr_info("s5l8740-fmss: not ready after read cmd ce=%u addr=%08x\n", + ce, addr); + goto fail_ctrl0; + } + + fmss_cmd(f, 0x7a); + for (i = 0; i < (int)chunks; i++) { + f->last_page_chunk = i; + f->last_parity_len[i] = 0; + writel(0x08000000, f->base + FMSTAT48); + if (with_parity) { + if (xfer_style == 1) { + writel(15, f->base + FMLEN); + writel(0x102, f->base + FMCE); + writel(1, f->base + FMUNK18); + writel(0, f->base + FMUNK24); + writel(0x1e2, f->base + FMCTRL1); + } else { + /* OSOS 50D960 parity: FMLEN=52 FMCE=16 +0x24=0 CTRL1=34 + * — do not touch +0x18 or +0x28 here. + */ + writel(52, f->base + FMLEN); + writel(parity_fmce, f->base + FMCE); + writel(0, f->base + FMUNK24); + writel(xfer_ctrl1, f->base + FMCTRL1); + } + if (fmss_wait48(f, 8)) { + pr_info("s5l8740-fmss: parity xfer timeout ce=%u addr=%08x chunk=%d st=%08x style=%u\n", + ce, addr, i, f->last_stat48, xfer_style); + goto fail_ctrl0; + } + /* + * Live: after FMCE=16 FMLEN=52, D622C can read bytes + * (head often 02 …) — parity lands on the DATA FIFO. + * Never drain it when ecc_before_drain=1 (OSOS leaves + * it for 4EB458). Optional strip only when ECC off. + */ + if (xfer_style == 0 && !ecc_before_drain) { + u8 dig[64]; + + memset(dig, 0, sizeof(dig)); + if (!fmss_pio_read(f, dig, 53)) { + memcpy(f->last_spare, dig, 53); + f->last_spare_len = 53; + if (!quiet) + fmss_info("stripped parity-fifo %02x%02x%02x%02x…\n", + dig[0], dig[1], dig[2], dig[3]); + } + } + } + writel(1023, f->base + FMLEN); + if (xfer_style == 1) { + writel(2, f->base + FMUNK18); + writel(0x201, f->base + FMCE); + writel(0, f->base + FMUNK24); + writel(0x1e2, f->base + FMCTRL1); + } else { + writel(1, f->base + FMUNK18); + writel(data_fmce, f->base + FMCE); + writel(0, f->base + FMUNK24); + writel(xfer_ctrl1, f->base + FMCTRL1); + } + if (fmss_wait48(f, 8)) { + pr_info("s5l8740-fmss: data xfer timeout ce=%u addr=%08x chunk=%d st=%08x nand=%08x style=%u\n", + ce, addr, i, f->last_stat48, f->last_nandstat, + xfer_style); + goto fail_ctrl0; + } + /* OSOS: ECC/descramble BEFORE FIFO drain when parity path used. */ + if (with_parity && ecc_before_drain) { + ecc_ret = fmss_ecc_chunk(f, 0); + if (ecc_ret == 1) { + /* Clean page — leave zeros already in dst. */ + f->last_page_ret = 0; + f->last_page_len = chunks * FMSS_CHUNK; + fmss_cmd(f, 0x77); + writel(0, f->base + FMCTRL0); + return 0; + } + if (ecc_ret) { + /* Fall through to raw drain — better than wedge. */ + pr_info("s5l8740-fmss: ECC soft-fail ce=%u addr=%08x chunk=%d ret=%d (raw drain)\n", + ce, addr, i, ecc_ret); + } + } + if (fmss_pio_read(f, dst + i * FMSS_CHUNK, FMSS_CHUNK)) { + pr_info("s5l8740-fmss: PIO drain timeout ce=%u addr=%08x chunk=%d\n", + ce, addr, i); + goto fail_ctrl0; + } + } + + memset(f->last_spare, 0, sizeof(f->last_spare)); + f->last_spare_len = 0; + if (spare_len && spare_len <= sizeof(f->last_spare)) { + if (fmss_data_in(f, f->last_spare, spare_len)) { + pr_info("s5l8740-fmss: spare timeout ce=%u addr=%08x len=%u\n", + ce, addr, spare_len); + } else { + f->last_spare_len = spare_len; + } + } + + fmss_cmd(f, 0x77); + writel(0, f->base + FMCTRL0); + f->last_page_ret = 0; + f->last_page_len = chunks * FMSS_CHUNK; + return 0; + +fail_ctrl0: + writel(0, f->base + FMCTRL0); + f->last_page_ret = ret; + return ret; +} + +/* + * OSOS 4EDDDC / D39EC: FMSS command-list DMA page read. + * Sequence program is the OSOS blob at 0x8980EA0 (embedded). + * 16-byte PPN spare per 4K sector lands in the spare DMA buffer + * (Sogeti type/bank/weaveSeq/lpn). Status bytes go to stbuf. + */ +static int fmss_wait_cs(struct fmss_n31 *f, unsigned int loops) +{ + unsigned int i; + u32 irq; + + for (i = 0; i < loops; i++) { + irq = readl(f->base + FMSEQIRQ); + if ((irq & 0xd) == 1) + return 0; + if (irq & 0xc) { + f->last_dma_c0c = irq; + return -EIO; + } + udelay(10); + } + f->last_dma_c0c = readl(f->base + FMSEQIRQ); + return -ETIMEDOUT; +} + +static void fmss_dma_teardown(struct fmss_n31 *f) +{ + struct device *dev = f->dev; + + if (f->irq > 0) { + free_irq(f->irq, f); + f->irq = 0; + } + if (dev) { + if (f->seq) + dma_free_coherent(dev, FMSS_SEQ_READ_LEN, f->seq, f->seq_dma); + if (f->cmdl) + dma_free_coherent(dev, FMSS_DMA_CMDLIST_LEN, f->cmdl, f->cmdl_dma); + if (f->data) + dma_free_coherent(dev, FMSS_PAGE_LEN, f->data, f->data_dma); + if (f->spare) + dma_free_coherent(dev, FMSS_DMA_SPARE_LEN, f->spare, f->spare_dma); + if (f->stbuf) + dma_free_coherent(dev, FMSS_DMA_STATUS_LEN, f->stbuf, f->stbuf_dma); + } + f->seq = f->cmdl = f->data = f->spare = f->stbuf = NULL; + f->dma_ok = 0; + f->dma_mapped = 0; +} + +static irqreturn_t fmss_cs_irq(int irq, void *data) +{ + struct fmss_n31 *f = data; + + f->last_dma_c0c = readl(f->base + FMSEQIRQ); + complete(&f->cs_irq); + return IRQ_HANDLED; +} + +static void fmss_peek_vic1(struct fmss_n31 *f) +{ + void __iomem *vic1; + + vic1 = ioremap(0x38e01000ul, 0x20); + if (!vic1) + return; + f->last_vic_raw = readl(vic1 + 0x08); + f->last_vic_en = readl(vic1 + 0x10); + iounmap(vic1); +} + +static int fmss_dma_setup(struct fmss_n31 *f, struct device *dev) +{ + int ret; + + ret = dma_coerce_mask_and_coherent(dev, DMA_BIT_MASK(32)); + if (ret) + dev_warn(dev, "dma mask: %d\n", ret); + f->dev = dev; + init_completion(&f->cs_irq); + + f->seq = dma_alloc_coherent(dev, FMSS_SEQ_READ_LEN, &f->seq_dma, GFP_KERNEL); + f->cmdl = dma_alloc_coherent(dev, FMSS_DMA_CMDLIST_LEN, &f->cmdl_dma, GFP_KERNEL); + f->data = dma_alloc_coherent(dev, FMSS_PAGE_LEN, &f->data_dma, GFP_KERNEL); + f->spare = dma_alloc_coherent(dev, FMSS_DMA_SPARE_LEN, &f->spare_dma, GFP_KERNEL); + f->stbuf = dma_alloc_coherent(dev, FMSS_DMA_STATUS_LEN, &f->stbuf_dma, GFP_KERNEL); + if (!f->seq || !f->cmdl || !f->data || !f->spare || !f->stbuf) { + fmss_dev_info(dev, "DMA coherent alloc failed, PIO only\n"); + fmss_dma_teardown(f); + return -ENOMEM; + } + memcpy(f->seq, fmss_seq_read_blob, FMSS_SEQ_READ_LEN); + memset(f->cmdl, 0, FMSS_DMA_CMDLIST_LEN); + memset(f->data, 0, FMSS_PAGE_LEN); + memset(f->spare, 0, FMSS_DMA_SPARE_LEN); + memset(f->stbuf, 0, FMSS_DMA_STATUS_LEN); + f->dma_mapped = 1; + f->dma_ok = 1; + + if (dma_irq > 0) { + ret = request_irq(dma_irq, fmss_cs_irq, 0, "fmss-cs", f); + if (ret) { + dev_warn(dev, "CS IRQ %d: %d (polling C0C)\n", dma_irq, ret); + f->irq = 0; + } else { + f->irq = dma_irq; + fmss_dev_info(dev, "CS IRQ %d (OSOS 54 / VIC1 22)\n", f->irq); + } + } + + fmss_dev_info(dev, "DMA seq_phys=0x%08lx cmdl=0x%08lx data=0x%08lx seq0=%02x %02x %02x %02x coherent=1\n", + (unsigned long)f->seq_dma, (unsigned long)f->cmdl_dma, + (unsigned long)f->data_dma, + ((u8 *)f->seq)[0], ((u8 *)f->seq)[1], + ((u8 *)f->seq)[2], ((u8 *)f->seq)[3]); + return 0; +} + +static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) +{ + u32 *cl; + u32 ce_bit; + unsigned int nsect = dma_nsect; + unsigned int i, spare_bytes; + u8 *dp; + int ret; + u32 dregs[32]; + + if (!f->dma_ok) + return -ENODEV; + if (ce > 7 || nsect < 1 || nsect > 4) + return -EINVAL; + + /* Refresh CS microcode + wipe all coherent targets (device-visible). */ + memcpy(f->seq, fmss_seq_read_blob, FMSS_SEQ_READ_LEN); + memset(f->data, 0, FMSS_PAGE_LEN); + memset(f->spare, 0, FMSS_DMA_SPARE_LEN); + memset(f->stbuf, 0, FMSS_DMA_STATUS_LEN); + memset(f->cmdl, 0, FMSS_DMA_CMDLIST_LEN); + + cl = f->cmdl; + ce_bit = 1u << (16 + ce); + + /* + * 4EDDDC one-page descriptor list: + * desc0: CE-select dword0=1<<(ce+16), later |0x80000000 if last for CE + * dword2/3 = packed physical address (v40: low in [2]) + * desc1: transfer dword0=(1<<(ce+16))|1, [1]=span, [2]=meta, [3]=data + * term: 0x00010002 + */ + /* + * 4EDDDC address qword from 5172A0 (READ, v40 / multi-LBA page): + * lo = (rec * span) | ((rec * slot) << 16) // length | column<<16 + * hi = encoded_ppn (5173CA, mode 0) + * desc[2]=lo, desc[3]=hi when dma_d14>=7 (v40). + */ + cl[0] = ce_bit; + cl[1] = 0; + { + unsigned int span = nsect; + unsigned int slot = dma_slot; + u32 rec = dma_rec ? dma_rec : FMSS_PPN_REC; + u32 col_len; + + if (slot > 3) + slot = 3; + if (span < 1) + span = 1; + if (span > 4) + span = 4; + if (slot + span > 4) + span = 4 - slot; + col_len = (rec * span) | ((rec * slot) << 16); + + if (dma_row_in_lo) { + /* experiment: put ppn in lo (broken for normal path) */ + cl[2] = addr; + cl[3] = (addr >> 31); + } else if (dma_d14 >= 7) { + cl[2] = col_len; + cl[3] = addr; + } else { + /* !v40: only HIDWORD consumed as ppn; keep col_len in lo anyway */ + cl[2] = addr; + cl[3] = 0; + } + fmss_info("dma slice slot=%u span=%u rec=%u col_len=%08x ppn=%08x\n", + slot, span, rec, col_len, addr); + } + /* Post-pass: OR bit31 onto last CE-select for each CE present. */ + cl[0] |= 0x80000000u; + cl[4] = ce_bit | 1u; + cl[5] = nsect; + cl[6] = (u32)f->spare_dma; + cl[7] = (u32)f->data_dma; + cl[8] = 0x00010002u; + + if (dma_pulse) { + unsigned int t; + + writel(0, f->base + 0xc6c); + writel(1, f->base + 0xc60); + for (t = 0; t < 200000; t++) { + if (readl(f->base + 0xc64) == 1) + break; + udelay(1); + } + writel(0, f->base + 0xc60); + } else { + writel(dma_c6c, f->base + 0xc6c); + } + + if (dma_preamble) { + writel(1, f->base + 0xc10); + writel(4, f->base + 0xc58); + writel(0x0b00, f->base + 0xc4c); + } + + /* 4EDDDC register skeleton — bus addresses only. */ + writel(page_ctrl0_or, f->base + FMGEN1); /* D04 timing template */ + writel((u32)f->cmdl_dma, f->base + FMGEN2); /* D08 descriptor list */ + writel(FMSS_SECTOR_LEN, f->base + FMGEN3); /* D0C = 4096 */ + writel((u32)f->stbuf_dma, f->base + FMGEN4); /* D10 status */ + writel(dma_d14, f->base + FMGEN5); /* D14 = addr_cycles-1 */ + writel((u32)f->seq_dma, f->base + FMSEQBASE); /* C04 = seq program */ + /* + * Do NOT poke +0x81C here — that is 4EB458 ECC, not in 4EDDDC/D39EC. + * A spurious 81C write before CS previously correlated with SoC wedges. + */ + /* D39EC: C00 = 0xFFF5. Do NOT use 0x80000 (reset) here. */ + reinit_completion(&f->cs_irq); + fmss_info("dma kick ce=%u addr=%08x seq=%08x cmdl=%08x data=%08x meta=%08x st=%08x d14=%u kick=%04x dry=%d\n", + ce, addr, (u32)f->seq_dma, (u32)f->cmdl_dma, (u32)f->data_dma, + (u32)f->spare_dma, (u32)f->stbuf_dma, dma_d14, dma_kick, dma_dry); + if (dma_dry) { + f->last_dma_c0c = readl(f->base + FMSEQIRQ); + f->last_dma_d00 = readl(f->base + FMGEN0); + f->last_dma_c00 = readl(f->base + FMSEQ); + ret = -EAGAIN; + goto dma_done; + } + writel(dma_kick, f->base + FMSEQ); + + /* + * Prefer short poll of C0C. IRQ wait alone can hang the process context + * if VIC routing is wrong; poll always bounds the wait. + */ + ret = fmss_wait_cs(f, 20000); + if (ret && f->irq > 0 && + try_wait_for_completion(&f->cs_irq)) + ret = ((f->last_dma_c0c & 0xd) == 1) ? 0 : ret; + f->last_dma_c0c = readl(f->base + FMSEQIRQ); + f->last_dma_d00 = readl(f->base + FMGEN0); + f->last_dma_c00 = readl(f->base + FMSEQ); + fmss_peek_vic1(f); + + for (i = 0; i < 32; i++) + dregs[i] = readl(f->base + FMGEN0 + i * 4); + + writel(13, f->base + FMSEQIRQ); + for (i = 0; i < 10000; i++) { + if ((readl(f->base + FMSEQIRQ) & 0xd) == 0) + break; + udelay(1); + } + writel(1, f->base + FMCTRL0); + +dma_done: + if (ret == -EAGAIN) { + for (i = 0; i < 32; i++) + dregs[i] = readl(f->base + FMGEN0 + i * 4); + fmss_peek_vic1(f); + } + + memcpy(f->last_page, f->data, FMSS_PAGE_LEN); + spare_bytes = nsect * 16; + if (spare_bytes > sizeof(f->last_spare)) + spare_bytes = sizeof(f->last_spare); + memcpy(f->last_spare, f->spare, spare_bytes); + f->last_spare_len = spare_bytes; + /* 16 B Sogeti spare per 4 KiB sector → copy into last_parity[group]. */ + for (i = 0; i < nsect; i++) { + unsigned int c, idx = i * 4; + + if (idx >= FMSS_MAX_CHUNKS) + break; + memcpy(f->last_parity[idx], f->spare + i * 16, 16); + f->last_parity_len[idx] = 16; + for (c = 1; c < 4 && idx + c < FMSS_MAX_CHUNKS; c++) + f->last_parity_len[idx + c] = 0; + } + f->last_page_ce = (int)ce; + f->last_page_addr = addr; + f->last_page_len = nsect * FMSS_SECTOR_LEN; + f->last_page_chunk = (int)((u8 *)f->stbuf)[0]; + f->last_page_ret = ret; + f->last_stat48 = readl(f->base + FMSTAT48); + f->last_nandstat = readl(f->base + NANDSTAT); + + dp = f->data; + fmss_info("dma ce=%u addr=%08x ret=%d kick=%04x nsect=%u rowlo=%u d14=%u c00=%08x c04=%08x c0c=%08x c6c=%08x d00=%08x d04=%08x d08=%08x d0c=%08x d10=%08x d14=%08x st=%08x spare=%02x%02x%02x%02x data=%02x%02x%02x%02x%02x%02x%02x%02x\n", + ce, addr, ret, dma_kick, nsect, dma_row_in_lo, dma_d14, + f->last_dma_c00, readl(f->base + FMSEQBASE), f->last_dma_c0c, + readl(f->base + 0xc6c), dregs[0], dregs[1], dregs[2], dregs[3], + dregs[4], dregs[5], *(u32 *)f->stbuf, + f->last_spare[0], f->last_spare[1], f->last_spare[2], + f->last_spare[3], + dp[0], dp[1], dp[2], dp[3], dp[4], dp[5], dp[6], dp[7]); + fmss_info("dma D00..D7C: %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x | %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n", + dregs[0], dregs[1], dregs[2], dregs[3], dregs[4], dregs[5], + dregs[6], dregs[7], dregs[8], dregs[9], dregs[10], dregs[11], + dregs[12], dregs[13], dregs[14], dregs[15], dregs[16], dregs[17], + dregs[18], dregs[19], dregs[20], dregs[21], dregs[22], dregs[23], + dregs[24], dregs[25], dregs[26], dregs[27], dregs[28], dregs[29], + dregs[30], dregs[31]); + fmss_info("dma cmdl: %08x %08x %08x %08x | %08x %08x %08x %08x | %08x seq_dma=%08x cmdl_dma=%08x data_dma=%08x meta_dma=%08x\n", + cl[0], cl[1], cl[2], cl[3], cl[4], cl[5], cl[6], cl[7], cl[8], + (u32)f->seq_dma, (u32)f->cmdl_dma, (u32)f->data_dma, + (u32)f->spare_dma); + return ret; +} + +/* + * sub_12FA84: PPN parameter page, 512 bytes via 41AE38. + * cmd 0x92, addr 0, cmd 0x97, 0x77/0x7D, 41DA70, cmd 0x7A, 512 PIO. + */ +static int fmss_param_read(struct fmss_n31 *f, unsigned int ce) +{ + u8 st = 0; + int ret; + + if (ce > 7) + return -EINVAL; + memset(f->last_param, 0, FMSS_PARAM_LEN); + f->last_param_ce = (int)ce; + + writel(fmss_ctrl0(ce), f->base + FMCTRL0); + if (fmss_cmd(f, 0x92)) { + pr_info("s5l8740-fmss: param cmd 0x92 timeout\n"); + ret = -ETIMEDOUT; + goto out_idle; + } + if (fmss_addr1(f, 0)) { + pr_info("s5l8740-fmss: param addr timeout st=%08x\n", f->last_stat48); + ret = -ETIMEDOUT; + goto out_idle; + } + if (fmss_cmd(f, 0x97)) { + pr_info("s5l8740-fmss: param cmd 0x97 timeout\n"); + ret = -ETIMEDOUT; + goto out_idle; + } + fmss_cmd(f, 0x77); + fmss_cmd(f, 0x7d); + if (fmss_wait_status(f, 100000, &st)) { + pr_info("s5l8740-fmss: param ready timeout NANDSTAT=%02x st48=%08x\n", + st, f->last_stat48); + ret = -ETIMEDOUT; + goto out_idle; + } + fmss_cmd(f, 0x7a); + ret = fmss_data_in(f, f->last_param, FMSS_PARAM_LEN); + fmss_cmd(f, 0x77); +out_idle: + writel(1, f->base + FMCTRL0); + f->last_param_ret = ret; + fmss_info("param ce=%u ret=%d nandstat=%02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + ce, ret, st, + f->last_param[0], f->last_param[1], f->last_param[2], f->last_param[3], + f->last_param[4], f->last_param[5], f->last_param[6], f->last_param[7]); + return ret; +} + +/* + * sub_10453C: controller reset only (no NAND array write). + * Followed by per-CE cmd 0xFF as in 130060(a3=1). + */ +static int fmss_ctrl_reset(struct fmss_n31 *f) +{ + unsigned int i; + u32 v; + + writel(1, f->base + FMCTRL0); + writel(8, f->base + FMSEQ); + v = readl(f->base + FMSEQSTAT); + fmss_info("10453C after C00=8 c00=%08x c08=%08x\n", + readl(f->base + FMSEQ), v); + for (i = 0; i < 200000; i++) { + v = readl(f->base + FMSEQSTAT); + if (v == 4) { + writel(2, f->base + FMSEQ); + for (i = 0; i < 200000; i++) { + v = readl(f->base + FMSEQSTAT); + if (v == 3 || v == 0) + break; + udelay(1); + } + break; + } + if (!v) + break; + udelay(1); + } + if (readl(f->base + 0xc6c) != 16) { + writel(1, f->base + 0xc60); + for (i = 0; i < 200000; i++) { + if (readl(f->base + 0xc64) == 1) + break; + udelay(1); + } + writel(0, f->base + 0xc60); + } + writel(0x80000, f->base + FMSEQ); + writel(2048, f->base + FMCTRL1); + for (i = 0; i < 200000; i++) { + if (readl(f->base + FMCTRL1) & 0x40000000u) + break; + udelay(1); + } + if (!(readl(f->base + FMCTRL1) & 0x40000000u)) { + pr_info("s5l8740-fmss: 10453C FMCTRL1 bit30 timeout v=%08x\n", + readl(f->base + FMCTRL1)); + return -ETIMEDOUT; + } + writel(0x80000000, f->base + FMCTRL1); + writel(1, f->base + FMUNK81C); + return 0; +} + +/* 130060: 10453C, then NAND RESET (0xFF) on CE0/CE1. */ +static int fmss_nand_reset(struct fmss_n31 *f) +{ + unsigned int ce; + int ret; + + ret = fmss_ctrl_reset(f); + if (ret) + return ret; + for (ce = 0; ce < 2; ce++) { + writel((2u * (1u << ce)) | 0xFF001u, f->base + FMCTRL0); + if (fmss_cmd(f, 0xff)) + pr_info("s5l8740-fmss: cmd 0xFF timeout ce=%u st=%08x\n", + ce, f->last_stat48); + } + msleep(50); + writel(1, f->base + FMCTRL0); + /* 130544: PPN_FEATURE__POWER_STATE (384) = 2 on each CE. */ + for (ce = 0; ce < 2; ce++) + fmss_set_feature(f, ce, 384, 2); + fmss_info("nand_reset done NANDSTAT=%08x STAT48=%08x\n", + readl(f->base + NANDSTAT), readl(f->base + FMSTAT48)); + f->pages_since_reset = 0; + return 0; +} + +static u32 fmss_le32(const u8 *p, unsigned int off) +{ + u32 v; + + memcpy(&v, p + off, 4); + return le32_to_cpu(v); +} + +static ssize_t regs_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + static const u32 offs[] = { + FMCTRL0, FMCTRL1, FMCMD, FMADDR, FMCE, FMUNK18, FMUNK24, FMUNK28, + FMCYCLES, FMLEN, FMUNK38, FMSTAT48, NANDSTAT, FMDATA, FMSEQ, FMSEQSTAT, + 0xc04, 0xc10, 0xc38, 0xc4c, 0xc58, 0xc60, 0xc64, 0xc6c, FMUNK81C, + }; + int i, n = 0; + + if (!f || !f->base) + return -ENODEV; + for (i = 0; i < ARRAY_SIZE(offs); i++) + n += scnprintf(buf + n, PAGE_SIZE - n, "+0x%03x: 0x%08x\n", + offs[i], readl(f->base + offs[i])); + return n; +} +static DEVICE_ATTR_RO(regs); + +static ssize_t id_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + + if (!f) + return -ENODEV; + return sysfs_emit(buf, "ce=%d %02x %02x %02x %02x %02x %02x %02x %02x\n", + f->last_ce, + f->last_id[0], f->last_id[1], f->last_id[2], + f->last_id[3], f->last_id[4], f->last_id[5], + f->last_id[6], f->last_id[7]); +} +static DEVICE_ATTR_RO(id); + +static ssize_t read_id_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce; + int ret; + + if (!f) + return -ENODEV; + if (kstrtouint(buf, 0, &ce)) + return -EINVAL; + mutex_lock(&f->lock); + ret = fmss_read_id(f, ce); + mutex_unlock(&f->lock); + fmss_dev_info(dev, "read_id ce=%u ret=%d id=%02x%02x%02x%02x%02x%02x%02x%02x\n", + ce, ret, + f->last_id[0], f->last_id[1], f->last_id[2], f->last_id[3], + f->last_id[4], f->last_id[5], f->last_id[6], f->last_id[7]); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(read_id); + +static ssize_t page_status_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + + if (!f) + return -ENODEV; + return sysfs_emit(buf, + "ce=%d addr=0x%08x ret=%d chunk=%d len=%u parity=%d cycles=%u chunks=%u stat48=0x%08x nand=0x%08x dma=%d c0c=0x%08x d00=0x%08x c00=0x%08x\n", + f->last_page_ce, f->last_page_addr, f->last_page_ret, + f->last_page_chunk, f->last_page_len, with_parity, addr_cycles, + page_chunks, f->last_stat48, f->last_nandstat, f->dma_ok, + f->last_dma_c0c, f->last_dma_d00, f->last_dma_c00); +} +static DEVICE_ATTR_RO(page_status); + +static ssize_t page_hex_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + int i, n = 0; + + if (!f) + return -ENODEV; + for (i = 0; i < 256; i++) { + n += scnprintf(buf + n, PAGE_SIZE - n, "%02x%s", + f->last_page[i], ((i + 1) % 16) ? " " : "\n"); + } + return n; +} +static DEVICE_ATTR_RO(page_hex); + +static ssize_t spare_hex_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + int i, n = 0; + + if (!f) + return -ENODEV; + n += scnprintf(buf + n, PAGE_SIZE - n, "len=%u ", f->last_spare_len); + for (i = 0; i < (int)f->last_spare_len && i < 64; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, "%02x ", f->last_spare[i]); + n += scnprintf(buf + n, PAGE_SIZE - n, "\n"); + return n; +} +static DEVICE_ATTR_RO(spare_hex); + +static u32 fmss_meta_lpn(const u8 *m, unsigned int len) +{ + if (len < 12) + return ~0u; + return get_unaligned_le32(m + 8); +} + +static u8 fmss_meta_type(const u8 *m, unsigned int len) +{ + if (!len) + return 0; + return m[0]; +} + +static ssize_t parity_hex_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int i, j, n = 0; + + if (!f) + return -ENODEV; + for (i = 0; i < FMSS_MAX_CHUNKS; i++) { + if (!f->last_parity_len[i]) + continue; + n += scnprintf(buf + n, PAGE_SIZE - n, "chunk%u len=%u", + i, f->last_parity_len[i]); + for (j = 0; j < f->last_parity_len[i] && j < 32; j++) + n += scnprintf(buf + n, PAGE_SIZE - n, " %02x", + f->last_parity[i][j]); + n += scnprintf(buf + n, PAGE_SIZE - n, " lpn=%u type=%02x\n", + fmss_meta_lpn(f->last_parity[i], f->last_parity_len[i]), + fmss_meta_type(f->last_parity[i], f->last_parity_len[i])); + } + if (!n) + return sysfs_emit(buf, "(no parity captured — page_read with with_parity=1)\n"); + return n; +} +static DEVICE_ATTR_RO(parity_hex); + +static ssize_t page_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, a, b, c, d, cycles; + u32 addr; + int nf, ret; + + if (!f) + return -ENODEV; + /* "CE CAU BLOCK PAGE SLC" or "CE ADDR [cycles]" */ + nf = sscanf(buf, "%u %i %u %u %u", &ce, &a, &b, &c, &d); + if (nf == 5) { + if (a > 1 || b >= FMSS_BLOCKS_PER_CAU || c > FMSS_BTOC_PAGE) + return -EINVAL; + addr = fmss_ppn_addr(a, b, c, d); + } else { + nf = sscanf(buf, "%u %i %u", &ce, &addr, &cycles); + if (nf < 2) + return -EINVAL; + if (nf >= 3 && cycles >= 1 && cycles <= 8) + addr_cycles = cycles; + } + mutex_lock(&f->lock); + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(page_read); + +static ssize_t dma_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, a, b, c, d, cycles; + u32 addr; + int nf, ret; + + if (!f) + return -ENODEV; + if (!f->dma_ok) + return -ENODEV; + nf = sscanf(buf, "%u %i %u %u %u", &ce, &a, &b, &c, &d); + if (nf == 5) { + if (a > 1 || b >= FMSS_BLOCKS_PER_CAU || c > FMSS_BTOC_PAGE) + return -EINVAL; + addr = fmss_ppn_addr(a, b, c, d); + } else { + nf = sscanf(buf, "%u %i %u", &ce, &addr, &cycles); + if (nf < 2) + return -EINVAL; + } + mutex_lock(&f->lock); + /* Always re-init PPN before CS — cold CS kick can bus-hang the SoC. */ + fmss_nand_reset(f); + ret = fmss_dma_page_read(f, ce, addr); + f->pages_since_reset++; + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(dma_read); + +static int fmss_page_blankish(const u8 *p, unsigned int n); + +/* + * PPN DATA spare (CS META stream, type 0x01): + * +0 type, +1 bank/flags, +2..+7 weaveSeq48, +8..+11 LBA LE, +12..+15 aux. + * Whimory chooses the newest weave claimant before FMSS; manual phys reads + * can hit stale historical LBA copies. lba_weave_scan lists them newest-first. + */ +#define FMSS_LBA_CLAIM_MAX 64 +#define FMSS_LBA_TARGETS_MAX 8 +struct fmss_lba_claim { + u64 weave; + u32 lba; + u32 ppn; + u16 block; + u8 ce; + u8 cau; + u8 page; + u8 slot; + u8 typ; +}; + +static struct fmss_lba_claim lba_claims[FMSS_LBA_CLAIM_MAX]; +static unsigned int lba_claim_count; +static unsigned int lba_claim_target; /* primary / first target */ +static unsigned int lba_claim_targets[FMSS_LBA_TARGETS_MAX]; +static unsigned int lba_claim_ntargets; +static unsigned int lba_claim_scanned; +static unsigned int lba_claim_hits; +static unsigned int lba_claim_small; /* type01 && lba < 4096 sightings */ +static unsigned int lba_claim_be_alt; /* BE@+8 matched a target */ +static char lba_claim_log[PAGE_SIZE]; +static unsigned int lba_claim_log_len; + +/* N31 META: weave[15:0]@+2 LE16 | weave[47:16]@+4 LE32 (5688C4 / 568ED4) */ +static u64 fmss_ppn_weave48(const u8 *m) +{ + return (u64)get_unaligned_le16(m + 2) | + ((u64)get_unaligned_le32(m + 4) << 16); +} + +static u32 fmss_ppn_meta_lba(const u8 *m) +{ + return get_unaligned_le32(m + 8); +} + +static bool fmss_lba_is_target(u32 lba) +{ + unsigned int i; + + for (i = 0; i < lba_claim_ntargets; i++) { + if (lba_claim_targets[i] == lba) + return true; + } + return false; +} + +static void fmss_lba_claim_insert(const struct fmss_lba_claim *c) +{ + unsigned int i, j; + + for (i = 0; i < lba_claim_count; i++) { + if (lba_claims[i].ce == c->ce && lba_claims[i].cau == c->cau && + lba_claims[i].block == c->block && + lba_claims[i].page == c->page && + lba_claims[i].slot == c->slot && + lba_claims[i].lba == c->lba) + return; + } + for (i = 0; i < lba_claim_count; i++) { + if (c->weave > lba_claims[i].weave) + break; + } + if (i >= FMSS_LBA_CLAIM_MAX) + return; + if (lba_claim_count < FMSS_LBA_CLAIM_MAX) + lba_claim_count++; + for (j = lba_claim_count - 1; j > i; j--) + lba_claims[j] = lba_claims[j - 1]; + lba_claims[i] = *c; +} + +static void fmss_lba_claim_note_page(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, u32 ppn) +{ + unsigned int s, nslots; + const u8 *meta; + + if (f->last_page_ret) + return; + nslots = f->last_spare_len / 16; + if (nslots > FMSS_VBAS_PER_PAGE) + nslots = FMSS_VBAS_PER_PAGE; + for (s = 0; s < nslots; s++) { + struct fmss_lba_claim c; + u32 lba_le, lba_be; + + meta = f->last_spare + s * 16; + /* + * Diagnostic: any type whose LE LBA matches a target. + * Production mapping still prefers type 0x01. + */ + lba_le = fmss_ppn_meta_lba(meta); + lba_be = get_unaligned_be32(meta + 8); + if (meta[0] == 0x01 && lba_le < 4096u) + lba_claim_small++; + if (fmss_lba_is_target(lba_be) && !fmss_lba_is_target(lba_le)) + lba_claim_be_alt++; + if (!fmss_lba_is_target(lba_le)) + continue; + c.weave = fmss_ppn_weave48(meta); + c.lba = lba_le; + c.ppn = ppn; + c.block = (u16)block; + c.ce = (u8)ce; + c.cau = (u8)cau; + c.page = (u8)page; + c.slot = (u8)s; + c.typ = meta[0]; + fmss_lba_claim_insert(&c); + lba_claim_hits++; + /* Raw META for positive-control / endian debug. */ + pr_info("s5l8740-fmss: META hit lba=%u typ=%02x weave=%012llx ce=%u cau=%u blk=%u pg=%u slot=%u meta=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x\n", + lba_le, meta[0], (unsigned long long)c.weave, + ce, cau, block, page, s, + meta[0], meta[1], meta[2], meta[3], + meta[4], meta[5], meta[6], meta[7], + meta[8], meta[9], meta[10], meta[11], + meta[12], meta[13], meta[14], meta[15]); + } +} + +/* Erased NAND only — do not treat all-zero DMA failure residue as blank. */ +static int fmss_page_erased(const u8 *p, unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) { + if (p[i] != 0xff) + return 0; + } + return 1; +} + +/* + * CS full-page (slot0/span4) meta hunt for one or more LBAs. + * echo "LBA [START [NBLOCKS]]" > lba_weave_scan + * echo "121,122,123 [START [NBLOCKS]]" > lba_weave_scan + * Default: START=0 NBLOCKS=256 (user area; skip VFL tail). + */ +static int fmss_lba_weave_scan(struct fmss_n31 *f, unsigned int start, + unsigned int nblocks) +{ + unsigned int ce, cau, b, pg; + unsigned int saved_nsect, saved_slot, user_max; + u32 addr; + int ret = 0; + + user_max = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + if (!nblocks) + nblocks = 256; + if (start >= user_max) + return -EINVAL; + if (start + nblocks > user_max) + nblocks = user_max - start; + if (!lba_claim_ntargets) + return -EINVAL; + + lba_claim_count = 0; + lba_claim_hits = 0; + lba_claim_scanned = 0; + lba_claim_small = 0; + lba_claim_be_alt = 0; + lba_claim_target = lba_claim_targets[0]; + lba_claim_log_len = 0; + + saved_nsect = dma_nsect; + saved_slot = dma_slot; + dma_nsect = FMSS_VBAS_PER_PAGE; + dma_slot = 0; + + for (unsigned int cei = 0; cei < FMSS_NUM_CE; cei++) { + /* Prefer CE1 first — known user DATA / music live there. */ + ce = (cei == 0) ? 1u : 0u; + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + nblocks; b++) { + unsigned int fail = 0; + + for (pg = 0; pg < FMSS_BTOC_PAGE; pg++) { + /* Always re-init before CS — hung C0C wedges SoC. */ + fmss_nand_reset(f); + f->pages_since_reset = 0; + addr = fmss_ppn_addr(cau, b, pg, 0); + ret = fmss_dma_page_read(f, ce, addr); + lba_claim_scanned++; + if (ret) { + fail++; + if (fail >= 2) + break; + continue; + } + fail = 0; + fmss_lba_claim_note_page(f, ce, cau, b, + pg, addr); + /* Fully erased page0 → skip rest of block. */ + if (pg == 0 && + fmss_page_erased(f->last_page, 64) && + fmss_page_erased(f->last_spare, 16)) + break; + } + if ((b & 7) == 0) { + pr_info("s5l8740-fmss: lba_weave_scan prog targets=%u scanned=%u hits=%u small=%u be_alt=%u ce=%u cau=%u blk=%u\n", + lba_claim_ntargets, + lba_claim_scanned, lba_claim_hits, + lba_claim_small, lba_claim_be_alt, + ce, cau, b); + lba_claim_log_len = scnprintf( + lba_claim_log, sizeof(lba_claim_log), + "INPROGRESS scanned=%u hits=%u small=%u be_alt=%u kept=%u ce=%u cau=%u blk=%u\n", + lba_claim_scanned, lba_claim_hits, + lba_claim_small, lba_claim_be_alt, + lba_claim_count, ce, cau, b); + } + } + } + } + + dma_nsect = saved_nsect; + dma_slot = saved_slot; + + lba_claim_log_len = scnprintf(lba_claim_log, sizeof(lba_claim_log), + "targets=%u scanned=%u hits=%u small=%u be_alt=%u kept=%u (newest weave first)\n", + lba_claim_ntargets, lba_claim_scanned, lba_claim_hits, + lba_claim_small, lba_claim_be_alt, lba_claim_count); + for (b = 0; b < lba_claim_count; b++) { + const struct fmss_lba_claim *c = &lba_claims[b]; + + lba_claim_log_len += scnprintf( + lba_claim_log + lba_claim_log_len, + sizeof(lba_claim_log) - lba_claim_log_len, + "CSV %u,%012llx,%u,%u,%u,%u,%u,%08x\n", + c->lba, (unsigned long long)c->weave, c->ce, c->cau, + c->block, c->page, c->slot, c->ppn); + lba_claim_log_len += scnprintf( + lba_claim_log + lba_claim_log_len, + sizeof(lba_claim_log) - lba_claim_log_len, + "%u: lba=%u weave=%012llx ce=%u cau=%u blk=%u pg=%u slot=%u ppn=%08x\n", + b, c->lba, (unsigned long long)c->weave, c->ce, c->cau, + c->block, c->page, c->slot, c->ppn); + } + pr_info("s5l8740-fmss: lba_weave_scan %s", lba_claim_log); + return 0; +} + +static int fmss_lba_parse_targets(const char *s, unsigned int *start, + unsigned int *nblocks) +{ + unsigned int n = 0, a = 0, b = 256; + const char *p = s; + char *end; + + lba_claim_ntargets = 0; + while (*p && *p != ' ' && *p != '\t' && *p != '\n') { + unsigned long v = simple_strtoul(p, &end, 0); + + if (end == p) + return -EINVAL; + if (n >= FMSS_LBA_TARGETS_MAX) + return -EINVAL; + lba_claim_targets[n++] = (unsigned int)v; + p = end; + if (*p == ',') { + p++; + continue; + } + break; + } + if (!n) + return -EINVAL; + while (*p == ' ' || *p == '\t') + p++; + if (*p) { + if (sscanf(p, "%u %u", &a, &b) < 1) + return -EINVAL; + } + *start = a; + *nblocks = b; + lba_claim_ntargets = n; + return 0; +} + +static ssize_t lba_weave_scan_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int start = 0, nblocks = 256; + int ret; + + if (!f || !f->dma_ok) + return -ENODEV; + ret = fmss_lba_parse_targets(buf, &start, &nblocks); + if (ret) + return ret; + mutex_lock(&f->lock); + fmss_nand_reset(f); + ret = fmss_lba_weave_scan(f, start, nblocks); + mutex_unlock(&f->lock); + return ret ? ret : count; +} + +static ssize_t lba_weave_scan_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (!lba_claim_log_len) + return sysfs_emit(buf, "no scan yet\n"); + return sysfs_emit(buf, "%s", lba_claim_log); +} +static DEVICE_ATTR_RW(lba_weave_scan); + +static ssize_t seq_kick_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int cmd; + + if (!f) + return -ENODEV; + if (kstrtouint(buf, 0, &cmd)) + return -EINVAL; + mutex_lock(&f->lock); + writel(cmd, f->base + FMSEQ); + f->last_dma_c00 = readl(f->base + FMSEQ); + f->last_dma_c0c = readl(f->base + FMSEQIRQ); + fmss_dev_info(dev, "seq_kick wrote 0x%x now c00=%08x c08=%08x c0c=%08x c04=%08x c38=%08x\n", + cmd, f->last_dma_c00, readl(f->base + FMSEQSTAT), + f->last_dma_c0c, readl(f->base + FMSEQBASE), + readl(f->base + 0xc38)); + mutex_unlock(&f->lock); + return count; +} +static DEVICE_ATTR_WO(seq_kick); + +static int fmss_find(const u8 *p, unsigned int n, const char *s, unsigned int sl) +{ + unsigned int i; + + if (sl == 0 || sl > n) + return 0; + for (i = 0; i + sl <= n; i++) { + if (!memcmp(p + i, s, sl)) + return 1; + } + return 0; +} + +/* + * Match ASCII needle as UTF-16LE or UTF-16BE (FAT LFN / wide strings). + * Returns match byte offset+1, or 0 if not found. + */ +static unsigned int fmss_find_utf16(const u8 *p, unsigned int n, const char *s, + unsigned int sl, bool be) +{ + unsigned int i, j; + + if (sl == 0 || n < sl * 2) + return 0; + for (i = 0; i + sl * 2 <= n; i++) { + for (j = 0; j < sl; j++) { + u8 lo = be ? p[i + j * 2 + 1] : p[i + j * 2]; + u8 hi = be ? p[i + j * 2] : p[i + j * 2 + 1]; + + if (hi != 0 || lo != (u8)s[j]) + break; + } + if (j == sl) + return i + 1; + } + return 0; +} + +/* 16-bit byte-swap of each pair: "AB" -> "BA" stream — PIO/DMA mis-endian probe. */ +static int fmss_find_bswap16(const u8 *p, unsigned int n, const char *s, + unsigned int sl) +{ + unsigned int i, j; + + if (sl < 2 || n < sl) + return 0; + for (i = 0; i + sl <= n; i++) { + for (j = 0; j + 1 < sl; j += 2) { + if (p[i + j] != (u8)s[j + 1] || + p[i + j + 1] != (u8)s[j]) + break; + } + if (j >= sl - (sl & 1)) { + if (!(sl & 1) || p[i + j] == (u8)s[j]) + return 1; + } + } + return 0; +} + +enum { + FMSS_MATCH_ASCII = 1, + FMSS_MATCH_UTF16LE = 2, + FMSS_MATCH_UTF16BE = 4, + FMSS_MATCH_BSWAP16 = 8, +}; + +/* OR of FMSS_MATCH_* flags; also returns first hit offset via *off_out. */ +static unsigned int fmss_find_enc(const u8 *p, unsigned int n, const char *s, + unsigned int sl, unsigned int *off_out) +{ + unsigned int flags = 0, i, u; + + if (off_out) + *off_out = 0; + if (sl == 0 || sl > n) + return 0; + for (i = 0; i + sl <= n; i++) { + if (!memcmp(p + i, s, sl)) { + flags |= FMSS_MATCH_ASCII; + if (off_out && !*off_out) + *off_out = i; + break; + } + } + u = fmss_find_utf16(p, n, s, sl, false); + if (u) { + flags |= FMSS_MATCH_UTF16LE; + if (off_out && !*off_out) + *off_out = u - 1; + } + u = fmss_find_utf16(p, n, s, sl, true); + if (u) { + flags |= FMSS_MATCH_UTF16BE; + if (off_out && !*off_out) + *off_out = u - 1; + } + if (fmss_find_bswap16(p, n, s, sl)) + flags |= FMSS_MATCH_BSWAP16; + return flags; +} + +static const char *fmss_match_enc_name(unsigned int flags) +{ + if (flags & FMSS_MATCH_ASCII) + return "ascii"; + if (flags & FMSS_MATCH_UTF16LE) + return "utf16le"; + if (flags & FMSS_MATCH_UTF16BE) + return "utf16be"; + if (flags & FMSS_MATCH_BSWAP16) + return "bswap16"; + return "none"; +} + +static int fmss_page_blankish(const u8 *p, unsigned int n) +{ + unsigned int i, ff = 0, zz = 0; + + for (i = 0; i < n; i++) { + if (p[i] == 0xff) + ff++; + else if (p[i] == 0x00) + zz++; + } + return ff == n || zz == n; +} + +static u32 fmss_btoc_entry_be(const u8 *btoc_page, unsigned int idx) +{ + return get_unaligned_be32(btoc_page + idx * 4); +} + +static u32 fmss_btoc_entry_le(const u8 *btoc_page, unsigned int idx) +{ + return get_unaligned_le32(btoc_page + idx * 4); +} + +/* Default YaFTL/Sogeti BTOC is big-endian (live blk64: 00 00 00 0b …). */ +static u32 fmss_btoc_entry(const u8 *btoc_page, unsigned int idx) +{ + return fmss_btoc_entry_be(btoc_page, idx); +} + +/* Pick BE vs LE for a BTOC page: prefer the endian with more plausible LPNs. */ +static bool fmss_btoc_prefer_le(const u8 *btoc) +{ + unsigned int i, good_be = 0, good_le = 0; + u32 prev_be = 0, prev_le = 0; + unsigned int seq_be = 0, seq_le = 0; + + for (i = 0; i < 16; i++) { + u32 be = fmss_btoc_entry_be(btoc, i); + u32 le = fmss_btoc_entry_le(btoc, i); + + if (be != 0xffffffff && be < 0x01000000u) + good_be++; + if (le != 0xffffffff && le < 0x01000000u) + good_le++; + if (i && be == prev_be + 1) + seq_be++; + if (i && le == prev_le + 1) + seq_le++; + prev_be = be; + prev_le = le; + } + if (seq_le > seq_be && good_le >= good_be) + return true; + if (good_le >= 4 && good_be <= 1) + return true; + return false; +} + +/* D: probe 2026-08-24: EB 3C 90 OEM "*UOKJIHC", vol "AISPOD FAT32" + * Live copy may have 55AA at 0x1C9 rather than 510 — match OEM, patch on carve. + */ +static bool fmss_apple_fat_sig(const u8 *s) +{ + return s[0] == 0xeb && s[1] == 0x3c && s[2] == 0x90 && + s[3] == '*' && s[4] == 'U' && s[5] == 'O' && s[6] == 'K' && + s[7] == 'J' && s[8] == 'I' && s[9] == 'H' && s[10] == 'C'; +} + +static bool fmss_apple_fat_boot(const u8 *s) +{ + return fmss_apple_fat_sig(s) && s[510] == 0x55 && s[511] == 0xaa; +} + +static bool fmss_page_find_apple_bpb(const u8 *page, unsigned int len, + unsigned int *off_out) +{ + unsigned int off; + + if (len < FMSS_SECTOR_LEN) + return false; + for (off = 0; off + FMSS_SECTOR_LEN <= len; off++) { + if (fmss_apple_fat_sig(page + off)) { + if (off_out) + *off_out = off; + return true; + } + } + return false; +} + +static bool fmss_page_has_fat_boot(const u8 *page, unsigned int len, + unsigned int *off_out) +{ + unsigned int off; + + for (off = 0; off + 512 <= len; off += 512) { + if (fmss_apple_fat_boot(page + off) || + fmss_apple_fat_sig(page + off)) { + if (off_out) + *off_out = off; + return true; + } + } + return fmss_page_find_apple_bpb(page, len, off_out); +} + +/* FAT32 DataStart = reserved + nFATS * FATSz32 (logical sectors). */ +static unsigned int fmss_bpb_data_start(const u8 *bpb) +{ + u16 bps = get_unaligned_le16(bpb + 11); + u16 rsvd = get_unaligned_le16(bpb + 14); + u8 nfats = bpb[16]; + u32 fatz = get_unaligned_le32(bpb + 36); + unsigned int start; + + if (bps != 512 && bps != 4096) + return 1916; + if (!nfats || nfats > 4 || !fatz) + return 1916; + start = (unsigned int)rsvd + (unsigned int)nfats * fatz; + if (!start || start > FMSS_FTL_DEFAULT_CAPACITY) + return 1916; + return start; +} + +static u32 fmss_l2v_pack_sec(unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, + unsigned int sec) +{ + return L2V_VALID | + ((ce & 3u) << L2V_CE_SHIFT) | + ((cau & 3u) << L2V_CAU_SHIFT) | + ((sec & L2V_SEC_MASK) << L2V_SEC_SHIFT) | + ((block & L2V_BLOCK_MASK) << L2V_BLOCK_SHIFT) | + (page & L2V_PAGE_MASK); +} + +static u32 fmss_l2v_pack(unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page) +{ + return fmss_l2v_pack_sec(ce, cau, block, page, L2V_SEC_FROM_LBA); +} + +static void fmss_l2v_unpack_sec(u32 e, unsigned int *ce, unsigned int *cau, + unsigned int *block, unsigned int *page, + unsigned int *sec) +{ + *ce = (e >> L2V_CE_SHIFT) & 3u; + *cau = (e >> L2V_CAU_SHIFT) & 3u; + *sec = (e >> L2V_SEC_SHIFT) & L2V_SEC_MASK; + *block = (e >> L2V_BLOCK_SHIFT) & L2V_BLOCK_MASK; + *page = e & L2V_PAGE_MASK; +} + +static void fmss_l2v_unpack(u32 e, unsigned int *ce, unsigned int *cau, + unsigned int *block, unsigned int *page) +{ + unsigned int sec; + + fmss_l2v_unpack_sec(e, ce, cau, block, page, &sec); + (void)sec; +} + +static void fmss_wmr_map_free(void) +{ + vfree(wmr_block_map); + wmr_block_map = NULL; + wmr_block_map_n = 0; +} + +static void fmss_l2v_free(void) +{ + vfree(l2v_map); + l2v_map = NULL; + l2v_map_size = 0; + l2v_mapped = 0; + l2v_max_lpn = 0; + l2v_btoc_hits = 0; + l2v_bmap_hits = 0; + l2v_meta_hits = 0; + fmss_wmr_map_free(); +} + +static int fmss_l2v_ensure(unsigned int max_lpn) +{ + unsigned int need = max_lpn + 1; + u32 *n; + + if (l2v_map && l2v_map_size >= need) { + l2v_max_lpn = max_lpn; + return 0; + } + n = vzalloc(array_size(need, sizeof(*n))); + if (!n) + return -ENOMEM; + if (l2v_map) { + memcpy(n, l2v_map, l2v_map_size * sizeof(*n)); + vfree(l2v_map); + } + l2v_map = n; + l2v_map_size = need; + l2v_max_lpn = max_lpn; + return 0; +} + +static void fmss_l2v_index_note(unsigned int lpn, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page) +{ + unsigned int i; + + for (i = 0; i < lpn_index_count; i++) { + if (lpn_index[i].lpn == lpn) { + lpn_index[i].ce = ce; + lpn_index[i].cau = cau; + lpn_index[i].block = block; + lpn_index[i].page = page; + return; + } + } + if (lpn_index_count >= FMSS_LPN_INDEX_MAX) + return; + lpn_index[lpn_index_count].lpn = lpn; + lpn_index[lpn_index_count].ce = ce; + lpn_index[lpn_index_count].cau = cau; + lpn_index[lpn_index_count].block = block; + lpn_index[lpn_index_count].page = page; + lpn_index_count++; +} + +static void fmss_l2v_set(unsigned int lpn, unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page) +{ + u32 prev; + + if (!l2v_map || lpn >= l2v_map_size) + return; + prev = l2v_map[lpn]; + l2v_map[lpn] = fmss_l2v_pack(ce, cau, block, page); + if (!(prev & L2V_VALID)) + l2v_mapped++; + fmss_l2v_index_note(lpn, ce, cau, block, page); +} + +/* Early LBA map (boot+FAT+root): SFTL LBA → packed phys+sec. */ +#define FMSS_EARLY_LBA_MAX 8192u +static u32 *early_lba_map; +static unsigned int early_lba_mapped; + +static void fmss_early_lba_free(void) +{ + vfree(early_lba_map); + early_lba_map = NULL; + early_lba_mapped = 0; +} + +static int fmss_early_lba_ensure(void) +{ + if (early_lba_map) + return 0; + early_lba_map = vzalloc(FMSS_EARLY_LBA_MAX * sizeof(*early_lba_map)); + return early_lba_map ? 0 : -ENOMEM; +} + +static void fmss_early_lba_set(unsigned int lba, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, unsigned int sec) +{ + u32 prev; + + if (lba >= FMSS_EARLY_LBA_MAX || fmss_early_lba_ensure()) + return; + prev = early_lba_map[lba]; + early_lba_map[lba] = fmss_l2v_pack_sec(ce, cau, block, page, sec & 3u); + if (!(prev & L2V_VALID)) + early_lba_mapped++; +} + +static int fmss_early_lba_lookup(unsigned int lba, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page, unsigned int *sec) +{ + u32 e; + + if (!early_lba_map || lba >= FMSS_EARLY_LBA_MAX) + return -ENOENT; + e = early_lba_map[lba]; + if (!(e & L2V_VALID)) + return -ENOENT; + fmss_l2v_unpack_sec(e, ce, cau, block, page, sec); + return 0; +} + +static int fmss_l2v_lookup(unsigned int lpn, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page) +{ + u32 e; + + if (!l2v_map || lpn >= l2v_map_size) + return -ENOENT; + e = l2v_map[lpn]; + if (!(e & L2V_VALID)) + return -ENOENT; + fmss_l2v_unpack(e, ce, cau, block, page); + return 0; +} + +/* + * Plausible YaFTL/Whimory BTOC: first entries are small LPNs and usually + * sequential (we see 11,12,13 on blk 64 or 0,1,... on the boot superblock). + * Used by on-demand lpn_read scan; l2v_build uses a looser ingest. + */ +static bool fmss_btoc_plausible(const u8 *btoc_page, unsigned int target_lpn, + unsigned int *opage) +{ + unsigned int p, lpn0, lpn1, lpn2, hits = 0; + + lpn0 = fmss_btoc_entry(btoc_page, 0); + lpn1 = fmss_btoc_entry(btoc_page, 1); + lpn2 = fmss_btoc_entry(btoc_page, 2); + if (target_lpn == 0) { + /* Boot superblock: BTOC[0]==0 && BTOC[1]==1 (not a stray 0 elsewhere). */ + if (lpn0 == 0 && lpn1 == 1) { + *opage = 0; + return true; + } + return false; + } + if (lpn0 > 0x1000000 || lpn1 > 0x1000000) + return false; + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + if (fmss_btoc_entry(btoc_page, p) == target_lpn) { + hits++; + *opage = p; + } + } + if (!hits) + return false; + /* Prefer superblocks whose first slots look like FTL metadata. */ + if (lpn0 <= target_lpn && lpn1 == lpn0 + 1) + return true; + if (lpn0 == 11 && lpn1 == 12 && lpn2 == 13) + return true; + return hits == 1; +} + +/* Strict BTOC: need a dense BE (or LE) run — loose >=2 poisoned L2V. */ +static bool fmss_btoc_ingestible(const u8 *btoc) +{ + unsigned int i, good_be = 0, good_le = 0, seq_be = 0, seq_le = 0; + u32 prev_be = 0xffffffff, prev_le = 0xffffffff; + + if (fmss_page_blankish(btoc, 64)) + return false; + for (i = 0; i < 32; i++) { + u32 be = fmss_btoc_entry_be(btoc, i); + u32 le = fmss_btoc_entry_le(btoc, i); + + if (be != 0xffffffff && be < 0x01000000u) { + good_be++; + if (prev_be != 0xffffffff && be == prev_be + 1) + seq_be++; + prev_be = be; + } else { + prev_be = 0xffffffff; + } + if (le != 0xffffffff && le < 0x01000000u) { + good_le++; + if (prev_le != 0xffffffff && le == prev_le + 1) + seq_le++; + prev_le = le; + } else { + prev_le = 0xffffffff; + } + } + /* Prefer sequential tables (live blk64: 11,12,13…). */ + if (good_be >= 8 && seq_be >= 4) + return true; + if (good_le >= 8 && seq_le >= 4 && good_le > good_be) + return true; + /* Boot SB: (0,1) or (0,vbas_per_page) */ + if (fmss_btoc_entry_be(btoc, 0) == 0 && + (fmss_btoc_entry_be(btoc, 1) == 1 || + fmss_btoc_entry_be(btoc, 1) == FMSS_VBAS_PER_PAGE)) + return true; + if (fmss_btoc_entry_le(btoc, 0) == 0 && + (fmss_btoc_entry_le(btoc, 1) == 1 || + fmss_btoc_entry_le(btoc, 1) == FMSS_VBAS_PER_PAGE)) + return true; + return false; +} + +static int fmss_boot_carve_try(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page); + +static void fmss_l2v_ingest_btoc(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + const u8 *btoc, unsigned int max_lpn) +{ + unsigned int p; + bool use_le; + + if (!fmss_btoc_ingestible(btoc)) + return; + /* N31 live BTOCs are BE; only use LE when clearly better. */ + use_le = fmss_btoc_prefer_le(btoc); + l2v_btoc_hits++; + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + u32 lpn = use_le ? fmss_btoc_entry_le(btoc, p) + : fmss_btoc_entry_be(btoc, p); + + if (lpn == 0xffffffff || lpn > max_lpn) + continue; + if (lpn == 0) { + /* + * Do not fmss_boot_carve_try here — nested full-page + * reads during the BTOC walk wedge FMSS. Discover + * handles BTOC[0]==0 after the walk. + */ + continue; + } + fmss_l2v_set(lpn, ce, cau, block, p); + /* Also fill early LBA map for boot/FAT (4 LBAs per page LPN). */ + if (lpn < FMSS_EARLY_LBA_MAX / FMSS_VBAS_PER_PAGE) { + unsigned int s; + + for (s = 0; s < FMSS_VBAS_PER_PAGE; s++) + fmss_early_lba_set(lpn * FMSS_VBAS_PER_PAGE + s, + ce, cau, block, p, s); + } + } +} + +/* + * SFTL on-flash BTOC (meta type 28): 16-byte BE records + * +0 weaveSeqAdd, +4 aux, +8 lba, +12 … +15 span in low byte (live: + * 00 00 00 00 | a7 00 00 1d | 00 00 00 79 | 05 00 00 02 → lba=121 span=2). + * Used when YaFTL u32 LPN table is not ingestible. + */ +static bool fmss_page_looks_bte(const u8 *page) +{ + u32 weave0, lba0, span0, lba1, span1; + + if (fmss_page_blankish(page, 64)) + return false; + weave0 = get_unaligned_be32(page); + lba0 = get_unaligned_be32(page + 8); + span0 = page[15]; + if (weave0 != 0 || !span0 || span0 > 128 || lba0 >= 0x01000000u) + return false; + lba1 = get_unaligned_be32(page + 16 + 8); + span1 = page[16 + 15]; + if (!span1 || span1 > 128 || lba1 >= 0x01000000u) + return false; + /* Prefer abutting/near spans (live 121+2 → 123). */ + if (lba1 != lba0 + span0 && lba1 + span1 != lba0 && + (lba1 < lba0 || lba1 > lba0 + span0 + 8)) + return false; + return true; +} + +static void fmss_l2v_ingest_bte(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + const u8 *page, unsigned int max_lpn) +{ + unsigned int i, recs, vba_ofs = 0, hit = 0; + unsigned int usable = 1024; /* page_chunks=1 BTOC walk only has 1 KiB */ + + (void)f; + (void)max_lpn; + if (!fmss_page_looks_bte(page)) + return; + if (fmss_early_lba_ensure()) + return; + recs = usable / 16; + for (i = 0; i < recs; i++) { + const u8 *r = page + i * 16; + u32 lba = get_unaligned_be32(r + 8); + u32 span = r[15]; + unsigned int j; + + if (!span || span > 128) + break; + if (lba >= 0x01000000u) + break; + hit++; + for (j = 0; j < span; j++) { + u32 cur = lba + j; + unsigned int pg = vba_ofs / FMSS_VBAS_PER_PAGE; + unsigned int sec = vba_ofs % FMSS_VBAS_PER_PAGE; + + if (pg >= FMSS_BTOC_PAGE) + goto done; + if (cur < FMSS_EARLY_LBA_MAX) + fmss_early_lba_set(cur, ce, cau, block, pg, sec); + vba_ofs++; + } + } +done: + if (hit) + l2v_btoc_hits++; +} + +/* + * Classic Whimory block-map heuristic: dense array of u16 vblock ids. + * Tries LE then BE. Returns entry count hint. + */ +static bool fmss_page_looks_block_map(const u8 *page, unsigned int len, + bool *be_out, unsigned int *nents) +{ + unsigned int max = min(len / 2u, 2048u); + unsigned int i, good_le = 0, bad_le = 0, good_be = 0, bad_be = 0; + + if (max < 64) + return false; + for (i = 0; i < max; i++) { + u16 le = get_unaligned_le16(page + i * 2); + u16 be = get_unaligned_be16(page + i * 2); + + if (le && le != 0xffff) { + if (le < FMSS_BLOCKS_PER_CAU) + good_le++; + else + bad_le++; + } + if (be && be != 0xffff) { + if (be < FMSS_BLOCKS_PER_CAU) + good_be++; + else + bad_be++; + } + } + if (good_le >= 32 && bad_le * 4 <= good_le && good_le >= good_be) { + *be_out = false; + *nents = max; + return true; + } + if (good_be >= 32 && bad_be * 4 <= good_be) { + *be_out = true; + *nents = max; + return true; + } + return false; +} + +static unsigned int fmss_vfl_phys(unsigned int cau, unsigned int virt); +static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, + unsigned int block, const u8 *hdr); +static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, u8 *dst, unsigned int dst_len); + +/* + * Classic VFL ftlctrlblocks[3] are LE u16 at +4/+6/+8 (after usn). + * N31 wrmx headers differ; accept the triple only when all look like + * in-range block ids. Also probe a few other offsets in the first 512B. + */ +static bool fmss_wmr_try_ftlctrl_at(const u8 *hdr, unsigned int off, + u16 *out3) +{ + u16 a, b, c; + unsigned int usable = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + + if (off + 6 > FMSS_VFL_HDR_LEN) + return false; + a = get_unaligned_le16(hdr + off); + b = get_unaligned_le16(hdr + off + 2); + c = get_unaligned_le16(hdr + off + 4); + if (!a || a >= usable || b >= usable || c >= usable) + return false; + /* Prefer distinct-ish ctrl blocks (allow one duplicate). */ + if (a == b && b == c) + return false; + out3[0] = a; + out3[1] = b; + out3[2] = c; + return true; +} + +static bool fmss_wmr_extract_ftlctrl(const u8 *hdr, u16 *out3) +{ + static const unsigned int offs[] = { + offsetof(struct wmr_vfl_cxt, ftlctrlblocks), /* 4 */ + 0x08, 0x0c, 0x10, 0x14, 0x18, 0x20, 0x28, 0x30, + }; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(offs); i++) { + if (fmss_wmr_try_ftlctrl_at(hdr, offs[i], out3)) + return true; + } + return false; +} + +static int fmss_wmr_ensure_map(void) +{ + if (wmr_block_map) + return 0; + wmr_block_map = vmalloc(array_size(WMR_BLOCK_MAP_MAX, sizeof(u16))); + if (!wmr_block_map) + return -ENOMEM; + memset(wmr_block_map, 0xff, WMR_BLOCK_MAP_MAX * sizeof(u16)); + wmr_block_map_n = 0; + return 0; +} + +static unsigned int fmss_wmr_load_map_page(const u8 *page, unsigned int len, + bool be, unsigned int nents, + unsigned int dst_off) +{ + unsigned int i, take; + + if (fmss_wmr_ensure_map()) + return 0; + if (dst_off >= WMR_BLOCK_MAP_MAX) + return 0; + take = min(nents, WMR_BLOCK_MAP_MAX - dst_off); + take = min(take, len / 2u); + for (i = 0; i < take; i++) { + u16 v = be ? get_unaligned_be16(page + i * 2) + : get_unaligned_le16(page + i * 2); + + wmr_block_map[dst_off + i] = v; + } + if (dst_off + take > wmr_block_map_n) + wmr_block_map_n = dst_off + take; + return take; +} + +/* Decode classic vPage → CE/CAU/block/page for N31 PPN. */ +static bool fmss_wmr_vpage_to_phys(u32 vpage, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page) +{ + unsigned int pg = vpage % WMR_PAGES_PER_BLOCK; + unsigned int vbn = vpage / WMR_PAGES_PER_BLOCK; + unsigned int usable = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + unsigned int phys; + + *page = pg; + + /* Prefer CE0/CAU0 with identity VFL remap. */ + if (vbn < usable) { + phys = fmss_vfl_phys(0, vbn); + if (phys < FMSS_BLOCKS_PER_CAU) { + *ce = 0; + *cau = 0; + *block = phys; + return true; + } + } + + /* Pack vblock across CAUs (then CEs) when identity is out of range. */ + if (!usable) + return false; + { + unsigned int blk = vbn % usable; + unsigned int cau_i = (vbn / usable) % FMSS_NUM_CAU; + unsigned int ce_i = (vbn / (usable * FMSS_NUM_CAU)) % + FMSS_NUM_CE; + + phys = fmss_vfl_phys(cau_i, blk); + *ce = ce_i; + *cau = cau_i; + *block = phys; + return true; + } +} + +static void fmss_wmr_maybe_reset(struct fmss_n31 *f) +{ + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } +} + +/* Bounded DEVICEINFOSIGN hunt: early blocks + VFL tail, page 0 only. */ +static void fmss_wmr_scan_deviceinfo(struct fmss_n31 *f, unsigned int nblocks) +{ + unsigned int ce, cau, b, saved; + unsigned int start_tail; + u32 addr; + int ret; + + wmr_dis_hits = 0; + if (!nblocks || nblocks > WMR_MOUNT_MAX_BLOCKS) + nblocks = min(grep_max_blocks, WMR_MOUNT_MAX_BLOCKS); + if (!nblocks) + nblocks = 16; + start_tail = FMSS_BLOCKS_PER_CAU - nblocks; + + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = 0; b < nblocks; b++) { + fmss_wmr_maybe_reset(f); + addr = fmss_ppn_addr(cau, b, 0, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (!ret && + fmss_find_enc(f->last_page, f->last_page_len, + WMR_DEVICEINFOSIGN, + strlen(WMR_DEVICEINFOSIGN), + NULL)) { + wmr_dis_hits++; + if (wmr_dis_hits == 1) { + wmr_dis_ce = ce; + wmr_dis_cau = cau; + wmr_dis_block = b; + wmr_dis_page = 0; + } + } + } + for (b = start_tail; b < FMSS_BLOCKS_PER_CAU; b++) { + fmss_wmr_maybe_reset(f); + addr = fmss_ppn_addr(cau, b, 0, 1); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (!ret && + fmss_find_enc(f->last_page, f->last_page_len, + WMR_DEVICEINFOSIGN, + strlen(WMR_DEVICEINFOSIGN), + NULL)) { + wmr_dis_hits++; + if (wmr_dis_hits == 1) { + wmr_dis_ce = ce; + wmr_dis_cau = cau; + wmr_dis_block = b; + wmr_dis_page = 0; + } + } + } + } + } + page_chunks = saved; +} + +/* Tail VFL wrmx/xrmw ingest + optional classic ftlctrlblocks. */ +static void fmss_wmr_scan_vfl(struct fmss_n31 *f, unsigned int nblocks) +{ + unsigned int ce, cau, i, saved, start; + u16 ctrl[3]; + int ret; + + wmr_vfl_hits = 0; + wmr_ftlctrl_hits = 0; + wmr_ftlctrl_n = 0; + vfl_map_count = 0; + if (!nblocks || nblocks > WMR_MOUNT_MAX_BLOCKS) + nblocks = min(vfl_build_blocks ? vfl_build_blocks : 32u, + WMR_MOUNT_MAX_BLOCKS); + if (nblocks > FMSS_VFL_TAIL) + nblocks = FMSS_VFL_TAIL; + start = FMSS_BLOCKS_PER_CAU - nblocks; + + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (i = 0; i < nblocks; i++) { + unsigned int blk = start + i; + u32 addr; + + fmss_wmr_maybe_reset(f); + addr = fmss_ppn_addr(cau, blk, 0, 1); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 512)) + continue; + if (fmss_vfl_ingest(f, cau, blk, f->last_page)) + wmr_vfl_hits++; + if (fmss_wmr_extract_ftlctrl(f->last_page, + ctrl)) { + wmr_ftlctrl_hits++; + if (!wmr_ftlctrl_n) { + wmr_ftlctrl[0] = ctrl[0]; + wmr_ftlctrl[1] = ctrl[1]; + wmr_ftlctrl[2] = ctrl[2]; + wmr_ftlctrl_n = 3; + } + } + } + } + } + page_chunks = saved; +} + +static void fmss_wmr_try_load_bmap(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int *dst_off) +{ + bool be = false; + unsigned int nents = 0, take; + + if (fmss_read_lpn_page(f, ce, cau, block, 0, NULL, 0)) + return; + if (!fmss_page_looks_block_map(f->last_page, f->last_page_len, + &be, &nents)) + return; + take = fmss_wmr_load_map_page(f->last_page, f->last_page_len, be, + nents, *dst_off); + if (take) { + wmr_bmap_pages++; + *dst_off += take; + } +} + +/* Probe ftlctrl blocks + bounded early/tail for type-0x44-like maps. */ +static void fmss_wmr_scan_block_maps(struct fmss_n31 *f, unsigned int nblocks) +{ + unsigned int ce, cau, b, i, saved, dst = 0; + unsigned int start_tail; + + wmr_bmap_pages = 0; + fmss_wmr_map_free(); + if (!nblocks || nblocks > WMR_MOUNT_MAX_BLOCKS) + nblocks = min(grep_max_blocks, WMR_MOUNT_MAX_BLOCKS); + if (!nblocks) + nblocks = 16; + start_tail = FMSS_BLOCKS_PER_CAU - nblocks; + + saved = page_chunks; + page_chunks = 16; + + for (i = 0; i < wmr_ftlctrl_n && dst < WMR_BLOCK_MAP_MAX; i++) { + u16 vb = wmr_ftlctrl[i]; + + if (!vb || vb >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + continue; + for (ce = 0; ce < FMSS_NUM_CE && dst < WMR_BLOCK_MAP_MAX; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU && + dst < WMR_BLOCK_MAP_MAX; cau++) { + unsigned int phys = fmss_vfl_phys(cau, vb); + + fmss_wmr_try_load_bmap(f, ce, cau, phys, &dst); + } + } + } + + for (ce = 0; ce < FMSS_NUM_CE && dst < WMR_BLOCK_MAP_MAX; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU && + dst < WMR_BLOCK_MAP_MAX; cau++) { + unsigned int probes = 0; + + for (b = 0; b < nblocks && probes < 24 && + dst < WMR_BLOCK_MAP_MAX; b++) { + fmss_wmr_try_load_bmap(f, ce, cau, b, &dst); + probes++; + } + for (b = start_tail; b < FMSS_BLOCKS_PER_CAU && + probes < 40 && dst < WMR_BLOCK_MAP_MAX; b++) { + fmss_wmr_try_load_bmap(f, ce, cau, b, &dst); + probes++; + } + } + } + page_chunks = saved; +} + +static unsigned int fmss_wmr_fill_l2v(unsigned int max_lpn) +{ + unsigned int lpn, filled = 0; + + if (!wmr_block_map || !wmr_block_map_n) + return 0; + if (fmss_l2v_ensure(max_lpn)) + return 0; + + for (lpn = 0; lpn <= max_lpn; lpn++) { + u32 vpage; + unsigned int ce, cau, block, page; + + vpage = wmr_lpage_to_vpage(lpn, WMR_PAGES_PER_BLOCK, + wmr_block_map, wmr_block_map_n); + if (vpage == ~0u) + continue; + if (!fmss_wmr_vpage_to_phys(vpage, &ce, &cau, &block, &page)) + continue; + /* Do not clobber denser BTOC hits already present. */ + if (l2v_map && lpn < l2v_map_size && + (l2v_map[lpn] & L2V_VALID)) + continue; + fmss_l2v_set(lpn, ce, cau, block, page); + filled++; + } + return filled; +} + +/* + * Classic freemyipod Whimory mount adapted for N31 PPN. + * Does not clear existing l2v_build / boot_carve results. + * Usage: echo 1 > whimory_mount + * echo "NBLOCKS [MAX_LPN]" > whimory_mount + */ +static int fmss_whimory_mount(struct fmss_n31 *f, unsigned int nblocks, + unsigned int max_lpn) +{ + if (!max_lpn) + max_lpn = FMSS_L2V_DEFAULT_MAX_LPN; + if (!nblocks || nblocks > WMR_MOUNT_MAX_BLOCKS) + nblocks = min(grep_max_blocks ? grep_max_blocks : 32u, + WMR_MOUNT_MAX_BLOCKS); + + wmr_l2v_filled = 0; + wmr_mount_ret = 0; + + fmss_wmr_scan_deviceinfo(f, nblocks); + fmss_wmr_scan_vfl(f, min(nblocks, FMSS_VFL_TAIL)); + fmss_wmr_scan_block_maps(f, nblocks); + wmr_l2v_filled = fmss_wmr_fill_l2v(max_lpn); + + if (!wmr_block_map_n && !wmr_l2v_filled) + wmr_mount_ret = -ENOENT; + else + wmr_mount_ret = 0; + + fmss_dev_info(f->dev, + "whimory_mount n=%u max_lpn=%u dis=%u vfl=%u ftlctrl=%u bmap_pages=%u map_ents=%u filled=%u ret=%d\n", + nblocks, max_lpn, wmr_dis_hits, wmr_vfl_hits, + wmr_ftlctrl_hits, wmr_bmap_pages, wmr_block_map_n, + wmr_l2v_filled, wmr_mount_ret); + return wmr_mount_ret; +} + +static unsigned int fmss_vfl_phys(unsigned int cau, unsigned int virt) +{ + unsigned int i; + + for (i = 0; i < vfl_map_count; i++) { + if (vfl_map[i].cau == cau && vfl_map[i].virt == virt) + return vfl_map[i].phys; + } + return virt; +} + +/* + * wrmx/xrmw VFLCxt: 512-byte header, u32 remap table begins @ +0x100. + * Live pod: entries are LE phys block numbers (e.g. 0x827 = 2087). + */ +static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, + unsigned int block, const u8 *hdr) +{ + unsigned int i, virt, phys, added = 0; + const u8 *tab = hdr + 0x100; + const char *magic = "????"; + + if (hdr[0] == 'w' && hdr[1] == 'r' && hdr[2] == 'm' && hdr[3] == 'x') + magic = "wrmx"; + else if (hdr[0] == 'x' && hdr[1] == 'r' && hdr[2] == 'm' && + hdr[3] == 'w') + magic = "xrmw"; + else + return 0; + + if (cau < FMSS_NUM_CAU) { + vfl_ctx_cau[cau] = cau; + vfl_ctx_block[cau] = block; + } + + for (i = 0; i < 256; i++) { + memcpy(&phys, tab + i * 4, 4); + phys = le32_to_cpu(phys); + if (!phys || phys >= FMSS_BLOCKS_PER_CAU) + continue; + virt = i; + if (vfl_map_count < FMSS_VFL_MAP_MAX) { + vfl_map[vfl_map_count].cau = cau; + vfl_map[vfl_map_count].virt = virt; + vfl_map[vfl_map_count].phys = phys; + vfl_map_count++; + added++; + } + } + fmss_dev_info(f->dev, "vfl_ingest cau=%u blk=%u magic=%s entries=%u total=%u\n", + cau, block, magic, added, vfl_map_count); + return added; +} + +static void fmss_vfl_format_log(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, u32 addr) +{ + unsigned int i, off = 0; + u32 t0, t1, t2, t3; + const char *magic = "????"; + + if (f->last_page[0] == 'w' && f->last_page[1] == 'r' && + f->last_page[2] == 'm' && f->last_page[3] == 'x') + magic = "wrmx"; + else if (f->last_page[0] == 'x' && f->last_page[1] == 'r' && + f->last_page[2] == 'm' && f->last_page[3] == 'w') + magic = "xrmw"; + + memcpy(&t0, f->last_page + 256, 4); + memcpy(&t1, f->last_page + 260, 4); + memcpy(&t2, f->last_page + 264, 4); + memcpy(&t3, f->last_page + 268, 4); + /* Also dump first table dwords @ +0x100 (OSOS 4EB7E4). */ + if (off < PAGE_SIZE - 80) { + u32 u0, u1, u2, u3; + + memcpy(&u0, f->last_page + 0x100, 4); + memcpy(&u1, f->last_page + 0x104, 4); + memcpy(&u2, f->last_page + 0x108, 4); + memcpy(&u3, f->last_page + 0x10c, 4); + off += scnprintf(vfl_log + off, PAGE_SIZE - off, + "table+0x100: %08x %08x %08x %08x\n", + le32_to_cpu(u0), le32_to_cpu(u1), + le32_to_cpu(u2), le32_to_cpu(u3)); + } + + off += scnprintf(vfl_log + off, PAGE_SIZE - off, + "ce=%u cau=%u blk=%u addr=0x%08x magic=%s\n", + ce, cau, block, addr, magic); + off += scnprintf(vfl_log + off, PAGE_SIZE - off, + "table+256: %08x %08x %08x %08x\n", + le32_to_cpu(t0), le32_to_cpu(t1), + le32_to_cpu(t2), le32_to_cpu(t3)); + for (i = 0; i < FMSS_VFL_HDR_LEN && off < PAGE_SIZE - 4; i++) { + off += scnprintf(vfl_log + off, PAGE_SIZE - off, "%02x%s", + f->last_page[i], ((i + 1) % 16) ? " " : "\n"); + } + vfl_log_len = off; +} + +static ssize_t vfl_log_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!vfl_log_len) + return sysfs_emit(buf, "(no vfl_dump yet)\n"); + memcpy(buf, vfl_log, min(vfl_log_len, PAGE_SIZE)); + return min(vfl_log_len, PAGE_SIZE); +} +static DEVICE_ATTR_RO(vfl_log); + +static ssize_t sector_hex_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!sector_log_len) + return sysfs_emit(buf, "(no lpn_read yet)\n"); + memcpy(buf, sector_log, sector_log_len); + return sector_log_len; +} +static DEVICE_ATTR_RO(sector_hex); + +static ssize_t lpn_index_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + unsigned int i, n = 0; + + if (!lpn_index_count) + return sysfs_emit(buf, "(empty — run lpn_read or lpn_build)\n"); + for (i = 0; i < lpn_index_count; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "lpn=%u ce=%u cau=%u blk=%u pg=%u\n", + lpn_index[i].lpn, lpn_index[i].ce, + lpn_index[i].cau, lpn_index[i].block, + lpn_index[i].page); + return n; +} +static DEVICE_ATTR_RO(lpn_index); + +/* + * Read one VFL context page (SLC page 0) and capture 512-byte header in vfl_log. + * Usage: echo "CE CAU BLOCK" > vfl_dump (CE/CAU default 0) + */ +static ssize_t vfl_dump_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce = 0, cau = 0, block; + unsigned int saved; + u32 addr; + int ret, nf; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u %u", &ce, &cau, &block); + if (nf == 1) { + block = ce; + ce = 0; + cau = 0; + } else if (nf != 3) { + return -EINVAL; + } + if (ce >= FMSS_NUM_CE || cau >= FMSS_NUM_CAU || + block >= FMSS_BLOCKS_PER_CAU) + return -EINVAL; + + mutex_lock(&f->lock); + saved = page_chunks; + page_chunks = 1; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, block, 0, 1); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (!ret) + fmss_vfl_format_log(f, ce, cau, block, addr); + if (!ret) + fmss_vfl_ingest(f, cau, block, f->last_page); + mutex_unlock(&f->lock); + fmss_dev_info(dev, "vfl_dump ce=%u cau=%u blk=%u ret=%d\n", ce, cau, block, ret); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(vfl_dump); + +static int fmss_find_lpn(struct fmss_n31 *f, unsigned int target_lpn, + unsigned int *oce, unsigned int *ocau, + unsigned int *oblock, unsigned int *opage); + +static int fmss_lpn_resolve(struct fmss_n31 *f, unsigned int target_lpn, + unsigned int *oce, unsigned int *ocau, + unsigned int *oblock, unsigned int *opage) +{ + unsigned int i; + + if (!fmss_l2v_lookup(target_lpn, oce, ocau, oblock, opage)) + return 0; + + for (i = 0; i < lpn_index_count; i++) { + if (lpn_index[i].lpn != target_lpn) + continue; + *oce = lpn_index[i].ce; + *ocau = lpn_index[i].cau; + *oblock = lpn_index[i].block; + *opage = lpn_index[i].page; + return 0; + } + return fmss_find_lpn(f, target_lpn, oce, ocau, oblock, opage); +} + +static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, u8 *dst, unsigned int dst_len) +{ + unsigned int vblock, saved; + u32 addr; + int ret; + + vblock = fmss_vfl_phys(cau, block); + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, vblock, page, 0); + if (f->dma_ok && use_dma) { + ret = fmss_dma_page_read(f, ce, addr); + if (ret) + ret = fmss_page_read(f, ce, addr); + } else { + ret = fmss_page_read(f, ce, addr); + } + f->pages_since_reset++; + page_chunks = saved; + if (!ret && dst && dst_len) { + unsigned int n = min(dst_len, f->last_page_len); + + memcpy(dst, f->last_page, n); + } + return ret; +} + +static int fmss_find_lpn(struct fmss_n31 *f, unsigned int target_lpn, + unsigned int *oce, unsigned int *ocau, + unsigned int *oblock, unsigned int *opage) +{ + unsigned int ce, cau, b, p, saved, limit, best = ~0u; + u32 addr; + int ret, found = 0; + + limit = lpn_scan_blocks; + if (!limit || limit > FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + limit = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = 0; b < limit; b++) { + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, b, FMSS_BTOC_PAGE, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 64)) + continue; + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + unsigned int score, pg = p; + unsigned int fat_off = 0; + bool fat_ok = false; + + if (fmss_btoc_entry(f->last_page, p) != target_lpn) + continue; + if (!fmss_btoc_plausible(f->last_page, target_lpn, &pg)) + continue; + if (fmss_read_lpn_page(f, ce, cau, b, pg, NULL, 0)) + continue; + if (target_lpn == 0) { + fat_ok = fmss_page_has_fat_boot( + f->last_page, f->last_page_len, + &fat_off); + if (!fat_ok) + continue; + } else if (fmss_page_blankish(f->last_page, 64)) { + continue; + } + /* Prefer ce0/cau0, lower blocks, LPN0 with FAT @ 0. */ + score = ce * 1000000u + cau * 100000u + b * 100u + pg; + if (target_lpn == 0 && fat_off == 0) + score /= 10; + if (score < best) { + best = score; + *oce = ce; + *ocau = cau; + *oblock = b; + *opage = pg; + found = 1; + } + } + } + } + } + page_chunks = saved; + return found ? 0 : -ENOENT; +} + +static void fmss_boot_apply_bpb(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, const u8 *bpb) +{ + u16 bps = get_unaligned_le16(bpb + 11); + u16 rsv = get_unaligned_le16(bpb + 14); + u32 fatz = get_unaligned_le32(bpb + 36); + + boot_carve_valid = true; + boot_carve_ce = ce; + boot_carve_cau = cau; + boot_carve_block = block; + boot_carve_page = page; + boot_carve_off = 0; + boot_data_start = fmss_bpb_data_start(bpb); + if (bps == FMSS_SECTOR_LEN && rsv && rsv < 4096) + boot_reserved_sects = rsv; + else + boot_reserved_sects = 32; + if (fatz && fatz < 0x100000u) + boot_fat_sects = fatz; + /* L2V[0] must point at this real boot page. */ + fmss_l2v_set(0, ce, cau, block, page); + fmss_dev_info(f->dev, + "boot_sb ce=%u cau=%u blk=%u pg=%u DataStart=%u rsv=%u fatz=%u\n", + ce, cau, block, page, boot_data_start, + boot_reserved_sects, boot_fat_sects); +} + +/* + * Accept ONLY an aligned live boot sector: BPB at page offset 0 with + * 55AA@510. Mid-page *UOKJIHC (e.g. off=7816) is a file copy — reject. + */ +static int fmss_boot_carve_try(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page) +{ + int ret; + + ret = fmss_read_lpn_page(f, ce, cau, block, page, NULL, 0); + if (ret) + return 0; + if (f->last_page_len < FMSS_SECTOR_LEN) + return 0; + if (!fmss_apple_fat_boot(f->last_page)) + return 0; + fmss_boot_apply_bpb(f, ce, cau, block, page, f->last_page); + return 1; +} + +/* + * Try aligned BPB on page p; on success ingest the saved BTOC table. + */ +static int fmss_boot_try_btoc_page(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + const u8 *btoc_page, unsigned int page) +{ + u8 *btoc_save; + int ok; + + btoc_save = kmemdup(btoc_page, FMSS_PAGE_LEN, GFP_KERNEL); + if (!btoc_save) + return 0; + ok = fmss_boot_carve_try(f, ce, cau, block, page); + if (ok) { + fmss_l2v_ingest_btoc(f, ce, cau, block, btoc_save, + l2v_max_lpn ? l2v_max_lpn : + FMSS_L2V_DEFAULT_MAX_LPN); + } + kfree(btoc_save); + return ok; +} + +/* + * Find boot superblock: ingestible BTOC with LPN0 in any slot (or boot-ish + * BTOC[0]==0 && BTOC[1] in {1,vbas_per_page}), then aligned BPB on that page. + * Falls back to page-0 aligned BPB scan (never mid-page OEM). + */ +static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, + unsigned int nblocks) +{ + unsigned int ce, cau, b, p, saved; + u32 addr, l0, l1; + int ret; + bool use_le; + + /* Optional explicit cache — only if params describe an aligned page. */ + if (boot_carve_block_param && boot_carve_off_param == 0) { + if (fmss_boot_carve_try(f, boot_carve_ce_param, + boot_carve_cau_param, + boot_carve_block_param, + boot_carve_page_param ? + boot_carve_page_param : 0)) + return 0; + fmss_dev_info(f->dev, + "cached boot_sb blk%u miss — scanning BTOC LPN0\n", + boot_carve_block_param); + } + + if (!nblocks) + nblocks = l2v_auto_blocks ? l2v_auto_blocks : 512; + if (nblocks > FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + nblocks = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + nblocks; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + if (reset_every && + f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, b, FMSS_BTOC_PAGE, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 64)) + continue; + use_le = fmss_btoc_prefer_le(f->last_page); + l0 = use_le ? fmss_btoc_entry_le(f->last_page, 0) + : fmss_btoc_entry_be(f->last_page, 0); + l1 = use_le ? fmss_btoc_entry_le(f->last_page, 1) + : fmss_btoc_entry_be(f->last_page, 1); + /* + * LPN0 candidate: BTOC[0]==0 (even if [1] is junk — + * live ce1/cau1/blk63). Do not scan every random + * zero dword in non-ingestible pages (wedges NAND). + */ + if (l0 == 0) { + page_chunks = 16; + if (fmss_boot_try_btoc_page(f, ce, cau, b, + f->last_page, + 0)) { + page_chunks = saved; + return 0; + } + page_chunks = 1; + } + if (!fmss_btoc_ingestible(f->last_page)) + continue; + for (p = 1; p < FMSS_BTOC_PAGE; p++) { + u32 lpn = use_le ? + fmss_btoc_entry_le(f->last_page, p) : + fmss_btoc_entry_be(f->last_page, p); + + if (lpn != 0) + continue; + page_chunks = 16; + if (fmss_boot_try_btoc_page(f, ce, cau, b, + f->last_page, + p)) { + page_chunks = saved; + return 0; + } + page_chunks = 1; + } + } + } + } + + /* + * Aligned BPB: page0 of each block, then all pages of open SBs + * (page0 programmed && page127 not closed BTOC/BTE). Never mid-page OEM. + */ + page_chunks = 16; + { + unsigned int boot_scan = nblocks ? nblocks : 256; + + if (boot_scan > 512) + boot_scan = 512; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + boot_scan; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + if (fmss_boot_carve_try(f, ce, cau, b, 0)) { + page_chunks = saved; + return 0; + } + } + } + } + + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + boot_scan; b++) { + unsigned int pg; + u32 addr; + int r; + bool closed; + + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + if (reset_every && + f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, b, 0, 0); + r = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (r || fmss_page_blankish(f->last_page, 64)) + continue; + addr = fmss_ppn_addr(cau, b, FMSS_BTOC_PAGE, 0); + r = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + closed = !r && !fmss_page_blankish(f->last_page, 64) && + (fmss_btoc_ingestible(f->last_page) || + fmss_page_looks_bte(f->last_page)); + if (closed) + continue; + + page_chunks = 16; + for (pg = 0; pg < FMSS_BTOC_PAGE; pg++) { + unsigned int sec; + + if (fmss_boot_carve_try(f, ce, cau, b, pg)) { + page_chunks = saved; + return 0; + } + if (fmss_read_lpn_page(f, ce, cau, b, pg, + NULL, 0)) + continue; + for (sec = 1; sec < FMSS_VBAS_PER_PAGE; sec++) { + unsigned int off = sec * FMSS_SECTOR_LEN; + const u8 *s = f->last_page + off; + + if (off + FMSS_SECTOR_LEN > f->last_page_len) + break; + if (!fmss_apple_fat_boot(s)) + continue; + fmss_boot_apply_bpb(f, ce, cau, b, pg, s); + fmss_early_lba_ensure(); + fmss_early_lba_set(0, ce, cau, b, pg, sec); + page_chunks = saved; + fmss_dev_info(f->dev, + "boot_sb open-SB sec=%u ce=%u cau=%u blk=%u pg=%u\n", + sec, ce, cau, b, pg); + return 0; + } + } + page_chunks = 1; + } + } + } + } + page_chunks = saved; + return -ENOENT; +} + +static bool fmss_page_has_n31os_dirent(const u8 *page, unsigned int len) +{ + unsigned int off, flags; + static const char *const needles[] = { + "N31OS", "n31os", "README", "IPOD_CON", "iPod_C", NULL + }; + unsigned int n; + + /* FAT 8.3 short names (ASCII, space-padded) — LE cluster fields elsewhere. */ + for (off = 0; off + 32 <= len; off += 32) { + if (!memcmp(page + off, "N31OS ", 8) || + !memcmp(page + off, "README ", 8) || + !memcmp(page + off, "IPOD_CON", 8)) + return true; + } + for (n = 0; needles[n]; n++) { + flags = fmss_find_enc(page, len, needles[n], + strlen(needles[n]), &off); + if (flags) + return true; + } + return false; +} + +static int fmss_root_dir_discover(struct fmss_n31 *f, unsigned int start, + unsigned int nblocks) +{ + unsigned int ce, cau, b, p, lpn; + unsigned int saved, pages = 0; + const unsigned int page_cap = 4096; + + lpn = boot_data_start / FMSS_FTL_SECTORS_PER_LPN; + if (!fmss_l2v_lookup(lpn, &ce, &cau, &b, &p)) { + root_dir_valid = true; + root_dir_ce = ce; + root_dir_cau = cau; + root_dir_block = b; + root_dir_page = p; + root_dir_lpn = lpn; + return 0; + } + + if (!nblocks) + nblocks = 24; + if (nblocks > 48) + nblocks = 48; + + saved = page_chunks; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + nblocks; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + if (pages >= page_cap) + goto out; + if (fmss_read_lpn_page(f, ce, cau, b, p, + NULL, 0)) + continue; + pages++; + if (!fmss_page_has_n31os_dirent( + f->last_page, + f->last_page_len)) + continue; + root_dir_valid = true; + root_dir_ce = ce; + root_dir_cau = cau; + root_dir_block = b; + root_dir_page = p; + root_dir_lpn = lpn; + fmss_l2v_set(lpn, ce, cau, b, p); + page_chunks = saved; + fmss_dev_info(f->dev, + "root_dir N31OS ce=%u cau=%u blk=%u pg=%u lpn=%u\n", + ce, cau, b, p, lpn); + return 0; + } + } + } + } +out: + page_chunks = saved; + return -ENOENT; +} + +static void fmss_l2v_try_block_map_page(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int max_lpn) +{ + bool be = false; + unsigned int nents = 0, i, saved, take; + u32 addr; + u8 *mapbuf; + int ret; + + if (fmss_read_lpn_page(f, ce, cau, block, 0, NULL, 0)) + return; + if (!fmss_page_looks_block_map(f->last_page, f->last_page_len, + &be, &nents)) + return; + + take = min(nents, 64u); + mapbuf = kmalloc(take * 2, GFP_KERNEL); + if (!mapbuf) + return; + memcpy(mapbuf, f->last_page, take * 2); + l2v_bmap_hits++; + + saved = page_chunks; + page_chunks = 1; + for (i = 0; i < take; i++) { + u16 vbn = be ? get_unaligned_be16(mapbuf + i * 2) + : get_unaligned_le16(mapbuf + i * 2); + + if (!vbn || vbn >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + continue; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, fmss_vfl_phys(cau, vbn), + FMSS_BTOC_PAGE, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 64)) + continue; + fmss_l2v_ingest_btoc(f, ce, cau, vbn, f->last_page, max_lpn); + } + page_chunks = saved; + kfree(mapbuf); +} + +/* + * Build dense L2V from BTOC page 127 (+ optional classic block-map pages). + * Bounded: [start, start+nblocks) per CE/CAU. Also carve boot + root dir. + */ +static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, + unsigned int start, unsigned int nblocks) +{ + unsigned int ce, cau, b, saved; + u32 addr; + int ret; + + if (!max_lpn) + max_lpn = FMSS_L2V_DEFAULT_MAX_LPN; + if (!nblocks) + nblocks = l2v_auto_blocks; /* 0 = carve-only, no BTOC walk */ + if (nblocks > FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + nblocks = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + if (start >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + start = 0; + + ret = fmss_l2v_ensure(max_lpn); + if (ret) + return ret; + + /* Rebuild map contents for this pass (keep allocation). */ + memset(l2v_map, 0, l2v_map_size * sizeof(*l2v_map)); + l2v_mapped = 0; + l2v_btoc_hits = 0; + l2v_bmap_hits = 0; + l2v_meta_hits = 0; + lpn_index_count = 0; + boot_carve_valid = false; + root_dir_valid = false; + if (early_lba_map) { + memset(early_lba_map, 0, + FMSS_EARLY_LBA_MAX * sizeof(*early_lba_map)); + early_lba_mapped = 0; + } else { + fmss_early_lba_ensure(); + } + + saved = page_chunks; + page_chunks = 1; + for (ce = 0; nblocks && ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + unsigned int bmap_probes = 0; + + for (b = start; b < start + nblocks; b++) { + bool btoc_ok; + + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + if (reset_every && + f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, b, FMSS_BTOC_PAGE, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + btoc_ok = !ret && + !fmss_page_blankish(f->last_page, 64); + if (btoc_ok) { + if (fmss_btoc_ingestible(f->last_page)) + fmss_l2v_ingest_btoc(f, ce, cau, b, + f->last_page, + max_lpn); + else + fmss_l2v_ingest_bte(f, ce, cau, b, + f->last_page, + max_lpn); + } + + /* + * Classic block-map heuristic only when BTOC is + * blank — capped probes to avoid wedging. + */ + if (!btoc_ok && bmap_probes < 32) { + page_chunks = 16; + fmss_l2v_try_block_map_page(f, ce, cau, + b, max_lpn); + page_chunks = 1; + bmap_probes++; + } + } + } + } + page_chunks = saved; + + fmss_boot_carve_discover(f, start, nblocks); + if (nblocks) + fmss_root_dir_discover(f, start ? start : 32, + min_t(unsigned int, nblocks, 48)); + + fmss_dev_info(f->dev, + "l2v_build max_lpn=%u range=%u+%u mapped=%u btoc=%u bmap=%u boot=%d root=%d\n", + max_lpn, start, nblocks, l2v_mapped, l2v_btoc_hits, + l2v_bmap_hits, boot_carve_valid, root_dir_valid); + return 0; +} + +static int fmss_build_lpn_index(struct fmss_n31 *f, unsigned int max_lpn) +{ + unsigned int nblocks = l2v_scan_blocks; + + if (!nblocks) + nblocks = lpn_scan_blocks; + if (!nblocks) + nblocks = FMSS_L2V_DEFAULT_BLOCKS; + return fmss_l2v_build(f, max_lpn, 0, nblocks); +} + +static ssize_t lpn_build_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int max_lpn = 32; + int ret; + + if (!f) + return -ENODEV; + if (buf[0] && buf[0] != '\n' && kstrtouint(buf, 0, &max_lpn)) + return -EINVAL; + mutex_lock(&f->lock); + ret = fmss_build_lpn_index(f, max_lpn); + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(lpn_build); + +/* + * Explicit dense L2V build (bounded). Usage: + * echo 1 > l2v_build + * echo "NBLOCKS" > l2v_build + * echo "START NBLOCKS [MAX_LPN]" > l2v_build + */ +static ssize_t l2v_build_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int start = 0, nblocks = 0, max_lpn = 0; + int nf, ret; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u %u", &start, &nblocks, &max_lpn); + if (nf == 1) { + if (start == 1) { + start = 0; + nblocks = l2v_scan_blocks; + } else { + nblocks = start; + start = 0; + } + } else if (nf == 2) { + ; /* start + nblocks */ + } else if (nf >= 3) { + ; /* start + nblocks + max_lpn */ + } else if (buf[0] && buf[0] != '\n') { + return -EINVAL; + } + if (!nblocks) + nblocks = l2v_scan_blocks ? l2v_scan_blocks + : FMSS_L2V_DEFAULT_BLOCKS; + if (!max_lpn) + max_lpn = FMSS_L2V_DEFAULT_MAX_LPN; + + mutex_lock(&f->lock); + ret = fmss_l2v_build(f, max_lpn, start, nblocks); + mutex_unlock(&f->lock); + if (ret) + return ret; + dev_info(dev, + "l2v_build done mapped=%u btoc=%u bmap=%u boot=%u root=%u DataStart=%u\n", + l2v_mapped, l2v_btoc_hits, l2v_bmap_hits, + boot_carve_valid, root_dir_valid, boot_data_start); + return count; +} +static DEVICE_ATTR_WO(l2v_build); + +static ssize_t l2v_status_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, + "mapped=%u max_lpn=%u size=%u btoc_hits=%u bmap_hits=%u meta_hits=%u early_lba=%u\n" + "boot_carve=%u ce=%u cau=%u blk=%u pg=%u off=%u DataStart=%u\n" + "root_dir=%u ce=%u cau=%u blk=%u pg=%u lpn=%u\n" + "lpn_index=%u\n" + "whimory ret=%d dis=%u vfl=%u ftlctrl=%u bmap_pages=%u map_ents=%u filled=%u\n", + l2v_mapped, l2v_max_lpn, l2v_map_size, l2v_btoc_hits, + l2v_bmap_hits, l2v_meta_hits, early_lba_mapped, + boot_carve_valid, boot_carve_ce, boot_carve_cau, + boot_carve_block, boot_carve_page, boot_carve_off, + boot_data_start, + root_dir_valid, root_dir_ce, root_dir_cau, root_dir_block, + root_dir_page, root_dir_lpn, + lpn_index_count, + wmr_mount_ret, wmr_dis_hits, wmr_vfl_hits, wmr_ftlctrl_hits, + wmr_bmap_pages, wmr_block_map_n, wmr_l2v_filled); +} +static DEVICE_ATTR_RO(l2v_status); + +static ssize_t whimory_status_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, + "ret=%d\n" + "deviceinfosign_hits=%u first=ce%u/cau%u/blk%u/pg%u\n" + "vfl_hits=%u ftlctrl_hits=%u ftlctrl=%u,%u,%u\n" + "bmap_pages=%u map_ents=%u l2v_filled=%u\n", + wmr_mount_ret, + wmr_dis_hits, wmr_dis_ce, wmr_dis_cau, wmr_dis_block, + wmr_dis_page, + wmr_vfl_hits, wmr_ftlctrl_hits, + wmr_ftlctrl_n > 0 ? wmr_ftlctrl[0] : 0, + wmr_ftlctrl_n > 1 ? wmr_ftlctrl[1] : 0, + wmr_ftlctrl_n > 2 ? wmr_ftlctrl[2] : 0, + wmr_bmap_pages, wmr_block_map_n, wmr_l2v_filled); +} +static DEVICE_ATTR_RO(whimory_status); + +/* + * Classic Whimory mount (bounded). Usage: + * echo 1 > whimory_mount + * echo "NBLOCKS" > whimory_mount + * echo "NBLOCKS MAX_LPN" > whimory_mount + */ +static ssize_t whimory_mount_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int nblocks = 0, max_lpn = 0; + int nf, ret; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u", &nblocks, &max_lpn); + if (nf == 1 && nblocks == 1) + nblocks = 0; /* echo 1 → defaults */ + else if (nf < 1 && buf[0] && buf[0] != '\n') + return -EINVAL; + if (!nblocks) + nblocks = min(grep_max_blocks ? grep_max_blocks : 32u, + WMR_MOUNT_MAX_BLOCKS); + if (!max_lpn) + max_lpn = FMSS_L2V_DEFAULT_MAX_LPN; + + mutex_lock(&f->lock); + ret = fmss_whimory_mount(f, nblocks, max_lpn); + mutex_unlock(&f->lock); + dev_info(dev, + "whimory_mount done ret=%d map_ents=%u filled=%u (l2v mapped=%u)\n", + ret, wmr_block_map_n, wmr_l2v_filled, l2v_mapped); + /* Mount soft-fails with -ENOENT when nothing found; still accept write. */ + if (ret && ret != -ENOENT) + return ret; + return count; +} +static DEVICE_ATTR_WO(whimory_mount); + +/* + * Resolve LPN → NAND page and copy one 4096-byte logical sector (must hold f->lock). + * Uses dense L2V / root-dir cache only — no on-demand BTOC scan (avoids wedging + * the device on sparse unmapped reads). Returns -ENOENT if unmapped. + */ +static int fmss_ftl_read_lpn_locked(struct fmss_n31 *f, unsigned int target_lpn, + unsigned int sector, u8 *buf) +{ + unsigned int ce, cau, block, page, vblock, off, saved; + u32 addr; + int ret; + + if (sector > FMSS_FTL_SECTORS_PER_LPN - 1) + return -EINVAL; + + if (root_dir_valid && target_lpn == root_dir_lpn) { + ce = root_dir_ce; + cau = root_dir_cau; + block = root_dir_block; + page = root_dir_page; + } else { + ret = fmss_l2v_lookup(target_lpn, &ce, &cau, &block, &page); + if (ret) + return ret; + } + + vblock = fmss_vfl_phys(cau, block); + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, vblock, page, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (ret) + return ret; + + off = sector * FMSS_SECTOR_LEN; + if (off + FMSS_SECTOR_LEN > f->last_page_len) + return -ERANGE; + + memcpy(buf, f->last_page + off, FMSS_SECTOR_LEN); + return 0; +} + +/* + * Read logical page N (BTOC LPN) and expose 4 KiB sector via sector_hex. + * Usage: echo "LPN [sector_in_page]" > lpn_read (sector 0..3, default 0) + */ +static ssize_t lpn_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int target_lpn, sector = 0; + unsigned int i, ce, cau, block, page, poff, saved; + u8 *secbuf; + u32 addr; + int ret, nf; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u", &target_lpn, §or); + if (nf < 1) + return -EINVAL; + if (sector > 3) + return -EINVAL; + + secbuf = kmalloc(FMSS_SECTOR_LEN, GFP_KERNEL); + if (!secbuf) + return -ENOMEM; + + mutex_lock(&f->lock); + /* Interactive: on-demand BTOC resolve; block I/O uses dense map only. */ + ret = fmss_lpn_resolve(f, target_lpn, &ce, &cau, &block, &page); + if (!ret) { + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, fmss_vfl_phys(cau, block), page, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (!ret) { + poff = sector * FMSS_SECTOR_LEN; + if (poff + FMSS_SECTOR_LEN <= f->last_page_len) + memcpy(secbuf, f->last_page + poff, + FMSS_SECTOR_LEN); + else + ret = -ERANGE; + } + } + mutex_unlock(&f->lock); + if (ret) { + dev_warn(dev, "lpn_read %u: resolve/read failed (%d)\n", + target_lpn, ret); + kfree(secbuf); + return ret; + } + + sector_log_len = 0; + for (i = 0; i < 128; i++) { + sector_log_len += scnprintf(sector_log + sector_log_len, + sizeof(sector_log) - sector_log_len, + "%02x%s", + secbuf[i], + ((i + 1) % 16) ? " " : "\n"); + } + + fmss_dev_info(dev, + "lpn=%u sector=%u head=%02x%02x%02x%02x\n", + target_lpn, sector, secbuf[0], secbuf[1], secbuf[2], secbuf[3]); + kfree(secbuf); + return count; +} +static DEVICE_ATTR_WO(lpn_read); + +static char grep_log[4096]; +static unsigned int grep_log_len; + +static ssize_t grep_log_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!grep_log_len) + return sysfs_emit(buf, "(no ftl_grep yet)\n"); + memcpy(buf, grep_log, min(grep_log_len, PAGE_SIZE)); + return min(grep_log_len, PAGE_SIZE); +} +static DEVICE_ATTR_RO(grep_log); + +/* + * Walk FTL superblocks and search page data for a short ASCII needle. + * Usage: echo "START N_BLOCKS NEEDLE" > ftl_grep + * echo "32 24 N31OS" > ftl_grep (defaults: start=32, n=24, N31OS) + * Caps N_BLOCKS at FMSS_GREP_MAX_BLOCKS per call to avoid RetailOS watchdog. + */ +static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, b, p, saved, start = 32, nblocks = 16; + unsigned int pages_done = 0, hits = 0; + char needle[48] = "N31OS"; + size_t nlen = 5; + int ret, nf; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u %47s", &start, &nblocks, needle); + if (nf >= 3) + nlen = strnlen(needle, sizeof(needle) - 1); + else if (nf == 2) + ; /* start + nblocks, default needle */ + else if (nf == 1 && start == 1) + start = 32; + if (nblocks == 0) + nblocks = 16; + if (nblocks > grep_max_blocks) + nblocks = grep_max_blocks; + if (start >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + start = 32; + + grep_log_len = 0; + mutex_lock(&f->lock); + saved = page_chunks; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + nblocks; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + /* + * Do not require BTOC page 127 — Apple FAT clusters + * live in data pages even when BTOC looks blank. + * (Root cause: old code skipped whole superblocks.) + */ + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + unsigned int off = 0, show, flags; + u32 lpn = ~0u; + + ret = fmss_read_lpn_page(f, ce, cau, b, p, + NULL, 0); + pages_done++; + if (ret) + continue; + flags = fmss_find_enc(f->last_page, + f->last_page_len, + needle, nlen, + &off); + if (!flags) + continue; + hits++; + fmss_dev_info(dev, + "grep hit ce=%u cau=%u blk=%u pg=%u off=%u enc=%s\n", + ce, cau, b, p, off, + fmss_match_enc_name(flags)); + if (grep_log_len < sizeof(grep_log) - 160) { + show = min(240u, + f->last_page_len - off); + grep_log_len += scnprintf( + grep_log + grep_log_len, + sizeof(grep_log) - grep_log_len, + "ce=%u cau=%u blk=%u pg=%u off=%u enc=%s\n", + ce, cau, b, p, off, + fmss_match_enc_name(flags)); + if (flags & FMSS_MATCH_ASCII) + grep_log_len += scnprintf( + grep_log + grep_log_len, + sizeof(grep_log) - grep_log_len, + "%.*s\n\n", + show, + f->last_page + off); + else + grep_log_len += scnprintf( + grep_log + grep_log_len, + sizeof(grep_log) - grep_log_len, + "(wide/bswap match, %u bytes from off)\n\n", + show); + } + (void)lpn; + } + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + if (!grep_log_len) + grep_log_len = scnprintf(grep_log, sizeof(grep_log), + "NO HIT needle=%s pages=%u (tried ascii/utf16le/utf16be/bswap16)\n", + needle, pages_done); + fmss_dev_info(dev, "ftl_grep start=%u n=%u needle=%s pages=%u hits=%u\n", + start, nblocks, needle, pages_done, hits); + return count; +} +static DEVICE_ATTR_WO(ftl_grep); + +/* + * Dump ASCII from a specific FTL page offset (after ftl_grep locates a file). + * Usage: echo "CE CAU BLK PG OFF LEN" > ftl_ascii (LEN default 512, max 2048) + */ +static ssize_t readme_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, b, p, saved, start = 32, nblocks = 48; + unsigned int pages_done = 0; + const char *needle = "N31OS boot files on the RetailOS FAT volume"; + size_t nlen = 43; + int ret, nf, found = 0; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u", &start, &nblocks); + if (nf == 1 && start == 1) + start = 32; + if (nblocks == 0) + nblocks = 48; + if (nblocks > grep_max_blocks) + nblocks = grep_max_blocks; + + grep_log_len = 0; + mutex_lock(&f->lock); + saved = page_chunks; + /* CE0 first — matches disk-mode primary LUN behaviour. */ + for (ce = 0; ce < FMSS_NUM_CE && !found; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU && !found; cau++) { + for (b = start; b < start + nblocks && !found; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + for (p = 0; p < FMSS_BTOC_PAGE && !found; p++) { + unsigned int off, show, dump; + + if (reset_every && + f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + ret = fmss_read_lpn_page(f, ce, cau, b, p, + NULL, 0); + pages_done++; + if (ret) + continue; + if (!fmss_find(f->last_page, + f->last_page_len, + needle, nlen)) + continue; + off = 0; + while (off + nlen <= f->last_page_len) { + if (!memcmp(f->last_page + off, + needle, nlen)) + break; + off++; + } + found = 1; + dump = min(1024u, f->last_page_len - off); + grep_log_len = scnprintf( + grep_log, sizeof(grep_log), + "OK ce=%u cau=%u blk=%u pg=%u off=%u len=%u pages=%u\n%.*s\n", + ce, cau, b, p, off, dump, pages_done, + dump, f->last_page + off); + fmss_dev_info(dev, "readme_read FOUND ce=%u cau=%u blk=%u pg=%u off=%u\n", + ce, cau, b, p, off); + } + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + if (!found) { + dev_warn(dev, "readme_read: not found start=%u n=%u pages=%u (stage D:\\n31os via install-n31os-disk.ps1?)\n", + start, nblocks, pages_done); + grep_log_len = scnprintf(grep_log, sizeof(grep_log), + "NOT FOUND (scanned %u pages from blk %u)\n", + pages_done, start); + return -ENOENT; + } + return count; +} +static DEVICE_ATTR_WO(readme_read); + +/* + * Locate Apple FAT32 boot sector by scanning FTL pages for EB3C90 *UOKJIHC. + * PIO only (fast). Usage: echo 1 > boot_read or echo "START N_BLOCKS" > boot_read + * Result in grep_log + sector_hex (512 B boot sector). + */ +static int fmss_read_ftl_page_pio(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page) +{ + unsigned int vblock, saved; + u32 addr; + int ret; + + vblock = fmss_vfl_phys(cau, block); + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, vblock, page, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + return ret; +} + +static ssize_t boot_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, b, saved, start = 32, nblocks = 32; + unsigned int fat_off = 0, sector_off = 0; + unsigned int pages_done = 0; + int ret, nf, found = 0; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u", &start, &nblocks); + if (nf == 1 && start == 1) + start = 32; + if (nblocks == 0) + nblocks = 32; + if (nblocks > grep_max_blocks) + nblocks = grep_max_blocks; + + grep_log_len = 0; + sector_log_len = 0; + mutex_lock(&f->lock); + saved = page_chunks; + page_chunks = 16; + + for (ce = 0; ce < FMSS_NUM_CE && !found; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU && !found; cau++) { + for (b = start; b < start + nblocks && !found; b++) { + unsigned int p; + + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + for (p = 0; p < FMSS_BTOC_PAGE && !found; p++) { + ret = fmss_read_ftl_page_pio(f, ce, cau, b, p); + pages_done++; + if (ret) + continue; + if (fmss_page_has_fat_boot(f->last_page, + f->last_page_len, + &fat_off)) { + sector_off = p; + found = 1; + } + } + } + } + } + + page_chunks = saved; + if (found) { + unsigned int i, dump = min(512u, f->last_page_len - fat_off); + + for (i = 0; i < dump; i++) { + sector_log_len += scnprintf(sector_log + sector_log_len, + sizeof(sector_log) - sector_log_len, + "%02x%s", + f->last_page[fat_off + i], + ((i + 1) % 16) ? " " : "\n"); + } + grep_log_len = scnprintf( + grep_log, sizeof(grep_log), + "OK boot ce=%u cau=%u blk=%u pg=%u off=%u pages=%u\nOEM=%.8s vol=%.11s\n", + ce, cau, b, sector_off, fat_off, pages_done, + f->last_page + fat_off + 3, + f->last_page + fat_off + 0x2b); + } else { + dev_warn(dev, "boot_read: no Apple FAT boot start=%u n=%u pages=%u\n", + start, nblocks, pages_done); + grep_log_len = scnprintf(grep_log, sizeof(grep_log), + "NOT FOUND (scanned %u pages from blk %u)\n", + pages_done, start); + } + mutex_unlock(&f->lock); + return found ? count : -ENOENT; +} +static DEVICE_ATTR_WO(boot_read); + +static ssize_t ftl_ascii_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, block, page, off = 0, len = 512; + int ret; + + if (!f) + return -ENODEV; + if (sscanf(buf, "%u %u %u %u %u %u", &ce, &cau, &block, &page, &off, &len) < 4) + return -EINVAL; + if (len > 2048) + len = 2048; + + mutex_lock(&f->lock); + ret = fmss_read_lpn_page(f, ce, cau, block, page, NULL, 0); + if (ret) { + mutex_unlock(&f->lock); + return ret; + } + if (off >= f->last_page_len) { + mutex_unlock(&f->lock); + return -ERANGE; + } + if (off + len > f->last_page_len) + len = f->last_page_len - off; + + grep_log_len = 0; + grep_log_len += scnprintf(grep_log, sizeof(grep_log), + "ce=%u cau=%u blk=%u pg=%u off=%u len=%u\n", + ce, cau, block, page, off, len); + grep_log_len += scnprintf(grep_log + grep_log_len, + sizeof(grep_log) - grep_log_len, + "%.*s\n", + len, f->last_page + off); + mutex_unlock(&f->lock); + fmss_dev_info(dev, "ftl_ascii ce=%u cau=%u blk=%u pg=%u off=%u len=%u\n", + ce, cau, block, page, off, len); + return count; +} +static DEVICE_ATTR_WO(ftl_ascii); + +/* + * OSOS 4EB7E4 / Sogeti PPN-VFL: walk last blocks of each CAU, SLC page 0. + * Without DMA meta we keep any non-blank data page and dump the 512-byte + * VFLCxt header plus the u32 table at +256. + */ +static ssize_t vfl_scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, i, saved_chunks; + unsigned int start = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + int hits = 0, xrmw = 0; + u32 addr, t0, t1, t2, t3; + int ret; + + if (!f) + return -ENODEV; + /* echo 1 just triggers. A value in [32, 2087] is the first CAU block. */ + if (buf[0] && buf[0] != '\n') { + if (kstrtouint(buf, 0, &start)) + return -EINVAL; + if (start < 32 || start >= FMSS_BLOCKS_PER_CAU) + start = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; + } + + mutex_lock(&f->lock); + saved_chunks = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (i = FMSS_BLOCKS_PER_CAU - 1; i >= start; i--) { + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, i, 0, 1); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret) + continue; + if (fmss_page_blankish(f->last_page, 512)) + continue; + hits++; + memcpy(&t0, f->last_page + 256, 4); + memcpy(&t1, f->last_page + 260, 4); + memcpy(&t2, f->last_page + 264, 4); + memcpy(&t3, f->last_page + 268, 4); + if (f->last_page[0] == 'x' && f->last_page[1] == 'r' && + f->last_page[2] == 'm' && f->last_page[3] == 'w') + xrmw++; + if (f->last_page[0] == 'w' && f->last_page[1] == 'r' && + f->last_page[2] == 'm' && f->last_page[3] == 'x') + xrmw++; + fmss_vfl_ingest(f, cau, i, f->last_page); + fmss_dev_info(dev, + "vfl ce=%u cau=%u blk=%u addr=0x%08x %02x %02x %02x %02x %02x %02x %02x %02x +256 %08x %08x %08x %08x\n", + ce, cau, i, addr, + f->last_page[0], f->last_page[1], + f->last_page[2], f->last_page[3], + f->last_page[4], f->last_page[5], + f->last_page[6], f->last_page[7], + le32_to_cpu(t0), le32_to_cpu(t1), + le32_to_cpu(t2), le32_to_cpu(t3)); + } + } + } + page_chunks = saved_chunks; + mutex_unlock(&f->lock); + fmss_dev_info(dev, "vfl_scan start_blk=%u hits=%d xrmw=%d\n", start, hits, xrmw); + return count; +} +static DEVICE_ATTR_WO(vfl_scan); + +static ssize_t vfl_map_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + unsigned int i, n = 0; + + if (!vfl_map_count) + return sysfs_emit(buf, "(empty — run vfl_scan or vfl_build)\n"); + for (i = 0; i < vfl_map_count && i < 32; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "cau=%u virt=%u phys=%u\n", + vfl_map[i].cau, vfl_map[i].virt, + vfl_map[i].phys); + if (vfl_map_count > 32) + n += scnprintf(buf + n, PAGE_SIZE - n, "... +%u more\n", + vfl_map_count - 32); + return n; +} +static DEVICE_ATTR_RO(vfl_map); + +/* + * Walk VFL tail (SLC page 0) on each CAU and ingest wrmx/xrmw remap tables. + * Usage: echo 1 > vfl_build (last vfl_build_blocks blocks, default 32) + * echo "START COUNT" > vfl_build (COUNT capped at FMSS_VFL_TAIL) + */ +static ssize_t vfl_build_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, i, saved, start, nblocks, scanned = 0; + int ingested = 0, ret; + + if (!f) + return -ENODEV; + nblocks = vfl_build_blocks; + if (!nblocks || nblocks > FMSS_VFL_TAIL) + nblocks = 32; + start = FMSS_BLOCKS_PER_CAU - nblocks; + if (buf[0] && buf[0] != '\n') { + unsigned int a, b = 0; + int nf = sscanf(buf, "%u %u", &a, &b); + + if (nf < 1) + return -EINVAL; + if (nf == 1 && a == 1) + ; /* echo 1 > vfl_build — tail defaults */ + else { + start = a; + if (nf >= 2 && b) + nblocks = b; + if (nblocks > FMSS_VFL_TAIL) + nblocks = FMSS_VFL_TAIL; + if (start >= FMSS_BLOCKS_PER_CAU) + return -EINVAL; + } + } + + mutex_lock(&f->lock); + vfl_map_count = 0; + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (i = 0; i < nblocks; i++) { + unsigned int blk = start + i; + u32 addr; + + if (blk >= FMSS_BLOCKS_PER_CAU) + break; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, blk, 0, 1); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + scanned++; + if (ret || fmss_page_blankish(f->last_page, 512)) + continue; + ingested += fmss_vfl_ingest(f, cau, blk, f->last_page); + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + fmss_dev_info(dev, "vfl_build start=%u n=%u scanned=%u ingested=%d map=%u\n", + start, nblocks, scanned, ingested, vfl_map_count); + return count; +} +static DEVICE_ATTR_WO(vfl_build); + +static char btoc_last[PAGE_SIZE]; +static unsigned int btoc_last_len; + +static ssize_t btoc_log_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!btoc_last_len) + return sysfs_emit(buf, "(no scan yet)\n"); + memcpy(buf, btoc_last, btoc_last_len); + return btoc_last_len; +} +static DEVICE_ATTR_RO(btoc_log); + +/* + * Sogeti/YaFTL BTOC: last page of a user superblock lists LPNs for pages 0..n-2. + * N31 page 127 is a BE u32 array (we saw 11,12,13,... on cau1 blk 64). + * Find the block whose BTOC[0]==0 — that page 0 is logical page 0 (FAT boot). + */ +static ssize_t btoc_scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, i, saved, start = 0, n = FMSS_BLOCKS_PER_CAU; + int hits = 0, lpn0 = 0; + u32 addr, a, b; + int ret; + + if (!f) + return -ENODEV; + btoc_last_len = 0; + if (buf[0] && buf[0] != '\n') { + if (sscanf(buf, "%u %u", &start, &n) < 1) + return -EINVAL; + if (n == 0 || n > FMSS_BLOCKS_PER_CAU) + n = FMSS_BLOCKS_PER_CAU; + if (start >= FMSS_BLOCKS_PER_CAU) + return -EINVAL; + if (start + n > FMSS_BLOCKS_PER_CAU) + n = FMSS_BLOCKS_PER_CAU - start; + } + mutex_lock(&f->lock); + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (i = start; i < start + n; i++) { + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, i, 127, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 16)) + continue; + a = ((u32)f->last_page[0] << 24) | ((u32)f->last_page[1] << 16) | + ((u32)f->last_page[2] << 8) | f->last_page[3]; + b = ((u32)f->last_page[4] << 24) | ((u32)f->last_page[5] << 16) | + ((u32)f->last_page[6] << 8) | f->last_page[7]; + hits++; + if (a == 0) + lpn0++; + if (a < 0x100000 && (b == a + 1 || a == 0)) { + fmss_dev_info(dev, + "btoc ce=%u cau=%u blk=%u addr=0x%08x lpn0=%u lpn1=%u %02x %02x %02x %02x\n", + ce, cau, i, addr, a, b, + f->last_page[0], f->last_page[1], + f->last_page[2], f->last_page[3]); + if (btoc_last_len < PAGE_SIZE - 80) + btoc_last_len += scnprintf( + btoc_last + btoc_last_len, + PAGE_SIZE - btoc_last_len, + "ce=%u cau=%u blk=%u lpn=%u,%u\n", + ce, cau, i, a, b); + } + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + fmss_dev_info(dev, "btoc_scan start=%u n=%u hits=%d lpn0=%d\n", start, n, hits, lpn0); + return count; +} +static DEVICE_ATTR_WO(btoc_scan); + +static ssize_t fat_scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, i, saved, start = 0, n = FMSS_BLOCKS_PER_CAU; + int hits = 0; + u32 addr; + int ret; + + if (!f) + return -ENODEV; + if (buf[0] && buf[0] != '\n' && sscanf(buf, "%u %u", &start, &n) >= 1) { + if (n == 0 || start + n > FMSS_BLOCKS_PER_CAU) + n = FMSS_BLOCKS_PER_CAU - start; + } + btoc_last_len = 0; + mutex_lock(&f->lock); + saved = page_chunks; + page_chunks = 1; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (i = start; i < start + n; i++) { + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, i, 0, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret) + continue; + if (fmss_find(f->last_page, 1024, "MSDOS", 5) || + fmss_find(f->last_page, 1024, "UOKJIHC", 7) || + fmss_find(f->last_page, 1024, "AISPOD", 6) || + fmss_find(f->last_page, 1024, "N31OS", 5) || + fmss_find(f->last_page, 1024, "FAT32", 5) || + fmss_find(f->last_page, 1024, "EXFAT", 5) || + fmss_find(f->last_page, 1024, "iPod_Control", 12) || + (f->last_page_len >= 512 && + fmss_apple_fat_boot(f->last_page))) { + hits++; + fmss_dev_info(dev, + "fat ce=%u cau=%u blk=%u addr=0x%08x %02x %02x %02x %02x %02x %02x %02x %02x\n", + ce, cau, i, addr, + f->last_page[0], f->last_page[1], + f->last_page[2], f->last_page[3], + f->last_page[4], f->last_page[5], + f->last_page[6], f->last_page[7]); + if (btoc_last_len < PAGE_SIZE - 80) + btoc_last_len += scnprintf( + btoc_last + btoc_last_len, + PAGE_SIZE - btoc_last_len, + "fat ce=%u cau=%u blk=%u p0 %02x %02x %02x %02x\n", + ce, cau, i, + f->last_page[0], f->last_page[1], + f->last_page[2], f->last_page[3]); + } + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + fmss_dev_info(dev, "fat_scan start=%u n=%u hits=%d\n", start, n, hits); + return count; +} +static DEVICE_ATTR_WO(fat_scan); + +/* + * Scan FTL user blocks for a FAT12/16/32 boot sector (0xEB/0xE9 + 0xAA55 @ +510). + * Usage: echo "START N_BLOCKS" > fat_boot_scan + */ +static ssize_t fat_boot_scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, b, p, saved, start = 32, nblocks = 24; + int hits = 0; + + if (!f) + return -ENODEV; + if (buf[0] && buf[0] != '\n' && sscanf(buf, "%u %u", &start, &nblocks) < 1) + return -EINVAL; + if (nblocks > grep_max_blocks) + nblocks = grep_max_blocks; + grep_log_len = 0; + mutex_lock(&f->lock); + saved = page_chunks; + for (ce = 0; ce < FMSS_NUM_CE; ce++) { + for (cau = 0; cau < FMSS_NUM_CAU; cau++) { + for (b = start; b < start + nblocks; b++) { + if (b >= FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + break; + for (p = 0; p < FMSS_BTOC_PAGE; p++) { + unsigned int off; + + if (fmss_read_lpn_page(f, ce, cau, b, p, NULL, 0)) + continue; + for (off = 0; off + 512 <= f->last_page_len; + off += 512) { + const u8 *s = f->last_page + off; + + if (s[0] != 0xeb && s[0] != 0xe9) + continue; + if (s[510] != 0x55 || s[511] != 0xaa) + continue; + if (!fmss_apple_fat_boot(s) && + !fmss_find(f->last_page + off, 512, + "FAT32", 5)) + continue; + hits++; + fmss_dev_info(dev, + "fat_boot ce=%u cau=%u blk=%u pg=%u off=%u oem=%.8s\n", + ce, cau, b, p, off, s + 3); + if (grep_log_len < sizeof(grep_log) - 96) + grep_log_len += scnprintf( + grep_log + grep_log_len, + sizeof(grep_log) - grep_log_len, + "ce=%u cau=%u blk=%u pg=%u off=%u oem=%.8s\n", + ce, cau, b, p, off, + s + 3); + } + } + } + } + } + page_chunks = saved; + mutex_unlock(&f->lock); + fmss_dev_info(dev, "fat_boot_scan start=%u n=%u hits=%d\n", start, nblocks, hits); + return count; +} +static DEVICE_ATTR_WO(fat_boot_scan); + +static ssize_t scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, start, n, i; + u32 addr; + int nonempty = 0; + + if (!f) + return -ENODEV; + if (sscanf(buf, "%u %u %u", &ce, &start, &n) != 3) + return -EINVAL; + if (ce > 7 || n > 64) + return -EINVAL; + mutex_lock(&f->lock); + for (i = 0; i < n; i++) { + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = (start + i) * 128u; + if (fmss_page_read(f, ce, addr)) { + fmss_dev_info(dev, "scan ce=%u blk=%u FAIL\n", ce, start + i); + f->pages_since_reset++; + continue; + } + f->pages_since_reset++; + if (f->last_page[0] != 0xff && f->last_page[0] != 0x00) { + nonempty++; + fmss_dev_info(dev, + "scan ce=%u blk=%u p0 %02x %02x %02x %02x %02x %02x %02x %02x\n", + ce, start + i, + f->last_page[0], f->last_page[1], f->last_page[2], + f->last_page[3], f->last_page[4], f->last_page[5], + f->last_page[6], f->last_page[7]); + } + } + mutex_unlock(&f->lock); + fmss_dev_info(dev, "scan ce=%u start=%u n=%u nonempty_head=%d\n", + ce, start, n, nonempty); + return count; +} +static DEVICE_ATTR_WO(scan); + +static ssize_t param_hex_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + int i, n = 0; + + if (!f) + return -ENODEV; + for (i = 0; i < 128; i++) { + n += scnprintf(buf + n, PAGE_SIZE - n, "%02x%s", + f->last_param[i], ((i + 1) % 16) ? " " : "\n"); + } + return n; +} +static DEVICE_ATTR_RO(param_hex); + +static ssize_t param_info_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct fmss_n31 *f = fmss_dev; + const u8 *p; + + if (!f) + return -ENODEV; + p = f->last_param; + return sysfs_emit(buf, + "ce=%d ret=%d\n" + "caus_per_channel=%u cau_bits=%u\n" + "blocks_per_cau=%u block_bits=%u\n" + "pages_per_block=%u pages_per_block_slc=%u\n" + "page_address_bits=%u bits_per_cell_addr=%u default_bits_per_cell=%u\n" + "page_size=%u\n", + f->last_param_ce, f->last_param_ret, + fmss_le32(p, 16), fmss_le32(p, 20), + fmss_le32(p, 24), fmss_le32(p, 28), + fmss_le32(p, 32), fmss_le32(p, 36), + fmss_le32(p, 40), fmss_le32(p, 44), fmss_le32(p, 48), + fmss_le32(p, 52)); +} +static DEVICE_ATTR_RO(param_info); + +static ssize_t param_read_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce = 0; + int ret; + + if (!f) + return -ENODEV; + if (buf[0] && kstrtouint(buf, 0, &ce)) + return -EINVAL; + mutex_lock(&f->lock); + ret = fmss_param_read(f, ce); + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(param_read); + +static ssize_t nand_reset_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + int ret; + + if (!f) + return -ENODEV; + mutex_lock(&f->lock); + ret = fmss_nand_reset(f); + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(nand_reset); + +static ssize_t set_feature_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, feat, val; + int ret; + + if (!f) + return -ENODEV; + if (sscanf(buf, "%u %i %i", &ce, &feat, &val) < 3) + return -EINVAL; + mutex_lock(&f->lock); + ret = fmss_set_feature(f, ce, (u16)feat, val); + mutex_unlock(&f->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(set_feature); + +static ssize_t get_feature_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, feat, len = 16; + int nf, ret; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %i %u", &ce, &feat, &len); + if (nf < 2) + return -EINVAL; + if (len < 4) + len = 4; + if (len > 16) + len = 16; + mutex_lock(&f->lock); + last_feat_ce = (int)ce; + last_feat_id = (u16)feat; + ret = fmss_get_feature(f, ce, (u16)feat, last_feat, len); + last_feat_ret = ret; + mutex_unlock(&f->lock); + return ret ? ret : count; +} + +static ssize_t get_feature_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, + "ce=%d feat=0x%04x ret=%d %02x %02x %02x %02x %02x %02x %02x %02x\n", + last_feat_ce, last_feat_id, last_feat_ret, + last_feat[0], last_feat[1], last_feat[2], last_feat[3], + last_feat[4], last_feat[5], last_feat[6], last_feat[7]); +} +static DEVICE_ATTR_RW(get_feature); + +static ssize_t page_data_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *attr, char *buf, + loff_t off, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + + if (!f) + return -ENODEV; + if (off >= FMSS_PAGE_LEN) + return 0; + if (off + count > FMSS_PAGE_LEN) + count = FMSS_PAGE_LEN - off; + memcpy(buf, f->last_page + off, count); + return count; +} +static BIN_ATTR_RO(page_data, FMSS_PAGE_LEN); + +static struct attribute *fmss_attrs[] = { + &dev_attr_regs.attr, + &dev_attr_id.attr, + &dev_attr_read_id.attr, + &dev_attr_page_read.attr, + &dev_attr_dma_read.attr, + &dev_attr_lba_weave_scan.attr, + &dev_attr_seq_kick.attr, + &dev_attr_scan.attr, + &dev_attr_vfl_scan.attr, + &dev_attr_vfl_dump.attr, + &dev_attr_vfl_build.attr, + &dev_attr_vfl_map.attr, + &dev_attr_vfl_log.attr, + &dev_attr_lpn_build.attr, + &dev_attr_l2v_build.attr, + &dev_attr_l2v_status.attr, + &dev_attr_whimory_mount.attr, + &dev_attr_whimory_status.attr, + &dev_attr_lpn_read.attr, + &dev_attr_ftl_grep.attr, + &dev_attr_readme_read.attr, + &dev_attr_boot_read.attr, + &dev_attr_ftl_ascii.attr, + &dev_attr_grep_log.attr, + &dev_attr_lpn_index.attr, + &dev_attr_sector_hex.attr, + &dev_attr_btoc_scan.attr, + &dev_attr_btoc_log.attr, + &dev_attr_fat_scan.attr, + &dev_attr_fat_boot_scan.attr, + &dev_attr_page_status.attr, + &dev_attr_page_hex.attr, + &dev_attr_spare_hex.attr, + &dev_attr_parity_hex.attr, + &dev_attr_param_read.attr, + &dev_attr_param_hex.attr, + &dev_attr_param_info.attr, + &dev_attr_nand_reset.attr, + &dev_attr_set_feature.attr, + &dev_attr_get_feature.attr, + NULL, +}; + +static struct bin_attribute *fmss_bin_attrs[] = { + &bin_attr_page_data, + NULL, +}; + +static const struct attribute_group fmss_group = { + .attrs = fmss_attrs, + .bin_attrs = fmss_bin_attrs, +}; +static const struct attribute_group *fmss_groups[] = { &fmss_group, NULL }; + +static int fmss_probe(struct platform_device *pdev) +{ + struct fmss_n31 *f; + + f = devm_kzalloc(&pdev->dev, sizeof(*f), GFP_KERNEL); + if (!f) + return -ENOMEM; + f->base = devm_ioremap(&pdev->dev, FMSS_PHYS, FMSS_SIZE); + if (!f->base) + return -ENOMEM; + mutex_init(&f->lock); + f->last_ce = -1; + f->last_page_ce = -1; + f->last_page_ret = -1; + f->last_param_ce = -1; + f->last_param_ret = -1; + fmss_dma_setup(f, &pdev->dev); + fmss_dev = f; + platform_set_drvdata(pdev, f); + dev_info(&pdev->dev, + "FMSS peek FMCTRL0=0x%08x NANDSTAT=0x%08x quiet=%d (read-only until read_id/page_read)\n", + readl(f->base + FMCTRL0), readl(f->base + NANDSTAT), quiet); + return 0; +} + +static void fmss_remove(struct platform_device *pdev) +{ + struct fmss_n31 *f = platform_get_drvdata(pdev); + + fmss_dev = NULL; + fmss_l2v_free(); + fmss_early_lba_free(); + boot_carve_valid = false; + root_dir_valid = false; + if (f) + fmss_dma_teardown(f); +} + +static struct platform_driver fmss_driver = { + .probe = fmss_probe, + .remove = fmss_remove, + .driver = { + .name = "s5l8740-fmss", + .dev_groups = fmss_groups, + }, +}; + +static struct platform_device *fmss_pdev; + +static int __init fmss_init(void) +{ + int ret; + + ret = platform_driver_register(&fmss_driver); + if (ret) + return ret; + fmss_pdev = platform_device_register_simple("s5l8740-fmss", -1, NULL, 0); + if (IS_ERR(fmss_pdev)) { + platform_driver_unregister(&fmss_driver); + return PTR_ERR(fmss_pdev); + } + return 0; +} + +static void __exit fmss_exit(void) +{ + platform_device_unregister(fmss_pdev); + platform_driver_unregister(&fmss_driver); +} + +/* --- Exported read-only FTL sector API (ftl-s5l8740.ko) --- */ + +bool fmss_ftl_present(void) +{ + return fmss_dev != NULL; +} +EXPORT_SYMBOL_GPL(fmss_ftl_present); + +struct device *fmss_ftl_device(void) +{ + return fmss_dev ? fmss_dev->dev : NULL; +} +EXPORT_SYMBOL_GPL(fmss_ftl_device); + +unsigned int fmss_ftl_lpn_count(void) +{ + return l2v_mapped ? l2v_mapped : lpn_index_count; +} +EXPORT_SYMBOL_GPL(fmss_ftl_lpn_count); + +int fmss_ftl_build_map(unsigned int max_lpn) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int nblocks; + int ret; + + if (!f) + return -ENODEV; + nblocks = l2v_scan_blocks ? l2v_scan_blocks : FMSS_L2V_DEFAULT_BLOCKS; + mutex_lock(&f->lock); + ret = fmss_l2v_build(f, max_lpn, 0, nblocks); + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(fmss_ftl_build_map); + +int fmss_ftl_read_sector(u64 logical_sector, void *buf) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int lpn, sec, ce, cau, block, page, vblock, off, saved; + u32 addr; + int ret; + + if (!f || !buf) + return -ENODEV; + + mutex_lock(&f->lock); + + /* Prefer SFTL BTE early-LBA map (boot/FAT). */ + if (logical_sector < FMSS_EARLY_LBA_MAX && + !fmss_early_lba_lookup((unsigned int)logical_sector, &ce, &cau, + &block, &page, &sec)) { + vblock = fmss_vfl_phys(cau, block); + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, vblock, page, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (!ret) { + off = sec * FMSS_SECTOR_LEN; + if (off + FMSS_SECTOR_LEN <= f->last_page_len) + memcpy(buf, f->last_page + off, FMSS_SECTOR_LEN); + else + ret = -ERANGE; + } + mutex_unlock(&f->lock); + return ret; + } + + lpn = (unsigned int)(logical_sector / FMSS_FTL_SECTORS_PER_LPN); + sec = (unsigned int)(logical_sector % FMSS_FTL_SECTORS_PER_LPN); + + ret = fmss_ftl_read_lpn_locked(f, lpn, sec, buf); + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(fmss_ftl_read_sector); + +module_init(fmss_init); +module_exit(fmss_exit); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("S5L8740 FMSS NAND controller (Whimory FIL, read-only)"); +MODULE_AUTHOR("n31"); diff --git a/drivers/misc/fmss-seq-read.h b/drivers/misc/fmss-seq-read.h new file mode 100755 index 00000000000000..635b9843acf401 --- /dev/null +++ b/drivers/misc/fmss-seq-read.h @@ -0,0 +1,185 @@ +/* Auto-extracted OSOS 1.0.2 FMSS read sequence at 0x8980EA0. Do not edit. */ +#ifndef FMSS_SEQ_READ_H +#define FMSS_SEQ_READ_H + +#define FMSS_SEQ_READ_LEN 2824u +static const u8 fmss_seq_read_blob[] = { + 0x0c, 0x0c, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x10, 0x0c, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x58, 0x0c, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x4c, 0x0c, 0x00, 0x01, 0x00, 0x0b, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x05, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0d, 0x06, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x06, 0x00, 0x06, 0x0b, 0x01, 0x08, 0x00, 0x01, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x14, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x2c, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x0a, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0xb8, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0x78, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x14, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x06, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x20, 0x01, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x05, 0x38, 0x01, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x37, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x05, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x80, + 0x00, 0x00, 0x00, 0x14, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x05, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x06, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x50, 0x02, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x05, 0x68, 0x02, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x37, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x0a, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0xe8, 0x01, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0x20, 0x07, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0xc8, 0x09, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0a, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x06, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x77, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x7d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0xca, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x0c, 0x07, 0x40, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x01, 0x04, 0xff, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0x20, 0x00, 0x03, 0x00, 0x48, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x01, 0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xf0, 0x0a, 0x00, 0x00, + 0x10, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x10, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x7a, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x02, 0x04, 0xff, 0xff, 0xff, 0xff, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x18, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0d, 0x07, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, 0x30, 0x00, 0x00, 0x01, 0x0f, 0x00, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x32, 0x00, 0x00, 0x00, + 0x18, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x18, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x10, 0x00, 0x00, 0x00, 0x18, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, + 0x30, 0x00, 0x00, 0x01, 0xff, 0x03, 0x00, 0x00, 0x58, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0x62, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x02, 0x11, 0x00, 0x00, + 0x40, 0x00, 0x01, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x60, 0x05, 0x00, 0x00, + 0x60, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x54, 0x0c, 0x00, 0x01, 0x0c, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x01, 0x02, 0x01, 0x00, 0x00, 0x18, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, + 0x14, 0x00, 0x00, 0x01, 0x01, 0x02, 0x00, 0x00, 0x18, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, + 0x14, 0x00, 0x00, 0x01, 0x02, 0x01, 0x00, 0x00, 0x18, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xe2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, + 0x02, 0x00, 0x02, 0x0d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0e, 0x88, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x02, 0x0e, 0x10, 0x06, 0x00, 0x00, 0x08, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0a, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x17, 0xe8, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x0d, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x17, 0x20, 0x07, 0x00, 0x00, 0x01, 0x00, 0x01, 0x0d, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x17, 0xc8, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x14, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xff, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xff, + 0x00, 0x00, 0x06, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x7d, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xca, 0x00, 0x00, 0x00, 0x40, 0x00, 0x0c, 0x07, 0x40, 0x00, 0x00, 0x00, + 0x4c, 0x00, 0x01, 0x04, 0xff, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x20, 0x00, 0x03, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, 0x01, 0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0e, 0xf0, 0x0a, 0x00, 0x00, 0x10, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x10, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x02, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x02, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x18, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x08, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x0c, 0x0d, 0x07, 0x04, 0xff, 0xff, 0xff, 0xff, 0x30, 0x00, 0x00, 0x01, 0x0f, 0x00, 0x00, 0x00, + 0x58, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x32, 0x00, 0x00, 0x00, + 0x18, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x18, 0x0d, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x01, 0x00, 0x01, 0x0c, 0x10, 0x00, 0x00, 0x00, 0x18, 0x0d, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x07, 0x00, 0x07, 0x0d, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x01, 0x01, 0x02, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0xff, 0x03, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xe2, 0x01, 0x00, 0x00, 0x40, 0x00, 0x00, 0x01, 0x02, 0x11, 0x00, 0x00, + 0x40, 0x00, 0x01, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0e, 0x00, 0x09, 0x00, 0x00, + 0x60, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, + 0x68, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x01, 0x11, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0d, 0x07, 0x04, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, 0x54, 0x0c, 0x00, 0x01, 0x0c, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x34, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x04, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, + 0x1c, 0x0d, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x01, 0x07, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x08, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x08, 0x05, 0x10, 0x06, 0x00, 0x00, 0x14, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, + 0x18, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0xff, 0x03, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x02, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x17, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x01, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x06, 0x0b, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x77, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, + 0x48, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0xca, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x0c, 0x07, 0x40, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x04, 0xff, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x01, 0x20, 0x00, 0x03, 0x00, 0x48, 0x00, 0x00, 0x01, 0x00, 0x00, 0x80, 0x01, + 0x00, 0x00, 0x00, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0xf0, 0x0a, 0x00, 0x00, + 0x4c, 0x00, 0x00, 0x04, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x62, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x0e, 0x68, 0x0a, 0x00, 0x00, 0x01, 0x00, 0x01, 0x13, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x0d, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0xd8, 0x0a, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x05, 0x00, 0x0a, 0x00, 0x00, 0x06, 0x00, 0x06, 0x0a, 0x01, 0xfe, 0xff, 0xfe, + 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x01, 0xfe, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0d, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +#endif diff --git a/drivers/misc/ftl-s5l8740.c b/drivers/misc/ftl-s5l8740.c new file mode 100755 index 00000000000000..331608791198bd --- /dev/null +++ b/drivers/misc/ftl-s5l8740.c @@ -0,0 +1,738 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 Whimory FTL read-only block devices + host partition aliases. + * + * Hardware: N31 is NAND-only (no SPI/NOR utility flash from nano4G onward). + * Whimory "FPart" (PPNFPart) manages special blocks under FTL — it is NOT the + * host MBR/name table. Host-visible slices (classic + N5/N6/N7 family) are: + * + * firmware — IMG1 / MSE (osos, rsrc, disk, gpfw, …) or "[hi]" style + * ipod — FAT32 user volume (Windows D:\, iPod_Control, n31os) + * + * This module: + * /dev/s5l8740-ftl — whole FTL LBA space (4096 B sectors) + * /dev/s5l8740-firmware — firmware slice if discovered / forced + * /dev/s5l8740-ipod — user FAT slice (or whole disk if superfloppy) + * /dev/s5l8740-rsrc — optional resource FS inside firmware + * + * Low-level NAND I/O: fmss-s5l8740.ko + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fmss-s5l8740-api.h" + +#define FTL_DISK_NAME "s5l8740-ftl" +#define FTL_VALIDATE_HEX 128 +#define FPART_MAX_DISKS 4 +#define FPART_SCAN_LBAS 4096u + +enum fpart_kind { + FPART_WHOLE = 0, + FPART_FIRMWARE, + FPART_IPOD, + FPART_RSRC, +}; + +struct fpart_slice { + const char *name; + enum fpart_kind kind; + u64 start_lba; /* 4096-byte FTL sectors */ + u64 nsectors; + bool present; + struct gendisk *disk; +}; + +static u64 ftl_capacity = FMSS_FTL_DEFAULT_CAPACITY; +module_param(ftl_capacity, ullong, 0644); +MODULE_PARM_DESC(ftl_capacity, + "FTL logical sector count (4096 B; N31 default ~3856968)"); + +static unsigned int ftl_map_max_lpn; +module_param(ftl_map_max_lpn, uint, 0644); + +static bool ftl_auto_map; +module_param(ftl_auto_map, bool, 0644); + +/* Manual overrides (4K LBAs). 0 = auto / unused. */ +static unsigned long fw_start_lba; +module_param(fw_start_lba, ulong, 0644); +MODULE_PARM_DESC(fw_start_lba, "Firmware slice start (4K LBA, default 0)"); + +static unsigned long fw_nsectors; +module_param(fw_nsectors, ulong, 0644); +MODULE_PARM_DESC(fw_nsectors, + "Firmware slice size in 4K sectors (0=auto from scan/FWPartSize)"); + +static unsigned long ipod_start_lba; +module_param(ipod_start_lba, ulong, 0644); +MODULE_PARM_DESC(ipod_start_lba, "User FAT start 4K LBA (0=auto)"); + +static unsigned long ipod_nsectors; +module_param(ipod_nsectors, ulong, 0644); +MODULE_PARM_DESC(ipod_nsectors, "User FAT size 4K sectors (0=to end of FTL)"); + +static bool fpart_auto_scan = true; +module_param(fpart_auto_scan, bool, 0644); +MODULE_PARM_DESC(fpart_auto_scan, "Scan FTL for MBR/[hi]/FAT and create named disks"); + +static struct gendisk *ftl_disk; +static struct platform_device *ftl_pdev; +static char fpart_status[PAGE_SIZE]; +static unsigned int fpart_status_len; + +static struct fpart_slice slices[FPART_MAX_DISKS] = { + { .name = "s5l8740-firmware", .kind = FPART_FIRMWARE }, + { .name = "s5l8740-ipod", .kind = FPART_IPOD }, + { .name = "s5l8740-rsrc", .kind = FPART_RSRC }, +}; + +static bool is_apple_fat_bpb(const u8 *s) +{ + if (s[0] != 0xeb && s[0] != 0xe9) + return false; + /* LE bytes/sector 512 or 4096 */ + { + u16 bps = s[11] | (s[12] << 8); + + if (bps != 512 && bps != 4096) + return false; + } + if (s[3] == '*' && s[4] == 'U' && s[5] == 'O') + return true; + if (s[0x52] == 'F' && s[0x53] == 'A' && s[0x54] == '3') + return true; + if (s[510] == 0x55 && s[511] == 0xaa) + return true; + return false; +} + +static bool is_hi_firmware_hdr(const u8 *s) +{ + /* Classic firmware volume header: "[hi]" at +0x100 (LE magic). */ + return s[0x100] == '[' && s[0x101] == 'h' && + s[0x102] == 'i' && s[0x103] == ']'; +} + +static bool is_mbr(const u8 *s) +{ + return s[510] == 0x55 && s[511] == 0xaa && + (s[0x1be + 4] != 0 || s[0x1ce + 4] != 0 || + s[0x1de + 4] != 0 || s[0x1ee + 4] != 0); +} + +static bool looks_img1_dir(const u8 *s) +{ + /* Loose: FourCC-ish names used in Apple MSE (LE dword text). */ + static const char *const tags[] = { + "osos", "soso", "rsrc", "crsr", "disk", "ksid", + "gpfw", "wfpg", "appl", NULL + }; + unsigned int i, t; + + for (i = 0; i + 40 <= 512; i += 40) { + for (t = 0; tags[t]; t++) { + if (!memcmp(s + i + 4, tags[t], 4) || + !memcmp(s + i, tags[t], 4)) + return true; + } + } + return false; +} + +static int ftl_read_lba(u64 lba, u8 *buf) +{ + if (lba >= ftl_capacity) + return -ERANGE; + return fmss_ftl_read_sector(lba, buf); +} + +static void fpart_status_reset(void) +{ + fpart_status_len = 0; +} + +static void fpart_status_printf(const char *fmt, ...) +{ + va_list ap; + int n; + + if (fpart_status_len >= sizeof(fpart_status) - 1) + return; + va_start(ap, fmt); + n = vscnprintf(fpart_status + fpart_status_len, + sizeof(fpart_status) - fpart_status_len, fmt, ap); + va_end(ap); + if (n > 0) + fpart_status_len += n; +} + +static void fpart_clear_slices(void) +{ + int i; + + for (i = 0; i < FPART_MAX_DISKS; i++) { + if (slices[i].disk) { + del_gendisk(slices[i].disk); + put_disk(slices[i].disk); + slices[i].disk = NULL; + } + slices[i].present = false; + slices[i].start_lba = 0; + slices[i].nsectors = 0; + } +} + +static void ftl_submit_bio_range(struct bio *bio, u64 start_lba, u64 nsectors) +{ + struct bio_vec bvec; + struct bvec_iter iter; + u8 *secbuf; + u64 pos; + int ret = 0; + + if (bio_op(bio) != REQ_OP_READ) { + bio_io_error(bio); + return; + } + + secbuf = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_NOIO); + if (!secbuf) { + bio_io_error(bio); + return; + } + + pos = (u64)bio->bi_iter.bi_sector << 9; + + bio_for_each_segment(bvec, bio, iter) { + unsigned long seg_done = 0; + + while (seg_done < bvec.bv_len) { + u64 byte = pos + seg_done; + u64 lsec = start_lba + byte / FMSS_FTL_SECTOR_SIZE; + unsigned int off = byte % FMSS_FTL_SECTOR_SIZE; + unsigned int chunk = min_t(unsigned int, + FMSS_FTL_SECTOR_SIZE - off, + bvec.bv_len - seg_done); + + if (byte / FMSS_FTL_SECTOR_SIZE >= nsectors || + lsec >= ftl_capacity) { + ret = -EIO; + goto out; + } + + ret = fmss_ftl_read_sector(lsec, secbuf); + if (ret) + goto out; + + { + void *page_addr = kmap_local_page(bvec.bv_page); + + memcpy(page_addr + bvec.bv_offset + seg_done, + secbuf + off, chunk); + kunmap_local(page_addr); + } + seg_done += chunk; + } + pos += bvec.bv_len; + } + +out: + kfree(secbuf); + if (ret) + bio_io_error(bio); + else + bio_endio(bio); +} + +static void ftl_submit_bio(struct bio *bio) +{ + ftl_submit_bio_range(bio, 0, ftl_capacity); +} + +static void fpart_submit_bio(struct bio *bio) +{ + struct fpart_slice *sl = bio->bi_bdev->bd_disk->private_data; + + if (!sl || !sl->present) { + bio_io_error(bio); + return; + } + ftl_submit_bio_range(bio, sl->start_lba, sl->nsectors); +} + +static const struct block_device_operations ftl_bd_ops = { + .owner = THIS_MODULE, + .submit_bio = ftl_submit_bio, +}; + +static const struct block_device_operations fpart_bd_ops = { + .owner = THIS_MODULE, + .submit_bio = fpart_submit_bio, +}; + +static int fpart_register_slice(struct fpart_slice *sl) +{ + struct queue_limits lim = { + .logical_block_size = FMSS_FTL_SECTOR_SIZE, + .physical_block_size = FMSS_FTL_SECTOR_SIZE, + }; + struct gendisk *disk; + int ret; + + if (!sl->present || !sl->nsectors) + return 0; + + disk = blk_alloc_disk(&lim, NUMA_NO_NODE); + if (IS_ERR(disk)) + return PTR_ERR(disk); + + disk->first_minor = 0; + disk->flags = GENHD_FL_NO_PART; + disk->fops = &fpart_bd_ops; + disk->private_data = sl; + snprintf(disk->disk_name, DISK_NAME_LEN, "%s", sl->name); + set_capacity(disk, sl->nsectors * (FMSS_FTL_SECTOR_SIZE / 512)); + + ret = add_disk(disk); + if (ret) { + put_disk(disk); + return ret; + } + sl->disk = disk; + dev_info(&ftl_pdev->dev, + "/dev/%s start_lba=%llu nsectors=%llu (%llu MiB)\n", + sl->name, sl->start_lba, sl->nsectors, + (sl->nsectors * FMSS_FTL_SECTOR_SIZE) >> 20); + return 0; +} + +static struct fpart_slice *fpart_by_kind(enum fpart_kind k) +{ + int i; + + for (i = 0; i < FPART_MAX_DISKS; i++) + if (slices[i].kind == k) + return &slices[i]; + return NULL; +} + +static void fpart_set(enum fpart_kind k, u64 start, u64 nsec) +{ + struct fpart_slice *sl = fpart_by_kind(k); + + if (!sl || !nsec || start >= ftl_capacity) + return; + if (start + nsec > ftl_capacity) + nsec = ftl_capacity - start; + sl->start_lba = start; + sl->nsectors = nsec; + sl->present = true; +} + +/* + * Discover host partitions inside the FTL LBA space. + * N31: FTL capacity often already equals the user FAT (WMR_Partition). + * Still probe for classic MBR / [hi] / second FAT so firmware can be split out. + */ +static int fpart_scan_ex(unsigned int scan_n) +{ + u8 *sec; + u64 i, fat0 = ~0ULL, fat1 = ~0ULL, hi_lba = ~0ULL; + u64 mbr_fat_start = ~0ULL, mbr_fat_size = 0; + u64 mbr_other_start = ~0ULL, mbr_other_size = 0; + bool have_mbr = false, l0_fat = false; + int ret; + + fpart_clear_slices(); + fpart_status_reset(); + + sec = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_KERNEL); + if (!sec) + return -ENOMEM; + + if (!scan_n) + scan_n = 64; + if (scan_n > FPART_SCAN_LBAS) + scan_n = FPART_SCAN_LBAS; + if (scan_n > ftl_capacity) + scan_n = (unsigned int)ftl_capacity; + + ret = ftl_read_lba(0, sec); + if (ret) { + fpart_status_printf("LBA0 read failed %d\n", ret); + kfree(sec); + return ret; + } + + l0_fat = is_apple_fat_bpb(sec); + if (is_hi_firmware_hdr(sec)) { + hi_lba = 0; + fpart_status_printf("LBA0: [hi] firmware volume header\n"); + } + if (is_mbr(sec)) { + unsigned int p; + + have_mbr = true; + fpart_status_printf("LBA0: MBR partition table\n"); + for (p = 0; p < 4; p++) { + const u8 *e = sec + 0x1be + p * 16; + u8 type = e[4]; + u32 start512 = e[8] | (e[9] << 8) | (e[10] << 16) | + (e[11] << 24); + u32 size512 = e[12] | (e[13] << 8) | (e[14] << 16) | + (e[15] << 24); + u64 start4k = (u64)start512 / 8; + u64 size4k = (u64)size512 / 8; + + if (!type || !size512) + continue; + fpart_status_printf( + " mbr[%u] type=0x%02x start4k=%llu size4k=%llu\n", + p, type, start4k, size4k); + if (type == 0x0b || type == 0x0c || type == 0x1b || + type == 0x1c) { + mbr_fat_start = start4k; + mbr_fat_size = size4k; + } else if (mbr_other_start == ~0ULL) { + mbr_other_start = start4k; + mbr_other_size = size4k; + } + } + } + if (l0_fat) + fpart_status_printf("LBA0: Apple/FAT BPB (superfloppy or volume)\n"); + if (looks_img1_dir(sec)) + fpart_status_printf("LBA0: possible IMG1/MSE directory tags\n"); + + if (fw_nsectors) + fpart_set(FPART_FIRMWARE, fw_start_lba, fw_nsectors); + if (ipod_start_lba || ipod_nsectors) { + u64 st = ipod_start_lba; + u64 ns = ipod_nsectors ? ipod_nsectors : (ftl_capacity - st); + + fpart_set(FPART_IPOD, st, ns); + } + + if (!fpart_by_kind(FPART_IPOD)->present) { + if (have_mbr && mbr_fat_start != ~0ULL && mbr_fat_size) { + fpart_set(FPART_IPOD, mbr_fat_start, mbr_fat_size); + if (!fpart_by_kind(FPART_FIRMWARE)->present && + mbr_other_start != ~0ULL) + fpart_set(FPART_FIRMWARE, mbr_other_start, + mbr_other_size); + } else if (l0_fat) { + fpart_set(FPART_IPOD, 0, ftl_capacity); + fpart_status_printf( + "layout: superfloppy — FTL == ipod userdata\n"); + } + } + + for (i = 1; i < scan_n; i++) { + if (ftl_read_lba(i, sec)) + continue; + if (hi_lba == ~0ULL && is_hi_firmware_hdr(sec)) { + hi_lba = i; + fpart_status_printf("LBA%llu: [hi] firmware header\n", i); + } + if (is_apple_fat_bpb(sec)) { + if (fat0 == ~0ULL) { + fat0 = i; + fpart_status_printf("LBA%llu: FAT BPB #1\n", i); + } else if (fat1 == ~0ULL && i > fat0 + 8) { + fat1 = i; + fpart_status_printf("LBA%llu: FAT BPB #2\n", i); + break; + } + } + if (i == 7 && is_hi_firmware_hdr(sec)) + fpart_status_printf("LBA7: [hi] (classic 512-LBA 63)\n"); + } + + if (!fpart_by_kind(FPART_FIRMWARE)->present && hi_lba != ~0ULL) { + u64 fw_end = (fat0 != ~0ULL && fat0 > hi_lba) ? fat0 : + (ftl_capacity / 32); + + if (fw_end > hi_lba) + fpart_set(FPART_FIRMWARE, hi_lba, fw_end - hi_lba); + } + + if (!fpart_by_kind(FPART_IPOD)->present && fat0 != ~0ULL) { + u64 ns = (fat1 != ~0ULL) ? (fat1 - fat0) : (ftl_capacity - fat0); + + fpart_set(FPART_IPOD, fat0, ns); + } + + if (!fpart_by_kind(FPART_IPOD)->present) { + fpart_set(FPART_IPOD, 0, ftl_capacity); + fpart_status_printf( + "fallback: ipod = whole FTL (no separate FAT found)\n"); + } + + { + struct fpart_slice *fw = fpart_by_kind(FPART_FIRMWARE); + struct fpart_slice *ipod = fpart_by_kind(FPART_IPOD); + + if (fw->present && fat0 != ~0ULL && + fat0 >= fw->start_lba && + fat0 < fw->start_lba + fw->nsectors && + fat0 != ipod->start_lba) { + u64 rsrc_n = fw->start_lba + fw->nsectors - fat0; + + if (ipod->present && ipod->start_lba > fat0) + rsrc_n = ipod->start_lba - fat0; + fpart_set(FPART_RSRC, fat0, rsrc_n); + } + } + + fpart_status_printf( + "scanned_lbas=%u summary: fw=%d@%llu+%llu ipod=%d@%llu+%llu rsrc=%d@%llu+%llu\n", + scan_n, + fpart_by_kind(FPART_FIRMWARE)->present, + fpart_by_kind(FPART_FIRMWARE)->start_lba, + fpart_by_kind(FPART_FIRMWARE)->nsectors, + fpart_by_kind(FPART_IPOD)->present, + fpart_by_kind(FPART_IPOD)->start_lba, + fpart_by_kind(FPART_IPOD)->nsectors, + fpart_by_kind(FPART_RSRC)->present, + fpart_by_kind(FPART_RSRC)->start_lba, + fpart_by_kind(FPART_RSRC)->nsectors); + + kfree(sec); + + ret = 0; + ret |= fpart_register_slice(fpart_by_kind(FPART_FIRMWARE)); + ret |= fpart_register_slice(fpart_by_kind(FPART_IPOD)); + ret |= fpart_register_slice(fpart_by_kind(FPART_RSRC)); + return ret < 0 ? ret : 0; +} + +static int fpart_scan(void) +{ + return fpart_scan_ex(64); +} + +static ssize_t validate_sector_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + u64 sector; + u8 *secbuf; + unsigned int i, n; + int ret; + + if (kstrtoull(buf, 0, §or)) + return -EINVAL; + if (sector >= ftl_capacity) + return -ERANGE; + + secbuf = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_KERNEL); + if (!secbuf) + return -ENOMEM; + + ret = fmss_ftl_read_sector(sector, secbuf); + if (ret) { + kfree(secbuf); + dev_warn(dev, "validate sector %llu failed: %d\n", sector, ret); + return ret; + } + + n = min_t(unsigned int, FTL_VALIDATE_HEX, FMSS_FTL_SECTOR_SIZE); + { + /* One line — bare printk("%02x") becomes a dmesg line each. */ + char hex[FTL_VALIDATE_HEX * 3 + 4]; + unsigned int pos = 0; + + for (i = 0; i < n && pos + 3 < sizeof(hex); i++) + pos += scnprintf(hex + pos, sizeof(hex) - pos, "%02x", + secbuf[i]); + dev_info(dev, "sector %llu ok head[%u]: %s\n", sector, n, hex); + } + kfree(secbuf); + return count; +} +static DEVICE_ATTR_WO(validate_sector); + +static ssize_t map_build_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int max_lpn = ftl_map_max_lpn; + int ret; + + if (buf[0] && buf[0] != '\n' && kstrtouint(buf, 0, &max_lpn)) + return -EINVAL; + if (!max_lpn) + max_lpn = (unsigned int)(ftl_capacity / FMSS_FTL_SECTORS_PER_LPN) + 64; + + ret = fmss_ftl_build_map(max_lpn); + if (ret) + return ret; + + dev_info(dev, "LPN map built max=%u entries=%u\n", + max_lpn, fmss_ftl_lpn_count()); + return count; +} +static DEVICE_ATTR_WO(map_build); + +static ssize_t fpart_scan_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int n = 64; + int ret; + + if (buf[0] && buf[0] != '\n' && kstrtouint(buf, 0, &n)) + return -EINVAL; + ret = fpart_scan_ex(n); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(fpart_scan); + +static ssize_t fpart_status_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!fpart_status_len) + return sysfs_emit(buf, "(no fpart_scan yet)\n"); + return sysfs_emit(buf, "%.*s", (int)fpart_status_len, fpart_status); +} +static DEVICE_ATTR_RO(fpart_status); + +static ssize_t lpn_count_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u\n", fmss_ftl_lpn_count()); +} +static DEVICE_ATTR_RO(lpn_count); + +static ssize_t capacity_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%llu\n", ftl_capacity); +} +static DEVICE_ATTR_RO(capacity); + +static struct attribute *ftl_attrs[] = { + &dev_attr_validate_sector.attr, + &dev_attr_map_build.attr, + &dev_attr_fpart_scan.attr, + &dev_attr_fpart_status.attr, + &dev_attr_lpn_count.attr, + &dev_attr_capacity.attr, + NULL, +}; + +static const struct attribute_group ftl_attr_group = { + .attrs = ftl_attrs, +}; + +static int ftl_register_disk(void) +{ + struct queue_limits lim = { + .logical_block_size = FMSS_FTL_SECTOR_SIZE, + .physical_block_size = FMSS_FTL_SECTOR_SIZE, + }; + int ret; + + ftl_disk = blk_alloc_disk(&lim, NUMA_NO_NODE); + if (IS_ERR(ftl_disk)) + return PTR_ERR(ftl_disk); + + ftl_disk->first_minor = 0; + ftl_disk->flags = GENHD_FL_NO_PART; + ftl_disk->fops = &ftl_bd_ops; + snprintf(ftl_disk->disk_name, DISK_NAME_LEN, "%s", FTL_DISK_NAME); + set_capacity(ftl_disk, ftl_capacity * (FMSS_FTL_SECTOR_SIZE / 512)); + + ret = add_disk(ftl_disk); + if (ret) { + put_disk(ftl_disk); + ftl_disk = NULL; + } + return ret; +} + +static void ftl_unregister_disk(void) +{ + fpart_clear_slices(); + if (ftl_disk) { + del_gendisk(ftl_disk); + put_disk(ftl_disk); + ftl_disk = NULL; + } +} + +static int __init ftl_init(void) +{ + unsigned int max_lpn; + int ret; + + if (!fmss_ftl_present()) { + pr_err("s5l8740-ftl: load fmss-s5l8740.ko first\n"); + return -ENODEV; + } + + ret = ftl_register_disk(); + if (ret) + return ret; + + ftl_pdev = platform_device_register_simple("s5l8740-ftl", -1, NULL, 0); + if (IS_ERR(ftl_pdev)) { + ret = PTR_ERR(ftl_pdev); + ftl_unregister_disk(); + return ret; + } + + ret = sysfs_create_group(&ftl_pdev->dev.kobj, &ftl_attr_group); + if (ret) { + platform_device_unregister(ftl_pdev); + ftl_unregister_disk(); + return ret; + } + + if (ftl_auto_map) { + max_lpn = ftl_map_max_lpn; + if (!max_lpn) + max_lpn = (unsigned int)(ftl_capacity / + FMSS_FTL_SECTORS_PER_LPN) + 64; + ret = fmss_ftl_build_map(max_lpn); + if (ret) + dev_warn(&ftl_pdev->dev, "auto map_build failed (%d)\n", + ret); + } + + if (fpart_auto_scan) { + ret = fpart_scan(); + if (ret) + dev_warn(&ftl_pdev->dev, "fpart_scan failed (%d)\n", ret); + } + + dev_info(&ftl_pdev->dev, + "NAND-only Whimory FTL /dev/%s (%llu x %uB); named slices via fpart_scan\n", + FTL_DISK_NAME, ftl_capacity, FMSS_FTL_SECTOR_SIZE); + return 0; +} + +static void __exit ftl_exit(void) +{ + if (ftl_pdev) { + sysfs_remove_group(&ftl_pdev->dev.kobj, &ftl_attr_group); + platform_device_unregister(ftl_pdev); + ftl_pdev = NULL; + } + ftl_unregister_disk(); +} + +module_init(ftl_init); +module_exit(ftl_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("S5L8740 Whimory FTL RO disks (ftl/firmware/ipod/rsrc)"); +MODULE_AUTHOR("n31"); +MODULE_SOFTDEP("pre: fmss_s5l8740"); diff --git a/drivers/misc/lis3lv02d/lis3lv02d_i2c.c b/drivers/misc/lis3lv02d/lis3lv02d_i2c.c index 15119584473caf..57da8adf17df06 100644 --- a/drivers/misc/lis3lv02d/lis3lv02d_i2c.c +++ b/drivers/misc/lis3lv02d/lis3lv02d_i2c.c @@ -52,7 +52,12 @@ static inline s32 lis3_i2c_write(struct lis3lv02d *lis3, int reg, u8 value) static inline s32 lis3_i2c_read(struct lis3lv02d *lis3, int reg, u8 *v) { struct i2c_client *c = lis3->bus_priv; - *v = i2c_smbus_read_byte_data(c, reg); + s32 ret = i2c_smbus_read_byte_data(c, reg); + + /* Do not store errno in *v — (u8)(-ETIMEDOUT/-110) is 0x92. */ + if (ret < 0) + return ret; + *v = (u8)ret; return 0; } @@ -71,9 +76,15 @@ static int lis3_i2c_init(struct lis3lv02d *lis3) lis3_reg_ctrl(lis3, LIS3_REG_ON); - lis3->read(lis3, WHO_AM_I, ®); + ret = lis3->read(lis3, WHO_AM_I, ®); + if (ret < 0) { + printk(KERN_ERR "lis3: WHO_AM_I read failed %d\n", ret); + return ret; + } + printk(KERN_INFO "lis3: WHO_AM_I=0x%02x expect=0x%02x\n", + reg, lis3->whoami); if (reg != lis3->whoami) - printk(KERN_ERR "lis3: power on failure\n"); + printk(KERN_ERR "lis3: power on failure (WHO_AM_I mismatch)\n"); /* power up the device */ ret = lis3->read(lis3, CTRL_REG1, ®); @@ -157,6 +168,23 @@ static int lis3lv02d_i2c_probe(struct i2c_client *client) i2c_set_clientdata(client, &lis3_dev); + { + s32 id = i2c_smbus_read_byte_data(client, WHO_AM_I); + + if (id < 0) + dev_info(&client->dev, + "LIS3 WHO_AM_I read failed %d (7bit=0x%02x, -5=-EIO not a chip id; wire 0x31 is the 8-bit READ addr)\n", + id, client->addr); + else + dev_info(&client->dev, + "LIS3 WHO_AM_I=0x%02x 7bit=0x%02x (0x33=8-bit 3DC)\n", + id, client->addr); + if (id < 0) { + ret = id; + goto fail2; + } + } + /* Provide power over the init call */ lis3_reg_ctrl(&lis3_dev, LIS3_REG_ON); diff --git a/drivers/misc/s5l8740-iis2-mmio.c b/drivers/misc/s5l8740-iis2-mmio.c new file mode 100755 index 00000000000000..100f5f180586c0 --- /dev/null +++ b/drivers/misc/s5l8740-iis2-mmio.c @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 IIS2 MMIO hook — FM digital RX @ 0x3D400000 (N31 RE). + * Register model OPEN: probe + regs sysfs only, no invented capture PCM. + */ +#include +#include +#include +#include +#include +#include + +#define IIS2_MMIO_LEN 0x40 + +struct s5l8740_iis2 { + void __iomem *base; + struct clk_bulk_data *clks; + int num_clks; +}; + +static ssize_t regs_show(struct device *dev, struct device_attribute *a, char *buf) +{ + struct s5l8740_iis2 *iis2 = dev_get_drvdata(dev); + unsigned int i; + ssize_t n = 0; + + if (!iis2 || !iis2->base) + return sysfs_emit(buf, "not mapped\n"); + + for (i = 0; i < IIS2_MMIO_LEN; i += 4) { + n += sysfs_emit_at(buf, n, "%02x: %08x\n", i, + readl(iis2->base + i)); + if (n >= PAGE_SIZE - 32) + break; + } + return n; +} +static DEVICE_ATTR_RO(regs); + +static struct attribute *iis2_attrs[] = { + &dev_attr_regs.attr, + NULL, +}; +static const struct attribute_group iis2_attr_group = { + .attrs = iis2_attrs, +}; + +static int s5l8740_iis2_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_iis2 *iis2; + struct resource *res; + int ret; + + iis2 = devm_kzalloc(dev, sizeof(*iis2), GFP_KERNEL); + if (!iis2) + return -ENOMEM; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + iis2->base = devm_ioremap_resource(dev, res); + if (IS_ERR(iis2->base)) + return PTR_ERR(iis2->base); + + ret = devm_clk_bulk_get_all(dev, &iis2->clks); + if (ret > 0) { + iis2->num_clks = ret; + clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); + } + + ret = sysfs_create_group(&dev->kobj, &iis2_attr_group); + if (ret) + dev_warn(dev, "sysfs: %d\n", ret); + + dev_set_drvdata(dev, iis2); + dev_info(dev, "IIS2 FM hook @%pR — regs sysfs; capture PCM OPEN\n", res); + return 0; +} + +static void s5l8740_iis2_remove(struct platform_device *pdev) +{ + struct s5l8740_iis2 *iis2 = platform_get_drvdata(pdev); + + sysfs_remove_group(&pdev->dev.kobj, &iis2_attr_group); + if (iis2 && iis2->num_clks) + clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); +} + +static const struct of_device_id s5l8740_iis2_of_match[] = { + { .compatible = "apple,s5l8740-iis2" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_iis2_of_match); + +static struct platform_driver s5l8740_iis2_driver = { + .probe = s5l8740_iis2_probe, + .remove = s5l8740_iis2_remove, + .driver = { + .name = "s5l8740-iis2", + .of_match_table = s5l8740_iis2_of_match, + }, +}; +module_platform_driver(s5l8740_iis2_driver); + +MODULE_DESCRIPTION("S5L8740 IIS2 FM MMIO hook (N31)"); +MODULE_LICENSE("GPL"); diff --git a/drivers/misc/whimory-ftl.h b/drivers/misc/whimory-ftl.h new file mode 100755 index 00000000000000..9d3a65f37e8969 --- /dev/null +++ b/drivers/misc/whimory-ftl.h @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Classic Whimory VFL/FTL structs (freemyipod Nano 2G FTL wiki). + * N31 uses PPN/SFTL (wrmx / L2V) but the same spare type codes and + * mount flow (VFL → ftlctrlblocks → FTL cxt 0x43 → map pages 0x44) apply + * as a family reference for Stage C. + * + * Endianness: all multi-byte fields are little-endian on flash. + */ +#ifndef WHIMORY_FTL_H +#define WHIMORY_FTL_H + +#include + +/* Spare type codes (meta[9] / user.type) */ +#define WMR_SPARE_DATA 0x40 +#define WMR_SPARE_DATA_LAST 0x41 +#define WMR_SPARE_FTL_CXT 0x43 +#define WMR_SPARE_BLOCK_MAP 0x44 +#define WMR_SPARE_ERASECTR 0x46 +#define WMR_SPARE_UNCLEAN 0x47 +#define WMR_SPARE_VFL_CXT 0x80 +/* PPN/SFTL (N31) */ +#define WMR_SPARE_VFLCXT_PPN 0x20 +#define WMR_SPARE_SFTL_CXT 0x1F + +#define WMR_DEVICEINFOSIGN "DEVICEINFOSIGN" + +/* User-data spare (types 0x40 / 0x41) — first 0xC bytes + ECC follow */ +struct wmr_spare_user { + __le32 lpn; + __le32 usn; + u8 field_8; + u8 type; + u8 eccmark; + u8 field_B; +} __packed; + +/* Meta spare (FTL/VFL context, block map, …) */ +struct wmr_spare_meta { + __le32 usn; + __le16 idx; + u8 field_6; + u8 field_7; + u8 field_8; + u8 type; + u8 eccmark; + u8 field_B; +} __packed; + +/* + * VFL context — Nano2G size; N31 wrmx header is a different layout but + * still carries FTL ctrl block hints in family ports. + */ +struct wmr_vfl_cxt { + __le32 usn; + __le16 ftlctrlblocks[3]; + u8 field_A[2]; + __le32 updatecount; + __le16 activecxtblock; + __le16 nextcxtpage; + u8 field_14[4]; + __le16 field_18; + __le16 spareused; + __le16 firstspare; + __le16 sparecount; + __le16 remaptable[0x334]; + u8 bbt[0x11A]; + __le16 vflcxtblocks[4]; + __le16 scheduledstart; + u8 field_7AC[0x4C]; + __le32 checksum1; + __le32 checksum2; +} __packed; + +/* FTL context (0x28C used by freemyipod on read) */ +struct wmr_ftl_cxt { + __le32 usn; + __le32 nextblockusn; + __le16 freecount; + __le16 nextfreeidx; + __le16 swapcounter; + __le16 blockpool[0x14]; + __le16 field_36; + __le32 ftl_map_pages[8]; + u8 field_58[0x28]; + __le32 ftl_erasectr_pages[8]; + u8 field_A0[0x70]; + __le32 ftl_map_ptr; + __le32 ftl_erasectr_ptr; + __le32 ftl_log_ptr; + __le32 erasedirty; + __le16 field_120; + __le16 ftlctrlblocks[3]; + __le32 ftlctrlpage; + __le32 clean_flag; + u8 field_130[0x15C]; +} __packed; + +/* Classic lPage → vPage using block map (u16 vBlock per lBlock). */ +static inline u32 wmr_lpage_to_vpage(u32 lpage, u32 pages_per_block, + const u16 *map, u32 map_entries) +{ + u32 lblock = lpage / pages_per_block; + u32 page = lpage % pages_per_block; + u16 vblock; + + if (lblock >= map_entries || !map) + return ~0u; + vblock = map[lblock]; + if (!vblock || vblock == 0xffff) + return ~0u; + return (u32)vblock * pages_per_block + page; +} + +#endif /* WHIMORY_FTL_H */ diff --git a/drivers/spi/spi-s5l8702.c b/drivers/spi/spi-s5l8702.c old mode 100644 new mode 100755 index 6fd63e1c84ab68..e08d34857dc19d --- a/drivers/spi/spi-s5l8702.c +++ b/drivers/spi/spi-s5l8702.c @@ -1,159 +1,507 @@ // SPDX-License-Identifier: GPL-2.0 /* - * SPI controller driver for Samsung/Apple S5L8702 - * (iPod nano 3rd generation) + * SPI controller for Samsung/Apple S5L8702 / S5L8740 * - * Ported from Rockbox's spi-s5l8702.c by Michael Sparmann. + * RetailOS PIO (sub_4043D0) is shared by SPI0/CS42 and SPI2/Nimbus: + * CS = SPIPIN bit1 (4045D4(n): assert=clear, idle=set) + * flush STATUS & 0x7C0 / 0xF800 == 0 + * per-byte: wait !0x7C0, TXDATA, wait 0xF800, RXDATA + * SETUP 0x402C|0x10, CLKDIV 4 + * + * Do not remux SPI0 pads 0–3 (SEC leftover 4/2/2/2). OSOS never GPIOCMDs + * those pads. Do not write 0x3CF00200 — CS42 CS is 4045D4(0), not GPIOCMD. + * + * "DMA" in 4043D0 is the SPI-controller block path (SETUP bit5 after + * 11B70), not PL080. No SPI0/SPI2 PL080 peri IDs in OSOS. sub_3914 + * only does STATUS |= 0x40003F. Nimbus/CS42 stay on this PIO loop; + * do not invent peri IDs. */ +#include +#include #include -#include #include #include #include #include -#define SPICTRL 0x00 -#define SPISETUP 0x04 -#define SPISTATUS 0x08 -#define SPIPIN 0x0c -#define SPITXDATA 0x10 -#define SPIRXDATA 0x20 -#define SPICLKDIV 0x30 -#define SPIRXLIMIT 0x34 - -// SPISTATUS: TX FIFO level in bits [8:4] (max 16 = full), -// RX FIFO level in bits [13:9]. -#define SPISTATUS_TXFULL 0x100 // TX FIFO level == 16 (full) +#define SPICTRL 0x00 +#define SPISETUP 0x04 +#define SPISTATUS 0x08 +#define SPIPIN 0x0c +#define SPITXDATA 0x10 +#define SPIRXDATA 0x20 +#define SPICLKDIV 0x30 +#define SPIRXLIMIT 0x34 +#define SPIUNK4C 0x4c /* 4043D0: write 1 after TXDATA */ + +#define SPISTATUS_TXFULL 0x100 #define SPISTATUS_TXLVL_MASK 0x1f0 -#define SPISTATUS_RXLVL_MASK 0x3e00 // any RX data present +#define SPISTATUS_RXLVL_MASK 0x3e00 +#define SPISTATUS_TXBUSY_ROS 0x7c0 +#define SPISTATUS_RXRDY_ROS 0xf800 -// SPISETUP: bit 0 enables bulk-receive mode. #define SPISETUP_RXMODE BIT(0) -#define SPISETUP_INIT 0x10618 +#define SPISETUP_RETAILOS 0x403c /* 0x402C | 0x10 — SPI2 / 11B70(2,0x1A,…) */ +#define SPISETUP_SPI0_11B70 0x403e /* 11B70(0,0x1A,0x2EE0,8) → 0x402E|0x10 */ -// SPICTRL: bits 3:2 reset FIFOs, bit 0 enables the controller. #define SPICTRL_RESET_FIFO 0xc #define SPICTRL_ENABLE 0x1 -// Fixed SoC peripheral addresses. -#define S5L8702_PCON0_PHYS 0x3cf00000UL -#define S5L8702_GPIOCMD_PHYS 0x3cf00200UL - -// PWRCON(1) = 0x3C500048 + 4*1; clockgate 34 sits in PWRCON1, bit 2 -#define S5L8702_PWRCON1_PHYS 0x3c50004cUL -#define PWRCON1_SPI0_BIT BIT(2) - -// PCON0[15:0]: set pins 0-3 to SPI function (each 4 bits = 0x2) -#define PCON0_SPI_MASK 0xffffU -#define PCON0_SPI_FUNC 0x2222U +#define SPISTATUS_KICK 0x400000 /* set after SETUP clear RXMODE */ -// GPIOCMD encoding for SPI0 CS: port 0, pin 0; 0xe = output-low, 0xf = output-high -#define GPIOCMD_SPI0_CS_ASSERT 0x0000eU -#define GPIOCMD_SPI0_CS_DEASSERT 0x0000fU +#define SPIPIN_CS_BIT BIT(1) -// Default clock divider: SPI clock = PClk / (div + 1) -#define SPI0_CLKDIV_DEFAULT 4 +#define S5L8702_PCON0_PHYS 0x3cf00000UL +#define S5L8702_GPIOCMD_PHYS 0x3cf001e0UL +#define S5L8702_PWRCON1_PHYS 0x3c50004cUL +#define S5L8702_PWRCON4_PHYS 0x3c50006cUL +#define PWRCON1_SPI0_BIT BIT(2) +#define PWRCON1_SPI2_BIT BIT(16) +#define PWRCON4_SPI0_2_BIT BIT(13) /* CLK_SPI0_2 / bring-up table */ +#define SPI_CLKDIV_DEFAULT 4 +#define SPI0_BASE_PHYS 0x3c300000UL +#define SPI2_BASE_PHYS 0x3d200000UL -// Worst-case timeout for a single FIFO slot -#define SPI_POLL_TIMEOUT_US 100000 +#define SPI_WAIT_GUARD 500000 struct s5l8702_spi { void __iomem *base; - void __iomem *pcon0; + struct device *dev; void __iomem *gpiocmd; + void __iomem *gpio_base; void __iomem *pwrcon1; + struct clk_bulk_data *clks; + int num_clks; + bool spi0; + bool spi2_nimbus; + bool prepared; + int last_err; + u32 last_status; }; -static int s5l8702_spi_wait_rx(struct s5l8702_spi *sspi) { +static void s5l8702_gpiocmd_func(struct s5l8702_spi *sspi, unsigned int gpio, u8 func) +{ + u32 bank = gpio >> 3; + u32 pin = gpio & 7; + void __iomem *b; + u32 dir; + + if (!sspi->gpiocmd) + return; + if (sspi->gpio_base) { + b = sspi->gpio_base + 32 * bank; + dir = readl(b + 0x14); + writel(dir | BIT(pin), b + 0x14); + } + writel((bank << 16) | (pin << 8) | func, sspi->gpiocmd); +} + +static void s5l8702_spi2_pinmux(struct s5l8702_spi *sspi) +{ + void __iomem *b; + u32 pin, punc; + + /* sub_20690(1): sub_23CD0(0x57, 0) clears PUNC (+0x10) on GPIO 87 */ + if (sspi->gpio_base) { + b = sspi->gpio_base + 32 * (0x57 >> 3); + pin = 0x57 & 7; + punc = readl(b + 0x10); + writel(punc & ~BIT(pin), b + 0x10); + } + s5l8702_gpiocmd_func(sspi, 0x57, 5); + s5l8702_gpiocmd_func(sspi, 0x58, 3); + s5l8702_gpiocmd_func(sspi, 0x59, 3); + s5l8702_gpiocmd_func(sspi, 0x5A, 3); +} + +/* OSOS sub_743A4: 43D38C(0,4) (1,2) (2,2) (3,2) then 11B70(0,0x1A,0x2EE0,8). */ +static void s5l8702_spi0_pinmux(struct s5l8702_spi *sspi) +{ + s5l8702_gpiocmd_func(sspi, 0, 4); + s5l8702_gpiocmd_func(sspi, 1, 2); + s5l8702_gpiocmd_func(sspi, 2, 2); + s5l8702_gpiocmd_func(sspi, 3, 2); +} + +/* + * sub_11B70(0, 0x1A, 0x2EE0, 8). Clock ids 4/5 via 43CFCC. + * Id 5 mux 0/5 is fixed 24000 (3D7A2C). 440A58(24000,1000)=24, + * 440A58(24000,0x2EE0)=2. Id 4 uses the same 24 k-unit if PLL field + * is the usual 24 MHz-class source. + */ +static void s5l8702_spi0_11b70(struct s5l8702_spi *sspi) +{ + const unsigned int a4 = 8; + const unsigned int clk_kunit = 24; + u32 dd = clk_kunit * a4; + u32 u3c = 3 * clk_kunit * (a4 + 1); + u32 clkdiv = 2; + + writel(0xf, sspi->base + SPISTATUS); + writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, + sspi->base + SPICTRL); + writel(10, sspi->base + 0x44); + writel(dd, sspi->base + 0x38); + writel(255, sspi->base + 0x40); + writel(u3c, sspi->base + 0x3c); + writel(clkdiv, sspi->base + SPICLKDIV); + writel(0x6, sspi->base + SPIPIN); + writel(SPISETUP_SPI0_11B70, sspi->base + SPISETUP); + writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, + sspi->base + SPICTRL); + writel(SPICTRL_ENABLE, sspi->base + SPICTRL); + sspi->prepared = true; + dev_info(sspi->dev, + "SPI0 11B70 SETUP=0x%x CLKDIV=%u dd=%u u3c=%u u40=255 u44=10\n", + SPISETUP_SPI0_11B70, clkdiv, dd, u3c); +} + +/* + * sub_11B70(2, 0x1A, 0x2EE0, 1) — same engine as SPI0, a4=1. + * CLKDIV stays 2 (440A58(24000, 0x2EE0)). Do not use the generic + * CLKDIV=4 path; that left +0x38/+0x3c at reset and ping RX was junk. + */ +static void s5l8702_spi2_11b70(struct s5l8702_spi *sspi) +{ + const unsigned int a4 = 1; + const unsigned int clk_kunit = 24; + u32 dd = clk_kunit * a4; + u32 u3c = 3 * clk_kunit * (a4 + 1); + u32 clkdiv = 2; + + /* + * sub_11B70(2, 0x1A, 0x2EE0, 1) — mode 0x1A → SETUP 0x402E|0x10 + * = 0x403E. Do not use 0x403C (that is a different mode bit). + * OSOS does not write SPIPIN or STATUS here. + */ + writel(10, sspi->base + 0x44); + writel(dd, sspi->base + 0x38); + writel(255, sspi->base + 0x40); + writel(u3c, sspi->base + 0x3c); + writel(clkdiv, sspi->base + SPICLKDIV); + writel(SPISETUP_SPI0_11B70, sspi->base + SPISETUP); + writel(SPICTRL_ENABLE, sspi->base + SPICTRL); + sspi->prepared = true; + dev_info(sspi->dev, + "SPI2 11B70 SETUP=0x%x CLKDIV=%u dd=%u u3c=%u (mode 0x1A)\n", + SPISETUP_SPI0_11B70, clkdiv, dd, u3c); +} + +static void s5l8702_spi_cs(struct s5l8702_spi *sspi, bool assert) +{ + u32 pin = readl(sspi->base + SPIPIN); + + if (assert) + pin &= ~SPIPIN_CS_BIT; + else + pin |= SPIPIN_CS_BIT; + writel(pin, sspi->base + SPIPIN); +} + +static int s5l8702_wait_clear(struct s5l8702_spi *sspi, u32 mask) +{ + unsigned int guard = SPI_WAIT_GUARD; u32 val; - return readl_poll_timeout(sspi->base + SPISTATUS, val, val & SPISTATUS_RXLVL_MASK, 0, SPI_POLL_TIMEOUT_US); + + while (guard--) { + val = readl(sspi->base + SPISTATUS); + if ((val & mask) == 0) + return 0; + cpu_relax(); + } + /* Rockbox Classic uses 0x1f0 TX-empty — accept either family */ + if (mask == SPISTATUS_TXBUSY_ROS) { + guard = SPI_WAIT_GUARD / 4; + while (guard--) { + val = readl(sspi->base + SPISTATUS); + if ((val & 0x1f0) == 0) + return 0; + cpu_relax(); + } + } + return -ETIMEDOUT; } -static int s5l8702_spi_wait_tx(struct s5l8702_spi *sspi) { +static int s5l8702_wait_set(struct s5l8702_spi *sspi, u32 mask) +{ + unsigned int guard = SPI_WAIT_GUARD; u32 val; - return readl_poll_timeout(sspi->base + SPISTATUS, val, (val & SPISTATUS_TXLVL_MASK) != SPISTATUS_TXFULL, 0, SPI_POLL_TIMEOUT_US); + + while (guard--) { + val = readl(sspi->base + SPISTATUS); + if (val & mask) + return 0; + cpu_relax(); + } + /* Rockbox Classic RX ready 0x3e00 */ + if (mask == SPISTATUS_RXRDY_ROS) { + guard = SPI_WAIT_GUARD / 4; + while (guard--) { + val = readl(sspi->base + SPISTATUS); + if (val & 0x3e00) + return 0; + cpu_relax(); + } + } + return -ETIMEDOUT; } -static void s5l8702_spi_prepare(struct s5l8702_spi *sspi) { +static void s5l8702_spi_hw_init(struct s5l8702_spi *sspi) +{ writel(0xf, sspi->base + SPISTATUS); - writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, sspi->base + SPICTRL); - writel(SPI0_CLKDIV_DEFAULT, sspi->base + SPICLKDIV); - writel(6, sspi->base + SPIPIN); - writel(SPISETUP_INIT, sspi->base + SPISETUP); - writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, sspi->base + SPICTRL); + writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, + sspi->base + SPICTRL); + writel(SPI_CLKDIV_DEFAULT, sspi->base + SPICLKDIV); + /* idle: CS deasserted (bit1 set), match prior SPIPIN=6 */ + writel(0x6, sspi->base + SPIPIN); + writel(SPISETUP_RETAILOS, sspi->base + SPISETUP); + writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, + sspi->base + SPICTRL); writel(SPICTRL_ENABLE, sspi->base + SPICTRL); + sspi->prepared = true; } -static void s5l8702_spi_set_cs(struct spi_device *spi, bool enable) { - struct s5l8702_spi *sspi = spi_controller_get_devdata(spi->controller); - writel(enable ? GPIOCMD_SPI0_CS_DEASSERT : GPIOCMD_SPI0_CS_ASSERT, sspi->gpiocmd); +/* Rockbox touch-nano7g: CLKDIV=4, SETUP 0x402C|0x10, CTRL=1. No SPIPIN. */ +static void s5l8702_spi2_hw_init(struct s5l8702_spi *sspi) +{ + writel(4, sspi->base + SPICLKDIV); + writel(0x402c, sspi->base + SPISETUP); + writel(SPISETUP_RETAILOS, sspi->base + SPISETUP); + writel(SPICTRL_ENABLE, sspi->base + SPICTRL); + sspi->prepared = true; } -static int s5l8702_spi_prepare_message(struct spi_controller *ctlr, struct spi_message *msg) { - s5l8702_spi_prepare(spi_controller_get_devdata(ctlr)); +static int s5l8702_spi2_pio_one(struct s5l8702_spi *sspi, + const u8 *tx, u8 *rx, unsigned int len) +{ + unsigned int i; + + for (i = 0; i < len; i++) { + unsigned int guard = SPI_WAIT_GUARD; + u32 st; + + writel(1, sspi->base + SPIRXLIMIT); + while (guard--) { + st = readl(sspi->base + SPISTATUS); + if ((st & 0x1f0) != 0x100) + break; + cpu_relax(); + } + if ((readl(sspi->base + SPISTATUS) & 0x1f0) == 0x100) + return -ETIMEDOUT; + writel(tx ? tx[i] : 0xff, sspi->base + SPITXDATA); + guard = SPI_WAIT_GUARD; + while (guard--) { + st = readl(sspi->base + SPISTATUS); + if (st & 0x3e00) + break; + cpu_relax(); + } + if (!(readl(sspi->base + SPISTATUS) & 0x3e00)) + return -ETIMEDOUT; + { + u8 b = (u8)readl(sspi->base + SPIRXDATA); + + if (rx) + rx[i] = b; + } + } return 0; } -static int s5l8702_spi_transfer_one(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer) { +static void s5l8702_spi_set_cs(struct spi_device *spi, bool enable) +{ + /* CS is owned by transfer_one (RetailOS 4045D4 order) */ + (void)spi; + (void)enable; +} + +static int s5l8702_spi_prepare_message(struct spi_controller *ctlr, + struct spi_message *msg) +{ struct s5l8702_spi *sspi = spi_controller_get_devdata(ctlr); - const u8 *tx = xfer->tx_buf; - u8 *rx = xfer->rx_buf; - unsigned int len = xfer->len; + + if (!sspi->prepared) + s5l8702_spi_hw_init(sspi); + return 0; +} + +static int s5l8702_spi_pio_one(struct s5l8702_spi *sspi, + const u8 *tx, u8 *rx, unsigned int len) +{ unsigned int i; - int ret = 0; - - if (rx && !tx) { - // Pure receive: program SPIRXLIMIT and set RXMODE. - writel(len, sspi->base + SPIRXLIMIT); - writel(readl(sspi->base + SPISETUP) | SPISETUP_RXMODE, sspi->base + SPISETUP); - for (i = 0; i < len; i++) { - ret = s5l8702_spi_wait_rx(sspi); - if (ret) goto out_rxmode; - rx[i] = readl(sspi->base + SPIRXDATA); - } -out_rxmode: - writel(readl(sspi->base + SPISETUP) & ~SPISETUP_RXMODE, sspi->base + SPISETUP); + int ret; + + s5l8702_spi_cs(sspi, true); + + /* sub_4043D0 preamble */ + writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, + sspi->base + SPICTRL); + ret = s5l8702_wait_clear(sspi, SPISTATUS_TXBUSY_ROS); + /* After 11B70, STATUS bit6 (0x40) stays set; 0x7C0 never hits 0. */ + if (ret && (readl(sspi->base + SPISTATUS) & SPISTATUS_TXBUSY_ROS) == 0x40) + ret = 0; + if (ret) + goto out_cs; + ret = s5l8702_wait_clear(sspi, SPISTATUS_RXRDY_ROS); + if (ret) + goto out_cs; + + /* + * 4043D0: TX present → SETUP &= ~1, STATUS |= 0x400000. + * RX-only (tx==NULL) → SETUP |= 1, STATUS |= 1 (auto-clock). + */ + if (tx) { + writel(readl(sspi->base + SPISETUP) & ~SPISETUP_RXMODE, + sspi->base + SPISETUP); + writel(readl(sspi->base + SPISTATUS) | SPISTATUS_KICK, + sspi->base + SPISTATUS); } else { - // TX or full-duplex. - for (i = 0; i < len; i++) { - writel(1, sspi->base + SPIRXLIMIT); - ret = s5l8702_spi_wait_tx(sspi); - if (ret) break; - writel(tx ? tx[i] : 0xff, sspi->base + SPITXDATA); - ret = s5l8702_spi_wait_rx(sspi); - if (ret) break; - if (rx) rx[i] = readl(sspi->base + SPIRXDATA); - else readl(sspi->base + SPIRXDATA); + writel(readl(sspi->base + SPISETUP) | SPISETUP_RXMODE, + sspi->base + SPISETUP); + writel(readl(sspi->base + SPISTATUS) | 1, + sspi->base + SPISTATUS); + } + + for (i = 0; i < len; i++) { + /* 4043D0: RXLIMIT=1 only when the call has an RX buffer */ + writel(rx ? 1 : 0, sspi->base + SPIRXLIMIT); + + ret = s5l8702_wait_clear(sspi, SPISTATUS_TXBUSY_ROS); + if (ret && (readl(sspi->base + SPISTATUS) & SPISTATUS_TXBUSY_ROS) == 0x40) + ret = 0; + if (ret) + break; + + if (tx) { + writel(tx[i], sspi->base + SPITXDATA); + /* 4043D0 PIO: * (base+0x4C) = 1 after each TX word */ + writel(1, sspi->base + SPIUNK4C); + } + + if (rx) { + ret = s5l8702_wait_set(sspi, SPISTATUS_RXRDY_ROS); + if (ret) + break; + rx[i] = (u8)readl(sspi->base + SPIRXDATA); } } + ret = s5l8702_wait_clear(sspi, SPISTATUS_TXBUSY_ROS); + if (ret && (readl(sspi->base + SPISTATUS) & SPISTATUS_TXBUSY_ROS) == 0x40) + ret = 0; + + /* sub_4043D0 epilogue: SETUP &= ~0x400001 */ + writel(readl(sspi->base + SPISETUP) & ~0x400001u, + sspi->base + SPISETUP); + +out_cs: + s5l8702_spi_cs(sspi, false); + if (ret) { + sspi->last_err = ret; + sspi->last_status = readl(sspi->base + SPISTATUS); + dev_err_ratelimited(sspi->dev, + "4043D0 timeout st=%08x setup=%08x ctrl=%08x pin=%08x dd=%08x u3c=%08x u40=%08x u44=%08x\n", + sspi->last_status, + readl(sspi->base + SPISETUP), + readl(sspi->base + SPICTRL), + readl(sspi->base + SPIPIN), + readl(sspi->base + 0x38), + readl(sspi->base + 0x3c), + readl(sspi->base + 0x40), + readl(sspi->base + 0x44)); + dev_err_ratelimited(sspi->dev, "4043D0 u4c=%08x\n", + readl(sspi->base + SPIUNK4C)); + } return ret; } -static int s5l8702_spi_probe(struct platform_device *pdev) { +static int s5l8702_spi_transfer_one(struct spi_controller *ctlr, + struct spi_device *spi, + struct spi_transfer *xfer) +{ + struct s5l8702_spi *sspi = spi_controller_get_devdata(ctlr); + + (void)spi; + return s5l8702_spi_pio_one(sspi, xfer->tx_buf, xfer->rx_buf, xfer->len); +} + +static int s5l8702_spi_probe(struct platform_device *pdev) +{ struct spi_controller *ctlr; struct s5l8702_spi *sspi; + struct resource *res; int ret; ctlr = devm_spi_alloc_host(&pdev->dev, sizeof(*sspi)); - if (!ctlr) return -ENOMEM; + if (!ctlr) + return -ENOMEM; sspi = spi_controller_get_devdata(ctlr); + sspi->dev = &pdev->dev; + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); sspi->base = devm_platform_ioremap_resource(pdev, 0); - if (IS_ERR(sspi->base)) return PTR_ERR(sspi->base); + if (IS_ERR(sspi->base)) + return PTR_ERR(sspi->base); - sspi->pcon0 = devm_ioremap(&pdev->dev, S5L8702_PCON0_PHYS, 4); - sspi->gpiocmd = devm_ioremap(&pdev->dev, S5L8702_GPIOCMD_PHYS, 4); - sspi->pwrcon1 = devm_ioremap(&pdev->dev, S5L8702_PWRCON1_PHYS, 4); - if (!sspi->pcon0 || !sspi->gpiocmd || !sspi->pwrcon1) return -ENOMEM; + sspi->spi0 = res && res->start == SPI0_BASE_PHYS; + sspi->spi2_nimbus = res && res->start == SPI2_BASE_PHYS; + + /* Optional DT clocks (CLK_SPI* / secondary); ignore -ENOENT */ + ret = devm_clk_bulk_get_all(&pdev->dev, &sspi->clks); + if (ret > 0) { + sspi->num_clks = ret; + ret = clk_bulk_prepare_enable(sspi->num_clks, sspi->clks); + if (ret) + dev_warn(&pdev->dev, "clk_bulk_prepare_enable failed: %d\n", ret); + else + dev_info(&pdev->dev, "enabled %d SPI clockgate(s)\n", sspi->num_clks); + } else { + sspi->num_clks = 0; + } - // Route GPIO pins to SPI function - writel((readl(sspi->pcon0) & ~PCON0_SPI_MASK) | PCON0_SPI_FUNC, sspi->pcon0); + if (sspi->spi0) { + sspi->pwrcon1 = devm_ioremap(&pdev->dev, S5L8702_PWRCON1_PHYS, 4); + sspi->gpiocmd = devm_ioremap(&pdev->dev, S5L8702_GPIOCMD_PHYS, 4); + sspi->gpio_base = devm_ioremap(&pdev->dev, S5L8702_PCON0_PHYS, 0x400); + if (!sspi->pwrcon1 || !sspi->gpiocmd || !sspi->gpio_base) + return -ENOMEM; + writel(readl(sspi->pwrcon1) & ~PWRCON1_SPI0_BIT, sspi->pwrcon1); + { + void __iomem *pwrcon4 = ioremap(S5L8702_PWRCON4_PHYS, 4); - // Enable SPI0 clock gate - writel(readl(sspi->pwrcon1) & ~PWRCON1_SPI0_BIT, sspi->pwrcon1); + if (pwrcon4) { + writel(readl(pwrcon4) & ~PWRCON4_SPI0_2_BIT, pwrcon4); + iounmap(pwrcon4); + } + } + s5l8702_spi0_pinmux(sspi); + s5l8702_spi0_11b70(sspi); + dev_info(&pdev->dev, "SPI0 CS42 4043D0 CS=SPIPIN.1 PWRCON1=%08x\n", + readl(sspi->pwrcon1)); + } else if (sspi->spi2_nimbus) { + void __iomem *pwrcon4; - // Deassert CS - writel(GPIOCMD_SPI0_CS_DEASSERT, sspi->gpiocmd); + sspi->pwrcon1 = devm_ioremap(&pdev->dev, S5L8702_PWRCON1_PHYS, 4); + sspi->gpiocmd = devm_ioremap(&pdev->dev, S5L8702_GPIOCMD_PHYS, 4); + sspi->gpio_base = devm_ioremap(&pdev->dev, S5L8702_PCON0_PHYS, 0x400); + if (!sspi->pwrcon1 || !sspi->gpiocmd || !sspi->gpio_base) + return -ENOMEM; + /* bit16 = 8702 SPI2; bit15 = 8720 SPI2 / I2S2 overlap — clear both */ + writel(readl(sspi->pwrcon1) & ~(PWRCON1_SPI2_BIT | BIT(15)), + sspi->pwrcon1); + pwrcon4 = ioremap(0x3c50006cUL, 4); + if (pwrcon4) { + writel(readl(pwrcon4) & ~BIT(15), pwrcon4); /* SPI2_2 */ + iounmap(pwrcon4); + } + dev_info(&pdev->dev, "SPI2 PWRCON1=%08x (after ungate)\n", + readl(sspi->pwrcon1)); + s5l8702_spi2_pinmux(sspi); + s5l8702_spi2_11b70(sspi); + dev_info(&pdev->dev, + "SPI2 Nimbus 4043D0 PIO (SETUP=0x%x CLKDIV=2 CS=SPIPIN.1 11B70)\n", + SPISETUP_SPI0_11B70); + } ctlr->dev.of_node = pdev->dev.of_node; ctlr->bus_num = pdev->id; @@ -166,15 +514,17 @@ static int s5l8702_spi_probe(struct platform_device *pdev) { platform_set_drvdata(pdev, ctlr); ret = devm_spi_register_controller(&pdev->dev, ctlr); - if (ret) dev_err(&pdev->dev, "failed to register SPI controller: %d\n", ret); + if (ret) + dev_err(&pdev->dev, "failed to register SPI controller: %d\n", ret); return ret; } static const struct of_device_id s5l8702_spi_of_match[] = { { .compatible = "apple,s5l8702-spi" }, + { .compatible = "samsung,s5l8702-spi" }, + { .compatible = "samsung,s5l8740-spi" }, {} }; - MODULE_DEVICE_TABLE(of, s5l8702_spi_of_match); static struct platform_driver s5l8702_spi_driver = { @@ -186,6 +536,5 @@ static struct platform_driver s5l8702_spi_driver = { }; module_platform_driver(s5l8702_spi_driver); -MODULE_DESCRIPTION("SPI controller driver for Samsung/Apple S5L8702"); -MODULE_AUTHOR("Tucker Osman "); +MODULE_DESCRIPTION("SPI controller driver for Samsung/Apple S5L87xx"); MODULE_LICENSE("GPL v2"); diff --git a/drivers/usb/dwc2/gadget.c b/drivers/usb/dwc2/gadget.c index ef1df44ee1b2ea..5bcb20c0478611 100644 --- a/drivers/usb/dwc2/gadget.c +++ b/drivers/usb/dwc2/gadget.c @@ -356,6 +356,12 @@ static void dwc2_hsotg_init_fifo(struct dwc2_hsotg *hsotg) dwc2_writel(hsotg, hsotg->hw_params.total_fifo_size | addr << GDFIFOCFG_EPINFOBASE_SHIFT, GDFIFOCFG); + dev_info(hsotg->dev, + "s5l87xx hwfifo rx=%u np=%u d1=%u d2=%u d3=%u d4=%u d5=%u d6=%u\n", + hsotg->params.g_rx_fifo_size, hsotg->params.g_np_tx_fifo_size, + hsotg->params.g_tx_fifo_size[1], hsotg->params.g_tx_fifo_size[2], + hsotg->params.g_tx_fifo_size[3], hsotg->params.g_tx_fifo_size[4], + hsotg->params.g_tx_fifo_size[5], hsotg->params.g_tx_fifo_size[6]); /* * according to p428 of the design guide, we need to ensure that * all fifos are flushed before continuing @@ -4210,8 +4216,13 @@ static int dwc2_hsotg_ep_enable(struct usb_ep *ep, val = (val >> FIFOSIZE_DEPTH_SHIFT) * 4; if (val < size) continue; - /* Search for smallest acceptable fifo */ - if (val < fifo_size) { + /* Notify (8 B) keeps a stub. HS bulk takes the largest. */ + if (size >= 64) { + if (!fifo_index || val > fifo_size) { + fifo_size = val; + fifo_index = i; + } + } else if (val < fifo_size) { fifo_size = val; fifo_index = i; } @@ -4227,6 +4238,9 @@ static int dwc2_hsotg_ep_enable(struct usb_ep *ep, epctrl |= DXEPCTL_TXFNUM(fifo_index); hs_ep->fifo_index = fifo_index; hs_ep->fifo_size = fifo_size; + dev_info(hsotg->dev, + "s5l87xx ep%u in fifo=%u bytes=%u mp=%u\n", + index, fifo_index, fifo_size, size); } /* for non control endpoints, set PID to D0 */ diff --git a/drivers/usb/dwc2/params.c b/drivers/usb/dwc2/params.c index 587271202917f3..9f03d4574e37c1 100644 --- a/drivers/usb/dwc2/params.c +++ b/drivers/usb/dwc2/params.c @@ -110,24 +110,21 @@ static void dwc2_set_s5l8702_params(struct dwc2_hsotg *hsotg) { struct dwc2_core_params *p = &hsotg->params; + /* Nano 3G (S5L8702) — lemonjesus bring-up. Do NOT share blindly with N20/N31. */ p->speed = DWC2_SPEED_PARAM_HIGH; p->otg_caps.hnp_support = true; p->otg_caps.srp_support = true; p->phy_utmi_width = 16; - // If we enable DMA, we get BULK packet corruption, eg. in the CDC EEM - // gadget, eg.: - // [ 5.530000] g_ether gadget.0: invalid EEM CRC - // I _think_ this is a DMA mechanism built into the DWC2 core, and not using - // the kernel DMAEngine, as that seems to be working fine (and this happens - // even if we have no DMA controllers). + /* DWC2 internal DMA corrupts BULK on this core (CDC EEM CRC). */ p->g_dma = false; - // The hardware expects USBRST/ENUMDONE/IEPInt/OEPInt/USBSUSP/WKUPINT/etc. - // masked until SessReqInt/ConIDStsChng signals session-valid. Without - // this gating, these bits fire during disconnect and the OTG state - // machine has no opportunity to make progress. + /* Gate device IRQs until SessReqInt/ConIDStsChng — Nano3-specific. */ p->session_valid_gintmsk_quirk = true; } +/* + * Nano 6G/7G (S5L872x/8740). Same g_dma caution; omit Nano3 GINTMSK gating + * until proven required (Slackware/INIT: Nano3 PHY/dwc2 deltas broke N7). + */ static void dwc2_set_s5l87xx_params(struct dwc2_hsotg *hsotg) { struct dwc2_core_params *p = &hsotg->params; @@ -477,8 +474,10 @@ const struct of_device_id dwc2_of_match_table[] = { { .compatible = "snps,dwc2" }, { .compatible = "samsung,s3c6400-hsotg", .data = dwc2_set_s3c6400_params }, - { .compatible = "apple,s5l87xx-usb", + { .compatible = "apple,s5l8702-usb", .data = dwc2_set_s5l8702_params }, + { .compatible = "apple,s5l87xx-usb", + .data = dwc2_set_s5l87xx_params }, { .compatible = "apple,s5l8740-usb", .data = dwc2_set_s5l87xx_params }, { .compatible = "amlogic,meson8-usb", diff --git a/drivers/usb/gadget/function/f_rndis.c b/drivers/usb/gadget/function/f_rndis.c index 050627f792975b..c3775774a23bee 100644 --- a/drivers/usb/gadget/function/f_rndis.c +++ b/drivers/usb/gadget/function/f_rndis.c @@ -580,14 +580,19 @@ static int rndis_set_alt(struct usb_function *f, unsigned intf, unsigned alt) DBG(cdev, "RNDIS RX/TX early activation ... \n"); net = gether_connect(&rndis->port); - if (IS_ERR(net)) + if (IS_ERR(net)) { + pr_info("s5l87xx gether_connect err=%ld\n", + PTR_ERR(net)); return PTR_ERR(net); + } + pr_info("s5l87xx gether_connect ok\n"); rndis_set_param_dev(rndis->params, net, &rndis->port.cdc_filter); rndis_set_param_medium(rndis->params, RNDIS_MEDIUM_802_3, gether_bitrate(cdev->gadget) / 100); rndis_signal_connect(rndis->params); + pr_info("s5l87xx rndis set_alt signal_connect\n"); } else goto fail; diff --git a/drivers/usb/gadget/function/rndis.c b/drivers/usb/gadget/function/rndis.c index 6a4810658767f8..59992c823ce3c4 100644 --- a/drivers/usb/gadget/function/rndis.c +++ b/drivers/usb/gadget/function/rndis.c @@ -511,6 +511,8 @@ static int gen_ndis_set_resp(struct rndis_params *params, u32 OID, * MULTICAST, ALL_MULTICAST, BROADCAST */ *params->filter = (u16)get_unaligned_le32(buf); + pr_info("s5l87xx rndis filter ndis=0x%x\n", + *params->filter); pr_debug("%s: RNDIS_OID_GEN_CURRENT_PACKET_FILTER %08x\n", __func__, *params->filter); @@ -818,6 +820,7 @@ int rndis_msg_parser(struct rndis_params *params, u8 *buf) ret = rndis_init_response(params, (rndis_init_msg_type *)buf); rndis_signal_connect(params); + pr_info("s5l87xx rndis INIT connected (tx after SET)\n"); return ret; } diff --git a/drivers/usb/misc/Kconfig b/drivers/usb/misc/Kconfig index 6497c4e81e951a..afbad6a01eb0cc 100644 --- a/drivers/usb/misc/Kconfig +++ b/drivers/usb/misc/Kconfig @@ -343,3 +343,4 @@ config USB_ONBOARD_DEV_USB5744 during hub start-up configuration stage. It is must to enable this option on AMD Kria KR260 Robotics Starter Kit as this hub is connected to USB-SD converter which mounts the root filesystem. + diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig index 7d476c673f6223..f8abafd4941fa5 100644 --- a/drivers/video/backlight/Kconfig +++ b/drivers/video/backlight/Kconfig @@ -492,12 +492,10 @@ config BACKLIGHT_LED endif # BACKLIGHT_CLASS_DEVICE +endmenu config BACKLIGHT_S5L8740 - tristate "Samsung/Apple S5L8740 backlight" - depends on BACKLIGHT_CLASS_DEVICE + tristate "Samsung/Apple S5L8740 LCD backlight" + depends on BACKLIGHT_CLASS_DEVICE && HAS_IOMEM help - LCD backlight MMIO at 0x3E000000 for iPod nano 7G (N31). - Does not touch LCDIF CON/PHTIME. - -endmenu + Backlight controller at 0x3E000000 for iPod nano 7G (N31). diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile index ae1ebfffc9b89d..4defc75de0b094 100644 --- a/drivers/video/backlight/Makefile +++ b/drivers/video/backlight/Makefile @@ -26,6 +26,7 @@ obj-$(CONFIG_BACKLIGHT_APPLE) += apple_bl.o obj-$(CONFIG_BACKLIGHT_AS3711) += as3711_bl.o obj-$(CONFIG_BACKLIGHT_BD6107) += bd6107.o obj-$(CONFIG_BACKLIGHT_CLASS_DEVICE) += backlight.o +obj-$(CONFIG_BACKLIGHT_S5L8740) += backlight-s5l8740.o obj-$(CONFIG_BACKLIGHT_DA903X) += da903x_bl.o obj-$(CONFIG_BACKLIGHT_DA9052) += da9052_bl.o obj-$(CONFIG_BACKLIGHT_EP93XX) += ep93xx_bl.o @@ -60,4 +61,3 @@ obj-$(CONFIG_BACKLIGHT_WM831X) += wm831x_bl.o obj-$(CONFIG_BACKLIGHT_ARCXCNN) += arcxcnn_bl.o obj-$(CONFIG_BACKLIGHT_RAVE_SP) += rave-sp-backlight.o obj-$(CONFIG_BACKLIGHT_LED) += led_bl.o -obj-$(CONFIG_BACKLIGHT_S5L8740) += backlight-s5l8740.o diff --git a/freemyipod/initramfs/rcS b/freemyipod/initramfs/rcS new file mode 100755 index 00000000000000..8731e36df86f90 --- /dev/null +++ b/freemyipod/initramfs/rcS @@ -0,0 +1,20 @@ +#!/bin/sh + +# Bring up USB gadget network. g_ether may take a moment to register usb0 +# after the host enumerates the device, so retry briefly. +for i in 1 2 3 4 5 6 7 8 9 10; do + if ifconfig usb0 192.168.7.2 netmask 255.255.255.0 up 2>/dev/null; then + echo "usb0 up at 192.168.7.2 (host: 192.168.7.1)" + break + fi + sleep 1 +done + +# No-auth root shell over telnet for development. +telnetd -l /bin/sh && echo "telnetd listening on :23" + +# Sleep/KEY_POWER → poweroff when power-watch is present in rootfs +if [ -x /usr/bin/power-watch ] || [ -x /bin/power-watch ]; then + (command -v power-watch >/dev/null && power-watch) & + echo "power-watch: KEY_POWER → poweroff -f" +fi diff --git a/include/dt-bindings/clock/samsung,s5l8702-clock.h b/include/dt-bindings/clock/samsung,s5l8702-clock.h old mode 100644 new mode 100755 index 5a532f87b8b62e..ce1517797d0582 --- a/include/dt-bindings/clock/samsung,s5l8702-clock.h +++ b/include/dt-bindings/clock/samsung,s5l8702-clock.h @@ -1,13 +1,77 @@ /* SPDX-License-Identifier: GPL-2.0 */ /* - * Device Tree binding constants for Samsung S5L8702 clock controller. + * DT bindings for Samsung/Apple S5L8702/S5L8740 CLKCON @0x3C500000 + * + * Gate IDs match Rockbox CLOCKGATE_* (bank = id>>5, bit = id&31). + * Enable polarity: CLEAR bit in PWRCONn (CLK_GATE_SET_TO_DISABLE). */ #ifndef _DT_BINDINGS_CLOCK_SAMSUNG_S5L8702_CLOCK_H #define _DT_BINDINGS_CLOCK_SAMSUNG_S5L8702_CLOCK_H -#define CLK_SHA1 0 -#define CLK_AES 1 -#define CLK_PRNG 2 +/* Legacy crypto IDs (keep stable) */ +#define CLK_SHA1 0 +#define CLK_AES 1 +#define CLK_PRNG 2 -#endif /* _DT_BINDINGS_CLOCK_SAMSUNG_S5L8702_CLOCK_H */ +/* Fixed / derived */ +#define CLK_OSC24 3 +#define CLK_PCLK 4 + +/* PWRCON0 / AHB @ +0x48 (gates 0..31) */ +#define CLK_LCD 5 +#define CLK_USBOTG 6 +#define CLK_SMX 7 +#define CLK_SM1 8 +#define CLK_ATA 9 +#define CLK_NAND 10 +#define CLK_AES_AHB 11 +#define CLK_NANDECC 12 +#define CLK_DMAC0 13 +#define CLK_DMAC1 14 +#define CLK_ROM 15 + +/* PWRCON1 / APB @ +0x4C (gates 32..63) — N31 proven subset */ +#define CLK_RTC 16 +#define CLK_CWHEEL 17 +#define CLK_SPI0 18 +#define CLK_USBPHY 19 +#define CLK_I2C0 20 +#define CLK_TIMER 21 +#define CLK_I2C1 22 +#define CLK_I2S0 23 +#define CLK_UART 24 +#define CLK_I2S1 25 +#define CLK_SPI1 26 +#define CLK_GPIO 27 +#define CLK_CHIPID 28 +#define CLK_I2S2 29 +#define CLK_SPI2 30 +/* Ambiguous 8720 map: SPI2 sometimes bit15 (gate 47) */ +#define CLK_SPI2_ALT 31 + +/* PWRCON2 @ +0x58 */ +#define CLK_SPI3 32 + +/* PWRCON4 secondary @ +0x6C (8720-style; harmless if RO on 8740) */ +#define CLK_SPI0_2 33 +#define CLK_SPI1_2 34 +#define CLK_SPI2_2 35 +#define CLK_I2C0_2 36 +#define CLK_I2C1_2 37 +#define CLK_UART_2 38 +#define CLK_LCD_2 39 +#define CLK_TIMERA_2 40 + +/* CG16 / RetailOS sub_41CBD8 gate IDs 7..13 (div-half enable = clear 0x8000/0x80000000) */ +#define CLK_CG16_7 41 +#define CLK_CG16_8 42 +#define CLK_CG16_9 43 +#define CLK_CG16_10 44 +#define CLK_CG16_11 45 +#define CLK_CG16_12 46 +#define CLK_CG16_13 47 + +#define CLK_S5L8702_NR_CLKS 48 + +#endif diff --git a/include/linux/n31-glass-mark.h b/include/linux/n31-glass-mark.h new file mode 100755 index 00000000000000..f3f587ec7d8e0d --- /dev/null +++ b/include/linux/n31-glass-mark.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * N31 LCD-only bring-up witness — no serial, no U-Boot vidconsole. + * pr_alert() + console=tty0 → readable on fbcon once DRM is up. + */ +#ifndef _LINUX_N31_GLASS_MARK_H +#define _LINUX_N31_GLASS_MARK_H + +#include + +#define n31_glass_mark(tag) \ + pr_alert("N31>> %s\n", (tag)) + +#endif /* _LINUX_N31_GLASS_MARK_H */ diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 793f7782e0d721..4f2f7a4e24a0d1 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -6,3 +6,24 @@ config SND_SOC_APPLE_MCA help This option enables an ASoC platform driver for MCA peripherals found on Apple Silicon SoCs. + +config SND_SOC_APPLE_NANO7 + tristate "iPod nano 7G audio machine" + depends on SND_SOC + select SND_SOC_APPLE_S5L8740_I2S + help + Registers ASoC card: S5L8740 IIS CPU DAI + CS42/dummy codec. + +config SND_SOC_APPLE_S5L8740_I2S + tristate "S5L8740 IIS0 I2S CPU DAI" + depends on SND_SOC && HAS_IOMEM + select SND_SOC_GENERIC_DMAENGINE_PCM + select SND_DMAENGINE_PCM + help + IIS0 @0x3CA00000 CPU DAI with optional PL080 dmaengine PCM. + +config SND_SOC_APPLE_CS42L81_SPI + tristate "CS42L81 / 338S1146 SPI control (N31)" + depends on SPI + help + RetailOS-matched SPI0 framing (0x6C/0x6D) and corpus bring-up. diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index 1eb8fbef60c617..f824672cde46ad 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -1,3 +1,6 @@ snd-soc-apple-mca-y := mca.o obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o +obj-$(CONFIG_SND_SOC_APPLE_NANO7) += nano7-audio.o +obj-$(CONFIG_SND_SOC_APPLE_CS42L81_SPI) += cs42l81-spi.o +obj-$(CONFIG_SND_SOC_APPLE_S5L8740_I2S) += s5l8740-i2s.o diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c new file mode 100755 index 00000000000000..ca35e915e031f9 --- /dev/null +++ b/sound/soc/apple/cs42l81-spi.c @@ -0,0 +1,863 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * CS42L81 / Apple 338S1146 — SPI0 control path (N31 RetailOS-matched) + * + * Framing (sub_43CDB4 / sub_43CDFA): + * write: 0x6C, reg_hi, reg_lo, 0, data + * read: 0x6D, reg_hi, reg_lo, 0, 0xFF (rx data on last byte) + * + * Bring-up (confirmed corpus): + * read 0x227; write 0xC96F=0x0E|0x1E; 0x9901 unlock A5 then 0; + * 0xC81F=0xFF; 0xC85F=0x0F + * + * RetailOS user volume is an integer 0..256 (debug HUD, NVRAM key 11/179 + * clamped in 37138, Vol+/- in 1BB754/1BB874). That value is a CoreAudio + * VolumeScalar (256 = unity), not a CS42 mixer byte. + * + * 0x403/0x404 are HP mixer tap indices from 174E7C / 440AA4(udiv, 160): + * play 5706F4(1): L=(0+159)/160+2 = 2, R=+1 = 1 + * Analog 0x527 is mute 0xFF / unmute 0x60 (F141C). Do not unlock 9901 + * after mixer. + * + * ASoC DAI cs42l81-hifi: analog on hw_params. IIS serializer is the CPU DAI. + * + * CS42L42/L83 (I2C, paged 8-bit regmap, snd_soc_cs42l42) are a newer Cirrus + * line with nearly identical maps — chip ID + MCLK_CTL defaults differ. N31 + * CS42L81 / 338S1146 is SPI-framed 16-bit addresses (see N31-102-CS42-REG-CORPUS); + * do not drop in cs42l42.c wholesale. + * + * Cirrus bring-up notes: + * Reset mutes outputs (0x527=0xFF); unmute 0x60 on play. + * LOS: BCLK/LRCK stop clears 0x2F bit6 — asp_lock after IIS kick. + * ASP lock after IIS clocks (414FAE), not before. + * I2S slave NB_NF 16-bit; no DAPM graph — path is register audio_on(). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CS42L81_USER_VOL_MAX 256 +#define CS42L81_MIX_TAP_L 2 /* 174E7C play */ +#define CS42L81_MIX_TAP_R 1 + +struct cs42l81 { + struct spi_device *spi; + struct mutex lock; + unsigned int user_vol; + bool dai_mute; +}; + +static struct cs42l81 *cs42l81_dev; + +int cs42l81_post_iis_start(void); +int cs42l81_play_prepare(void); + +static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val); +static int cs42l81_set_mute(struct cs42l81 *c, int mute); +static int cs42l81_apply_user_vol(struct cs42l81 *c); + +static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val) +{ + u8 tx[5] = { + 0x6c, + (reg >> 8) & 0xff, + reg & 0xff, + 0x00, + val, + }; + struct spi_transfer t = { .tx_buf = tx, .len = 5 }; + struct spi_message m; + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + return spi_sync(c->spi, &m); +} + +static int cs42l81_read(struct cs42l81 *c, u16 reg, u8 *val) +{ + u8 tx[5] = { + 0x6d, + (reg >> 8) & 0xff, + reg & 0xff, + 0x00, + 0xff, + }; + u8 rx[5] = { 0 }; + struct spi_transfer t = { .tx_buf = tx, .rx_buf = rx, .len = 5 }; + struct spi_message m; + int ret; + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + ret = spi_sync(c->spi, &m); + if (ret) + return ret; + *val = rx[4]; + return 0; +} + +static int cs42l81_rmw(struct cs42l81 *c, u16 reg, u8 mask, u8 val) +{ + u8 cur = 0; + int ret = cs42l81_read(c, reg, &cur); + + if (ret) + return ret; + return cs42l81_write(c, reg, (cur & ~mask) | (val & mask)); +} + +static int cs42l81_bringup(struct cs42l81 *c) +{ + u8 st = 0; + int ret; + + ret = cs42l81_read(c, 0x0227, &st); + dev_info(&c->spi->dev, "CS42 status 0x227 = 0x%02x (ret=%d)\n", st, ret); + + /* rail / backpower-ish — prefer safe idle 0x0E over 0x1E */ + ret = cs42l81_write(c, 0xc96f, 0x0e); + if (ret) + return ret; + + /* unlock-like */ + cs42l81_write(c, 0x9901, 0xa5); + cs42l81_write(c, 0x9901, 0x00); + + cs42l81_write(c, 0xc81f, 0xff); + cs42l81_write(c, 0xc85f, 0x0f); + + cs42l81_read(c, 0x0219, &st); + dev_info(&c->spi->dev, "CS42 companion 0x219 = 0x%02x\n", st); + return 0; +} + +/* sub_5707D8 ASP/mixer blast. Bytes from Hex-Rays, not invented. */ +static const u8 cs42l81_mix400[] = { + 0x04, 0x10, 0x00, 0x09, 0x08, 0x00, 0x00, 0x00, + 0x01, 0xe0, 0x01, 0x01, 0xe0, 0xfe, 0x00, 0xa0, + 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, + 0x00, 0x05, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, + 0x0a, 0x01, 0xe0, 0x0b, 0x01, 0xe0, 0xff, 0x00, + 0xa0, 0x0c, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x0e, + 0x00, 0x00, 0x0f, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x11, 0x00, 0x00, 0x12, 0x00, 0x00, 0x13, 0x00, + 0x00, +}; + +/* D3280(4) + 400330 2v5 + 183138(48k) + D3280(3) HP + 5707D8. */ +static int cs42l81_audio_on(struct cs42l81 *c) +{ + u8 st = 0, r219 = 0; + unsigned int i; + int ret; + + /* sub_D3280(a1==4) */ + cs42l81_rmw(c, 0x0007, 0x40, 0x00); + cs42l81_rmw(c, 0x0219, 0x78, 0x78); + cs42l81_write(c, 0x0229, 0x40); + cs42l81_rmw(c, 0x0006, 0x01, 0x00); + cs42l81_rmw(c, 0x0201, 0xe0, 0x40); + cs42l81_write(c, 0xc81f, 0xff); + cs42l81_write(c, 0xc85f, 0x0f); + ret = cs42l81_write(c, 0xc96f, 0x0e); + if (ret) + return ret; + cs42l81_write(c, 0x0223, 0x08); + cs42l81_write(c, 0x0224, 0x09); + cs42l81_write(c, 0x0225, 0x00); + + /* sub_400330: 2.5V backpower — RetailOS WRITES 0x219 */ + cs42l81_rmw(c, 0x0219, 0x07, 0x01); + msleep(100); + cs42l81_write(c, 0xc96f, 0x1e); + /* Glass: 5-byte write left 0x2F=0x00. write6 left 0x2F=0x80 (off). */ + cs42l81_write(c, 0x0227, 0x40); + + /* sub_183138 48 kHz (v10=12) */ + cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); + cs42l81_write(c, 0x012f, 0xcc); + cs42l81_write(c, 0x010b, 0x08); + cs42l81_write(c, 0x010c, 0x09); + cs42l81_rmw(c, 0x0131, 0x01, 0x01); + cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + cs42l81_rmw(c, 0x0220, 0x20, 0x20); + + /* sub_5707D8 */ + cs42l81_write(c, 0x0006, 0x24); + cs42l81_write(c, 0x0529, 0x2c); + cs42l81_write(c, 0x052a, 0x2c); + cs42l81_write(c, 0x0533, 0x2c); + cs42l81_write(c, 0x0534, 0x2c); + for (i = 0; i < ARRAY_SIZE(cs42l81_mix400); i++) + cs42l81_write(c, 0x0400 + i, cs42l81_mix400[i]); + cs42l81_write(c, 0x0400, 0x04); + cs42l81_write(c, 0x0401, 0x12); + /* + * 570620(1) → 174E7C: 0x403/0x404 are mixer tap indices, not + * the 0–256 user volume and not a saturate-at-0xA0 gain. + * 440AA4 is unsigned divide by 160. + */ + cs42l81_write(c, 0x0402, 0x00); + cs42l81_write(c, 0x0403, CS42L81_MIX_TAP_L); + cs42l81_write(c, 0x0404, CS42L81_MIX_TAP_R); + cs42l81_write(c, 0x0405, 0x00); + cs42l81_write(c, 0x0406, 0x00); + msleep(100); + cs42l81_write(c, 0x0500, 0x05); + /* sub_F141C(1): unmute/level. Mute path writes 0xFF. */ + cs42l81_write(c, 0x0527, 0x60); + /* 26DDDE → 416440 → 40C028(2): 42A5D6(117, 63, 60). Not 0x75 bit7. */ + cs42l81_rmw(c, 0x0075, 0x3f, 0x3c); + /* 570620: 42A5D6(1359, 240, 0) */ + cs42l81_rmw(c, 0x054f, 0xf0, 0x00); + cs42l81_rmw(c, 0x0220, 0x28, 0x28); + + /* + * D3280(3) after mixer: 0x0F bit7 (pad drive) and 0x220. + * D2EFC/9901 already ran in bringup — do not unlock again + * (that wiped 0x527/mixer on glass). 41CBD8(9,1) is IIS ungate. + */ + cs42l81_rmw(c, 0x0007, 0x40, 0x00); + cs42l81_rmw(c, 0x0006, 0x40, 0x00); + cs42l81_rmw(c, 0x0220, 0x28, 0x28); + cs42l81_rmw(c, 0x000f, 0x80, 0x80); + cs42l81_rmw(c, 0x0075, 0x40, 0x40); + { + u8 r74 = 0, r7b = 0, r7c = 0, r0f = 0, r2f = 0; + + cs42l81_read(c, 0x0074, &r74); + cs42l81_write(c, 0x0074, (r74 & 0xe7) | 0x08); + cs42l81_read(c, 0x007b, &r7b); + cs42l81_read(c, 0x007c, &r7c); + cs42l81_write(c, 0x0074, r74); + cs42l81_rmw(c, 0x0075, 0x40, 0x00); + cs42l81_rmw(c, 0x0075, 0x80, 0x80); + cs42l81_read(c, 0x000f, &r0f); + cs42l81_read(c, 0x002f, &r2f); + dev_info(&c->spi->dev, + "D3280(3) 0x0F=0x%02x 0x2F=0x%02x 0x7B=0x%02x 0x7C=0x%02x\n", + r0f, r2f, r7b, r7c); + } + + /* + * D34C0 with 892A038=0x28 (D3280(3)): short serial, not 183138. + * 0x0E bits7-6 = 11 then 01, 0x0F low=12, 0x12F=0xCC. + * Short path does not touch 0x131 — 183138 already set bit0. + */ + cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); + cs42l81_write(c, 0x012f, 0xcc); + cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + + /* + * 4F08: enable tip/ring sense. 7984: Class-H charge-pump kick + * plus 0x0B headset-type (CS42L73-class HP stays Hi-Z until this). + * 40C028(2) already programmed 0x75=0x3C; D3280(3) set bit7 (HP). + */ + cs42l81_rmw(c, 0x0073, 0xc3, 0x00); + cs42l81_rmw(c, 0x0073, 0xc0, 0xc0); + cs42l81_rmw(c, 0x0079, 0x60, 0x00); + { + u8 r220 = 0, r2f = 0, r0b = 0, r08 = 0, r09 = 0; + unsigned int i; + + cs42l81_read(c, 0x0220, &r220); + cs42l81_rmw(c, 0x0220, 0x40, 0x40); + msleep(1); + cs42l81_rmw(c, 0x0009, 0xc0, 0xc0); + for (i = 0; i < 3; i++) { + msleep(1); + cs42l81_read(c, 0x002f, &r2f); + if (r2f & 0x40) + break; + } + cs42l81_read(c, 0x000b, &r0b); + cs42l81_rmw(c, 0x0009, 0xc0, 0x80); + cs42l81_rmw(c, 0x0220, 0x40, r220 & 0x40); + cs42l81_read(c, 0x0008, &r08); + cs42l81_read(c, 0x0009, &r09); + dev_info(&c->spi->dev, + "HSDET 0x0B=0x%02x type=%u 0x2F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", + r0b, r0b & 3, r2f, r08, r09); + } + /* 42D364(1) play: unmute HP amp + mixer bit1. */ + cs42l81_write(c, 0x0527, 0x60); + cs42l81_rmw(c, 0x0401, 0x03, 0x02); + + cs42l81_read(c, 0x0227, &st); + cs42l81_read(c, 0x0219, &r219); + cs42l81_apply_user_vol(c); + dev_info(&c->spi->dev, + "CS42 audio_on C96F=0x1E status 0x227=0x%02x 0x219=0x%02x vol=%u/%u\n", + st, r219, c->user_vol, CS42L81_USER_VOL_MAX); + return 0; +} + +/* 42D364(0/1) + F141C: play unmute 0x527=0x60, mute 0xFF. */ +static int cs42l81_set_mute(struct cs42l81 *c, int mute) +{ + if (mute) { + cs42l81_write(c, 0x0527, 0xff); + cs42l81_rmw(c, 0x0401, 0x03, 0x01); + /* F1444: pulse meter/soft-ramp bits. */ + cs42l81_rmw(c, 0x051e, 0x20, 0x20); + cs42l81_rmw(c, 0x051e, 0x20, 0x00); + cs42l81_rmw(c, 0x0523, 0x20, 0x20); + cs42l81_rmw(c, 0x0523, 0x20, 0x00); + } else { + cs42l81_write(c, 0x0527, 0x60); + cs42l81_rmw(c, 0x0401, 0x03, 0x02); + } + return 0; +} + +static void cs42l81_push_pcm_q8(unsigned int vol) +{ + void (*set)(unsigned int); + + set = (void (*)(unsigned int))__symbol_get("s5l8740_set_user_vol_q8"); + if (set) { + set(vol); + __symbol_put("s5l8740_set_user_vol_q8"); + } +} + +/* RetailOS 0 = analog mute; 1..256 = unmute + Q8 PCM scalar (256 = unity). */ +static int cs42l81_apply_user_vol(struct cs42l81 *c) +{ + unsigned int q8 = c->dai_mute ? 0 : c->user_vol; + + cs42l81_push_pcm_q8(q8); + return cs42l81_set_mute(c, q8 == 0); +} + +static ssize_t reg_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + unsigned int reg, val; + int n; + + n = sscanf(buf, "%x %x", ®, &val); + if (n != 2 || reg > 0xffff || val > 0xff) + return -EINVAL; + mutex_lock(&c->lock); + n = cs42l81_write(c, (u16)reg, (u8)val); + mutex_unlock(&c->lock); + return n ? n : count; +} + +static ssize_t reg_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "write: echo \"RRRR VV\" > reg (hex)\n"); +} +static DEVICE_ATTR_RW(reg); + +static ssize_t status_227_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + u8 st = 0; + int ret; + + mutex_lock(&c->lock); + ret = cs42l81_read(c, 0x0227, &st); + mutex_unlock(&c->lock); + if (ret) + return ret; + return sysfs_emit(buf, "0x%02x\n", st); +} +static DEVICE_ATTR_RO(status_227); + +static ssize_t bringup_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + int ret; + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + + mutex_lock(&c->lock); + ret = cs42l81_bringup(c); + mutex_unlock(&c->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(bringup); + +static ssize_t dump_key_regs_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + static const u16 regs[] = { + 0x0227, 0x0219, 0xc96f, 0xc81f, 0xc85f, + 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, + 0x000c, 0x000d, 0x000e, 0x000f, 0x0012, 0x0019, + 0x0070, 0x0071, 0x0073, 0x0074, 0x0075, 0x0076, + 0x0079, 0x007a, 0x007b, 0x007c, + 0x002f, 0x0131, 0x0220, 0x0201, 0x0222, + 0x0223, 0x0224, 0x0121, 0x0122, 0x012f, + 0x010b, 0x010c, 0x0529, 0x052a, 0x0533, 0x0534, + 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, + 0x0527, 0x051e, 0x0523, 0x054f, + 0x0500, 0x051f, 0x0520, 0x0521, 0x0524, 0x0525, 0x0528, + 0x001a, 0x001b, 0x001e, 0x001f, + 0x0034, 0x0035, 0x0039, 0x003a, + }; + int i, n = 0, ret; + u8 val; + + mutex_lock(&c->lock); + for (i = 0; i < ARRAY_SIZE(regs); i++) { + ret = cs42l81_read(c, regs[i], &val); + if (ret) { + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: ERR %d\n", regs[i], ret); + } else { + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: 0x%02x\n", regs[i], val); + } + } + mutex_unlock(&c->lock); + return n; +} +static DEVICE_ATTR_RO(dump_key_regs); + +/* RetailOS user volume 0..256. 0x403/0x404 stay mixer taps. */ +static ssize_t volume_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + unsigned int vol; + int ret; + + if (kstrtouint(buf, 0, &vol)) + return -EINVAL; + if (vol > CS42L81_USER_VOL_MAX) + return -EINVAL; + + mutex_lock(&c->lock); + c->user_vol = vol; + ret = cs42l81_apply_user_vol(c); + mutex_unlock(&c->lock); + return ret ? ret : count; +} + +static ssize_t volume_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + u8 tap_l = 0, tap_r = 0, analog = 0; + int ra, rb, rc; + + mutex_lock(&c->lock); + ra = cs42l81_read(c, 0x0403, &tap_l); + rb = cs42l81_read(c, 0x0404, &tap_r); + rc = cs42l81_read(c, 0x0527, &analog); + mutex_unlock(&c->lock); + if (ra || rb || rc) + return sysfs_emit(buf, "read err %d/%d/%d\n", ra, rb, rc); + return sysfs_emit(buf, + "user=%u/%u dai_mute=%d\n" + "0x403=0x%02x 0x404=0x%02x (taps, play 2/1)\n" + "0x527=0x%02x analog_mute=%d\n", + c->user_vol, CS42L81_USER_VOL_MAX, c->dai_mute, + tap_l, tap_r, analog, analog == 0xff); +} +static DEVICE_ATTR_RW(volume); + +static ssize_t audio_on_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + int ret; + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + mutex_lock(&c->lock); + ret = cs42l81_audio_on(c); + mutex_unlock(&c->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(audio_on); + +static ssize_t mute_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + unsigned int v; + int ret; + + if (kstrtouint(buf, 0, &v)) + return -EINVAL; + mutex_lock(&c->lock); + c->dai_mute = v ? 1 : 0; + ret = cs42l81_apply_user_vol(c); + mutex_unlock(&c->lock); + return ret ? ret : count; +} + +static ssize_t mute_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + u8 v = 0; + int ret; + + mutex_lock(&c->lock); + ret = cs42l81_read(c, 0x0527, &v); + mutex_unlock(&c->lock); + if (ret) + return ret; + return sysfs_emit(buf, "0x527=0x%02x mute=%d\n", v, v == 0xff); +} +static DEVICE_ATTR_RW(mute); + +static ssize_t rreg_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + unsigned int reg; + u8 val = 0; + int ret; + + if (kstrtouint(buf, 0, ®) || reg > 0xffff) + return -EINVAL; + mutex_lock(&c->lock); + ret = cs42l81_read(c, (u16)reg, &val); + mutex_unlock(&c->lock); + if (ret) + return ret; + dev_info(&c->spi->dev, "rreg 0x%04x = 0x%02x\n", reg, val); + return count; +} +static DEVICE_ATTR_WO(rreg); + +/* + * After IIS BCLK/LRCK run (RetailOS 26DDDE: 414FAE before sustained PCM). + * Re-run 183138 clock regs and poll 0x2F bit6 (ASP sync / LOS clear). + * LOS does not always self-recover — pulse 0x220 and retry clock prog. + */ +static void cs42l81_asp_clock_pulse(struct cs42l81 *c) +{ + cs42l81_rmw(c, 0x0220, 0x20, 0x00); + udelay(50); + cs42l81_rmw(c, 0x0220, 0x20, 0x20); +} + +static void cs42l81_asp_program_48k(struct cs42l81 *c) +{ + cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); + cs42l81_write(c, 0x012f, 0xcc); + cs42l81_rmw(c, 0x000e, 0xc0, 0x40); +} + +static int cs42l81_asp_lock(struct cs42l81 *c) +{ + unsigned int attempt, i; + u8 r2f = 0, r0e = 0, r0f = 0; + + for (attempt = 0; attempt < 3; attempt++) { + if (attempt) + cs42l81_asp_clock_pulse(c); + cs42l81_asp_program_48k(c); + for (i = 0; i < 50; i++) { + cs42l81_read(c, 0x002f, &r2f); + if (r2f & 0x40) + break; + usleep_range(1000, 2000); + } + if (r2f & 0x40) + break; + } + cs42l81_read(c, 0x000e, &r0e); + cs42l81_read(c, 0x000f, &r0f); + dev_info(&c->spi->dev, + "asp_lock 0x2F=0x%02x 0x0E=0x%02x 0x0F=0x%02x (need bit6, IIS running)\n", + r2f, r0e, r0f); + return (r2f & 0x40) ? 0 : -EAGAIN; +} + +int cs42l81_play_prepare(void) +{ + struct cs42l81 *c = cs42l81_dev; + int ret; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + ret = cs42l81_audio_on(c); + if (!ret && !c->dai_mute) + cs42l81_set_mute(c, 0); + mutex_unlock(&c->lock); + return ret; +} +EXPORT_SYMBOL_GPL(cs42l81_play_prepare); + +/* + * Called after IIS TXCOM kick. Clears LOS mute and ensures HP path unmuted. + */ +int cs42l81_post_iis_start(void) +{ + struct cs42l81 *c = cs42l81_dev; + int ret; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + ret = cs42l81_asp_lock(c); + if (!ret && !c->dai_mute) + cs42l81_set_mute(c, 0); + mutex_unlock(&c->lock); + return ret; +} +EXPORT_SYMBOL_GPL(cs42l81_post_iis_start); + +static ssize_t asp_lock_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + int ret; + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + mutex_lock(&c->lock); + ret = cs42l81_asp_lock(c); + mutex_unlock(&c->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(asp_lock); + +static struct attribute *cs42l81_attrs[] = { + &dev_attr_reg.attr, + &dev_attr_status_227.attr, + &dev_attr_bringup.attr, + &dev_attr_dump_key_regs.attr, + &dev_attr_volume.attr, + &dev_attr_audio_on.attr, + &dev_attr_asp_lock.attr, + &dev_attr_mute.attr, + &dev_attr_rreg.attr, + NULL, +}; +ATTRIBUTE_GROUPS(cs42l81); + +static int cs42l81_dai_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); + int ret; + + mutex_lock(&c->lock); + ret = cs42l81_audio_on(c); + mutex_unlock(&c->lock); + dev_info(&c->spi->dev, "DAI hw_params rate=%u ret=%d\n", + params_rate(params), ret); + return ret; +} + +static int cs42l81_dai_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); + int ret = 0; + + if (substream->stream != SNDRV_PCM_STREAM_PLAYBACK) + return 0; + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + /* CPU IIS trigger runs first — BCLK/LRCK should be toggling. */ + mutex_lock(&c->lock); + ret = cs42l81_asp_lock(c); + if (!ret && !c->dai_mute) + cs42l81_set_mute(c, 0); + mutex_unlock(&c->lock); + dev_info(&c->spi->dev, "DAI trigger START asp=%d\n", ret); + break; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + break; + default: + return -EINVAL; + } + return ret; +} + +static int cs42l81_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream) +{ + struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); + + if (stream != SNDRV_PCM_STREAM_PLAYBACK) + return 0; + mutex_lock(&c->lock); + c->dai_mute = mute ? 1 : 0; + cs42l81_apply_user_vol(c); + mutex_unlock(&c->lock); + dev_info(&c->spi->dev, "DAI mute=%d user_vol=%u\n", mute, c->user_vol); + return 0; +} + +static const struct snd_soc_dai_ops cs42l81_dai_ops = { + .hw_params = cs42l81_dai_hw_params, + .trigger = cs42l81_dai_trigger, + .mute_stream = cs42l81_dai_mute_stream, +}; + +static int cs42l81_vol_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = CS42L81_USER_VOL_MAX; + return 0; +} + +static int cs42l81_vol_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *comp = snd_soc_kcontrol_component(kcontrol); + struct cs42l81 *c = snd_soc_component_get_drvdata(comp); + + ucontrol->value.integer.value[0] = c->user_vol; + return 0; +} + +static int cs42l81_vol_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *comp = snd_soc_kcontrol_component(kcontrol); + struct cs42l81 *c = snd_soc_component_get_drvdata(comp); + unsigned int vol = ucontrol->value.integer.value[0]; + int changed; + + if (vol > CS42L81_USER_VOL_MAX) + return -EINVAL; + mutex_lock(&c->lock); + changed = vol != c->user_vol; + c->user_vol = vol; + cs42l81_apply_user_vol(c); + mutex_unlock(&c->lock); + return changed; +} + +static const struct snd_kcontrol_new cs42l81_controls[] = { + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Master Playback Volume", + .info = cs42l81_vol_info, + .get = cs42l81_vol_get, + .put = cs42l81_vol_put, + }, +}; + +static struct snd_soc_dai_driver cs42l81_dai = { + .name = "cs42l81-hifi", + .playback = { + .stream_name = "Playback", + .channels_min = 2, + .channels_max = 2, + .rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000, + .formats = SNDRV_PCM_FMTBIT_S16_LE, + }, + .ops = &cs42l81_dai_ops, +}; + +static const struct snd_soc_component_driver cs42l81_component = { + .idle_bias_on = 1, + .endianness = 1, + .controls = cs42l81_controls, + .num_controls = ARRAY_SIZE(cs42l81_controls), +}; + +static int cs42l81_probe(struct spi_device *spi) +{ + struct cs42l81 *c; + int ret; + + c = devm_kzalloc(&spi->dev, sizeof(*c), GFP_KERNEL); + if (!c) + return -ENOMEM; + c->spi = spi; + c->user_vol = CS42L81_USER_VOL_MAX; + mutex_init(&c->lock); + spi_set_drvdata(spi, c); + + mutex_lock(&c->lock); + ret = cs42l81_bringup(c); + mutex_unlock(&c->lock); + if (ret) + dev_warn(&spi->dev, + "bring-up SPI err %d (codec unpowered/SPI0 — sysfs still up)\n", + ret); + + cs42l81_dev = c; + + ret = sysfs_create_groups(&spi->dev.kobj, cs42l81_groups); + if (ret) + dev_warn(&spi->dev, "sysfs groups failed: %d\n", ret); + + ret = devm_snd_soc_register_component(&spi->dev, &cs42l81_component, + &cs42l81_dai, 1); + if (ret) { + dev_err(&spi->dev, "snd_soc_register_component: %d\n", ret); + sysfs_remove_groups(&spi->dev.kobj, cs42l81_groups); + return ret; + } + + dev_info(&spi->dev, "CS42L81 SPI + ASoC DAI cs42l81-hifi\n"); + return 0; +} + +static void cs42l81_remove(struct spi_device *spi) +{ + if (cs42l81_dev == spi_get_drvdata(spi)) + cs42l81_dev = NULL; + sysfs_remove_groups(&spi->dev.kobj, cs42l81_groups); +} + +static const struct of_device_id cs42l81_of_match[] = { + { .compatible = "cirrus,cs42l81" }, + { .compatible = "apple,338s1146" }, + { } +}; +MODULE_DEVICE_TABLE(of, cs42l81_of_match); + +/* SPI core warns if OF compatibles have no matching id_table name. */ +static const struct spi_device_id cs42l81_ids[] = { + { "cs42l81", 0 }, + { "338s1146", 0 }, + { } +}; +MODULE_DEVICE_TABLE(spi, cs42l81_ids); + +static struct spi_driver cs42l81_driver = { + .driver = { + .name = "cs42l81-spi", + .of_match_table = cs42l81_of_match, + }, + .id_table = cs42l81_ids, + .probe = cs42l81_probe, + .remove = cs42l81_remove, +}; +module_spi_driver(cs42l81_driver); + +MODULE_DESCRIPTION("CS42L81 SPI codec + ASoC DAI (N31 RetailOS framing)"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/apple/nano7-audio.c b/sound/soc/apple/nano7-audio.c new file mode 100755 index 00000000000000..8a74bb78514f63 --- /dev/null +++ b/sound/soc/apple/nano7-audio.c @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * N31 ASoC machine — IIS0 CPU DAI + CS42L81 SPI codec + PL080 PCM. + * + * CS42 path has no snd_soc_dapm_route table — analog routing is explicit + * register writes in cs42l81_audio_on(). dai_fmt = I2S NB_NF CBS_CFS + * (SoC master, codec slave, 16-bit S16_LE). + */ +#include +#include +#include +#include + +SND_SOC_DAILINK_DEFS(playback, + DAILINK_COMP_ARRAY(COMP_CPU("s5l8740-i2s")), + DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "cs42l81-hifi")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("snd-soc-dummy"))); + +static struct snd_soc_dai_link nano7_dais[] = { + { + .name = "CS42L81", + .stream_name = "Playback", + SND_SOC_DAILINK_REG(playback), + .playback_only = 1, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS, + }, +}; + +static struct snd_soc_card nano7_card = { + .name = "nano7g-audio", + .owner = THIS_MODULE, + .dai_link = nano7_dais, + .num_links = ARRAY_SIZE(nano7_dais), +}; + +static int nano7_audio_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct device_node *cpu_np, *codec_np; + int ret; + + cpu_np = of_parse_phandle(dev->of_node, "apple,cpu", 0); + if (cpu_np) { + nano7_dais[0].cpus->of_node = cpu_np; + nano7_dais[0].cpus->dai_name = NULL; + nano7_dais[0].platforms->of_node = cpu_np; + nano7_dais[0].platforms->name = NULL; + } + + codec_np = of_parse_phandle(dev->of_node, "apple,codec", 0); + if (!codec_np) + codec_np = of_find_compatible_node(NULL, NULL, "cirrus,cs42l81"); + if (codec_np) { + nano7_dais[0].codecs->of_node = codec_np; + nano7_dais[0].codecs->name = NULL; + nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; + } else { + nano7_dais[0].codecs->name = "spi0.0"; + nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; + } + + nano7_card.dev = dev; + ret = devm_snd_soc_register_card(dev, &nano7_card); + if (ret) { + if (cpu_np) + of_node_put(cpu_np); + if (codec_np) + of_node_put(codec_np); + if (ret == -EPROBE_DEFER) + return ret; + dev_err(dev, "snd_soc_register_card failed: %d\n", ret); + return ret; + } + + dev_info(dev, "nano7g-audio: IIS0 + CS42L81 DAI (no dummy codec)\n"); + return 0; +} + +static const struct of_device_id nano7_audio_of_match[] = { + { .compatible = "apple,n31-audio" }, + { } +}; +MODULE_DEVICE_TABLE(of, nano7_audio_of_match); + +static struct platform_driver nano7_audio_driver = { + .probe = nano7_audio_probe, + .driver = { + .name = "nano7-audio", + .of_match_table = nano7_audio_of_match, + .pm = &snd_soc_pm_ops, + }, +}; +module_platform_driver(nano7_audio_driver); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("iPod nano 7G ASoC machine"); +MODULE_SOFTDEP("pre: cs42l81_spi s5l8740_i2s"); diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c new file mode 100755 index 00000000000000..c3f444a265b72d --- /dev/null +++ b/sound/soc/apple/s5l8740-i2s.c @@ -0,0 +1,961 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 IIS0 (I2S) platform DAI — N31 + * IIS0 @ 0x3CA00000, TX FIFO @ +0x10. Optional PL080 dmaengine PCM. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define S5L8740_I2S_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +#define S5L8740_I2S_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) +#define I2SCLKCON 0x00 +#define I2STXCON 0x04 +#define I2STXCOM 0x08 +#define I2STXFIFO 0x10 +#define I2SRXCON 0x30 +#define I2SRXCOM 0x34 +#define I2SSTATUS 0x3c +#define I2SCLKDIV 0x40 /* OSOS 4F716: *(base+64). Not Rockbox +0x24. */ +#define MCLK_ASSUME_HZ 12000000u +/* BCB60 a3!=0 a5!=24: 1048728|50331649 = 0x100098|0x03000001. Not 0x03100219. */ +#define I2STXCON_N31_16 0x03100099u +#define I2SRXCON_N31 0x1000u +/* OSOS enable ORs 0x100218 (bit20). Live: that bit holds STATUS at + * 0x24 (no external clock). Clearing it moves STATUS to 0x8020. + * Override via txcon= for bring-up; default stays OSOS. */ +static uint txcon = I2STXCON_N31_16; +module_param(txcon, uint, 0644); +MODULE_PARM_DESC(txcon, "I2STXCON (default 0x03100099 BCB60 16-bit)"); +/* + * D34C0 → 4F716(port, div). 48 kHz is 250, or 125 if 892A02C==6000. + * 0 = 12 MHz / rate (250 @ 48 kHz). + */ +static uint clkdiv; +module_param(clkdiv, uint, 0644); +MODULE_PARM_DESC(clkdiv, "I2SCLKDIV override; 0 = 12000000/rate"); +/* + * dma_tone FIFO beat width. Rockbox s5l8702 PCM is 16-bit (WIDTH_16). + * Default 2 = 16-bit stereo interleaved (Rockbox/OSOS BCB60 16-bit). + * 4 = packed LR 32-bit beats for pio_tone CPU path. + */ +static int tone_width = 2; +module_param(tone_width, int, 0644); +MODULE_PARM_DESC(tone_width, "dma_tone dst width bytes 2 or 4 (default 2)"); +/* + * OSOS B6620(port,0) does TXCOM |= 6 after PL080 is armed (peri 12). + * That is a DMA kick. CPU PIO has no DMA: Rockbox-family bit 3 must be + * set or the serializer never leaves STATUS 0x24. Default 0xC = PIO. + */ +#define I2STXCOM_DMA 0x6 +#define I2STXCOM_PIO 0xc +#define I2STXCOM_STOP 0x0 +#define CLKCON_PHYS 0x3c500000ul +#define GPIO_PHYS 0x3cf00000ul +#define GPIOCMD_PHYS 0x3cf001e0ul + +/* + * RetailOS user volume is 0..256 (VolumeScalar, 256 = unity). CS42 + * analog 0x527 is only mute/unmute; the integer is a PCM Q8 gain. + */ +#define S5L8740_USER_VOL_MAX 256 +static atomic_t s5l8740_user_vol_q8 = ATOMIC_INIT(S5L8740_USER_VOL_MAX); + +void s5l8740_set_user_vol_q8(unsigned int vol); +unsigned int s5l8740_get_user_vol_q8(void); + +void s5l8740_set_user_vol_q8(unsigned int vol) +{ + if (vol > S5L8740_USER_VOL_MAX) + vol = S5L8740_USER_VOL_MAX; + atomic_set(&s5l8740_user_vol_q8, vol); +} +EXPORT_SYMBOL_GPL(s5l8740_set_user_vol_q8); + +unsigned int s5l8740_get_user_vol_q8(void) +{ + return atomic_read(&s5l8740_user_vol_q8); +} +EXPORT_SYMBOL_GPL(s5l8740_get_user_vol_q8); + +static s16 s5l8740_scale_s16(s16 s) +{ + unsigned int q8 = atomic_read(&s5l8740_user_vol_q8); + + if (q8 >= S5L8740_USER_VOL_MAX) + return s; + return (s16)(((int)s * (int)q8) / S5L8740_USER_VOL_MAX); +} + +static u32 s5l8740_scale_lr(u32 sample) +{ + unsigned int q8 = atomic_read(&s5l8740_user_vol_q8); + s16 l, r; + + if (q8 >= S5L8740_USER_VOL_MAX) + return sample; + l = s5l8740_scale_s16((s16)sample); + r = s5l8740_scale_s16((s16)(sample >> 16)); + return ((u32)(u16)r << 16) | (u16)l; +} + +static int use_pio; +module_param(use_pio, int, 0644); +MODULE_PARM_DESC(use_pio, "1 = CPU FIFO PCM; 0 = OSOS PL080 M2P peri 12 (default)"); + +static int txcom_pio = I2STXCOM_PIO; +module_param(txcom_pio, int, 0644); +MODULE_PARM_DESC(txcom_pio, "TXCOM when use_pio=1 (default 0xC; OSOS DMA is 0x6)"); + +/* BCB60 sets DIR. Live pad_oe=0: GPIO 7/20 stop, GPIO 6 still + * toggles — BCLK/LRCK are SoC-driven, not codec-master. */ +static int pad_oe = 1; +module_param(pad_oe, int, 0644); +MODULE_PARM_DESC(pad_oe, "1 = OSOS DIR out (default); 0 = mode 3, DIR in"); + +struct s5l8740_i2s { + void __iomem *base; + void __iomem *clkcon; + void __iomem *gpio; + void __iomem *gpiocmd; + struct device *dev; + struct clk_bulk_data *clks; + int num_clks; + bool has_dma; + struct dma_chan *tx_chan; /* cached — avoid dma:tx symlink churn */ + struct mutex dma_lock; + struct snd_dmaengine_dai_dma_data play_dma; + struct snd_pcm_substream *ss; + struct task_struct *kthread; + bool pio_run; + unsigned int pio_hw_ptr; + unsigned int rate; +}; + +/* + * SEC sub_2034 leftovers. OSOS 983430 never programs clock 9; + * it does program clocks 6/20 into +0x1C after SEC. If U-Boot + * zeroed the pair, IIS has no parent. Do not write +00/+04/+44. + */ +#define SEC_CLKCON_18 0x20012001u +#define SEC_CLKCON_1C 0x10122003u + +/* sub_41CBD8(9,1): CLKCON+0x0C bit 15 clear = IIS0 CG16 on. */ +static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) +{ + u32 v, r18, r1c; + + if (!i2s->clkcon) + return; + r18 = readl(i2s->clkcon + 0x18); + r1c = readl(i2s->clkcon + 0x1c); + if (!r18) + writel(SEC_CLKCON_18, i2s->clkcon + 0x18); + if (!r1c) + writel(SEC_CLKCON_1C, i2s->clkcon + 0x1c); + v = readl(i2s->clkcon + 0x0c); + if (v & 0x8000u) + writel(v & ~0x8000u, i2s->clkcon + 0x0c); +} + +/* sub_43D38C(7,3) and (20,3) — IIS0 pads only (do not touch 0x0A061010 / GPIO86). */ +static void s5l8740_i2s_pads(struct s5l8740_i2s *i2s) +{ + static const u8 gpios[] = { 7, 20 }; + unsigned int i; + + if (!i2s->gpio || !i2s->gpiocmd) + return; + for (i = 0; i < ARRAY_SIZE(gpios); i++) { + unsigned int gpio = gpios[i]; + unsigned int bank = gpio >> 3; + unsigned int pin = gpio & 7; + void __iomem *b = i2s->gpio + 32 * bank; + u32 dir = readl(b + 0x14); + + if (pad_oe) + writel(dir | BIT(pin), b + 0x14); + else + writel(dir & ~BIT(pin), b + 0x14); + writel((bank << 16) | (pin << 8) | 3, i2s->gpiocmd); + } +} + +static void s5l8740_i2s_c09ac_start(struct s5l8740_i2s *i2s); +static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate); +static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma); + +static int fifo_wait_loops = 50; +module_param(fifo_wait_loops, int, 0644); +MODULE_PARM_DESC(fifo_wait_loops, "max polls for IIS TX FIFO ready before write"); + +static int s5l8740_i2s_codec_prepare(void) +{ + int (*prep)(void) = __symbol_get("cs42l81_play_prepare"); + int ret = 0; + + if (prep) { + ret = prep(); + __symbol_put("cs42l81_play_prepare"); + } + return ret; +} + +static int s5l8740_i2s_asp_lock(void) +{ + int (*asp)(void) = __symbol_get("cs42l81_post_iis_start"); + int ret = -ENOENT; + + if (asp) { + msleep(20); + ret = asp(); + __symbol_put("cs42l81_post_iis_start"); + } + return ret; +} + +static void s5l8740_i2s_fifo_write(struct s5l8740_i2s *i2s, s16 s) +{ + unsigned int n; + u32 status; + + for (n = 0; n < fifo_wait_loops; n++) { + status = readl(i2s->base + I2SSTATUS); + if (!(status & 0x20)) + break; + cpu_relax(); + } + writel((u32)(u16)s | ((u32)(u16)s << 16), i2s->base + I2STXFIFO); +} + +static int s5l8740_i2s_play_start(struct s5l8740_i2s *i2s, bool dma) +{ + int ret; + + ret = s5l8740_i2s_codec_prepare(); + if (ret && i2s->dev) + dev_warn(i2s->dev, "codec prepare: %d\n", ret); + s5l8740_i2s_program(i2s, 48000); + s5l8740_i2s_tx_kick(i2s, dma); + ret = s5l8740_i2s_asp_lock(); + if (i2s->dev) + dev_info(i2s->dev, "play_start dma=%d asp=%d status=0x%x txcom=0x%x\n", + dma, ret, readl(i2s->base + I2SSTATUS), + readl(i2s->base + I2STXCOM)); + return ret; +} + +/* + * 345D70 is JUMPOUT 0x22000350 = bootloader sub_350 (SCTLR C-bit). + * Play 414FAE only starts — it does not C09AC-stop first. + */ +static void s5l8740_i2s_c09ac_start(struct s5l8740_i2s *i2s) +{ + writel(1, i2s->base + I2SCLKCON); +} + +/* 26DDDE: 41CBD8(9,1), 5705DC RX, 414FAE (C09AC + BCB60), D34C0 CLKDIV. */ +static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate) +{ + u32 div = clkdiv ? clkdiv : MCLK_ASSUME_HZ / (rate ? rate : 48000); + u32 rxcom; + + if (div < 1) + div = 1; + s5l8740_i2s_ungate(i2s); + s5l8740_i2s_c09ac_start(i2s); + s5l8740_i2s_pads(i2s); + writel(txcon, i2s->base + I2STXCON); + writel(I2SRXCON_N31, i2s->base + I2SRXCON); + rxcom = readl(i2s->base + I2SRXCOM); + writel(rxcom & ~4u, i2s->base + I2SRXCOM); + writel(div, i2s->base + I2SCLKDIV); + /* C095E/BB9F8 is not on the 26DDDE play path. Bit15 looks W1C. */ + i2s->rate = rate ? rate : 48000; +} + +/* + * OSOS B6620(port,0): TXCOM |= 6 after PL080 armed. Glass also needs bit 3 + * (PIO path 0xC) or STATUS stays 0x24 / jack silent. Set bit 3 before DMA. + */ +static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma) +{ + u32 txcom; + + if (!i2s || !i2s->base) + return; + if (dma) { + txcom = readl(i2s->base + I2STXCOM); + writel(txcom | I2STXCOM_PIO, i2s->base + I2STXCOM); + writel(txcom | I2STXCOM_PIO | I2STXCOM_DMA, + i2s->base + I2STXCOM); + } else { + writel(txcom_pio, i2s->base + I2STXCOM); + } +} + +static int s5l8740_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); + unsigned int rate = params_rate(params); + u32 div; + + if (!i2s || !i2s->base) + return -ENODEV; + s5l8740_i2s_program(i2s, rate); + div = MCLK_ASSUME_HZ / (i2s->rate ? i2s->rate : 48000); + dev_info(dai->dev, "IIS hw_params rate=%u clkdiv=%u dma=%d pio=%d txcom=0x%x\n", + rate, div, i2s->has_dma, use_pio, + use_pio ? txcom_pio : I2STXCOM_DMA); + return 0; +} + +static int s5l8740_i2s_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); + + if (!i2s || !i2s->base) + return -ENODEV; + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + s5l8740_i2s_tx_kick(i2s, !use_pio); + i2s->pio_run = use_pio; + return 0; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + i2s->pio_run = false; + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + return 0; + default: + return -EINVAL; + } +} + +static int s5l8740_i2s_dai_probe(struct snd_soc_dai *dai) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); + + if (i2s->has_dma) + snd_soc_dai_init_dma_data(dai, &i2s->play_dma, NULL); + return 0; +} + +static const struct snd_soc_dai_ops s5l8740_i2s_dai_ops = { + .probe = s5l8740_i2s_dai_probe, + .hw_params = s5l8740_i2s_hw_params, + .trigger = s5l8740_i2s_trigger, +}; + +static struct snd_soc_dai_driver s5l8740_i2s_dai = { + .name = "s5l8740-i2s", + .playback = { + .stream_name = "I2S Playback", + .channels_min = 1, + .channels_max = 2, + .rates = S5L8740_I2S_RATES, + .formats = S5L8740_I2S_FORMATS, + }, + .ops = &s5l8740_i2s_dai_ops, +}; + +static const struct snd_pcm_hardware s5l8740_pio_hw = { + .info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER, + .formats = S5L8740_I2S_FORMATS, + .rates = S5L8740_I2S_RATES, + .rate_min = 44100, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = 65536, + .period_bytes_min = 256, + .period_bytes_max = 8192, + .periods_min = 2, + .periods_max = 16, +}; + +static int s5l8740_pio_thread(void *data) +{ + struct s5l8740_i2s *i2s = data; + + while (!kthread_should_stop()) { + struct snd_pcm_substream *ss = i2s->ss; + struct snd_pcm_runtime *rt; + unsigned int pos, rate, burst, i; + u32 sample; + + if (!READ_ONCE(i2s->pio_run) || !ss) { + usleep_range(2000, 4000); + continue; + } + rt = ss->runtime; + if (!rt || !rt->dma_area) { + usleep_range(2000, 4000); + continue; + } + /* + * Pace with udelay — usleep_range(167us) rounds to a jiffy + * (~10 ms) and a 3s tone hung for a minute. Burst ~4 ms of + * realtime writes, then cond_resched so RNDIS still runs. + */ + rate = i2s->rate ? i2s->rate : 48000; + burst = rate / 250; /* ~4 ms */ + if (burst < 16) + burst = 16; + pos = i2s->pio_hw_ptr; + for (i = 0; i < burst && READ_ONCE(i2s->pio_run); i++) { + if (pos >= rt->buffer_size) + pos = 0; + sample = s5l8740_scale_lr(*(u32 *)(rt->dma_area + + frames_to_bytes(rt, pos))); + writel(sample, i2s->base + I2STXFIFO); + pos++; + if (pos >= rt->buffer_size) + pos = 0; + if (rt->period_size && (pos % rt->period_size) == 0) + snd_pcm_period_elapsed(ss); + udelay(1000000 / rate); + } + i2s->pio_hw_ptr = pos; + cond_resched(); + } + return 0; +} + +static int s5l8740_pio_open(struct snd_soc_component *comp, + struct snd_pcm_substream *ss) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(comp->dev); + + if (!use_pio) + return 0; + snd_soc_set_runtime_hwparams(ss, &s5l8740_pio_hw); + i2s->ss = ss; + i2s->pio_hw_ptr = 0; + if (!i2s->kthread) { + i2s->kthread = kthread_run(s5l8740_pio_thread, i2s, + "n31-i2s-pio"); + if (IS_ERR(i2s->kthread)) { + int ret = PTR_ERR(i2s->kthread); + + i2s->kthread = NULL; + return ret; + } + } + return 0; +} + +static int s5l8740_pio_close(struct snd_soc_component *comp, + struct snd_pcm_substream *ss) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(comp->dev); + + i2s->pio_run = false; + i2s->ss = NULL; + return 0; +} + +static snd_pcm_uframes_t s5l8740_pio_pointer(struct snd_soc_component *comp, + struct snd_pcm_substream *ss) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(comp->dev); + + return i2s->pio_hw_ptr; +} + +static int s5l8740_pio_pcm_new(struct snd_soc_component *comp, + struct snd_soc_pcm_runtime *rtd) +{ + if (!use_pio) + return 0; + return snd_pcm_set_managed_buffer_all(rtd->pcm, SNDRV_DMA_TYPE_VMALLOC, + NULL, 64 * 1024, 64 * 1024); +} + +static const struct snd_soc_component_driver s5l8740_i2s_component = { + .name = "s5l8740-i2s", + .legacy_dai_naming = 1, + .open = s5l8740_pio_open, + .close = s5l8740_pio_close, + .pointer = s5l8740_pio_pointer, + .pcm_construct = s5l8740_pio_pcm_new, +}; + +/* DMA path: dmaengine_pcm owns PCM ops. Do not install pointer(). */ +static const struct snd_soc_component_driver s5l8740_i2s_dai_component = { + .name = "s5l8740-i2s", + .legacy_dai_naming = 1, +}; + +static ssize_t regs_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + static const u32 offs[] = { + I2SCLKCON, I2STXCON, I2STXCOM, I2STXFIFO, + I2SRXCON, I2SRXCOM, I2SSTATUS, I2SCLKDIV, + }; + int i, n = 0; + + if (!i2s || !i2s->base) + return -ENODEV; + for (i = 0; i < ARRAY_SIZE(offs); i++) + n += scnprintf(buf + n, PAGE_SIZE - n, "+0x%02x: 0x%08x\n", + offs[i], readl(i2s->base + offs[i])); + if (i2s->clkcon) { + static const u32 clk_offs[] = { + 0x00, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, + 0x44, 0x48, 0x4c, 0x58, 0x68, 0x6c, + }; + int c; + + for (c = 0; c < ARRAY_SIZE(clk_offs); c++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "clk+0x%02x: 0x%08x\n", clk_offs[c], + readl(i2s->clkcon + clk_offs[c])); + } + if (i2s->gpio) { + u32 p0 = readl(i2s->gpio); + u32 d0 = readl(i2s->gpio + 0x04); + u32 p2 = readl(i2s->gpio + 64); + u32 d2 = readl(i2s->gpio + 68); + + n += scnprintf(buf + n, PAGE_SIZE - n, + "pcon0=%08x din0=%08x pcon2=%08x din2=%08x\n", + p0, d0, p2, d2); + } + return n; +} +static DEVICE_ATTR_RO(regs); + +static ssize_t volume_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int vol; + + if (kstrtouint(buf, 0, &vol) || vol > S5L8740_USER_VOL_MAX) + return -EINVAL; + s5l8740_set_user_vol_q8(vol); + return count; +} + +static ssize_t volume_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "%u/%u (RetailOS Q8, 256=unity)\n", + s5l8740_get_user_vol_q8(), S5L8740_USER_VOL_MAX); +} +static DEVICE_ATTR_RW(volume); + +/* 1 kHz sine @ 48 kHz, 48 samples/period, peak 32767. No FP in the loop. */ +static const s16 sine_1khz_48k[48] = { + 0, 4277, 8481, 12540, 16384, 19948, 23170, 25997, + 28378, 30274, 31651, 32487, 32767, 32487, 31651, 30274, + 28378, 25997, 23170, 19948, 16384, 12540, 8481, 4277, + 0, -4277, -8481, -12540, -16384, -19948, -23170, -25997, + -28378, -30274, -31651, -32487, -32767, -32487, -31651, -30274, + -28378, -25997, -23170, -19948, -16384, -12540, -8481, -4277, +}; + +/* CPU-paced FIFO write. TXCOM 6 = OSOS B6620 TX start. */ +static ssize_t pio_tone_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + unsigned int frames, i; + s16 s; + + if (!i2s || !i2s->base) + return -ENODEV; + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + + s5l8740_i2s_play_start(i2s, false); + + frames = 48000 * 2; /* ~2 s */ + for (i = 0; i < frames; i++) { + s = s5l8740_scale_s16(sine_1khz_48k[i % 48]); + s5l8740_i2s_fifo_write(i2s, s); + udelay(20); + } + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + dev_info(dev, "pio_tone 2s done status=0x%08x\n", + readl(i2s->base + I2SSTATUS)); + return count; +} +static DEVICE_ATTR_WO(pio_tone); + +struct dma_chan *s5l_pl080_request_slave(struct device *consumer, + unsigned int idx); + +static struct dma_chan *s5l8740_i2s_tx_get(struct s5l8740_i2s *i2s, + struct device *dev) +{ + if (i2s->tx_chan) + return i2s->tx_chan; + i2s->tx_chan = s5l_pl080_request_slave(dev, 0); + return i2s->tx_chan; +} + +static void s5l8740_i2s_tx_put(struct s5l8740_i2s *i2s) +{ + if (!i2s || !i2s->tx_chan) + return; + dma_release_channel(i2s->tx_chan); + i2s->tx_chan = NULL; +} + +/* One-shot OSOS path: PL080 M2P -> +0x10, then TXCOM=0xE. */ +static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + struct dma_chan *chan; + struct dma_async_tx_descriptor *desc; + struct dma_slave_config cfg = { }; + dma_cookie_t cookie; + dma_addr_t dma; + s16 *tone; + size_t bytes = 48000 * 2 * 2 * 2; /* 2 s stereo S16 */ + unsigned int i; + s16 s; + int ret; + + if (!i2s || !i2s->base) + return -ENODEV; + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + + mutex_lock(&i2s->dma_lock); + chan = s5l8740_i2s_tx_get(i2s, dev); + if (IS_ERR(chan)) { + ret = PTR_ERR(chan); + dev_err(dev, "dma_tone request tx: %d\n", ret); + mutex_unlock(&i2s->dma_lock); + return ret; + } + + tone = dma_alloc_coherent(dev, bytes, &dma, GFP_KERNEL); + if (!tone) { + ret = -ENOMEM; + goto out_unlock; + } + for (i = 0; i < bytes / 4; i++) { + s = s5l8740_scale_s16(sine_1khz_48k[i % 48]); + tone[i * 2] = s; + tone[i * 2 + 1] = s; + } + dma_sync_single_for_device(dev, dma, bytes, DMA_TO_DEVICE); + + cfg.direction = DMA_MEM_TO_DEV; + cfg.dst_addr = i2s->play_dma.addr; + if (tone_width == 2) + cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; + else + cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + cfg.dst_maxburst = 1; + ret = dmaengine_slave_config(chan, &cfg); + if (ret) { + dev_err(dev, "dma_tone slave_config: %d\n", ret); + goto out_chan; + } + + s5l8740_i2s_codec_prepare(); + s5l8740_i2s_program(i2s, 48000); + desc = dmaengine_prep_slave_single(chan, dma, bytes, DMA_MEM_TO_DEV, + DMA_PREP_INTERRUPT); + if (!desc) { + dev_err(dev, "dma_tone prep_slave_single failed\n"); + ret = -EIO; + goto out_chan; + } + cookie = dmaengine_submit(desc); + if (dma_submit_error(cookie)) { + ret = cookie; + goto out_chan; + } + dma_async_issue_pending(chan); + s5l8740_i2s_tx_kick(i2s, true); + { + int asp = s5l8740_i2s_asp_lock(); + + dev_info(dev, "dma_tone asp_lock=%d\n", asp); + } + { + void __iomem *pl = ioremap(0x38200000ul, 0x200); + unsigned int t, i; + + if (i2s->gpio) { + u32 xor[8] = { }, last[8] = { }, pcon[8] = { }; + unsigned int b; + + for (b = 0; b < 8; b++) { + pcon[b] = readl(i2s->gpio + 32 * b); + last[b] = readl(i2s->gpio + 32 * b + 4); + } + for (i = 0; i < 20000; i++) { + for (b = 0; b < 8; b++) { + u32 d = readl(i2s->gpio + 32 * b + 4); + + xor[b] |= d ^ last[b]; + last[b] = d; + } + } + dev_info(dev, + "dma_tone pads xor %02x %02x %02x %02x %02x %02x %02x %02x\n", + xor[0], xor[1], xor[2], xor[3], + xor[4], xor[5], xor[6], xor[7]); + dev_info(dev, + "dma_tone pcon %08x %08x %08x %08x\n", + pcon[0], pcon[1], pcon[2], pcon[3]); + } + + if (pl) { + for (t = 0; t < 3; t++) { + u32 en = readl(pl + 0x1c); + u32 st = readl(i2s->base + I2SSTATUS); + int ch; + + dev_info(dev, + "dma_tone t=%ums status=0x%x txcom=0x%x en=0x%x rawtc=0x%x\n", + t * 100, st, + readl(i2s->base + I2STXCOM), en, + readl(pl + 0x14)); + for (ch = 0; ch < 8; ch++) { + u32 dst = readl(pl + 0x104 + ch * 0x20); + u32 src = readl(pl + 0x100 + ch * 0x20); + u32 cfg = readl(pl + 0x110 + ch * 0x20); + u32 c2 = readl(pl + 0x114 + ch * 0x20); + + if (!(en & BIT(ch)) && dst != 0x3ca00010) + continue; + dev_info(dev, + " ch%u src=0x%x dst=0x%x cfg=0x%x c2=0x%x\n", + ch, src, dst, cfg, c2); + } + if (t == 0) + msleep(100); + else if (t == 1) + msleep(1900); + } + iounmap(pl); + } + } + dev_info(dev, "dma_tone 1kHz 2s status=0x%x txcom=0x%x\n", + readl(i2s->base + I2SSTATUS), + readl(i2s->base + I2STXCOM)); + dmaengine_terminate_sync(chan); + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + ret = 0; +out_chan: + dma_free_coherent(dev, bytes, tone, dma); +out_unlock: + mutex_unlock(&i2s->dma_lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(dma_tone); + +/* Sample GPIO DIN xor across banks 0-7. Use after clk_run or at idle. */ +static ssize_t pad_scan_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + u32 xor[8] = { }, pcon[8] = { }, last[8] = { }; + unsigned int b, i, n = 0; + + if (!i2s || !i2s->gpio) + return -ENODEV; + for (b = 0; b < 8; b++) { + pcon[b] = readl(i2s->gpio + 32 * b); + last[b] = readl(i2s->gpio + 32 * b + 4); + } + for (i = 0; i < 40000; i++) { + for (b = 0; b < 8; b++) { + u32 d = readl(i2s->gpio + 32 * b + 4); + + xor[b] |= d ^ last[b]; + last[b] = d; + } + } + n += scnprintf(buf + n, PAGE_SIZE - n, + "clkcon=0x%x txcon=0x%x txcom=0x%x status=0x%x\n", + readl(i2s->base + I2SCLKCON), + readl(i2s->base + I2STXCON), + readl(i2s->base + I2STXCOM), + readl(i2s->base + I2SSTATUS)); + for (b = 0; b < 8; b++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "b%u pcon=%08x xor=%02x\n", b, pcon[b], xor[b]); + return n; +} +static DEVICE_ATTR_RO(pad_scan); + +/* Program IIS and leave TXCOM running so BCLK/LRCK (and MCLK if any) stay up. */ +static ssize_t clk_run_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + unsigned int v; + + if (!i2s || !i2s->base) + return -ENODEV; + if (kstrtouint(buf, 0, &v)) + return -EINVAL; + if (v) { + s5l8740_i2s_program(i2s, 48000); + s5l8740_i2s_tx_kick(i2s, false); + } else { + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + } + dev_info(dev, "clk_run=%u status=0x%x txcom=0x%x\n", + v, readl(i2s->base + I2SSTATUS), + readl(i2s->base + I2STXCOM)); + return count; +} +static DEVICE_ATTR_WO(clk_run); + +static int s5l8740_i2s_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_i2s *i2s; + struct resource *res; + int ret; + + i2s = devm_kzalloc(dev, sizeof(*i2s), GFP_KERNEL); + if (!i2s) + return -ENOMEM; + i2s->dev = dev; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + i2s->base = devm_ioremap_resource(dev, res); + if (IS_ERR(i2s->base)) + return PTR_ERR(i2s->base); + i2s->clkcon = devm_ioremap(dev, CLKCON_PHYS, 0x80); + i2s->gpio = devm_ioremap(dev, GPIO_PHYS, 0x200); + i2s->gpiocmd = devm_ioremap(dev, GPIOCMD_PHYS, 4); + + ret = devm_clk_bulk_get_all(dev, &i2s->clks); + if (ret > 0) { + i2s->num_clks = ret; + ret = clk_bulk_prepare_enable(i2s->num_clks, i2s->clks); + if (ret) + dev_warn(dev, "clk_bulk_prepare_enable: %d (CLKCON ungate-all still on)\n", + ret); + } + + if (res) { + i2s->play_dma.addr = res->start + I2STXFIFO; + i2s->play_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; + i2s->play_dma.maxburst = 1; + } + + platform_set_drvdata(pdev, i2s); + dev_set_drvdata(dev, i2s); + mutex_init(&i2s->dma_lock); + + if (!use_pio && of_property_present(dev->of_node, "dmas")) { + ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); + if (ret) { + dev_warn(dev, "dmaengine_pcm: %d — falling back to PIO\n", + ret); + use_pio = 1; + } else { + i2s->has_dma = true; + } + } else if (!use_pio) { + dev_warn(dev, "no dmas in DT — PIO\n"); + use_pio = 1; + } + + ret = devm_snd_soc_register_component(dev, + use_pio ? &s5l8740_i2s_component : + &s5l8740_i2s_dai_component, + &s5l8740_i2s_dai, 1); + if (ret) + return ret; + + ret = device_create_file(dev, &dev_attr_regs); + if (ret) + dev_warn(dev, "regs sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_volume); + if (ret) + dev_warn(dev, "volume sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_pio_tone); + if (ret) + dev_warn(dev, "pio_tone sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_dma_tone); + if (ret) + dev_warn(dev, "dma_tone sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_pad_scan); + if (ret) + dev_warn(dev, "pad_scan sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_clk_run); + if (ret) + dev_warn(dev, "clk_run sysfs: %d\n", ret); + + dev_info(dev, "S5L8740 IIS0 @%pR dma=%s pio=%d\n", + res, i2s->has_dma ? "yes" : "no", use_pio); + return 0; +} + +static void s5l8740_i2s_remove(struct platform_device *pdev) +{ + struct s5l8740_i2s *i2s = platform_get_drvdata(pdev); + + device_remove_file(&pdev->dev, &dev_attr_regs); + device_remove_file(&pdev->dev, &dev_attr_volume); + device_remove_file(&pdev->dev, &dev_attr_pio_tone); + device_remove_file(&pdev->dev, &dev_attr_dma_tone); + device_remove_file(&pdev->dev, &dev_attr_pad_scan); + device_remove_file(&pdev->dev, &dev_attr_clk_run); + if (i2s && i2s->kthread) { + i2s->pio_run = false; + kthread_stop(i2s->kthread); + i2s->kthread = NULL; + } + s5l8740_i2s_tx_put(i2s); + if (i2s && i2s->num_clks) + clk_bulk_disable_unprepare(i2s->num_clks, i2s->clks); +} + +static const struct of_device_id s5l8740_i2s_of_match[] = { + { .compatible = "apple,s5l8740-i2s" }, + { .compatible = "samsung,s5l8740-i2s" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_i2s_of_match); + +static struct platform_driver s5l8740_i2s_driver = { + .probe = s5l8740_i2s_probe, + .remove = s5l8740_i2s_remove, + .driver = { + .name = "s5l8740-i2s", + .of_match_table = s5l8740_i2s_of_match, + }, +}; +module_platform_driver(s5l8740_i2s_driver); + +MODULE_DESCRIPTION("S5L8740 IIS0 DAI + optional PL080 PCM (N31)"); +MODULE_LICENSE("GPL"); From 6403b68d117d536161a55a869e56bea42230e899 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 24 Aug 2026 05:04:17 +0300 Subject: [PATCH 12/31] s5l8702-aes: Fix CBC Peripheral does not keep chaining state between operations. IV needs to be set explicitly before CMD_START. --- drivers/crypto/s5l8702-aes.c | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/drivers/crypto/s5l8702-aes.c b/drivers/crypto/s5l8702-aes.c index 23463966c19ff3..104ab28700b32c 100644 --- a/drivers/crypto/s5l8702-aes.c +++ b/drivers/crypto/s5l8702-aes.c @@ -270,7 +270,7 @@ static void s5l8702_aes_hw_exit(struct s5l8702_aes_dev *aes_dev) clk_disable_unprepare(aes_dev->clk); } -static int s5l8702_aes_hw_init(struct s5l8702_aes_ctx *ctx, const u8 *iv, bool encrypt) +static int s5l8702_aes_hw_init(struct s5l8702_aes_ctx *ctx, bool encrypt) { struct s5l8702_aes_dev *aes_dev = ctx->aes_dev; struct device *dev = aes_dev->dev; @@ -326,16 +326,10 @@ static int s5l8702_aes_hw_init(struct s5l8702_aes_ctx *ctx, const u8 *iv, bool e cfg |= S5L8702_AES_CFG_PAUSE; // chaining mode - if (ctx->cbc) { - // CBC - cfg |= BIT(3); - - // IV - s5l8702_aes_write_iv(aes_dev, iv); - } else { - // ECB - cfg &= ~BIT(3); - } + if (ctx->cbc) + cfg |= BIT(3); // CBC + else + cfg &= ~BIT(3); // ECB // key size cfg &= ~S5L8702_AES_CFG_KEYSIZE; @@ -398,6 +392,17 @@ static int s5l8702_aes_hw_crypt(struct s5l8702_aes_dev *aes_dev, dma_addr_t src, return ret; } +static void s5l8702_aes_update_walk_iv(struct skcipher_walk *walk, unsigned int nbytes, bool encrypt) +{ + const u8 *src = walk->src.virt.addr; + const u8 *dst = walk->dst.virt.addr; + + if (encrypt) + memcpy(walk->iv, dst + nbytes - AES_BLOCK_SIZE, AES_BLOCK_SIZE); + else + memcpy(walk->iv, src + nbytes - AES_BLOCK_SIZE, AES_BLOCK_SIZE); +} + static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) { struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req); @@ -416,13 +421,17 @@ static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) mutex_lock(&aes_dev->lock); - ret = s5l8702_aes_hw_init(ctx, walk.iv, encrypt); + ret = s5l8702_aes_hw_init(ctx, encrypt); if (ret) goto out_unlock; while (walk.nbytes) { dma_addr_t src, dst; + // set IV for the current operation if needed + if (ctx->cbc) + s5l8702_aes_write_iv(aes_dev, walk.iv); + // map addresses src = dma_map_single(dev, walk.src.virt.addr, walk.nbytes, DMA_TO_DEVICE); if (dma_mapping_error(dev, src)) { @@ -446,6 +455,10 @@ static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) if (ret) break; + // prepare IV for the next operation if needed + if (ctx->cbc) + s5l8702_aes_update_walk_iv(&walk, walk.nbytes, encrypt); + // update remaining bytes and process next chunk ret = skcipher_walk_done(&walk, 0); if (ret) From 9046ce0106f263aca16280538cce675000d70ff5 Mon Sep 17 00:00:00 2001 From: Vencislav Atanasov Date: Mon, 24 Aug 2026 06:13:38 +0300 Subject: [PATCH 13/31] s5l8702-aes: Set correct key size for UID and GID key types --- drivers/crypto/s5l8702-aes.c | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/drivers/crypto/s5l8702-aes.c b/drivers/crypto/s5l8702-aes.c index 104ab28700b32c..75a411b5e5ad8b 100644 --- a/drivers/crypto/s5l8702-aes.c +++ b/drivers/crypto/s5l8702-aes.c @@ -334,21 +334,24 @@ static int s5l8702_aes_hw_init(struct s5l8702_aes_ctx *ctx, bool encrypt) // key size cfg &= ~S5L8702_AES_CFG_KEYSIZE; - switch (ctx->keylen) { - case AES_KEYSIZE_128: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_128); - break; - case AES_KEYSIZE_192: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_192); - break; - case AES_KEYSIZE_256: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_256); - break; - default: - dev_err(dev, "Invalid key length: %u\n", ctx->keylen); - ret = -EINVAL; - goto err_hw; + if (hw_key_type == S5L8702_AES_KEY_TYPE_USER_DEFINE) { + switch (ctx->keylen) { + case AES_KEYSIZE_128: + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_128); + break; + case AES_KEYSIZE_192: + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_192); + break; + case AES_KEYSIZE_256: + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_256); + break; + default: + dev_err(dev, "Invalid key length: %u\n", ctx->keylen); + ret = -EINVAL; + goto err_hw; + } } + // else i.e. for key types UID and GID, key size is set to 0 - nothing to do s5l8702_aes_writel(aes_dev, S5L8702_AES_CFG, cfg); From f99c6543e25d5e960fe4386d595da2c2d8cc2611 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Tue, 25 Aug 2026 11:39:31 -0230 Subject: [PATCH 14/31] N31: require DT cpu/codec phandles for nano7-audio card Fail probe if apple,cpu or CS42 node is missing instead of falling back to string names. Codec DAI remains cs42l81-hifi (no COMP_DUMMY). --- sound/soc/apple/nano7-audio.c | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/sound/soc/apple/nano7-audio.c b/sound/soc/apple/nano7-audio.c index 8a74bb78514f63..f59057a6338ded 100755 --- a/sound/soc/apple/nano7-audio.c +++ b/sound/soc/apple/nano7-audio.c @@ -41,32 +41,33 @@ static int nano7_audio_probe(struct platform_device *pdev) int ret; cpu_np = of_parse_phandle(dev->of_node, "apple,cpu", 0); - if (cpu_np) { - nano7_dais[0].cpus->of_node = cpu_np; - nano7_dais[0].cpus->dai_name = NULL; - nano7_dais[0].platforms->of_node = cpu_np; - nano7_dais[0].platforms->name = NULL; + if (!cpu_np) { + dev_err(dev, "missing apple,cpu phandle (IIS0)\n"); + return -EINVAL; } + nano7_dais[0].cpus->of_node = cpu_np; + nano7_dais[0].cpus->dai_name = NULL; + /* dmaengine PCM lives on the IIS platform device */ + nano7_dais[0].platforms->of_node = cpu_np; + nano7_dais[0].platforms->name = NULL; codec_np = of_parse_phandle(dev->of_node, "apple,codec", 0); if (!codec_np) codec_np = of_find_compatible_node(NULL, NULL, "cirrus,cs42l81"); - if (codec_np) { - nano7_dais[0].codecs->of_node = codec_np; - nano7_dais[0].codecs->name = NULL; - nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; - } else { - nano7_dais[0].codecs->name = "spi0.0"; - nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; + if (!codec_np) { + dev_err(dev, "missing apple,codec / cirrus,cs42l81\n"); + of_node_put(cpu_np); + return -EINVAL; } + nano7_dais[0].codecs->of_node = codec_np; + nano7_dais[0].codecs->name = NULL; + nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; nano7_card.dev = dev; ret = devm_snd_soc_register_card(dev, &nano7_card); if (ret) { - if (cpu_np) - of_node_put(cpu_np); - if (codec_np) - of_node_put(codec_np); + of_node_put(cpu_np); + of_node_put(codec_np); if (ret == -EPROBE_DEFER) return ret; dev_err(dev, "snd_soc_register_card failed: %d\n", ret); From 1d8dca99bf4df179d33cd4e9674a23bcae2762e3 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Tue, 25 Aug 2026 11:41:08 -0230 Subject: [PATCH 15/31] =?UTF-8?q?N31:=20sync=20ASoC=20Kconfig=20=E2=80=94?= =?UTF-8?q?=20select=20CS42=20DAI,=20drop=20dummy=20wording?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match tools/linux-n31/Kconfig: SND_SOC_APPLE_NANO7 selects SND_SOC_APPLE_CS42L81_SPI; CS42 depends on SPI && SND_SOC. --- sound/soc/apple/Kconfig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) mode change 100644 => 100755 sound/soc/apple/Kconfig diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig old mode 100644 new mode 100755 index 4f2f7a4e24a0d1..b380793f76f2c2 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -11,8 +11,9 @@ config SND_SOC_APPLE_NANO7 tristate "iPod nano 7G audio machine" depends on SND_SOC select SND_SOC_APPLE_S5L8740_I2S + select SND_SOC_APPLE_CS42L81_SPI help - Registers ASoC card: S5L8740 IIS CPU DAI + CS42/dummy codec. + Registers ASoC card: S5L8740 IIS0 CPU DAI + CS42L81 SPI codec. config SND_SOC_APPLE_S5L8740_I2S tristate "S5L8740 IIS0 I2S CPU DAI" @@ -23,7 +24,8 @@ config SND_SOC_APPLE_S5L8740_I2S IIS0 @0x3CA00000 CPU DAI with optional PL080 dmaengine PCM. config SND_SOC_APPLE_CS42L81_SPI - tristate "CS42L81 / 338S1146 SPI control (N31)" - depends on SPI + tristate "CS42L81 / 338S1146 SPI codec (N31)" + depends on SPI && SND_SOC help - RetailOS-matched SPI0 framing (0x6C/0x6D) and corpus bring-up. + RetailOS-matched SPI0 framing (0x6C/0x6D), analog bring-up, and + ASoC DAI (cs42l81-hifi). Sysfs reg/bringup/audio_on stay. From 928babfe42b77e64bb158fd109de18f1add2dbf5 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Thu, 27 Aug 2026 04:16:01 -0230 Subject: [PATCH 16/31] N31: sync glass audio/DMA/FTL/MikeyBus from live bring-up tree Sustained IIS0/PL080/ALSA playback at 44.1 kHz, CS42 5707D8 path, MikeyBus jack helper, and current FMSS/FTL sources. Analog HP still unproven on UCA222. --- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 87 +- arch/arm/configs/apple_n31_defconfig | 25 +- drivers/bluetooth/Kconfig | 8 +- drivers/bluetooth/bcm2078-bt.c | 565 +-- drivers/clk/clk-s5l8702.c | 15 +- drivers/crypto/s5l8702-aes.c | 130 +- drivers/dma/dma-s5l8740-pl080.c | 361 +- drivers/gpio/gpio-d1830.c | 17 +- drivers/gpio/gpio-s5l8740.c | 53 + drivers/input/touchscreen/apple-nimbus.c | 1293 +++++- drivers/misc/Kconfig | 16 +- drivers/misc/Makefile | 1 + drivers/misc/apple-mikeybus.c | 681 ++++ drivers/misc/fmss-s5l8740-api.h | 54 +- drivers/misc/fmss-s5l8740.c | 1186 +++++- drivers/misc/ftl-s5l8740.c | 4531 ++++++++++++++++++--- drivers/misc/whimory-s5l8740.h | 335 ++ sound/soc/apple/Kconfig | 13 +- sound/soc/apple/Makefile | 13 +- sound/soc/apple/cs42l81-spi.c | 2188 +++++++++- sound/soc/apple/n31-audio-rates.h | 115 + sound/soc/apple/nano7-audio.c | 47 +- sound/soc/apple/s5l8740-i2s.c | 893 ++-- sound/soc/apple/s5l8740-iis2.c | 366 ++ 24 files changed, 11224 insertions(+), 1769 deletions(-) create mode 100755 drivers/misc/apple-mikeybus.c create mode 100755 drivers/misc/whimory-s5l8740.h create mode 100755 sound/soc/apple/n31-audio-rates.h create mode 100755 sound/soc/apple/s5l8740-iis2.c diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index b21ab743daa2b9..591867ebdf9327 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -26,6 +26,11 @@ /* U-Boot CONFIG_BOOTARGS overrides this. g_ether is built-in (0525:a4a2). */ bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=-1 clk_ignore_unused init=/init"; stdout-path = "serial0"; + /* + * U-Boot ft_board_setup adds apple,n31-isys-addr / apple,n31-isys-size + * pointing at a reserved DRAM copy of the A34 IsyS object (0x560). + * Do not consume the original 0x2202FE1C pointer from Linux. + */ }; nclk: external_clock { @@ -58,6 +63,23 @@ reg = <0x08000000 0x04000000>; }; + /* + * U-Boot copies the 0x560 A34 IsyS object here (gap below DFU load, + * above the 16 MiB bootm window). Chosen apple,n31-isys-* is filled + * at ft_board_setup; this node keeps Linux from recycling the page. + */ + reserved-memory { + #address-cells = <1>; + #size-cells = <1>; + ranges; + + n31-isys@9dff000 { + compatible = "apple,n31-isys"; + reg = <0x09DFF000 0x1000>; + no-map; + }; + }; + soc { compatible = "simple-bus"; #address-cells = <1>; @@ -130,6 +152,18 @@ interrupt-parent = <&vic0>; interrupts = <25>; status = "okay"; + + /* BCM2078KUBG on UART1 @115200 — serdev → hci_bcm → hci0. + * Companion GPIO/FM lives under apple,n31-bcm2078-companion. */ + bluetooth { + compatible = "brcm,bcm4329-bt", "brcm,bcm2078"; + max-speed = <115200>; + shutdown-gpios = <&gpio 97 GPIO_ACTIVE_LOW>; + device-wakeup-gpios = <&gpio 98 GPIO_ACTIVE_HIGH>; + host-wakeup-gpios = <&gpio 119 GPIO_ACTIVE_HIGH>; + firmware-name = "brcm/BCM2076B1.hcd"; + status = "okay"; + }; }; uart2: serial@3dc00000 { @@ -140,7 +174,20 @@ reg-io-width = <4>; interrupt-parent = <&vic0>; interrupts = <26>; + /* + * MikeyBus (GPIO 66/67). uart3 stays first (samsung port 0). + * Do NOT serdev_device_open() at probe. Do NOT live-add s5l-uart + * (pinmux-then-probe locked glass 2026-08-27). uart_open sysfs + * only for remote RX after boot. + */ status = "okay"; + + mikeybus { + compatible = "apple,mikeybus", "apple,n31-mikeybus"; + current-speed = <115200>; + apple,force-plugged; + status = "okay"; + }; }; usbphy: usbphy@3c400000 { @@ -220,6 +267,13 @@ status = "okay"; }; + /* Thin N31 helper: RetailOS GPIO mode-2 + FM 0xFC15 debug via hci0. + * Does not own UART1 — that is bluetooth { } under uart1 → hci_bcm. */ + bcm2078_companion: bcm2078-companion { + compatible = "apple,n31-bcm2078-companion"; + status = "okay"; + }; + i2c0: i2c@3c600000 { compatible = "samsung,s5l8702-i2c"; #address-cells = <1>; @@ -363,7 +417,11 @@ }; }; - /* PL080 pair from OSOS. Peri 12/13 = IIS0. Not 0x384 (DWC2). */ + /* + * PL080 pair from OSOS. Not 0x384 (DWC2). + * RetailOS oracle: IIS0 TX peri 10 (DST 0x3ca00010); IIS0 RX 11 assumed; + * IIS2 FM RX peri 13 (SRC 0x3d400038). PL080_1 unused in snaps. + */ dmac: dma-controller@38200000 { compatible = "apple,s5l8740-pl080", "arm,pl080"; reg = <0x38200000 0x1000>, <0x38700000 0x1000>; @@ -376,15 +434,32 @@ i2s0: i2s@3ca00000 { compatible = "apple,s5l8740-i2s", "samsung,s5l8740-i2s"; reg = <0x3ca00000 0x1000>; + /* RetailOS music: TX peri 10 confirmed. RX 11 still assumed. */ dmas = <&dmac 10 0>, <&dmac 11 0>; dma-names = "tx", "rx"; #sound-dai-cells = <0>; status = "okay"; }; + /* + * BCM2078 digital PCM (SoC IIS2 @ 0x3D400000). + * RX FIFO @ +0x38, PL080 peri 13 — FM + module→SoC PCM in. + * IIS1 @ 0x3CD00000 is unused (XSP); BT A2DP out = UART1 HCI. + * RetailOS: bt-*-scsi-live/, fm-*-scsi-live/ + */ + i2s2: i2s@3d400000 { + compatible = "apple,s5l8740-bcm2078-pcm", "apple,s5l8740-iis2"; + reg = <0x3d400000 0x1000>; + dmas = <&dmac 13 0>; + dma-names = "rx"; + #sound-dai-cells = <0>; + status = "okay"; + }; + nano7_audio: audio { compatible = "apple,n31-audio"; apple,cpu = <&i2s0>; + apple,fm-cpu = <&i2s2>; apple,codec = <&cs42l81>; status = "okay"; }; @@ -399,6 +474,11 @@ charge-full-design-microamp-hours = <200000>; }; + /* + * Keep gpio-keys disabled (p5-keys-alsa): Home/Sleep/Play come from + * gpio-d1830 I2C poll + nIRQ; Vol± from gpio-s5l8740 DIN poll. + * Enabling this node would double-report and risk 0xFFFE on Vol pads. + */ gpio-keys { compatible = "gpio-keys"; status = "disabled"; @@ -425,8 +505,9 @@ }; }; - /* Disabled so gpio-keys-polled cannot direction_input/0xFFFE Vol pads. - * gpio-s5l8740 polls DIN itself and reports KEY_VOLUMEUP/DOWN. + /* Disabled: gpio-keys-polled must not direction_input/0xFFFE Vol pads. + * gpio-s5l8740 polls DIN and reports KEY_VOLUMEUP/DOWN; cs42l81-spi + * input_handler steps Master Playback Volume. */ gpio-keys-vol { compatible = "gpio-keys-polled"; diff --git a/arch/arm/configs/apple_n31_defconfig b/arch/arm/configs/apple_n31_defconfig index a5a586905ab45d..13125825232a99 100644 --- a/arch/arm/configs/apple_n31_defconfig +++ b/arch/arm/configs/apple_n31_defconfig @@ -33,6 +33,7 @@ CONFIG_USB_LIBCOMPOSITE=y CONFIG_CONFIGFS_FS=y CONFIG_PHY_S5L8702_USB2=y CONFIG_APPLE_TRISTAR_CBTL1609=m +CONFIG_APPLE_MIKEYBUS=m CONFIG_UNIX98_PTYS=y CONFIG_DEVPTS_FS=y CONFIG_NET=y @@ -106,7 +107,18 @@ CONFIG_SND_SOC_GENERIC_DMAENGINE_PCM=y CONFIG_SND_DMAENGINE_PCM=y CONFIG_SND_SOC_APPLE_CS42L81_SPI=m CONFIG_SND_SOC_APPLE_S5L8740_I2S=m +CONFIG_SND_SOC_APPLE_S5L8740_IIS2=m CONFIG_SND_SOC_APPLE_NANO7=m +# V4L2 FM radio (/dev/radio0) over BCM 0xFC15 +CONFIG_MEDIA_SUPPORT=y +CONFIG_MEDIA_RADIO_SUPPORT=y +CONFIG_VIDEO_DEV=y +# CONFIG_RADIO_ADAPTERS is not set +# CONFIG_MEDIA_TUNER is not set +# CONFIG_MEDIA_DIGITAL_TV_SUPPORT is not set +# CONFIG_MEDIA_CAMERA_SUPPORT is not set +# CONFIG_MEDIA_USB_SUPPORT is not set +# No stock V4L radio tuners — N31 FM is bcm2078 /dev/radio0 only. CONFIG_DMADEVICES=y CONFIG_DMA_ENGINE=y CONFIG_DMA_VIRTUAL_CHANNELS=y @@ -128,5 +140,14 @@ CONFIG_EFI_PARTITION=y CONFIG_SND_OSSEMUL=y CONFIG_SND_MIXER_OSS=y CONFIG_SND_PCM_OSS=y -CONFIG_FMSS_S5L8740=m -CONFIG_FTL_S5L8740=m +# Bluetooth: UART1 serdev → hci_bcm → hci0 (+ thin N31 GPIO companion) +CONFIG_BT=y +CONFIG_BT_BREDR=y +CONFIG_BT_LE=y +CONFIG_BT_HCIUART=y +CONFIG_BT_HCIUART_H4=y +CONFIG_BT_HCIUART_BCM=y +CONFIG_BT_BCM=y +CONFIG_BT_BCM2078_N31=y +CONFIG_SERIAL_DEV_BUS=y +CONFIG_SERIAL_DEV_CTRL_TTYPORT=y diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig index b881199eeb7457..1941a202451015 100644 --- a/drivers/bluetooth/Kconfig +++ b/drivers/bluetooth/Kconfig @@ -520,7 +520,11 @@ config BT_INTEL_PCIE endmenu config BT_BCM2078_N31 - tristate "Broadcom BCM2078 power companion (N31)" + tristate "Broadcom BCM2078 GPIO/FM companion (N31)" depends on BT && OF && HAS_IOMEM + depends on VIDEO_DEV help - Power/GPIO + Vincent patchram HCD companion for BCM2078 on N31. + Thin N31 helper: RetailOS GPIO mode-2, FM 0xFC15 via hci0, and + V4L2 /dev/radio0. Sysfs fm_* is debug. UART1 HCI is hci_bcm. + No FM->A2DP path — local IIS2 capture + IIS0 play only. + diff --git a/drivers/bluetooth/bcm2078-bt.c b/drivers/bluetooth/bcm2078-bt.c index ccf868662f086c..049f1ee42ff743 100755 --- a/drivers/bluetooth/bcm2078-bt.c +++ b/drivers/bluetooth/bcm2078-bt.c @@ -1,56 +1,82 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * BCM2078 companion — N31 + * BCM2078 N31 companion — RetailOS GPIO mode-2 + FM 0xFC15. * - * Power: RetailOS sub_43D38C mode-2 / 0xFFFE on GPIOs 0x61/0x62/0x77. - * UART1 HCI @ 0x3DB00000 / 115200 (BT Uart RxLoop). - * Patchram: stream Vincent HCD (Write_RAM 0xFC4C ×146 + Launch_RAM 0xFC4E). - * FM: HCI vendor 0xFC15 cookbook from RetailOS BroadcomFM FIFO (sub_4290C4). + * UART1 HCI / patchram belong to apple,s5l-uart + serdev + hci_bcm/btbcm + * (hci0). This driver must NOT ioremap UART1 or of_platform_device_create the + * bluetooth child — that steals the port from hci_bcm. + * + * FM vendor opcode 0xFC15 is sent via __hci_cmd_sync on hci0 when HCI_UP. + * Phase 3: thin V4L2 radio (/dev/radio0). Sysfs fm_* remains debug. + * Audio is IIS2 ALSA capture → userspace → IIS0 play. No FM→A2DP path. */ -#include #include -#include -#include #include #include #include -#include #include #include #include +#include + +#include +#include +#include + +#include +#include #define BCM_GPIO_PHYS 0x3cf00000UL #define BCM_GPIOCMD_OFF 0x1e0 -#define BCM_UART1_PHYS 0x3db00000UL -#define BCM_UART_STATUS 0x10 -#define BCM_UART_TX 0x20 -#define BCM_UART_RX 0x24 -#define BCM_UART_TXFULL 0x20 /* STATUS bit — refine on HW if needed */ -#define BCM_UART_RXRDY 0x01 - -#define BCM_GPIO_A 0x61 -#define BCM_GPIO_B 0x62 -#define BCM_GPIO_C 0x77 + +#define BCM_GPIO_A 0x61 /* 97 — shutdown / REG_ON */ +#define BCM_GPIO_B 0x62 /* 98 — device-wakeup */ +#define BCM_GPIO_C 0x77 /* 119 — host-wakeup */ #define BCM_GPIO_NOP 0xC8 #define BCM_MODE_POWER 2 #define BCM_MODE_CLEAR 0xFFFE -#define BCM_FW_NAME "brcm/BCM2076B1.hcd" #define HCI_OP_FC15 0xFC15 +/* V4L2_TUNER_CAP_LOW: 62.5 Hz units → kHz * 16 */ +#define BCM_FM_FREQ_TO_V4L(khz) ((khz) * 16u) +#define BCM_FM_V4L_TO_FREQ(f) ((f) / 16u) +#define BCM_FM_KHZ_MIN 87500u +#define BCM_FM_KHZ_MAX 108000u +#define BCM_FM_KHZ_DEFAULT 94700u /* Canada test station */ + +/* + * FC15 reg 0x05 audio ctrl (BCM4325/2048 family, via 0xFC15 on 2078): + * 0x0001 = RetailOS audio route (DD334) + * 0x0040 = 75 µs de-emphasis (Canada/US; 50 µs = clear bit) + * 0x0020 = I2S PCM route (IIS2 on N31) + */ +#define BCM_FM_AUDIO_ROUTE_ORACLE 0x0001u +#define BCM_FM_AUDIO_DEMPH_75US 0x0040u +#define BCM_FM_AUDIO_ROUTE_I2S 0x0020u +#define BCM_FM_AUDIO_CTRL0_DEFAULT (BCM_FM_AUDIO_ROUTE_ORACLE | \ + BCM_FM_AUDIO_DEMPH_75US | \ + BCM_FM_AUDIO_ROUTE_I2S) + +static u16 fm_audio_ctrl0 = BCM_FM_AUDIO_CTRL0_DEFAULT; +module_param(fm_audio_ctrl0, ushort, 0644); +MODULE_PARM_DESC(fm_audio_ctrl0, + "FC15 reg0x05 audio ctrl (default 0x61 = route+I2S+75us deemph)"); + struct bcm2078_bt { struct device *dev; void __iomem *gpio; void __iomem *gpiocmd; - void __iomem *uart; - struct clk *uart_clk; bool powered; - bool patched; - bool fw_present; + bool fm_on; + unsigned int fm_khz; struct mutex lock; + struct v4l2_device v4l2_dev; + struct video_device vdev; + bool radio_registered; }; -/* ---------- GPIO (RetailOS sub_43D38C) ---------- */ +/* ---------- GPIO (RetailOS sub_43D38C mode-2) ---------- */ static void bcm_43D38C(struct bcm2078_bt *bt, unsigned int gpio, u16 mode, int val) { @@ -92,147 +118,37 @@ static void bcm_power_pins_off(struct bcm2078_bt *bt) bcm_43D38C(bt, BCM_GPIO_C, BCM_MODE_CLEAR, 0); } -/* ---------- UART1 H4 HCI ---------- */ - -static int bcm_uart_tx(struct bcm2078_bt *bt, const u8 *buf, size_t len) -{ - size_t i; - unsigned guard; - - for (i = 0; i < len; i++) { - guard = 200000; - while (guard-- && (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_TXFULL)) - cpu_relax(); - writel(buf[i], bt->uart + BCM_UART_TX); - } - return 0; -} +/* ---------- FM 0xFC15 via hci0 (not raw UART) ---------- */ -static size_t bcm_uart_rx(struct bcm2078_bt *bt, u8 *buf, size_t maxlen, - unsigned timeout_ms) +static int bcm_fc15(struct bcm2078_bt *bt, const u8 *payload, u8 plen) { - size_t n = 0; - unsigned long deadline = jiffies + msecs_to_jiffies(timeout_ms); - - while (n < maxlen && time_before(jiffies, deadline)) { - if (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_RXRDY) - buf[n++] = (u8)readl(bt->uart + BCM_UART_RX); - else - cpu_relax(); + struct hci_dev *hdev; + struct sk_buff *skb; + + hdev = hci_dev_get(0); + if (!hdev) { + dev_warn_ratelimited(bt->dev, + "FC15: no hci0 — bring up hci_bcm first\n"); + return -ENODEV; } - return n; -} - -static void bcm_uart_drain(struct bcm2078_bt *bt) -{ - unsigned guard = 10000; - - while (guard-- && (readl(bt->uart + BCM_UART_STATUS) & BCM_UART_RXRDY)) - (void)readl(bt->uart + BCM_UART_RX); -} - -static int bcm_hci_cmd(struct bcm2078_bt *bt, u16 opcode, const u8 *plen_payload, - u8 plen, u8 *evt, size_t evt_max, size_t *evt_n) -{ - u8 hdr[4]; - size_t n; - - hdr[0] = 0x01; /* H4 CMD */ - hdr[1] = opcode & 0xff; - hdr[2] = opcode >> 8; - hdr[3] = plen; - bcm_uart_drain(bt); - bcm_uart_tx(bt, hdr, 4); - if (plen && plen_payload) - bcm_uart_tx(bt, plen_payload, plen); - n = bcm_uart_rx(bt, evt, evt_max, 500); - if (evt_n) - *evt_n = n; - return n > 0 ? 0 : -ETIMEDOUT; -} - -static int bcm_hci_reset(struct bcm2078_bt *bt) -{ - u8 evt[32]; - size_t n; - int ret; - - ret = bcm_hci_cmd(bt, 0x0c03, NULL, 0, evt, sizeof(evt), &n); - dev_info(bt->dev, "HCI Reset → %d RX %zu:%*ph\n", ret, n, (int)n, evt); - return ret; -} - -/* ---------- Patchram (Vincent HCD) ---------- */ - -static int bcm_load_hcd(struct bcm2078_bt *bt) -{ - const struct firmware *fw; - const u8 *p, *end; - u8 evt[64]; - size_t n; - unsigned cmds = 0; - int ret; - - ret = request_firmware(&fw, BCM_FW_NAME, bt->dev); - if (ret) { - dev_err(bt->dev, "firmware %s: %d\n", BCM_FW_NAME, ret); - return ret; + if (!test_bit(HCI_UP, &hdev->flags)) { + hci_dev_put(hdev); + dev_warn_ratelimited(bt->dev, + "FC15: hci0 down — run n31-bt-up / HCIDEVUP\n"); + return -ENETDOWN; } - p = fw->data; - end = p + fw->size; - while (p + 4 <= end) { - u8 type = p[0]; - u16 opcode; - u8 plen; - - if (type != 0x01) { - dev_err(bt->dev, "HCD bad type %02x @+%zx\n", - type, p - fw->data); - ret = -EINVAL; - break; - } - opcode = p[1] | (p[2] << 8); - plen = p[3]; - if (p + 4 + plen > end) { - ret = -EINVAL; - break; - } - bcm_uart_drain(bt); - bcm_uart_tx(bt, p, 4 + plen); - n = bcm_uart_rx(bt, evt, sizeof(evt), 1000); - cmds++; - if (n < 2 || evt[0] != 0x04) { - dev_warn(bt->dev, - "patch cmd#%u op=%04x plen=%u RX %zu:%*ph\n", - cmds, opcode, plen, n, (int)n, evt); - } - p += 4 + plen; - /* Launch_RAM is last */ - if (opcode == 0xfc4e) - break; + skb = __hci_cmd_sync(hdev, HCI_OP_FC15, plen, payload, HCI_CMD_TIMEOUT); + hci_dev_put(hdev); + if (IS_ERR(skb)) { + dev_dbg(bt->dev, "FC15 plen=%u → %ld\n", plen, PTR_ERR(skb)); + return PTR_ERR(skb); } - release_firmware(fw); - bt->patched = (ret == 0); - dev_info(bt->dev, "patchram %s — %u HCI cmds\n", - ret ? "FAIL" : "OK", cmds); - return ret; -} - -/* ---------- FM 0xFC15 cookbook (RetailOS BroadcomFM) ---------- */ - -static int bcm_fc15(struct bcm2078_bt *bt, const u8 *payload, u8 plen) -{ - u8 evt[64]; - size_t n; - int ret; - - ret = bcm_hci_cmd(bt, HCI_OP_FC15, payload, plen, evt, sizeof(evt), &n); - dev_dbg(bt->dev, "FC15 plen=%u → RX %zu:%*ph\n", plen, n, (int)n, evt); - return ret; + dev_dbg(bt->dev, "FC15 plen=%u → OK len=%u\n", plen, skb->len); + kfree_skb(skb); + return 0; } -/* Encode FIFO-style write8: plen=3, reg, 0x00, val */ static int bcm_fm_w8(struct bcm2078_bt *bt, u8 reg, u8 val) { u8 p[3] = { reg, 0x00, val }; @@ -250,7 +166,7 @@ static int bcm_fm_w16(struct bcm2078_bt *bt, u8 reg, u16 val) static int bcm_fm_power_on(struct bcm2078_bt *bt) { int ret; - /* DD136 + DD334(0) + DD458 + DD2FC(33,20) */ + ret = bcm_fm_w8(bt, 0x00, 0x03); if (ret) return ret; @@ -260,16 +176,15 @@ static int bcm_fm_power_on(struct bcm2078_bt *bt) ret = bcm_fm_w8(bt, 0x02, 0x02); if (ret) return ret; - ret = bcm_fm_w16(bt, 0x05, 0x0001); + ret = bcm_fm_w16(bt, 0x05, fm_audio_ctrl0); if (ret) return ret; { - u8 rd[3] = { 0x4d, 0x01, 0x01 }; /* read status */ + u8 rd[3] = { 0x4d, 0x01, 0x01 }; bcm_fc15(bt, rd, 3); } { - /* RSSI=33 noise=20 — DD2FC */ u8 p[11] = { 0xf9, 0x00, 0x21, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00 @@ -277,15 +192,17 @@ static int bcm_fm_power_on(struct bcm2078_bt *bt) ret = bcm_fc15(bt, p, 11); } - dev_info(bt->dev, "FM power ON (0xFC15 cookbook)%s\n", - ret ? " FAIL" : ""); + bt->fm_on = !ret; + dev_info(bt->dev, "FM power ON (0xFC15 via hci0) audio_ctrl=0x%04x%s\n", + fm_audio_ctrl0, ret ? " FAIL" : ""); return ret; } static int bcm_fm_power_off(struct bcm2078_bt *bt) { - int ret = bcm_fm_w8(bt, 0x00, 0x00); /* DD118: 00 00 00 */ + int ret = bcm_fm_w8(bt, 0x00, 0x00); + bt->fm_on = false; dev_info(bt->dev, "FM power OFF%s\n", ret ? " FAIL" : ""); return ret; } @@ -295,11 +212,14 @@ static int bcm_fm_tune_khz(struct bcm2078_bt *bt, unsigned int khz) u16 enc; int ret; - /* Band: >=87500 → 2 else 3 */ + if (!bt->fm_on) { + ret = bcm_fm_power_on(bt); + if (ret) + return ret; + } ret = bcm_fm_w8(bt, 0x01, khz >= 87500 ? 2 : 3); if (ret) return ret; - /* Pre-tune 56DB66: reg 0x10 = 0x1203 */ ret = bcm_fm_w16(bt, 0x10, 0x1203); if (ret) return ret; @@ -307,7 +227,9 @@ static int bcm_fm_tune_khz(struct bcm2078_bt *bt, unsigned int khz) ret = bcm_fm_w16(bt, 0x0a, enc); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x09, 0x01); /* unmute */ + ret = bcm_fm_w8(bt, 0x09, 0x01); + if (!ret) + bt->fm_khz = khz; dev_info(bt->dev, "FM tune %u kHz enc=%04x%s\n", khz, enc, ret ? " FAIL" : ""); return ret; @@ -336,31 +258,200 @@ static int bcm_fm_seek(struct bcm2078_bt *bt, int up, u8 rssi) return ret; } -/* ---------- Power / bring-up ---------- */ - static int bcm_power_on(struct bcm2078_bt *bt) { - if (bt->uart_clk) - clk_prepare_enable(bt->uart_clk); bcm_power_pins_on(bt); msleep(150); bt->powered = true; - bcm_hci_reset(bt); + dev_info(bt->dev, + "RetailOS mode-2 GPIOs on — hci_bcm owns UART1/hci0\n"); return 0; } static void bcm_power_off(struct bcm2078_bt *bt) { - if (bt->patched) - bcm_fm_power_off(bt); bcm_power_pins_off(bt); - if (bt->uart_clk) - clk_disable_unprepare(bt->uart_clk); bt->powered = false; - bt->patched = false; } -/* ---------- sysfs ---------- */ +/* ---------- V4L2 radio (tuner control only; PCM is ALSA IIS2) ---------- */ + +static int bcm_radio_querycap(struct file *file, void *fh, + struct v4l2_capability *cap) +{ + strscpy(cap->driver, "bcm2078-fm", sizeof(cap->driver)); + strscpy(cap->card, "N31 BCM2078 FM", sizeof(cap->card)); + strscpy(cap->bus_info, "hci0:0xFC15", sizeof(cap->bus_info)); + cap->device_caps = V4L2_CAP_RADIO | V4L2_CAP_TUNER | + V4L2_CAP_HW_FREQ_SEEK; + cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS; + return 0; +} + +static int bcm_radio_g_tuner(struct file *file, void *fh, struct v4l2_tuner *t) +{ + struct bcm2078_bt *bt = video_drvdata(file); + + if (t->index > 0) + return -EINVAL; + strscpy(t->name, "FM", sizeof(t->name)); + t->type = V4L2_TUNER_RADIO; + t->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO | + V4L2_TUNER_CAP_FREQ_BANDS; + t->rangelow = BCM_FM_FREQ_TO_V4L(BCM_FM_KHZ_MIN); + t->rangehigh = BCM_FM_FREQ_TO_V4L(BCM_FM_KHZ_MAX); + t->rxsubchans = V4L2_TUNER_SUB_STEREO; + t->audmode = V4L2_TUNER_MODE_STEREO; + t->signal = bt->fm_on ? 0xffff : 0; + t->afc = 0; + return 0; +} + +static int bcm_radio_s_tuner(struct file *file, void *fh, + const struct v4l2_tuner *t) +{ + if (t->index > 0) + return -EINVAL; + return 0; +} + +static int bcm_radio_g_frequency(struct file *file, void *fh, + struct v4l2_frequency *f) +{ + struct bcm2078_bt *bt = video_drvdata(file); + + if (f->tuner != 0) + return -EINVAL; + f->type = V4L2_TUNER_RADIO; + f->frequency = BCM_FM_FREQ_TO_V4L(bt->fm_khz ? bt->fm_khz : + BCM_FM_KHZ_DEFAULT); + return 0; +} + +static int bcm_radio_s_frequency(struct file *file, void *fh, + const struct v4l2_frequency *f) +{ + struct bcm2078_bt *bt = video_drvdata(file); + unsigned int khz; + int ret; + + if (f->tuner != 0 || f->type != V4L2_TUNER_RADIO) + return -EINVAL; + khz = BCM_FM_V4L_TO_FREQ(f->frequency); + if (khz < BCM_FM_KHZ_MIN) + khz = BCM_FM_KHZ_MIN; + if (khz > BCM_FM_KHZ_MAX) + khz = BCM_FM_KHZ_MAX; + + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + if (!bt->fm_on) { + ret = bcm_fm_power_on(bt); + if (ret) + goto out; + } + ret = bcm_fm_tune_khz(bt, khz); +out: + mutex_unlock(&bt->lock); + return ret; +} + +static int bcm_radio_s_hw_freq_seek(struct file *file, void *fh, + const struct v4l2_hw_freq_seek *a) +{ + struct bcm2078_bt *bt = video_drvdata(file); + int ret; + + if (a->tuner != 0 || a->type != V4L2_TUNER_RADIO) + return -EINVAL; + + mutex_lock(&bt->lock); + if (!bt->powered) + bcm_power_on(bt); + if (!bt->fm_on) { + ret = bcm_fm_power_on(bt); + if (ret) + goto out; + } + ret = bcm_fm_seek(bt, a->seek_upward ? 1 : 0, 33); +out: + mutex_unlock(&bt->lock); + return ret; +} + +static int bcm_radio_enum_freq_bands(struct file *file, void *fh, + struct v4l2_frequency_band *band) +{ + if (band->tuner != 0 || band->index > 0) + return -EINVAL; + band->type = V4L2_TUNER_RADIO; + band->capability = V4L2_TUNER_CAP_LOW | V4L2_TUNER_CAP_STEREO; + band->rangelow = BCM_FM_FREQ_TO_V4L(BCM_FM_KHZ_MIN); + band->rangehigh = BCM_FM_FREQ_TO_V4L(BCM_FM_KHZ_MAX); + band->modulation = V4L2_BAND_MODULATION_FM; + return 0; +} + +static const struct v4l2_ioctl_ops bcm_radio_ioctl_ops = { + .vidioc_querycap = bcm_radio_querycap, + .vidioc_g_tuner = bcm_radio_g_tuner, + .vidioc_s_tuner = bcm_radio_s_tuner, + .vidioc_g_frequency = bcm_radio_g_frequency, + .vidioc_s_frequency = bcm_radio_s_frequency, + .vidioc_s_hw_freq_seek = bcm_radio_s_hw_freq_seek, + .vidioc_enum_freq_bands = bcm_radio_enum_freq_bands, +}; + +static const struct v4l2_file_operations bcm_radio_fops = { + .owner = THIS_MODULE, + .open = v4l2_fh_open, + .release = v4l2_fh_release, + .unlocked_ioctl = video_ioctl2, +}; + +static int bcm_radio_register(struct bcm2078_bt *bt) +{ + int ret; + + ret = v4l2_device_register(bt->dev, &bt->v4l2_dev); + if (ret) + return ret; + + strscpy(bt->v4l2_dev.name, "bcm2078-fm", sizeof(bt->v4l2_dev.name)); + bt->vdev = (struct video_device){ + .name = "N31 FM Radio", + .v4l2_dev = &bt->v4l2_dev, + .fops = &bcm_radio_fops, + .ioctl_ops = &bcm_radio_ioctl_ops, + .release = video_device_release_empty, + .device_caps = V4L2_CAP_RADIO | V4L2_CAP_TUNER | + V4L2_CAP_HW_FREQ_SEEK, + }; + video_set_drvdata(&bt->vdev, bt); + + ret = video_register_device(&bt->vdev, VFL_TYPE_RADIO, -1); + if (ret) { + v4l2_device_unregister(&bt->v4l2_dev); + return ret; + } + bt->radio_registered = true; + bt->fm_khz = BCM_FM_KHZ_DEFAULT; + dev_info(bt->dev, "V4L2 radio %s (0xFC15; headphones required; no FM→A2DP)\n", + video_device_node_name(&bt->vdev)); + return 0; +} + +static void bcm_radio_unregister(struct bcm2078_bt *bt) +{ + if (!bt->radio_registered) + return; + video_unregister_device(&bt->vdev); + v4l2_device_unregister(&bt->v4l2_dev); + bt->radio_registered = false; +} + +/* ---------- sysfs (debug FM + GPIO helper) ---------- */ static ssize_t power_on_show(struct device *dev, struct device_attribute *a, char *buf) @@ -388,26 +479,26 @@ static ssize_t power_on_store(struct device *dev, struct device_attribute *a, } static DEVICE_ATTR_RW(power_on); -static ssize_t patchram_store(struct device *dev, struct device_attribute *a, - const char *buf, size_t count) +static ssize_t patchram_show(struct device *dev, struct device_attribute *a, + char *buf) { - struct bcm2078_bt *bt = dev_get_drvdata(dev); - int ret; + struct hci_dev *hdev = hci_dev_get(0); + int up = 0; - mutex_lock(&bt->lock); - if (!bt->powered) - bcm_power_on(bt); - ret = bcm_load_hcd(bt); - mutex_unlock(&bt->lock); - return ret ? ret : count; + if (hdev) { + up = test_bit(HCI_UP, &hdev->flags); + hci_dev_put(hdev); + } + return sysfs_emit(buf, "%d\n", up); } -static ssize_t patchram_show(struct device *dev, struct device_attribute *a, - char *buf) +static ssize_t patchram_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) { - struct bcm2078_bt *bt = dev_get_drvdata(dev); - - return sysfs_emit(buf, "%d\n", bt->patched ? 1 : 0); + /* Interim: patchram is owned by hci_bcm/btbcm — do not steal UART. */ + dev_info(dev, + "patchram retired — use hci0 (hci_bcm + brcm/BCM2076B1.hcd)\n"); + return count; } static DEVICE_ATTR_RW(patchram); @@ -423,8 +514,6 @@ static ssize_t fm_power_store(struct device *dev, struct device_attribute *a, mutex_lock(&bt->lock); if (!bt->powered) bcm_power_on(bt); - if (!bt->patched) - bcm_load_hcd(bt); ret = on ? bcm_fm_power_on(bt) : bcm_fm_power_off(bt); mutex_unlock(&bt->lock); return ret ? ret : count; @@ -438,17 +527,20 @@ static ssize_t fm_tune_store(struct device *dev, struct device_attribute *a, unsigned int khz; int ret; - /* accept kHz or MHz*10 (e.g. 991 for 99.1) */ if (kstrtouint(buf, 0, &khz)) return -EINVAL; if (khz < 1000) - khz *= 100; /* 991 → 99100 */ + khz *= 100; mutex_lock(&bt->lock); if (!bt->powered) bcm_power_on(bt); - if (!bt->patched) - bcm_load_hcd(bt); + if (!bt->fm_on) { + ret = bcm_fm_power_on(bt); + if (ret) + goto out; + } ret = bcm_fm_tune_khz(bt, khz); +out: mutex_unlock(&bt->lock); return ret ? ret : count; } @@ -466,8 +558,6 @@ static ssize_t fm_seek_store(struct device *dev, struct device_attribute *a, mutex_lock(&bt->lock); if (!bt->powered) bcm_power_on(bt); - if (!bt->patched) - bcm_load_hcd(bt); ret = bcm_fm_seek(bt, up, 33); mutex_unlock(&bt->lock); return ret ? ret : count; @@ -478,12 +568,14 @@ static ssize_t patchram_info_show(struct device *dev, struct device_attribute *a char *buf) { return sysfs_emit(buf, - "hcd=/lib/firmware/%s\n" - "hci=Write_RAM(0xFC4C)x146+Launch_RAM(0xFC4E)\n" - "load=echo 1 > patchram\n" - "fm=echo 1 > fm_power; echo 99100 > fm_tune; echo up > fm_seek\n" - "fc15=RetailOS BroadcomFM FIFO cookbook\n", - BCM_FW_NAME); + "hci=hci_bcm/serdev on uart1 (not this companion)\n" + "hcd=/lib/firmware/brcm/BCM2076B1.hcd\n" + "bringup=/bin/n31-bt-up → hci0 + HCIDEVUP\n" + "gpio=RetailOS mode-2 on 0x61/0x62/0x77 via power_on\n" + "radio=/dev/radio0 V4L2 (prefer); sysfs fm_* = debug\n" + "audio=IIS2 capture → arecord|aplay IIS0 (headphones required)\n" + "fm_default=94700 kHz deemph=75us (Canada)\n" + "no_fm_a2dp=1 (local speakers/HP only)\n"); } static DEVICE_ATTR_RO(patchram_info); @@ -502,12 +594,13 @@ static int bcm2078_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct bcm2078_bt *bt; - const struct firmware *fw; + int ret; bt = devm_kzalloc(dev, sizeof(*bt), GFP_KERNEL); if (!bt) return -ENOMEM; bt->dev = dev; + bt->fm_khz = BCM_FM_KHZ_DEFAULT; mutex_init(&bt->lock); platform_set_drvdata(pdev, bt); @@ -516,31 +609,20 @@ static int bcm2078_probe(struct platform_device *pdev) return -ENOMEM; bt->gpiocmd = bt->gpio + BCM_GPIOCMD_OFF; - bt->uart = devm_ioremap(dev, BCM_UART1_PHYS, 0x40); - if (!bt->uart) - return -ENOMEM; - - bt->uart_clk = devm_clk_get_optional(dev->parent ? dev->parent : dev, - "uart"); - if (IS_ERR(bt->uart_clk)) - bt->uart_clk = NULL; - - if (request_firmware(&fw, BCM_FW_NAME, dev) == 0) { - bt->fw_present = fw->size > 0; - release_firmware(fw); - } - if (sysfs_create_groups(&dev->kobj, bcm_groups)) dev_warn(dev, "sysfs groups failed\n"); + ret = bcm_radio_register(bt); + if (ret) + dev_warn(dev, "V4L2 radio register: %d (sysfs FM still OK)\n", + ret); + /* - * Do NOT power UART or load patchram at probe — boot-time HCI + raw GPIO - * poke (0x61/0x62/0x77) correlated with reset back to RetailOS. - * Use sysfs power_on / patchram or /bin/n31-bt-up after init is up. + * Do NOT poke GPIOs or touch UART at probe — early mode-2 correlated + * with reset to RetailOS. Use power_on / n31-bt-up after init is up. */ dev_info(dev, - "BCM2078 deferred — echo 1 > power_on; echo 1 > patchram (fw=%s)\n", - bt->fw_present ? BCM_FW_NAME : "missing"); + "BCM2078 companion (GPIO+FM+V4L2) — UART1 owned by hci_bcm\n"); return 0; } @@ -548,13 +630,13 @@ static void bcm2078_remove(struct platform_device *pdev) { struct bcm2078_bt *bt = platform_get_drvdata(pdev); + bcm_radio_unregister(bt); sysfs_remove_groups(&pdev->dev.kobj, bcm_groups); bcm_power_off(bt); } static const struct of_device_id bcm2078_of_match[] = { - { .compatible = "brcm,bcm2078" }, - { .compatible = "brcm,bcm4329-bt" }, + { .compatible = "apple,n31-bcm2078-companion" }, { } }; MODULE_DEVICE_TABLE(of, bcm2078_of_match); @@ -568,26 +650,7 @@ static struct platform_driver bcm2078_driver = { }, }; -static int __init bcm2078_init(void) -{ - struct device_node *np; - - for_each_compatible_node(np, NULL, "brcm,bcm2078") - if (of_device_is_available(np)) - of_platform_device_create(np, NULL, NULL); - for_each_compatible_node(np, NULL, "brcm,bcm4329-bt") - if (of_device_is_available(np)) - of_platform_device_create(np, NULL, NULL); - return platform_driver_register(&bcm2078_driver); -} -module_init(bcm2078_init); - -static void __exit bcm2078_exit(void) -{ - platform_driver_unregister(&bcm2078_driver); -} -module_exit(bcm2078_exit); +module_platform_driver(bcm2078_driver); -MODULE_DESCRIPTION("BCM2078 HCI patchram + FM 0xFC15 (N31)"); +MODULE_DESCRIPTION("N31 BCM2078 GPIO companion + V4L2 FM (0xFC15 via hci0)"); MODULE_LICENSE("GPL"); -MODULE_FIRMWARE(BCM_FW_NAME); diff --git a/drivers/clk/clk-s5l8702.c b/drivers/clk/clk-s5l8702.c index ab6cc6c7e2bdac..7257863b05af12 100755 --- a/drivers/clk/clk-s5l8702.c +++ b/drivers/clk/clk-s5l8702.c @@ -2,13 +2,20 @@ /* * S5L8702 / S5L8740 Clockgates * - * Bring-up policy: ungate documented PWRCON banks so peripherals stay - * alive without a Linux consumer. CCF also marks the published gates - * CLK_IS_CRITICAL | CLK_IGNORE_UNUSED so clk_disable_unused cannot - * write those bits later. + * Bring-up policy (Phases 0–4): ungate-all documented PWRCON banks so + * peripherals stay alive without a Linux consumer. CCF also marks the + * published gates CLK_IS_CRITICAL | CLK_IGNORE_UNUSED so + * clk_disable_unused cannot write those bits later. + * + * Selective IIS/UART/CG16 CCF consumers are DEFERRED to Phase 5 + * (p5-ccf-pm). Rationale: HCI/ALSA/FM glass proof still needs a stable + * clock baseline; early selective gating caused false leads (timer poke, + * SYS remux). Keep absolute CLKCON+0x30 play (0x32190-class) / + * idle (0x1c20) in the IIS drivers until audio/BT prove out. * * Never remux SYS PLL (+0x00/+0x04) — that kills live DRAM. * Never write CLKCON+0x50 — that is the fatal/WDT latch (0xA5). + * Never poke the TIMER MMIO block @0x3C700000 from here. */ #include diff --git a/drivers/crypto/s5l8702-aes.c b/drivers/crypto/s5l8702-aes.c index 75a411b5e5ad8b..adfb411b55bc40 100644 --- a/drivers/crypto/s5l8702-aes.c +++ b/drivers/crypto/s5l8702-aes.c @@ -1,6 +1,10 @@ // SPDX-License-Identifier: GPL-2.0 /* * S5L8702 AES Accelerator Driver + * + * Used on N31 (S5L8740) via compatible "samsung,s5l8702-aes". + * Fused UID/GID CFG follows N31 RetailOS (0x0F enc / 0x0E dec), not + * Rockbox S5L8702 hwkeyaes 0x09/0x08 — see s5l8702_aes_hw_init(). */ #include @@ -71,7 +75,8 @@ #define S5L8702_AES_CMD_CONTINUE 3 #define S5L8702_AES_CFG_KEYSIZE GENMASK(5, 4) -#define S5L8702_AES_CFG_PAUSE GENMASK(2, 1) +/* Bits 2:1 — semantics unproven ("pause" was a guess). N31 fused GID uses them. */ +#define S5L8702_AES_CFG_UNK_2_1 GENMASK(2, 1) #define S5L8702_AES_IRQ_ALL GENMASK(3, 0) #define S5L8702_AES_IRQ_XFR_DONE BIT(0) @@ -259,6 +264,8 @@ static inline int s5l8702_aes_check_fused_key_length(struct crypto_skcipher *tfm return -EINVAL; } + /* Fused keys ignore key bytes; still record length for ctx consistency. */ + ctx->keylen = keylen; return 0; } @@ -310,48 +317,62 @@ static int s5l8702_aes_hw_init(struct s5l8702_aes_ctx *ctx, bool encrypt) } } - // unknown register + /* Unknown register; Rockbox/OSOS write 0 for fused and software paths. */ s5l8702_aes_writel(aes_dev, S5L8702_AES_UNK8C, 0); - // config - cfg = s5l8702_aes_readl(aes_dev, S5L8702_AES_CFG); - - // encrypt/decrypt - if (encrypt) - cfg |= BIT(0); - else - cfg &= ~BIT(0); + /* + * UID/GID fused CFG: N31/S5L8740 RetailOS (OSOS sub_422FFA / nimbus + * 422FFA MMIO) writes exactly (encrypt ? 1 : 0) | 0xE → 0x0F / 0x0E + * (CBC + bits 2:1 + direction; no software key-size field). + * + * Rockbox S5L8702 hwkeyaes() uses 0x09 encrypt / 0x08 decrypt — do + * NOT transplant that encoding onto this N31 driver without separate + * proof; the SoCs are related but CFG fused encoding differs here. + */ + if (hw_key_type == S5L8702_AES_KEY_TYPE_GLOBAL_ID || + hw_key_type == S5L8702_AES_KEY_TYPE_USER_ID) { + cfg = (encrypt ? BIT(0) : 0) | 0xeu; + } else { + /* Software / zero-key ECB/CBC — keep existing RMW path. */ + cfg = s5l8702_aes_readl(aes_dev, S5L8702_AES_CFG); + + if (encrypt) + cfg |= BIT(0); + else + cfg &= ~BIT(0); + + cfg |= S5L8702_AES_CFG_UNK_2_1; - // pause engine - cfg |= S5L8702_AES_CFG_PAUSE; - - // chaining mode - if (ctx->cbc) - cfg |= BIT(3); // CBC - else - cfg &= ~BIT(3); // ECB + if (ctx->cbc) + cfg |= BIT(3); /* CBC */ + else + cfg &= ~BIT(3); /* ECB */ - // key size - cfg &= ~S5L8702_AES_CFG_KEYSIZE; + cfg &= ~S5L8702_AES_CFG_KEYSIZE; - if (hw_key_type == S5L8702_AES_KEY_TYPE_USER_DEFINE) { - switch (ctx->keylen) { + /* Zero-key selector: leave KEYSIZE cleared (same as before). */ + if (hw_key_type == S5L8702_AES_KEY_TYPE_USER_DEFINE) { + switch (ctx->keylen) { case AES_KEYSIZE_128: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_128); + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, + S5L8702_AES_KEY_SIZE_128); break; case AES_KEYSIZE_192: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_192); + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, + S5L8702_AES_KEY_SIZE_192); break; case AES_KEYSIZE_256: - cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, S5L8702_AES_KEY_SIZE_256); + cfg |= FIELD_PREP(S5L8702_AES_CFG_KEYSIZE, + S5L8702_AES_KEY_SIZE_256); break; default: - dev_err(dev, "Invalid key length: %u\n", ctx->keylen); + dev_err(dev, "Invalid key length: %u\n", + ctx->keylen); ret = -EINVAL; goto err_hw; + } } } - // else i.e. for key types UID and GID, key size is set to 0 - nothing to do s5l8702_aes_writel(aes_dev, S5L8702_AES_CFG, cfg); @@ -368,10 +389,15 @@ static int s5l8702_aes_hw_crypt(struct s5l8702_aes_dev *aes_dev, dma_addr_t src, u32 irq; int ret; - // set src/dst buffer addresses and size + /* set src/dst buffer addresses and size */ s5l8702_aes_write_buf(aes_dev, src, dst, len); - // go! + dev_dbg(dev, + "AES pre-START CIPHERKEY_SEL=0x%x COMPLIMENT=0x%x CFG=0x%x len=%u\n", + s5l8702_aes_readl(aes_dev, S5L8702_AES_CIPHERKEY_SEL), + s5l8702_aes_readl(aes_dev, S5L8702_AES_COMPLIMENT), + s5l8702_aes_readl(aes_dev, S5L8702_AES_CFG), len); + s5l8702_aes_writel(aes_dev, S5L8702_AES_COMMAND, S5L8702_AES_CMD_START); // wait for completion @@ -395,17 +421,6 @@ static int s5l8702_aes_hw_crypt(struct s5l8702_aes_dev *aes_dev, dma_addr_t src, return ret; } -static void s5l8702_aes_update_walk_iv(struct skcipher_walk *walk, unsigned int nbytes, bool encrypt) -{ - const u8 *src = walk->src.virt.addr; - const u8 *dst = walk->dst.virt.addr; - - if (encrypt) - memcpy(walk->iv, dst + nbytes - AES_BLOCK_SIZE, AES_BLOCK_SIZE); - else - memcpy(walk->iv, src + nbytes - AES_BLOCK_SIZE, AES_BLOCK_SIZE); -} - static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) { struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req); @@ -430,12 +445,27 @@ static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) while (walk.nbytes) { dma_addr_t src, dst; + u8 next_iv[AES_BLOCK_SIZE]; - // set IV for the current operation if needed + /* set IV for the current operation if needed */ if (ctx->cbc) s5l8702_aes_write_iv(aes_dev, walk.iv); - // map addresses + /* + * CBC decrypt next-IV must be the last *ciphertext* block. + * Save it before DMA: in-place decrypt overwrites src. + */ + if (ctx->cbc && !encrypt) { + if (walk.nbytes < AES_BLOCK_SIZE) { + ret = -EINVAL; + break; + } + memcpy(next_iv, + walk.src.virt.addr + walk.nbytes - AES_BLOCK_SIZE, + AES_BLOCK_SIZE); + } + + /* map addresses */ src = dma_map_single(dev, walk.src.virt.addr, walk.nbytes, DMA_TO_DEVICE); if (dma_mapping_error(dev, src)) { ret = -ENOMEM; @@ -451,18 +481,24 @@ static int s5l8702_aes_crypt(struct skcipher_request *req, bool encrypt) ret = s5l8702_aes_hw_crypt(aes_dev, src, dst, walk.nbytes); - // unmap addresses + /* unmap addresses */ dma_unmap_single(dev, dst, walk.nbytes, DMA_FROM_DEVICE); dma_unmap_single(dev, src, walk.nbytes, DMA_TO_DEVICE); if (ret) break; - // prepare IV for the next operation if needed - if (ctx->cbc) - s5l8702_aes_update_walk_iv(&walk, walk.nbytes, encrypt); + /* prepare IV for the next walk chunk if needed */ + if (ctx->cbc) { + if (encrypt) + memcpy(walk.iv, + walk.dst.virt.addr + walk.nbytes - AES_BLOCK_SIZE, + AES_BLOCK_SIZE); + else + memcpy(walk.iv, next_iv, AES_BLOCK_SIZE); + } - // update remaining bytes and process next chunk + /* update remaining bytes and process next chunk */ ret = skcipher_walk_done(&walk, 0); if (ret) break; diff --git a/drivers/dma/dma-s5l8740-pl080.c b/drivers/dma/dma-s5l8740-pl080.c index c0ae6d3c0a5212..893ea74730319e 100755 --- a/drivers/dma/dma-s5l8740-pl080.c +++ b/drivers/dma/dma-s5l8740-pl080.c @@ -7,16 +7,22 @@ * DT #dma-cells = <2>: * Quirks (PL080 + I²S on S5L8740/N31): * Burst: M2P dest=1 beat (fixed IIS FIFO @+0x10); src≈4 beats (half FIFO). - * Peri: glass IIS0 TX/RX = 10/11 (Rockbox 0xA); OSOS table 12/13 never TCs. + * Peri: RetailOS oracle 2026-08-25 — IIS0 TX **10** (→0x3CA00010), IIS2 RX **13** + * (←0x3D400038); BT A2DP uses UART1 only (no PL080). Glass: peri 12 stuck; + * Rockbox IIS0 TX=0xA. See artifacts/retailos-mmio/README.md. * Cache: PL080 not coherent — dma_sync in start(); no CTL_PROT_CACHE on slave. - * LLI: dma_alloc_coherent, 16-byte aligned chain; misaligned LLI hangs engine. + * LLI: OSOS B424C uses **5×u32 / 20-byte** nodes (src,dst,lli,ctl,count). + * Count lives in LLI[4] and is programmed to CONTROL2 @+0x114. * terminate_all: CFG disable + bounded ENBLD poll — never spin on BUSY (amba-pl08x). * SG: multi-element builds LLI chain; contiguous buffers preferred (CMA). * AHB: M2P src=mem on AHB2 (ahb_s=1), dst=FIFO on AHB1 (ahb_d=0). * FIFO: S3C64xx-style ~64 deep — src burst 8 (m2p_src_burst=2), dst=1. - * PL080S: CONTROL2 @+0x114 holds count (OSOS B424C), not CTL low bits. - * Cache: ARM1176 32-byte lines — LLI/buffer 32-byte aligned; sync in start(). + * PL080S/S5L: CTL @+0x10c, CFG @+0x110, CONTROL2 count @+0x114 (OSOS B424C). + * Not the mainline Samsung map (CONTROL2@+0x10 / CFG@+0x14). + * Cache: ARM1176 32-byte lines — buffer 32-byte aligned; sync in start(). */ +#include +#include #include #include #include @@ -79,20 +85,30 @@ #define CTL_PROT_CACHE BIT(30) /* Glass: peri 12 Active+c2 stuck. peri 10 SRC walks. Rockbox IIS0 TX=0xA. */ -static int force_peri = 10; +static int force_peri = -1; module_param(force_peri, int, 0644); MODULE_PARM_DESC(force_peri, "override DT DMA peri id (-1 = use DT)"); static int force_mem; module_param(force_mem, int, 0644); MODULE_PARM_DESC(force_mem, "1 = M2M flow + soft req, dest still FIFO"); -static int force_flow = -1; +/* + * RetailOS music CFG = 0x28a81 (Active RO bit17 set mid-play → base 0x8a81): + * DstPeri=10, FlowCntrl=1 (M2P DMA), ITC=1, IE=0. + * Earlier misread of Active as Flow=5; keep Flow=1 per PL080-DECODE.md. + */ +static int force_flow = 1; module_param(force_flow, int, 0644); -MODULE_PARM_DESC(force_flow, "PL080 FlowCntrl -1=auto M2P, 0=M2M+soft, 1=M2P, 5=M2P-peri"); +MODULE_PARM_DESC(force_flow, "PL080 FlowCntrl -1=auto M2P, 0=M2M+soft, 1=M2P (RetailOS), 5=M2P-peri"); /* DDI0196 CxControl bits 24/25: 0=AHB1, 1=AHB2. Kitra memcpy uses AHB1. */ -/* M2P: AHB2→memory, AHB1→APB FIFO (Samsung PL080S topology). */ -static int ahb_s = 1; +/* + * RetailOS music-playing CTL = 0x84249000: + * Prot=0, SI=1, width=16, SB=1, DB=1, AHB_S=0, AHB_D=0, TC_IRQ=1. + * Prior Linux defaults (ahb_s=1, SB=8, DB=1, Prot=PRIV|BUFF) yielded + * CTL 0xb5242000 and STATUS stuck in 0x2A0 class vs retail 0x320. + */ +static int ahb_s; module_param(ahb_s, int, 0644); -MODULE_PARM_DESC(ahb_s, "source AHB master (0=AHB1/periph-side, 1=AHB2/mem)"); +MODULE_PARM_DESC(ahb_s, "source AHB master (0=AHB1 RetailOS music, 1=AHB2)"); static int ahb_d; module_param(ahb_d, int, 0644); MODULE_PARM_DESC(ahb_d, "dest AHB master (0=AHB1/periph, 1=AHB2/mem)"); @@ -100,27 +116,38 @@ MODULE_PARM_DESC(ahb_d, "dest AHB master (0=AHB1/periph, 1=AHB2/mem)"); static int xfer_width = 1; module_param(xfer_width, int, 0644); MODULE_PARM_DESC(xfer_width, "PL080 src/dst width 0=8 1=16 2=32"); -/* M2P: dest burst 1; src 8 beats (~half 64-entry IIS FIFO). */ -static int m2p_src_burst = 2; /* enc: 2=8 beats */ +/* RetailOS music SBSIZE/DBSIZE enc = 1 (4-beat? enc1) — CTL 0x84249000. */ +static int m2p_src_burst = 1; module_param(m2p_src_burst, int, 0644); -MODULE_PARM_DESC(m2p_src_burst, "M2P SBSIZE enc (default 2=8 beats)"); -static int m2p_dst_burst; /* 0=1 beat — do not burst into IIS TX FIFO */ +MODULE_PARM_DESC(m2p_src_burst, "M2P SBSIZE enc (default 1 = RetailOS music)"); +static int m2p_dst_burst = 1; module_param(m2p_dst_burst, int, 0644); -MODULE_PARM_DESC(m2p_dst_burst, "M2P DBSIZE enc (default 0=1 beat)"); -static int force_eng = -1; +MODULE_PARM_DESC(m2p_dst_burst, "M2P DBSIZE enc (default 1 = RetailOS music)"); +/* 1=match RetailOS Prot=0 on slave; 0=PRIV|BUFF (old Linux). */ +static int retail_prot = 1; +module_param(retail_prot, int, 0644); +MODULE_PARM_DESC(retail_prot, "1=Prot=0 on M2P/P2M (RetailOS music CTL)"); +static int force_eng = 0; module_param(force_eng, int, 0644); -MODULE_PARM_DESC(force_eng, "PL080 engine 0/1 for xlate (-1 = either)"); +MODULE_PARM_DESC(force_eng, "PL080 engine 0/1 for xlate (-1 = either; default 0 = PL080_0)"); +/* Prefer physical channel (RetailOS music uses ch2). -1 = first free. */ +static int force_ch = 2; +module_param(force_ch, int, 0644); +MODULE_PARM_DESC(force_ch, "prefer PL080 channel id 0..7 (-1=any; default 2=RetailOS)"); +/* OSOS B424C descriptor stride is 20 bytes; keep pool 32-byte aligned. */ #define PL080_LLI_ALIGN 32 #define PL080_TERM_POLL_US 10 #define PL080_TERM_POLL_MAX 10 +#define PL080S_XFER_COUNT_MASK 0x1fffffffu struct pl080_lli { __le32 src; __le32 dst; __le32 lli; __le32 ctrl; -} __aligned(PL080_LLI_ALIGN); + __le32 ctrl2; /* transfer count → CONTROL2 (OSOS v27) */ +}; static size_t s5l_pl080_lli_size(unsigned int nlli) { @@ -132,21 +159,8 @@ static dma_addr_t s5l_pl080_lli_pa(dma_addr_t base, unsigned int idx) return base + idx * sizeof(struct pl080_lli); } -static struct pl080_lli *s5l_pl080_lli_alloc(struct device *dev, - unsigned int nlli, - dma_addr_t *phys) -{ - size_t bytes = s5l_pl080_lli_size(nlli); - struct pl080_lli *lli; +#define PL080_LLI_POOL_NODES 64 - lli = dma_alloc_coherent(dev, bytes, phys, GFP_NOWAIT); - if (!lli) - return NULL; - if (*phys & (PL080_LLI_ALIGN - 1)) - dev_warn(dev, "LLI phys misaligned pa=%pad (need %u)\n", - &*phys, PL080_LLI_ALIGN); - return lli; -} struct s5l_pl080_chan { struct virt_dma_chan vc; struct s5l_pl080 *host; @@ -165,16 +179,23 @@ struct s5l_pl080_desc { struct pl080_lli *lli; dma_addr_t lli_phys; unsigned int nlli; + unsigned int lli_off; + bool lli_from_pool; u32 cfg; bool cyclic; dma_addr_t buf_addr; size_t buf_len; + size_t period_len; + unsigned int periods; + unsigned int periods_done; }; struct s5l_pl080; struct dma_chan *s5l_pl080_request_slave(struct device *consumer, unsigned int idx); +struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri); +int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en); struct s5l_pl080 { struct device *dev; @@ -185,9 +206,82 @@ struct s5l_pl080 { spinlock_t lock; void *dummy_cpu; dma_addr_t dummy_dma; + struct pl080_lli *lli_pool; + dma_addr_t lli_pool_phys; + DECLARE_BITMAP(lli_busy, PL080_LLI_POOL_NODES); struct task_struct *pump; }; +static struct pl080_lli *s5l_pl080_lli_alloc(struct s5l_pl080 *pl, + unsigned int nlli, + dma_addr_t *phys, + unsigned int *off, + bool *from_pool) +{ + unsigned long flags; + unsigned int i, j; + struct pl080_lli *lli; + + if (!pl || !nlli || !phys || !off || !from_pool) + return NULL; + + if (pl->lli_pool && nlli <= PL080_LLI_POOL_NODES) { + spin_lock_irqsave(&pl->lock, flags); + for (i = 0; i + nlli <= PL080_LLI_POOL_NODES; i++) { + for (j = 0; j < nlli; j++) { + if (test_bit(i + j, pl->lli_busy)) + break; + } + if (j != nlli) + continue; + for (j = 0; j < nlli; j++) + set_bit(i + j, pl->lli_busy); + *phys = pl->lli_pool_phys + + i * sizeof(struct pl080_lli); + *off = i; + *from_pool = true; + spin_unlock_irqrestore(&pl->lock, flags); + return pl->lli_pool + i; + } + spin_unlock_irqrestore(&pl->lock, flags); + } + + lli = dma_alloc_coherent(pl->dev, s5l_pl080_lli_size(nlli), phys, + GFP_NOWAIT); + if (!lli) { + dev_warn_ratelimited(pl->dev, + "LLI alloc nlli=%u ENOMEM\n", nlli); + return NULL; + } + *off = 0; + *from_pool = false; + return lli; +} + +static void s5l_pl080_lli_release(struct s5l_pl080 *pl, struct s5l_pl080_desc *d) +{ + unsigned long flags; + unsigned int j; + + if (!pl || !d || !d->lli) + return; + if (d->lli_from_pool) { + spin_lock_irqsave(&pl->lock, flags); + for (j = 0; j < d->nlli && d->lli_off + j < PL080_LLI_POOL_NODES; + j++) + clear_bit(d->lli_off + j, pl->lli_busy); + spin_unlock_irqrestore(&pl->lock, flags); + } else if (!irqs_disabled() && !in_atomic()) { + dma_free_coherent(pl->dev, s5l_pl080_lli_size(d->nlli), + d->lli, d->lli_phys); + } else { + dev_warn_ratelimited(pl->dev, + "LLI leak nlli=%u (atomic free)\n", + d->nlli); + } + d->lli = NULL; +} + static int s5l_pl080_need_soft(void) { /* Flow 0/4: M2M or M2P under DMA control — drive with SOFT_BREQ. */ @@ -260,6 +354,11 @@ static unsigned int s5l_pl080_unit(void) return 1u << w; } +/* + * CTL template only — transfer count is NOT in CTL[11:0] on this SoC. + * OSOS B424C / RetailOS music CTL (e.g. 0x84249000) keep size bits clear; + * count is written to CONTROL2 and LLI ctrl2. + */ static u32 s5l_pl080_build_ctl(struct s5l_pl080_chan *ch, u32 words, bool src_inc, bool dst_inc, bool irq) { @@ -267,19 +366,20 @@ static u32 s5l_pl080_build_ctl(struct s5l_pl080_chan *ch, u32 words, unsigned int sb, db; u32 ctl; + (void)words; if (w > 2) w = 1; if (ch && (ch->dir == DMA_MEM_TO_DEV || ch->dir == DMA_DEV_TO_MEM)) { sb = ch->src_burst; db = ch->dst_burst; - ctl = CTL_PROT_PRIV | CTL_PROT_BUFF; + ctl = retail_prot ? 0 : (CTL_PROT_PRIV | CTL_PROT_BUFF); } else { /* M2M selftest / memcpy: Rockbox pcm-s5l8702 8/4 */ sb = 2; db = 1; ctl = CTL_PROT_PRIV | CTL_PROT_BUFF | CTL_PROT_CACHE; } - ctl |= words | (w << CTL_WIDTH_SHIFT) | (w << (CTL_WIDTH_SHIFT + 3)) | + ctl |= (w << CTL_WIDTH_SHIFT) | (w << (CTL_WIDTH_SHIFT + 3)) | (sb << CTL_SBSIZE_SHIFT) | (db << CTL_DBSIZE_SHIFT); if (ahb_s) ctl |= BIT(24); @@ -345,11 +445,11 @@ static void s5l_pl080_start(struct s5l_pl080_chan *ch, struct s5l_pl080_desc *d) s5l_pl080_chan_disable(ch); writel(le32_to_cpu(first->src), b + PL080_Cx_SRC(id)); writel(le32_to_cpu(first->dst), b + PL080_Cx_DST(id)); - /* Next LLI, not the first (already loaded into SRC/DST/CTL). */ + /* Next LLI, not the first (already loaded into SRC/DST/CTL/C2). */ writel(le32_to_cpu(first->lli), b + PL080_Cx_LLI(id)); writel(le32_to_cpu(first->ctrl), b + PL080_Cx_CTL(id)); - /* B424C: CONTROL2 = transfer count (v27 & 0x1FFFFFFF), not CTL. */ - writel(le32_to_cpu(first->ctrl) & 0x1fffffffu, + /* B424C: *v25 = v27 & 0x1FFFFFFF — count only, never the CTL word. */ + writel(le32_to_cpu(first->ctrl2) & PL080S_XFER_COUNT_MASK, b + PL080S_Cx_CONTROL2(id)); writel(d->cfg | CFG_ENABLE, b + PL080_Cx_CFG(id)); /* M2M / force_flow 0|4: software request. M2P peri waits for IIS DRQ. */ @@ -359,11 +459,11 @@ static void s5l_pl080_start(struct s5l_pl080_chan *ch, struct s5l_pl080_desc *d) writel(BIT(id), b + PL080_SOFT_SREQ); } ch->running = d; - dev_info(ch->host->dev, - "ch%u start peri=%u cfg=0x%x nlli=%u src=0x%x dst=0x%x ctl=0x%x\n", + dev_dbg(ch->host->dev, + "ch%u start peri=%u cfg=0x%x nlli=%u src=0x%x dst=0x%x ctl=0x%x c2=0x%x\n", ch->id, ch->peri, (u32)(d->cfg | CFG_ENABLE), d->nlli, le32_to_cpu(first->src), le32_to_cpu(first->dst), - le32_to_cpu(first->ctrl)); + le32_to_cpu(first->ctrl), le32_to_cpu(first->ctrl2)); } static void s5l_pl080_issue(struct dma_chan *c) @@ -399,16 +499,23 @@ static enum dma_status s5l_pl080_tx_status(struct dma_chan *c, spin_lock_irqsave(&ch->vc.lock, flags); d = ch->running; if (d && d->buf_len) { - u8 id = ch->id % PL080_CH_COUNT; - - cur = readl(ch->base + ((ch->dir == DMA_DEV_TO_MEM) ? - PL080_Cx_DST(id) : PL080_Cx_SRC(id))); - start = lower_32_bits(d->buf_addr); - end = start + d->buf_len; - if (cur >= start && cur < end) - state->residue = end - cur; - else - state->residue = d->buf_len; + if (d->cyclic && d->period_len) { + size_t pos = (size_t)d->periods_done * d->period_len; + + pos %= d->buf_len; + state->residue = d->buf_len - pos; + } else { + u8 id = ch->id % PL080_CH_COUNT; + + cur = readl(ch->base + ((ch->dir == DMA_DEV_TO_MEM) ? + PL080_Cx_DST(id) : PL080_Cx_SRC(id))); + start = lower_32_bits(d->buf_addr); + end = start + d->buf_len; + if (cur >= start && cur < end) + state->residue = end - cur; + else + state->residue = d->buf_len; + } } spin_unlock_irqrestore(&ch->vc.lock, flags); return st; @@ -429,9 +536,7 @@ static void s5l_pl080_desc_free(struct virt_dma_desc *vd) struct s5l_pl080_desc *d = to_s5l_desc(vd); struct s5l_pl080_chan *ch = to_s5l_chan(vd->tx.chan); - if (d->lli && !irqs_disabled() && !in_atomic()) - dma_free_coherent(ch->host->dev, s5l_pl080_lli_size(d->nlli), - d->lli, d->lli_phys); + s5l_pl080_lli_release(ch->host, d); kfree(d); } @@ -466,7 +571,8 @@ s5l_pl080_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl, if (!d) return NULL; - lli = s5l_pl080_lli_alloc(ch->host->dev, nlli, &lli_phys); + lli = s5l_pl080_lli_alloc(ch->host, nlli, &lli_phys, &d->lli_off, + &d->lli_from_pool); if (!lli) { kfree(d); return NULL; @@ -475,7 +581,10 @@ s5l_pl080_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl, cfg = 0; dev_addr = ch->fifo_addr; if (force_flow >= 0) { - cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_IE | CFG_ITC; + /* Retail music Flow=1 → CFG 0x8a81 (ITC only; Active RO adds 0x20000). */ + cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_ITC; + if (force_flow != 1 && force_flow != 5) + cfg |= CFG_IE; if (dir == DMA_MEM_TO_DEV) cfg |= (ch->peri & 0x1f) << CFG_DST_PERI_SHIFT; else @@ -528,6 +637,8 @@ s5l_pl080_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl, dir == DMA_MEM_TO_DEV, dir == DMA_DEV_TO_MEM, idx == nlli - 1)); + lli[idx].ctrl2 = cpu_to_le32(words & + PL080S_XFER_COUNT_MASK); if (idx < nlli - 1) lli[idx].lli = cpu_to_le32( @@ -583,7 +694,8 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, if (!d) return NULL; - lli = s5l_pl080_lli_alloc(ch->host->dev, nlli, &lli_phys); + lli = s5l_pl080_lli_alloc(ch->host, nlli, &lli_phys, &d->lli_off, + &d->lli_from_pool); if (!lli) { kfree(d); return NULL; @@ -592,7 +704,10 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, cfg = 0; dev_addr = ch->fifo_addr; if (force_flow >= 0) { - cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_IE | CFG_ITC; + /* Retail music Flow=1 → CFG 0x8a81 (ITC only; Active RO adds 0x20000). */ + cfg |= ((force_flow & 7) << CFG_FLOW_SHIFT) | CFG_ITC; + if (force_flow != 1 && force_flow != 5) + cfg |= CFG_IE; if (dir == DMA_MEM_TO_DEV) cfg |= (ch->peri & 0x1f) << CFG_DST_PERI_SHIFT; else @@ -645,6 +760,8 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, dir == DMA_MEM_TO_DEV, dir == DMA_DEV_TO_MEM, period_last)); + lli[idx].ctrl2 = cpu_to_le32(words & + PL080S_XFER_COUNT_MASK); if (idx + 1 < nlli) lli[idx].lli = cpu_to_le32(lower_32_bits( @@ -668,6 +785,9 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, d->cyclic = true; d->buf_addr = buf_addr; d->buf_len = buf_len; + d->period_len = period_len; + d->periods = periods; + d->periods_done = 0; dev_info(ch->host->dev, "cyclic ok peri=%u nlli=%u periods=%u period=%zu fifo=0x%x\n", ch->peri, nlli, periods, period_len, @@ -682,12 +802,19 @@ static int s5l_pl080_config(struct dma_chan *c, if (cfg->direction == DMA_MEM_TO_DEV) { ch->fifo_addr = cfg->dst_addr; - ch->src_burst = cfg->src_maxburst ? - s5l_pl080_burst_enc(cfg->src_maxburst) : - clamp(m2p_src_burst, 0, 7); - ch->dst_burst = cfg->dst_maxburst ? - s5l_pl080_burst_enc(cfg->dst_maxburst) : - clamp(m2p_dst_burst, 0, 7); + /* + * ALSA/dma_tone often pass maxburst=1. burst_enc(1)=0, but + * RetailOS music CTL 0x84249000 needs SB/DB enc=1. Prefer + * module params (oracle) over a 1-beat slave hint. + */ + if (cfg->src_maxburst > 1) + ch->src_burst = s5l_pl080_burst_enc(cfg->src_maxburst); + else + ch->src_burst = clamp(m2p_src_burst, 0, 7); + if (cfg->dst_maxburst > 1) + ch->dst_burst = s5l_pl080_burst_enc(cfg->dst_maxburst); + else + ch->dst_burst = clamp(m2p_dst_burst, 0, 7); } else { ch->fifo_addr = cfg->src_addr; ch->src_burst = cfg->src_maxburst ? @@ -706,8 +833,8 @@ static int s5l_pl080_terminate(struct dma_chan *c) unsigned long flags; struct virt_dma_desc *vd; - dev_info(ch->host->dev, - "term ch%u en=0x%x src=0x%x dst=0x%x lli=0x%x ctl=0x%x cfg=0x%x rawtc=0x%x rawerr=0x%x\n", + dev_dbg(ch->host->dev, + "term ch%u en=0x%x src=0x%x dst=0x%x lli=0x%x ctl=0x%x cfg=0x%x rawtc=0x%x rawerr=0x%x\n", ch->id, readl(ch->base + PL080_ENBLD_CHNS), readl(ch->base + PL080_Cx_SRC(id)), readl(ch->base + PL080_Cx_DST(id)), @@ -746,9 +873,8 @@ static irqreturn_t s5l_pl080_irq(int irq, void *data) tc = readl(b + PL080_INT_TC_STATUS); err = readl(b + PL080_INT_ERR_STATUS); if (tc || err) - dev_info_ratelimited(pl->dev, - "irq eng%u tc=0x%x err=0x%x\n", - eng, tc, err); + dev_dbg(pl->dev, "irq eng%u tc=0x%x err=0x%x\n", + eng, tc, err); if (tc) writel(tc, b + PL080_INT_TC_CLEAR); if (err) @@ -765,6 +891,9 @@ static irqreturn_t s5l_pl080_irq(int irq, void *data) spin_lock_irqsave(&ch->vc.lock, flags); d = ch->running; if (d && d->cyclic) { + d->periods_done++; + if (d->periods) + d->periods_done %= d->periods; vchan_cyclic_callback(&d->vd); spin_unlock_irqrestore(&ch->vc.lock, flags); @@ -807,6 +936,27 @@ static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, eng_lo = 0; eng_hi = PL080_CH_COUNT * 2; } + /* + * RetailOS music: peri 10 on physical ch2 (EnbldChns=0x4). Prefer it. + * (ASoC often already holds ch2 — dma_tone must reuse via lookup_peri, + * not allocate a second peri-10 channel on ch3.) + */ + if (force_ch >= 0 && force_ch < PL080_CH_COUNT && peri == 10) { + unsigned int prefer = eng_lo + force_ch; + + if (prefer < eng_hi) { + ch = &pl->chans[prefer]; + if (ch->base && !ch->vc.chan.client_count) { + ch->peri = peri; + ch->src_burst = clamp(m2p_src_burst, 0, 7); + ch->dst_burst = clamp(m2p_dst_burst, 0, 7); + dev_info(pl->dev, + "xlate DT peri=%u -> ch%u (forced) peri=%u\n", + spec->args[0] & 0x1f, prefer, ch->peri); + return dma_get_slave_channel(&ch->vc.chan); + } + } + } for (i = eng_lo; i < eng_hi; i++) { ch = &pl->chans[i]; if (!ch->base || ch->vc.chan.client_count) @@ -860,6 +1010,70 @@ struct dma_chan *s5l_pl080_request_slave(struct device *consumer, } EXPORT_SYMBOL_GPL(s5l_pl080_request_slave); +/* + * Return an already-owned channel for peri (no client_count bump). + * dma_tone uses this so it rides RetailOS ch2 held by ASoC instead of + * allocating a second peri-10 channel. + */ +struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri) +{ + struct device_node *np; + struct platform_device *pdev; + struct s5l_pl080 *pl; + unsigned int i; + struct dma_chan *found = NULL; + + np = of_find_compatible_node(NULL, NULL, "apple,s5l8740-pl080"); + if (!np) + np = of_find_compatible_node(NULL, NULL, "arm,pl080"); + if (!np) + return NULL; + pdev = of_find_device_by_node(np); + of_node_put(np); + if (!pdev) + return NULL; + pl = platform_get_drvdata(pdev); + if (!pl) { + put_device(&pdev->dev); + return NULL; + } + peri &= 0x1f; + for (i = 0; i < PL080_CH_COUNT * 2; i++) { + struct s5l_pl080_chan *ch = &pl->chans[i]; + + if (!ch->base || ch->peri != peri || !ch->vc.chan.client_count) + continue; + found = &ch->vc.chan; + dev_dbg(pl->dev, "lookup peri=%u -> ch%u clients=%u\n", + peri, ch->id, ch->vc.chan.client_count); + break; + } + put_device(&pdev->dev); + return found; +} +EXPORT_SYMBOL_GPL(s5l_pl080_lookup_peri); + +int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en) +{ + struct dma_chan *chan; + struct s5l_pl080_chan *ch; + u8 id; + + chan = s5l_pl080_lookup_peri(peri); + if (!chan) + return -ENODEV; + ch = to_s5l_chan(chan); + id = ch->id % PL080_CH_COUNT; + if (src) + *src = readl(ch->base + PL080_Cx_SRC(id)); + if (dst) + *dst = readl(ch->base + PL080_Cx_DST(id)); + if (en) + *en = readl(ch->base + PL080_ENBLD_CHNS); + return 0; +} +EXPORT_SYMBOL_GPL(s5l_pl080_peri_snapshot); + static ssize_t chregs_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -1057,6 +1271,15 @@ static int s5l_pl080_probe(struct platform_device *pdev) if (!pl->dummy_cpu) dev_warn(dev, "dummy DMA sink alloc failed\n"); + pl->lli_pool = dmam_alloc_coherent(dev, + s5l_pl080_lli_size(PL080_LLI_POOL_NODES), + &pl->lli_pool_phys, GFP_KERNEL); + if (!pl->lli_pool) + dev_warn(dev, "LLI pool alloc failed — GFP_NOWAIT fallback only\n"); + else + dev_info(dev, "LLI pool %u nodes pa=%pad\n", + PL080_LLI_POOL_NODES, &pl->lli_pool_phys); + pl->pump = kthread_run(s5l_pl080_pump, pl, "n31-pl080-pump"); if (IS_ERR(pl->pump)) { dev_warn(dev, "soft-req pump: %ld\n", PTR_ERR(pl->pump)); @@ -1068,7 +1291,7 @@ static int s5l_pl080_probe(struct platform_device *pdev) if (ret) dev_warn(dev, "chregs sysfs: %d\n", ret); dev_info(dev, - "PL080 dmaengine @%pR id=%02x peri IIS0=10/11 (glass) OSOS=12/13\n", + "PL080 dmaengine @%pR id=%02x peri IIS0=10/11 IIS2 RX=13 (RetailOS)\n", platform_get_resource(pdev, IORESOURCE_MEM, 0), id0); return 0; } diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index a078bffa76c750..37a6a649ccf6db 100755 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -413,9 +413,9 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) if (!sleep) { if (gpio_dev->last_sleep) { - dev_dbg(&client->dev, - "n31-btn SLEEP PRESS r7=0x%02x (bit5 1->0)\n", - r7); + dev_info(&client->dev, + "n31-btn SLEEP PRESS r7=0x%02x (bit5 1->0)\n", + r7); s5l8740_n31_report_key(KEY_POWER, 1); if (gpio_dev->input) { input_report_key(gpio_dev->input, KEY_POWER, 1); @@ -423,13 +423,20 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) } } /* Hold Sleep across 5 polls (~500ms) before cutting power. - * One noisy I2C byte must not hibernate. */ + * Short press still emits KEY_POWER for power-watch / + * pm_power_off; hold is the kernel-direct fallback when + * userspace is not watching. One noisy I2C byte must not + * hibernate. */ if (gpio_dev->sleep_hold < 5) gpio_dev->sleep_hold++; if (gpio_dev->sleep_hold == 5) { dev_warn(&client->dev, "n31-btn SLEEP held — poweroff\n"); - d1830_cut_power(client); + /* Prefer machine pm_power_off (same cut_power). */ + if (pm_power_off) + pm_power_off(); + else + d1830_cut_power(client); } } else { if (!gpio_dev->last_sleep) { diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c index d4f4ccb5d2f793..590081d837f35b 100644 --- a/drivers/gpio/gpio-s5l8740.c +++ b/drivers/gpio/gpio-s5l8740.c @@ -335,6 +335,59 @@ static void s5l8740_sec_gpio86(struct s5l8740_gpio *sg) s5l8740_log_pinmux_map(sg, "after-SEC-86-iis0"); } +/* SEC sub_223C IIS0 ASP pins: func2 + DIR (table @ 0x22004C6C). */ +static void s5l8740_iis0_pinmux_sec(struct s5l8740_gpio *sg) +{ + s5l8740_pinmux_apply_word(sg->base, 0x00061002u); /* GPIO6 BCLK? */ + s5l8740_pinmux_apply_word(sg->base, 0x00071002u); /* GPIO7 */ + s5l8740_pinmux_apply_word(sg->base, 0x02041002u); /* GPIO20 */ +} + +/* + * OSOS BCB60 IIS0 pad enable. mode 3=on, 2=off (BCB60 teardown). + * Re-applies SEC func2 on 6/7/20 then GPIOCMD. + */ +void s5l8740_iis0_pads_enable(unsigned int mode) +{ + struct s5l8740_gpio *sg = s5l8740_n31; + u16 m = mode ? mode : 3; + + if (!sg) + return; + s5l8740_iis0_pinmux_sec(sg); + s5l8740_gpiocmd_mode(sg, 20, m, 0); + s5l8740_gpiocmd_mode(sg, 7, m, 0); +} +EXPORT_SYMBOL_GPL(s5l8740_iis0_pads_enable); + +void s5l8740_iis0_pads_disable(void) +{ + s5l8740_iis0_pads_enable(2); +} +EXPORT_SYMBOL_GPL(s5l8740_iis0_pads_disable); + +void s5l8740_iis0_pad6_enable(unsigned int mode) +{ + struct s5l8740_gpio *sg = s5l8740_n31; + u16 m = mode ? mode : 3; + + if (!sg) + return; + s5l8740_pinmux_apply_word(sg->base, 0x00061002u); + s5l8740_gpiocmd_mode(sg, 6, m, 0); +} +EXPORT_SYMBOL_GPL(s5l8740_iis0_pad6_enable); + +void s5l8740_gpio_log_iis0_pads(const char *tag) +{ + struct s5l8740_gpio *sg = s5l8740_n31; + + if (!sg) + return; + s5l8740_log_pinmux_map(sg, tag ? tag : "iis0"); +} +EXPORT_SYMBOL_GPL(s5l8740_gpio_log_iis0_pads); + int s5l8740_n31_din86(void) { if (!s5l8740_n31) diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c index 1f92893c4c66b6..e2f85488a29fc6 100755 --- a/drivers/input/touchscreen/apple-nimbus.c +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -7,24 +7,49 @@ * teardown sub_1A878: IRQ off, RST, 20690(0), rail off, EN mode 1 * (13A20 retries: 1A878 + sleep 50 + 1A5AC, max 3) * grape.bin IS the app. SEC bootloader has no grape/Nimbus path. - * bootload cmd sub_20848(6593) = 19 C1 + (18 E1)* - * FW load 1A640 204E0: ARM at +0x400, size le32(+0x0c) - * rev 3: 422FFA GID-CBC IV=0 decrypt in place - * 273A0: 2D640(ARM) → 2D7A4(IsyS/cal +350 @ 0x400200) → - * 2D5B0 (34AD0 + poke 0x011F RequestCal) → 2D54C - * 2D7A4 window is this unit's NVRAM cal. sub_564 copies - * 1376B IsyS from A34(0x18)=0x2202FE18 into BSS; 43CFB4 - * returns that object. DFU Linux never ran sub_564; the - * 0x22xxxxxx window is Grape-internal. Use grape.bin +350. - * chunk pack via 35C1C→3B9D0 (18 E1 / 30 01 / …) - * status poll sub_3D5706: TX 1A A1 → rev16 status - * ping sub_182590 type 490 - * read sub_17E404 EA 01 01 - * report sub_187AB4 type 0x44 → MT-B - * 1703E8 10 failed 188FFC → 13A20(0) + 13A20(1) + * 1A5AC delays 2075A(1)+5 → 20766(1)+15 → 20690(1)+5 → 11B70 → + * 20848 +15 → 2075A(0)+30 → 20E94 (no extra POR pulse) + * bootload cmd sub_20848(6593): 6593 = 0x19C1 = HBPP ENTER (not a + * firmware byte count). TX 19 C1 + (18 E1)* pad. * - * Firmware: request_firmware("apple/grape-nimbus.bin") — optional; without - * it we still bootload+ping (chip may already be programmed). + * Firmware (SPI → controller @ dest=offset, start 0) — 1A640 / 204E0 / 2D640: + * Full "8740" GrapeFirmware-style container on disk: + * 0x000..0x3ff Apple/N31 header (NOT sent over SPI) + * 0x400.. ARM app body; length = le32(file+0x0c) + * rev 3: GID-CBC IV=0 decrypt of ARM body before send + * ARM-only cut (no 8740 magic, e.g. 18 F0 9F E5…): whole file = body + * Chunks: max 0x1FF0. Upload = sub_3B9D0 envelope (see below). ACK 0x4BC1. + * Callsite 2D640: r1 = firmware offset (NOT 0x00100000). EXEC 0x00100018 + * is the bootloader-mapped app PC, not the upload destination. + * + * Calibration (SPI → controller @ 0x00400200) — 2D7A4 / 273A0: + * Per-device IsyS comes from the A34 handoff, not a host file: + * desc @ 0x2202FE18 (sub_A34(24)): magic 0x53797349 "IsyS", ptr @ +4 + * memcpy 0x560 from ptr (sub_564) + * cal = bytes at decimal +350, length 0x200, reverse each u32 (sub_273A0) + * then 2D7A4 that window; DATA packet still does b1b0b3b2 wire swizzle + * Callsite 2D7A4: r1 = 0x00400200 + offset. + * No grape-nimbus-cal.bin, no FTL IsyS scan, no GrapeFirmware.bin+350. + * Preferred source: U-Boot copy in reserved DRAM, advertised in /chosen + * apple,n31-isys-addr / apple,n31-isys-size. Never consume the original + * A34 pointer. Live A34 ioremap is fallback only. + * + * sub_3B9D0 upload frame (full SPI length = payload_len + 16): + * [0..1] 18 E1 + * [2..3] 30 01 + * [4..5] word_count = len>>2 as hi,lo (len>>10, len>>2) + * [6..9] dest swizzled BYTE1,BYTE0,BYTE3,BYTE2 + * [10..11] u16 byte-sum of [4..9], big-endian + * [12..12+len) payload u32s swizzled B1 B0 B3 B2 + * [12+len..] u32 byte-sum of swizzled payload, stored B1 B0 B3 B2 + * First FW prefix (dest=0,len=0x1FF0): 18 E1 30 01 07 FC 00 00 00 00 01 03 + * Cal prefix (dest=0x400200,len=0x200): 18 E1 30 01 00 80 02 00 00 40 00 C2 + * + * After cal: 2D5B0 RequestCal → 2D54C EXEC → 40 ms → runtime ping. + * Register input only after runtime ping. runtime_ready = ping csum. + * + * Firmware host file: request_firmware("apple/grape-nimbus.bin") and/or + * FTL gpfw/8740 when fw_prefer_ftl=1. That is the ARM app, not cal. */ #include #include @@ -51,6 +76,9 @@ #include #include #include +#include +#include +#include #include #define NIMBUS_MAGIC 0xEA @@ -65,13 +93,19 @@ #define NIMBUS_SCALE_Y_DIV 0x1482 #define NIMBUS_CHUNK_MAX 0x1FF0 /* 8176 — sub_2D640 */ -#define NIMBUS_HDR_LEN 16 +#define NIMBUS_HDR_LEN 16 /* 2 outer + 10 body hdr + 4 payload sum */ +#define NIMBUS_CAL_DEST 0x00400200u /* 2D7A4 literal 0x400200 */ #define NIMBUS_FW_HDR_OFF 350 #define NIMBUS_FW_HDR_LEN 0x200 #define NIMBUS_ARM_OFFICIAL 0xe970 /* 8740 le32(+0x0c); 204E0 2D640 size */ #define NIMBUS_ISYS_MAGIC 0x53797349u /* 'IsyS' — sub_564 */ #define NIMBUS_ISYS_LEN 0x560 #define NIMBUS_A34_BASE 0x2202fe00UL /* sub_A34(idx) = 0x2202FE00+idx */ +#define NIMBUS_A34_ISYS_DESC (NIMBUS_A34_BASE + 0x18) /* sub_A34(24) */ + +/* Whimory FTL (fmss-s5l8740.ko) — optional cal/FW from device NAND */ +#define NIMBUS_FTL_SECTOR_SIZE 4096U +#define NIMBUS_GPFW_TAG 0x67706677u /* 'gpfw' LE */ #define NIMBUS_ACK_CHUNK 0x4BC1 /* 19393 */ #define NIMBUS_ACK_34AD0 0x4AD1 /* 19153 */ @@ -109,18 +143,28 @@ static int spi_clkdiv = 16; module_param(spi_clkdiv, int, 0644); MODULE_PARM_DESC(spi_clkdiv, "SPI2 CLKDIV (higher=slower; try 8-32 for FW download)"); -static int reset_hold_ms = 10; +static int reset_hold_ms = 5; module_param(reset_hold_ms, int, 0644); -MODULE_PARM_DESC(reset_hold_ms, "RST low ms before bootload"); -static int reset_release_ms = 100; +MODULE_PARM_DESC(reset_hold_ms, "RST low ms in optional extra_por_pulse (1A5AC uses 5)"); +/* 1A5AC: 2075A(0) then sleep 30 before 20E94 — not FAMILY 15 / old 100. */ +static int reset_release_ms = 30; module_param(reset_release_ms, int, 0644); -MODULE_PARM_DESC(reset_release_ms, "ms after RST release before SPI FW"); +MODULE_PARM_DESC(reset_release_ms, "ms after RST release before probe/FW (1A5AC=30)"); +static int extra_por_pulse; +module_param(extra_por_pulse, int, 0644); +MODULE_PARM_DESC(extra_por_pulse, "1=extra RST low/high/low before 1A5AC (not in RetailOS)"); static int go_spi_setup; module_param(go_spi_setup, int, 0644); MODULE_PARM_DESC(go_spi_setup, "SPI2 SETUP override for 2D54C GO (0=11B70)"); +/* 0=8-bit PIO (RetailOS HBPP default), 1=u16 TXDATA pairs, 2=spi_sync */ +static int go_xfer; +module_param(go_xfer, int, 0644); +MODULE_PARM_DESC(go_xfer, + "2D54C EXEC xfer: 0=8-bit burst 1=u16 burst 2=spi_sync"); +/* Default 0: N31 RetailOS path has no Z2/5A5A host container. */ static int prepend_z2_hdr; module_param(prepend_z2_hdr, int, 0644); -MODULE_PARM_DESC(prepend_z2_hdr, "0=none 1=5A5A+BE len+CRC32 2=c3f5 hdr"); +MODULE_PARM_DESC(prepend_z2_hdr, "0=none (N31 default) 1=5A5A+BE len+CRC32 2=c3f5 hdr"); static int chunk_spi; module_param(chunk_spi, int, 0644); MODULE_PARM_DESC(chunk_spi, "1=spi_sync chunk xfers (apple_z2-style atomic CS)"); @@ -131,6 +175,65 @@ static int skip_download; module_param(skip_download, int, 0644); MODULE_PARM_DESC(skip_download, "1=bootload+ping only, no FW chunks"); +static int force_gid; +module_param(force_gid, int, 0644); +MODULE_PARM_DESC(force_gid, "1=422FFA decrypt attempt even without 8740 rev3 hdr"); + +static int cal_try_dt = 1; +module_param(cal_try_dt, int, 0644); +MODULE_PARM_DESC(cal_try_dt, + "Read U-Boot IsyS copy from /chosen apple,n31-isys-* (default on)"); + +static int cal_try_a34 = 1; +module_param(cal_try_a34, int, 0644); +MODULE_PARM_DESC(cal_try_a34, + "Fallback: read live A34 descriptor at 0x2202FE18 (default on)"); + +static unsigned int exec_wait_ms = 40; +module_param(exec_wait_ms, uint, 0644); +MODULE_PARM_DESC(exec_wait_ms, + "ms after EXEC before runtime ping (OSOS 2D54C success wait = 40)"); + +static unsigned int cal_ftl_start; +module_param(cal_ftl_start, uint, 0644); +MODULE_PARM_DESC(cal_ftl_start, "FTL LBA to start gpfw/8740 firmware scan (not cal)"); + +static unsigned int cal_ftl_count = 4096; +module_param(cal_ftl_count, uint, 0644); +MODULE_PARM_DESC(cal_ftl_count, + "FTL LBAs to scan for gpfw/8740 firmware (not IsyS cal)"); + +static int fw_prefer_ftl; +module_param(fw_prefer_ftl, int, 0644); +MODULE_PARM_DESC(fw_prefer_ftl, + "1=try gpfw/8740 from FTL before grape-nimbus.bin (DFU default 0)"); + +static int fw_allow_file = 1; +module_param(fw_allow_file, int, 0644); +MODULE_PARM_DESC(fw_allow_file, "1=allow apple/grape-nimbus.bin fallback"); + +/* + * 2D640 r1 = firmware offset (start 0). Do NOT default to 0x00100000 — + * that is the EXEC-mapped app window, not the upload dest. Override only + * for deliberate A/B experiments. + */ +static unsigned int fw_dest; +module_param(fw_dest, uint, 0644); +MODULE_PARM_DESC(fw_dest, + "2D640 ARM upload base dest (OSOS offset 0; cal stays 0x400200)"); + +static unsigned int exec_addr = 0x00100018; +module_param(exec_addr, uint, 0644); +MODULE_PARM_DESC(exec_addr, + "2D54C EXEC word0 (OSOS 0x00100018; bootloader-mapped PC)"); +static unsigned int exec_word1 = 0x00000100; +module_param(exec_word1, uint, 0644); +MODULE_PARM_DESC(exec_word1, "2D54C EXEC word1 (OSOS 0x00000100)"); + +/* fmss-s5l8740.ko exports (optional link). */ +bool fmss_ftl_present(void); +int fmss_ftl_read_sector(u64 logical_sector, void *buf); + static bool nimbus_verbose = true; #define nimbus_vinfo(n, fmt, ...) \ @@ -151,12 +254,22 @@ struct nimbus { struct task_struct *thread; struct mutex lock; bool stopped; - bool fw_loaded; + bool fw_uploaded; /* 2D640/2D7A4 transport ACKs */ + bool cal_uploaded; + bool requestcal_done; /* 2D5B0 / 1F01 path done */ + bool exec_sent; /* 2D54C SPI xfer completed — not runtime */ + bool runtime_ready; /* valid 182590 ping checksum */ + bool fw_loaded; /* alias of runtime_ready for older call sites */ bool fw_tried; bool spi_ok; bool use_irq; bool blob16; /* S5L TXDATA is 8-bit; 16-bit writes fail 4BC1 */ bool parked; /* give up after recycle budget — stop SPI spam */ + bool have_isys; + bool have_cal; + bool isys_sysfs; + u8 isys[NIMBUS_ISYS_LEN]; + u8 cal_upload[NIMBUS_FW_HDR_LEN]; int irq; unsigned int ping_fails; unsigned int recycle_count; @@ -453,6 +566,8 @@ static int nimbus_xfer(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len) /* sub_2C87E — bootloader opcode whitelist */ static bool nimbus_opcode_known(u16 w); static bool nimbus_looks_like_arm(const u8 *p, size_t n); +static void nimbus_bswap32_words(u8 *p, unsigned int len); +static u32 nimbus_sum32(const u8 *p, unsigned int len); static bool nimbus_opcode_known(u16 w) { @@ -520,6 +635,65 @@ static bool nimbus_fw_has_z2fw_hdr(const u8 *data, size_t size) return magic == NIMBUS_Z2FW_MAGIC; } +/* Classify host grape file: full 8740 container vs ARM-only cut vs Z2FW. */ +static void nimbus_fwfile_classify(struct nimbus *n, const u8 *data, size_t size) +{ + bool h8740 = nimbus_fw_has_8740_hdr(data, size); + bool hz2 = nimbus_fw_has_z2fw_hdr(data, size); + bool arm0 = size >= 4 && nimbus_looks_like_arm(data, size); + bool arm400 = size >= 0x410 && nimbus_looks_like_arm(data + 0x400, 16); + u32 le0c = (h8740 && size >= 0x10) ? get_unaligned_le32(data + 0xc) : 0; + u8 rev = (h8740 && size >= 5) ? data[4] : 0; + + dev_info(&n->spi->dev, + "FWFILE size=%zu first16=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x has_8740=%d rev=%u le32(+0xc)=0x%x arm@0=%d arm@0x400=%d Z2FW=%d\n", + size, + size > 0 ? data[0] : 0, size > 1 ? data[1] : 0, + size > 2 ? data[2] : 0, size > 3 ? data[3] : 0, + size > 4 ? data[4] : 0, size > 5 ? data[5] : 0, + size > 6 ? data[6] : 0, size > 7 ? data[7] : 0, + size > 8 ? data[8] : 0, size > 9 ? data[9] : 0, + size > 10 ? data[10] : 0, size > 11 ? data[11] : 0, + size > 12 ? data[12] : 0, size > 13 ? data[13] : 0, + size > 14 ? data[14] : 0, size > 15 ? data[15] : 0, + h8740, rev, le0c, arm0, arm400, hz2); + if (!h8740 && arm0) + dev_warn(&n->spi->dev, + "FWFILE is ARM-only cut — grape file +350 is not IsyS cal\n"); +} + +static void __maybe_unused nimbus_log_calcand(struct nimbus *n, const char *name, + const u8 *data, size_t size, unsigned int off) +{ + u8 tmp[NIMBUS_FW_HDR_LEN]; + u32 s; + + if (size < off + NIMBUS_FW_HDR_LEN) { + dev_info(&n->spi->dev, "CALCAND %s off=%u OOB (file=%zu)\n", + name, off, size); + return; + } + memcpy(tmp, data + off, NIMBUS_FW_HDR_LEN); + nimbus_bswap32_words(tmp, NIMBUS_FW_HDR_LEN); + s = nimbus_sum32(tmp, NIMBUS_FW_HDR_LEN); + dev_info(&n->spi->dev, + "CALCAND %s off=%u sum32=0x%08x first16=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x (post-bswap)\n", + name, off, s, + tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], + tmp[7], tmp[8], tmp[9], tmp[10], tmp[11], tmp[12], tmp[13], + tmp[14], tmp[15]); +} + +static void __maybe_unused nimbus_dump_calcands(struct nimbus *n, const u8 *data, + size_t size) +{ + /* Diagnostic only — does not select a candidate for upload. */ + nimbus_log_calcand(n, "dec350", data, size, 350); + nimbus_log_calcand(n, "hex350", data, size, 0x350); + nimbus_log_calcand(n, "arm_plus_dec350", data, size, 0x400 + 350); + nimbus_log_calcand(n, "arm_plus_hex350", data, size, 0x400 + 0x350); +} + static u32 nimbus_crc32_payload(const u8 *p, size_t len) { return crc32_le(~0U, p, len) ^ ~0U; @@ -673,6 +847,81 @@ static void nimbus_peek(struct nimbus *n, const char *tag) } } +/* HBPP MemRead, dest packing is B1,B0,B3,B2 — same as DATA offset. */ +static int nimbus_rdmem(struct nimbus *n, u32 addr, u8 *buf, unsigned int len) +{ + unsigned int i; + + if (len & 3) + return -EINVAL; + for (i = 0; i < len; i += 4) { + u32 v = 0; + + if (nimbus_rdreg(n, addr + i, &v)) + return -EIO; + put_unaligned_le32(v, buf + i); + } + return 0; +} + +/* + * Prove whether 2D640 landed the ARM image at dest 0 or at the EXEC + * word 0x00100018. Cal dest 0x400200 is a separate window. + */ +static void nimbus_fw_readback(struct nimbus *n, const char *tag) +{ + static const u32 addrs[] = { + 0x00000000, 0x00000018, 0x00100000, 0x00100018, + 0x00400000, 0x00400200, + }; + u8 *buf; + unsigned int i; + + buf = kmalloc(0x1000, GFP_KERNEL); + if (!buf) + return; + dev_info(&n->spi->dev, "NIMBUS FW_READBACK %s:\n", tag); + for (i = 0; i < ARRAY_SIZE(addrs); i++) { + u32 crc100, crc1000; + + memset(buf, 0xa5, 0x1000); + if (nimbus_rdmem(n, addrs[i], buf, 0x1000)) { + dev_warn(&n->spi->dev, + " addr=%08x RDREG fail\n", addrs[i]); + continue; + } + crc100 = nimbus_crc32_payload(buf, 0x100); + crc1000 = nimbus_crc32_payload(buf, 0x1000); + dev_info(&n->spi->dev, + " addr=%08x first32=%32ph crc100=0x%08x crc1000=0x%08x\n", + addrs[i], buf, crc100, crc1000); + } + kfree(buf); +} + +static void nimbus_cal_readback(struct nimbus *n, const u8 *upload) +{ + u8 *buf; + u32 crc_chip, crc_host; + + buf = kmalloc(NIMBUS_FW_HDR_LEN, GFP_KERNEL); + if (!buf) + return; + if (nimbus_rdmem(n, NIMBUS_CAL_DEST, buf, NIMBUS_FW_HDR_LEN)) { + dev_warn(&n->spi->dev, "cal readback RDREG fail @0x%08x\n", + NIMBUS_CAL_DEST); + kfree(buf); + return; + } + crc_chip = nimbus_crc32_payload(buf, NIMBUS_FW_HDR_LEN); + crc_host = nimbus_crc32_payload(upload, NIMBUS_FW_HDR_LEN); + dev_info(&n->spi->dev, + "NIMBUS CAL_READBACK @%08x first64=%32ph %32ph crc200=0x%08x host_crc=0x%08x match=%d\n", + NIMBUS_CAL_DEST, buf, buf + 32, crc_chip, crc_host, + crc_chip == crc_host && !memcmp(buf, upload, NIMBUS_FW_HDR_LEN)); + kfree(buf); +} + /* sub_20848(6593) */ static int nimbus_bootload_cmd(struct nimbus *n) { @@ -735,141 +984,514 @@ static u32 nimbus_sum32(const u8 *p, unsigned int len) return s; } -static bool nimbus_cal_from_isys(struct nimbus *n, const u8 *blob, size_t len, - u8 *win) +/* + * Family clue (iPhone 4S AppleMultitouchN1SPI): cal is a separate + * multi-touch-calibration property starting "NI" (4e 49 …), not FW+350. + * N31 RetailOS window has been observed as 4e 49 02 01 (vs 4S 4e 49 01 01). + * Host/IsyS order is checked BEFORE the RetailOS u32-reverse into win[]. + */ +static bool nimbus_cal_looks_ni(const u8 *p) { - if (len < NIMBUS_FW_HDR_OFF + NIMBUS_FW_HDR_LEN) - return false; - memcpy(win, blob + NIMBUS_FW_HDR_OFF, NIMBUS_FW_HDR_LEN); - nimbus_bswap32_words(win, NIMBUS_FW_HDR_LEN); + return p && p[0] == 0x4e && p[1] == 0x49; +} + +/* + * OSOS sub_273A0: copy IsyS[350 : 350+0x200], reverse each u32 in that + * copy only (do not mutate the 0x560 object). + */ +static int nimbus_prepare_cal_from_isys(struct nimbus *n, const u8 *isys, + size_t isys_len) +{ + const u8 *raw; + u32 sum; + + if (isys_len != NIMBUS_ISYS_LEN) { + dev_err(&n->spi->dev, "IsyS bad size: got=%zu want=0x%x\n", + isys_len, NIMBUS_ISYS_LEN); + return -EINVAL; + } + + memcpy(n->isys, isys, NIMBUS_ISYS_LEN); + n->have_isys = true; + + raw = n->isys + NIMBUS_FW_HDR_OFF; + memcpy(n->cal_upload, raw, NIMBUS_FW_HDR_LEN); + dev_info(&n->spi->dev, + "cal +350 raw head %02x %02x %02x %02x%s\n", + raw[0], raw[1], raw[2], raw[3], + nimbus_cal_looks_ni(raw) ? " (NI family — good)" : + " (not NI — suspect vs 4S/IOReg cal)"); + nimbus_bswap32_words(n->cal_upload, NIMBUS_FW_HDR_LEN); + sum = nimbus_sum32(n->cal_upload, NIMBUS_FW_HDR_LEN); dev_info(&n->spi->dev, - "cal +350 sum32=0x%08x head %02x %02x %02x %02x byte8=%u\n", - nimbus_sum32(win, NIMBUS_FW_HDR_LEN), - win[0], win[1], win[2], win[3], win[8]); - return true; + "Nimbus IsyS cal prepared: off=%u len=0x%x sum32=0x%08x upload_first32=%32ph\n", + NIMBUS_FW_HDR_OFF, NIMBUS_FW_HDR_LEN, sum, n->cal_upload); + if (!sum) { + dev_err(&n->spi->dev, + "IsyS +350 window is all zeros — not a usable cal\n"); + n->have_cal = false; + return -EINVAL; + } + n->have_cal = true; + return 0; } -static bool nimbus_try_isys_slot(struct nimbus *n, phys_addr_t slot, u8 *win) +/* + * OSOS sub_564: desc = sub_A34(24) = 0x2202FE18 + * desc[0] == 0x53797349 + * memcpy(0x08A8B510, desc[1], 0x560) + * Linux reads the live descriptor if boot preserved that SRAM. + */ +static int nimbus_load_isys_from_a34(struct nimbus *n) { - void __iomem *p, *src; - u32 magic, ptr; + void __iomem *desc_io; + void __iomem *src_io; + u32 magic; + u32 ptr; u8 *tmp; - bool ok = false; + int ret; + + if (!cal_try_a34) + return -ENOENT; + + desc_io = ioremap(NIMBUS_A34_ISYS_DESC, 8); + if (!desc_io) + return -ENOMEM; + + magic = readl(desc_io); + ptr = readl(desc_io + 4); + iounmap(desc_io); + + dev_info(&n->spi->dev, + "A34 IsyS descriptor: magic=0x%08x ptr=0x%08x\n", + magic, ptr); + + if (magic != NIMBUS_ISYS_MAGIC) { + dev_warn(&n->spi->dev, + "A34 IsyS missing: magic=0x%08x want=0x%08x\n", + magic, NIMBUS_ISYS_MAGIC); + return -ENOENT; + } + if (!ptr) { + dev_warn(&n->spi->dev, "A34 IsyS pointer is NULL\n"); + return -ENOENT; + } + + src_io = ioremap(ptr, NIMBUS_ISYS_LEN); + if (!src_io) + return -ENOMEM; - p = ioremap(slot, 8); - if (!p) - return false; - magic = readl(p); - ptr = readl(p + 4); - iounmap(p); - dev_info(&n->spi->dev, "IsyS slot 0x%lx magic=0x%08x ptr=0x%08x\n", - (unsigned long)slot, magic, ptr); - if (magic != NIMBUS_ISYS_MAGIC || !ptr) - return false; - src = ioremap(ptr, NIMBUS_ISYS_LEN); - if (!src) - return false; tmp = kmalloc(NIMBUS_ISYS_LEN, GFP_KERNEL); if (!tmp) { - iounmap(src); - return false; + iounmap(src_io); + return -ENOMEM; } - memcpy_fromio(tmp, src, NIMBUS_ISYS_LEN); - iounmap(src); - ok = nimbus_cal_from_isys(n, tmp, NIMBUS_ISYS_LEN, win); + + memcpy_fromio(tmp, src_io, NIMBUS_ISYS_LEN); + iounmap(src_io); + + dev_info(&n->spi->dev, + "A34 IsyS read: ptr=0x%08x len=0x%x first32=%32ph calraw_first16=%16ph\n", + ptr, NIMBUS_ISYS_LEN, tmp, tmp + NIMBUS_FW_HDR_OFF); + + ret = nimbus_prepare_cal_from_isys(n, tmp, NIMBUS_ISYS_LEN); kfree(tmp); - return ok; + return ret; } /* - * 2D7A4 payload = 43CFB4()+350, 512B, u32-reversed. - * RetailOS source is sub_564: A34(0x18) → copy 0x560 into BSS 0x8A8B510. - * A34 lives at Grape 0x2202FE18; ioremap of that is not AP RAM on DFU. - * 273A0 uses the grape image itself at +350. + * U-Boot copies the 0x560 IsyS object to reserved DRAM and publishes + * apple,n31-isys-addr / apple,n31-isys-size on /chosen. That copy is the + * safe address — never the original A34 pointer. */ -static int nimbus_load_cal_window(struct nimbus *n, u8 *win, - const u8 *fw, size_t fw_len) +static int nimbus_load_isys_from_dt(struct nimbus *n) { - if (fw && nimbus_cal_from_isys(n, fw, fw_len, win)) { - dev_info(&n->spi->dev, - "2D7A4 cal from grape.bin +350 (skipped A34 0x22)\n"); + struct device_node *chosen; + u32 addr; + u32 size; + void *p; + u8 *tmp; + int ret; + + if (!cal_try_dt) + return -ENOENT; + + chosen = of_find_node_by_path("/chosen"); + if (!chosen) + return -ENOENT; + + ret = of_property_read_u32(chosen, "apple,n31-isys-addr", &addr); + if (ret) + goto out; + + ret = of_property_read_u32(chosen, "apple,n31-isys-size", &size); + if (ret) + goto out; + + dev_info(&n->spi->dev, "DT IsyS: addr=0x%08x size=0x%x\n", addr, size); + + if (!addr || size != NIMBUS_ISYS_LEN) { + ret = -EINVAL; + goto out; + } + + p = memremap(addr, size, MEMREMAP_WB); + if (!p) { + ret = -ENOMEM; + goto out; + } + + tmp = kmemdup(p, size, GFP_KERNEL); + memunmap(p); + if (!tmp) { + ret = -ENOMEM; + goto out; + } + + dev_info(&n->spi->dev, + "DT IsyS read: addr=0x%08x len=0x%x first32=%32ph calraw_first16=%16ph\n", + addr, size, tmp, tmp + NIMBUS_FW_HDR_OFF); + + ret = nimbus_prepare_cal_from_isys(n, tmp, size); + kfree(tmp); + +out: + of_node_put(chosen); + return ret; +} + +static int nimbus_acquire_isys_cal(struct nimbus *n) +{ + int ret; + + if (n->have_cal) + return 0; + + ret = nimbus_load_isys_from_dt(n); + if (!ret) + return 0; + + dev_info(&n->spi->dev, "DT IsyS unavailable: %d; trying A34 live\n", + ret); + + ret = nimbus_load_isys_from_a34(n); + if (!ret) return 0; + + dev_err(&n->spi->dev, + "No IsyS calibration from DT or A34; not registering input\n"); + return ret; +} + +/* Optional fmss FTL export — grape firmware only, not IsyS cal. */ +static bool (*nimbus_ftl_present_fn)(void); +static int (*nimbus_ftl_read_fn)(u64 logical_sector, void *buf); +static bool nimbus_ftl_inited; + +static void nimbus_ftl_init_once(void) +{ + if (nimbus_ftl_inited) + return; + nimbus_ftl_inited = true; + nimbus_ftl_present_fn = symbol_get(fmss_ftl_present); + nimbus_ftl_read_fn = symbol_get(fmss_ftl_read_sector); +} + +static bool nimbus_ftl_ready(void) +{ + nimbus_ftl_init_once(); + return nimbus_ftl_present_fn && nimbus_ftl_read_fn && + nimbus_ftl_present_fn(); +} + +/* + * Walk FTL for Apple 8740 / gpfw IMG1. Returns kmalloc'd buffer + size. + * Caller kfree() on success. This is the ARM app, not IsyS cal. + */ +static u8 *nimbus_try_gpfw_from_ftl(struct device *dev, size_t *out_len) +{ + u8 *sec, *buf = NULL; + u64 lba, end; + unsigned int off; + size_t need, got; + u32 body_sz; + + if (!fw_prefer_ftl || !nimbus_ftl_ready()) + return NULL; + + sec = kmalloc(NIMBUS_FTL_SECTOR_SIZE, GFP_KERNEL); + if (!sec) + return NULL; + + end = min_t(u64, cal_ftl_start + cal_ftl_count, 256ULL); + for (lba = 0; lba < end; lba++) { + if (nimbus_ftl_read_fn(lba, sec)) + continue; + for (off = 0; off + 0x410 <= NIMBUS_FTL_SECTOR_SIZE; off += 4) { + if (memcmp(sec + off, "8740", 4)) + continue; + body_sz = get_unaligned_le32(sec + off + 0x0c); + if (!body_sz || body_sz > 1024 * 1024) + continue; + need = 0x400 + round_up(body_sz, 16); + buf = kmalloc(need, GFP_KERNEL); + if (!buf) + goto out; + memcpy(buf, sec + off, min_t(size_t, need, + NIMBUS_FTL_SECTOR_SIZE - off)); + got = min_t(size_t, need, NIMBUS_FTL_SECTOR_SIZE - off); + while (got < need && lba + 1 < end) { + lba++; + if (nimbus_ftl_read_fn(lba, sec)) { + kfree(buf); + buf = NULL; + goto out; + } + memcpy(buf + got, sec, + min_t(size_t, need - got, + NIMBUS_FTL_SECTOR_SIZE)); + got += min_t(size_t, need - got, + NIMBUS_FTL_SECTOR_SIZE); + } + if (got >= 0x410) { + dev_info(dev, + "gpfw/8740 from FTL lba=%llu off=%u need=%zu got=%zu rev=%u\n", + lba, off, need, got, buf[7]); + *out_len = got; + goto out; + } + kfree(buf); + buf = NULL; + } } - dev_warn(&n->spi->dev, - "no grape.bin +350 window — 2D7A4 zeros\n"); - return -ENOENT; +out: + kfree(sec); + return buf; +} + +static int nimbus_acquire_fw(struct device *dev, const u8 **data, + size_t *size, const struct firmware **fw_out, + u8 **kbuf_out) +{ + size_t flen = 0; + u8 *ftl; + + *fw_out = NULL; + *kbuf_out = NULL; + if (fw_prefer_ftl) { + ftl = nimbus_try_gpfw_from_ftl(dev, &flen); + if (ftl) { + *data = ftl; + *size = flen; + *kbuf_out = ftl; + return 0; + } + } + if (!fw_allow_file) + return -ENOENT; + if (request_firmware(fw_out, "apple/grape-nimbus.bin", dev) || + !*fw_out) + return -ENOENT; + *data = (*fw_out)->data; + *size = (*fw_out)->size; + return 0; +} + +static void nimbus_release_fw(const struct firmware *fw, u8 *kbuf) +{ + if (kbuf) + kfree(kbuf); + else if (fw) + release_firmware(fw); } /* - * sub_2D640 / 2D7A4 + trampoline sub_35C1C → sub_3B9D0: - * [0..1] 18 E1 - * [2..3] 30 01 - * [4..5] (len>>10), (len>>2) — len must be multiple of 4 - * [6..9] offset packed BYTE1,0,3,2 - * [10..11] sum16 of bytes [4..9] - * [12 .. 12+len) swizzled payload - * [12+len .. +4) sum32 of payload, stored BYTE1,0,3,2 - * SPI len = len + 16; ACK 0x4BC1 (retry ≤5). + * 2D7A4 payload → controller @ 0x00400200. + * Cal is the transformed A34 IsyS window only. */ -static int nimbus_send_chunk_ex(struct nimbus *n, const u8 *data, - unsigned int offset, unsigned int len, - unsigned int cs_flags) +static int nimbus_load_cal_window(struct nimbus *n, u8 *win) { - u8 *buf; - u16 hdr_sum; - u32 body_sum; - unsigned int i; - int ret, try; + int ret; - if (!len || len > NIMBUS_CHUNK_MAX || (len & 3)) - return -EINVAL; + ret = nimbus_acquire_isys_cal(n); + if (ret) + return ret; + memcpy(win, n->cal_upload, NIMBUS_FW_HDR_LEN); + return 0; +} - buf = kzalloc(len + NIMBUS_HDR_LEN, GFP_KERNEL); - if (!buf) - return -ENOMEM; +/* + * sub_2D640 / 2D7A4 + trampoline sub_35C1C → sub_3B9D0: + * frame[0..1] 18 E1 + * body @ +2: + * 30 01 + * word_count hi/lo = (len>>10),(len>>2) + * dest B1 B0 B3 B2 + * u16 byte-sum of previous 6 body bytes (words+dest), BE + * payload u32s swizzled B1 B0 B3 B2 + * u32 byte-sum of swizzled payload, stored B1 B0 B3 B2 + * SPI len = payload_len + 16; max payload 0x1FF0; ACK 0x4BC1 (retry ≤5). + * + * Expected prefixes (exact glass check): + * FW chunk0 dest=0 len=0x1FF0: + * 18 E1 30 01 07 FC 00 00 00 00 01 03 + * CAL chunk0 dest=0x00400200 len=0x200: + * 18 E1 30 01 00 80 02 00 00 40 00 C2 + */ +static unsigned int nimbus_build_upload_frame(u8 *buf, u32 dest, + const u8 *src, unsigned int len) +{ + u16 hdr_sum; + u32 payload_sum; buf[0] = 0x18; buf[1] = 0xe1; buf[2] = 0x30; buf[3] = 0x01; + /* word_count = len/4 as big-endian u16 via (len>>10),(len>>2) */ buf[4] = (len >> 10) & 0xff; buf[5] = (len >> 2) & 0xff; - buf[6] = (offset >> 8) & 0xff; - buf[7] = offset & 0xff; - buf[8] = (offset >> 24) & 0xff; - buf[9] = (offset >> 16) & 0xff; + /* dest swizzle B1 B0 B3 B2 */ + buf[6] = (dest >> 8) & 0xff; + buf[7] = dest & 0xff; + buf[8] = (dest >> 24) & 0xff; + buf[9] = (dest >> 16) & 0xff; hdr_sum = nimbus_sum16(buf + 4, 6); buf[10] = (hdr_sum >> 8) & 0xff; buf[11] = hdr_sum & 0xff; - nimbus_grape_swizzle32(buf + 12, data, len); + nimbus_grape_swizzle32(buf + 12, src, len); + + payload_sum = nimbus_sum32(buf + 12, len); + buf[12 + len] = (payload_sum >> 8) & 0xff; + buf[12 + len + 1] = payload_sum & 0xff; + buf[12 + len + 2] = (payload_sum >> 24) & 0xff; + buf[12 + len + 3] = (payload_sum >> 16) & 0xff; + + return len + NIMBUS_HDR_LEN; +} + +/* Glass/oracle prefixes from RetailOS 2D640 / 2D7A4 — fail loud if wrong. */ +static void nimbus_check_upload_prefix(struct nimbus *n, u32 dest, + unsigned int len, const u8 *tx) +{ + static const u8 fw0[12] = { + 0x18, 0xe1, 0x30, 0x01, 0x07, 0xfc, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x03 + }; + static const u8 cal0[12] = { + 0x18, 0xe1, 0x30, 0x01, 0x00, 0x80, 0x02, 0x00, + 0x00, 0x40, 0x00, 0xc2 + }; + + if (dest == 0 && len == NIMBUS_CHUNK_MAX && memcmp(tx, fw0, 12)) { + dev_err(&n->spi->dev, + "FW_UPLOAD prefix MISMATCH want 18 e1 30 01 07 fc 00 00 00 00 01 03 got %12ph\n", + tx); + } + if (dest == NIMBUS_CAL_DEST && len == NIMBUS_FW_HDR_LEN && + memcmp(tx, cal0, 12)) { + dev_err(&n->spi->dev, + "CAL_UPLOAD prefix MISMATCH want 18 e1 30 01 00 80 02 00 00 40 00 c2 got %12ph\n", + tx); + } +} + +static void nimbus_log_upload_prefix(struct nimbus *n, const char *tag, + unsigned int chunk_idx, u32 dest, + unsigned int len, const u8 *tx, + unsigned int xfer_len, u16 ack, int ack_ret) +{ + dev_info(&n->spi->dev, + "NIMBUS %s chunk=%u dest=%08x len=%04x xfer=%u ACK=0x%04x ret=%d\n", + tag, chunk_idx, dest, len, xfer_len, ack, ack_ret); + dev_info(&n->spi->dev, + " tx[0:16] = %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", + tx[0], tx[1], tx[2], tx[3], tx[4], tx[5], tx[6], tx[7], + tx[8], tx[9], tx[10], tx[11], + xfer_len > 12 ? tx[12] : 0, xfer_len > 13 ? tx[13] : 0, + xfer_len > 14 ? tx[14] : 0, xfer_len > 15 ? tx[15] : 0); +} + +static void nimbus_dump_hbpp_tx(struct nimbus *n, const char *tag, + const u8 *raw, unsigned int chunk_idx, + unsigned int dest, unsigned int chunk_len, + const u8 *tx, unsigned int xfer_len, u16 ack, + int ack_ret) +{ + unsigned int last_off; + + nimbus_log_upload_prefix(n, tag, chunk_idx, dest, chunk_len, + tx, xfer_len, ack, ack_ret); + if (raw && chunk_len >= 64) + dev_info(&n->spi->dev, " raw first64=%32ph %32ph\n", + raw, raw + 32); + else if (raw) + dev_info(&n->spi->dev, " raw first%u=%*ph\n", + chunk_len, chunk_len, raw); + if (xfer_len >= 96) + dev_info(&n->spi->dev, + " tx first96=%32ph %32ph %32ph\n", + tx, tx + 32, tx + 64); + else if (xfer_len > 16) + dev_info(&n->spi->dev, " tx first%u=%*ph\n", + xfer_len, xfer_len, tx); + if (xfer_len >= 32) { + last_off = xfer_len - 32; + dev_info(&n->spi->dev, " tx last32=%32ph\n", tx + last_off); + } +} + +static int nimbus_send_chunk_ex(struct nimbus *n, const u8 *data, + unsigned int dest, unsigned int len, + unsigned int cs_flags, bool dump, + const char *tag, unsigned int chunk_idx, + unsigned int file_off) +{ + u8 *buf; + unsigned int xfer_len; + int ret = -EIO, try, ack_ret = -ETIMEDOUT; + u16 ack = 0; + + if (!len || len > NIMBUS_CHUNK_MAX || (len & 3)) + return -EINVAL; + + xfer_len = len + NIMBUS_HDR_LEN; + buf = kzalloc(xfer_len, GFP_KERNEL); + if (!buf) + return -ENOMEM; - body_sum = 0; - for (i = 0; i < len; i++) - body_sum += buf[12 + i]; - buf[12 + len] = (body_sum >> 8) & 0xff; - buf[12 + len + 1] = body_sum & 0xff; - buf[12 + len + 2] = (body_sum >> 24) & 0xff; - buf[12 + len + 3] = (body_sum >> 16) & 0xff; + if (nimbus_build_upload_frame(buf, dest, data, len) != xfer_len) { + kfree(buf); + return -EINVAL; + } + nimbus_check_upload_prefix(n, dest, len, buf); /* Z2 SEND_BLOB: spi_sync keeps CS down for whole HBPP frame. */ for (try = 0; try < 5; try++) { if (chunk_spi) - ret = nimbus_xfer(n, buf, NULL, len + NIMBUS_HDR_LEN); + ret = nimbus_xfer(n, buf, NULL, xfer_len); else if (n->blob16) ret = nimbus_burst_u16_ex(n, buf, NULL, - len + NIMBUS_HDR_LEN, cs_flags); + xfer_len, cs_flags); else - ret = nimbus_burst_ex(n, buf, NULL, - len + NIMBUS_HDR_LEN, cs_flags); + ret = nimbus_burst_ex(n, buf, NULL, xfer_len, cs_flags); if (ret) continue; - if (nimbus_wait_ack(n, NIMBUS_ACK_CHUNK, 8) == 0) { - if (!offset) - dev_info(&n->spi->dev, - "chunk0 %u bytes ACK 0x4BC1 (%s)\n", - len, n->blob16 ? "u16" : "u8"); + /* 1A A1 → 2 bytes → rev16; expect 0x4BC1 */ + ack_ret = nimbus_wait_ack(n, NIMBUS_ACK_CHUNK, 8); + if (ack_ret == 0) { + ack = NIMBUS_ACK_CHUNK; + if (dump) + nimbus_dump_hbpp_tx(n, tag, data, chunk_idx, + dest, len, buf, xfer_len, + ack, ack_ret); + else if (!chunk_idx) + nimbus_log_upload_prefix(n, tag, chunk_idx, + dest, len, buf, + xfer_len, ack, + ack_ret); kfree(buf); return 0; } @@ -879,26 +1501,38 @@ static int nimbus_send_chunk_ex(struct nimbus *n, const u8 *data, "16-bit DATA no 4BC1 — falling back to 8-bit PIO\n"); } } + if (dump || !chunk_idx) + nimbus_dump_hbpp_tx(n, tag, data, chunk_idx, dest, len, buf, + xfer_len, ack, ack_ret); kfree(buf); return -EIO; } static int nimbus_send_chunk(struct nimbus *n, const u8 *data, - unsigned int offset, unsigned int len) + unsigned int dest, unsigned int len, bool dump, + unsigned int file_off, const char *tag, + unsigned int chunk_idx) { - return nimbus_send_chunk_ex(n, data, offset, len, - NIMBUS_CS_BEGIN | NIMBUS_CS_END); + return nimbus_send_chunk_ex(n, data, dest, len, + NIMBUS_CS_BEGIN | NIMBUS_CS_END, dump, + tag, chunk_idx, file_off); } static int nimbus_send_blob(struct nimbus *n, const u8 *data, unsigned int len, unsigned int dest_off) { unsigned int off = 0; + unsigned int chunk_idx = 0; u8 pad[4]; + const char *tag; + bool is_cal = (dest_off == NIMBUS_CAL_DEST); + + tag = is_cal ? "CAL_UPLOAD" : "FW_UPLOAD"; while (off < len) { unsigned int chunk = min_t(unsigned int, len - off, NIMBUS_CHUNK_MAX); int ret; + bool last, dump; /* RetailOS always transfers whole words */ if (chunk & 3) @@ -906,12 +1540,18 @@ static int nimbus_send_blob(struct nimbus *n, const u8 *data, unsigned int len, if (!chunk) { memset(pad, 0, sizeof(pad)); memcpy(pad, data + off, len - off); - return nimbus_send_chunk(n, pad, dest_off + off, 4); + return nimbus_send_chunk(n, pad, dest_off + off, 4, + true, off, tag, chunk_idx); } - ret = nimbus_send_chunk(n, data + off, dest_off + off, chunk); + last = (off + chunk >= len); + /* Always dump first + last FW chunk and the sole cal chunk. */ + dump = (off == 0) || last || is_cal; + ret = nimbus_send_chunk(n, data + off, dest_off + off, chunk, + dump, off, tag, chunk_idx); if (ret) return ret; off += chunk; + chunk_idx++; } return 0; } @@ -971,10 +1611,17 @@ static int nimbus_post_download(struct nimbus *n) }; for (i = 0; i < ARRAY_SIZE(pokes); i++) { + u32 rb = 0; + ret = nimbus_cmd_34ad0(n, pokes[i].a1, pokes[i].a2, pokes[i].a3); dev_info(&n->spi->dev, "34AD0[%d] %d\n", i, ret); if (ret) return ret; + /* 4AD1 = write ACK only; verify with RDREG while still in HBPP. */ + if (nimbus_rdreg(n, pokes[i].a1, &rb) == 0) + dev_info(&n->spi->dev, + "34AD0[%d] RDREG 0x%08x -> 0x%08x (wrote %u)\n", + i, pokes[i].a1, rb, pokes[i].a2); } put_unaligned_le16(NIMBUS_POST_POKE, tx); @@ -985,6 +1632,7 @@ static int nimbus_post_download(struct nimbus *n) /* 2D5B0: 3D5706 success only — does not require 0x4BC1 */ if (nimbus_status_poll(n, &st) == 0) { dev_info(&n->spi->dev, "post-poke status 0x%04x\n", st); + n->requestcal_done = true; return 0; } return -EIO; @@ -1013,38 +1661,61 @@ static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) go_spi_setup, saved_setup); } } - ret = nimbus_burst(n, tx, rx, 12); + if (go_xfer == 2) + ret = nimbus_xfer(n, tx, rx, 12); + else if (go_xfer == 1) + ret = nimbus_burst_u16(n, tx, rx, 12); + else + ret = nimbus_burst(n, tx, rx, 12); if (saved_setup) writel(saved_setup, n->spi2 + SPI2_SETUP); dev_info(&n->spi->dev, - "2D54C %08x %08x ret=%d rx %02x %02x %02x %02x %02x %02x\n", - word0, word1, ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5]); + "2D54C %08x %08x ret=%d xfer=%d rx %02x %02x %02x %02x %02x %02x\n", + word0, word1, ret, go_xfer, rx[0], rx[1], rx[2], rx[3], + rx[4], rx[5]); return ret; } -static void nimbus_drain(struct nimbus *n, unsigned int bytes) +static void __maybe_unused nimbus_drain(struct nimbus *n, unsigned int bytes) { u8 tx[NIMBUS_FRAME_LEN] = { 0 }; u8 rx[NIMBUS_FRAME_LEN] = { 0 }; unsigned int nxf = bytes < NIMBUS_FRAME_LEN ? bytes : NIMBUS_FRAME_LEN; + /* Optional post-fail diagnostics only — never call on EXEC path. */ nimbus_burst(n, tx, rx, nxf); } +static void nimbus_pre_exec_verify(struct nimbus *n) +{ + static const u32 addrs[] = { + 0x00000000, 0x00000004, 0x00000008, 0x00000020, + 0x00400200, 0x00400204, 0x004003fc, + }; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(addrs); i++) { + u32 v = 0; + + if (nimbus_rdreg(n, addrs[i], &v) == 0) + dev_info(&n->spi->dev, "pre-EXEC RDREG 0x%08x=0x%08x\n", + addrs[i], v); + } +} + +/* + * sub_2D54C — one-shot EXEC packet only. + * Do NOT poll 1A A1 / drain after EXEC: that keeps speaking HBPP across the + * bootloader→runtime boundary. Success is proven only by 182590 ping csum. + */ static int nimbus_cmd_2d54c(struct nimbus *n) { - u16 st = 0; int ret; - /* 273A0: 2D54C immediately after 2D5B0. No HBPP MemRead around go. */ - nimbus_drain(n, 16); - ret = nimbus_cmd_2d54c_raw(n, 0x00100018, 0x00000100); - if (!ret) { - msleep(40); - if (nimbus_status_poll(n, &st) == 0) - dev_info(&n->spi->dev, "post-2D54C status 0x%04x\n", st); - nimbus_drain(n, 16); - } + nimbus_pre_exec_verify(n); + ret = nimbus_cmd_2d54c_raw(n, exec_addr, exec_word1); + if (!ret) + n->exec_sent = true; return ret; } @@ -1122,9 +1793,6 @@ static int nimbus_probe_ping16(struct nimbus *n) * for 8740 rev 3 only. grape-nimbus.bin on DFU is usually pre-decrypted-cut. * force_gid=1 tries 422FFA even when loading plaintext blob (bring-up). */ -static int force_gid; -module_param(force_gid, int, 0644); -MODULE_PARM_DESC(force_gid, "1=422FFA decrypt attempt even without 8740 rev3 hdr"); static int nimbus_422ffa_mmio(struct device *dev, u8 *buf, unsigned int len, bool encrypt) { @@ -1254,6 +1922,57 @@ static bool nimbus_looks_like_arm(const u8 *p, size_t n) p[2] == 0x9f && p[3] == 0xe5; } +/* + * v6 RE (2026-08-25): post-GID plaintext may already be a preconstructed + * HBPP DATA object (18 E1 30 01 …) — Corellium GEN_1 sends Constructed + * Firmware unchanged. Detect that and SPI-send as-is (ACK 0x4BC1). + */ +static bool nimbus_looks_like_hbpp_data(const u8 *p, size_t n) +{ + if (n >= 4 && p[0] == 0x18 && p[1] == 0xe1 && + p[2] == 0x30 && p[3] == 0x01) + return true; + if (n >= 2 && p[0] == 0x30 && p[1] == 0x01) + return true; + return false; +} + +/** + * nimbus_send_preconstructed_hbpp - SPI the whole HBPP frame, expect 0x4BC1 + * (DATA ACK). Do not re-wrap or swizzle — bytes are already HBPP. + */ +static int nimbus_send_preconstructed_hbpp(struct nimbus *n, const u8 *data, + size_t len) +{ + int try, ret; + + if (len < 16) + return -EINVAL; + + for (try = 0; try < 5; try++) { + if (chunk_spi) + ret = nimbus_xfer(n, data, NULL, len); + else if (n->blob16) + ret = nimbus_burst_u16_ex(n, data, NULL, len, 0); + else + ret = nimbus_burst_ex(n, data, NULL, len, 0); + if (ret) + continue; + if (nimbus_wait_ack(n, NIMBUS_ACK_CHUNK, 8) == 0) { + dev_info(&n->spi->dev, + "preconstructed HBPP %zuB ACK 0x4BC1 try=%d\n", + len, try); + return 0; + } + if (n->blob16 && try == 0) { + n->blob16 = false; + dev_info(&n->spi->dev, + "preconstructed HBPP: fall back to 8-bit\n"); + } + } + return -EIO; +} + /* * 204E0 sends le32(8740+0x0c)=0xe970. The decrypted cut is 0xecf0 and the * extra 896 bytes are 0x53/0x43 fill. Downloading that fill to dest 0xe970 @@ -1282,6 +2001,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, bool apple_hdr = nimbus_fw_has_8740_hdr(data, size); (void)arm_at_zero; + nimbus_fwfile_classify(n, data, size); /* * 1A640 NOR 8740 → 204E0. ARM at +0x400, size le32(+0x0c). @@ -1301,7 +2021,10 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, /* Short NOR slice: take the ARM bytes we have. Never expand. */ if (!hdr_sz || hdr_sz > size - 0x400) hdr_sz = size - 0x400; - hdr_sz &= ~3u; + /* 204E0: size rounded up to 16 for 422FFA; keep ≤ available. */ + hdr_sz = round_up(hdr_sz, 16); + if (hdr_sz > size - 0x400) + hdr_sz = (size - 0x400) & ~15u; body = data + 0x400; body_len = hdr_sz; rev = data[7]; @@ -1408,6 +2131,24 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, } send: + /* + * v6: if body is already 18 E1 30 01… (post-GID constructed FW), + * send once and run post-download. Do not re-packetize ARM. + */ + if (nimbus_looks_like_hbpp_data(body, body_len)) { + dev_info(&n->spi->dev, + "preconstructed HBPP DATA %zuB — direct SPI (no ARM wrap)\n", + body_len); + ret = nimbus_send_preconstructed_hbpp(n, body, body_len); + if (!ret) { + ret = nimbus_post_download(n); + if (!ret) + ret = nimbus_cmd_2d54c(n); + } + kfree(dec); + return ret; + } + { size_t official = nimbus_official_arm_len(body, body_len); size_t dl_len = body_len; @@ -1450,27 +2191,59 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, kfree(dec); return -ENOMEM; } - /* 273A0: 43CFB4()+350 from the grape image, not A34 0x22. */ - cal = nimbus_load_cal_window(n, win, data, size); + /* + * Host cal source ≠ controller address. + * 2D640 ARM → dest = fw_dest + offset (OSOS fw_dest=0). + * 2D7A4 cal → dest = 0x00400200 + offset. + * EXEC 0x00100018 is mapped app PC — not upload dest. + */ + cal = nimbus_load_cal_window(n, win); + if (cal < 0) { + dev_err(&n->spi->dev, "2D7A4 aborted: no device cal\n"); + kfree(win); + kfree(z2_prep); + kfree(pad_buf); + kfree(dec); + return cal; + } + + dev_info(&n->spi->dev, + "2D640 ARM dest_base=0x%08x EXEC=0x%08x cal=0x%08x\n", + fw_dest, exec_addr, NIMBUS_CAL_DEST); + if (fw_dest == 0) + dev_info(&n->spi->dev, + "expect FW prefix: 18 e1 30 01 07 fc 00 00 00 00 01 03 (len=0x1ff0)\n"); + else + dev_warn(&n->spi->dev, + "fw_dest override 0x%08x — OSOS uses 0 (A/B only)\n", + fw_dest); + dev_info(&n->spi->dev, + "expect CAL prefix: 18 e1 30 01 00 80 02 00 00 40 00 c2\n"); /* 20E94: 273A0 up to 3 times, no 1A878 between. */ for (try = 0; try < 3; try++) { - ret = nimbus_send_blob(n, dl_body, dl_len, 0); + ret = nimbus_send_blob(n, dl_body, dl_len, fw_dest); if (ret) { dev_err(&n->spi->dev, - "2D640 try %d: %d\n", try, ret); + "2D640 ARM@0x%08x try %d: %d\n", + fw_dest, try, ret); continue; } + n->fw_uploaded = true; + nimbus_fw_readback(n, "post-2D640"); ret = nimbus_send_blob(n, win, NIMBUS_FW_HDR_LEN, - 0x400200); + NIMBUS_CAL_DEST); if (ret) { dev_err(&n->spi->dev, - "2D7A4 try %d: %d\n", try, ret); + "2D7A4 cal@0x%08x try %d: %d\n", + NIMBUS_CAL_DEST, try, ret); continue; } + n->cal_uploaded = true; dev_info(&n->spi->dev, - "2D7A4 512B %s @0x400200 ACK\n", - cal ? "zeros" : "grape+350"); + "2D7A4 512B cal @0x%08x ACK (transport only)\n", + NIMBUS_CAL_DEST); + nimbus_cal_readback(n, win); ret = nimbus_post_download(n); if (ret) { dev_warn(&n->spi->dev, @@ -1478,8 +2251,12 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, continue; } ret = nimbus_cmd_2d54c(n); - if (!ret) + if (!ret) { + dev_info(&n->spi->dev, + "2D54C EXEC sent (try %d) — await runtime ping\n", + try); break; + } dev_warn(&n->spi->dev, "2D54C try %d: %d\n", try, ret); } kfree(win); @@ -1487,10 +2264,8 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, kfree(pad_buf); } kfree(dec); - if (ret) - return ret; - n->fw_loaded = true; - return 0; + /* EXEC transport success is NOT runtime_ready / fw_loaded. */ + return ret; } static int nimbus_ping(struct nimbus *n, u16 *status_out) @@ -1504,9 +2279,14 @@ static int nimbus_ping(struct nimbus *n, u16 *status_out) csum = nimbus_sum16(tx, 14); put_unaligned_le16(csum, tx + 14); - /* 182590: up to 5 retries, sleep 1 between. Burst matches DMA. */ + /* 182590: up to 5 retries, sleep 1 between. After EXEC, 16-bit + * pairs match the app SPI width; 8-bit PIO is bootloader-only. + */ for (tries = 0; tries < 6; tries++) { - ret = nimbus_burst16(n, tx, rx); + if (n->exec_sent && go_xfer) + ret = nimbus_burst_u16(n, tx, rx, NIMBUS_FRAME_LEN); + else + ret = nimbus_burst16(n, tx, rx); if (ret) return ret; @@ -1675,19 +2455,26 @@ static void nimbus_dump_pad(struct nimbus *n, unsigned int gpio, const char *nam !!(readl(b + 0x10) & BIT(pin))); } +/* + * Optional glass experiment only — not in OSOS sub_1A5AC. + * Default off (extra_por_pulse=0). + */ static void nimbus_gpio_por_reset(struct nimbus *n) { - /* Clear latched download mode from a prior failed attempt. */ nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); msleep(reset_hold_ms); nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 1); msleep(reset_release_ms); nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); msleep(5); - dev_info(&n->spi->dev, "POR RST %dms low / %dms high\n", + dev_info(&n->spi->dev, "extra POR RST %dms low / %dms high\n", reset_hold_ms, reset_release_ms); } +/* + * 1A5AC GPIO half (before 20848): + * 2075A(1) sleep5 → 20766(1) sleep15 → 20690(1) sleep5 → 11B70 + */ static void nimbus_gpio_bringup(struct nimbus *n) { int rail; @@ -1724,6 +2511,7 @@ static void nimbus_gpio_bringup(struct nimbus *n) nimbus_dump_pad(n, 90, "spi2-90"); } +/* 1A5AC: 2075A(0) then sleep reset_release_ms (default 30). */ static void nimbus_gpio_release_reset(struct nimbus *n) { nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 1); @@ -1746,23 +2534,23 @@ static int nimbus_1a5ac_and_download(struct nimbus *n, const u8 *data, { int err; - nimbus_gpio_por_reset(n); + /* Dump path has no pre-1A5AC POR; gate for glass A/B only. */ + if (extra_por_pulse) + nimbus_gpio_por_reset(n); nimbus_gpio_bringup(n); err = nimbus_bootload_cmd(n); if (err) dev_warn(&n->spi->dev, "bootload %s: %d\n", tag, err); - msleep(15); + msleep(15); /* 1A5AC: after 20848, before 2075A(0) */ nimbus_gpio_release_reset(n); if (skip_download) { dev_info(&n->spi->dev, "skip_download — no 2D640\n"); return 0; } + /* 20E94: 26494 then 273A0; settle already done in release (30ms). */ if (nimbus_probe_26494(n, tag)) { - msleep(30); - if (nimbus_probe_26494(n, tag)) { - dev_warn(&n->spi->dev, "26494 %s failed\n", tag); - return -EIO; - } + dev_warn(&n->spi->dev, "26494 %s failed\n", tag); + return -EIO; } err = nimbus_download_fw(n, data, size, false); n->fw_tried = true; @@ -1790,6 +2578,9 @@ static void nimbus_park(struct nimbus *n, const char *why) static void nimbus_recycle(struct nimbus *n) { const struct firmware *fw = NULL; + const u8 *data; + u8 *kbuf = NULL; + size_t size; u16 st = 0; if (n->parked) @@ -1808,14 +2599,13 @@ static void nimbus_recycle(struct nimbus *n) n->spi_ok = false; n->ping_fails = 0; msleep(50); - if (request_firmware(&fw, "apple/grape-nimbus.bin", &n->spi->dev) || - !fw) { - dev_warn(&n->spi->dev, "recycle: no grape-nimbus.bin\n"); + if (nimbus_acquire_fw(&n->spi->dev, &data, &size, &fw, &kbuf)) { + dev_warn(&n->spi->dev, "recycle: no FW (FTL or file)\n"); nimbus_park(n, "no firmware"); return; } - nimbus_1a5ac_and_download(n, fw->data, fw->size, "recycle"); - release_firmware(fw); + nimbus_1a5ac_and_download(n, data, size, "recycle"); + nimbus_release_fw(fw, kbuf); msleep(2); if (!nimbus_ping(n, &st)) { n->spi_ok = true; @@ -1861,7 +2651,9 @@ static irqreturn_t nimbus_irq_thread(int irq, void *data) static void nimbus_try_firmware(struct nimbus *n) { const struct firmware *fw = NULL; - int err; + const u8 *data; + u8 *kbuf = NULL; + size_t size; if (n->fw_tried || n->fw_loaded) return; @@ -1874,13 +2666,12 @@ static void nimbus_try_firmware(struct nimbus *n) n->spi_ok = true; } - err = request_firmware(&fw, "apple/grape-nimbus.bin", &n->spi->dev); - if (err || !fw) + if (nimbus_acquire_fw(&n->spi->dev, &data, &size, &fw, &kbuf)) return; n->fw_tried = true; - nimbus_download_fw(n, fw->data, fw->size, false); - release_firmware(fw); + nimbus_download_fw(n, data, size, false); + nimbus_release_fw(fw, kbuf); } static int nimbus_poll_thread(void *data) @@ -1942,11 +2733,48 @@ static int nimbus_poll_thread(void *data) return 0; } +static ssize_t isys_blob_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *attr, char *buf, + loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct nimbus *n = dev_get_drvdata(dev); + + if (!n || !n->have_isys) + return -ENODATA; + if (off >= NIMBUS_ISYS_LEN) + return 0; + if (off + count > NIMBUS_ISYS_LEN) + count = NIMBUS_ISYS_LEN - off; + memcpy(buf, n->isys + off, count); + return count; +} + +static struct bin_attribute isys_blob_attr = { + .attr = { + .name = "isys_blob", + .mode = 0444, + }, + .size = NIMBUS_ISYS_LEN, + .read = isys_blob_read, +}; + +static void nimbus_isys_sysfs_remove(struct nimbus *n) +{ + if (!n || !n->isys_sysfs) + return; + sysfs_remove_bin_file(&n->spi->dev.kobj, &isys_blob_attr); + n->isys_sysfs = false; +} + static int nimbus_probe(struct spi_device *spi) { struct nimbus *n; struct input_dev *input; const struct firmware *fw = NULL; + const u8 *data; + u8 *kbuf = NULL; + size_t size; u16 ping_st = 0; int err; @@ -1975,11 +2803,25 @@ static int nimbus_probe(struct spi_device *spi) if (IS_ERR(n->attn)) return PTR_ERR(n->attn); - err = request_firmware(&fw, "apple/grape-nimbus.bin", &spi->dev); - if (err || !fw) - dev_warn(&spi->dev, "grape-nimbus.bin missing (%d)\n", err); + err = nimbus_acquire_isys_cal(n); + if (err) + return err; + if (!sysfs_create_bin_file(&spi->dev.kobj, &isys_blob_attr)) + n->isys_sysfs = true; + else + dev_warn(&spi->dev, "isys_blob sysfs failed\n"); + + data = NULL; + size = 0; + err = nimbus_acquire_fw(&spi->dev, &data, &size, &fw, &kbuf); + if (err) + dev_warn(&spi->dev, + "no grape firmware (%d) — A34 IsyS is present but 2D640 cannot run\n", + err); - { + if (!data || !size) { + nimbus_park(n, "no grape firmware"); + } else { int attempt; /* @@ -1987,46 +2829,67 @@ static int nimbus_probe(struct spi_device *spi) * only after a failed 1A5AC, max 3. remove() already * 1A878s on reload. */ - for (attempt = 0; attempt < 3 && fw; attempt++) { + for (attempt = 0; attempt < 3; attempt++) { if (attempt) { nimbus_power_down(n); msleep(50); } /* 0xEE is iOS3 Z2-only; N31 wake is 19 C1 in reset. */ mutex_lock(&n->lock); - nimbus_1a5ac_and_download(n, fw->data, fw->size, + n->fw_uploaded = false; + n->cal_uploaded = false; + n->requestcal_done = false; + n->exec_sent = false; + n->runtime_ready = false; + n->fw_loaded = false; + n->spi_ok = false; + nimbus_1a5ac_and_download(n, data, size, attempt ? "retry" : "1A5AC"); { u16 st = 0; - /* 20E94: sleep 2 after 273A0; 1703E8 pings next. */ - msleep(2); + /* + * Cross EXEC boundary: short wait then runtime + * ping only. No HBPP 1A A1 until ping fails. + */ + if (n->exec_sent) + msleep(exec_wait_ms ? exec_wait_ms : 1); + else + msleep(2); err = nimbus_ping(n, &ping_st); if (!err) { n->spi_ok = true; + n->runtime_ready = true; + n->fw_loaded = true; dev_info(&spi->dev, - "ping ok, status=0x%04x\n", + "runtime ping ok status=0x%04x (ready)\n", ping_st); } else { + dev_warn(&spi->dev, + "runtime ping fail attempt %d (exec_sent=%d)\n", + attempt, n->exec_sent); nimbus_peek(n, "post-go-fail"); + /* Diagnostics only after runtime fail. */ if (nimbus_status_poll(n, &st) == 0) dev_info(&spi->dev, - "post-go status 0x%04x\n", + "post-fail HBPP status 0x%04x\n", st); + err = 0; /* keep 1A878 retries going */ } } mutex_unlock(&n->lock); - /* 20E94 does not 1A878 after a successful 273A0. */ - if (n->fw_loaded) + /* Only runtime_ready ends the 1A878 retry loop. */ + if (n->runtime_ready) break; } } - if (fw) - release_firmware(fw); - dev_info(&spi->dev, "fw download attempted, fw_loaded=%d spi_ok=%d\n", - n->fw_loaded, n->spi_ok); + nimbus_release_fw(fw, kbuf); + dev_info(&spi->dev, + "nimbus state uploaded=%d cal=%d reqcal=%d exec=%d runtime=%d spi_ok=%d\n", + n->fw_uploaded, n->cal_uploaded, n->requestcal_done, + n->exec_sent, n->runtime_ready, n->spi_ok); - if (n->fw_loaded || n->spi_ok) { + if (n->cal_uploaded && n->exec_sent && n->runtime_ready) { /* 1A5AC: 20490 after 20E94, then MultitouchTask 1703E8/188FFC. */ nimbus_irq_enable(n); n->irq = spi->irq; @@ -2048,12 +2911,19 @@ static int nimbus_probe(struct spi_device *spi) } } } else { - dev_info(&spi->dev, "SPI not talking — IRQ/poll parked\n"); + dev_err(&spi->dev, + "Nimbus boot failed: cal=%d exec=%d runtime=%d; not registering input\n", + n->cal_uploaded, n->exec_sent, n->runtime_ready); + if (!n->parked) + nimbus_park(n, "boot incomplete"); + return 0; } input = devm_input_allocate_device(&spi->dev); - if (!input) + if (!input) { + nimbus_isys_sysfs_remove(n); return -ENOMEM; + } n->input = input; input->name = "Apple Nimbus"; input->phys = "nimbus/input0"; @@ -2063,39 +2933,31 @@ static int nimbus_probe(struct spi_device *spi) input_set_abs_params(input, ABS_MT_POSITION_X, 0, NIMBUS_ABS_X_MAX, 0, 0); input_set_abs_params(input, ABS_MT_POSITION_Y, 0, NIMBUS_ABS_Y_MAX, 0, 0); err = input_mt_init_slots(input, NIMBUS_SLOTS, INPUT_MT_DIRECT); - if (err) + if (err) { + nimbus_isys_sysfs_remove(n); return err; + } err = input_register_device(input); - if (err) + if (err) { + nimbus_isys_sysfs_remove(n); return err; + } - /* - * FW chunk ACK alone is not enough — RDREG still shows bootloader - * after GO. Only run MultitouchTask when ping works; otherwise park - * so we do not recycle forever while MtCl/cal is unfinished. - */ - if (n->spi_ok) { - if (ping_st) { - mutex_lock(&n->lock); - nimbus_read_reports(n, ping_st); - mutex_unlock(&n->lock); - } - n->thread = kthread_run(nimbus_poll_thread, n, "nimbus-poll"); - if (IS_ERR(n->thread)) { - dev_warn(&spi->dev, - "nimbus-poll kthread %ld — poll via IRQ only\n", - PTR_ERR(n->thread)); - n->thread = NULL; - } - } else if (n->fw_loaded) { - nimbus_park(n, "GO left chip in bootloader"); + if (ping_st) { + mutex_lock(&n->lock); + nimbus_read_reports(n, ping_st); + mutex_unlock(&n->lock); + } + n->thread = kthread_run(nimbus_poll_thread, n, "nimbus-poll"); + if (IS_ERR(n->thread)) { + dev_warn(&spi->dev, + "nimbus-poll kthread %ld — poll via IRQ only\n", + PTR_ERR(n->thread)); + n->thread = NULL; } - if (n->parked) - dev_info_once(&spi->dev, "Nimbus parked (touch offline)\n"); - else - dev_info(&spi->dev, "Nimbus up (attn=%d spi_ok=%d)\n", - !!n->attn, n->spi_ok); + dev_info(&spi->dev, "Nimbus up (attn=%d runtime=%d)\n", + !!n->attn, n->runtime_ready); return 0; } @@ -2106,6 +2968,7 @@ static void nimbus_remove(struct spi_device *spi) n->stopped = true; if (n->thread) kthread_stop(n->thread); + nimbus_isys_sysfs_remove(n); nimbus_power_down(n); } diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig index 8e9d6ba2684da6..7dd6112ed82468 100644 --- a/drivers/misc/Kconfig +++ b/drivers/misc/Kconfig @@ -666,6 +666,14 @@ config FTL_S5L8740 Higher-level FTL helper layered on FMSS_S5L8740 for N31 restore / LBA mapping experiments. +config APPLE_MIKEYBUS + tristate "Apple MikeyBus headset jack (N31 UART2)" + depends on SERIAL_DEV_BUS + help + MikeyBus on UART2 (GPIO 66/67): headset model ID (open circuit / + A18/B18/…) and remote button RX. Not Tristar; not CS42 amp. + Baud/protocol OPEN until accessory snap. force_plugged for glass. + config APPLE_TRISTAR_CBTL1609 tristate "Apple Lightning Tristar (CBTL1609A1) mux" depends on I2C @@ -675,8 +683,8 @@ config APPLE_TRISTAR_CBTL1609 (CONFIG_USB host is off, so drivers/usb/misc is invisible). config S5L8740_IIS2_MMIO - tristate "S5L8740 IIS2 FM MMIO hook (N31)" - depends on HAS_IOMEM + tristate "S5L8740 IIS2 FM MMIO hook (obsolete)" + depends on HAS_IOMEM && BROKEN help - IIS2 @0x3D400000 FM digital RX hook — MMIO regs sysfs only. - Capture PCM register model still OPEN per N31 RE. + Obsolete. Use CONFIG_SND_SOC_APPLE_S5L8740_IIS2 instead. + diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index f4df4f82c213b6..e554cd659478cd 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -75,6 +75,7 @@ lan966x-pci-objs := lan966x_pci.o lan966x-pci-objs += lan966x_pci.dtbo.o obj-$(CONFIG_MCHP_LAN966X_PCI) += lan966x-pci.o obj-y += keba/ +obj-$(CONFIG_APPLE_MIKEYBUS) += apple-mikeybus.o obj-$(CONFIG_APPLE_TRISTAR_CBTL1609) += apple-tristar-cbtl1609.o obj-$(CONFIG_S5L8740_IIS2_MMIO) += s5l8740-iis2-mmio.o obj-$(CONFIG_FMSS_S5L8740) += fmss-s5l8740.o diff --git a/drivers/misc/apple-mikeybus.c b/drivers/misc/apple-mikeybus.c new file mode 100755 index 00000000000000..20f7d69d0d9153 --- /dev/null +++ b/drivers/misc/apple-mikeybus.c @@ -0,0 +1,681 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Apple MikeyBus — N31 headset jack model / remote (UART2 @ 0x3DC00000) + * + * RetailOS (osos 1.0.2): + * Tasks: CMikeyBusUartReadTask / CMikeyBusUartResistorTask (sub_35A4) + * UART open pinmux: sub_5714EE case 2 → GPIOCMD(0x42,2) + (0x43,2) + * = GPIO 66/67 func mode 2, then sub_428F70(0x42,1) + * Model table: sub_DCEC / mikeyTask.cpp — MEMORY[0x8925CD3] + * 1=A18 … 0xB=open circuit (unplugged) … 0x10=B187 + * headsetHasMikey: sub_40BE5C + * + * This is jack *identity* + remote (resistor/UART), not Tristar Lightning mux + * and not the CS42 HP amp itself. RetailOS HP mute is CS42 0x527; mixer + * bring-up sub_570620 is gated on headset state 0x8925CF4==1 — Linux CS42 + * audio_on already applies the HP sequence, but jack model must still be + * tracked so we do not treat open-circuit as headphones. + * + * Baud / byte protocol: OPEN until accessory MMIO snap. Default trial + * 115200 8N1 (family heuristic). force_model sysfs for glass bring-up. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MIKEY_UART_PHYS 0x3dc00000ul +#define MIKEY_UART_LEN 0x3c +#define GPIO_PHYS 0x3cf00000ul +#define GPIOCMD_PHYS 0x3cf001e0ul + +/* sub_5714EE case 2 */ +#define MIKEY_GPIO_TX 0x42u /* 66 */ +#define MIKEY_GPIO_RX 0x43u /* 67 */ + +#define MIKEY_MODEL_OPEN 0x0Bu +#define MIKEY_MODEL_A18 0x01u /* passive HP family */ + +/* Glass: analog HP is in. Resistor protocol is still OPEN. */ +static bool force_plugged_param = true; +module_param_named(force_plugged, force_plugged_param, bool, 0644); +MODULE_PARM_DESC(force_plugged, + "Treat jack as plugged until resistor task works (default 1)"); + +/* + * Live s5l-uart instantiate (pinmux then platform_device_add) locked + * glass 2026-08-27 — CPU died during samsung probe. UART2 is enabled + * from DT at boot (uart3 remains first). This param is ignored. + */ +static bool instantiate_uart2; +module_param(instantiate_uart2, bool, 0444); +MODULE_PARM_DESC(instantiate_uart2, + "ignored; live s5l-uart add locked glass — use DT uart2 okay"); + +struct apple_mikeybus { + struct device *dev; + struct serdev_device *serdev; + void __iomem *gpio; + void __iomem *gpiocmd; + struct mutex lock; + u8 model; /* 0x8925CD3 mirror */ + bool force_plugged; /* glass: ignore open-circuit until resistor RE */ + bool pinmux_on; + u32 baud; + u32 rx_bytes; + u8 rx_last[64]; + unsigned int rx_last_len; + bool uart_opened; +}; + +static struct apple_mikeybus *mikeybus_singleton; +static DEFINE_MUTEX(mikeybus_singleton_lock); +static struct platform_device *mikey_plat_pdev; +static void mikey_ensure_plat(struct work_struct *work); +static DECLARE_WORK(mikey_plat_work, mikey_ensure_plat); + +/* sub_DCEC name table (non-LVTM branch). */ +static const char *mikey_model_name(u8 model) +{ + switch (model) { + case 1: return "A18"; + case 2: return "B18"; + case 3: return "A62"; + case 4: return "B15"; + case 5: return "A36"; + case 6: return "Apple noise occluding"; + case 7: return "mfg noise occluding"; + case 8: return "mfg noise occluding w/ mic"; + case 9: return "mfg std"; + case 0xA: return "mfg std w/ mic"; + case 0xB: return "open circuit"; + case 0xD: return "B60f"; + case 0xE: return "B60g"; + case 0xF: return "B149"; + case 0x10: return "B187"; + default: return "inscrutable"; + } +} + +/* sub_40BE5C — models expected to speak Mikey UART remote. */ +static bool mikey_headset_has_remote(u8 model) +{ + switch (model) { + case 2: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 0xA: + case 0xD: + case 0xE: + case 0x10: + return true; + default: + return false; + } +} + +static bool mikey_headset_ready_locked(struct apple_mikeybus *m) +{ + if (m->force_plugged) + return true; + /* + * 0xB is RetailOS "open circuit" only after the resistor task. + * Unmeasured model 0 is not unplugged — analog HP may already be in. + */ + if (m->model == MIKEY_MODEL_OPEN) + return false; + return true; +} + +static bool mikey_jack_present_locked(struct apple_mikeybus *m) +{ + if (m->force_plugged) + return true; + if (m->model == 0 || m->model == MIKEY_MODEL_OPEN) + return false; + return true; +} + +/** + * apple_mikeybus_jack_present - headphones / headset tip present + * Return: 1 present, 0 open circuit / unknown, -ENODEV if no driver + */ +int apple_mikeybus_jack_present(void) +{ + int ret; + + mutex_lock(&mikeybus_singleton_lock); + if (!mikeybus_singleton) { + mutex_unlock(&mikeybus_singleton_lock); + return -ENODEV; + } + mutex_lock(&mikeybus_singleton->lock); + ret = mikey_jack_present_locked(mikeybus_singleton) ? 1 : 0; + mutex_unlock(&mikeybus_singleton->lock); + mutex_unlock(&mikeybus_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_mikeybus_jack_present); + +/** + * apple_mikeybus_headset_ready - RetailOS 0x8925CF4 gate for sub_570620 + * Return: 1 ready, 0 not ready, -ENODEV if no driver + */ +int apple_mikeybus_headset_ready(void) +{ + int ret; + + mutex_lock(&mikeybus_singleton_lock); + if (!mikeybus_singleton) { + mutex_unlock(&mikeybus_singleton_lock); + return -ENODEV; + } + mutex_lock(&mikeybus_singleton->lock); + ret = mikey_headset_ready_locked(mikeybus_singleton) ? 1 : 0; + mutex_unlock(&mikeybus_singleton->lock); + mutex_unlock(&mikeybus_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_mikeybus_headset_ready); + +static void mikey_gpiocmd(struct apple_mikeybus *m, u8 gpio, u8 mode) +{ + u32 bank = gpio >> 3; + u32 pin = gpio & 7; + + writel((bank << 16) | (pin << 8) | mode, m->gpiocmd); +} + +/* sub_5714EE(UART2): mode 2 on 66/67. Close path uses mode 0xFFFE (65534). */ +static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) +{ + u32 bank, pin, dir; + void __iomem *b; + + if (!m->gpio || !m->gpiocmd) + return; + + if (on) { + /* mode 2 → DIR out + GPIOCMD mode byte (sub_43D38C) */ + bank = MIKEY_GPIO_TX >> 3; + pin = MIKEY_GPIO_TX & 7; + b = m->gpio + 32 * bank; + dir = readl(b + 0x14); + writel(dir | BIT(pin), b + 0x14); + mikey_gpiocmd(m, MIKEY_GPIO_TX, 2); + + bank = MIKEY_GPIO_RX >> 3; + pin = MIKEY_GPIO_RX & 7; + b = m->gpio + 32 * bank; + dir = readl(b + 0x14); + writel(dir | BIT(pin), b + 0x14); + mikey_gpiocmd(m, MIKEY_GPIO_RX, 2); + m->pinmux_on = true; + } else { + /* mode 0xFFFE: clear DIR, cmd 0 (sub_571374 close) */ + bank = MIKEY_GPIO_TX >> 3; + pin = MIKEY_GPIO_TX & 7; + b = m->gpio + 32 * bank; + dir = readl(b + 0x14); + writel(dir & ~BIT(pin), b + 0x14); + mikey_gpiocmd(m, MIKEY_GPIO_TX, 0); + + bank = MIKEY_GPIO_RX >> 3; + pin = MIKEY_GPIO_RX & 7; + b = m->gpio + 32 * bank; + dir = readl(b + 0x14); + writel(dir & ~BIT(pin), b + 0x14); + mikey_gpiocmd(m, MIKEY_GPIO_RX, 0); + m->pinmux_on = false; + } +} + +/* + * When the running DTB still has uart2 disabled, serdev never probes. + * Bind a platform device so headset_ready() is 1 (force_plugged) instead + * of -ENODEV. Do NOT platform_device_add("s5l-uart") — that locked glass + * (samsung probe after GPIO 66/67 pinmux, 2026-08-27). UART2 itself is + * enabled from DT at boot, uart3 first. + */ +static void mikey_ensure_plat(struct work_struct *work) +{ + struct device_node *uart_np, *mikey_np = NULL; + int ret; + + (void)work; + + if (mikeybus_singleton) + return; + + uart_np = of_find_node_by_path("/soc/serial@3dc00000"); + if (uart_np && of_device_is_available(uart_np)) { + pr_info("apple-mikeybus: uart2 okay in DT — waiting on serdev\n"); + of_node_put(uart_np); + return; + } + if (uart_np) + mikey_np = of_get_child_by_name(uart_np, "mikeybus"); + + mikey_plat_pdev = platform_device_alloc("apple-mikeybus-plat", + PLATFORM_DEVID_NONE); + if (!mikey_plat_pdev) + goto out; + if (mikey_np) + mikey_plat_pdev->dev.of_node = of_node_get(mikey_np); + ret = platform_device_add(mikey_plat_pdev); + if (ret) { + pr_warn("apple-mikeybus: plat add %d\n", ret); + platform_device_put(mikey_plat_pdev); + mikey_plat_pdev = NULL; + } else { + pr_info("apple-mikeybus: platform bind (uart2 still DT-disabled)\n"); + } +out: + if (mikey_np) + of_node_put(mikey_np); + if (uart_np) + of_node_put(uart_np); +} + +static ssize_t model_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 model; + + mutex_lock(&m->lock); + model = m->model; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "0x%02x %s\n", model, mikey_model_name(model)); +} + +static ssize_t model_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret || v > 0xff) + return -EINVAL; + mutex_lock(&m->lock); + m->model = (u8)v; + mutex_unlock(&m->lock); + dev_info(dev, "model set 0x%02x (%s) has_remote=%d plugged=%d\n", + m->model, mikey_model_name(m->model), + mikey_headset_has_remote(m->model), + mikey_jack_present_locked(m)); + return count; +} +static DEVICE_ATTR_RW(model); + +static ssize_t plugged_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + int p; + + mutex_lock(&m->lock); + p = mikey_jack_present_locked(m); + mutex_unlock(&m->lock); + return sysfs_emit(buf, "%d\n", p); +} +static DEVICE_ATTR_RO(plugged); + +static ssize_t force_plugged_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", m->force_plugged); +} + +static ssize_t force_plugged_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret) + return ret; + mutex_lock(&m->lock); + m->force_plugged = !!v; + if (m->force_plugged && + (m->model == 0 || m->model == MIKEY_MODEL_OPEN)) + m->model = MIKEY_MODEL_A18; + mutex_unlock(&m->lock); + return count; +} +static DEVICE_ATTR_RW(force_plugged); + +static ssize_t pinmux_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", m->pinmux_on); +} + +static ssize_t pinmux_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret) + return ret; + mutex_lock(&m->lock); + mikey_pinmux_uart(m, !!v); + mutex_unlock(&m->lock); + return count; +} +static DEVICE_ATTR_RW(pinmux); + +static ssize_t baud_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", m->baud); +} + +static ssize_t baud_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret || !v) + return -EINVAL; + mutex_lock(&m->lock); + m->baud = v; + if (m->serdev) + serdev_device_set_baudrate(m->serdev, v); + mutex_unlock(&m->lock); + return count; +} +static DEVICE_ATTR_RW(baud); + +static ssize_t rx_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + ssize_t n; + + mutex_lock(&m->lock); + n = sysfs_emit(buf, "bytes=%u last_len=%u last=%*ph\n", + m->rx_bytes, m->rx_last_len, + m->rx_last_len, m->rx_last); + mutex_unlock(&m->lock); + return n; +} +static DEVICE_ATTR_RO(rx); + +static ssize_t uart_open_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", m->uart_opened); +} + +static ssize_t uart_open_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret) + return ret; + + mutex_lock(&m->lock); + if (v && !m->uart_opened) { + if (!m->serdev) { + mutex_unlock(&m->lock); + return -ENODEV; + } + ret = serdev_device_open(m->serdev); + if (ret) { + mutex_unlock(&m->lock); + return ret; + } + serdev_device_set_baudrate(m->serdev, m->baud); + serdev_device_set_flow_control(m->serdev, false); + m->uart_opened = true; + dev_info(dev, "Mikey UART opened baud=%u\n", m->baud); + } else if (!v && m->uart_opened) { + serdev_device_close(m->serdev); + m->uart_opened = false; + dev_info(dev, "Mikey UART closed\n"); + } + mutex_unlock(&m->lock); + return count; +} +static DEVICE_ATTR_RW(uart_open); + +static ssize_t info_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, + "MikeyBus UART2 @0x3DC GPIO 66/67\n" + "model=0x%02x (%s) remote=%d plugged=%d force=%d\n" + "pinmux=%d baud=%u uart_open=%d rx_bytes=%u\n" + "protocol baud OPEN — resistor task RE pending\n", + m->model, mikey_model_name(m->model), + mikey_headset_has_remote(m->model), + mikey_jack_present_locked(m), m->force_plugged, + m->pinmux_on, m->baud, m->uart_opened, m->rx_bytes); +} +static DEVICE_ATTR_RO(info); + +static struct attribute *mikey_attrs[] = { + &dev_attr_model.attr, + &dev_attr_plugged.attr, + &dev_attr_force_plugged.attr, + &dev_attr_pinmux.attr, + &dev_attr_baud.attr, + &dev_attr_rx.attr, + &dev_attr_uart_open.attr, + &dev_attr_info.attr, + NULL, +}; +ATTRIBUTE_GROUPS(mikey); + +static size_t mikey_serdev_receive(struct serdev_device *serdev, + const u8 *data, size_t count) +{ + struct apple_mikeybus *m = serdev_device_get_drvdata(serdev); + size_t n; + + if (!m || !count) + return count; + + mutex_lock(&m->lock); + m->rx_bytes += count; + n = min(count, sizeof(m->rx_last)); + memcpy(m->rx_last, data + count - n, n); + m->rx_last_len = n; + /* Protocol OPEN — log only until resistor/remote decode lands. */ + dev_info(m->dev, "Mikey RX %zu: %*ph\n", count, (int)min(count, 16), + data); + mutex_unlock(&m->lock); + return count; +} + +static const struct serdev_device_ops mikey_serdev_ops = { + .receive_buf = mikey_serdev_receive, +}; + +static int mikey_bind(struct device *dev, struct serdev_device *serdev) +{ + struct apple_mikeybus *m; + u32 baud = 115200; + int ret; + + m = devm_kzalloc(dev, sizeof(*m), GFP_KERNEL); + if (!m) + return -ENOMEM; + + m->dev = dev; + m->serdev = serdev; + m->baud = baud; + m->model = 0; + m->force_plugged = force_plugged_param || + of_property_read_bool(dev->of_node, + "apple,force-plugged"); + if (dev->of_node && + !of_property_read_u32(dev->of_node, "current-speed", &baud)) + m->baud = baud; + if (m->force_plugged) + m->model = MIKEY_MODEL_A18; + + mutex_init(&m->lock); + m->gpio = devm_ioremap(dev, GPIO_PHYS, 0x200); + m->gpiocmd = devm_ioremap(dev, GPIOCMD_PHYS, 4); + if (!m->gpio || !m->gpiocmd) + dev_warn(dev, "GPIO/GPIOCMD map failed — pinmux sysfs limited\n"); + + dev_set_drvdata(dev, m); + if (serdev) { + serdev_device_set_drvdata(serdev, m); + serdev_device_set_client_ops(serdev, &mikey_serdev_ops); + } + + mikey_pinmux_uart(m, true); + + ret = sysfs_create_groups(&dev->kobj, mikey_groups); + if (ret) + dev_warn(dev, "sysfs: %d\n", ret); + + mutex_lock(&mikeybus_singleton_lock); + mikeybus_singleton = m; + mutex_unlock(&mikeybus_singleton_lock); + + dev_info(dev, + "MikeyBus ready (%s) baud=%u model=0x%02x (%s) force_plugged=%d\n", + serdev ? "serdev, UART not opened" : "platform, uart2 bound", + m->baud, m->model, mikey_model_name(m->model), + m->force_plugged); + return 0; +} + +static void mikey_unbind(struct device *dev) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + if (!m) + return; + mutex_lock(&mikeybus_singleton_lock); + if (mikeybus_singleton == m) + mikeybus_singleton = NULL; + mutex_unlock(&mikeybus_singleton_lock); + + sysfs_remove_groups(&dev->kobj, mikey_groups); + mikey_pinmux_uart(m, false); + if (m->uart_opened && m->serdev) { + serdev_device_close(m->serdev); + m->uart_opened = false; + } +} + +static int mikey_serdev_probe(struct serdev_device *serdev) +{ + return mikey_bind(&serdev->dev, serdev); +} + +static void mikey_serdev_remove(struct serdev_device *serdev) +{ + mikey_unbind(&serdev->dev); +} + +static const struct of_device_id mikey_serdev_of_match[] = { + { .compatible = "apple,mikeybus" }, + { .compatible = "apple,n31-mikeybus" }, + { } +}; +MODULE_DEVICE_TABLE(of, mikey_serdev_of_match); + +static struct serdev_device_driver mikey_serdev_driver = { + .probe = mikey_serdev_probe, + .remove = mikey_serdev_remove, + .driver = { + .name = "apple-mikeybus", + .of_match_table = mikey_serdev_of_match, + }, +}; + +static int mikey_plat_probe(struct platform_device *pdev) +{ + return mikey_bind(&pdev->dev, NULL); +} + +static void mikey_plat_remove(struct platform_device *pdev) +{ + mikey_unbind(&pdev->dev); +} + +static struct platform_driver mikey_plat_driver = { + .probe = mikey_plat_probe, + .remove = mikey_plat_remove, + .driver = { + .name = "apple-mikeybus-plat", + }, +}; + +static int __init mikey_init(void) +{ + int ret; + + ret = serdev_device_driver_register(&mikey_serdev_driver); + if (ret) + return ret; + ret = platform_driver_register(&mikey_plat_driver); + if (ret) { + serdev_device_driver_unregister(&mikey_serdev_driver); + return ret; + } + if (instantiate_uart2) + pr_warn("apple-mikeybus: instantiate_uart2 ignored (live s5l-uart add locked glass)\n"); + schedule_work(&mikey_plat_work); + return 0; +} + +static void __exit mikey_exit(void) +{ + cancel_work_sync(&mikey_plat_work); + if (mikey_plat_pdev) { + platform_device_unregister(mikey_plat_pdev); + mikey_plat_pdev = NULL; + } + platform_driver_unregister(&mikey_plat_driver); + serdev_device_driver_unregister(&mikey_serdev_driver); +} + +module_init(mikey_init); +module_exit(mikey_exit); + +MODULE_DESCRIPTION("Apple MikeyBus headset jack model/remote (N31 UART2)"); +MODULE_AUTHOR("FreeMyiPod"); +MODULE_LICENSE("GPL"); diff --git a/drivers/misc/fmss-s5l8740-api.h b/drivers/misc/fmss-s5l8740-api.h index f4775c434c7611..539e98c6fe9cb2 100755 --- a/drivers/misc/fmss-s5l8740-api.h +++ b/drivers/misc/fmss-s5l8740-api.h @@ -1,11 +1,12 @@ /* SPDX-License-Identifier: GPL-2.0-only */ /* - * S5L8740 FMSS → FTL block layer export (Whimory read path). - * Consumed by ftl-s5l8740.ko; implemented by fmss-s5l8740.ko. + * S5L8740 FMSS FIL export — raw PPN page I/O for the Whimory stack. * - * Logical disk: 4096-byte sectors, LPN = sector >> 2 (4 sectors / 16 KiB page). - * Build the dense L2V via sysfs l2v_build (or fmss_ftl_build_map); sector 0 is - * served from a carved *UOKJIHC BPB when present. Unmapped sectors return -ENOENT. + * fmss-s5l8740.ko owns the controller. whimory / ftl-s5l8740.ko owns + * FPart, VFL, SFTL, L2V, and the block device. + * + * fmss_ftl_read_sector() is a compatibility hook for apple-nimbus.ko. + * After Whimory opens successfully it registers the real LBA reader. */ #ifndef FMSS_S5L8740_API_H #define FMSS_S5L8740_API_H @@ -13,10 +14,33 @@ #include #include -/* Apple RetailOS FAT32 on N31 (4096-byte logical sectors). Override via module param. */ -#define FMSS_FTL_SECTOR_SIZE 4096U -#define FMSS_FTL_SECTORS_PER_LPN 4U -#define FMSS_FTL_DEFAULT_CAPACITY 3856968U +#define FMSS_FTL_SECTOR_SIZE 4096U +#define FMSS_FTL_SECTORS_PER_LPN 4U +#define FMSS_FTL_DEFAULT_CAPACITY 3856968U + +#define S5L8740_FMSS_MAX_CE 2U +#define S5L8740_FMSS_MAX_CAU 2U +#define S5L8740_FMSS_PAGE_SIZE 16384U +#define S5L8740_FMSS_META_SIZE 64U /* 4 × 16-byte SFTL slots */ + +struct s5l8740_fmss_geom { + u32 num_ce; + u32 num_cau; + u32 blocks_per_cau; + u32 pages_per_block; + u32 pages_per_block_slc; + u32 page_size; + u32 vfl_tail; + u32 page_bits; + u32 block_bits; + u32 cau_bits; + u32 caus_per_channel; + u32 dev_id; /* FIL selector 101 analogue */ + u32 geom_104; /* FIL selector 104 analogue */ + u32 geom_105; /* FIL selector 105 analogue */ + u32 geom_135; /* FIL selector 135 analogue */ + bool from_param_page; +}; bool fmss_ftl_present(void); struct device *fmss_ftl_device(void); @@ -24,4 +48,16 @@ unsigned int fmss_ftl_lpn_count(void); int fmss_ftl_build_map(unsigned int max_lpn); int fmss_ftl_read_sector(u64 logical_sector, void *buf); +u32 s5l8740_fmss_fil_get_info(u32 selector); +int s5l8740_fmss_available(void); +int s5l8740_fmss_hw_init(void); +int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g); +int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, + unsigned int slc, unsigned int chunks, + void *data, size_t data_len, + void *meta, size_t meta_len); +int s5l8740_fmss_nand_reset(void); +void s5l8740_fmss_register_ftl_read(int (*fn)(u64 lba, void *buf)); + #endif /* FMSS_S5L8740_API_H */ diff --git a/drivers/misc/fmss-s5l8740.c b/drivers/misc/fmss-s5l8740.c index 86e539067d96d6..7068bfb30bb118 100755 --- a/drivers/misc/fmss-s5l8740.c +++ b/drivers/misc/fmss-s5l8740.c @@ -98,7 +98,7 @@ #define FMSS_GREP_MAX_BLOCKS 64 #define FMSS_VFL_MAP_MAX 512 #define FMSS_L2V_DEFAULT_BLOCKS 256 -/* Map is keyed by page LPN (YaFTL); early LBAs also in early_lba_map. */ +/* Map is keyed by page LPN (YaFTL); full LBA map preferred for block I/O. */ #define FMSS_L2V_DEFAULT_MAX_LPN \ ((FMSS_FTL_DEFAULT_CAPACITY / FMSS_FTL_SECTORS_PER_LPN) + 64) @@ -108,12 +108,15 @@ #define WMR_MOUNT_MAX_BLOCKS 64u #define WMR_FTLCTRL_MAX 3u -/* Packed L2V entry: valid|ce[1:0]|cau[1:0]|sec[1:0]|block[11:0]|page[6:0] +/* Packed L2V entry: + * valid|ce[1:0]|cau[1:0]|PHYS|sec[1:0]|block[11:0]|page[6:0] * sec = 4K index within the NAND page (SFTL VBA). 0x3 = “use LBA%4”. + * PHYS: block is already physical (BTOC/BTE/META/carve) — do NOT VFL-remap. */ #define L2V_VALID BIT(31) #define L2V_CE_SHIFT 29 #define L2V_CAU_SHIFT 27 +#define L2V_PHYS BIT(26) /* already-physical block */ #define L2V_SEC_SHIFT 19 #define L2V_SEC_MASK 0x3u #define L2V_SEC_FROM_LBA 0x3u /* sentinel: derive sec from lba%4 */ @@ -121,6 +124,16 @@ #define L2V_PAGE_MASK 0x7fu #define L2V_BLOCK_MASK 0xfffu +/* Claim source priority for newest-wins (higher wins on equal weave). */ +enum { + L2V_SRC_NONE = 0, + L2V_SRC_CARVE = 1, + L2V_SRC_WMR = 2, /* classic virt block-map (may need VFL) */ + L2V_SRC_BTOC = 3, + L2V_SRC_BTE = 4, + L2V_SRC_META = 5, +}; + struct fmss_vfl_map { unsigned int cau; unsigned int virt; @@ -166,12 +179,27 @@ static unsigned int vfl_build_blocks = 32; module_param(vfl_build_blocks, uint, 0644); MODULE_PARM_DESC(vfl_build_blocks, "max blocks per CAU for vfl_build (default 32, tail only)"); +/* + * VFL remap of map entries that are NOT marked L2V_PHYS. + * BTOC/BTE/carve store physical scan blocks — remapping those double-translates. + * Default off until wrmx+0x100 is proven as direct virt→phys. + */ +static char vfl_remap_mode[16] = "off"; +module_param_string(vfl_remap_mode, vfl_remap_mode, sizeof(vfl_remap_mode), 0644); +MODULE_PARM_DESC(vfl_remap_mode, + "off (default) | direct256 | tail_only — VFL remap for non-PHYS map entries"); + +static unsigned int vfl_remap_applied; +static unsigned int vfl_remap_skipped_phys; + /* Tiny list for sysfs lpn_index (debug); dense map is authoritative. */ static struct fmss_lpn_map lpn_index[FMSS_LPN_INDEX_MAX]; static unsigned int lpn_index_count; /* Dense LPN → physical page map (vzalloc). */ static u32 *l2v_map; +static u64 *l2v_weave; +static u8 *l2v_src; static unsigned int l2v_map_size; static unsigned int l2v_mapped; static unsigned int l2v_max_lpn; @@ -179,6 +207,28 @@ static unsigned int l2v_btoc_hits; static unsigned int l2v_bmap_hits; static unsigned int l2v_meta_hits; +/* + * Full LBA → packed phys+sec map (SFTL BTE / boot carve). Preferred over LPN + * dense map for block I/O. + * + * WARNING: FMSS_FTL_DEFAULT_CAPACITY is ~3.8M *sectors* (~15GB media). A dense + * map of that size is ~50MB+ RAM (u32+u64+u8) and OOMs N31 before RNDIS. + * Cap with lba_map_max (default 262144 ≈ 1GB LBA space ≈ 3.4MB RAM). + */ +#define FMSS_LBA_MAP_HARDMAX FMSS_FTL_DEFAULT_CAPACITY +static unsigned int lba_map_max = 262144; +module_param(lba_map_max, uint, 0644); +MODULE_PARM_DESC(lba_map_max, + "max LBA entries for dense map (default 262144; full media is ~3.8M / ~50MB)"); +static u32 *lba_map; +static u64 *lba_weave; +static u8 *lba_src; +static unsigned int lba_mapped; + +/* Last resolution chain for sysfs resolve_log. */ +static char resolve_log[512]; +static unsigned int resolve_log_len; + /* Carved Apple FAT boot (*UOKJIHC); sector 0 served from here. */ static bool boot_carve_valid; static unsigned int boot_carve_ce; @@ -210,11 +260,20 @@ MODULE_PARM_DESC(boot_carve_block, "cached aligned boot block (0=BTOC hunt for LPN0)"); /* Default l2v_build width when sysfs omits NBLOCKS (user blocks to scan). */ -static unsigned int l2v_auto_blocks = 512; +static unsigned int l2v_auto_blocks = 256; module_param(l2v_auto_blocks, uint, 0644); MODULE_PARM_DESC(l2v_auto_blocks, "default l2v_build block span per CE/CAU (0=carve/boot hunt only)"); +/* + * Legacy dense-map META ingest. Off unless l2v_build/whimory_mount is + * running — FIL reads from the Whimory driver must not allocate the + * ~50MB LBA map as a side effect. + */ +static bool fmss_legacy_meta_ingest; + +static int (*fmss_ftl_read_hook)(u64 lba, void *buf); + /* Root-dir physical page when LPN(DataStart) is otherwise unmapped. */ static bool root_dir_valid; static unsigned int root_dir_ce; @@ -265,12 +324,14 @@ struct fmss_n31 { int last_page_ret; int last_page_chunk; unsigned int last_page_len; + unsigned int last_clean_chunks; u8 last_param[FMSS_PARAM_LEN]; int last_param_ce; int last_param_ret; u32 last_stat48; u32 last_nandstat; unsigned int pages_since_reset; + unsigned int ecc_soft_fails; int dma_ok; int dma_mapped; struct device *dev; @@ -332,9 +393,22 @@ static unsigned int page_chunks = 16; module_param(page_chunks, uint, 0644); MODULE_PARM_DESC(page_chunks, "1K PIO chunks per page_read (16=full 16KiB data)"); -static unsigned int spare_len = 16; +static unsigned int spare_len = 64; module_param(spare_len, uint, 0644); -MODULE_PARM_DESC(spare_len, "extra PIO bytes after data (OSOS meta is 16)"); +MODULE_PARM_DESC(spare_len, + "PIO META bytes after data (default 64 = 4×16B slots; was 16)"); + +/* + * 50D960 parity FIFO is 53 bytes per 1K: 16B host spare (Sogeti META) + ECC. + * Draining it before 4EB458 steals the syndrome (whitened data). A second + * 50D960 with ecc_before_drain=0 copies META and keeps the first pass's data. + * Extra style-1 FMLEN=15 beats after a style-0 page returned 0xFF and made + * the next programmed page ECC-clean (glass 2026-08-27). + */ +static bool meta_pass = true; +module_param(meta_pass, bool, 0644); +MODULE_PARM_DESC(meta_pass, + "Second 50D960 to stash 53-byte parity into META slots (default Y)"); /* * 4EDDDC: D14 = (v40 ? 8D102F0 : 8D102EC) - 1. @@ -744,6 +818,10 @@ static int fmss_ecc_chunk(struct fmss_n31 *f, unsigned int seed_a1) * Per 1KiB chunk: parity beat → data xfer wait → 4EB458 → PIO drain. * Linux previously drained before ECC — that left DATA pages whitened. */ +static void fmss_meta_ingest_spare(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, unsigned int slot0); + static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) { int i, ret = -EIO, ecc_ret; @@ -759,10 +837,13 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) chunks = 3; memset(dst, 0, FMSS_PAGE_LEN); + memset(f->last_spare, 0, sizeof(f->last_spare)); + f->last_spare_len = 0; f->last_page_ce = (int)ce; f->last_page_addr = addr; f->last_page_chunk = -1; f->last_page_len = 0; + f->last_clean_chunks = 0; writel(7, f->base + FMUNK38); writel(fmss_page_ctrl0(ce), f->base + FMCTRL0); @@ -828,11 +909,31 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) memset(dig, 0, sizeof(dig)); if (!fmss_pio_read(f, dig, 53)) { - memcpy(f->last_spare, dig, 53); - f->last_spare_len = 53; + memcpy(f->last_parity[i], dig, 53); + f->last_parity_len[i] = 53; + /* + * Host-visible spare is the first 16 of + * the 53-byte beat, once per 4K slot. + */ + if ((i & 3) == 0) { + unsigned int slot = (unsigned int)i / 4u; + unsigned int pick = 0; + + if (slot < 4) { + if (dig[0] != 0x30) { + if (dig[16] == 0x30) + pick = 16; + else if (dig[32] == 0x30) + pick = 32; + } + memcpy(f->last_spare + slot * 16, + dig + pick, 16); + f->last_spare_len = (slot + 1) * 16; + } + } if (!quiet) - fmss_info("stripped parity-fifo %02x%02x%02x%02x…\n", - dig[0], dig[1], dig[2], dig[3]); + fmss_info("stripped parity-fifo ch=%d %02x%02x%02x%02x…\n", + i, dig[0], dig[1], dig[2], dig[3]); } } } @@ -858,17 +959,21 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) if (with_parity && ecc_before_drain) { ecc_ret = fmss_ecc_chunk(f, 0); if (ecc_ret == 1) { - /* Clean page — leave zeros already in dst. */ - f->last_page_ret = 0; - f->last_page_len = chunks * FMSS_CHUNK; - fmss_cmd(f, 0x77); - writel(0, f->base + FMCTRL0); - return 0; + /* + * Chunk is erased/clean — leave zeros and + * keep going. Aborting the whole page on + * chunk0 made FPart/VFL miss SLC specials + * (glass: 4096 tail reads, tag30=0). + */ + f->last_clean_chunks++; + continue; } if (ecc_ret) { - /* Fall through to raw drain — better than wedge. */ - pr_info("s5l8740-fmss: ECC soft-fail ce=%u addr=%08x chunk=%d ret=%d (raw drain)\n", - ce, addr, i, ecc_ret); + /* Blank BTOC pages are normal during l2v_build — count, don't spam. */ + f->ecc_soft_fails++; + if (!quiet && i == 0) + fmss_info("ECC soft-fail ce=%u addr=%08x ret=%d (raw drain)\n", + ce, addr, ecc_ret); } } if (fmss_pio_read(f, dst + i * FMSS_CHUNK, FMSS_CHUNK)) { @@ -878,21 +983,25 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) } } - memset(f->last_spare, 0, sizeof(f->last_spare)); - f->last_spare_len = 0; - if (spare_len && spare_len <= sizeof(f->last_spare)) { - if (fmss_data_in(f, f->last_spare, spare_len)) { - pr_info("s5l8740-fmss: spare timeout ce=%u addr=%08x len=%u\n", - ce, addr, spare_len); - } else { - f->last_spare_len = spare_len; - } - } + /* + * Trailing fmss_data_in(64) after 16×1K is an empty FIFO (zeros) and + * must not be treated as META (that polluted lba_map with type 0x00). + * Extra style-1 FMLEN=15 beats after this path desynced the next page. + */ fmss_cmd(f, 0x77); writel(0, f->base + FMCTRL0); f->last_page_ret = 0; f->last_page_len = chunks * FMSS_CHUNK; + /* Full-page PIO: ingest all META slots into lba_map (pass 2). */ + if (chunks >= 16 && f->last_spare_len >= 16) { + unsigned int pg = addr & L2V_PAGE_MASK; + unsigned int blk = (addr >> FMSS_PAGE_BITS) & L2V_BLOCK_MASK; + unsigned int cau = (addr >> (FMSS_PAGE_BITS + FMSS_BLOCK_BITS)) & + ((1u << FMSS_CAU_BITS) - 1u); + + fmss_meta_ingest_spare(f, ce, cau, blk, pg, 0); + } return 0; fail_ctrl0: @@ -901,6 +1010,56 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) return ret; } +/* + * FIL needs both descrambled data (pass 1, ECC) and 16B Sogeti META + * (pass 2, drain 53-byte parity FIFO). Mutex is already held. + */ +static int fmss_page_read_with_meta(struct fmss_n31 *f, unsigned int ce, + u32 addr) +{ + static u8 page_bak[FMSS_PAGE_LEN]; + unsigned int dlen; + int ret, ret2; + bool saved_ecc; + int saved_ce, saved_chunk; + u32 saved_addr; + + ret = fmss_page_read(f, ce, addr); + if (ret || !meta_pass) + return ret; + /* + * Erased PPN page: all 16 chunks ECC-clean. Spare is 0xFF; a second + * 50D960 only burns the controller (tail+brute are mostly empty). + */ + if (f->last_clean_chunks && + f->last_clean_chunks >= (page_chunks ? page_chunks : 16)) { + memset(f->last_spare, 0xff, sizeof(f->last_spare)); + f->last_spare_len = 64; + return ret; + } + dlen = f->last_page_len; + if (dlen > FMSS_PAGE_LEN) + dlen = FMSS_PAGE_LEN; + memcpy(page_bak, f->last_page, dlen); + saved_ce = f->last_page_ce; + saved_addr = f->last_page_addr; + saved_chunk = f->last_page_chunk; + saved_ecc = ecc_before_drain; + ecc_before_drain = false; + ret2 = fmss_page_read(f, ce, addr); + ecc_before_drain = saved_ecc; + memcpy(f->last_page, page_bak, dlen); + f->last_page_len = dlen; + f->last_page_ce = saved_ce; + f->last_page_addr = saved_addr; + f->last_page_chunk = saved_chunk; + f->last_page_ret = ret; + if (ret2 && !quiet) + pr_info("s5l8740-fmss: meta pass ce=%u addr=%08x ret=%d (data kept)\n", + ce, addr, ret2); + return ret; +} + /* * OSOS 4EDDDC / D39EC: FMSS command-list DMA page read. * Sequence program is the OSOS blob at 0x8980EA0 (embedded). @@ -1220,6 +1379,14 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) cl[0], cl[1], cl[2], cl[3], cl[4], cl[5], cl[6], cl[7], cl[8], (u32)f->seq_dma, (u32)f->cmdl_dma, (u32)f->data_dma, (u32)f->spare_dma); + if (!ret && f->last_spare_len >= 16) { + unsigned int pg = addr & L2V_PAGE_MASK; + unsigned int blk = (addr >> FMSS_PAGE_BITS) & L2V_BLOCK_MASK; + unsigned int cau = (addr >> (FMSS_PAGE_BITS + FMSS_BLOCK_BITS)) & + ((1u << FMSS_CAU_BITS) - 1u); + + fmss_meta_ingest_spare(f, ce, cau, blk, pg, dma_slot); + } return ret; } @@ -1336,11 +1503,19 @@ static int fmss_nand_reset(struct fmss_n31 *f) unsigned int ce; int ret; + /* Abort a wedged 50D960 (glass: STAT48 stuck 0x080c3002, FIL -110). */ + writel(0, f->base + FMCTRL0); + writel(0x0FF00FFE, f->base + FMSTAT48); + writel(13, f->base + FMSEQIRQ); + udelay(100); + writel(1, f->base + FMCTRL0); + ret = fmss_ctrl_reset(f); if (ret) return ret; for (ce = 0; ce < 2; ce++) { writel((2u * (1u << ce)) | 0xFF001u, f->base + FMCTRL0); + writel(2, f->base + FMSTAT48); if (fmss_cmd(f, 0xff)) pr_info("s5l8740-fmss: cmd 0xFF timeout ce=%u st=%08x\n", ce, f->last_stat48); @@ -2162,20 +2337,24 @@ static unsigned int fmss_bpb_data_start(const u8 *bpb) static u32 fmss_l2v_pack_sec(unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, - unsigned int sec) + unsigned int sec, bool phys) { - return L2V_VALID | - ((ce & 3u) << L2V_CE_SHIFT) | - ((cau & 3u) << L2V_CAU_SHIFT) | - ((sec & L2V_SEC_MASK) << L2V_SEC_SHIFT) | - ((block & L2V_BLOCK_MASK) << L2V_BLOCK_SHIFT) | - (page & L2V_PAGE_MASK); + u32 e = L2V_VALID | + ((ce & 3u) << L2V_CE_SHIFT) | + ((cau & 3u) << L2V_CAU_SHIFT) | + ((sec & L2V_SEC_MASK) << L2V_SEC_SHIFT) | + ((block & L2V_BLOCK_MASK) << L2V_BLOCK_SHIFT) | + (page & L2V_PAGE_MASK); + + if (phys) + e |= L2V_PHYS; + return e; } static u32 fmss_l2v_pack(unsigned int ce, unsigned int cau, - unsigned int block, unsigned int page) + unsigned int block, unsigned int page, bool phys) { - return fmss_l2v_pack_sec(ce, cau, block, page, L2V_SEC_FROM_LBA); + return fmss_l2v_pack_sec(ce, cau, block, page, L2V_SEC_FROM_LBA, phys); } static void fmss_l2v_unpack_sec(u32 e, unsigned int *ce, unsigned int *cau, @@ -2198,6 +2377,18 @@ static void fmss_l2v_unpack(u32 e, unsigned int *ce, unsigned int *cau, (void)sec; } +static bool fmss_claim_better(u8 old_src, u64 old_weave, u8 new_src, + u64 new_weave) +{ + if (!old_src) + return true; + if (new_weave > old_weave) + return true; + if (new_weave < old_weave) + return false; + return new_src > old_src; +} + static void fmss_wmr_map_free(void) { vfree(wmr_block_map); @@ -2205,10 +2396,55 @@ static void fmss_wmr_map_free(void) wmr_block_map_n = 0; } +static void fmss_lba_map_free(void) +{ + vfree(lba_map); + vfree(lba_weave); + vfree(lba_src); + lba_map = NULL; + lba_weave = NULL; + lba_src = NULL; + lba_mapped = 0; +} + +static unsigned int fmss_lba_map_cap(void) +{ + unsigned int cap = lba_map_max; + + if (!cap) + cap = 262144; + if (cap > FMSS_LBA_MAP_HARDMAX) + cap = FMSS_LBA_MAP_HARDMAX; + return cap; +} + +static int fmss_lba_map_ensure(void) +{ + unsigned int cap; + + if (lba_map) + return 0; + cap = fmss_lba_map_cap(); + lba_map = vzalloc(array_size(cap, sizeof(*lba_map))); + lba_weave = vzalloc(array_size(cap, sizeof(*lba_weave))); + lba_src = vzalloc(array_size(cap, sizeof(*lba_src))); + if (!lba_map || !lba_weave || !lba_src) { + fmss_lba_map_free(); + return -ENOMEM; + } + pr_info("fmss-s5l8740: LBA dense map cap=%u (~%u KiB)\n", + cap, (cap * (4 + 8 + 1)) / 1024); + return 0; +} + static void fmss_l2v_free(void) { vfree(l2v_map); + vfree(l2v_weave); + vfree(l2v_src); l2v_map = NULL; + l2v_weave = NULL; + l2v_src = NULL; l2v_map_size = 0; l2v_mapped = 0; l2v_max_lpn = 0; @@ -2222,19 +2458,35 @@ static int fmss_l2v_ensure(unsigned int max_lpn) { unsigned int need = max_lpn + 1; u32 *n; + u64 *nw; + u8 *ns; if (l2v_map && l2v_map_size >= need) { l2v_max_lpn = max_lpn; return 0; } n = vzalloc(array_size(need, sizeof(*n))); - if (!n) + nw = vzalloc(array_size(need, sizeof(*nw))); + ns = vzalloc(array_size(need, sizeof(*ns))); + if (!n || !nw || !ns) { + vfree(n); + vfree(nw); + vfree(ns); return -ENOMEM; + } if (l2v_map) { memcpy(n, l2v_map, l2v_map_size * sizeof(*n)); + if (l2v_weave) + memcpy(nw, l2v_weave, l2v_map_size * sizeof(*nw)); + if (l2v_src) + memcpy(ns, l2v_src, l2v_map_size * sizeof(*ns)); vfree(l2v_map); + vfree(l2v_weave); + vfree(l2v_src); } l2v_map = n; + l2v_weave = nw; + l2v_src = ns; l2v_map_size = need; l2v_max_lpn = max_lpn; return 0; @@ -2265,72 +2517,85 @@ static void fmss_l2v_index_note(unsigned int lpn, unsigned int ce, lpn_index_count++; } -static void fmss_l2v_set(unsigned int lpn, unsigned int ce, unsigned int cau, - unsigned int block, unsigned int page) +static void fmss_l2v_set_ex(unsigned int lpn, unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, bool phys, + u8 src, u64 weave) { - u32 prev; + u32 prev, packed; + u8 old_src; + u64 old_weave; if (!l2v_map || lpn >= l2v_map_size) return; prev = l2v_map[lpn]; - l2v_map[lpn] = fmss_l2v_pack(ce, cau, block, page); + old_src = l2v_src ? l2v_src[lpn] : 0; + old_weave = l2v_weave ? l2v_weave[lpn] : 0; + if ((prev & L2V_VALID) && + !fmss_claim_better(old_src, old_weave, src, weave)) + return; + packed = fmss_l2v_pack(ce, cau, block, page, phys); + l2v_map[lpn] = packed; + if (l2v_src) + l2v_src[lpn] = src; + if (l2v_weave) + l2v_weave[lpn] = weave; if (!(prev & L2V_VALID)) l2v_mapped++; fmss_l2v_index_note(lpn, ce, cau, block, page); + if (!quiet && l2v_mapped <= 8) + pr_info("s5l8740-fmss: l2v_set lpn=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u weave=%llx\n", + lpn, src, phys, ce, cau, block, page, + (unsigned long long)weave); } -/* Early LBA map (boot+FAT+root): SFTL LBA → packed phys+sec. */ -#define FMSS_EARLY_LBA_MAX 8192u -static u32 *early_lba_map; -static unsigned int early_lba_mapped; - -static void fmss_early_lba_free(void) +static void fmss_lba_set(unsigned int lba, unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, unsigned int sec, + bool phys, u8 src, u64 weave) { - vfree(early_lba_map); - early_lba_map = NULL; - early_lba_mapped = 0; -} + u32 prev, packed; + u8 old_src; + u64 old_weave; -static int fmss_early_lba_ensure(void) -{ - if (early_lba_map) - return 0; - early_lba_map = vzalloc(FMSS_EARLY_LBA_MAX * sizeof(*early_lba_map)); - return early_lba_map ? 0 : -ENOMEM; -} - -static void fmss_early_lba_set(unsigned int lba, unsigned int ce, - unsigned int cau, unsigned int block, - unsigned int page, unsigned int sec) -{ - u32 prev; - - if (lba >= FMSS_EARLY_LBA_MAX || fmss_early_lba_ensure()) + if (lba >= fmss_lba_map_cap() || fmss_lba_map_ensure()) + return; + prev = lba_map[lba]; + old_src = lba_src[lba]; + old_weave = lba_weave[lba]; + if ((prev & L2V_VALID) && + !fmss_claim_better(old_src, old_weave, src, weave)) return; - prev = early_lba_map[lba]; - early_lba_map[lba] = fmss_l2v_pack_sec(ce, cau, block, page, sec & 3u); + packed = fmss_l2v_pack_sec(ce, cau, block, page, sec & 3u, phys); + lba_map[lba] = packed; + lba_src[lba] = src; + lba_weave[lba] = weave; if (!(prev & L2V_VALID)) - early_lba_mapped++; + lba_mapped++; + if (!quiet && lba_mapped <= 8) + pr_info("s5l8740-fmss: lba_set lba=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u sec=%u weave=%llx\n", + lba, src, phys, ce, cau, block, page, sec & 3u, + (unsigned long long)weave); } -static int fmss_early_lba_lookup(unsigned int lba, unsigned int *ce, - unsigned int *cau, unsigned int *block, - unsigned int *page, unsigned int *sec) +static int fmss_lba_lookup(unsigned int lba, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page, unsigned int *sec, u32 *packed) { u32 e; - if (!early_lba_map || lba >= FMSS_EARLY_LBA_MAX) + if (!lba_map || lba >= fmss_lba_map_cap()) return -ENOENT; - e = early_lba_map[lba]; + e = lba_map[lba]; if (!(e & L2V_VALID)) return -ENOENT; fmss_l2v_unpack_sec(e, ce, cau, block, page, sec); + if (packed) + *packed = e; return 0; } -static int fmss_l2v_lookup(unsigned int lpn, unsigned int *ce, - unsigned int *cau, unsigned int *block, - unsigned int *page) +static int fmss_l2v_lookup_ex(unsigned int lpn, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page, u32 *packed) { u32 e; @@ -2340,9 +2605,98 @@ static int fmss_l2v_lookup(unsigned int lpn, unsigned int *ce, if (!(e & L2V_VALID)) return -ENOENT; fmss_l2v_unpack(e, ce, cau, block, page); + if (packed) + *packed = e; return 0; } +static int fmss_l2v_lookup(unsigned int lpn, unsigned int *ce, + unsigned int *cau, unsigned int *block, + unsigned int *page) +{ + return fmss_l2v_lookup_ex(lpn, ce, cau, block, page, NULL); +} + +static void fmss_early_lba_free(void) +{ + fmss_lba_map_free(); +} + +/* + * Pass 2: promote PIO/DMA META slots into full lba_map. + * type 0x01 data records; weave newest-wins via fmss_lba_set. + */ +static void fmss_meta_ingest_spare(struct fmss_n31 *f, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, unsigned int slot0) +{ + unsigned int s, nslots; + + if (!fmss_legacy_meta_ingest) + return; + if (!f || f->last_page_ret || f->last_spare_len < 16) + return; + if (fmss_lba_map_ensure()) + return; + nslots = f->last_spare_len / 16; + if (nslots > FMSS_VBAS_PER_PAGE) + nslots = FMSS_VBAS_PER_PAGE; + if (slot0 >= FMSS_VBAS_PER_PAGE) + slot0 = 0; + if (slot0 + nslots > FMSS_VBAS_PER_PAGE) + nslots = FMSS_VBAS_PER_PAGE - slot0; + for (s = 0; s < nslots; s++) { + const u8 *meta = f->last_spare + s * 16; + u32 lba, before; + u64 weave; + + if (meta[0] != 0x01) + continue; + lba = get_unaligned_le32(meta + 8); + if (lba >= fmss_lba_map_cap()) + continue; + weave = fmss_ppn_weave48(meta); + before = lba_map[lba]; + fmss_lba_set(lba, ce, cau, block, page, slot0 + s, true, + L2V_SRC_META, weave); + if (lba_map[lba] != before) + l2v_meta_hits++; + } +} + +/* Compat: physical BTOC-style set (legacy callers). */ +static void __maybe_unused fmss_l2v_set(unsigned int lpn, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page) +{ + fmss_l2v_set_ex(lpn, ce, cau, block, page, true, L2V_SRC_BTOC, 0); +} + +/* Thin wrapper — BTOC/carve physical fills. */ +static void __maybe_unused fmss_early_lba_set(unsigned int lba, unsigned int ce, + unsigned int cau, + unsigned int block, + unsigned int page, + unsigned int sec) +{ + fmss_lba_set(lba, ce, cau, block, page, sec, true, L2V_SRC_BTOC, 0); +} + +static int __maybe_unused fmss_early_lba_lookup(unsigned int lba, + unsigned int *ce, + unsigned int *cau, + unsigned int *block, + unsigned int *page, + unsigned int *sec) +{ + return fmss_lba_lookup(lba, ce, cau, block, page, sec, NULL); +} + +static int __maybe_unused fmss_early_lba_ensure(void) +{ + return fmss_lba_map_ensure(); +} + /* * Plausible YaFTL/Whimory BTOC: first entries are small LPNs and usually * sequential (we see 11,12,13 on blk 64 or 0,1,... on the boot superblock). @@ -2458,14 +2812,15 @@ static void fmss_l2v_ingest_btoc(struct fmss_n31 *f, unsigned int ce, */ continue; } - fmss_l2v_set(lpn, ce, cau, block, p); - /* Also fill early LBA map for boot/FAT (4 LBAs per page LPN). */ - if (lpn < FMSS_EARLY_LBA_MAX / FMSS_VBAS_PER_PAGE) { + fmss_l2v_set_ex(lpn, ce, cau, block, p, true, L2V_SRC_BTOC, 0); + /* Fill LBA map for all 4 sectors of this page LPN. */ + { unsigned int s; for (s = 0; s < FMSS_VBAS_PER_PAGE; s++) - fmss_early_lba_set(lpn * FMSS_VBAS_PER_PAGE + s, - ce, cau, block, p, s); + fmss_lba_set(lpn * FMSS_VBAS_PER_PAGE + s, + ce, cau, block, p, s, true, + L2V_SRC_BTOC, 0); } } } @@ -2503,19 +2858,25 @@ static void fmss_l2v_ingest_bte(struct fmss_n31 *f, unsigned int ce, const u8 *page, unsigned int max_lpn) { unsigned int i, recs, vba_ofs = 0, hit = 0; - unsigned int usable = 1024; /* page_chunks=1 BTOC walk only has 1 KiB */ + unsigned int usable = 1024; - (void)f; (void)max_lpn; if (!fmss_page_looks_bte(page)) return; - if (fmss_early_lba_ensure()) + if (fmss_lba_map_ensure()) return; + /* Prefer full page when available (pass 2: fill full lba_map). */ + if (f && f->last_page_len) + usable = f->last_page_len; + if (usable > FMSS_PAGE_LEN) + usable = FMSS_PAGE_LEN; recs = usable / 16; for (i = 0; i < recs; i++) { const u8 *r = page + i * 16; u32 lba = get_unaligned_be32(r + 8); u32 span = r[15]; + u64 weave = ((u64)get_unaligned_be32(r) << 16) | + (get_unaligned_be16(r + 4) & 0xffffu); unsigned int j; if (!span || span > 128) @@ -2530,8 +2891,9 @@ static void fmss_l2v_ingest_bte(struct fmss_n31 *f, unsigned int ce, if (pg >= FMSS_BTOC_PAGE) goto done; - if (cur < FMSS_EARLY_LBA_MAX) - fmss_early_lba_set(cur, ce, cau, block, pg, sec); + if (cur < fmss_lba_map_cap()) + fmss_lba_set(cur, ce, cau, block, pg, sec, + true, L2V_SRC_BTE, weave); vba_ofs++; } } @@ -2583,6 +2945,9 @@ static bool fmss_page_looks_block_map(const u8 *page, unsigned int len, } static unsigned int fmss_vfl_phys(unsigned int cau, unsigned int virt); +static unsigned int fmss_vfl_resolve(unsigned int cau, unsigned int virt); +static unsigned int fmss_map_to_phys(unsigned int cau, unsigned int block, + u32 packed); static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, unsigned int block, const u8 *hdr); static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, @@ -2921,7 +3286,9 @@ static unsigned int fmss_wmr_fill_l2v(unsigned int max_lpn) if (l2v_map && lpn < l2v_map_size && (l2v_map[lpn] & L2V_VALID)) continue; - fmss_l2v_set(lpn, ce, cau, block, page); + /* Classic WMR vpage already resolved to physical block. */ + fmss_l2v_set_ex(lpn, ce, cau, block, page, true, + L2V_SRC_WMR, 0); filled++; } return filled; @@ -2974,6 +3341,51 @@ static unsigned int fmss_vfl_phys(unsigned int cau, unsigned int virt) return virt; } +/* + * Resolve virt→phys according to vfl_remap_mode. + * Never call this for L2V_PHYS entries — use fmss_map_to_phys(). + */ +static unsigned int fmss_vfl_resolve(unsigned int cau, unsigned int virt) +{ + unsigned int phys, i; + + if (!strncmp(vfl_remap_mode, "off", 3)) + return virt; + + phys = fmss_vfl_phys(cau, virt); + if (phys == virt) + return virt; + + if (!strncmp(vfl_remap_mode, "tail_only", 9)) { + if (virt < FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL) + return virt; + } else if (strncmp(vfl_remap_mode, "direct256", 9)) { + /* Unknown mode → treat as off. */ + return virt; + } + + for (i = 0; i < vfl_map_count; i++) { + if (vfl_map[i].cau == cau && vfl_map[i].virt == virt) { + vfl_remap_applied++; + if (!quiet && vfl_remap_applied <= 32) + pr_info("s5l8740-fmss: vfl_remap mode=%s cau=%u in=%u out=%u idx=%u\n", + vfl_remap_mode, cau, virt, phys, i); + return phys; + } + } + return virt; +} + +static unsigned int fmss_map_to_phys(unsigned int cau, unsigned int block, + u32 packed) +{ + if (packed & L2V_PHYS) { + vfl_remap_skipped_phys++; + return block; + } + return fmss_vfl_resolve(cau, block); +} + /* * wrmx/xrmw VFLCxt: 512-byte header, u32 remap table begins @ +0x100. * Live pod: entries are LE phys block numbers (e.g. 0x827 = 2087). @@ -3177,18 +3589,19 @@ static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, u8 *dst, unsigned int dst_len) { - unsigned int vblock, saved; + unsigned int pblock, saved; u32 addr; int ret; - vblock = fmss_vfl_phys(cau, block); + /* Callers pass physical scan blocks (find_lpn / BTOC). Do not VFL-remap. */ + pblock = fmss_map_to_phys(cau, block, L2V_PHYS); saved = page_chunks; page_chunks = 16; if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); f->pages_since_reset = 0; } - addr = fmss_ppn_addr(cau, vblock, page, 0); + addr = fmss_ppn_addr(cau, pblock, page, 0); if (f->dma_ok && use_dma) { ret = fmss_dma_page_read(f, ce, addr); if (ret) @@ -3293,8 +3706,9 @@ static void fmss_boot_apply_bpb(struct fmss_n31 *f, unsigned int ce, boot_reserved_sects = 32; if (fatz && fatz < 0x100000u) boot_fat_sects = fatz; - /* L2V[0] must point at this real boot page. */ - fmss_l2v_set(0, ce, cau, block, page); + /* L2V[0] / LBA0 must point at this real boot page (physical). */ + fmss_l2v_set_ex(0, ce, cau, block, page, true, L2V_SRC_CARVE, 0); + fmss_lba_set(0, ce, cau, block, page, 0, true, L2V_SRC_CARVE, 0); fmss_dev_info(f->dev, "boot_sb ce=%u cau=%u blk=%u pg=%u DataStart=%u rsv=%u fatz=%u\n", ce, cau, block, page, boot_data_start, @@ -3508,8 +3922,8 @@ static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, if (!fmss_apple_fat_boot(s)) continue; fmss_boot_apply_bpb(f, ce, cau, b, pg, s); - fmss_early_lba_ensure(); - fmss_early_lba_set(0, ce, cau, b, pg, sec); + fmss_lba_set(0, ce, cau, b, pg, sec, + true, L2V_SRC_CARVE, 0); page_chunks = saved; fmss_dev_info(f->dev, "boot_sb open-SB sec=%u ce=%u cau=%u blk=%u pg=%u\n", @@ -3596,7 +4010,8 @@ static int fmss_root_dir_discover(struct fmss_n31 *f, unsigned int start, root_dir_block = b; root_dir_page = p; root_dir_lpn = lpn; - fmss_l2v_set(lpn, ce, cau, b, p); + fmss_l2v_set_ex(lpn, ce, cau, b, p, true, + L2V_SRC_CARVE, 0); page_chunks = saved; fmss_dev_info(f->dev, "root_dir N31OS ce=%u cau=%u blk=%u pg=%u lpn=%u\n", @@ -3646,13 +4061,18 @@ static void fmss_l2v_try_block_map_page(struct fmss_n31 *f, unsigned int ce, fmss_nand_reset(f); f->pages_since_reset = 0; } - addr = fmss_ppn_addr(cau, fmss_vfl_phys(cau, vbn), - FMSS_BTOC_PAGE, 0); - ret = fmss_page_read(f, ce, addr); - f->pages_since_reset++; - if (ret || fmss_page_blankish(f->last_page, 64)) - continue; - fmss_l2v_ingest_btoc(f, ce, cau, vbn, f->last_page, max_lpn); + { + unsigned int pblk = fmss_vfl_resolve(cau, vbn); + + addr = fmss_ppn_addr(cau, pblk, FMSS_BTOC_PAGE, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + if (ret || fmss_page_blankish(f->last_page, 64)) + continue; + /* Store physical block + PHYS (already VFL-resolved). */ + fmss_l2v_ingest_btoc(f, ce, cau, pblk, f->last_page, + max_lpn); + } } page_chunks = saved; kfree(mapbuf); @@ -3682,8 +4102,14 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, if (ret) return ret; + fmss_legacy_meta_ingest = true; + /* Rebuild map contents for this pass (keep allocation). */ memset(l2v_map, 0, l2v_map_size * sizeof(*l2v_map)); + if (l2v_weave) + memset(l2v_weave, 0, l2v_map_size * sizeof(*l2v_weave)); + if (l2v_src) + memset(l2v_src, 0, l2v_map_size * sizeof(*l2v_src)); l2v_mapped = 0; l2v_btoc_hits = 0; l2v_bmap_hits = 0; @@ -3691,13 +4117,19 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, lpn_index_count = 0; boot_carve_valid = false; root_dir_valid = false; - if (early_lba_map) { - memset(early_lba_map, 0, - FMSS_EARLY_LBA_MAX * sizeof(*early_lba_map)); - early_lba_mapped = 0; + f->ecc_soft_fails = 0; + if (lba_map) { + memset(lba_map, 0, fmss_lba_map_cap() * sizeof(*lba_map)); + if (lba_weave) + memset(lba_weave, 0, fmss_lba_map_cap() * sizeof(*lba_weave)); + if (lba_src) + memset(lba_src, 0, fmss_lba_map_cap() * sizeof(*lba_src)); + lba_mapped = 0; } else { - fmss_early_lba_ensure(); + fmss_lba_map_ensure(); } + vfl_remap_applied = 0; + vfl_remap_skipped_phys = 0; saved = page_chunks; page_chunks = 1; @@ -3725,10 +4157,24 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, fmss_l2v_ingest_btoc(f, ce, cau, b, f->last_page, max_lpn); - else - fmss_l2v_ingest_bte(f, ce, cau, b, - f->last_page, - max_lpn); + else if (fmss_page_looks_bte(f->last_page)) { + /* + * Pass 2: BTE needs the full + * 16 KiB page; walk used 1-chunk + * probe — re-read full page. + */ + unsigned int saved2 = page_chunks; + + page_chunks = 16; + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved2; + if (!ret) + fmss_l2v_ingest_bte( + f, ce, cau, b, + f->last_page, + max_lpn); + } } /* @@ -3752,10 +4198,13 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, fmss_root_dir_discover(f, start ? start : 32, min_t(unsigned int, nblocks, 48)); + fmss_legacy_meta_ingest = false; + fmss_dev_info(f->dev, - "l2v_build max_lpn=%u range=%u+%u mapped=%u btoc=%u bmap=%u boot=%d root=%d\n", + "l2v_build max_lpn=%u range=%u+%u mapped=%u btoc=%u bmap=%u boot=%d root=%d ecc_soft=%u\n", max_lpn, start, nblocks, l2v_mapped, l2v_btoc_hits, - l2v_bmap_hits, boot_carve_valid, root_dir_valid); + l2v_bmap_hits, boot_carve_valid, root_dir_valid, + f->ecc_soft_fails); return 0; } @@ -3842,19 +4291,20 @@ static ssize_t l2v_status_show(struct device *dev, struct device_attribute *attr char *buf) { return sysfs_emit(buf, - "mapped=%u max_lpn=%u size=%u btoc_hits=%u bmap_hits=%u meta_hits=%u early_lba=%u\n" + "mapped=%u max_lpn=%u size=%u btoc_hits=%u bmap_hits=%u meta_hits=%u lba_mapped=%u\n" "boot_carve=%u ce=%u cau=%u blk=%u pg=%u off=%u DataStart=%u\n" "root_dir=%u ce=%u cau=%u blk=%u pg=%u lpn=%u\n" - "lpn_index=%u\n" + "lpn_index=%u vfl_mode=%s remap_applied=%u skipped_phys=%u\n" "whimory ret=%d dis=%u vfl=%u ftlctrl=%u bmap_pages=%u map_ents=%u filled=%u\n", l2v_mapped, l2v_max_lpn, l2v_map_size, l2v_btoc_hits, - l2v_bmap_hits, l2v_meta_hits, early_lba_mapped, + l2v_bmap_hits, l2v_meta_hits, lba_mapped, boot_carve_valid, boot_carve_ce, boot_carve_cau, boot_carve_block, boot_carve_page, boot_carve_off, boot_data_start, root_dir_valid, root_dir_ce, root_dir_cau, root_dir_block, root_dir_page, root_dir_lpn, - lpn_index_count, + lpn_index_count, vfl_remap_mode, vfl_remap_applied, + vfl_remap_skipped_phys, wmr_mount_ret, wmr_dis_hits, wmr_vfl_hits, wmr_ftlctrl_hits, wmr_bmap_pages, wmr_block_map_n, wmr_l2v_filled); } @@ -3927,8 +4377,8 @@ static DEVICE_ATTR_WO(whimory_mount); static int fmss_ftl_read_lpn_locked(struct fmss_n31 *f, unsigned int target_lpn, unsigned int sector, u8 *buf) { - unsigned int ce, cau, block, page, vblock, off, saved; - u32 addr; + unsigned int ce, cau, block, page, pblock, off, saved; + u32 addr, packed = L2V_PHYS; int ret; if (sector > FMSS_FTL_SECTORS_PER_LPN - 1) @@ -3939,20 +4389,22 @@ static int fmss_ftl_read_lpn_locked(struct fmss_n31 *f, unsigned int target_lpn, cau = root_dir_cau; block = root_dir_block; page = root_dir_page; + packed = L2V_PHYS; } else { - ret = fmss_l2v_lookup(target_lpn, &ce, &cau, &block, &page); + ret = fmss_l2v_lookup_ex(target_lpn, &ce, &cau, &block, &page, + &packed); if (ret) return ret; } - vblock = fmss_vfl_phys(cau, block); + pblock = fmss_map_to_phys(cau, block, packed); saved = page_chunks; page_chunks = 16; if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); f->pages_since_reset = 0; } - addr = fmss_ppn_addr(cau, vblock, page, 0); + addr = fmss_ppn_addr(cau, pblock, page, 0); ret = fmss_page_read(f, ce, addr); f->pages_since_reset++; page_chunks = saved; @@ -3994,26 +4446,36 @@ static ssize_t lpn_read_store(struct device *dev, struct device_attribute *attr, return -ENOMEM; mutex_lock(&f->lock); - /* Interactive: on-demand BTOC resolve; block I/O uses dense map only. */ - ret = fmss_lpn_resolve(f, target_lpn, &ce, &cau, &block, &page); - if (!ret) { - saved = page_chunks; - page_chunks = 16; - if (reset_every && f->pages_since_reset >= reset_every) { - fmss_nand_reset(f); - f->pages_since_reset = 0; - } - addr = fmss_ppn_addr(cau, fmss_vfl_phys(cau, block), page, 0); - ret = fmss_page_read(f, ce, addr); - f->pages_since_reset++; - page_chunks = saved; + { + u32 packed = L2V_PHYS; + + /* Prefer dense L2V packed flags; else on-demand resolve (PHYS). */ + ret = fmss_l2v_lookup_ex(target_lpn, &ce, &cau, &block, &page, + &packed); + if (ret) + ret = fmss_lpn_resolve(f, target_lpn, &ce, &cau, + &block, &page); if (!ret) { - poff = sector * FMSS_SECTOR_LEN; - if (poff + FMSS_SECTOR_LEN <= f->last_page_len) - memcpy(secbuf, f->last_page + poff, - FMSS_SECTOR_LEN); - else - ret = -ERANGE; + saved = page_chunks; + page_chunks = 16; + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + addr = fmss_ppn_addr(cau, + fmss_map_to_phys(cau, block, packed), + page, 0); + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (!ret) { + poff = sector * FMSS_SECTOR_LEN; + if (poff + FMSS_SECTOR_LEN <= f->last_page_len) + memcpy(secbuf, f->last_page + poff, + FMSS_SECTOR_LEN); + else + ret = -ERANGE; + } } } mutex_unlock(&f->lock); @@ -4041,6 +4503,142 @@ static ssize_t lpn_read_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(lpn_read); +static ssize_t resolve_log_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + if (!resolve_log_len) + return sysfs_emit(buf, "(no resolve yet)\n"); + return sysfs_emit(buf, "%s", resolve_log); +} +static DEVICE_ATTR_RO(resolve_log); + +static ssize_t read_sector_dense_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int lba; + u8 *secbuf; + int ret; + + if (!f) + return -ENODEV; + if (kstrtouint(buf, 0, &lba)) + return -EINVAL; + secbuf = kmalloc(FMSS_SECTOR_LEN, GFP_KERNEL); + if (!secbuf) + return -ENOMEM; + ret = fmss_ftl_read_sector(lba, secbuf); + dev_info(dev, "read_sector_dense LBA=%u ret=%d %s", + lba, ret, resolve_log); + kfree(secbuf); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(read_sector_dense); + +static ssize_t read_sector_slow_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int lba, lpn, sec, ce, cau, block, page, pblock, saved; + u8 *secbuf; + u32 addr, packed = L2V_PHYS; + int ret; + + if (!f) + return -ENODEV; + if (kstrtouint(buf, 0, &lba)) + return -EINVAL; + secbuf = kmalloc(FMSS_SECTOR_LEN, GFP_KERNEL); + if (!secbuf) + return -ENOMEM; + + /* Try dense first. */ + ret = fmss_ftl_read_sector(lba, secbuf); + if (!ret) { + dev_info(dev, "read_sector_slow LBA=%u via dense OK\n", lba); + kfree(secbuf); + return count; + } + + lpn = lba / FMSS_FTL_SECTORS_PER_LPN; + sec = lba % FMSS_FTL_SECTORS_PER_LPN; + mutex_lock(&f->lock); + ret = fmss_l2v_lookup_ex(lpn, &ce, &cau, &block, &page, &packed); + if (ret) { + ret = fmss_lpn_resolve(f, lpn, &ce, &cau, &block, &page); + packed = L2V_PHYS; + } + if (!ret) { + pblock = fmss_map_to_phys(cau, block, packed); + saved = page_chunks; + page_chunks = 16; + addr = fmss_ppn_addr(cau, pblock, page, 0); + ret = fmss_page_read(f, ce, addr); + page_chunks = saved; + if (!ret) { + memcpy(secbuf, f->last_page + sec * FMSS_SECTOR_LEN, + FMSS_SECTOR_LEN); + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "slow LBA=%u via on-demand lpn_resolve phys=%d ce=%u cau=%u blk=%u→%u pg=%u sec=%u head=%02x%02x%02x%02x\n", + lba, !!(packed & L2V_PHYS), ce, cau, block, + pblock, page, sec, + secbuf[0], secbuf[1], secbuf[2], secbuf[3]); + } + } + mutex_unlock(&f->lock); + dev_info(dev, "read_sector_slow LBA=%u ret=%d %s", lba, ret, resolve_log); + kfree(secbuf); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(read_sector_slow); + +static ssize_t read_sector_phys_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int ce, cau, block, page, sec = 0, saved; + u8 *secbuf; + u32 addr; + int nf, ret; + + if (!f) + return -ENODEV; + nf = sscanf(buf, "%u %u %u %u %u", &ce, &cau, &block, &page, &sec); + if (nf < 4) + return -EINVAL; + if (sec > 3) + return -EINVAL; + secbuf = kmalloc(FMSS_SECTOR_LEN, GFP_KERNEL); + if (!secbuf) + return -ENOMEM; + mutex_lock(&f->lock); + saved = page_chunks; + page_chunks = 16; + addr = fmss_ppn_addr(cau, block, page, 0); + ret = fmss_page_read(f, ce, addr); + page_chunks = saved; + if (!ret) { + memcpy(secbuf, f->last_page + sec * FMSS_SECTOR_LEN, + FMSS_SECTOR_LEN); + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "phys ce=%u cau=%u blk=%u pg=%u sec=%u head=%02x%02x%02x%02x\n", + ce, cau, block, page, sec, + secbuf[0], secbuf[1], secbuf[2], secbuf[3]); + memcpy(f->last_page, secbuf, FMSS_SECTOR_LEN); + sector_log_len = 0; + } + mutex_unlock(&f->lock); + dev_info(dev, "read_sector_phys ret=%d %s", ret, resolve_log); + kfree(secbuf); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(read_sector_phys); + static char grep_log[4096]; static unsigned int grep_log_len; @@ -4256,18 +4854,19 @@ static int fmss_read_ftl_page_pio(struct fmss_n31 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page) { - unsigned int vblock, saved; + unsigned int pblock, saved; u32 addr; int ret; - vblock = fmss_vfl_phys(cau, block); + /* Physical block from boot/carve scan — never VFL-remap. */ + pblock = fmss_map_to_phys(cau, block, L2V_PHYS); saved = page_chunks; page_chunks = 16; if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); f->pages_since_reset = 0; } - addr = fmss_ppn_addr(cau, vblock, page, 0); + addr = fmss_ppn_addr(cau, pblock, page, 0); ret = fmss_page_read(f, ce, addr); f->pages_since_reset++; page_chunks = saved; @@ -4982,6 +5581,10 @@ static struct attribute *fmss_attrs[] = { &dev_attr_whimory_mount.attr, &dev_attr_whimory_status.attr, &dev_attr_lpn_read.attr, + &dev_attr_read_sector_dense.attr, + &dev_attr_read_sector_slow.attr, + &dev_attr_read_sector_phys.attr, + &dev_attr_resolve_log.attr, &dev_attr_ftl_grep.attr, &dev_attr_readme_read.attr, &dev_attr_boot_read.attr, @@ -5126,50 +5729,309 @@ EXPORT_SYMBOL_GPL(fmss_ftl_build_map); int fmss_ftl_read_sector(u64 logical_sector, void *buf) { struct fmss_n31 *f = fmss_dev; - unsigned int lpn, sec, ce, cau, block, page, vblock, off, saved; - u32 addr; + unsigned int lpn, sec, ce, cau, block, page, pblock, off, saved; + u32 addr, packed; int ret; + int (*hook)(u64, void *); - if (!f || !buf) + if (!buf) + return -ENODEV; + + /* + * Whimory registers the real LBA reader after FTL_Open. Call it + * without the FMSS mutex — the FIL page_read wrapper takes that lock. + */ + hook = READ_ONCE(fmss_ftl_read_hook); + if (hook) + return hook(logical_sector, buf); + + if (!f) return -ENODEV; mutex_lock(&f->lock); - /* Prefer SFTL BTE early-LBA map (boot/FAT). */ - if (logical_sector < FMSS_EARLY_LBA_MAX && - !fmss_early_lba_lookup((unsigned int)logical_sector, &ce, &cau, - &block, &page, &sec)) { - vblock = fmss_vfl_phys(cau, block); + /* Prefer full LBA map (BTE / BTOC sector fills / carve / META). */ + if (logical_sector < fmss_lba_map_cap() && + !fmss_lba_lookup((unsigned int)logical_sector, &ce, &cau, + &block, &page, &sec, &packed)) { + pblock = fmss_map_to_phys(cau, block, packed); saved = page_chunks; page_chunks = 16; if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); f->pages_since_reset = 0; } - addr = fmss_ppn_addr(cau, vblock, page, 0); + addr = fmss_ppn_addr(cau, pblock, page, 0); ret = fmss_page_read(f, ce, addr); f->pages_since_reset++; page_chunks = saved; if (!ret) { - off = sec * FMSS_SECTOR_LEN; - if (off + FMSS_SECTOR_LEN <= f->last_page_len) - memcpy(buf, f->last_page + off, FMSS_SECTOR_LEN); - else - ret = -ERANGE; + if (sec == L2V_SEC_FROM_LBA) + sec = (unsigned int)logical_sector % + FMSS_VBAS_PER_PAGE; + /* Pass 2: refuse stale map if META LBA disagrees. */ + if (f->last_spare_len >= 16 * (sec + 1)) { + const u8 *m = f->last_spare + sec * 16; + + if (m[0] == 0x01) { + u32 mlba = get_unaligned_le32(m + 8); + + if (mlba != (u32)logical_sector) + ret = -EIO; + } + } + if (!ret) { + off = sec * FMSS_SECTOR_LEN; + if (off + FMSS_SECTOR_LEN <= f->last_page_len) + memcpy(buf, f->last_page + off, + FMSS_SECTOR_LEN); + else + ret = -ERANGE; + } + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "dense LBA=%llu src=lba_map phys=%d ce=%u cau=%u blk=%u→%u pg=%u sec=%u ret=%d head=%02x%02x%02x%02x\n", + (unsigned long long)logical_sector, + !!(packed & L2V_PHYS), ce, cau, block, pblock, + page, sec, ret, + ret ? 0 : ((u8 *)buf)[0], + ret ? 0 : ((u8 *)buf)[1], + ret ? 0 : ((u8 *)buf)[2], + ret ? 0 : ((u8 *)buf)[3]); + if (!ret) { + mutex_unlock(&f->lock); + return 0; + } + /* Mapped read failed / META mismatch — fall through to LPN. */ + } else { + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "dense LBA=%llu src=lba_map PHYS-fail ret=%d; try l2v_lpn\n", + (unsigned long long)logical_sector, ret); + /* Fall through to LPN path. */ } - mutex_unlock(&f->lock); - return ret; } lpn = (unsigned int)(logical_sector / FMSS_FTL_SECTORS_PER_LPN); sec = (unsigned int)(logical_sector % FMSS_FTL_SECTORS_PER_LPN); ret = fmss_ftl_read_lpn_locked(f, lpn, sec, buf); + if (!ret) + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "dense LBA=%llu src=l2v_lpn lpn=%u sec=%u ret=0 head=%02x%02x%02x%02x\n", + (unsigned long long)logical_sector, lpn, sec, + ((u8 *)buf)[0], ((u8 *)buf)[1], + ((u8 *)buf)[2], ((u8 *)buf)[3]); + else + resolve_log_len = scnprintf( + resolve_log, sizeof(resolve_log), + "dense LBA=%llu src=l2v_lpn lpn=%u sec=%u ret=%d\n", + (unsigned long long)logical_sector, lpn, sec, ret); mutex_unlock(&f->lock); return ret; } EXPORT_SYMBOL_GPL(fmss_ftl_read_sector); +int s5l8740_fmss_available(void) +{ + return fmss_dev != NULL; +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_available); + +int s5l8740_fmss_hw_init(void) +{ + struct fmss_n31 *f = fmss_dev; + int ret; + + if (!f) + return -ENODEV; + mutex_lock(&f->lock); + ret = fmss_nand_reset(f); + if (!ret) + (void)fmss_param_read(f, 0); + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_hw_init); + +int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g) +{ + struct fmss_n31 *f = fmss_dev; + const u8 *p; + u32 caus, cau_bits, blocks, block_bits, pages, pages_slc; + u32 page_bits, page_size; + + if (!g) + return -EINVAL; + if (!f) + return -ENODEV; + + memset(g, 0, sizeof(*g)); + g->num_ce = FMSS_NUM_CE; + g->num_cau = FMSS_NUM_CAU; + g->blocks_per_cau = FMSS_BLOCKS_PER_CAU; + g->pages_per_block = 128; + g->pages_per_block_slc = 128; + g->page_size = FMSS_PAGE_LEN; + g->vfl_tail = FMSS_VFL_TAIL; + g->page_bits = FMSS_PAGE_BITS; + g->block_bits = FMSS_BLOCK_BITS; + g->cau_bits = FMSS_CAU_BITS; + g->caus_per_channel = FMSS_NUM_CAU; + + mutex_lock(&f->lock); + if (f->last_param_ret != 0) + (void)fmss_param_read(f, 0); + p = f->last_param; + if (f->last_param_ret == 0) { + caus = fmss_le32(p, 16); + cau_bits = fmss_le32(p, 20); + blocks = fmss_le32(p, 24); + block_bits = fmss_le32(p, 28); + pages = fmss_le32(p, 32); + pages_slc = fmss_le32(p, 36); + page_bits = fmss_le32(p, 40); + page_size = fmss_le32(p, 52); + if (caus && caus <= 4) + g->caus_per_channel = caus; + if (cau_bits && cau_bits <= 4) + g->cau_bits = cau_bits; + if (blocks && blocks <= 8192) + g->blocks_per_cau = blocks; + if (block_bits && block_bits <= 16) + g->block_bits = block_bits; + if (pages && pages <= 256) + g->pages_per_block = pages; + if (pages_slc && pages_slc <= 256) + g->pages_per_block_slc = pages_slc; + if (page_bits && page_bits <= 16) + g->page_bits = page_bits; + if (page_size == 4096 || page_size == 8192 || + page_size == 16384) + g->page_size = page_size; + g->from_param_page = true; + } + mutex_unlock(&f->lock); + + /* + * FIL GetInfo (vtable +80, sub_12F83C): + * 101 — NAND present / signature +0x34 geometry (WhimoryBoot.c:169,260) + * 0 → "No NAND device found". Compared to sig[+0x34]. + * Value stored at format is blocks_per_cau (sub_12ED9C → 0x8D102CC + * is the first geometry word copied from the param page). + * 104 — BUF_Init data bytes (sub_D1960 first arg) = physical page size + * 105 — BUF_Init meta bytes (sub_D1960 second arg) = 16 (sub_12ED9C) + * 135 — stored at 0x8D0CE2C and unused after GetInfo + */ + g->dev_id = g->blocks_per_cau; + g->geom_104 = g->page_size; + g->geom_105 = 16; + g->geom_135 = g->page_size >> 12; + if (!g->dev_id) + return -ENODEV; + return 0; +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_query_geometry); + +u32 s5l8740_fmss_fil_get_info(u32 selector) +{ + struct s5l8740_fmss_geom g; + + if (s5l8740_fmss_query_geometry(&g)) + return 0; + switch (selector) { + case 101: + return g.dev_id; + case 104: + return g.geom_104; + case 105: + return g.geom_105; + case 135: + return g.geom_135; + default: + return 0; + } +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_fil_get_info); + +int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, + unsigned int slc, unsigned int chunks, + void *data, size_t data_len, + void *meta, size_t meta_len) +{ + struct fmss_n31 *f = fmss_dev; + unsigned int saved; + u32 addr; + int ret; + + if (!f) + return -ENODEV; + if (ce >= FMSS_NUM_CE || cau >= FMSS_NUM_CAU || + block >= FMSS_BLOCKS_PER_CAU || page > FMSS_BTOC_PAGE) + return -EINVAL; + if (!chunks || chunks > FMSS_MAX_CHUNKS) + chunks = FMSS_MAX_CHUNKS; + + mutex_lock(&f->lock); + if (reset_every && f->pages_since_reset >= reset_every) { + fmss_nand_reset(f); + f->pages_since_reset = 0; + } + saved = page_chunks; + page_chunks = chunks; + addr = fmss_ppn_addr(cau, block, page, slc); + /* + * PIO last_spare is not proven Sogeti/Whimory META (glass 2026-08-27: + * 53-byte beat is FIFO garbage). Only take the second pass when the + * caller actually asked for a meta buffer. + */ + if (meta && meta_len) + ret = fmss_page_read_with_meta(f, ce, addr); + else + ret = fmss_page_read(f, ce, addr); + f->pages_since_reset++; + page_chunks = saved; + if (!ret) { + if (data && data_len) { + if (data_len > f->last_page_len) + data_len = f->last_page_len; + memcpy(data, f->last_page, data_len); + } + if (meta && meta_len) { + memset(meta, 0xff, meta_len); + if (f->last_spare_len) + memcpy(meta, f->last_spare, + min_t(size_t, meta_len, + f->last_spare_len)); + } + } + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_page_read); + +int s5l8740_fmss_nand_reset(void) +{ + struct fmss_n31 *f = fmss_dev; + int ret; + + if (!f) + return -ENODEV; + mutex_lock(&f->lock); + ret = fmss_nand_reset(f); + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_nand_reset); + +void s5l8740_fmss_register_ftl_read(int (*fn)(u64 lba, void *buf)) +{ + WRITE_ONCE(fmss_ftl_read_hook, fn); +} +EXPORT_SYMBOL_GPL(s5l8740_fmss_register_ftl_read); + module_init(fmss_init); module_exit(fmss_exit); MODULE_LICENSE("GPL"); diff --git a/drivers/misc/ftl-s5l8740.c b/drivers/misc/ftl-s5l8740.c index 331608791198bd..0c93a3dfc94628 100755 --- a/drivers/misc/ftl-s5l8740.c +++ b/drivers/misc/ftl-s5l8740.c @@ -1,738 +1,4159 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * S5L8740 Whimory FTL read-only block devices + host partition aliases. + * S5L8740 Whimory PPN read-only block driver (N31). * - * Hardware: N31 is NAND-only (no SPI/NOR utility flash from nano4G onward). - * Whimory "FPart" (PPNFPart) manages special blocks under FTL — it is NOT the - * host MBR/name table. Host-visible slices (classic + N5/N6/N7 family) are: + * FIL (fmss-s5l8740.ko) + * → FPart ReadSpecial type 0xC101 len 0x600 (chunked object; xrmw at payload+0) + * → GetInfo(101) vs sig[+0x34] hard gate + * → VFL_Open (type 0x20 CXT, identity VBN, bank bitmap) + * → FTL_Open / s_boot: classify → s_cxt_load → BTOC/META + * → L2V_Search (sub_428694) + * → VFL read + s_read META check (sub_56C328) + * → /dev/s5l8740-ftl (4096-byte logical, read-only) * - * firmware — IMG1 / MSE (osos, rsrc, disk, gpfw, …) or "[hi]" style - * ipod — FAT32 user volume (Windows D:\, iPod_Control, n31os) - * - * This module: - * /dev/s5l8740-ftl — whole FTL LBA space (4096 B sectors) - * /dev/s5l8740-firmware — firmware slice if discovered / forced - * /dev/s5l8740-ipod — user FAT slice (or whole disk if superfloppy) - * /dev/s5l8740-rsrc — optional resource FS inside firmware - * - * Low-level NAND I/O: fmss-s5l8740.ko + * The disk is registered only after read_lba_4k(0) returns a metadata- + * validated FAT32 boot sector. Empty L2V never yields a block device. */ #include +#include #include -#include -#include +#include +#include +#include #include #include +#include #include +#include #include #include #include +#include +#include -#include "fmss-s5l8740-api.h" +#include "whimory-s5l8740.h" #define FTL_DISK_NAME "s5l8740-ftl" -#define FTL_VALIDATE_HEX 128 -#define FPART_MAX_DISKS 4 -#define FPART_SCAN_LBAS 4096u - -enum fpart_kind { - FPART_WHOLE = 0, - FPART_FIRMWARE, - FPART_IPOD, - FPART_RSRC, -}; +#define FTL_IPOD_NAME "s5l8740-ipod" -struct fpart_slice { - const char *name; - enum fpart_kind kind; - u64 start_lba; /* 4096-byte FTL sectors */ - u64 nsectors; - bool present; - struct gendisk *disk; -}; +#define WHIMORY_ORACLE_SIG "apple/n31-whimory-sig.bin" +#define WHIMORY_ORACLE_ROOT "apple/n31-whimory-l2v-root.bin" +#define WHIMORY_ORACLE_NODES "apple/n31-whimory-l2v-nodes.bin" +#define WHIMORY_ORACLE_GLOBALS "apple/n31-whimory-l2v-globals.bin" -static u64 ftl_capacity = FMSS_FTL_DEFAULT_CAPACITY; -module_param(ftl_capacity, ullong, 0644); -MODULE_PARM_DESC(ftl_capacity, - "FTL logical sector count (4096 B; N31 default ~3856968)"); +#define WHIMORY_SPECIAL_LBA 0xFFFF0000u -static unsigned int ftl_map_max_lpn; -module_param(ftl_map_max_lpn, uint, 0644); +static bool import_l2v_oracle; +module_param(import_l2v_oracle, bool, 0644); +MODULE_PARM_DESC(import_l2v_oracle, + "Load L2V root/nodes/globals from /lib/firmware/apple/"); -static bool ftl_auto_map; -module_param(ftl_auto_map, bool, 0644); +static unsigned int scan_blocks; +module_param(scan_blocks, uint, 0644); +MODULE_PARM_DESC(scan_blocks, + "User blocks per CE/CAU to classify (0 = all user blocks)"); -/* Manual overrides (4K LBAs). 0 = auto / unused. */ -static unsigned long fw_start_lba; -module_param(fw_start_lba, ulong, 0644); -MODULE_PARM_DESC(fw_start_lba, "Firmware slice start (4K LBA, default 0)"); +static unsigned int max_open_sbs; +module_param(max_open_sbs, uint, 0644); +MODULE_PARM_DESC(max_open_sbs, + "Max open superblocks to META-rebuild (0 = all)"); -static unsigned long fw_nsectors; -module_param(fw_nsectors, ulong, 0644); -MODULE_PARM_DESC(fw_nsectors, - "Firmware slice size in 4K sectors (0=auto from scan/FWPartSize)"); +static unsigned int meta0_scan_sbs = 4; +module_param(meta0_scan_sbs, uint, 0644); +MODULE_PARM_DESC(meta0_scan_sbs, + "Closed SBs to full-scan for META lba=0 after classify (0=skip extra)"); -static unsigned long ipod_start_lba; -module_param(ipod_start_lba, ulong, 0644); -MODULE_PARM_DESC(ipod_start_lba, "User FAT start 4K LBA (0=auto)"); +static bool allow_sigless_debug; +module_param(allow_sigless_debug, bool, 0644); +MODULE_PARM_DESC(allow_sigless_debug, + "If true, classify/recover without xrmw (default N — NAND wedge / fake META)"); -static unsigned long ipod_nsectors; -module_param(ipod_nsectors, ulong, 0644); -MODULE_PARM_DESC(ipod_nsectors, "User FAT size 4K sectors (0=to end of FTL)"); +static unsigned int sig_scan_blocks; +module_param(sig_scan_blocks, uint, 0644); +MODULE_PARM_DESC(sig_scan_blocks, + "FPart assignment scan: tail blocks (0 = vfl_tail)"); -static bool fpart_auto_scan = true; -module_param(fpart_auto_scan, bool, 0644); -MODULE_PARM_DESC(fpart_auto_scan, "Scan FTL for MBR/[hi]/FAT and create named disks"); +static unsigned int fpart_assign_pages = 16; +module_param(fpart_assign_pages, uint, 0644); +MODULE_PARM_DESC(fpart_assign_pages, + "Pages per tail block to scan for META 0x30 assignment (default 16)"); -static struct gendisk *ftl_disk; -static struct platform_device *ftl_pdev; -static char fpart_status[PAGE_SIZE]; -static unsigned int fpart_status_len; +static bool sig_brute_scan; +module_param(sig_brute_scan, bool, 0644); +MODULE_PARM_DESC(sig_brute_scan, + "META 0x30 page0 brute (default N — PIO spare is not Sogeti)"); -static struct fpart_slice slices[FPART_MAX_DISKS] = { - { .name = "s5l8740-firmware", .kind = FPART_FIRMWARE }, - { .name = "s5l8740-ipod", .kind = FPART_IPOD }, - { .name = "s5l8740-rsrc", .kind = FPART_RSRC }, -}; +static bool payload_magic_scan = true; +module_param(payload_magic_scan, bool, 0644); +MODULE_PARM_DESC(payload_magic_scan, + "Data-only xrmw/wrmx hunt; skip META locate and sigless classify (default Y)"); -static bool is_apple_fat_bpb(const u8 *s) -{ - if (s[0] != 0xeb && s[0] != 0xe9) - return false; - /* LE bytes/sector 512 or 4096 */ - { - u16 bps = s[11] | (s[12] << 8); +/* Kept so existing insmod lines do not fail. Recovery always runs at probe. */ +static bool ftl_auto_map __maybe_unused; +module_param(ftl_auto_map, bool, 0644); +MODULE_PARM_DESC(ftl_auto_map, "ignored; Whimory always recovers at insmod"); - if (bps != 512 && bps != 4096) - return false; - } - if (s[3] == '*' && s[4] == 'U' && s[5] == 'O') - return true; - if (s[0x52] == 'F' && s[0x53] == 'A' && s[0x54] == '3') - return true; - if (s[510] == 0x55 && s[511] == 0xaa) - return true; - return false; -} +static bool fpart_auto_scan __maybe_unused; +module_param(fpart_auto_scan, bool, 0644); +MODULE_PARM_DESC(fpart_auto_scan, "ignored; host slices created after LBA0"); -static bool is_hi_firmware_hdr(const u8 *s) -{ - /* Classic firmware volume header: "[hi]" at +0x100 (LE magic). */ - return s[0x100] == '[' && s[0x101] == 'h' && - s[0x102] == 'i' && s[0x103] == ']'; -} +static struct whimory *whimory_dev; +static struct platform_device *ftl_pdev; + +static void whimory_l2v_find_frag(struct whimory *w); +static void whimory_l2v_free_tree(struct whimory *w, u32 node_idx, u32 root_idx); +static int whimory_l2v_update_packed(struct whimory *w, u32 ridx, u32 off, + u32 span, u32 vba); +static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, + bool allow_blank); -static bool is_mbr(const u8 *s) +static u64 whimory_weave48(const u8 *m) { - return s[510] == 0x55 && s[511] == 0xaa && - (s[0x1be + 4] != 0 || s[0x1ce + 4] != 0 || - s[0x1de + 4] != 0 || s[0x1ee + 4] != 0); + return (u64)get_unaligned_le16(m + 2) | + ((u64)get_unaligned_le32(m + 4) << 16); } -static bool looks_img1_dir(const u8 *s) +static bool whimory_page_blank(const u8 *p, unsigned int n) { - /* Loose: FourCC-ish names used in Apple MSE (LE dword text). */ - static const char *const tags[] = { - "osos", "soso", "rsrc", "crsr", "disk", "ksid", - "gpfw", "wfpg", "appl", NULL - }; - unsigned int i, t; + unsigned int i; + u8 all_ff = 0xff, all_00 = 0; - for (i = 0; i + 40 <= 512; i += 40) { - for (t = 0; tags[t]; t++) { - if (!memcmp(s + i + 4, tags[t], 4) || - !memcmp(s + i, tags[t], 4)) - return true; - } + if (!p || !n) + return true; + for (i = 0; i < n; i++) { + all_ff &= p[i]; + all_00 |= p[i]; } - return false; + return all_ff == 0xff || all_00 == 0; } -static int ftl_read_lba(u64 lba, u8 *buf) +static bool whimory_special_lba(u32 lba) { - if (lba >= ftl_capacity) - return -ERANGE; - return fmss_ftl_read_sector(lba, buf); + return (lba & 0xFFFF0000u) == WHIMORY_SPECIAL_LBA || + lba == WHIMORY_LBA_BLANK || lba == WHIMORY_LBA_DELETED; } -static void fpart_status_reset(void) +static u32 whimory_vfl_phys(struct whimory *w, u32 cau, u32 virt) { - fpart_status_len = 0; + /* + * sub_4EAE40: PBN = VBN (identity over blocks_per_cau). + * The u16 table at CXT +0x200 is a VFL CXT copy journal + * (sub_3D26D8: value = index | (gen<<15), 0xC070 = free), not + * virt→phys. Failed user blocks keep the same VBN and switch + * CAU via the bank bitmap (sub_3D1438 / sub_4EAD34). + */ + if (cau >= w->geom.num_cau || !w->vfl.remap[cau]) + return virt; + if (virt >= w->geom.blocks_per_cau) + return virt; + return w->vfl.remap[cau][virt]; } -static void fpart_status_printf(const char *fmt, ...) +/* sub_3D1438: banks that participate in this VBN. */ +static u32 whimory_vfl_banks_in_vbn(struct whimory *w, u32 vbn, u8 *out, + u32 out_max) { - va_list ap; - int n; + u8 mask; + u32 n = 0, b; - if (fpart_status_len >= sizeof(fpart_status) - 1) - return; - va_start(ap, fmt); - n = vscnprintf(fpart_status + fpart_status_len, - sizeof(fpart_status) - fpart_status_len, fmt, ap); - va_end(ap); - if (n > 0) - fpart_status_len += n; -} + if (w->vfl.cached_vbn == (u16)vbn && w->vfl.cached_n) { + n = min_t(u32, w->vfl.cached_n, out_max); + if (out) + memcpy(out, w->vfl.cached_banks, n); + return w->vfl.cached_n; + } + mask = 0; + if (w->vfl.bank_mask && vbn < w->geom.blocks_per_cau) { + u32 stride = w->vfl.bank_stride ? w->vfl.bank_stride : 1; + const u8 *row = w->vfl.bank_mask + stride * vbn; + u32 b; -static void fpart_clear_slices(void) -{ - int i; + for (b = 0; b < w->geom.num_cau && b < 8; b++) { + u32 bi = b >> 3; - for (i = 0; i < FPART_MAX_DISKS; i++) { - if (slices[i].disk) { - del_gendisk(slices[i].disk); - put_disk(slices[i].disk); - slices[i].disk = NULL; + if (bi < stride && (row[bi] & (1u << (b & 7)))) + mask |= (u8)(1u << b); } - slices[i].present = false; - slices[i].start_lba = 0; - slices[i].nsectors = 0; } + if (!mask) + mask = (1u << w->geom.num_cau) - 1; + for (b = 0; b < w->geom.num_cau && b < 8; b++) { + if (!(mask & (1u << b))) + continue; + if (out && n < out_max) + out[n] = (u8)b; + if (n < S5L8740_FMSS_MAX_CAU) + w->vfl.cached_banks[n] = (u8)b; + n++; + } + w->vfl.cached_vbn = (u16)vbn; + w->vfl.cached_n = (u8)n; + return n; } -static void ftl_submit_bio_range(struct bio *bio, u64 start_lba, u64 nsectors) +static u32 whimory_vfl_bank(struct whimory *w, u32 cau, u32 vblock) { - struct bio_vec bvec; - struct bvec_iter iter; - u8 *secbuf; - u64 pos; - int ret = 0; + u8 banks[S5L8740_FMSS_MAX_CAU]; + u32 n, i; - if (bio_op(bio) != REQ_OP_READ) { - bio_io_error(bio); - return; + n = whimory_vfl_banks_in_vbn(w, vblock, banks, ARRAY_SIZE(banks)); + if (!n) + return cau; + for (i = 0; i < n; i++) { + if (banks[i] == (u8)cau) + return cau; } + return banks[0]; +} - secbuf = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_NOIO); - if (!secbuf) { - bio_io_error(bio); - return; +static u32 whimory_vfl_virt(struct whimory *w, u32 cau, u32 phys) +{ + u32 i, n; + + if (cau >= w->geom.num_cau || !w->vfl.remap[cau]) + return phys; + n = w->geom.blocks_per_cau; + for (i = 0; i < n; i++) { + if (w->vfl.remap[cau][i] == phys) + return i; } + return phys; +} - pos = (u64)bio->bi_iter.bi_sector << 9; +static u32 s_g_addr_to_vba(const struct whimory *w, u32 sb, u32 ofs) +{ + return sb * w->sftl.vbas_per_sb + ofs; +} - bio_for_each_segment(bvec, bio, iter) { - unsigned long seg_done = 0; - - while (seg_done < bvec.bv_len) { - u64 byte = pos + seg_done; - u64 lsec = start_lba + byte / FMSS_FTL_SECTOR_SIZE; - unsigned int off = byte % FMSS_FTL_SECTOR_SIZE; - unsigned int chunk = min_t(unsigned int, - FMSS_FTL_SECTOR_SIZE - off, - bvec.bv_len - seg_done); - - if (byte / FMSS_FTL_SECTOR_SIZE >= nsectors || - lsec >= ftl_capacity) { - ret = -EIO; - goto out; - } +static u32 s_g_vba_to_sb(const struct whimory *w, u32 vba) +{ + if (!w->sftl.vbas_per_sb) + return 0; + return vba / w->sftl.vbas_per_sb; +} - ret = fmss_ftl_read_sector(lsec, secbuf); - if (ret) - goto out; +static u32 s_g_vba_to_ofs(const struct whimory *w, u32 vba) +{ + if (!w->sftl.vbas_per_sb) + return 0; + return vba % w->sftl.vbas_per_sb; +} - { - void *page_addr = kmap_local_page(bvec.bv_page); +static u32 whimory_sb_index(const struct whimory *w, u32 ce, u32 cau, + u32 vblock) +{ + return (ce * w->geom.num_cau + cau) * w->sftl.user_blocks + vblock; +} - memcpy(page_addr + bvec.bv_offset + seg_done, - secbuf + off, chunk); - kunmap_local(page_addr); - } - seg_done += chunk; - } - pos += bvec.bv_len; - } +static u32 whimory_pack_vba(const struct whimory *w, u32 ce, u32 cau, + u32 vblock, u32 page, u32 slot) +{ + u32 sb = whimory_sb_index(w, ce, cau, vblock); + u32 ofs = page * w->sftl.vbas_per_page + slot; -out: - kfree(secbuf); - if (ret) - bio_io_error(bio); - else - bio_endio(bio); + return s_g_addr_to_vba(w, sb, ofs); } -static void ftl_submit_bio(struct bio *bio) +static int whimory_unpack_vba(const struct whimory *w, u32 vba, + u32 *ce, u32 *cau, u32 *vblock, + u32 *page, u32 *slot) { - ftl_submit_bio_range(bio, 0, ftl_capacity); + u32 sb, ofs, per_ce; + + if (!w->sftl.vbas_per_sb || !w->sftl.vbas_per_page || + !w->sftl.user_blocks) + return -EINVAL; + sb = s_g_vba_to_sb(w, vba); + ofs = s_g_vba_to_ofs(w, vba); + *page = ofs / w->sftl.vbas_per_page; + *slot = ofs % w->sftl.vbas_per_page; + per_ce = w->geom.num_cau * w->sftl.user_blocks; + if (!per_ce) + return -EINVAL; + *ce = sb / per_ce; + sb %= per_ce; + *cau = sb / w->sftl.user_blocks; + *vblock = sb % w->sftl.user_blocks; + if (*ce >= w->geom.num_ce || *cau >= w->geom.num_cau) + return -ERANGE; + if (*page >= w->sftl.pages_per_sb) + return -ERANGE; + return 0; } -static void fpart_submit_bio(struct bio *bio) +static void whimory_set_status(struct whimory *w, const char *fmt, ...) { - struct fpart_slice *sl = bio->bi_bdev->bd_disk->private_data; + va_list ap; - if (!sl || !sl->present) { - bio_io_error(bio); - return; - } - ftl_submit_bio_range(bio, sl->start_lba, sl->nsectors); + va_start(ap, fmt); + vsnprintf(w->status, sizeof(w->status), fmt, ap); + va_end(ap); } -static const struct block_device_operations ftl_bd_ops = { - .owner = THIS_MODULE, - .submit_bio = ftl_submit_bio, -}; - -static const struct block_device_operations fpart_bd_ops = { - .owner = THIS_MODULE, - .submit_bio = fpart_submit_bio, -}; +/* ------------------------------------------------------------------ */ +/* Interval map: weave-order LBA→VBA, then packed into the L2V tree. */ +/* ------------------------------------------------------------------ */ -static int fpart_register_slice(struct fpart_slice *sl) +static struct whimory_range *whimory_range_find(struct rb_root *root, u32 lba) { - struct queue_limits lim = { - .logical_block_size = FMSS_FTL_SECTOR_SIZE, - .physical_block_size = FMSS_FTL_SECTOR_SIZE, - }; - struct gendisk *disk; - int ret; + struct rb_node *n = root->rb_node; - if (!sl->present || !sl->nsectors) - return 0; + while (n) { + struct whimory_range *r = rb_entry(n, struct whimory_range, rb); + + if (lba < r->start) + n = n->rb_left; + else if (lba >= r->start + r->len) + n = n->rb_right; + else + return r; + } + return NULL; +} - disk = blk_alloc_disk(&lim, NUMA_NO_NODE); - if (IS_ERR(disk)) - return PTR_ERR(disk); +static int whimory_range_link(struct rb_root *root, struct whimory_range *n) +{ + struct rb_node **link = &root->rb_node, *parent = NULL; - disk->first_minor = 0; - disk->flags = GENHD_FL_NO_PART; - disk->fops = &fpart_bd_ops; - disk->private_data = sl; - snprintf(disk->disk_name, DISK_NAME_LEN, "%s", sl->name); - set_capacity(disk, sl->nsectors * (FMSS_FTL_SECTOR_SIZE / 512)); + while (*link) { + struct whimory_range *r = rb_entry(*link, struct whimory_range, + rb); - ret = add_disk(disk); - if (ret) { - put_disk(disk); - return ret; + parent = *link; + if (n->start < r->start) + link = &(*link)->rb_left; + else + link = &(*link)->rb_right; } - sl->disk = disk; - dev_info(&ftl_pdev->dev, - "/dev/%s start_lba=%llu nsectors=%llu (%llu MiB)\n", - sl->name, sl->start_lba, sl->nsectors, - (sl->nsectors * FMSS_FTL_SECTOR_SIZE) >> 20); + rb_link_node(&n->rb, parent, link); + rb_insert_color(&n->rb, root); return 0; } -static struct fpart_slice *fpart_by_kind(enum fpart_kind k) +static int whimory_range_split(struct whimory *w, struct whimory_range *r, + u32 at) { - int i; + struct whimory_range *right; + u32 left_len; - for (i = 0; i < FPART_MAX_DISKS; i++) - if (slices[i].kind == k) - return &slices[i]; - return NULL; + if (at <= r->start || at >= r->start + r->len) + return 0; + right = kzalloc(sizeof(*right), GFP_KERNEL); + if (!right) + return -ENOMEM; + left_len = at - r->start; + right->start = at; + right->len = r->len - left_len; + right->vba = r->vba + left_len; + r->len = left_len; + whimory_range_link(&w->ranges, right); + w->sftl.range_nodes++; + return 0; } -static void fpart_set(enum fpart_kind k, u64 start, u64 nsec) +static void whimory_range_erase(struct whimory *w, struct whimory_range *r) { - struct fpart_slice *sl = fpart_by_kind(k); - - if (!sl || !nsec || start >= ftl_capacity) - return; - if (start + nsec > ftl_capacity) - nsec = ftl_capacity - start; - sl->start_lba = start; - sl->nsectors = nsec; - sl->present = true; + rb_erase(&r->rb, &w->ranges); + kfree(r); + if (w->sftl.range_nodes) + w->sftl.range_nodes--; } -/* - * Discover host partitions inside the FTL LBA space. - * N31: FTL capacity often already equals the user FAT (WMR_Partition). - * Still probe for classic MBR / [hi] / second FAT so firmware can be split out. - */ -static int fpart_scan_ex(unsigned int scan_n) +static int whimory_range_insert_new(struct whimory *w, u32 start, u32 len, + u32 vba) { - u8 *sec; - u64 i, fat0 = ~0ULL, fat1 = ~0ULL, hi_lba = ~0ULL; - u64 mbr_fat_start = ~0ULL, mbr_fat_size = 0; - u64 mbr_other_start = ~0ULL, mbr_other_size = 0; - bool have_mbr = false, l0_fat = false; - int ret; - - fpart_clear_slices(); - fpart_status_reset(); + struct whimory_range *n; - sec = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_KERNEL); - if (!sec) + if (!len) + return 0; + n = kzalloc(sizeof(*n), GFP_KERNEL); + if (!n) return -ENOMEM; + n->start = start; + n->len = len; + n->vba = vba; + whimory_range_link(&w->ranges, n); + w->sftl.range_nodes++; + return 0; +} - if (!scan_n) - scan_n = 64; - if (scan_n > FPART_SCAN_LBAS) - scan_n = FPART_SCAN_LBAS; - if (scan_n > ftl_capacity) - scan_n = (unsigned int)ftl_capacity; - - ret = ftl_read_lba(0, sec); - if (ret) { - fpart_status_printf("LBA0 read failed %d\n", ret); - kfree(sec); - return ret; - } +static void whimory_range_coalesce_at(struct whimory *w, u32 start) +{ + struct whimory_range *r, *prev, *next; + struct rb_node *p, *q; - l0_fat = is_apple_fat_bpb(sec); - if (is_hi_firmware_hdr(sec)) { - hi_lba = 0; - fpart_status_printf("LBA0: [hi] firmware volume header\n"); + r = whimory_range_find(&w->ranges, start); + if (!r) + return; + p = rb_prev(&r->rb); + if (p) { + prev = rb_entry(p, struct whimory_range, rb); + if (prev->start + prev->len == r->start && + prev->vba + prev->len == r->vba) { + prev->len += r->len; + whimory_range_erase(w, r); + r = prev; + } } - if (is_mbr(sec)) { - unsigned int p; - - have_mbr = true; - fpart_status_printf("LBA0: MBR partition table\n"); - for (p = 0; p < 4; p++) { - const u8 *e = sec + 0x1be + p * 16; - u8 type = e[4]; - u32 start512 = e[8] | (e[9] << 8) | (e[10] << 16) | - (e[11] << 24); - u32 size512 = e[12] | (e[13] << 8) | (e[14] << 16) | - (e[15] << 24); - u64 start4k = (u64)start512 / 8; - u64 size4k = (u64)size512 / 8; - - if (!type || !size512) - continue; - fpart_status_printf( - " mbr[%u] type=0x%02x start4k=%llu size4k=%llu\n", - p, type, start4k, size4k); - if (type == 0x0b || type == 0x0c || type == 0x1b || - type == 0x1c) { - mbr_fat_start = start4k; - mbr_fat_size = size4k; - } else if (mbr_other_start == ~0ULL) { - mbr_other_start = start4k; - mbr_other_size = size4k; - } + q = rb_next(&r->rb); + if (q) { + next = rb_entry(q, struct whimory_range, rb); + if (r->start + r->len == next->start && + r->vba + r->len == next->vba) { + r->len += next->len; + whimory_range_erase(w, next); } } - if (l0_fat) - fpart_status_printf("LBA0: Apple/FAT BPB (superfloppy or volume)\n"); - if (looks_img1_dir(sec)) - fpart_status_printf("LBA0: possible IMG1/MSE directory tags\n"); +} - if (fw_nsectors) - fpart_set(FPART_FIRMWARE, fw_start_lba, fw_nsectors); - if (ipod_start_lba || ipod_nsectors) { - u64 st = ipod_start_lba; - u64 ns = ipod_nsectors ? ipod_nsectors : (ftl_capacity - st); +static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) +{ + u32 end = lba + span; + struct whimory_range *hit; + struct rb_node *node, *next; + int ret; - fpart_set(FPART_IPOD, st, ns); - } + if (!span || whimory_special_lba(lba)) + return 0; - if (!fpart_by_kind(FPART_IPOD)->present) { - if (have_mbr && mbr_fat_start != ~0ULL && mbr_fat_size) { - fpart_set(FPART_IPOD, mbr_fat_start, mbr_fat_size); - if (!fpart_by_kind(FPART_FIRMWARE)->present && - mbr_other_start != ~0ULL) - fpart_set(FPART_FIRMWARE, mbr_other_start, - mbr_other_size); - } else if (l0_fat) { - fpart_set(FPART_IPOD, 0, ftl_capacity); - fpart_status_printf( - "layout: superfloppy — FTL == ipod userdata\n"); - } + hit = whimory_range_find(&w->ranges, lba); + if (hit) { + ret = whimory_range_split(w, hit, lba); + if (ret) + return ret; } - - for (i = 1; i < scan_n; i++) { - if (ftl_read_lba(i, sec)) - continue; - if (hi_lba == ~0ULL && is_hi_firmware_hdr(sec)) { - hi_lba = i; - fpart_status_printf("LBA%llu: [hi] firmware header\n", i); - } - if (is_apple_fat_bpb(sec)) { - if (fat0 == ~0ULL) { - fat0 = i; - fpart_status_printf("LBA%llu: FAT BPB #1\n", i); - } else if (fat1 == ~0ULL && i > fat0 + 8) { - fat1 = i; - fpart_status_printf("LBA%llu: FAT BPB #2\n", i); - break; - } + if (end) { + hit = whimory_range_find(&w->ranges, end - 1); + if (hit && hit->start < end) { + ret = whimory_range_split(w, hit, end); + if (ret) + return ret; } - if (i == 7 && is_hi_firmware_hdr(sec)) - fpart_status_printf("LBA7: [hi] (classic 512-LBA 63)\n"); } - if (!fpart_by_kind(FPART_FIRMWARE)->present && hi_lba != ~0ULL) { - u64 fw_end = (fat0 != ~0ULL && fat0 > hi_lba) ? fat0 : - (ftl_capacity / 32); + node = rb_first(&w->ranges); + while (node) { + struct whimory_range *r = rb_entry(node, struct whimory_range, + rb); - if (fw_end > hi_lba) - fpart_set(FPART_FIRMWARE, hi_lba, fw_end - hi_lba); + next = rb_next(node); + if (r->start >= end) + break; + if (r->start >= lba && r->start + r->len <= end) + whimory_range_erase(w, r); + node = next; } - if (!fpart_by_kind(FPART_IPOD)->present && fat0 != ~0ULL) { - u64 ns = (fat1 != ~0ULL) ? (fat1 - fat0) : (ftl_capacity - fat0); - - fpart_set(FPART_IPOD, fat0, ns); - } + ret = whimory_range_insert_new(w, lba, span, vba); + if (ret) + return ret; + whimory_range_coalesce_at(w, lba); + return 0; +} - if (!fpart_by_kind(FPART_IPOD)->present) { - fpart_set(FPART_IPOD, 0, ftl_capacity); - fpart_status_printf( - "fallback: ipod = whole FTL (no separate FAT found)\n"); - } +/* + * sub_3F8958 L2V_Update.c: split at 0x8000 root boundaries, then insert. + * The interval map is the RO observable of the live tree. + */ +static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) +{ + w->sftl.l2v_update_calls++; + if (vba >= w->l2v.invalid_vba) + w->sftl.l2v_unmap_calls++; + while (span) { + u32 chunk = WHIMORY_L2V_ROOT_SPAN - + (lba & (WHIMORY_L2V_ROOT_SPAN - 1)); + int ret; - { - struct fpart_slice *fw = fpart_by_kind(FPART_FIRMWARE); - struct fpart_slice *ipod = fpart_by_kind(FPART_IPOD); + if (chunk > span) + chunk = span; + if (w->l2v.root && w->l2v.num_roots) { + u32 ridx = lba >> 15; + u8 *rec; + u16 ver, node_idx; - if (fw->present && fat0 != ~0ULL && - fat0 >= fw->start_lba && - fat0 < fw->start_lba + fw->nsectors && - fat0 != ipod->start_lba) { - u64 rsrc_n = fw->start_lba + fw->nsectors - fat0; + if (ridx < w->l2v.num_roots) { + rec = w->l2v.root + ridx * WHIMORY_L2V_ROOT_REC_SIZE; + ver = get_unaligned_le16(rec + 4); + if (ver == 0xffff) + ver = 0; + put_unaligned_le16(ver + 1, rec + 4); + /* + * sub_110734: whole-root unmap (off=0, + * span=0x8000, vba=invalid) frees the tree. + */ + if (!(lba & 0x7fff) && + chunk == WHIMORY_L2V_ROOT_SPAN && + vba >= w->l2v.invalid_vba) { + node_idx = get_unaligned_le16(rec); + if (node_idx != WHIMORY_L2V_INVALID_ROOT) + whimory_l2v_free_tree(w, + node_idx, + ridx); + put_unaligned_le16( + WHIMORY_L2V_INVALID_ROOT, rec); + } + } + w->l2v.updates++; + w->l2v.gen++; + if (w->l2v.updates >= WHIMORY_L2V_UPDATE_REPACK) + w->l2v.updates = 0; + } + ret = whimory_range_update(w, lba, chunk, vba); + if (ret) + return ret; + if (w->l2v.root && w->l2v.num_roots) { + u32 ridx = lba >> 15; + bool whole_unmap = !(lba & 0x7fff) && + chunk == WHIMORY_L2V_ROOT_SPAN && + vba >= w->l2v.invalid_vba; - if (ipod->present && ipod->start_lba > fat0) - rsrc_n = ipod->start_lba - fat0; - fpart_set(FPART_RSRC, fat0, rsrc_n); + if (ridx < w->l2v.num_roots && !whole_unmap) { + ret = whimory_l2v_update_packed(w, ridx, + lba & 0x7fff, chunk, vba); + if (ret) + dev_dbg(w->dev, + "L2V packed update r=%u %d\n", + ridx, ret); + } } + span -= chunk; + lba += chunk; + if (vba < w->l2v.invalid_vba) + vba += chunk; } + return 0; +} - fpart_status_printf( - "scanned_lbas=%u summary: fw=%d@%llu+%llu ipod=%d@%llu+%llu rsrc=%d@%llu+%llu\n", - scan_n, - fpart_by_kind(FPART_FIRMWARE)->present, - fpart_by_kind(FPART_FIRMWARE)->start_lba, - fpart_by_kind(FPART_FIRMWARE)->nsectors, - fpart_by_kind(FPART_IPOD)->present, - fpart_by_kind(FPART_IPOD)->start_lba, - fpart_by_kind(FPART_IPOD)->nsectors, - fpart_by_kind(FPART_RSRC)->present, - fpart_by_kind(FPART_RSRC)->start_lba, - fpart_by_kind(FPART_RSRC)->nsectors); +static void whimory_range_free(struct whimory *w) +{ + struct rb_node *n; - kfree(sec); + while ((n = rb_first(&w->ranges))) { + struct whimory_range *r = rb_entry(n, struct whimory_range, rb); - ret = 0; - ret |= fpart_register_slice(fpart_by_kind(FPART_FIRMWARE)); - ret |= fpart_register_slice(fpart_by_kind(FPART_IPOD)); - ret |= fpart_register_slice(fpart_by_kind(FPART_RSRC)); - return ret < 0 ? ret : 0; + whimory_range_erase(w, r); + } + w->ranges = RB_ROOT; + w->sftl.range_nodes = 0; } -static int fpart_scan(void) +/* ------------------------------------------------------------------ */ +/* L2V init / lookup / tree pack (sub_E8CA0, sub_428694) */ +/* ------------------------------------------------------------------ */ + +static void whimory_l2v_free(struct whimory *w) { - return fpart_scan_ex(64); + kvfree(w->l2v.root); + kvfree(w->l2v.nodes); + kvfree(w->l2v.leaf_scratch); + w->l2v.root = NULL; + w->l2v.nodes = NULL; + w->l2v.leaf_scratch = NULL; + w->l2v.num_roots = 0; + w->l2v.nodepool_bytes = 0; + w->l2v.free_head = WHIMORY_L2V_INVALID_ROOT; + w->l2v.free_count = 0; } -static ssize_t validate_sector_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +/* L2V_Mem.c sub_3EB0DC / sub_3EAEC8 — intrusive free list in node[0]. */ +static void whimory_l2v_mem_free(struct whimory_l2v *l2v, u32 idx) { - u64 sector; - u8 *secbuf; - unsigned int i, n; - int ret; + u8 *node; + u32 n = l2v->nodepool_bytes / WHIMORY_L2V_NODE_SIZE; - if (kstrtoull(buf, 0, §or)) - return -EINVAL; - if (sector >= ftl_capacity) - return -ERANGE; + if (!l2v->nodes || idx >= n) + return; + node = l2v->nodes + idx * WHIMORY_L2V_NODE_SIZE; + put_unaligned_le32(l2v->free_head, node); + l2v->free_head = idx; + l2v->free_count++; +} - secbuf = kmalloc(FMSS_FTL_SECTOR_SIZE, GFP_KERNEL); - if (!secbuf) - return -ENOMEM; +static void whimory_l2v_mem_reset(struct whimory_l2v *l2v) +{ + u32 n = l2v->nodepool_bytes / WHIMORY_L2V_NODE_SIZE; + s32 j; - ret = fmss_ftl_read_sector(sector, secbuf); - if (ret) { - kfree(secbuf); - dev_warn(dev, "validate sector %llu failed: %d\n", sector, ret); - return ret; - } + l2v->free_head = WHIMORY_L2V_INVALID_ROOT; + l2v->free_count = 0; + l2v->nodes_used = 0; + if (!l2v->nodes || !n) + return; + for (j = (s32)n - 1; j >= 0; j--) + whimory_l2v_mem_free(l2v, (u32)j); +} - n = min_t(unsigned int, FTL_VALIDATE_HEX, FMSS_FTL_SECTOR_SIZE); - { - /* One line — bare printk("%02x") becomes a dmesg line each. */ - char hex[FTL_VALIDATE_HEX * 3 + 4]; - unsigned int pos = 0; +static u32 whimory_l2v_alloc_node(struct whimory_l2v *l2v) +{ + u32 idx, next; + u8 *node; + u32 n = l2v->nodepool_bytes / WHIMORY_L2V_NODE_SIZE; - for (i = 0; i < n && pos + 3 < sizeof(hex); i++) - pos += scnprintf(hex + pos, sizeof(hex) - pos, "%02x", - secbuf[i]); - dev_info(dev, "sector %llu ok head[%u]: %s\n", sector, n, hex); - } - kfree(secbuf); - return count; + idx = l2v->free_head; + if (idx == WHIMORY_L2V_INVALID_ROOT || !l2v->free_count || idx >= n) + return WHIMORY_L2V_INVALID_ROOT; + node = l2v->nodes + idx * WHIMORY_L2V_NODE_SIZE; + next = get_unaligned_le32(node); + l2v->free_head = next; + l2v->free_count--; + memset(node, 0, WHIMORY_L2V_NODE_SIZE); + if (idx + 1 > l2v->nodes_used) + l2v->nodes_used = idx + 1; + return idx; } -static DEVICE_ATTR_WO(validate_sector); -static ssize_t map_build_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static int whimory_l2v_init(struct whimory *w, u32 max_lba, + u32 vba_factor_a, u32 vba_factor_b, + u32 nodepool_bytes) { - unsigned int max_lpn = ftl_map_max_lpn; - int ret; + struct whimory_l2v *l2v = &w->l2v; + u64 prod; + + whimory_l2v_free(w); - if (buf[0] && buf[0] != '\n' && kstrtouint(buf, 0, &max_lpn)) + if (nodepool_bytes < WHIMORY_MIN_NODEPOOL_BYTES) + nodepool_bytes = WHIMORY_MIN_NODEPOOL_BYTES; + + prod = 2ull * vba_factor_a * vba_factor_b; + if (prod < 2) + return -EINVAL; + l2v->bits_vba = fls64(prod - 1) - 1; + if (!l2v->bits_vba || l2v->bits_vba > 30) return -EINVAL; - if (!max_lpn) - max_lpn = (unsigned int)(ftl_capacity / FMSS_FTL_SECTORS_PER_LPN) + 64; + l2v->spanbits_vba = 30 - l2v->bits_vba; - ret = fmss_ftl_build_map(max_lpn); - if (ret) - return ret; + prod = 2ull * (nodepool_bytes >> 6); + if (prod < 2) + return -EINVAL; + l2v->bits_nodeidx = fls64(prod - 1) - 1; + if (!l2v->bits_nodeidx || l2v->bits_nodeidx > 30) + return -EINVAL; + l2v->spanbits_nodeidx = 30 - l2v->bits_nodeidx; - dev_info(dev, "LPN map built max=%u entries=%u\n", - max_lpn, fmss_ftl_lpn_count()); - return count; -} -static DEVICE_ATTR_WO(map_build); + l2v->sentinel_vba = (1u << l2v->bits_vba) - 1; + l2v->invalid_vba = l2v->sentinel_vba; + l2v->num_roots = (max_lba >> 15) + 1; + l2v->nodepool_bytes = nodepool_bytes; + l2v->nodes_used = 0; + l2v->updates = 0; + l2v->gen = 0; + l2v->frag_count = 0; + l2v->frag_max = 0; + l2v->free_head = WHIMORY_L2V_INVALID_ROOT; + l2v->free_count = 0; -static ssize_t fpart_scan_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) -{ - unsigned int n = 64; - int ret; + l2v->root = kvzalloc(WHIMORY_L2V_ROOT_REC_SIZE * l2v->num_roots, + GFP_KERNEL); + if (!l2v->root) + return -ENOMEM; + l2v->nodes = kvzalloc(nodepool_bytes, GFP_KERNEL); + if (!l2v->nodes) { + kvfree(l2v->root); + l2v->root = NULL; + return -ENOMEM; + } + l2v->leaf_scratch = kvcalloc(WHIMORY_L2V_ROOT_SPAN, + sizeof(*l2v->leaf_scratch), GFP_KERNEL); + if (!l2v->leaf_scratch) { + kvfree(l2v->nodes); + kvfree(l2v->root); + l2v->nodes = NULL; + l2v->root = NULL; + return -ENOMEM; + } + memset(l2v->root, 0xff, WHIMORY_L2V_ROOT_REC_SIZE * l2v->num_roots); + memset(l2v->nodes, 0, nodepool_bytes); + whimory_l2v_mem_reset(l2v); - if (buf[0] && buf[0] != '\n' && kstrtouint(buf, 0, &n)) - return -EINVAL; - ret = fpart_scan_ex(n); - return ret ? ret : count; + dev_info(w->dev, + "L2V init roots=%u nodepool=0x%x bits_vba=%u/%u bits_node=%u/%u invalid=0x%x\n", + l2v->num_roots, l2v->nodepool_bytes, + l2v->bits_vba, l2v->spanbits_vba, + l2v->bits_nodeidx, l2v->spanbits_nodeidx, + l2v->invalid_vba); + return 0; } -static DEVICE_ATTR_WO(fpart_scan); -static ssize_t fpart_status_show(struct device *dev, struct device_attribute *attr, - char *buf) +static int whimory_l2v_encode(struct whimory_l2v *l2v, u8 *node, + u32 *front, u32 *back, bool is_node, + u32 value, u32 span) { - if (!fpart_status_len) - return sysfs_emit(buf, "(no fpart_scan yet)\n"); - return sysfs_emit(buf, "%.*s", (int)fpart_status_len, fpart_status); -} -static DEVICE_ATTR_RO(fpart_status); + u8 value_bits = is_node ? l2v->bits_nodeidx : l2v->bits_vba; + u8 span_bits = is_node ? l2v->spanbits_nodeidx : l2v->spanbits_vba; + u32 span_m1, span_mask, value_mask, e, need; + bool has_ext; -static ssize_t lpn_count_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - return sysfs_emit(buf, "%u\n", fmss_ftl_lpn_count()); + if (!span || !value_bits) + return -EINVAL; + span_m1 = span - 1; + span_mask = span_bits ? ((1u << span_bits) - 1) : 0; + value_mask = (1u << value_bits) - 1; + if (value > value_mask) + return -EINVAL; + has_ext = span_m1 > span_mask; + if (has_ext && span_bits && (span_m1 >> span_bits) > 0xffffu) + return -E2BIG; + need = 4 + (has_ext ? 2 : 0); + if (*front + need > *back) + return -ENOSPC; + e = (is_node ? 1u : 0u) | (has_ext ? 2u : 0u); + e |= (value & value_mask) << 2; + e |= (span_m1 & span_mask) << (value_bits + 2); + put_unaligned_le32(e, node + *front); + *front += 4; + if (has_ext) { + *back -= 2; + put_unaligned_le16((u16)(span_m1 >> span_bits), + node + *back); + } + return 0; } -static DEVICE_ATTR_RO(lpn_count); -static ssize_t capacity_show(struct device *dev, struct device_attribute *attr, - char *buf) +static u32 whimory_leaf_span_sum(const struct whimory_leaf *l, u32 n) { - return sysfs_emit(buf, "%llu\n", ftl_capacity); -} -static DEVICE_ATTR_RO(capacity); + u32 i, s = 0; -static struct attribute *ftl_attrs[] = { - &dev_attr_validate_sector.attr, - &dev_attr_map_build.attr, - &dev_attr_fpart_scan.attr, - &dev_attr_fpart_status.attr, - &dev_attr_lpn_count.attr, - &dev_attr_capacity.attr, - NULL, -}; + for (i = 0; i < n; i++) + s += l[i].span; + return s; +} -static const struct attribute_group ftl_attr_group = { - .attrs = ftl_attrs, -}; +static int whimory_l2v_pack_leaves(struct whimory *w, + const struct whimory_leaf *leaves, u32 n, + u32 *idx_out); -static int ftl_register_disk(void) +static int whimory_l2v_pack_parent(struct whimory *w, u32 left, u32 span_l, + u32 right, u32 span_r, u32 *idx_out) { - struct queue_limits lim = { - .logical_block_size = FMSS_FTL_SECTOR_SIZE, - .physical_block_size = FMSS_FTL_SECTOR_SIZE, - }; + struct whimory_l2v *l2v = &w->l2v; + u8 *node; + u32 idx, front, back; int ret; - ftl_disk = blk_alloc_disk(&lim, NUMA_NO_NODE); - if (IS_ERR(ftl_disk)) - return PTR_ERR(ftl_disk); - - ftl_disk->first_minor = 0; - ftl_disk->flags = GENHD_FL_NO_PART; - ftl_disk->fops = &ftl_bd_ops; - snprintf(ftl_disk->disk_name, DISK_NAME_LEN, "%s", FTL_DISK_NAME); - set_capacity(ftl_disk, ftl_capacity * (FMSS_FTL_SECTOR_SIZE / 512)); - - ret = add_disk(ftl_disk); + idx = whimory_l2v_alloc_node(l2v); + if (idx == WHIMORY_L2V_INVALID_ROOT) + return -ENOMEM; + node = l2v->nodes + idx * WHIMORY_L2V_NODE_SIZE; + front = 0; + back = WHIMORY_L2V_NODE_SIZE; + ret = whimory_l2v_encode(l2v, node, &front, &back, true, left, span_l); + if (!ret) + ret = whimory_l2v_encode(l2v, node, &front, &back, true, + right, span_r); if (ret) { - put_disk(ftl_disk); - ftl_disk = NULL; + memset(node, 0xff, WHIMORY_L2V_NODE_SIZE); + return ret; } - return ret; + if (front <= back - 4) + put_unaligned_le32(0xffffffff, node + front); + *idx_out = idx; + return 0; } -static void ftl_unregister_disk(void) +static int whimory_l2v_pack_leaves(struct whimory *w, + const struct whimory_leaf *leaves, u32 n, + u32 *idx_out) { - fpart_clear_slices(); - if (ftl_disk) { - del_gendisk(ftl_disk); - put_disk(ftl_disk); - ftl_disk = NULL; + struct whimory_l2v *l2v = &w->l2v; + u8 *node; + u32 idx, front, back, i, mid, left, right, span_l, span_r; + int ret; + + if (!n) + return -EINVAL; + + idx = whimory_l2v_alloc_node(l2v); + if (idx == WHIMORY_L2V_INVALID_ROOT) + return -ENOMEM; + node = l2v->nodes + idx * WHIMORY_L2V_NODE_SIZE; + front = 0; + back = WHIMORY_L2V_NODE_SIZE; + for (i = 0; i < n; i++) { + ret = whimory_l2v_encode(l2v, node, &front, &back, false, + leaves[i].vba, leaves[i].span); + if (ret) + break; } -} + if (!ret) { + if (front <= back - 4) + put_unaligned_le32(0xffffffff, node + front); + *idx_out = idx; + return 0; + } + memset(node, 0xff, WHIMORY_L2V_NODE_SIZE); + if (l2v->nodes_used == idx + 1) + l2v->nodes_used = idx; -static int __init ftl_init(void) -{ - unsigned int max_lpn; - int ret; + if (n == 1) { + struct whimory_leaf half[2]; + u32 s0, s1; - if (!fmss_ftl_present()) { - pr_err("s5l8740-ftl: load fmss-s5l8740.ko first\n"); - return -ENODEV; + s0 = leaves[0].span / 2; + s1 = leaves[0].span - s0; + if (!s0 || !s1) + return -EINVAL; + half[0].vba = leaves[0].vba; + half[0].span = s0; + half[1].vba = leaves[0].vba + s0; + half[1].span = s1; + ret = whimory_l2v_pack_leaves(w, half, 1, &left); + if (ret) + return ret; + ret = whimory_l2v_pack_leaves(w, half + 1, 1, &right); + if (ret) + return ret; + return whimory_l2v_pack_parent(w, left, s0, right, s1, idx_out); } - ret = ftl_register_disk(); + mid = n / 2; + if (!mid) + mid = 1; + ret = whimory_l2v_pack_leaves(w, leaves, mid, &left); if (ret) return ret; - - ftl_pdev = platform_device_register_simple("s5l8740-ftl", -1, NULL, 0); - if (IS_ERR(ftl_pdev)) { - ret = PTR_ERR(ftl_pdev); - ftl_unregister_disk(); + ret = whimory_l2v_pack_leaves(w, leaves + mid, n - mid, &right); + if (ret) return ret; - } + span_l = whimory_leaf_span_sum(leaves, mid); + span_r = whimory_leaf_span_sum(leaves + mid, n - mid); + return whimory_l2v_pack_parent(w, left, span_l, right, span_r, idx_out); +} - ret = sysfs_create_group(&ftl_pdev->dev.kobj, &ftl_attr_group); - if (ret) { - platform_device_unregister(ftl_pdev); - ftl_unregister_disk(); - return ret; - } +static u32 whimory_l2v_collect_root(struct whimory *w, u32 ridx, + struct whimory_leaf *leaves) +{ + struct whimory_l2v *l2v = &w->l2v; + u32 base = ridx * WHIMORY_L2V_ROOT_SPAN; + u32 win_end = base + WHIMORY_L2V_ROOT_SPAN; + u32 cursor = base, nleaf = 0; + struct rb_node *n; - if (ftl_auto_map) { - max_lpn = ftl_map_max_lpn; - if (!max_lpn) - max_lpn = (unsigned int)(ftl_capacity / - FMSS_FTL_SECTORS_PER_LPN) + 64; - ret = fmss_ftl_build_map(max_lpn); - if (ret) - dev_warn(&ftl_pdev->dev, "auto map_build failed (%d)\n", - ret); - } + for (n = rb_first(&w->ranges); n; n = rb_next(n)) { + struct whimory_range *rg = rb_entry(n, struct whimory_range, rb); + u32 s, e, vba, span; - if (fpart_auto_scan) { - ret = fpart_scan(); - if (ret) - dev_warn(&ftl_pdev->dev, "fpart_scan failed (%d)\n", ret); + if (rg->start >= win_end) + break; + e = rg->start + rg->len; + if (e <= base) + continue; + s = max(rg->start, base); + e = min(e, win_end); + if (s >= e) + continue; + vba = rg->vba + (s - rg->start); + if (s > cursor) { + leaves[nleaf].vba = l2v->invalid_vba; + leaves[nleaf].span = s - cursor; + nleaf++; + } + span = e - s; + leaves[nleaf].vba = vba; + leaves[nleaf].span = span; + nleaf++; + cursor = e; } - - dev_info(&ftl_pdev->dev, - "NAND-only Whimory FTL /dev/%s (%llu x %uB); named slices via fpart_scan\n", - FTL_DISK_NAME, ftl_capacity, FMSS_FTL_SECTOR_SIZE); - return 0; + if (nleaf && cursor < win_end) { + leaves[nleaf].vba = l2v->invalid_vba; + leaves[nleaf].span = win_end - cursor; + nleaf++; + } + return nleaf; } -static void __exit ftl_exit(void) +/* sub_E8EC0 analogue: free this root's tree, pack from the interval map. */ +static int whimory_l2v_pack_root(struct whimory *w, u32 ridx) { + struct whimory_l2v *l2v = &w->l2v; + struct whimory_leaf *leaves = l2v->leaf_scratch; + u8 *rec; + u16 ver, node_old; + u32 nleaf, node_idx, used0; + int ret; + + if (!leaves || ridx >= l2v->num_roots) + return -EINVAL; + rec = l2v->root + ridx * WHIMORY_L2V_ROOT_REC_SIZE; + nleaf = whimory_l2v_collect_root(w, ridx, leaves); + node_old = get_unaligned_le16(rec); + if (node_old != WHIMORY_L2V_INVALID_ROOT) + whimory_l2v_free_tree(w, node_old, ridx); + if (!nleaf) { + put_unaligned_le16(WHIMORY_L2V_INVALID_ROOT, rec); + put_unaligned_le16(0, rec + 2); + return 0; + } + w->sftl.l2v_repack_roots++; + used0 = l2v->nodes_used; + ret = whimory_l2v_pack_leaves(w, leaves, nleaf, &node_idx); + if (ret) { + put_unaligned_le16(WHIMORY_L2V_INVALID_ROOT, rec); + put_unaligned_le16(0, rec + 2); + return ret; + } + ver = get_unaligned_le16(rec + 4); + if (ver == 0xffff) + ver = 1; + put_unaligned_le16((u16)node_idx, rec); + put_unaligned_le16((u16)min_t(u32, l2v->nodes_used - used0, 0xffff), + rec + 2); + put_unaligned_le16(ver, rec + 4); + return 0; +} + +/* sub_10FE4C: first insert into an empty root — one node, up to 3 leaves. */ +static int whimory_l2v_grow_empty(struct whimory *w, u32 ridx, u32 off, + u32 span, u32 vba) +{ + struct whimory_l2v *l2v = &w->l2v; + u8 *node, *rec; + u32 idx, front = 0, back = WHIMORY_L2V_NODE_SIZE; + u32 old = l2v->invalid_vba; + int ret; + + if (off + span > WHIMORY_L2V_ROOT_SPAN) + return -EINVAL; + idx = whimory_l2v_alloc_node(l2v); + if (idx == WHIMORY_L2V_INVALID_ROOT) + return -ENOMEM; + node = l2v->nodes + idx * WHIMORY_L2V_NODE_SIZE; + if (off) { + ret = whimory_l2v_encode(l2v, node, &front, &back, false, old, + off); + if (ret) + goto fail; + } + ret = whimory_l2v_encode(l2v, node, &front, &back, false, vba, span); + if (ret) + goto fail; + if (off + span < WHIMORY_L2V_ROOT_SPAN) { + u32 tail = WHIMORY_L2V_ROOT_SPAN - off - span; + u32 tail_vba = old; + + if (old < l2v->invalid_vba) + tail_vba = old + off + span; + ret = whimory_l2v_encode(l2v, node, &front, &back, false, + tail_vba, tail); + if (ret) + goto fail; + } + if (front < back) + memset(node + front, 0xff, back - front); + rec = l2v->root + ridx * WHIMORY_L2V_ROOT_REC_SIZE; + put_unaligned_le16((u16)idx, rec); + put_unaligned_le16(1, rec + 2); + return 0; +fail: + whimory_l2v_mem_free(l2v, idx); + return ret; +} + +static int whimory_l2v_update_packed(struct whimory *w, u32 ridx, u32 off, + u32 span, u32 vba) +{ + u16 node_idx; + u8 *rec; + + if (!w->l2v.root || ridx >= w->l2v.num_roots || !span) + return 0; + rec = w->l2v.root + ridx * WHIMORY_L2V_ROOT_REC_SIZE; + node_idx = get_unaligned_le16(rec); + if (node_idx == WHIMORY_L2V_INVALID_ROOT) + return whimory_l2v_grow_empty(w, ridx, off, span, vba); + return whimory_l2v_pack_root(w, ridx); +} + +static int whimory_l2v_build_from_ranges(struct whimory *w) +{ + struct whimory_l2v *l2v = &w->l2v; + u32 ridx, mapped_roots = 0, mapped_lbas = 0; + int ret = 0; + struct rb_node *n; + + if (!l2v->root || !l2v->nodes || !l2v->leaf_scratch) + return -ENODEV; + + { + u16 *vers; + u32 i; + + vers = kvmalloc_array(l2v->num_roots, sizeof(u16), GFP_KERNEL); + if (!vers) + return -ENOMEM; + for (i = 0; i < l2v->num_roots; i++) + vers[i] = get_unaligned_le16( + l2v->root + i * WHIMORY_L2V_ROOT_REC_SIZE + 4); + for (i = 0; i < l2v->num_roots; i++) { + u8 *rec = l2v->root + i * WHIMORY_L2V_ROOT_REC_SIZE; + + put_unaligned_le16(WHIMORY_L2V_INVALID_ROOT, rec); + put_unaligned_le16(0, rec + 2); + put_unaligned_le16(vers[i], rec + 4); + } + kvfree(vers); + whimory_l2v_mem_reset(l2v); + } + + for (ridx = 0; ridx < l2v->num_roots; ridx++) { + ret = whimory_l2v_pack_root(w, ridx); + if (ret) + break; + if (get_unaligned_le16(l2v->root + + ridx * WHIMORY_L2V_ROOT_REC_SIZE) != + WHIMORY_L2V_INVALID_ROOT) + mapped_roots++; + } + for (n = rb_first(&w->ranges); n; n = rb_next(n)) { + struct whimory_range *rg = rb_entry(n, struct whimory_range, rb); + + if (rg->vba < l2v->invalid_vba) + mapped_lbas += rg->len; + } + w->sftl.mapped_roots = mapped_roots; + w->sftl.mapped_lbas = mapped_lbas; + if (ret) + return ret; + whimory_l2v_find_frag(w); + if (l2v->free_count < WHIMORY_L2V_MIN_FREE) + dev_warn(w->dev, "L2V free %u < %u after pack\n", + l2v->free_count, WHIMORY_L2V_MIN_FREE); + dev_info(w->dev, + "L2V recovery OK mapped_roots=%u mapped_lbas=%u nodes_used=%u range_nodes=%u frag=%u/%u free=%u\n", + mapped_roots, mapped_lbas, l2v->nodes_used, + w->sftl.range_nodes, l2v->frag_count, l2v->frag_max, + l2v->free_count); + return mapped_roots ? 0 : -ENOENT; +} + +/* L2V_FindFrag.c sub_10C344 — walk leaves, record fragment stats. */ +static void whimory_l2v_find_frag_node(struct whimory *w, u32 node_idx, + u32 *count, u32 *maxspan, int depth) +{ + struct whimory_l2v *l2v = &w->l2v; + const u8 *node; + u32 front = 0, back = WHIMORY_L2V_NODE_SIZE; + + if (depth > WHIMORY_L2V_FINDFRAG_WIN || + (node_idx + 1) * WHIMORY_L2V_NODE_SIZE > + l2v->nodepool_bytes) + return; + node = l2v->nodes + node_idx * WHIMORY_L2V_NODE_SIZE; + while (front + 4 <= back) { + u32 e = get_unaligned_le32(node + front); + bool is_node, has_ext; + u32 value_bits, span_bits, value, span_minus1, span; + + if (e == 0xffffffff) + break; + is_node = e & 1; + has_ext = e & 2; + value_bits = is_node ? l2v->bits_nodeidx : l2v->bits_vba; + span_bits = is_node ? l2v->spanbits_nodeidx : l2v->spanbits_vba; + value = (e >> 2) & ((1u << value_bits) - 1); + span_minus1 = span_bits ? + ((e >> (value_bits + 2)) & ((1u << span_bits) - 1)) : 0; + if (has_ext) { + back -= 2; + if (front + 4 > back) + break; + span_minus1 += (u32)get_unaligned_le16(node + back) << + span_bits; + } + span = span_minus1 + 1; + if (is_node) + whimory_l2v_find_frag_node(w, value, count, maxspan, + depth + 1); + else { + (*count)++; + if (span > *maxspan) + *maxspan = span; + } + front += 4; + } +} + +static void whimory_l2v_free_tree(struct whimory *w, u32 node_idx, u32 root_idx) +{ + struct whimory_l2v *l2v = &w->l2v; + const u8 *node; + u32 front = 0, back = WHIMORY_L2V_NODE_SIZE; + u8 *rec; + + if ((node_idx + 1) * WHIMORY_L2V_NODE_SIZE > l2v->nodepool_bytes) + return; + node = l2v->nodes + node_idx * WHIMORY_L2V_NODE_SIZE; + while (front + 4 <= back) { + u32 e = get_unaligned_le32(node + front); + bool is_node, has_ext; + u32 value_bits, value; + + if (e == 0xffffffff) + break; + is_node = e & 1; + has_ext = e & 2; + value_bits = is_node ? l2v->bits_nodeidx : l2v->bits_vba; + value = (e >> 2) & ((1u << value_bits) - 1); + if (has_ext) { + back -= 2; + if (front + 4 > back) + break; + } + if (is_node) + whimory_l2v_free_tree(w, value, root_idx); + front += 4; + } + whimory_l2v_mem_free(l2v, node_idx); + if (root_idx < l2v->num_roots) { + rec = l2v->root + root_idx * WHIMORY_L2V_ROOT_REC_SIZE; + { + u16 n_nodes = get_unaligned_le16(rec + 2); + + if (n_nodes && n_nodes != 0xffff) + put_unaligned_le16(n_nodes - 1, rec + 2); + } + } +} + +static void whimory_l2v_find_frag(struct whimory *w) +{ + struct whimory_l2v *l2v = &w->l2v; + u32 ridx, count = 0, maxspan = 0; + + if (!l2v->root || !l2v->nodes) + return; + for (ridx = 0; ridx < l2v->num_roots; ridx++) { + u32 node_idx = get_unaligned_le16( + l2v->root + ridx * WHIMORY_L2V_ROOT_REC_SIZE); + + if (node_idx == WHIMORY_L2V_INVALID_ROOT) + continue; + whimory_l2v_find_frag_node(w, node_idx, &count, &maxspan, 0); + } + l2v->frag_count = count; + l2v->frag_max = maxspan; +} + +static int whimory_l2v_lookup(struct whimory *w, u32 lba, + u32 *vba_out, u32 *span_out) +{ + struct whimory_l2v *l2v = &w->l2v; + u32 root_idx = lba >> 15; + u32 target = lba & 0x7fff; + u32 consumed; + u32 node_idx; + int depth; + + if (!l2v->root || !l2v->nodes) + return -ENODEV; + if (root_idx >= l2v->num_roots) + return -ERANGE; + + node_idx = get_unaligned_le16(l2v->root + 6 * root_idx); + if (node_idx == WHIMORY_L2V_INVALID_ROOT) + return -ENOENT; + + for (depth = 0; depth < 32; depth++) { + const u8 *node; + u32 front = 0; + u32 back = WHIMORY_L2V_NODE_SIZE; + + if ((node_idx + 1) * WHIMORY_L2V_NODE_SIZE > + l2v->nodepool_bytes) + return -EINVAL; + node = l2v->nodes + node_idx * WHIMORY_L2V_NODE_SIZE; + consumed = 0; + + while (front + 4 <= back) { + u32 e = get_unaligned_le32(node + front); + bool is_node, has_ext; + u32 value_bits, span_bits, value_mask, value; + u32 span_minus1, span; + + if (e == 0xffffffff) + break; + is_node = e & 1; + has_ext = e & 2; + if (is_node) { + value_bits = l2v->bits_nodeidx; + span_bits = l2v->spanbits_nodeidx; + } else { + value_bits = l2v->bits_vba; + span_bits = l2v->spanbits_vba; + } + value_mask = value_bits ? ((1u << value_bits) - 1) : 0; + value = (e >> 2) & value_mask; + span_minus1 = e >> (value_bits + 2); + if (span_bits) + span_minus1 &= (1u << span_bits) - 1; + else + span_minus1 = 0; + if (has_ext) { + back -= 2; + if (front + 4 > back) + return -EINVAL; + span_minus1 += + (u32)get_unaligned_le16(node + back) << + span_bits; + } + span = span_minus1 + 1; + if (!span) + return -EINVAL; + if (target < consumed + span) { + u32 delta = target - consumed; + + if (is_node) { + node_idx = value; + goto next_level; + } + if (value >= l2v->invalid_vba) + return -ENOENT; + *vba_out = value + delta; + *span_out = span - delta; + return 0; + } + consumed += span; + front += 4; + } + return -EINVAL; +next_level: + continue; + } + return -ELOOP; +} + +/* Prefer the interval map (L2V_Update result); packed tree is for Search. */ +static int whimory_l2v_search(struct whimory *w, u32 lba, + u32 *vba_out, u32 *span_out) +{ + struct whimory_range *r = whimory_range_find(&w->ranges, lba); + + if (r) { + u32 delta = lba - r->start; + + *vba_out = r->vba + delta; + *span_out = r->len - delta; + return 0; + } + return whimory_l2v_lookup(w, lba, vba_out, span_out); +} + +/* ------------------------------------------------------------------ */ +/* FIL */ +/* ------------------------------------------------------------------ */ + +static int whimory_fil_init(struct whimory *w) +{ + struct s5l8740_fmss_geom g; + int ret; + + ret = s5l8740_fmss_hw_init(); + if (ret) + return ret; + ret = s5l8740_fmss_query_geometry(&g); + if (ret) + return ret; + if (!g.dev_id) + return -ENODEV; + + w->geom.num_ce = g.num_ce; + w->geom.num_cau = g.num_cau; + w->geom.blocks_per_cau = g.blocks_per_cau; + w->geom.pages_per_block = g.pages_per_block; + w->geom.page_size = g.page_size; + w->geom.vfl_tail = g.vfl_tail; + w->geom.user_blocks = g.blocks_per_cau - g.vfl_tail; + w->geom.dev_id = s5l8740_fmss_fil_get_info(101); + w->geom.geom_104 = s5l8740_fmss_fil_get_info(104); + w->geom.geom_105 = s5l8740_fmss_fil_get_info(105); + w->geom.geom_135 = s5l8740_fmss_fil_get_info(135); + if (!w->geom.dev_id) + return -ENODEV; + if (w->geom.geom_104 && w->geom.geom_104 != w->geom.page_size) { + dev_err(w->dev, + "FIL GetInfo(104)=%u != page_size=%u\n", + w->geom.geom_104, w->geom.page_size); + return -EINVAL; + } + if (w->geom.geom_105 && w->geom.geom_105 != WHIMORY_FIL_META_BYTES) { + dev_err(w->dev, "FIL GetInfo(105)=%u != %u\n", + w->geom.geom_105, WHIMORY_FIL_META_BYTES); + return -EINVAL; + } + + dev_info(w->dev, + "FIL_Init OK dev_id=%u g104=%u g105=%u g135=%u ce=%u cau=%u blocks=%u user=%u param=%d\n", + w->geom.dev_id, w->geom.geom_104, w->geom.geom_105, + w->geom.geom_135, w->geom.num_ce, w->geom.num_cau, + w->geom.blocks_per_cau, w->geom.user_blocks, + g.from_param_page); + w->fil_ok = true; + return 0; +} + +/* ------------------------------------------------------------------ */ +/* FPart — signature from media (or oracle firmware file) */ +/* ------------------------------------------------------------------ */ + +/* + * sub_12F368 / sub_1122FC — FPart signature is NOT a user-page hunt. + * OSOS: memset(sig, 0xA5, 0x600) then _fpart->op80(sig, 0x600, 0xC101). + * READ ONLY — never AllocateSpecialBlock / WriteSpecial / erase. + * Validate: magic 0x776d7278, ver<=6, +0x34 == FIL GetInfo(101). + */ +static void whimory_log_sig_fields(struct whimory *w, const u8 *s, + const char *why) +{ + u32 magic = whimory_sig32(s, 0x00); + u32 ver = whimory_sig32(s, 0x08); + u32 ftl_m = whimory_sig32(s, 0x0c); + u32 ftl_n = whimory_sig32(s, 0x10); + u32 vfl_m = whimory_sig32(s, 0x18); + u32 vfl_n = whimory_sig32(s, 0x1c); + u32 vfl_arg = whimory_sig32(s, 0x20); + u32 fpt_m = whimory_sig32(s, 0x24); + u32 fpt_n = whimory_sig32(s, 0x28); + u32 fpt_a = whimory_sig32(s, 0x2c); + u32 extra = whimory_sig32(s, 0x30); + u32 geom = whimory_sig32(s, 0x34); + u32 cfg = whimory_sig32(s, 0xb8); + + dev_info(w->dev, + "WHIMORY_SIG %s magic=%08x ver=%u ftl=%u.%u vfl=%u.%u fpart=%u.%u geom=%u fil101=%u vfl_arg=%u fpart_arg=%u extra=%u cfg_b8=%u first32=%32ph\n", + why, magic, ver, ftl_m, ftl_n, vfl_m, vfl_n, fpt_m, fpt_n, + geom, w->geom.dev_id, vfl_arg, fpt_a, extra, cfg, s); +} + +/* OSOS sub_1122FC checks — not the old ver>=1 / major<=16 heuristic. */ +static int whimory_validate_signature(struct whimory *w, const u8 *sig) +{ + u32 magic = whimory_sig32(sig, 0x00); + u32 ver = whimory_sig32(sig, 0x08); + u32 geom = whimory_sig32(sig, 0x34); + + whimory_log_sig_fields(w, sig, "validate"); + if (magic != WHIMORY_SIG_MAGIC) { + dev_info(w->dev, + "FPART_SIG_READ reject: magic=%08x want=776d7278\n", + magic); + return -EINVAL; + } + if (ver > 6) { + dev_info(w->dev, + "FPART_SIG_READ reject: version=%u > 6\n", ver); + return -EINVAL; + } + if (geom != w->geom.dev_id) { + dev_info(w->dev, + "FPART_SIG_READ reject: geom=%u != FIL GetInfo(101)=%u\n", + geom, w->geom.dev_id); + return -EINVAL; + } + return 0; +} + +static int whimory_parse_signature(struct whimory *w, const u8 *s) +{ + int ret; + + ret = whimory_validate_signature(w, s); + if (ret) + return ret; + memcpy(w->sig.raw, s, WHIMORY_SIG_SIZE); + w->sig.version = whimory_sig32(s, 0x08); + w->sig.ftl_major = whimory_sig32(s, 0x0c); + w->sig.ftl_minor = whimory_sig32(s, 0x10); + w->sig.vfl_major = whimory_sig32(s, 0x18); + w->sig.vfl_minor = whimory_sig32(s, 0x1c); + w->sig.fpart_major = whimory_sig32(s, 0x24); + w->sig.fpart_minor = whimory_sig32(s, 0x28); + w->sig.sig_geom = whimory_sig32(s, 0x34); + w->sig.flags_or_open = whimory_sig32(s, 0x20); + w->sig.fpart_arg = whimory_sig32(s, 0x2c); + w->sig.extra_arg = whimory_sig32(s, 0x30); + w->sig_ok = true; + dev_info(w->dev, + "Whimory sig OK ver=%u fpart=%u.%u vfl=%u.%u ftl=%u.%u geom=%u vfl_arg=%u fpart_arg=%u extra=%u\n", + w->sig.version, w->sig.fpart_major, w->sig.fpart_minor, + w->sig.vfl_major, w->sig.vfl_minor, + w->sig.ftl_major, w->sig.ftl_minor, w->sig.sig_geom, + w->sig.flags_or_open, w->sig.fpart_arg, w->sig.extra_arg); + return 0; +} + +static int n31_fpart_init(struct whimory *w) +{ + if (!w->fil_ok) + return -ENODEV; + memset(w->fpart_ctx.table, 0xff, sizeof(w->fpart_ctx.table)); + w->fpart_ctx.count = 0; + w->fpart_ctx.scanned = false; + return 0; +} + +static u32 n31_fpart_minor(struct whimory *w) +{ + return w->sig.fpart_minor; +} + +static u16 fpart_num_banks(const struct whimory *w) +{ + return w->geom.num_ce * w->geom.num_cau; +} + +static void fpart_bank_to_ce_cau(const struct whimory *w, u16 bank, + unsigned int *ce, unsigned int *cau) +{ + u16 ncau = w->geom.num_cau ? w->geom.num_cau : 1; + + *ce = bank / ncau; + *cau = bank % ncau; +} + +static bool fpart_type_class1(u16 type_word) +{ + return ((type_word >> 8) & FPART_SPECIAL_CLASS_MASK) == + FPART_SPECIAL_CLASS; +} + +static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out); +static bool fpart_has_xrmw(const u8 *page); + +/* + * sub_3E5650 op=1 analogue. Special objects often live on SLC; try SLC + * then MLC. Full 16 KiB data + 64B META; special uses first 16 META bytes. + */ +static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, + u32 page, void *data, u8 *meta) +{ + unsigned int ce, cau, i; + int last = -EIO; + const unsigned int slc_order[2] = { 1, 0 }; + + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + if (ce >= w->geom.num_ce || cau >= w->geom.num_cau || + block >= w->geom.blocks_per_cau || + page >= w->geom.pages_per_block) + return -EINVAL; + + for (i = 0; i < 2; i++) { + int ret; + + ret = s5l8740_fmss_page_read(ce, cau, block, page, slc_order[i], + 16, data, w->geom.page_size, + meta, S5L8740_FMSS_META_SIZE); + if (ret) + continue; + last = 0; + if (fpart_meta_special(meta, 0, NULL) || fpart_has_xrmw(data)) + return 0; + } + return last; +} + +/* sub_4EB0CC — 16-byte META copy. LE type_word at +2 (RE). */ +static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out) +{ + unsigned int slot; + + if (!meta) + return false; + for (slot = 0; slot < 4; slot++) { + const u8 *m = meta + slot * WHIMORY_META_SIZE; + + if (m[0] != FPART_SPECIAL_TAG) + continue; + if (m[1] != want_chunk) + continue; + if (type_out) + *type_out = get_unaligned_le16(m + 2); + return true; + } + return false; +} + +static int fpart_meta_special_slot(const u8 *meta, u8 want_chunk, u16 *type_out) +{ + unsigned int slot; + + if (!meta) + return -1; + for (slot = 0; slot < 4; slot++) { + const u8 *m = meta + slot * WHIMORY_META_SIZE; + + if (m[0] != FPART_SPECIAL_TAG) + continue; + if (m[1] != want_chunk) + continue; + if (type_out) + *type_out = get_unaligned_le16(m + 2); + return (int)slot; + } + return -1; +} + +static bool fpart_meta_interesting(const u8 *m) +{ + return m && (m[0] == FPART_SPECIAL_TAG || + m[0] == WHIMORY_META_TYPE_VFL_CXT || + m[0] == WHIMORY_META_TYPE_SFTL_CXT || + m[0] == WHIMORY_META_TYPE_BTOC); +} + +static u32 fpart_word_at(const u8 *page, unsigned int off) +{ + return get_unaligned_le32(page + off); +} + +static bool fpart_has_xrmw(const u8 *page) +{ + u32 a = fpart_word_at(page, 0); + u32 b = fpart_word_at(page, FPART_SPECIAL_HDR); + + return a == WHIMORY_SIG_MAGIC || b == WHIMORY_SIG_MAGIC; +} + +static bool fpart_has_wrmx(const u8 *page) +{ + u32 a = fpart_word_at(page, 0); + u32 b = fpart_word_at(page, FPART_SPECIAL_HDR); + + return a == WHIMORY_SIG_MAGIC_WRMX || b == WHIMORY_SIG_MAGIC_WRMX; +} + +/* + * Cache insert: sorted by type_word, then bank, then block (OSOS + * fpart_special_cache_add_pairs). Table size 0x2d0 / 6 = 120. + */ +static int fpart_cache_add(struct whimory *w, u16 bank, u16 block, + u16 type_word) +{ + struct fpart_special_entry *t = w->fpart_ctx.table; + u16 n = w->fpart_ctx.count; + u16 i, j; + + if (n >= FPART_SPECIAL_MAX_ENTRIES) + return -ENOSPC; + + for (i = 0; i < n; i++) { + if (t[i].type_word == type_word && t[i].bank == bank && + t[i].block == block) + return 0; + if (t[i].type_word > type_word) + break; + if (t[i].type_word == type_word && t[i].bank > bank) + break; + if (t[i].type_word == type_word && t[i].bank == bank && + t[i].block > block) + break; + } + for (j = n; j > i; j--) + t[j] = t[j - 1]; + t[i].bank = bank; + t[i].block = block; + t[i].type_word = type_word; + w->fpart_ctx.count = n + 1; + return 1; +} + +/* Assignment page DATA: up to eight u16 bank, u16 block; bank==0xffff ends. */ +static bool fpart_data_is_assignment(const u8 *data, u16 nbanks, u32 nblk) +{ + unsigned int i, valid = 0; + + for (i = 0; i < FPART_ASSIGN_MAX_PAIRS; i++) { + u16 bank = get_unaligned_le16(data + i * 4); + u16 block = get_unaligned_le16(data + i * 4 + 2); + + if (bank == 0xffff) + return i == 0 || valid > 0; + if (bank >= nbanks || block >= nblk) + return false; + valid++; + } + return valid > 0; +} + +static unsigned int fpart_ingest_pairs(struct whimory *w, const u8 *data, + u16 type_word) +{ + unsigned int i, added = 0; + u16 nbanks = fpart_num_banks(w); + + for (i = 0; i < FPART_ASSIGN_MAX_PAIRS; i++) { + u16 bank = get_unaligned_le16(data + i * 4); + u16 block = get_unaligned_le16(data + i * 4 + 2); + + if (bank == 0xffff) + break; + if (bank >= nbanks || block >= w->geom.blocks_per_cau) { + dev_info(w->dev, + "FPART_ASSIGN skip pair bank=%u block=%u (banks=%u blocks=%u)\n", + bank, block, nbanks, w->geom.blocks_per_cau); + continue; + } + if (fpart_cache_add(w, bank, block, type_word) > 0) { + dev_info(w->dev, + "FPART_ASSIGN_ADD bank=%u block=%u type=0x%04x\n", + bank, block, type_word); + added++; + } + } + return added; +} + +static bool fpart_find_in_cache(struct whimory *w, u16 *index, u16 type) +{ + u8 low = type & 0xff; + u16 i; + + for (i = 0; i < w->fpart_ctx.count; i++) { + if ((w->fpart_ctx.table[i].type_word & 0xff) == low) { + *index = i; + return true; + } + } + return false; +} + +static u16 fpart_count_special_copies(struct whimory *w, u16 type_word) +{ + u8 low = type_word & 0xff; + u16 n = 0, i; + + for (i = 0; i < w->fpart_ctx.count; i++) { + if ((w->fpart_ctx.table[i].type_word & 0xff) == low) + n++; + } + return n; +} + +static int fpart_scan_region(struct whimory *w, u16 type, + u32 block_lo, u32 block_hi, + unsigned int page_lo, unsigned int page_hi, + bool *matched) +{ + u8 *page; + u8 meta[S5L8740_FMSS_META_SIZE]; + u16 bank, nbanks = fpart_num_banks(w); + u32 b, p; + int ret, reads = 0, tag30 = 0, xrmw = 0, wrmx = 0, fail = 0; + unsigned int sample = 0; + u32 hist[256]; + + page = kvmalloc(w->geom.page_size, GFP_KERNEL); + if (!page) + return -ENOMEM; + + memset(hist, 0, sizeof(hist)); + + if (page_hi >= w->geom.pages_per_block) + page_hi = w->geom.pages_per_block - 1; + + s5l8740_fmss_nand_reset(); + + for (bank = 0; bank < nbanks; bank++) { + for (b = block_hi; b > block_lo; b--) { + u32 blk = b - 1; + + for (p = page_lo; p <= page_hi; p++) { + u16 type_word = 0; + unsigned int ce, cau, pairs; + u32 obj_len; + bool special, magic; + + cond_resched(); + ret = fpart_fil_read_page(w, bank, blk, p, + page, meta); + reads++; + if (ret) { + fail++; + continue; + } + hist[meta[0]]++; + if (sample < 12) { + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + dev_info(w->dev, + "FPART_META_SAMPLE n=%u bank=%u ce=%u cau=%u blk=%u pg=%u meta=%16ph data00=%32ph data80=%32ph\n", + sample, bank, ce, cau, blk, p, + meta, page, + page + FPART_SPECIAL_HDR); + sample++; + } + { + unsigned int s, interesting = 0; + + for (s = 0; s < 4; s++) + if (fpart_meta_interesting(meta + + s * WHIMORY_META_SIZE)) + interesting++; + if (interesting && w->fpart_ctx.slot_logs < 48) { + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + for (s = 0; s < 4; s++) { + const u8 *m = meta + s * WHIMORY_META_SIZE; + const u8 *d = page + s * WHIMORY_LBA_SIZE; + + if (!fpart_meta_interesting(m) && + s != 0) + continue; + dev_info(w->dev, + "FPART_SLOTS n=%u bank=%u ce=%u cau=%u blk=%u pg=%u slot=%u type=%02x chunk=%02x tw=0x%04x meta=%16ph data=%32ph\n", + w->fpart_ctx.slot_logs, + bank, ce, cau, blk, p, s, + m[0], m[1], + get_unaligned_le16(m + 2), + m, d); + } + w->fpart_ctx.slot_logs++; + } + } + special = fpart_meta_special(meta, 0, &type_word); + magic = fpart_has_xrmw(page); + if (fpart_has_wrmx(page)) + wrmx++; + if (!special && !magic) + continue; + if (special) + tag30++; + if (magic) { + xrmw++; + if (!special) + type_word = WHIMORY_SIG_TYPE; + } + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + dev_info(w->dev, + "FPART_ASSIGN_SCAN bank=%u ce=%u cau=%u block=%u page=%u slot=%d type_word=0x%04x blank=%d m0=%16ph m1=%16ph m2=%16ph m3=%16ph data00=%32ph data80=%32ph\n", + bank, ce, cau, blk, p, + fpart_meta_special_slot(meta, 0, NULL), + type_word, + whimory_page_blank(page, 256), + meta, meta + 16, meta + 32, meta + 48, + page, page + FPART_SPECIAL_HDR); + if (whimory_page_blank(page, 256)) { + dev_info(w->dev, + "FPART_ASSIGN skip blank data type_word=0x%04x\n", + type_word); + continue; + } + if (fpart_data_is_assignment(page, nbanks, + w->geom.blocks_per_cau)) { + pairs = fpart_ingest_pairs(w, page, + type_word); + dev_info(w->dev, + "FPART_ASSIGN_PAGE type_word=0x%04x pairs=%u count=%u\n", + type_word, pairs, + w->fpart_ctx.count); + } else { + obj_len = get_unaligned_le32(page + + FPART_SPECIAL_LEN_OFF); + if (magic || + (obj_len && obj_len != 0xffffffffu && + obj_len < 0x100000u)) { + if (fpart_cache_add(w, bank, + blk, + type_word) > 0) + dev_info(w->dev, + "FPART_ASSIGN_ADD bank=%u block=%u type=0x%04x (object chunk0)\n", + bank, blk, + type_word); + } + } + if ((type_word & 0xff) == (type & 0xff)) + *matched = true; + } + } + } + dev_info(w->dev, + "FPART_SCAN blk[%u,%u) pages[%u,%u] reads=%d fail=%d tag30=%d xrmw=%d wrmx=%d entries=%u matched=%d meta0_top=%02x/%u %02x/%u %02x/%u %02x/%u\n", + block_lo, block_hi, page_lo, page_hi, reads, fail, tag30, xrmw, + wrmx, w->fpart_ctx.count, *matched, + 0xff, hist[0xff], 0x00, hist[0], 0x30, hist[0x30], 0x20, + hist[0x20]); + kvfree(page); + return 0; +} + +/* + * fpart_locate_special_4EBBDC: cache by low byte, else scan tail assignment + * pages (META 0x30 chunk 0). scanned=true after a full miss so we do not + * rescan. sub_3E5650 op=4 bitmap is not ported — every tail block is read. + */ +static bool fpart_locate_special(struct whimory *w, u16 *index, u16 type) +{ + u32 tail, start, nblk = w->geom.blocks_per_cau; + unsigned int npg = fpart_assign_pages ? fpart_assign_pages : 1; + bool matched = false; + + if (fpart_find_in_cache(w, index, type)) + return true; + if (w->fpart_ctx.scanned) + return false; + + tail = sig_scan_blocks ? sig_scan_blocks : w->geom.vfl_tail; + if (!tail) + tail = 128; + if (tail > nblk) + tail = nblk; + start = nblk - tail; + + dev_info(w->dev, + "FPART_LOCATE type=0x%04x tail=%u start=%u pages=0..%u banks=%u\n", + type, tail, start, npg - 1, fpart_num_banks(w)); + + if (fpart_scan_region(w, type, start, nblk, 0, npg - 1, &matched)) + return false; + + if (!matched && sig_brute_scan && start) { + dev_info(w->dev, + "FPART_LOCATE brute remaining blk[0,%u) (debug)\n", + start); + fpart_scan_region(w, type, 0, start, 0, 0, &matched); + } + + w->fpart_ctx.scanned = true; + if (!fpart_find_in_cache(w, index, type)) { + dev_info(w->dev, + "FPART_LOCATE type=0x%04x miss entries=%u\n", + type, w->fpart_ctx.count); + return false; + } + return true; +} + +/* + * fpart_read_special_copy_4F1420. + * Chunk 0: META 30 type_word; object_len @+0x24, gen @+0x28, + * payload @+0x80. Later chunks: payload @+0, dst off = page_size*chunk-128. + */ +static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, + u16 entry_i, u32 *gen_out) +{ + struct fpart_special_entry *e; + u8 *page; + u8 meta[S5L8740_FMSS_META_SIZE]; + u32 page_size, chunk_count = 1, copy_slots, chunk, slot; + u32 object_len = 0, copy_len = 0, generation = 0; + int ret = -ENOENT; + + if (entry_i >= w->fpart_ctx.count) + return -EINVAL; + e = &w->fpart_ctx.table[entry_i]; + page_size = w->geom.page_size; + copy_slots = w->geom.pages_per_block; + if (!page_size || !copy_slots) + return -EINVAL; + + page = kvmalloc(page_size, GFP_KERNEL); + if (!page) + return -ENOMEM; + + for (chunk = 0; chunk < chunk_count; chunk++) { + bool got = false; + + for (slot = 0; slot < copy_slots; slot++) { + u32 pg = chunk + slot * chunk_count; + u16 meta_type = 0; + + if (pg >= w->geom.pages_per_block) + break; + cond_resched(); + ret = fpart_fil_read_page(w, e->bank, e->block, pg, + page, meta); + if (ret) + continue; + if (!fpart_meta_special(meta, chunk, &meta_type)) + continue; + if (meta_type != e->type_word) + continue; + got = true; + + if (chunk == 0) { + object_len = get_unaligned_le32(page + + FPART_SPECIAL_LEN_OFF); + generation = get_unaligned_le32(page + + FPART_SPECIAL_GEN_OFF); + if (!object_len || object_len == 0xffffffffu) { + got = false; + continue; + } + copy_len = min(object_len, dst_len ? dst_len : + object_len); + chunk_count = DIV_ROUND_UP(copy_len + + FPART_SPECIAL_HDR, + page_size); + if (!chunk_count) + chunk_count = 1; + copy_slots = w->geom.pages_per_block / + chunk_count; + if (!copy_slots) + copy_slots = 1; + dev_info(w->dev, + "FPART_SPECIAL_COPY entry=%u bank=%u block=%u type_word=0x%04x chunk0 page=%u meta=%16ph object_len=0x%x gen=%u raw00=%32ph raw80=%32ph\n", + entry_i, e->bank, e->block, + e->type_word, pg, meta, object_len, + generation, page, + page + FPART_SPECIAL_HDR); + if (dst && dst_len) { + u32 n = min(page_size - FPART_SPECIAL_HDR, + copy_len); + + n = min(n, dst_len); + memcpy(dst, page + FPART_SPECIAL_HDR, n); + } + } else if (dst && dst_len) { + u32 dst_off = page_size * chunk - FPART_SPECIAL_HDR; + u32 n; + + if (dst_off >= dst_len || dst_off >= copy_len) + break; + n = min(page_size, copy_len - dst_off); + n = min(n, dst_len - dst_off); + memcpy(dst + dst_off, page, n); + } + break; + } + if (!got) { + ret = -ENOENT; + goto out; + } + } + if (gen_out) + *gen_out = generation; + if (dst && dst_len) + dev_info(w->dev, + "FPART_SPECIAL_PAYLOAD entry=%u first32=%32ph\n", + entry_i, dst); + ret = 0; +out: + kvfree(page); + return ret; +} + +/* Newest generation among contiguous low-byte copies (4F12DC / 4EB428). */ +static int fpart_read_special_by_index(struct whimory *w, u8 *dst, u32 len, + u16 index) +{ + u16 type_word, copies, i, best_i = 0; + bool have = false; + u32 best_gen = 0; + + if (index >= w->fpart_ctx.count) + return -EINVAL; + type_word = w->fpart_ctx.table[index].type_word; + copies = fpart_count_special_copies(w, type_word); + dev_info(w->dev, + "FPART_READ_SPECIAL type=0x%04x index=%u copies=%u class=%u\n", + type_word, index, copies, (type_word >> 8) & + FPART_SPECIAL_CLASS_MASK); + + for (i = 0; i < copies; i++) { + u16 entry_i = index + i; + u32 gen = 0; + int ok; + + if (entry_i >= w->fpart_ctx.count) + break; + if ((w->fpart_ctx.table[entry_i].type_word & 0xff) != + (type_word & 0xff)) + break; + dev_info(w->dev, + "FPART_READ_SPECIAL copy=%u bank=%u block=%u type_word=0x%04x\n", + i, w->fpart_ctx.table[entry_i].bank, + w->fpart_ctx.table[entry_i].block, + w->fpart_ctx.table[entry_i].type_word); + ok = fpart_read_special_copy(w, have ? NULL : dst, + have ? 0 : len, entry_i, &gen); + if (ok) + continue; + if (!have) { + have = true; + best_gen = gen; + best_i = entry_i; + continue; + } + if (gen > best_gen) { + if (!fpart_read_special_copy(w, dst, len, entry_i, + &gen)) { + best_gen = gen; + best_i = entry_i; + } else if (fpart_read_special_copy(w, dst, len, + best_i, NULL)) { + return -EIO; + } + } + } + if (!have) + return -ENOENT; + dev_info(w->dev, + "FPART_READ_SPECIAL selected entry=%u gen=%u\n", + best_i, best_gen); + return 0; +} + +/* + * fpart_read_special_common_4EEB68 / vtable +80. + * Do NOT expect xrmw at raw page offset 0 — payload is after the 0x80 header. + */ +static int whimory_fpart_read_special(struct whimory *w, u32 type, u8 *buf, + size_t len) +{ + u16 index = (u16)type; + u16 type_word; + + if (!buf || !len || type > 0xffff) + return -EINVAL; + + memset(buf, 0xa5, len); + if (!fpart_locate_special(w, &index, (u16)type)) + return -ENOENT; + + type_word = w->fpart_ctx.table[index].type_word; + dev_info(w->dev, + "FPART_LOCATE type=0x%04x index=%u entry bank=%u block=%u type_word=0x%04x class=%u\n", + type, index, w->fpart_ctx.table[index].bank, + w->fpart_ctx.table[index].block, type_word, + (type_word >> 8) & FPART_SPECIAL_CLASS_MASK); + if (!fpart_type_class1(type_word)) + return -EINVAL; + + return fpart_read_special_by_index(w, buf, len, index); +} + +static int n31_fpart_read_signature(struct whimory *w, u8 *buf, size_t len) +{ + return whimory_fpart_read_special(w, WHIMORY_SIG_TYPE, buf, len); +} + +static const struct whimory_fpart_ops n31_ppn_fpart_ops = { + .major = 0, + .minor = n31_fpart_minor, + .init = n31_fpart_init, + .read_special = whimory_fpart_read_special, + .read_signature = n31_fpart_read_signature, +}; + +static void whimory_dump256(struct whimory *w, const char *tag, const u8 *p) +{ + unsigned int i; + + for (i = 0; i < 256; i += 32) + dev_info(w->dev, "%s +0x%02x: %32ph\n", tag, i, p + i); +} + +static int whimory_payload_read_page(struct whimory *w, u16 bank, u32 block, + u32 page, void *data, unsigned int *slc_out) +{ + unsigned int ce, cau, i; + const unsigned int slc_order[2] = { 1, 0 }; + int last = -EIO; + + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + if (ce >= w->geom.num_ce || cau >= w->geom.num_cau || + block >= w->geom.blocks_per_cau || + page >= w->geom.pages_per_block) + return -EINVAL; + + for (i = 0; i < 2; i++) { + int ret; + + ret = s5l8740_fmss_page_read(ce, cau, block, page, slc_order[i], + 16, data, w->geom.page_size, + NULL, 0); + if (ret) { + last = ret; + continue; + } + if (slc_out) + *slc_out = slc_order[i]; + /* One successful FIL read is enough — do not MLC-retry blanks. */ + return 0; + } + return last; +} + +static int whimory_payload_check_hit(struct whimory *w, u16 bank, u32 blk, + u32 pg, unsigned int slc, const u8 *page) +{ + unsigned int ce, cau, i; + const unsigned int offs[2] = { 0, FPART_SPECIAL_HDR }; + + fpart_bank_to_ce_cau(w, bank, &ce, &cau); + for (i = 0; i < 2; i++) { + unsigned int off = offs[i]; + u32 mag; + const char *kind; + + if (off + 4 > w->geom.page_size) + continue; + mag = get_unaligned_le32(page + off); + if (mag == WHIMORY_SIG_MAGIC) + kind = "xrmw"; + else if (mag == WHIMORY_SIG_MAGIC_WRMX) + kind = "wrmx"; + else + continue; + dev_info(w->dev, + "PAYLOAD_MAGIC hit kind=%s off=0x%x bank=%u ce=%u cau=%u blk=%u pg=%u slc=%u\n", + kind, off, bank, ce, cau, blk, pg, slc); + whimory_dump256(w, "PAYLOAD_MAGIC data00", page); + whimory_dump256(w, "PAYLOAD_MAGIC data80", page + FPART_SPECIAL_HDR); + if (mag == WHIMORY_SIG_MAGIC && + off + WHIMORY_SIG_SIZE <= w->geom.page_size && + !whimory_parse_signature(w, page + off)) + dev_info(w->dev, "PAYLOAD_MAGIC parsed xrmw as signature\n"); + return 1; + } + return 0; +} + +static int whimory_payload_scan_range(struct whimory *w, void *page, + u32 block_lo, u32 block_hi, + unsigned int page_lo, unsigned int page_hi, + const char *why, int *reads) +{ + u16 bank, nbanks = fpart_num_banks(w); + u32 b, p; + + if (page_hi >= w->geom.pages_per_block) + page_hi = w->geom.pages_per_block - 1; + if (block_hi > w->geom.blocks_per_cau) + block_hi = w->geom.blocks_per_cau; + if (block_lo >= block_hi) + return 0; + + dev_info(w->dev, + "PAYLOAD_SCAN %s blk[%u,%u) pages[%u,%u] banks=%u\n", + why, block_lo, block_hi, page_lo, page_hi, nbanks); + + for (bank = 0; bank < nbanks; bank++) { + for (b = block_hi; b > block_lo; b--) { + u32 blk = b - 1; + + for (p = page_lo; p <= page_hi; p++) { + unsigned int slc = 0; + int ret; + + cond_resched(); + ret = whimory_payload_read_page(w, bank, blk, p, + page, &slc); + (*reads)++; + if (!(*reads % 512)) + dev_info(w->dev, + "PAYLOAD_SCAN %s progress reads=%d blk=%u pg=%u\n", + why, *reads, blk, p); + if (ret) + continue; + if (whimory_payload_check_hit(w, bank, blk, p, + slc, page)) + return 1; + } + } + } + return 0; +} + +/* + * Data-only xrmw/wrmx hunt. PIO last_spare is not Sogeti META. Abort on + * first hit. No classify / L2V. + */ +static int whimory_payload_magic_scan(struct whimory *w) +{ + void *page; + int reads = 0, hit; + u32 nblk = w->geom.blocks_per_cau; + u32 user = w->geom.user_blocks; + u32 around = 1461; + + if (!user || user > nblk) + user = nblk > 128 ? nblk - 128 : nblk; + if (around >= nblk) + around = nblk / 2; + + page = kvmalloc(w->geom.page_size, GFP_KERNEL); + if (!page) + return -ENOMEM; + + s5l8740_fmss_nand_reset(); + hit = whimory_payload_scan_range(w, page, user, nblk, 0, + w->geom.pages_per_block - 1, + "tail", &reads); + if (!hit) + hit = whimory_payload_scan_range(w, page, 0, user, 0, 0, + "user-pg0", &reads); + if (!hit && around + 1 < nblk) + hit = whimory_payload_scan_range(w, page, around, around + 1, 0, + w->geom.pages_per_block - 1, + "blk1461", &reads); + dev_info(w->dev, "PAYLOAD_SCAN done hit=%d reads=%d sig=%d\n", + hit, reads, w->sig_ok); + kvfree(page); + return hit; +} + +static int whimory_read_signature(struct whimory *w) +{ + int ret; + + w->fpart = &n31_ppn_fpart_ops; + ret = w->fpart->init(w); + if (ret) + return ret; + if (payload_magic_scan) { + whimory_payload_magic_scan(w); + if (w->sig_ok) + return 0; + dev_warn(w->dev, + "PAYLOAD_SCAN: no usable xrmw. PIO META locate skipped; sigless classify off.\n"); + if (!allow_sigless_debug) { + dev_err(w->dev, + "sig=0 true_meta=unproven: stopping (no VFL/FTL/L2V)\n"); + return -ENOENT; + } + } + ret = w->fpart->read_signature(w, w->sig.raw, WHIMORY_SIG_SIZE); + if (ret) { + dev_warn(w->dev, + "FPart special 0xC101 miss (%d). sig=0 is not a native open.\n", + ret); + if (!allow_sigless_debug) { + dev_err(w->dev, + "allow_sigless_debug=0: refusing VFL/FTL without signature\n"); + return ret; + } + dev_warn(w->dev, + "allow_sigless_debug=1: classify/recover anyway (research only)\n"); + return 0; + } + return whimory_parse_signature(w, w->sig.raw); +} + +static u32 n31_vfl_minor(struct whimory *w) +{ + return w->sig.vfl_minor; +} + +static u32 n31_sftl_minor(struct whimory *w) +{ + return w->sig.ftl_minor; +} + +/* ------------------------------------------------------------------ */ +/* VFL */ +/* ------------------------------------------------------------------ */ + +static int n31_vfl_init(struct whimory *w) +{ + unsigned int cau, i, n, cxt_len; + + n = w->geom.blocks_per_cau; + if (!n) + return -EINVAL; + cxt_len = 16; + w->vfl.cxt_u16_len = cxt_len; + w->vfl.bank_stride = 1; + w->vfl.bank_mask = kvmalloc(n, GFP_KERNEL); + if (!w->vfl.bank_mask) + return -ENOMEM; + memset(w->vfl.bank_mask, (1u << min_t(u32, w->geom.num_cau, 8)) - 1, n); + w->vfl.cached_vbn = 0xffff; + w->vfl.cached_n = 0; + for (cau = 0; cau < w->geom.num_cau; cau++) { + w->vfl.remap[cau] = kvmalloc_array(n, sizeof(u32), GFP_KERNEL); + w->vfl.cxt_u16[cau] = kvmalloc_array(cxt_len, sizeof(u16), + GFP_KERNEL); + if (!w->vfl.remap[cau] || !w->vfl.cxt_u16[cau]) + return -ENOMEM; + for (i = 0; i < n; i++) + w->vfl.remap[cau][i] = i; + for (i = 0; i < cxt_len; i++) + w->vfl.cxt_u16[cau][i] = 0xffff; + w->vfl.ctx_block[cau] = ~0u; + } + return 0; +} + +static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int block, + const u8 *page, unsigned int page_len, + const u8 *meta) +{ + unsigned int i, loc = 0; + const u8 *tab; + bool magic_ok, type_ok; + + if (!page || page_len < 0x200) + return 0; + magic_ok = (page[0] == 'w' && page[1] == 'r' && page[2] == 'm' && + page[3] == 'x') || + (page[0] == 'x' && page[1] == 'r' && page[2] == 'm' && + page[3] == 'w') || + (page[FPART_SPECIAL_HDR] == 'w' && + page[FPART_SPECIAL_HDR + 1] == 'r' && + page[FPART_SPECIAL_HDR + 2] == 'm' && + page[FPART_SPECIAL_HDR + 3] == 'x') || + (page[FPART_SPECIAL_HDR] == 'x' && + page[FPART_SPECIAL_HDR + 1] == 'r' && + page[FPART_SPECIAL_HDR + 2] == 'm' && + page[FPART_SPECIAL_HDR + 3] == 'w'); + type_ok = meta && meta[0] == WHIMORY_META_TYPE_VFL_CXT; + if (!magic_ok && !type_ok) + return 0; + if (cau >= w->geom.num_cau || !w->vfl.remap[cau]) + return 0; + w->vfl.ctx_ce[cau] = ce; + w->vfl.ctx_block[cau] = block; + + /* + * sub_4EB7E4: memcpy(cxt_copies, data+0x100, 4 * num_copies). + * Each record is {le16 phys_block, u8 bank, u8 flags} — VFL CXT + * copy locations in the tail, not a user virt→phys table. + * Live glass: first u32 is often 0x827 (block 2087). + */ + tab = page + 0x100; + for (i = 0; i < 64 && 0x100 + 4 * (i + 1) <= 0x200; i++) { + u16 blk = get_unaligned_le16(tab + i * 4); + u8 bank = tab[i * 4 + 2]; + + if (!blk) + break; + if (blk < w->geom.blocks_per_cau && bank < w->geom.num_cau) + loc++; + } + w->vfl.cxt_loc_count += loc; + + /* sub_4EB098: per-bank u16 CXT copy journal at +0x200 + 32*bank */ + if (page_len >= WHIMORY_VFL_CXT_HDR + + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau + 2) { + unsigned int b, j, n16 = w->vfl.cxt_u16_len; + + for (b = 0; b < w->geom.num_cau; b++) { + if (!w->vfl.cxt_u16[b]) + continue; + for (j = 0; j < n16; j++) { + const u8 *src = page + WHIMORY_VFL_CXT_HDR + + WHIMORY_VFL_SPARE_STRIDE * b + + 2 * j; + u16 v; + + if (src + 2 > page + page_len) + break; + v = get_unaligned_le16(src); + w->vfl.cxt_u16[b][j] = v; + if (v != 0xffff && v != WHIMORY_VFL_SPARE_FREE) + w->vfl.spare_applied++; + } + } + } + + /* + * sub_3D1438 bitmap: one byte per VBN (stride 0x8D0D0F0 = 1 on N31), + * bit = bank. Not in the 0x200 header / spare journal. Try the + * remainder of this CXT page; reject if any byte has bits outside + * num_cau (would be unrelated payload). + */ + { + unsigned int off = WHIMORY_VFL_CXT_HDR + + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau; + u8 def = (1u << min_t(u32, w->geom.num_cau, 8)) - 1; + unsigned int i, nblk = w->geom.blocks_per_cau; + + if (w->vfl.bank_mask && page_len >= off + nblk && nblk) { + const u8 *src = page + off; + bool ok = true; + + for (i = 0; i < nblk; i++) { + if (src[i] & ~def) { + ok = false; + break; + } + } + if (ok) { + memcpy(w->vfl.bank_mask, src, nblk); + w->vfl.bitmap_loaded = 1; + w->vfl.cached_vbn = 0xffff; + } + } + } + + /* + * User VBN→PBN is identity over blocks_per_cau (sub_4EAE40: + * vbn < mcxt.dev.blocks_per_cau). Failed-block replacement lives + * in the u16 tables, not in a 256-entry slice of +0x100. + */ + w->vfl.remap_count = w->geom.blocks_per_cau; + dev_info(w->dev, + "VFL ingest ce=%u cau=%u blk=%u magic=%d type20=%d cxt_loc=%u identity=%u\n", + ce, cau, block, magic_ok, type_ok, loc, + w->geom.blocks_per_cau); + return loc || type_ok || magic_ok; +} + +static int n31_vfl_open(struct whimory *w) +{ + u8 *page; + u8 meta[S5L8740_FMSS_META_SIZE]; + unsigned int ce, cau, b, start, pg, slc; + int hits = 0; + + page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); + if (!page) + return -ENOMEM; + start = w->geom.blocks_per_cau - w->geom.vfl_tail; + s5l8740_fmss_nand_reset(); + for (ce = 0; ce < w->geom.num_ce; ce++) { + for (cau = 0; cau < w->geom.num_cau; cau++) { + for (b = start; b < w->geom.blocks_per_cau; b++) { + for (pg = 0; pg < 8; pg++) { + int got = -EIO; + + cond_resched(); + for (slc = 0; slc < 2; slc++) { + got = s5l8740_fmss_page_read(ce, + cau, b, pg, slc, 16, + page, + S5L8740_FMSS_PAGE_SIZE, + meta, sizeof(meta)); + if (!got) + break; + } + if (got) + continue; + if (n31_vfl_ingest_ctx(w, ce, cau, b, + page, + S5L8740_FMSS_PAGE_SIZE, + meta)) + hits++; + } + } + } + } + kvfree(page); + w->vfl.ctx_hits = hits; + w->vfl_ok = true; + dev_info(w->dev, + "VFL_Open OK ctx_hits=%u remap_ents=%u cxt_loc=%u bitmap=%u spare=%u\n", + hits, w->vfl.remap_count, w->vfl.cxt_loc_count, + w->vfl.bitmap_loaded, w->vfl.spare_applied); + return 0; +} + +static u32 n31_vfl_get_param(struct whimory *w, u32 selector) +{ + switch (selector) { + case WHIMORY_VFL_PARAM_NUM_SB: + return w->geom.num_ce * w->geom.num_cau * w->geom.user_blocks; + default: + return 0; + } +} + +static int n31_vfl_read_vba(struct whimory *w, u32 vba, u32 count, + void *data, struct whimory_meta *meta) +{ + u32 i, ce, cau, vblock, page, slot, pblock; + u32 last_ce = ~0u, last_cau = ~0u, last_pblock = ~0u, last_page = ~0u; + u8 *pagebuf; + u8 spare[S5L8740_FMSS_META_SIZE]; + int ret; + + if (!count || count > WHIMORY_VBAS_PER_PAGE) + return -EINVAL; + pagebuf = w->sftl.data_page; + if (!pagebuf) + return -ENOMEM; + + for (i = 0; i < count; i++) { + u8 *dst = (u8 *)data + i * WHIMORY_LBA_SIZE; + + ret = whimory_unpack_vba(w, vba + i, &ce, &cau, &vblock, + &page, &slot); + if (ret) + return ret; + if (page >= w->sftl.pages_per_sb || + slot >= w->sftl.vbas_per_page) + return -ERANGE; + cau = whimory_vfl_bank(w, cau, vblock); + pblock = whimory_vfl_phys(w, cau, vblock); + if (ce != last_ce || cau != last_cau || pblock != last_pblock || + page != last_page) { + ret = s5l8740_fmss_page_read(ce, cau, pblock, page, 0, + 16, pagebuf, + S5L8740_FMSS_PAGE_SIZE, + spare, sizeof(spare)); + if (ret) + return ret; + last_ce = ce; + last_cau = cau; + last_pblock = pblock; + last_page = page; + } + memcpy(dst, pagebuf + slot * WHIMORY_LBA_SIZE, WHIMORY_LBA_SIZE); + if (meta && i == 0) { + memset(meta, 0xff, sizeof(*meta)); + if (sizeof(spare) >= (slot + 1) * WHIMORY_META_SIZE) + memcpy(meta, spare + slot * WHIMORY_META_SIZE, + sizeof(*meta)); + } + } + return 0; +} + +static const struct whimory_vfl_ops n31_vfl_ops = { + .major = 0, + .minor = n31_vfl_minor, + .init = n31_vfl_init, + .open = n31_vfl_open, + .get_param = n31_vfl_get_param, + .read_vba = n31_vfl_read_vba, +}; + +static int whimory_vfl_open(struct whimory *w) +{ + int ret; + + ret = w->vfl_ops->init(w); + if (ret) { + dev_err(w->dev, "VFL_Init failed: %d\n", ret); + return ret; + } + ret = w->vfl_ops->open(w); + if (ret) { + dev_err(w->dev, "VFL_Open failed: %d\n", ret); + return ret; + } + return 0; +} + +/* ------------------------------------------------------------------ */ +/* SFTL recovery — classify SBs, replay BTOC/META by weave */ +/* ------------------------------------------------------------------ */ + +/* sub_50CFA0: FFFF0001 payload is {count, [lba,span]...} → unmap. */ +static int whimory_sftl_apply_list(struct whimory *w, u32 vba) +{ + u8 *buf; + u32 count, i, lba, span; + struct whimory_meta meta; + int ret; + + buf = w->sftl.gc_data; + if (!buf) + buf = w->sftl.data_page; + if (!buf || !w->vfl_ops || !w->vfl_ops->read_vba) + return -ENOMEM; + ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); + if (ret) + return ret; + count = get_unaligned_le32(buf); + if (!count || count > (WHIMORY_LBA_SIZE - 4) / 8) + return -EINVAL; + for (i = 0; i < count; i++) { + lba = get_unaligned_le32(buf + 4 + 8 * i); + span = get_unaligned_le32(buf + 8 + 8 * i); + if (!span || whimory_special_lba(lba)) + break; + ret = whimory_l2v_update(w, lba, span, w->l2v.invalid_vba); + if (ret) + return ret; + w->sftl.token_list_applied++; + } + return 0; +} + +static bool whimory_btoc_looks_be_lpn(const u8 *page) +{ + u32 a = get_unaligned_be32(page); + u32 b = get_unaligned_be32(page + 4); + u32 c = get_unaligned_be32(page + 8); + u32 d = get_unaligned_be32(page + 12); + unsigned int i, ok = 0; + + if (a < 0x01000000u && b == a + 1 && c == b + 1 && d == c + 1) + return true; + if (a == 0 && (b == 1 || b == WHIMORY_VBAS_PER_PAGE) && + c == b + (b == 1 ? 1 : WHIMORY_VBAS_PER_PAGE)) + return true; + for (i = 0; i < 16; i++) { + u32 v = get_unaligned_be32(page + i * 4); + + if (v != 0xffffffff && v < 0x01000000u) + ok++; + } + return ok >= 12; +} + +/* Live N31 SFTL BTOC: 16-byte BE records, span in last byte (fmss glass). */ +static bool whimory_btoc_looks_be_bte(const u8 *page) +{ + u32 weave0, lba0, lba1; + u32 span0, span1; + + if (whimory_page_blank(page, 64)) + return false; + weave0 = get_unaligned_be32(page); + lba0 = get_unaligned_be32(page + 8); + span0 = page[15]; + if (weave0 != 0 || !span0 || span0 > WHIMORY_DATA_VBAS_PER_SB || + lba0 >= 0x01000000u) + return false; + lba1 = get_unaligned_be32(page + 16 + 8); + span1 = page[16 + 15]; + if (!span1 || span1 > WHIMORY_DATA_VBAS_PER_SB || lba1 >= 0x01000000u) + return false; + if (lba1 != lba0 + span0 && lba1 + span1 != lba0 && + (lba1 < lba0 || lba1 > lba0 + span0 + 8)) + return false; + return true; +} + +static bool whimory_btoc_parse_be_lpn(struct whimory *w, const u8 *page, + unsigned int len, unsigned int ce, + unsigned int cau, unsigned int vblock) +{ + unsigned int i, n, hit = 0, valid = 0; + bool page_gran; + + n = min_t(unsigned int, len / 4, WHIMORY_DATA_VBAS_PER_SB); + for (i = 0; i < n; i++) { + u32 lpn = get_unaligned_be32(page + i * 4); + + if (lpn == 0xffffffff) + break; + valid++; + } + page_gran = valid > 0 && valid <= WHIMORY_DATA_PAGES_PER_SB; + if (w->sftl.btoc_pages_valid < 5) + dev_info(w->dev, + "BTOC_BE_LPN valid=%u %s ce=%u cau=%u vblock=%u\n", + valid, + page_gran ? "page-granularity x4" : "slot-granularity", + ce, cau, vblock); + + if (page_gran) + n = min(valid, (unsigned int)WHIMORY_DATA_PAGES_PER_SB); + for (i = 0; i < n; i++) { + u32 lpn = get_unaligned_be32(page + i * 4); + u32 vba; + unsigned int slot, pg; + + w->sftl.btoc_entries_seen++; + if (lpn == 0xffffffff || lpn == WHIMORY_LBA_BLANK) + continue; + if (whimory_special_lba(lpn)) { + w->sftl.token_hole++; + continue; + } + if (lpn >= 0x01000000u) + continue; + if (page_gran) { + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + vba = whimory_pack_vba(w, ce, cau, vblock, i, + slot); + if (whimory_l2v_update(w, + lpn * WHIMORY_VBAS_PER_PAGE + + slot, 1, vba)) + return hit > 0; + w->sftl.btoc_l2v_updates++; + w->sftl.btoc_recs++; + hit++; + } + } else { + pg = i / w->sftl.vbas_per_page; + slot = i % w->sftl.vbas_per_page; + vba = whimory_pack_vba(w, ce, cau, vblock, pg, slot); + if (whimory_l2v_update(w, lpn, 1, vba)) + break; + w->sftl.btoc_l2v_updates++; + w->sftl.btoc_recs++; + hit++; + } + } + return hit > 0; +} + +static bool whimory_btoc_parse_be_bte(struct whimory *w, const u8 *page, + unsigned int len, unsigned int ce, + unsigned int cau, unsigned int vblock) +{ + unsigned int i, recs, vba_ofs = 0, hit = 0; + + recs = len / 16; + for (i = 0; i < recs; i++) { + const u8 *r = page + i * 16; + u32 lba = get_unaligned_be32(r + 8); + u32 span = r[15]; + u32 vba; + int upd; + + w->sftl.btoc_entries_seen++; + if (!span) + break; + if (whimory_special_lba(lba)) { + if (lba == WHIMORY_LBA_LIST) + w->sftl.btoc_holelist_ffff0001++; + else if (lba == WHIMORY_LBA_HOLE) + w->sftl.btoc_token_ffff0000++; + else if (lba == WHIMORY_LBA_DELETED) + w->sftl.btoc_token_ffffff00++; + else if (lba == WHIMORY_LBA_BLANK) + w->sftl.btoc_token_ffffffff++; + w->sftl.token_hole++; + if (vba_ofs + span > WHIMORY_VBAS_PER_SB) + break; + vba_ofs += span; + continue; + } + if (span > WHIMORY_DATA_VBAS_PER_SB || lba >= 0x01000000u) + break; + if (vba_ofs + span > WHIMORY_DATA_VBAS_PER_SB) + break; + vba = whimory_pack_vba(w, ce, cau, vblock, + vba_ofs / w->sftl.vbas_per_page, + vba_ofs % w->sftl.vbas_per_page); + upd = whimory_l2v_update(w, lba, span, vba); + if (upd) + break; + w->sftl.btoc_l2v_updates++; + vba_ofs += span; + hit++; + w->sftl.btoc_recs++; + } + return hit > 0; +} + +static bool whimory_btoc_parse_bte(struct whimory *w, const u8 *page, + unsigned int len, unsigned int ce, + unsigned int cau, unsigned int vblock) +{ + unsigned int i, recs, vba_ofs = 0, hit = 0; + + if (len < sizeof(struct whimory_bte) || whimory_page_blank(page, 64)) + return false; + + recs = len / sizeof(struct whimory_bte); + if (le32_to_cpu(((const struct whimory_bte *)page)->weave_seq_add)) + dev_dbg(w->dev, "BTOC weaveSeqAdd[0] != 0\n"); + + for (i = 0; i < recs; i++) { + const struct whimory_bte *bte = + (const struct whimory_bte *)(page + i * sizeof(*bte)); + u32 lba = le32_to_cpu(bte->lba); + u32 span = le32_to_cpu(bte->span); + u32 vba; + int upd; + + w->sftl.btoc_entries_seen++; + if (!span) + break; + if (whimory_special_lba(lba)) { + if (lba == WHIMORY_LBA_LIST) { + vba = whimory_pack_vba(w, ce, cau, vblock, + vba_ofs / w->sftl.vbas_per_page, + vba_ofs % w->sftl.vbas_per_page); + if (whimory_sftl_apply_list(w, vba)) + dev_warn(w->dev, + "list token vba=%u failed\n", + vba); + w->sftl.token_list++; + w->sftl.btoc_holelist_ffff0001++; + } else if (lba == WHIMORY_LBA_HOLE) { + w->sftl.btoc_token_ffff0000++; + w->sftl.token_hole++; + } else if (lba == WHIMORY_LBA_DELETED) { + w->sftl.btoc_token_ffffff00++; + w->sftl.token_hole++; + } else if (lba == WHIMORY_LBA_BLANK) { + w->sftl.btoc_token_ffffffff++; + w->sftl.token_hole++; + } else + w->sftl.token_hole++; + if (vba_ofs + span > WHIMORY_VBAS_PER_SB) + break; + vba_ofs += span; + continue; + } + if (span > WHIMORY_DATA_VBAS_PER_SB) + break; + if (lba >= 0x01000000u) + break; + if (vba_ofs + span > WHIMORY_DATA_VBAS_PER_SB) + break; + vba = whimory_pack_vba(w, ce, cau, vblock, + vba_ofs / w->sftl.vbas_per_page, + vba_ofs % w->sftl.vbas_per_page); + upd = whimory_l2v_update(w, lba, span, vba); + if (upd) + break; + w->sftl.btoc_l2v_updates++; + vba_ofs += span; + hit++; + w->sftl.btoc_recs++; + } + return hit > 0; +} + +static int whimory_ingest_btoc_page(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int vblock, + const u8 *page, unsigned int len) +{ + const char *verdict = "NONE"; + int hit = 0; + + if (whimory_page_blank(page, 64)) + return 0; + if (whimory_btoc_looks_be_bte(page)) { + if (whimory_btoc_parse_be_bte(w, page, len, ce, cau, vblock)) { + verdict = "BE_BTE"; + hit = 1; + } + } + if (!hit && whimory_btoc_looks_be_lpn(page)) { + if (whimory_btoc_parse_be_lpn(w, page, len, ce, cau, vblock)) { + verdict = "BE_LPN_ARRAY"; + hit = 1; + } + } + if (!hit && whimory_btoc_parse_bte(w, page, len, ce, cau, vblock)) { + verdict = "LE_BTE"; + hit = 1; + } + if (w->sftl.btoc_pages_read <= 8) + dev_info(w->dev, + "BTOC_VERDICT ce=%u cau=%u vblock=%u %s first32=%32ph\n", + ce, cau, vblock, verdict, page); + return hit; +} + +static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) +{ + unsigned int pg, slot, vblock; + u8 *data = w->sftl.data_page; + u8 spare[S5L8740_FMSS_META_SIZE]; + int ret, hits = 0; + + vblock = whimory_vfl_virt(w, sb->cau, sb->block); + for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; pg++) { + ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, pg, 0, + 16, data, S5L8740_FMSS_PAGE_SIZE, + spare, sizeof(spare)); + if (ret) + break; + if (whimory_page_blank(data, 64) && + whimory_page_blank(spare, 16)) + break; + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + const u8 *m = spare + slot * WHIMORY_META_SIZE; + u32 lba, vba; + + w->sftl.open_slots_seen++; + if (m[0] != WHIMORY_META_TYPE_DATA && + m[0] != WHIMORY_META_TYPE_DATA2) + continue; + if (m[1] & 0x02) + continue; + lba = get_unaligned_le32(m + 8); + w->sftl.open_slots_valid_meta++; + if (whimory_special_lba(lba) || lba >= 0x01000000u) + continue; + if (lba == 0 && w->sftl.open_l2v_updates < 8) + dev_info(w->dev, + "OPEN_META_SCAN lba=0 ce=%u cau=%u blk=%u page=%u slot=%u type=%02x flags=%02x first64=%32ph\n", + sb->ce, sb->cau, sb->block, pg, slot, + m[0], m[1], data + slot * WHIMORY_LBA_SIZE); + vba = whimory_pack_vba(w, sb->ce, sb->cau, vblock, pg, + slot); + if (whimory_l2v_update(w, lba, 1, vba)) + return -ENOMEM; + w->sftl.open_l2v_updates++; + hits++; + } + } + return hits; +} + +static int whimory_sb_cmp(const void *a, const void *b) +{ + const struct whimory_sb *sa = a, *sb = b; + + if (sa->weave < sb->weave) + return -1; + if (sa->weave > sb->weave) + return 1; + if (sa->ce != sb->ce) + return sa->ce < sb->ce ? -1 : 1; + if (sa->cau != sb->cau) + return sa->cau < sb->cau ? -1 : 1; + if (sa->block != sb->block) + return sa->block < sb->block ? -1 : 1; + return 0; +} + +/* sub_569D18 analogue: CXT SB VBAs are not L2V_Update'd (sub_5884D4). */ +static bool whimory_vba_is_cxt(struct whimory *w, u32 vba) +{ + u32 ce, cau, vblock, page, slot, phys, i; + + if (whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot)) + return false; + cau = whimory_vfl_bank(w, cau, vblock); + phys = whimory_vfl_phys(w, cau, vblock); + if (!w->sftl.sbs) + return false; + for (i = 0; i < w->sftl.num_sb; i++) { + struct whimory_sb *sb = &w->sftl.sbs[i]; + + if (sb->kind == WHIMORY_SB_CXT && sb->ce == ce && + sb->cau == cau && sb->block == phys) + return true; + } + return false; +} + +static int whimory_cxt_add_base(struct whimory *w, u32 sb, u64 weave) +{ + int i; + + if (w->n_cxt >= WHIMORY_CXT_MAX_SB) + return -ENOSPC; + for (i = w->n_cxt; i > 0; i--) { + if (w->cxt[i - 1].weave >= weave) + break; + w->cxt[i] = w->cxt[i - 1]; + } + w->cxt[i].sb = sb; + w->cxt[i].weave = weave; + w->n_cxt++; + w->sftl.cxt_bases = w->n_cxt; + return 0; +} + +static int whimory_cxt_load_contig(struct whimory *w, const u8 *data, + unsigned int len) +{ + u32 lba, span, vba, i, n; + + if (len < 16) + return 0; + n = len / 8; + lba = get_unaligned_le32(data); + span = get_unaligned_le32(data + 4); + if (span == 0xffffffff) + return 0; + if (span != WHIMORY_CXT_CONTIG_SPAN) + return -EINVAL; + if (w->cxt_lba_valid && lba != w->cxt_next_lba) { + dev_err(w->dev, + "cxt lba not consecutive want=%u got=%u\n", + w->cxt_next_lba, lba); + return -EINVAL; + } + w->cxt_lba_valid = true; + for (i = 1; i < n; i++) { + vba = get_unaligned_le32(data + 8 * i); + span = get_unaligned_le32(data + 8 * i + 4); + if (vba == 0xffffffff || !span) + break; + w->sftl.cxt_records_seen++; + if (vba < w->l2v.invalid_vba && !whimory_vba_is_cxt(w, vba)) { + if (whimory_l2v_update(w, lba, span, vba)) + return -ENOMEM; + w->sftl.cxt_l2v_updates++; + } + lba += span; + } + w->cxt_next_lba = lba; + return 0; +} + +static int whimory_cxt_handle_vba(struct whimory *w, const u8 *data, + const u8 *meta) +{ + u8 tag; + + if (meta[0] != WHIMORY_META_TYPE_SFTL_CXT) + return 0; + tag = meta[1]; + if (tag == WHIMORY_CXT_TAG_END) + return 1; + if (tag != WHIMORY_CXT_TAG_L2V) + return 0; + return whimory_cxt_load_contig(w, data, WHIMORY_LBA_SIZE); +} + +static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) +{ + struct whimory_sftl *s = &w->sftl; + u32 ce, cau, vblock, page, slot, ofs, vba, pblock; + u32 last_ce = ~0u, last_cau = ~0u, last_pblock = ~0u, last_page = ~0u; + u32 zone, n, i; + u8 *data, *gmeta; + u8 spare[S5L8740_FMSS_META_SIZE]; + int ret, done = 0; + + if (sb_idx >= s->num_sb) + return -EINVAL; + w->sftl.cxt_blocks_seen++; + zone = s->gc_zone_size; + data = s->gc_data; + gmeta = s->gc_meta; + if (!zone || !data || !gmeta || zone % s->vbas_per_page) + return -ENOMEM; + + w->cxt_lba_valid = false; + w->cxt_next_lba = 0; + /* sub_4FDBE8: VFL_Read in chunks of sftl.gc.zoneSize into ED7C/ED80. */ + for (ofs = 0; ofs < s->vbas_per_sb && !done; ofs += zone) { + n = min(zone, s->vbas_per_sb - ofs); + for (i = 0; i < n; i++) { + vba = s_g_addr_to_vba(w, sb_idx, ofs + i); + ret = whimory_unpack_vba(w, vba, &ce, &cau, &vblock, + &page, &slot); + if (ret) + return ret; + cau = whimory_vfl_bank(w, cau, vblock); + pblock = whimory_vfl_phys(w, cau, vblock); + if (ce != last_ce || cau != last_cau || + pblock != last_pblock || page != last_page) { + ret = s5l8740_fmss_page_read(ce, cau, pblock, + page, 0, 16, + s->data_page, + S5L8740_FMSS_PAGE_SIZE, + spare, + sizeof(spare)); + if (ret) + return ret; + last_ce = ce; + last_cau = cau; + last_pblock = pblock; + last_page = page; + } + memcpy(data + i * WHIMORY_LBA_SIZE, + s->data_page + slot * WHIMORY_LBA_SIZE, + WHIMORY_LBA_SIZE); + if (sizeof(spare) >= (slot + 1) * WHIMORY_META_SIZE) + memcpy(gmeta + i * WHIMORY_META_SIZE, + spare + slot * WHIMORY_META_SIZE, + WHIMORY_META_SIZE); + else + memset(gmeta + i * WHIMORY_META_SIZE, 0xff, + WHIMORY_META_SIZE); + } + for (i = 0; i < n; i++) { + ret = whimory_cxt_handle_vba(w, + data + i * WHIMORY_LBA_SIZE, + gmeta + i * WHIMORY_META_SIZE); + if (ret < 0) + return ret; + if (ret > 0) { + done = 1; + break; + } + } + } + return 0; +} + +static int whimory_cxt_load(struct whimory *w) +{ + unsigned int i; + int ret, loaded = 0; + + for (i = 0; i < w->n_cxt; i++) { + u32 sb = w->cxt[i].sb; + + dev_info(w->dev, "s_cxt_load base sb=%u weave=%llu\n", + sb, w->cxt[i].weave); + ret = whimory_cxt_load_sb(w, sb); + if (ret) { + dev_warn(w->dev, "cxt sb=%u failed %d\n", sb, ret); + continue; + } + w->cxt_base_weave = w->cxt[i].weave; + loaded = 1; + break; + } + w->sftl.cxt_loaded = loaded; + return 0; +} + +static void whimory_note_meta0(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, const u8 *data, const u8 *meta) +{ + unsigned int slot; + + if (!data || !meta) + return; + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + const u8 *m = meta + slot * WHIMORY_META_SIZE; + const u8 *d = data + slot * WHIMORY_LBA_SIZE; + u32 lba = get_unaligned_le32(m + 8); + u32 vba, vblock; + u16 bps; + + if (lba != 0) + continue; + if (m[0] != WHIMORY_META_TYPE_DATA && + m[0] != WHIMORY_META_TYPE_DATA2) + continue; + w->sftl.meta0_hits++; + if (w->sftl.meta0_hits > 24) + continue; + vblock = whimory_vfl_virt(w, cau, block); + vba = whimory_pack_vba(w, ce, cau, vblock, page, slot); + bps = get_unaligned_le16(d + 11); + dev_info(w->dev, + "META0_HIT vba=%u ce=%u cau=%u blk=%u page=%u slot=%u type=%02x first64=%32ph %32ph bps=%u\n", + vba, ce, cau, block, page, slot, m[0], d, d + 32, bps); + } +} + +static void whimory_dump_btoc_page(struct whimory *w, const struct whimory_sb *sb, + u32 vblock, const u8 *page, const u8 *meta) +{ + u32 be0 = get_unaligned_be32(page); + u32 be1 = get_unaligned_be32(page + 4); + u32 be2 = get_unaligned_be32(page + 8); + bool lpn = whimory_btoc_looks_be_lpn(page); + + dev_info(w->dev, + "BTOC_DUMP sb_ce=%u cau=%u blk=%u vblock=%u page=%u first32=%32ph meta0=%16ph be=%u %u %u%s\n", + sb->ce, sb->cau, sb->block, vblock, WHIMORY_BTOC_PAGE, + page, meta, be0, be1, be2, + lpn ? " (BE LPN array)" : ""); +} + +static void whimory_print_recovery_stats(struct whimory *w) +{ + struct whimory_sftl *s = &w->sftl; + + dev_info(w->dev, + "RECOVERY_STATS:\n" + " fpart_sig=%u vfl_ctx_hits=%u vfl_cxt_loc=%u vfl_bitmap=%u\n" + " classified_empty=%u classified_closed=%u classified_open=%u classified_cxt=%u\n" + " cxt_blocks_seen=%u cxt_records_seen=%u cxt_l2v_updates=%u\n" + " btoc_pages_read=%u btoc_pages_valid=%u btoc_entries_seen=%u btoc_l2v_updates=%u\n" + " btoc_token_ffff0000=%u btoc_token_ffffff00=%u btoc_token_ffffffff=%u btoc_holelist_ffff0001=%u\n" + " open_slots_seen=%u open_slots_valid_meta=%u open_l2v_updates=%u\n" + " l2v_update_calls=%u l2v_unmap_calls=%u l2v_repack_roots=%u mapped_lbas=%u mapped_roots=%u meta0_hits=%u\n", + w->sig_ok, w->vfl.ctx_hits, w->vfl.cxt_loc_count, + w->vfl.bitmap_loaded, + s->empty_sbs, s->btoc_sbs, s->open_sbs, s->cxt_sbs, + s->cxt_blocks_seen, s->cxt_records_seen, s->cxt_l2v_updates, + s->btoc_pages_read, s->btoc_pages_valid, s->btoc_entries_seen, + s->btoc_l2v_updates, + s->btoc_token_ffff0000, s->btoc_token_ffffff00, + s->btoc_token_ffffffff, s->btoc_holelist_ffff0001, + s->open_slots_seen, s->open_slots_valid_meta, + s->open_l2v_updates, + s->l2v_update_calls, s->l2v_unmap_calls, s->l2v_repack_roots, + s->mapped_lbas, s->mapped_roots, s->meta0_hits); +} + +static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) +{ + unsigned int i, pg, scanned = 0, cap; + u8 spare[S5L8740_FMSS_META_SIZE]; + u8 *data = w->sftl.data_page; + + cap = meta0_scan_sbs; + if (!cap || !data) + return; + dev_info(w->dev, + "META0_SCAN deeper closed SBs=%u pages 0..%u (independent of L2V)\n", + cap, WHIMORY_DATA_PAGES_PER_SB - 1); + for (i = 0; i < nsb && scanned < cap; i++) { + struct whimory_sb *sb = &w->sftl.sbs[i]; + + if (sb->kind != WHIMORY_SB_CLOSED) + continue; + scanned++; + for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; pg++) { + int ret; + + ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, + pg, 0, 16, data, + S5L8740_FMSS_PAGE_SIZE, + spare, sizeof(spare)); + if (ret) + break; + whimory_note_meta0(w, sb->ce, sb->cau, sb->block, pg, + data, spare); + if (!(pg & 0x1f)) + cond_resched(); + } + } +} + +static void whimory_dump_vba_page(struct whimory *w, u32 vba) +{ + u32 ce, cau, vblock, page, slot, pblock; + u8 spare[S5L8740_FMSS_META_SIZE]; + u8 *data = w->sftl.data_page; + int ret; + + if (!data) + return; + if (whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot)) { + dev_warn(w->dev, "BAD_VBA unpack failed vba=%u\n", vba); + return; + } + cau = whimory_vfl_bank(w, cau, vblock); + pblock = whimory_vfl_phys(w, cau, vblock); + dev_info(w->dev, + "BAD_VBA vba=%u sb=%u ofs=%u -> ce=%u cau=%u vblock=%u pbn=%u page=%u map_slot=%u\n", + vba, s_g_vba_to_sb(w, vba), s_g_vba_to_ofs(w, vba), + ce, cau, vblock, pblock, page, slot); + ret = s5l8740_fmss_page_read(ce, cau, pblock, page, 0, 16, data, + S5L8740_FMSS_PAGE_SIZE, spare, + sizeof(spare)); + if (ret) { + dev_warn(w->dev, "BAD_VBA page read %d\n", ret); + return; + } + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + const u8 *m = spare + slot * WHIMORY_META_SIZE; + const u8 *d = data + slot * WHIMORY_LBA_SIZE; + u32 meta_lba = get_unaligned_le32(m + 8); + u16 bps = get_unaligned_le16(d + 11); + + dev_info(w->dev, + "BAD_VBA slot=%u type=%02x flags=%02x meta_lba=%u bps=%u first64=%32ph %32ph meta=%16ph\n", + slot, m[0], m[1], meta_lba, bps, d, d + 32, m); + } +} + +static int whimory_sftl_recover_l2v_from_media(struct whimory *w) +{ + struct whimory_sftl *s = &w->sftl; + unsigned int ce, cau, b, nscan, nsb = 0, i, open_done = 0; + u8 meta0[S5L8740_FMSS_META_SIZE]; + u8 meta127[S5L8740_FMSS_META_SIZE]; + u8 *p127; + int ret; + + nscan = scan_blocks ? scan_blocks : s->user_blocks; + if (nscan > s->user_blocks) + nscan = s->user_blocks; + + p127 = s->btoc_page; + if (!p127) + return -ENOMEM; + s->btoc_dumps_left = 5; + + dev_info(w->dev, "SFTL classify scan ce=%u cau=%u blocks=%u\n", + w->geom.num_ce, w->geom.num_cau, nscan); + + for (ce = 0; ce < w->geom.num_ce; ce++) { + for (cau = 0; cau < w->geom.num_cau; cau++) { + for (b = 0; b < nscan; b++) { + struct whimory_sb *sb; + int r0, r127; + + if (nsb >= s->num_sb) + goto classify_done; + if ((b & 0x7f) == 0 && cau == 0 && ce == 0) + dev_info(w->dev, + "SFTL classify ce=%u cau=%u blk=%u/%u nsb=%u\n", + ce, cau, b, nscan, nsb); + r0 = s5l8740_fmss_page_read(ce, cau, b, 0, 0, 16, + w->sftl.data_page, + S5L8740_FMSS_PAGE_SIZE, + meta0, sizeof(meta0)); + r127 = s5l8740_fmss_page_read(ce, cau, b, + WHIMORY_BTOC_PAGE, + 0, 16, p127, + S5L8740_FMSS_PAGE_SIZE, + meta127, + sizeof(meta127)); + if (!r0) + whimory_note_meta0(w, ce, cau, b, 0, + w->sftl.data_page, + meta0); + if (r0 && r127) + continue; + if ((!r0 && whimory_page_blank(w->sftl.data_page, 64) && + whimory_page_blank(meta0, 16)) && + (r127 || (whimory_page_blank(p127, 64) && + whimory_page_blank(meta127, 16)))) { + s->empty_sbs++; + continue; + } + sb = &s->sbs[nsb]; + sb->ce = ce; + sb->cau = cau; + sb->block = b; + sb->weave = 0; + if (!r0 && (meta0[0] == WHIMORY_META_TYPE_DATA || + meta0[0] == WHIMORY_META_TYPE_DATA2 || + meta0[0] == WHIMORY_META_TYPE_SFTL_CXT)) + sb->weave = whimory_weave48(meta0); + if ((!r0 && meta0[0] == WHIMORY_META_TYPE_SFTL_CXT) || + (!r127 && meta127[0] == WHIMORY_META_TYPE_SFTL_CXT)) { + u32 vblock = whimory_vfl_virt(w, cau, b); + u32 sb_idx = whimory_sb_index(w, ce, cau, + vblock); + + sb->kind = WHIMORY_SB_CXT; + s->cxt_sbs++; + if ((!r0 && meta0[1] == 1) || + (!r127 && meta127[1] == 1)) + whimory_cxt_add_base(w, sb_idx, + sb->weave); + } else if (!r127 && + (meta127[0] == WHIMORY_META_TYPE_BTOC || + !whimory_page_blank(p127, 64))) { + sb->kind = WHIMORY_SB_CLOSED; + s->btoc_sbs++; + } else { + sb->kind = WHIMORY_SB_OPEN; + s->open_sbs++; + } + nsb++; + } + } + } +classify_done: + sort(s->sbs, nsb, sizeof(s->sbs[0]), whimory_sb_cmp, NULL); + dev_info(w->dev, + "SFTL classified nsb=%u closed=%u open=%u cxt=%u empty=%u\n", + nsb, s->btoc_sbs, s->open_sbs, s->cxt_sbs, s->empty_sbs); + + ret = whimory_cxt_load(w); + if (ret) + dev_warn(w->dev, "s_cxt_load %d; continuing with BTOC replay\n", + ret); + if (s->cxt_loaded) + dev_info(w->dev, "s_cxt_load OK bases=%u weave=%llu mapped=%u\n", + w->n_cxt, w->cxt_base_weave, s->range_nodes); + + for (i = 0; i < nsb; i++) { + struct whimory_sb *sb = &s->sbs[i]; + u32 vblock = whimory_vfl_virt(w, sb->cau, sb->block); + + if (sb->kind == WHIMORY_SB_CXT) + continue; + if (s->cxt_loaded && sb->weave && sb->weave < w->cxt_base_weave) + continue; + if (sb->kind == WHIMORY_SB_CLOSED) { + int ingested; + + ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, + WHIMORY_BTOC_PAGE, 0, 16, + s->btoc_page, + S5L8740_FMSS_PAGE_SIZE, + meta127, sizeof(meta127)); + if (ret) + continue; + s->btoc_pages_read++; + if (s->btoc_dumps_left && + (s->btoc_pages_read <= 2 || + whimory_btoc_looks_be_lpn(s->btoc_page))) { + whimory_dump_btoc_page(w, sb, vblock, + s->btoc_page, meta127); + s->btoc_dumps_left--; + } + ingested = whimory_ingest_btoc_page(w, sb->ce, sb->cau, + vblock, s->btoc_page, + S5L8740_FMSS_PAGE_SIZE); + if (ingested) + s->btoc_pages_valid++; + } else if (sb->kind == WHIMORY_SB_OPEN) { + if (max_open_sbs && open_done >= max_open_sbs) + continue; + ret = whimory_rebuild_open_sb(w, sb); + if (ret > 0) + open_done++; + else if (ret < 0) + return ret; + } + } + + ret = whimory_l2v_build_from_ranges(w); + if (ret && s->range_nodes) { + dev_warn(w->dev, + "L2V pack %d; using interval map (%u ranges)\n", + ret, s->range_nodes); + ret = 0; + } else if (ret) { + return ret; + } else { + s->packed_ok = true; + } + w->l2v_ok = true; + whimory_scan_closed_meta0(w, nsb); + whimory_print_recovery_stats(w); + return 0; +} + +static int whimory_sftl_alloc(struct whimory *w) +{ + struct whimory_sftl *s = &w->sftl; + u32 nsb; + + s->vbas_per_page = WHIMORY_VBAS_PER_PAGE; + s->pages_per_sb = WHIMORY_PAGES_PER_SB; + s->vbas_per_sb = WHIMORY_VBAS_PER_SB; + s->user_blocks = w->geom.user_blocks; + nsb = w->geom.num_ce * w->geom.num_cau * s->user_blocks; + if (w->vfl_ops && w->vfl_ops->get_param) { + u32 p = w->vfl_ops->get_param(w, WHIMORY_VFL_PARAM_NUM_SB); + + if (p) + nsb = p; + } + s->num_sb = nsb; + s->vba_factor_a = nsb; + s->vba_factor_b = s->vbas_per_sb; + s->nodepool_bytes = WHIMORY_MIN_NODEPOOL_BYTES; + + s->btoc_page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); + s->data_page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); + s->meta_page = kvmalloc(WHIMORY_META_SIZE * WHIMORY_VBAS_PER_PAGE * + (WHIMORY_DATA_PAGES_PER_SB + 1), GFP_KERNEL); + s->sbs = kvcalloc(nsb, sizeof(*s->sbs), GFP_KERNEL); + if (!s->btoc_page || !s->data_page || !s->meta_page || !s->sbs) + return -ENOMEM; + + /* + * sub_56863C: max_pages_per_btoc = + * div(page_bytes + 16 * vbas_per_sb - 1, page_bytes) + 1 + * 16×512 BTE bytes fit in a 16KiB NAND page → 1; OSOS adds 1 → 2. + */ + { + u32 page_bytes = w->geom.page_size ? + w->geom.page_size : S5L8740_FMSS_PAGE_SIZE; + u32 i; + + s->max_pages_per_btoc = + (page_bytes + 16 * s->vbas_per_sb - 1) / page_bytes + 1; + if (!s->max_pages_per_btoc) + return -EINVAL; + for (i = 0; i < WHIMORY_BTOC_OPEN; i++) { + s->btoc_lba[i] = kvmalloc_array(s->vbas_per_sb, + sizeof(u32), + GFP_KERNEL); + if (!s->btoc_lba[i]) + return -ENOMEM; + memset(s->btoc_lba[i], 0xff, + s->vbas_per_sb * sizeof(u32)); + } + } + + /* + * sub_56A328: zoneSize starts at 0x8D0EC98 * vbas_per_page and + * doubles until >= 16. Minimum from the loop is 16; must be a + * multiple of vbas_per_page. CXT load (sub_4FDBE8) reads this + * many VBAs into gc_data / gc_meta. + */ + s->gc_zone_size = WHIMORY_GC_ZONE_MIN; + if (s->gc_zone_size % s->vbas_per_page) + return -EINVAL; + s->gc_data = kvmalloc((size_t)WHIMORY_LBA_SIZE * s->gc_zone_size, + GFP_KERNEL); + s->gc_meta = kvmalloc((size_t)WHIMORY_META_SIZE * s->gc_zone_size, + GFP_KERNEL); + if (!s->gc_data || !s->gc_meta) + return -ENOMEM; + /* + * sub_130158 full-size FTL: num_superblocks * user VBAs per SB. + * BTOC page is not host LBA space (DATA_VBAS_PER_SB). + */ + { + u64 cap = (u64)nsb * WHIMORY_DATA_VBAS_PER_SB; + + w->total_4k_sectors = cap ? cap : FMSS_FTL_DEFAULT_CAPACITY; + } + return 0; +} + +static int whimory_oracle_load(struct whimory *w) +{ + const struct firmware *gfw = NULL, *rfw = NULL, *nfw = NULL, *sfw = NULL; + int ret; + + ret = request_firmware(&sfw, WHIMORY_ORACLE_SIG, w->dev); + if (!ret && sfw && sfw->size >= WHIMORY_SIG_SIZE) { + ret = whimory_parse_signature(w, sfw->data); + if (ret) + dev_err(w->dev, "oracle signature invalid: %d\n", ret); + } + if (sfw) + release_firmware(sfw); + + ret = request_firmware(&gfw, WHIMORY_ORACLE_GLOBALS, w->dev); + if (ret) { + dev_err(w->dev, "oracle globals missing: %d\n", ret); + return ret; + } + if (gfw->size < 28) { + release_firmware(gfw); + return -EINVAL; + } + { + u32 num_roots = get_unaligned_le32(gfw->data + 0); + u32 nodepool = get_unaligned_le32(gfw->data + 4); + u32 max_lba; + + if (gfw->size >= 32) + max_lba = get_unaligned_le32(gfw->data + 28); + else + max_lba = (u32)w->total_4k_sectors; + if (!num_roots || !nodepool) + ret = -EINVAL; + else + ret = whimory_l2v_init(w, max_lba ? max_lba : + (u32)w->total_4k_sectors, + w->sftl.vba_factor_a, + w->sftl.vba_factor_b, + nodepool); + if (!ret && gfw->size >= 12) { + w->l2v.bits_vba = gfw->data[8]; + w->l2v.spanbits_vba = gfw->data[9]; + w->l2v.bits_nodeidx = gfw->data[10]; + w->l2v.spanbits_nodeidx = gfw->data[11]; + if (gfw->size >= 16) + w->l2v.invalid_vba = + get_unaligned_le32(gfw->data + 12); + w->l2v.sentinel_vba = w->l2v.invalid_vba; + if (num_roots && num_roots != w->l2v.num_roots) + dev_warn(w->dev, + "oracle num_roots=%u init=%u\n", + num_roots, w->l2v.num_roots); + } + } + release_firmware(gfw); + if (ret) + return ret; + + ret = request_firmware(&rfw, WHIMORY_ORACLE_ROOT, w->dev); + if (ret) + return ret; + if (rfw->size < WHIMORY_L2V_ROOT_REC_SIZE * w->l2v.num_roots) { + release_firmware(rfw); + return -EINVAL; + } + memcpy(w->l2v.root, rfw->data, + WHIMORY_L2V_ROOT_REC_SIZE * w->l2v.num_roots); + release_firmware(rfw); + + ret = request_firmware(&nfw, WHIMORY_ORACLE_NODES, w->dev); + if (ret) + return ret; + if (nfw->size < w->l2v.nodepool_bytes) { + release_firmware(nfw); + return -EINVAL; + } + memcpy(w->l2v.nodes, nfw->data, w->l2v.nodepool_bytes); + release_firmware(nfw); + + w->oracle_used = true; + w->l2v_ok = true; + whimory_l2v_find_frag(w); + dev_info(w->dev, + "L2V oracle loaded roots=%u nodes=0x%x frag=%u/%u\n", + w->l2v.num_roots, w->l2v.nodepool_bytes, + w->l2v.frag_count, w->l2v.frag_max); + return 0; +} + +static int n31_sftl_init(struct whimory *w) +{ + if (!w->vfl_ok) + return -ENODEV; + w->sftl.vbas_per_page = WHIMORY_VBAS_PER_PAGE; + w->sftl.pages_per_sb = WHIMORY_PAGES_PER_SB; + w->sftl.vbas_per_sb = WHIMORY_VBAS_PER_SB; + w->sftl.user_blocks = w->geom.user_blocks; + if (!w->sftl.user_blocks || !w->sftl.vbas_per_sb) + return -EINVAL; + return 0; +} + +static int whimory_l2v_selftest(struct whimory *w) +{ + u32 vba = ~0u, span = 0; + int fail = 0; + + if (whimory_l2v_update(w, 0, 1, 100) || + whimory_l2v_search(w, 0, &vba, &span) || vba != 100) + fail++; + if (whimory_l2v_update(w, 1, 10, 101) || + whimory_l2v_search(w, 5, &vba, &span) || vba != 105) + fail++; + if (whimory_l2v_update(w, 0x7fff, 4, 200) || + whimory_l2v_search(w, 0x7fff, &vba, &span) || vba != 200) + fail++; + if (whimory_l2v_search(w, 0x8000, &vba, &span) || vba != 201) + fail++; + whimory_range_free(w); + if (w->l2v.root && w->l2v.num_roots) + memset(w->l2v.root, 0xff, + WHIMORY_L2V_ROOT_REC_SIZE * w->l2v.num_roots); + whimory_l2v_mem_reset(&w->l2v); + w->sftl.l2v_update_calls = 0; + w->sftl.l2v_unmap_calls = 0; + w->sftl.l2v_repack_roots = 0; + dev_info(w->dev, "L2V_SELFTEST %s\n", fail ? "FAIL" : "OK"); + return fail ? -EINVAL : 0; +} + +static int n31_sftl_open(struct whimory *w) +{ + int ret; + + /* + * OSOS FTL_Open: sub_56863C BTOC (6 slots / 2 open LBA maps), + * sub_56A328 GC zone, sub_56B56C block tables, sub_56C7B8 SB + * state, nodepool ≥ 0x80000, sub_E8CA0 L2V_Init, then s_boot. + */ + ret = whimory_sftl_alloc(w); + if (ret) + return ret; + + if (import_l2v_oracle) { + ret = whimory_oracle_load(w); + if (ret) { + dev_err(w->dev, "L2V oracle load failed: %d\n", ret); + return ret; + } + return 0; + } + + ret = whimory_l2v_init(w, (u32)w->total_4k_sectors, + w->sftl.vba_factor_a, w->sftl.vba_factor_b, + w->sftl.nodepool_bytes); + if (ret) + return ret; + whimory_l2v_selftest(w); + + ret = whimory_sftl_recover_l2v_from_media(w); + if (ret) + return ret; + return 0; +} + +static const struct whimory_ftl_ops n31_sftl_ops = { + .major = 0, + .minor = n31_sftl_minor, + .init = n31_sftl_init, + .open = n31_sftl_open, + .read_lba = n31_sftl_read_lba, +}; + +static int whimory_select_ops(struct whimory *w) +{ + /* + * OSOS dispatches VFL/FTL by signature major through a table that + * is not named in the static dump. N31 media is PPN VFL + SFTL; + * those are the only ops this module implements. Log the majors + * from the signature (when present) and bind the N31 ops. + */ + w->vfl_ops = &n31_vfl_ops; + w->ftl = &n31_sftl_ops; + if (w->sig_ok) { + dev_info(w->dev, + "VFL_SELECT major=%u minor=%u arg=%u\n", + w->sig.vfl_major, w->sig.vfl_minor, + w->sig.flags_or_open); + dev_info(w->dev, + "FTL_SELECT major=%u minor=%u\n", + w->sig.ftl_major, w->sig.ftl_minor); + dev_info(w->dev, + "ops bound fpart=%u.%u vfl=%u.%u ftl=%u.%u\n", + w->sig.fpart_major, w->sig.fpart_minor, + w->sig.vfl_major, w->sig.vfl_minor, + w->sig.ftl_major, w->sig.ftl_minor); + } else { + dev_warn(w->dev, + "VFL_SELECT/FTL_SELECT skipped: sig=0, binding N31 PPN+SFTL fallback\n"); + } + return 0; +} + +static int whimory_ftl_open(struct whimory *w) +{ + int ret; + + ret = w->ftl->init(w); + if (ret) { + dev_err(w->dev, "FTL_Init failed: %d\n", ret); + return ret; + } + ret = w->ftl->open(w); + if (ret) { + dev_err(w->dev, "FTL_Open failed: %d\n", ret); + return ret; + } + w->ftl_ok = true; + dev_info(w->dev, "FTL_Open OK\n"); + return 0; +} + +/* ------------------------------------------------------------------ */ +/* Read path (sub_56AB3C / sub_56C328) */ +/* ------------------------------------------------------------------ */ + +static int whimory_validate_meta(struct whimory *w, + const struct whimory_meta *m, + u32 expected_lba) +{ + u32 meta_lba = le32_to_cpu(m->lba); + + if (meta_lba != expected_lba) { + dev_err(w->dev, + "sftl lba mismatch want=0x%x meta=0x%x type=%02x flags=%02x\n", + expected_lba, meta_lba, m->type, m->flags); + return -EIO; + } + if (m->flags & 0x02) { + dev_err(w->dev, + "sftl uECC flag lba=0x%x type=%02x flags=%02x\n", + expected_lba, m->type, m->flags); + return -EIO; + } + return 0; +} + +static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, + bool allow_blank) +{ + struct whimory_meta meta; + u32 vba = 0, span = 0; + int ret; + + if (!w->l2v_ok) + return -ENODEV; + ret = whimory_l2v_search(w, lba, &vba, &span); + if (ret) { + if (allow_blank && ret == -ENOENT) { + memset(buf, 0xff, WHIMORY_LBA_SIZE); + return 0; + } + return ret; + } + if (lba == 0) + w->lba0_vba = vba; + if (vba >= w->l2v.invalid_vba) { + if (!allow_blank) + return -ENOENT; + memset(buf, 0xff, WHIMORY_LBA_SIZE); + return 0; + } + if (lba == 0) { + dev_info(w->dev, "L2V lookup LBA0 -> VBA=%u span=%u\n", + vba, span); + whimory_dump_vba_page(w, vba); + } + ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); + if (ret) + return ret; + ret = whimory_validate_meta(w, &meta, lba); + if (!ret) + dev_dbg(w->dev, "meta OK lba=%u vba=%u type=%02x\n", + lba, vba, meta.type); + return ret; +} + +static int whimory_read_lba_4k(struct whimory *w, u32 lba, void *buf) +{ + return n31_sftl_read_lba(w, lba, buf, true); +} + +static int whimory_ftl_read_hook(u64 lba, void *buf) +{ + struct whimory *w = whimory_dev; + + if (!w || !w->l2v_ok) + return -ENODEV; + if (lba >= w->total_4k_sectors) + return -ERANGE; + return whimory_read_lba_4k(w, (u32)lba, buf); +} + +static int whimory_check_lba0(struct whimory *w) +{ + u8 *buf; + int ret; + u16 bps; + u32 total32, rootclus, serial; + + buf = kzalloc(WHIMORY_LBA_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + ret = n31_sftl_read_lba(w, 0, buf, false); + if (ret) { + dev_err(w->dev, "LBA0 read failed: %d\n", ret); + goto out; + } + + dev_info(w->dev, "LBA0 first32=%32ph\n", buf); + dev_info(w->dev, "LBA0 next32=%32ph\n", buf + 32); + + bps = get_unaligned_le16(buf + 11); + total32 = get_unaligned_le32(buf + 32); + rootclus = get_unaligned_le32(buf + 44); + serial = get_unaligned_le32(buf + 67); + dev_info(w->dev, + "BPB bytes_per_sector=%u sectors_per_cluster=%u total32=%u rootclus=%u serial=%08x label=%.11s\n", + bps, buf[13], total32, rootclus, serial, buf + 71); + + if (bps != 4096) { + ret = -EINVAL; + goto out; + } + if (buf[0] != 0xeb && buf[0] != 0xe9) { + ret = -EINVAL; + goto out; + } + dev_info(w->dev, "meta OK lba=0\n"); + w->lba0_ok = true; + if (total32) + w->total_4k_sectors = total32; +out: + kfree(buf); + return ret; +} + +/* ------------------------------------------------------------------ */ +/* Block device */ +/* ------------------------------------------------------------------ */ + +static void whimory_submit_bio_range(struct bio *bio, u64 start_4k, + u64 n_4k) +{ + struct whimory *w = whimory_dev; + struct bvec_iter iter; + struct bio_vec bvec; + sector_t sector = bio->bi_iter.bi_sector; + int ret = 0; + + if (!w || !w->lba0_ok) { + bio_io_error(bio); + return; + } + if (op_is_write(bio_op(bio))) { + bio_io_error(bio); + return; + } + + bio_for_each_segment(bvec, bio, iter) { + u8 *dst = kmap_local_page(bvec.bv_page) + bvec.bv_offset; + unsigned int done_bytes = 0; + + while (done_bytes < bvec.bv_len) { + u32 lba4k = (u32)(start_4k + (sector >> 3)); + unsigned int off = (sector & 7) * 512; + unsigned int n = min_t(unsigned int, + bvec.bv_len - done_bytes, + 4096 - off); + + if ((u64)lba4k >= start_4k + n_4k || + lba4k >= w->total_4k_sectors) { + ret = -EIO; + kunmap_local(dst); + goto done; + } + mutex_lock(&w->bounce_lock); + ret = whimory_read_lba_4k(w, lba4k, w->bounce); + if (!ret) + memcpy(dst + done_bytes, w->bounce + off, n); + mutex_unlock(&w->bounce_lock); + if (ret) { + kunmap_local(dst); + goto done; + } + done_bytes += n; + sector += n >> 9; + } + kunmap_local(dst); + } +done: + if (ret) + bio_io_error(bio); + else + bio_endio(bio); +} + +static void whimory_submit_bio(struct bio *bio) +{ + struct whimory *w = whimory_dev; + + whimory_submit_bio_range(bio, 0, w ? w->total_4k_sectors : 0); +} + +static void whimory_ipod_submit_bio(struct bio *bio) +{ + struct whimory *w = whimory_dev; + + whimory_submit_bio_range(bio, 0, w ? w->total_4k_sectors : 0); +} + +static const struct block_device_operations whimory_bd_ops = { + .owner = THIS_MODULE, + .submit_bio = whimory_submit_bio, +}; + +static const struct block_device_operations whimory_ipod_ops = { + .owner = THIS_MODULE, + .submit_bio = whimory_ipod_submit_bio, +}; + +static struct gendisk *whimory_alloc_disk(struct whimory *w, const char *name, + const struct block_device_operations *ops) +{ + struct queue_limits lim = { + .logical_block_size = WHIMORY_LBA_SIZE, + .physical_block_size = WHIMORY_LBA_SIZE, + }; + struct gendisk *gd; + int ret; + + gd = blk_alloc_disk(&lim, NUMA_NO_NODE); + if (IS_ERR(gd)) + return gd; + gd->first_minor = 0; + gd->flags = GENHD_FL_NO_PART; + gd->fops = ops; + gd->private_data = w; + snprintf(gd->disk_name, DISK_NAME_LEN, "%s", name); + set_capacity(gd, w->total_4k_sectors * (WHIMORY_LBA_SIZE / 512)); + set_disk_ro(gd, 1); + ret = add_disk(gd); + if (ret) { + put_disk(gd); + return ERR_PTR(ret); + } + return gd; +} + +static int whimory_register_disk(struct whimory *w) +{ + struct gendisk *gd; + + if (!w->sig_ok) { + dev_warn(w->dev, + "sig=0: native PASS requires FPart 0xC101 xrmw signature\n"); + if (!allow_sigless_debug) + return -ENODEV; + } + if (!w->vfl_ok || !w->ftl_ok || !w->l2v_ok || !w->lba0_ok) + return -ENODEV; + + gd = whimory_alloc_disk(w, FTL_DISK_NAME, &whimory_bd_ops); + if (IS_ERR(gd)) + return PTR_ERR(gd); + w->disk = gd; + gd = whimory_alloc_disk(w, FTL_IPOD_NAME, &whimory_ipod_ops); + if (!IS_ERR(gd)) + w->ipod_disk = gd; + s5l8740_fmss_register_ftl_read(whimory_ftl_read_hook); + dev_info(w->dev, + "/dev/%s registered read-only (%llu x %uB)\n", + FTL_DISK_NAME, w->total_4k_sectors, WHIMORY_LBA_SIZE); + return 0; +} + +static void whimory_unregister_disk(struct whimory *w) +{ + s5l8740_fmss_register_ftl_read(NULL); + if (w->ipod_disk) { + del_gendisk(w->ipod_disk); + put_disk(w->ipod_disk); + w->ipod_disk = NULL; + } + if (w->disk) { + del_gendisk(w->disk); + put_disk(w->disk); + w->disk = NULL; + } +} + +static ssize_t whimory_status_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct whimory *w = whimory_dev; + + if (!w) + return sysfs_emit(buf, "no device\n"); + return sysfs_emit(buf, + "fil=%d sig=%d vfl=%d ftl=%d l2v=%d lba0=%d oracle=%d\n" + "mapped_roots=%u mapped_lbas=%u btoc_sbs=%u open_sbs=%u cxt_sbs=%u empty=%u recs=%u cxt_loaded=%d packed=%d\n" + "lba0_vba=%u cap=%llu vbas_per_sb=%u hole=%u list=%u\n" + "spare_applied=%u bitmap=%u frag=%u/%u gc_zone=%u btoc_pages=%u updates=%u gen=%u free=%u list_unmapped=%u\n%s\n", + w->fil_ok, w->sig_ok, w->vfl_ok, w->ftl_ok, + w->l2v_ok, w->lba0_ok, w->oracle_used, + w->sftl.mapped_roots, w->sftl.mapped_lbas, + w->sftl.btoc_sbs, w->sftl.open_sbs, w->sftl.cxt_sbs, + w->sftl.empty_sbs, w->sftl.btoc_recs, + w->sftl.cxt_loaded, w->sftl.packed_ok, + w->lba0_vba, w->total_4k_sectors, w->sftl.vbas_per_sb, + w->sftl.token_hole, w->sftl.token_list, + w->vfl.spare_applied, w->vfl.bitmap_loaded, + w->l2v.frag_count, w->l2v.frag_max, + w->sftl.gc_zone_size, w->sftl.max_pages_per_btoc, + w->l2v.updates, w->l2v.gen, w->l2v.free_count, + w->sftl.token_list_applied, + w->status); +} +static DEVICE_ATTR_RO(whimory_status); + +static struct attribute *ftl_attrs[] = { + &dev_attr_whimory_status.attr, + NULL, +}; +static const struct attribute_group ftl_attr_group = { + .attrs = ftl_attrs, +}; + +static void whimory_free(struct whimory *w) +{ + unsigned int cau; + + if (!w) + return; + whimory_unregister_disk(w); + whimory_range_free(w); + whimory_l2v_free(w); + kvfree(w->sftl.btoc_page); + kvfree(w->sftl.data_page); + kvfree(w->sftl.meta_page); + kvfree(w->sftl.sbs); + kvfree(w->sftl.gc_data); + kvfree(w->sftl.gc_meta); + kvfree(w->vfl.bank_mask); + for (cau = 0; cau < WHIMORY_BTOC_OPEN; cau++) + kvfree(w->sftl.btoc_lba[cau]); + for (cau = 0; cau < S5L8740_FMSS_MAX_CAU; cau++) { + kvfree(w->vfl.remap[cau]); + kvfree(w->vfl.cxt_u16[cau]); + } + kfree(w->bounce); + kfree(w); +} + +static int whimory_open_stack(struct whimory *w) +{ + int ret; + + ret = whimory_fil_init(w); + if (ret) { + whimory_set_status(w, "FIL_Init failed %d", ret); + return ret; + } + ret = whimory_read_signature(w); + if (ret) { + whimory_set_status(w, "signature failed %d", ret); + return ret; + } + ret = whimory_select_ops(w); + if (ret) + return ret; + ret = whimory_vfl_open(w); + if (ret) { + whimory_set_status(w, "VFL_Open failed %d", ret); + return ret; + } + ret = whimory_ftl_open(w); + if (ret) { + whimory_set_status(w, "FTL_Open failed %d", ret); + return ret; + } + ret = whimory_check_lba0(w); + if (ret) { + whimory_set_status(w, "LBA0 check failed %d", ret); + return ret; + } + ret = whimory_register_disk(w); + if (ret) { + whimory_set_status(w, "disk register failed %d", ret); + return ret; + } + whimory_set_status(w, "ready"); + return 0; +} + +static int __init ftl_init(void) +{ + struct whimory *w; + int ret; + + if (!s5l8740_fmss_available()) { + pr_err("s5l8740-ftl: load fmss-s5l8740.ko first\n"); + return -ENODEV; + } + + w = kzalloc(sizeof(*w), GFP_KERNEL); + if (!w) + return -ENOMEM; + w->dev = fmss_ftl_device(); + mutex_init(&w->bounce_lock); + mutex_init(&w->tree_lock); + w->ranges = RB_ROOT; + w->bounce = kzalloc(WHIMORY_LBA_SIZE, GFP_KERNEL); + if (!w->bounce) { + kfree(w); + return -ENOMEM; + } + w->total_4k_sectors = FMSS_FTL_DEFAULT_CAPACITY; + whimory_dev = w; + + ftl_pdev = platform_device_register_simple("s5l8740-ftl", -1, NULL, 0); + if (IS_ERR(ftl_pdev)) { + ret = PTR_ERR(ftl_pdev); + ftl_pdev = NULL; + whimory_free(w); + whimory_dev = NULL; + return ret; + } + w->pdev = ftl_pdev; + w->dev = &ftl_pdev->dev; + ret = sysfs_create_group(&ftl_pdev->dev.kobj, &ftl_attr_group); + if (ret) { + platform_device_unregister(ftl_pdev); + whimory_free(w); + whimory_dev = NULL; + ftl_pdev = NULL; + return ret; + } + + ret = whimory_open_stack(w); + if (ret) { + dev_err(w->dev, + "Whimory open failed (%d) — NOT registering /dev/%s (fil=%d sig=%d vfl=%d ftl=%d l2v=%d lba0=%d)\n", + ret, FTL_DISK_NAME, w->fil_ok, w->sig_ok, w->vfl_ok, + w->ftl_ok, w->l2v_ok, w->lba0_ok); + /* + * Keep the platform device so sysfs status is visible. + * The block disk is absent until LBA0 works. + */ + return 0; + } + return 0; +} + +static void __exit ftl_exit(void) +{ + struct whimory *w = whimory_dev; + if (ftl_pdev) { sysfs_remove_group(&ftl_pdev->dev.kobj, &ftl_attr_group); platform_device_unregister(ftl_pdev); ftl_pdev = NULL; } - ftl_unregister_disk(); + whimory_dev = NULL; + whimory_free(w); } module_init(ftl_init); module_exit(ftl_exit); MODULE_LICENSE("GPL"); -MODULE_DESCRIPTION("S5L8740 Whimory FTL RO disks (ftl/firmware/ipod/rsrc)"); +MODULE_DESCRIPTION("S5L8740 Whimory PPN SFTL read-only block driver"); MODULE_AUTHOR("n31"); MODULE_SOFTDEP("pre: fmss_s5l8740"); +MODULE_FIRMWARE(WHIMORY_ORACLE_SIG); +MODULE_FIRMWARE(WHIMORY_ORACLE_ROOT); +MODULE_FIRMWARE(WHIMORY_ORACLE_NODES); +MODULE_FIRMWARE(WHIMORY_ORACLE_GLOBALS); diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h new file mode 100755 index 00000000000000..bb5e25e3b18b8e --- /dev/null +++ b/drivers/misc/whimory-s5l8740.h @@ -0,0 +1,335 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * N31 Whimory PPN / SFTL / L2V — in-kernel structures. + * + * On-flash constants from OSOS 1.0.2 (sub_111B0C / sub_1122FC / sub_E8CA0 / + * sub_428694 / sub_56AB3C / sub_56C328). Runtime L2V bytes are allocated + * here; they are not present in the static OSOS dump. + */ +#ifndef WHIMORY_S5L8740_H +#define WHIMORY_S5L8740_H + +#include +#include +#include +#include + +#include "fmss-s5l8740-api.h" + +#define WHIMORY_SIG_SIZE 0x600 +#define WHIMORY_SIG_MAGIC 0x776d7278u /* "xrmw" LE — payload, not raw page+0 */ +#define WHIMORY_SIG_TYPE 0xC101u /* FPart special type; op80/op20 */ +#define WHIMORY_SIG_MAGIC_WRMX 0x786d7277u /* "wrmx" LE — VFL CXT, not FPart sig */ +#define WHIMORY_SIG_MAGIC_REV 0x78726d77u /* "wmrx" byte-reversed hunt */ + +/* OSOS fpart_read_special_copy_4F1420 / locate_4EBBDC */ +#define FPART_SPECIAL_TAG 0x30u +#define FPART_SPECIAL_CLASS 1u +#define FPART_SPECIAL_CLASS_MASK 0x3fu +#define FPART_SPECIAL_HDR 0x80u /* chunk0 payload starts here */ +#define FPART_SPECIAL_LEN_OFF 0x24u +#define FPART_SPECIAL_GEN_OFF 0x28u +#define FPART_SPECIAL_TABLE_BYTES 0x2d0u /* ctx+112; 120 × 6 */ +#define FPART_SPECIAL_MAX_ENTRIES (FPART_SPECIAL_TABLE_BYTES / 6) +#define FPART_ASSIGN_MAX_PAIRS 8u +#define FPART_SPECIAL_TYPE_C104 0xC104u +#define FPART_SPECIAL_TYPE_C105 0xC105u + +struct fpart_special_entry { + u16 bank; + u16 block; + u16 type_word; +} __packed; + +struct whimory_fpart { + struct fpart_special_entry table[FPART_SPECIAL_MAX_ENTRIES]; + u16 count; + bool scanned; + unsigned int slot_logs; +}; + +#define WHIMORY_LBA_SIZE 4096U +#define WHIMORY_MIN_NODEPOOL_BYTES 0x80000 +#define WHIMORY_L2V_NODE_SIZE 64 +#define WHIMORY_L2V_ROOT_SPAN 0x8000 +#define WHIMORY_L2V_ROOT_REC_SIZE 6 +#define WHIMORY_L2V_INVALID_ROOT 0xffff +#define WHIMORY_META_SIZE 16 +#define WHIMORY_VBAS_PER_PAGE 4 +#define WHIMORY_PAGES_PER_SB 128 +#define WHIMORY_DATA_PAGES_PER_SB 127 +#define WHIMORY_BTOC_PAGE 127 +/* s_g_vbas_per_sb includes the BTOC page: addr_to_vba(sb, vfl+76-1) + * is the last VBA of page 127 (sub_56B77C). */ +#define WHIMORY_VBAS_PER_SB (WHIMORY_PAGES_PER_SB * \ + WHIMORY_VBAS_PER_PAGE) +#define WHIMORY_DATA_VBAS_PER_SB (WHIMORY_DATA_PAGES_PER_SB * \ + WHIMORY_VBAS_PER_PAGE) + +#define WHIMORY_META_TYPE_DATA 0x01 +#define WHIMORY_META_TYPE_DATA2 0x02 +#define WHIMORY_META_TYPE_BTOC 0x1c +#define WHIMORY_META_TYPE_SFTL_CXT 0x1f +#define WHIMORY_META_TYPE_VFL_CXT 0x20 + +#define WHIMORY_SB_EMPTY 0 +#define WHIMORY_SB_CLOSED 1 +#define WHIMORY_SB_OPEN 2 +#define WHIMORY_SB_CXT 7 /* s_cxt_diff.c type 7 */ + +#define WHIMORY_CXT_MAX_SB 32 +#define WHIMORY_CXT_TAG_STATS 2 +#define WHIMORY_CXT_TAG_L2V 4 +#define WHIMORY_CXT_TAG_END 255 +#define WHIMORY_CXT_CONTIG_SPAN 0xfffffff0u +#define WHIMORY_FIL_META_BYTES 16 /* FIL GetInfo(105); sub_12ED9C */ + +/* BTOC / META tokens (sub_5688C4). Occupy VBA stream, not user L2V. */ +#define WHIMORY_LBA_HOLE 0xFFFF0000u +#define WHIMORY_LBA_LIST 0xFFFF0001u +#define WHIMORY_LBA_DELETED 0xFFFFFF00u +#define WHIMORY_LBA_BLANK 0xFFFFFFFFu + +/* VFL_GetParam selector: num_superblocks (WhimoryBoot.c sub_130158). */ +#define WHIMORY_VFL_PARAM_NUM_SB 0x02000100u +#define WHIMORY_VFL_SPARE_FREE 0xC070u /* sub_4EABA4 / 3D26D8 */ +#define WHIMORY_VFL_CXT_HDR 0x200u /* sub_4EB02C */ +#define WHIMORY_VFL_SPARE_STRIDE 32u /* sub_4EB098 */ +#define WHIMORY_BTOC_SLOTS 6 /* s_btoc.c sub_56863C */ +#define WHIMORY_BTOC_OPEN 2 +#define WHIMORY_GC_ZONE_MIN 16 /* s_gc.c sub_56A328 */ +#define WHIMORY_L2V_FINDFRAG_WIN 32 /* L2V_FindFrag.c */ +#define WHIMORY_L2V_MIN_FREE 0x22u /* sub_3F8958: free > 0x21 */ +#define WHIMORY_L2V_UPDATE_REPACK 0xC8u /* sub_E8EA8 */ + +struct whimory; + +struct whimory_geometry { + u32 num_ce; + u32 num_cau; + u32 blocks_per_cau; + u32 user_blocks; + u32 pages_per_block; + u32 page_size; + u32 vfl_tail; + u32 dev_id; + u32 geom_104; + u32 geom_105; + u32 geom_135; +}; + +struct whimory_signature { + u8 raw[WHIMORY_SIG_SIZE]; + u32 version; + u32 ftl_major; + u32 ftl_minor; + u32 vfl_major; + u32 vfl_minor; + u32 fpart_major; + u32 fpart_minor; + u32 sig_geom; + u32 flags_or_open; /* +0x20 VFL/open arg */ + u32 fpart_arg; /* +0x2c */ + u32 extra_arg; /* +0x30 */ +}; + +struct whimory_meta { + u8 type; + u8 flags; + u8 unk02[6]; + __le32 lba; + u8 unk0c[4]; +} __packed; + +/* s_btoc.c sub_567E3C / s_cxt_diff.c sub_5694F0 — LE u32×4 */ +struct whimory_bte { + __le32 weave_seq_add; + __le32 aux; + __le32 lba; + __le32 span; +} __packed; + +struct whimory_leaf { + u32 vba; + u32 span; +}; + +struct whimory_l2v { + u32 num_roots; + u32 nodepool_bytes; + u32 nodes_used; + u8 bits_vba; + u8 spanbits_vba; + u8 bits_nodeidx; + u8 spanbits_nodeidx; + u32 invalid_vba; + u32 sentinel_vba; + u32 updates; /* 0x8D100B0 */ + u32 gen; /* 0x8D100B4 */ + u32 frag_count; /* L2V_FindFrag */ + u32 frag_max; + u32 free_head; /* L2V_Mem.c 0x8D100DC; 0xffffffff empty */ + u32 free_count; /* 0x8D100E0 */ + u8 *root; /* 6 bytes × numRoots */ + u8 *nodes; + struct whimory_leaf *leaf_scratch; +}; + +struct whimory_range { + struct rb_node rb; + u32 start; + u32 len; + u32 vba; +}; + +struct whimory_vfl { + u32 *remap[S5L8740_FMSS_MAX_CAU]; + u16 *cxt_u16[S5L8740_FMSS_MAX_CAU]; + u32 ctx_ce[S5L8740_FMSS_MAX_CAU]; + u32 ctx_block[S5L8740_FMSS_MAX_CAU]; + u32 remap_count; + u32 cxt_u16_len; + u32 cxt_loc_count; + u32 ctx_hits; + u32 spare_applied; + u32 bitmap_loaded; + u32 bank_stride; /* 0x8D0D0F0; N31 = 1 byte/VBN */ + u8 *bank_mask; /* [blocks_per_cau] bank bitmask; sub_3D1438 */ + u16 cached_vbn; + u8 cached_n; + u8 cached_banks[S5L8740_FMSS_MAX_CAU]; +}; + +struct whimory_sb { + u16 ce; + u16 cau; + u16 block; + u8 kind; + u64 weave; +}; + +struct whimory_sftl { + u32 vba_factor_a; + u32 vba_factor_b; + u32 nodepool_bytes; + u32 vbas_per_page; + u32 vbas_per_sb; + u32 pages_per_sb; + u32 num_sb; + u32 user_blocks; + u8 *btoc_page; + u8 *data_page; + u8 *meta_page; + struct whimory_sb *sbs; + u32 mapped_roots; + u32 mapped_lbas; + u32 btoc_sbs; + u32 open_sbs; + u32 empty_sbs; + u32 cxt_sbs; + u32 btoc_recs; + u32 range_nodes; + u32 cxt_bases; + u32 token_hole; + u32 token_list; + u32 token_list_applied; + u32 max_pages_per_btoc; + u32 gc_zone_size; + u8 *gc_data; + u8 *gc_meta; + u32 *btoc_lba[WHIMORY_BTOC_OPEN]; + bool cxt_loaded; + bool packed_ok; + u32 cxt_blocks_seen; + u32 cxt_records_seen; + u32 cxt_l2v_updates; + u32 btoc_pages_read; + u32 btoc_pages_valid; + u32 btoc_entries_seen; + u32 btoc_l2v_updates; + u32 btoc_token_ffff0000; + u32 btoc_token_ffffff00; + u32 btoc_token_ffffffff; + u32 btoc_holelist_ffff0001; + u32 open_slots_seen; + u32 open_slots_valid_meta; + u32 open_l2v_updates; + u32 l2v_update_calls; + u32 l2v_unmap_calls; + u32 l2v_repack_roots; + u32 meta0_hits; + u32 btoc_dumps_left; +}; + +struct whimory_cxt_base { + u32 sb; + u64 weave; +}; + +struct whimory_fpart_ops { + u32 major; + u32 (*minor)(struct whimory *w); + int (*init)(struct whimory *w); + int (*read_special)(struct whimory *w, u32 type, u8 *buf, size_t len); + int (*read_signature)(struct whimory *w, u8 *buf, size_t len); +}; + +struct whimory_vfl_ops { + u32 major; + u32 (*minor)(struct whimory *w); + int (*init)(struct whimory *w); + int (*open)(struct whimory *w); + u32 (*get_param)(struct whimory *w, u32 selector); + int (*read_vba)(struct whimory *w, u32 vba, u32 count, + void *data, struct whimory_meta *meta); +}; + +struct whimory_ftl_ops { + u32 major; + u32 (*minor)(struct whimory *w); + int (*init)(struct whimory *w); + int (*open)(struct whimory *w); + int (*read_lba)(struct whimory *w, u32 lba, void *buf, bool allow_blank); +}; + +struct whimory { + struct device *dev; + struct whimory_geometry geom; + struct whimory_signature sig; + struct whimory_l2v l2v; + struct whimory_vfl vfl; + struct whimory_sftl sftl; + struct rb_root ranges; + struct whimory_fpart fpart_ctx; + const struct whimory_fpart_ops *fpart; + const struct whimory_vfl_ops *vfl_ops; + const struct whimory_ftl_ops *ftl; + u64 total_4k_sectors; + u8 *bounce; + struct mutex bounce_lock; + struct mutex tree_lock; + struct gendisk *disk; + struct gendisk *ipod_disk; + struct platform_device *pdev; + u32 lba0_vba; + u64 cxt_base_weave; + struct whimory_cxt_base cxt[WHIMORY_CXT_MAX_SB]; + u32 n_cxt; + u32 cxt_next_lba; + bool cxt_lba_valid; + bool fil_ok; + bool sig_ok; + bool vfl_ok; + bool ftl_ok; + bool l2v_ok; + bool lba0_ok; + bool oracle_used; + char status[512]; +}; + +static inline u32 whimory_sig32(const u8 *sig, unsigned int off) +{ + return get_unaligned_le32(sig + off); +} + +#endif /* WHIMORY_S5L8740_H */ diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index b380793f76f2c2..a6fd71134174e5 100755 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -11,9 +11,11 @@ config SND_SOC_APPLE_NANO7 tristate "iPod nano 7G audio machine" depends on SND_SOC select SND_SOC_APPLE_S5L8740_I2S + select SND_SOC_APPLE_S5L8740_IIS2 select SND_SOC_APPLE_CS42L81_SPI help - Registers ASoC card: S5L8740 IIS0 CPU DAI + CS42L81 SPI codec. + Registers ASoC card: S5L8740 IIS0+CS42 playback and optional + IIS2 FM capture (local speakers/HP only; no FM→BT). config SND_SOC_APPLE_S5L8740_I2S tristate "S5L8740 IIS0 I2S CPU DAI" @@ -23,6 +25,15 @@ config SND_SOC_APPLE_S5L8740_I2S help IIS0 @0x3CA00000 CPU DAI with optional PL080 dmaengine PCM. +config SND_SOC_APPLE_S5L8740_IIS2 + tristate "S5L8740 IIS2 FM capture CPU DAI" + depends on SND_SOC && HAS_IOMEM + select SND_SOC_GENERIC_DMAENGINE_PCM + select SND_DMAENGINE_PCM + help + IIS2 @0x3D400000 FM digital RX (peri 13 ← FIFO +0x38). Oracle + register program from RetailOS fm-playing MMIO. Local PCM only. + config SND_SOC_APPLE_CS42L81_SPI tristate "CS42L81 / 338S1146 SPI codec (N31)" depends on SPI && SND_SOC diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index f824672cde46ad..d4dc2d1bc173ce 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -1,6 +1,7 @@ -snd-soc-apple-mca-y := mca.o - -obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o -obj-$(CONFIG_SND_SOC_APPLE_NANO7) += nano7-audio.o -obj-$(CONFIG_SND_SOC_APPLE_CS42L81_SPI) += cs42l81-spi.o -obj-$(CONFIG_SND_SOC_APPLE_S5L8740_I2S) += s5l8740-i2s.o +snd-soc-apple-mca-y := mca.o + +obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o +obj-$(CONFIG_SND_SOC_APPLE_NANO7) += nano7-audio.o +obj-$(CONFIG_SND_SOC_APPLE_CS42L81_SPI) += cs42l81-spi.o +obj-$(CONFIG_SND_SOC_APPLE_S5L8740_I2S) += s5l8740-i2s.o +obj-$(CONFIG_SND_SOC_APPLE_S5L8740_IIS2) += s5l8740-iis2.o diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c index ca35e915e031f9..3014bc5583744e 100755 --- a/sound/soc/apple/cs42l81-spi.c +++ b/sound/soc/apple/cs42l81-spi.c @@ -16,9 +16,17 @@ * * 0x403/0x404 are HP mixer tap indices from 174E7C / 440AA4(udiv, 160): * play 5706F4(1): L=(0+159)/160+2 = 2, R=+1 = 1 + * play 5706F4(4): L=(160+159)/160+2 = 3, R=+1 = 2 * Analog 0x527 is mute 0xFF / unmute 0x60 (F141C). Do not unlock 9901 * after mixer. * + * Play graph: RetailOS sub_570620. If 8A8FB58 is set → sub_5707D8 static + * blast; else dynamic 5706F4 → 174E7C → 174E38 → 165BD4 → 0x401 latch. + * Do not assume tap index == mixer slot index. + * + * 10B4EA / 1042FC are RTOS SVC domain 33 (enter/exit), NOT a CS42 SPI page. + * 43DF0A / 43DF04 are domain 24 around SPI. Linux: c->lock + logged no-op. + * * ASoC DAI cs42l81-hifi: analog on hw_params. IIS serializer is the CPU DAI. * * CS42L42/L83 (I2C, paged 8-bit regmap, snd_soc_cs42l42) are a newer Cirrus @@ -28,42 +36,272 @@ * * Cirrus bring-up notes: * Reset mutes outputs (0x527=0xFF); unmute 0x60 on play. - * LOS: BCLK/LRCK stop clears 0x2F bit6 — asp_lock after IIS kick. + * 0x2F bit6: glass shows 0x40 idle (no IIS), 0x00 while BCLK/LRCLK run. + * Treat bit6 as LOS / no-sync when asp_bit6_is_los=1 (default): asp_lock + * succeeds when bit6 is CLEAR. Legacy asp_bit6_is_los=0 waits for bit6 set. * ASP lock after IIS clocks (414FAE), not before. * I2S slave NB_NF 16-bit; no DAPM graph — path is register audio_on(). + * + * ARTP routes (CoreAudio debug: "BT-%s, HP-%s, USB-%s, MB-%s"): + * HP = headphone jack (CS42 IIS0). MB = mainboard/dock internal route, + * not MikeyBus and not a feed into the 3.5 mm jack. USB/BT are separate + * digital paths (Lightning/Tristar, BCM2078 A2DP). N31 Linux: HP only. + * + * Play/stop: sub_42D364(1)=F141C(1)+570620; stop=42D364(0). + * + * HPDET / jack (p5-hpdet): OPEN — no CONFIRMED_N31 GPIO ID or plug status + * bit. Family schematics show an HPDET pin; N31 RESET/IRQ/HPDET GPIOs are + * explicitly unmapped. Do not wire ALSA Jack/Switch or mute-on-unplug until + * glass diffs close docs/N31-HPDET-OPEN.md. Headphones plugged on glass = + * detect=1 baseline (N31-GLASS-AUDIO-CAPTURE.md). */ +#include #include +#include #include #include #include #include #include #include +#include #include +#include #include #include #include #include +#include "n31-audio-rates.h" + #define CS42L81_USER_VOL_MAX 256 -#define CS42L81_MIX_TAP_L 2 /* 174E7C play */ -#define CS42L81_MIX_TAP_R 1 +#define CS42L81_VOL_STEP 16 /* ~16 presses full 0..256 range */ + +/* + * Play rate for D34C0/183138. 0 = follow PCM hw_params (OSOS 44100 if none). + * RetailOS local music is 44100. Do not force 48000. + */ +static unsigned int play_rate; +module_param(play_rate, uint, 0644); +MODULE_PARM_DESC(play_rate, "CS42 D34C0 rate; 0=follow PCM (default, OSOS 44100)"); + +/* + * graph_mode: RetailOS sub_570620 play-graph selector. + * 0 = static sub_5707D8 (DEFAULT — music path when 8A8FB58 non-NULL) + * 1 / 3 / 4 = dynamic 5706F4→174E7C→174E38→165BD4 + * + * RE (osos.dec.bin): BSS at 0x892A05D/05E/060/061 and dword table at + * 0x892A068 have ZERO writers and ZERO ROM init. 174E7C therefore always + * sees table_v=0 and idx=0 → accum_l/r=0. Mode 4 cannot grow non-zero + * slot gains from firmware recovery; inventing table words would not be + * RetailOS. Music uses the hardcoded 5707D8 image (gains 01 E0). + */ +static int graph_mode; +module_param(graph_mode, int, 0644); +MODULE_PARM_DESC(graph_mode, + "CS42 play graph: 0=static 5707D8 (default); 1/3/4=dynamic"); + +/* -1 = use computed 174E7C taps; else force 0x403/0x404 after compute. */ +static int graph_tap_l_override = -1; +static int graph_tap_r_override = -1; +module_param(graph_tap_l_override, int, 0644); +module_param(graph_tap_r_override, int, 0644); +MODULE_PARM_DESC(graph_tap_l_override, "Override 0x403 tap (-1=computed)"); +MODULE_PARM_DESC(graph_tap_r_override, "Override 0x404 tap (-1=computed)"); + +/* + * Debug only: after dynamic/static graph, force slot at 0x410 to 02 01 E0. + * Default off — tap!=slot; do not use as the primary fix. + */ +static bool graph_slot2_gain; +module_param(graph_slot2_gain, bool, 0644); +MODULE_PARM_DESC(graph_slot2_gain, + "DEBUG: bake 0x410=02 01 E0 after graph (default 0)"); + +/* + * 174E7C table word at 0x892A068[idx_l_hi]. Recovered value is 0 (BSS). + * Module param kept only for deliberate experiments — not a RetailOS value. + */ +static unsigned int graph_table_v; /* RE: always 0 */ +module_param(graph_table_v, uint, 0644); +MODULE_PARM_DESC(graph_table_v, + "174E7C table[idx] override (RE default 0; do not invent)"); +/* + * audio_route: 0=HP jack (default). USB/BT/MB are CoreAudio ARTP names for + * alternate sinks — no RE-backed CS42 mux to HP on N31 yet. + */ +static int audio_route; +module_param(audio_route, int, 0644); +MODULE_PARM_DESC(audio_route, "0=HP jack (default); USB/BT/MB unimplemented"); + +/* 570620 gates on 0x8925CF4==1 (headset ready). */ +static bool force_headset; +module_param(force_headset, bool, 0644); +MODULE_PARM_DESC(force_headset, "1=skip headset-ready gate (glass bring-up)"); + +static unsigned int jack_poll_ms = 500; +module_param(jack_poll_ms, uint, 0644); +MODULE_PARM_DESC(jack_poll_ms, "MikeyBus/HSDET poll period ms (0=off)"); + +/* + * audio_path_mode (i2s trigger also reads via cs42l81_get_audio_path_mode): + * 0 = legacy: play graph folded into codec prepare (debug only) + * 1 = RetailOS: F141C+570620 before DMA/TXCOM (default) + * 2 = RetailOS: DMA/TXCOM before F141C+570620 + */ +static int audio_path_mode = 1; +module_param(audio_path_mode, int, 0644); +MODULE_PARM_DESC(audio_path_mode, "0=legacy soup; 1=play before IIS; 2=play after IIS"); + +/* 1 = redo D3280 prepare on every play_prepare (no stale brought_up). */ +static bool force_full_prepare = true; +module_param(force_full_prepare, bool, 0644); +MODULE_PARM_DESC(force_full_prepare, "1=re-run codec prepare every session (default)"); + +/* + * RetailOS sub_10B4EA → SVC domain 33 enter; sub_1042FC → domain 33 exit. + * NOT a CS42 SPI page. Linux: log + marker under c->lock (already held). + * Legacy alias allow_no_page33 kept so old glass scripts still load. + */ +static bool allow_no_page33 = true; +module_param(allow_no_page33, bool, 0644); +MODULE_PARM_DESC(allow_no_page33, + "legacy alias; domain 33 is always a no-op (never blocks graph)"); + +static bool dump_regs; +module_param(dump_regs, bool, 0644); +MODULE_PARM_DESC(dump_regs, "1=verbose CS42 FINAL + 5707D8 verify dumps"); + +/* + * post_iis_401_rmw=1 (default): force 401&3=2 after ASP (legacy glass). + * =0: leave static 5707D8 latch 401=0x12 alone (A/B if silent). + */ +static bool post_iis_401_rmw = true; +module_param(post_iis_401_rmw, bool, 0644); +MODULE_PARM_DESC(post_iis_401_rmw, "1=post_iis 401&3=2 (default); 0=keep graph 0x12"); + +/* D3280(4) RE writes C96F=0x0E. Glass sometimes needed 0x1E — A/B. */ +static int c96f_final = 0x0e; +module_param(c96f_final, int, 0644); +MODULE_PARM_DESC(c96f_final, "D3280(4) final 0xC96F (default 0x0E RE; try 0x1E)"); + +/* Dynamic 570620: force 41F944 gate pass (route_present=1, busy=0). */ +static bool graph_force_gate = true; +module_param(graph_force_gate, bool, 0644); +MODULE_PARM_DESC(graph_force_gate, "1=force 570620 gate pass (default bring-up)"); + +/* + * Recovered OSOS graph BSS layout at 0x892A05C (Thumb base for 174E7C / + * 165BD4 / 5706F4). All idx_* and table[] are never stored in OSOS — left + * zero. Only 5706F4 writes count_l/r and base_l/r. + */ +static const u32 cs42_osos_graph_table_892a068[] = { + /* table[0] at 0x892A068 — only entry 174E7C can hit with idx_hi==0 */ + 0x00000000, +}; + +struct cs42_graph_state { + u8 count_l; + u8 count_r; + u8 idx_l_lo; /* 0x892A05D */ + u8 idx_l_hi; /* 0x892A05E */ + u8 idx_r_lo; /* 0x892A060 */ + u8 idx_r_hi; /* 0x892A061 */ + u16 base_l; /* 0x892A062 */ + u16 base_r; /* 0x892A064 */ + u16 accum_l; /* 0x8AE4E50 */ + u16 accum_r; /* 0x8AE4E54 */ + u8 tap_l; /* 0x8AE4E4C → 0x403 */ + u8 tap_r; /* 0x8AE4E4D → 0x404 */ + u8 status_528; + int mode; +}; struct cs42l81 { struct spi_device *spi; struct mutex lock; unsigned int user_vol; + unsigned int rate; /* last D34C0 rate; ASP reprogram uses this */ bool dai_mute; + bool input_handler_reg; + struct snd_soc_component *component; + struct input_handler input_handler; + struct work_struct vol_work; + struct delayed_work asp_post_work; + struct delayed_work jack_work; + atomic_t vol_steps; + bool jack_poll_active; + bool jack_last_present; + bool route_playing; /* 892A058 mirror */ + bool codec_prepared; + bool play_started; + int graph_domain; /* RetailOS SVC domain 33 held (not SPI page) */ + struct cs42_graph_state graph; +}; + +struct cs42_regval { + u16 reg; + u8 val; }; static struct cs42l81 *cs42l81_dev; +static unsigned int cs42_pick_rate(struct cs42l81 *c, unsigned int rate) +{ + if (play_rate) + return n31_pick_rate(play_rate); + if (rate) + return n31_pick_rate(rate); + if (c && c->rate) + return n31_pick_rate(c->rate); + return N31_RATE_DEFAULT; +} + +/* + * 0x2F bit6 polarity: glass idle (no IIS) reads 0x40; running IIS reads 0x00. + * Default asp_bit6_is_los=1 → bit6 set = loss/no-sync, clear = ASP locked. + */ +static bool asp_bit6_is_los = true; +module_param(asp_bit6_is_los, bool, 0644); +MODULE_PARM_DESC(asp_bit6_is_los, + "1=bit6 is LOS flag (clear=synced); 0=legacy bit6-set=synced"); + +/* + * asp_gate_unmute=0 (default): post_iis always forces HP unmute; 0x2F is telemetry. + * asp_gate_unmute=1: legacy — unmute only when asp probe reports synced. + */ +static bool asp_gate_unmute; +module_param(asp_gate_unmute, bool, 0644); +MODULE_PARM_DESC(asp_gate_unmute, "1=gate unmute on 0x2F probe; 0=force unmute (default)"); + +/* + * of_asp_slave=1: clear 0x0F bit7 before IIS (CS42L73-family ASP slave test). + * Default 0 keeps RetailOS D3280(3) pad-drive write. + */ +static bool of_asp_slave; +module_param(of_asp_slave, bool, 0644); +MODULE_PARM_DESC(of_asp_slave, "1=force CS42 0x0F bit7=0 (ASP slave) before IIS"); + +static bool cs42l81_asp_synced(u8 r2f) +{ + if (asp_bit6_is_los) + return !(r2f & 0x40); + return !!(r2f & 0x40); +} + int cs42l81_post_iis_start(void); +int cs42l81_play_stop(void); +int cs42l81_play_start(void); int cs42l81_play_prepare(void); +int cs42l81_pre_iis_start(void); +int cs42l81_get_audio_path_mode(void); +void cs42l81_schedule_post_iis(void); +void cs42l81_cancel_post_iis(void); static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val); -static int cs42l81_set_mute(struct cs42l81 *c, int mute); static int cs42l81_apply_user_vol(struct cs42l81 *c); +static void cs42l81_log_start_state(struct cs42l81 *c, const char *tag); +static void cs42l81_push_pcm_q8(unsigned int vol); static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val) { @@ -140,172 +378,1240 @@ static int cs42l81_bringup(struct cs42l81 *c) return 0; } -/* sub_5707D8 ASP/mixer blast. Bytes from Hex-Rays, not invented. */ -static const u8 cs42l81_mix400[] = { - 0x04, 0x10, 0x00, 0x09, 0x08, 0x00, 0x00, 0x00, - 0x01, 0xe0, 0x01, 0x01, 0xe0, 0xfe, 0x00, 0xa0, - 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x04, 0x00, - 0x00, 0x05, 0x00, 0x00, 0x06, 0x00, 0x00, 0x07, - 0x00, 0x00, 0x08, 0x00, 0x00, 0x09, 0x00, 0x00, - 0x0a, 0x01, 0xe0, 0x0b, 0x01, 0xe0, 0xff, 0x00, - 0xa0, 0x0c, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x0e, - 0x00, 0x00, 0x0f, 0x00, 0x00, 0x10, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x12, 0x00, 0x00, 0x13, 0x00, - 0x00, +/* + * RetailOS sub_5707D8 — literal SPI table (Hex-Rays). Not a loop guess. + * Preceded by 0x006/529/52A/533/534; ends with msleep(100)+0x500+read 528. + */ +static const struct cs42_regval cs42_static_5707d8[] = { + { 0x0006, 0x24 }, + { 0x0529, 0x2c }, + { 0x052a, 0x2c }, + { 0x0533, 0x2c }, + { 0x0534, 0x2c }, + + { 0x0400, 0x04 }, + { 0x0401, 0x10 }, + { 0x0402, 0x00 }, + { 0x0403, 0x09 }, + { 0x0404, 0x08 }, + { 0x0405, 0x00 }, + { 0x0406, 0x00 }, + + { 0x0407, 0x00 }, { 0x0408, 0x01 }, { 0x0409, 0xe0 }, + { 0x040a, 0x01 }, { 0x040b, 0x01 }, { 0x040c, 0xe0 }, + { 0x040d, 0xfe }, { 0x040e, 0x00 }, { 0x040f, 0xa0 }, + + { 0x0410, 0x02 }, { 0x0411, 0x00 }, { 0x0412, 0x00 }, + { 0x0413, 0x03 }, { 0x0414, 0x00 }, { 0x0415, 0x00 }, + { 0x0416, 0x04 }, { 0x0417, 0x00 }, { 0x0418, 0x00 }, + { 0x0419, 0x05 }, { 0x041a, 0x00 }, { 0x041b, 0x00 }, + { 0x041c, 0x06 }, { 0x041d, 0x00 }, { 0x041e, 0x00 }, + { 0x041f, 0x07 }, { 0x0420, 0x00 }, { 0x0421, 0x00 }, + { 0x0422, 0x08 }, { 0x0423, 0x00 }, { 0x0424, 0x00 }, + { 0x0425, 0x09 }, { 0x0426, 0x00 }, { 0x0427, 0x00 }, + + { 0x0428, 0x0a }, { 0x0429, 0x01 }, { 0x042a, 0xe0 }, + { 0x042b, 0x0b }, { 0x042c, 0x01 }, { 0x042d, 0xe0 }, + { 0x042e, 0xff }, { 0x042f, 0x00 }, { 0x0430, 0xa0 }, + + { 0x0431, 0x0c }, { 0x0432, 0x00 }, { 0x0433, 0x00 }, + { 0x0434, 0x0d }, { 0x0435, 0x00 }, { 0x0436, 0x00 }, + { 0x0437, 0x0e }, { 0x0438, 0x00 }, { 0x0439, 0x00 }, + { 0x043a, 0x0f }, { 0x043b, 0x00 }, { 0x043c, 0x00 }, + { 0x043d, 0x10 }, { 0x043e, 0x00 }, { 0x043f, 0x00 }, + { 0x0440, 0x11 }, { 0x0441, 0x00 }, { 0x0442, 0x00 }, + { 0x0443, 0x12 }, { 0x0444, 0x00 }, { 0x0445, 0x00 }, + { 0x0446, 0x13 }, { 0x0447, 0x00 }, { 0x0448, 0x00 }, + + { 0x0400, 0x04 }, + { 0x0401, 0x12 }, }; -/* D3280(4) + 400330 2v5 + 183138(48k) + D3280(3) HP + 5707D8. */ -static int cs42l81_audio_on(struct cs42l81 *c) +/* + * RetailOS RTOS domains (SVC 0x46) — NOT CS42 hardware pages. + * 10B4EA / 43DD18(33) = graph critical-section enter + * 1042FC / 43DDA0(33) = graph critical-section exit/status + * 43DF0A / 43DD18(24) = SPI critical-section enter + * 43DF04 / 43DDA0(24) = SPI critical-section exit + * Linux: caller already holds c->lock; these are markers + logs only. + */ +static void cs42_domain33_enter(struct cs42l81 *c) +{ + if (c->graph_domain == 33) + return; + c->graph_domain = 33; + (void)allow_no_page33; /* legacy param; domain 33 never blocks */ + dev_info_once(&c->spi->dev, + "RetailOS domain 33 enter (10B4EA→SVC 0x46; Linux no-op under lock)\n"); + if (dump_regs) + dev_info(&c->spi->dev, "domain33 enter\n"); +} + +static void cs42_domain33_exit(struct cs42l81 *c) +{ + if (c->graph_domain != 33) + return; + c->graph_domain = 0; + if (dump_regs) + dev_info(&c->spi->dev, "domain33 exit (1042FC)\n"); +} + +static int cs42_graph_begin(struct cs42l81 *c, int page) +{ + /* page arg kept for call-site clarity; only 33 is used by RE. */ + if (page != 33 && dump_regs) + dev_info(&c->spi->dev, "domain enter id=%d (expected 33)\n", page); + cs42_domain33_enter(c); + return 0; +} + +static int cs42_graph_end(struct cs42l81 *c, int page) +{ + (void)page; + cs42_domain33_exit(c); + return 0; +} + +static int cs42_write_table(struct cs42l81 *c, const struct cs42_regval *t, + unsigned int n) { - u8 st = 0, r219 = 0; unsigned int i; int ret; - /* sub_D3280(a1==4) */ - cs42l81_rmw(c, 0x0007, 0x40, 0x00); - cs42l81_rmw(c, 0x0219, 0x78, 0x78); - cs42l81_write(c, 0x0229, 0x40); - cs42l81_rmw(c, 0x0006, 0x01, 0x00); - cs42l81_rmw(c, 0x0201, 0xe0, 0x40); - cs42l81_write(c, 0xc81f, 0xff); - cs42l81_write(c, 0xc85f, 0x0f); - ret = cs42l81_write(c, 0xc96f, 0x0e); + for (i = 0; i < n; i++) { + ret = cs42l81_write(c, t[i].reg, t[i].val); + if (ret) + return ret; + } + return 0; +} + +/* Read back last-write-wins expected values for 0x400..0x448 + key regs. */ +static void cs42_verify_5707d8(struct cs42l81 *c) +{ + static const struct cs42_regval expect[] = { + { 0x0006, 0x24 }, + { 0x0529, 0x2c }, { 0x052a, 0x2c }, + { 0x0533, 0x2c }, { 0x0534, 0x2c }, + { 0x0400, 0x04 }, { 0x0401, 0x12 }, + { 0x0402, 0x00 }, { 0x0403, 0x09 }, { 0x0404, 0x08 }, + { 0x0405, 0x00 }, { 0x0406, 0x00 }, + { 0x0407, 0x00 }, { 0x0408, 0x01 }, { 0x0409, 0xe0 }, + { 0x040a, 0x01 }, { 0x040b, 0x01 }, { 0x040c, 0xe0 }, + { 0x0428, 0x0a }, { 0x0429, 0x01 }, { 0x042a, 0xe0 }, + { 0x0500, 0x05 }, + }; + unsigned int i, mism = 0; + u8 v; + + for (i = 0; i < ARRAY_SIZE(expect); i++) { + if (cs42l81_read(c, expect[i].reg, &v)) + continue; + if (v != expect[i].val) { + dev_warn(&c->spi->dev, + "5707D8 MISMATCH 0x%03x got=%02x want=%02x\n", + expect[i].reg, v, expect[i].val); + mism++; + } + } + dev_info(&c->spi->dev, "5707D8 verify: %u mismatches (0=good)\n", mism); +} + +static void cs42_log_final_state(struct cs42l81 *c, const char *tag) +{ + static const u16 regs[] = { + 0x006, 0x007, 0x00e, 0x00f, + 0x075, 0x074, 0x07b, 0x07c, + 0x201, 0x203, 0x204, 0x205, 0x206, 0x207, + 0x219, 0x220, 0x223, 0x224, 0x225, 0x227, 0x229, + 0x400, 0x401, 0x402, 0x403, 0x404, 0x405, 0x406, + 0x500, 0x527, 0x528, 0x54f, + 0xc81f, 0xc85f, 0xc96f, + }; + char line[128]; + unsigned int i, n = 0; + u8 v; + + dev_info(&c->spi->dev, "CS42 FINAL %s:\n", tag); + for (i = 0; i < ARRAY_SIZE(regs); i++) { + if (cs42l81_read(c, regs[i], &v)) + continue; + n += scnprintf(line + n, sizeof(line) - n, "%03x=%02x ", + regs[i], v); + if (n > 90 || i + 1 == ARRAY_SIZE(regs)) { + dev_info(&c->spi->dev, " %s\n", line); + n = 0; + line[0] = '\0'; + } + } +} + +/* OSOS sub_5706F4(mode) — route counts / bases into graph state. */ +static int cs42_5706f4_route_state(struct cs42l81 *c, int mode) +{ + struct cs42_graph_state *g = &c->graph; + + memset(g, 0, sizeof(*g)); + g->mode = mode; + + switch (mode) { + case 1: + g->count_l = 0; + g->count_r = 0; + g->base_l = 0; + g->base_r = 0; + break; + case 3: + g->count_l = 0; + g->count_r = 0; + g->base_l = 160; + g->base_r = 160; + break; + case 4: + g->count_l = 2; + g->count_r = 0; + g->base_l = 160; + g->base_r = 160; + break; + default: + dev_err(&c->spi->dev, "unsupported RE graph mode %d\n", mode); + return -EINVAL; + } + + /* idx_* not assigned in visible 5706F4; BSS-style zero + log. */ + dev_info(&c->spi->dev, + "CS42 5706F4: mode=%d count_l=%u count_r=%u base_l=%u base_r=%u\n", + mode, g->count_l, g->count_r, g->base_l, g->base_r); + return 0; +} + +/* OSOS sub_440AA4(a,b) — plain unsigned divide for tap math. */ +static u32 cs42_440aa4_udiv(u32 num, u32 den) +{ + if (!den) + return 0; + return num / den; +} + +/* OSOS sub_174E7C — compute tap_l / tap_r (+ optional accum). */ +static int cs42_174e7c_compute_taps(struct cs42l81 *c) +{ + struct cs42_graph_state *g = &c->graph; + u32 table_v; + + /* + * OSOS: v0 = *(u32 *)(0x892A068 + 4 * idx_l_hi). + * Recovered: table[0]==0 and idx_hi never written → always 0. + * graph_table_v overrides only for lab experiments. + */ + if (graph_table_v) + table_v = graph_table_v; + else if (g->idx_l_hi < ARRAY_SIZE(cs42_osos_graph_table_892a068)) + table_v = cs42_osos_graph_table_892a068[g->idx_l_hi]; + else + table_v = 0; + + if (g->count_l) + g->accum_l = (g->idx_l_lo + 1) * table_v; + g->tap_l = cs42_440aa4_udiv(g->base_l + g->accum_l * g->count_l + 159, + 160) + 2; + + if (g->count_r) + g->accum_r = (g->idx_r_lo + 1) * table_v; + g->tap_r = cs42_440aa4_udiv(g->base_r + g->accum_r * g->count_r + 159, + 160) + 1; + + if (graph_tap_l_override >= 0 && graph_tap_l_override <= 0xff) + g->tap_l = graph_tap_l_override; + if (graph_tap_r_override >= 0 && graph_tap_r_override <= 0xff) + g->tap_r = graph_tap_r_override; + + dev_info(&c->spi->dev, + "CS42 174E7C: tap_l=%u tap_r=%u accum_l=%u accum_r=%u table_v=%u (RE BSS)\n", + g->tap_l, g->tap_r, g->accum_l, g->accum_r, table_v); + return 0; +} + +/* OSOS sub_174E38(side) — program 0x529+ / 0x533+ from idx nibbles. */ +static int cs42_174e38_program_range(struct cs42l81 *c, int side) +{ + struct cs42_graph_state *g = &c->graph; + u16 reg; + u8 count; + u8 val; + int i, ret; + + if (side) { + reg = 0x533; + count = g->count_r; + val = g->idx_r_hi | (g->idx_r_lo << 4); + } else { + reg = 0x529; + count = g->count_l; + val = g->idx_l_hi | (g->idx_l_lo << 4); + } + + for (i = 0; i < count; i++) { + ret = cs42l81_rmw(c, reg + i, 0x3f, val); + if (ret) + return ret; + } + + dev_info(&c->spi->dev, + "CS42 174E38 side=%d reg=0x%03x count=%u val=0x%02x\n", + side, reg, count, val); + return 0; +} + +static int cs42_write_slot3(struct cs42l81 *c, u16 reg, u8 a, u8 b, u8 cbyte) +{ + int ret; + + ret = cs42l81_write(c, reg + 0, a); if (ret) return ret; - cs42l81_write(c, 0x0223, 0x08); - cs42l81_write(c, 0x0224, 0x09); - cs42l81_write(c, 0x0225, 0x00); + ret = cs42l81_write(c, reg + 1, b); + if (ret) + return ret; + return cs42l81_write(c, reg + 2, cbyte); +} - /* sub_400330: 2.5V backpower — RetailOS WRITES 0x219 */ - cs42l81_rmw(c, 0x0219, 0x07, 0x01); - msleep(100); - cs42l81_write(c, 0xc96f, 0x1e); - /* Glass: 5-byte write left 0x2F=0x00. write6 left 0x2F=0x80 (off). */ - cs42l81_write(c, 0x0227, 0x40); - - /* sub_183138 48 kHz (v10=12) */ - cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); - cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); - cs42l81_write(c, 0x012f, 0xcc); - cs42l81_write(c, 0x010b, 0x08); - cs42l81_write(c, 0x010c, 0x09); - cs42l81_rmw(c, 0x0131, 0x01, 0x01); - cs42l81_rmw(c, 0x000e, 0xc0, 0x40); - cs42l81_rmw(c, 0x0220, 0x20, 0x20); +/* OSOS sub_165BD4(side) — dynamic 11-slot triple writer. */ +static int cs42_165bd4_build_slots(struct cs42l81 *c, int side) +{ + struct cs42_graph_state *g = &c->graph; + u8 count; + u8 source; + u16 base_reg; + u16 base_val; + u16 gain; + u8 term; + int i, ret; + + if (side) { + count = g->count_r; + source = 10; + base_reg = 0x428; + base_val = g->base_r; + gain = g->accum_r; + term = 0xff; + } else { + count = g->count_l; + source = 0; + base_reg = 0x407; + base_val = g->base_l; + gain = g->accum_l; + term = 0xfe; + } + + for (i = 0; i < 11; i++) { + u16 reg = base_reg + 3 * i; + u8 a, b, cc; + + if (i < count) { + a = source++; + b = gain >> 8; + cc = gain & 0xff; + } else if (i == count && base_val) { + a = term; + b = base_val >> 8; + cc = base_val & 0xff; + } else if (i == 10 && !base_val) { + a = term; + b = 0; + cc = 0; + } else { + a = source++; + b = 0; + cc = 0; + } + + ret = cs42_write_slot3(c, reg, a, b, cc); + if (ret) + return ret; + } + + dev_info(&c->spi->dev, + "CS42 165BD4 side=%d base=0x%03x count=%u base_val=%u gain=%u\n", + side, base_reg, count, base_val, gain); + return 0; +} + +static void cs42_log_graph_snapshot(struct cs42l81 *c, const char *tag) +{ + struct cs42_graph_state *g = &c->graph; + u8 v; + u16 r; + char line[96]; + int n, i; + + if (!dump_regs) + return; + + dev_info(&c->spi->dev, + "CS42 GRAPH %s: mode=%d tap_l=%u tap_r=%u status528=%02x\n", + tag, g->mode, g->tap_l, g->tap_r, g->status_528); + + n = 0; + for (r = 0x400; r <= 0x406; r++) { + if (cs42l81_read(c, r, &v)) + break; + n += scnprintf(line + n, sizeof(line) - n, "%03x=%02x ", r, v); + } + dev_info(&c->spi->dev, "CS42 GRAPH hdr: %s\n", line); + + for (i = 0; i < 22; i++) { + u16 base = 0x407 + 3 * i; + + if (cs42l81_read(c, base, &v)) + break; + n = scnprintf(line, sizeof(line), "%03x:", base); + n += scnprintf(line + n, sizeof(line) - n, " %02x", v); + if (!cs42l81_read(c, base + 1, &v)) + n += scnprintf(line + n, sizeof(line) - n, " %02x", v); + if (!cs42l81_read(c, base + 2, &v)) + n += scnprintf(line + n, sizeof(line) - n, " %02x", v); + dev_info(&c->spi->dev, "CS42 GRAPH slot%02d %s\n", i, line); + } + + n = 0; + for (r = 0x529; r <= 0x534; r++) { + if (cs42l81_read(c, r, &v)) + break; + n += scnprintf(line + n, sizeof(line) - n, "%03x=%02x ", r, v); + } + dev_info(&c->spi->dev, "CS42 GRAPH 529..: %s\n", line); + + if (!cs42l81_read(c, 0x54f, &v)) + dev_info(&c->spi->dev, "CS42 GRAPH 54f=%02x\n", v); +} + +/* RetailOS sub_5707D8 — exact static graph (literal table + verify). */ +static int cs42_build_play_graph_static(struct cs42l81 *c) +{ + int ret; + + ret = cs42_graph_begin(c, 33); + if (ret) + return ret; + + ret = cs42_write_table(c, cs42_static_5707d8, + ARRAY_SIZE(cs42_static_5707d8)); + if (ret) + goto out; + + msleep(100); /* sub_43E006(100) → RTOS sleep */ + ret = cs42l81_write(c, 0x0500, 0x05); + if (ret) + goto out; + cs42l81_read(c, 0x0528, &c->graph.status_528); + c->graph.mode = 0; + c->graph.tap_l = 0x09; + c->graph.tap_r = 0x08; + + cs42_verify_5707d8(c); + cs42_log_graph_snapshot(c, "post_5707D8"); + +out: + cs42_graph_end(c, 33); + if (!ret) + dev_info(&c->spi->dev, + "CS42 5707D8 static exact: taps=9/8 status528=%02x\n", + c->graph.status_528); + return ret; +} + +/* + * RetailOS sub_570620 dynamic branch (8A8FB58 == NULL): + * 10B4EA → 5706F4 → 174E7C → 174E38×2 → 54F/401/402..406 → 165BD4×2 + * → 401 bit1=2 → sleep 100 → read 528 → 1042FC + */ +static int cs42_build_play_graph_retailos(struct cs42l81 *c, int route_mode) +{ + int ret; + + if (route_mode == 0) + return cs42_build_play_graph_static(c); + + dev_info(&c->spi->dev, + "CS42 570620 dynamic mode=%d (BSS table/idx=0) force_gate=%d\n", + route_mode, graph_force_gate); - /* sub_5707D8 */ - cs42l81_write(c, 0x0006, 0x24); - cs42l81_write(c, 0x0529, 0x2c); - cs42l81_write(c, 0x052a, 0x2c); - cs42l81_write(c, 0x0533, 0x2c); - cs42l81_write(c, 0x0534, 0x2c); - for (i = 0; i < ARRAY_SIZE(cs42l81_mix400); i++) - cs42l81_write(c, 0x0400 + i, cs42l81_mix400[i]); - cs42l81_write(c, 0x0400, 0x04); - cs42l81_write(c, 0x0401, 0x12); /* - * 570620(1) → 174E7C: 0x403/0x404 are mixer tap indices, not - * the 0–256 user volume and not a saturate-at-0xA0 gain. - * 440AA4 is unsigned divide by 160. + * RetailOS 41F944 gate: if route_present!=1 or busy, 570620 calls + * 42D364(0) and returns 17. Bring-up forces pass. */ - cs42l81_write(c, 0x0402, 0x00); - cs42l81_write(c, 0x0403, CS42L81_MIX_TAP_L); - cs42l81_write(c, 0x0404, CS42L81_MIX_TAP_R); - cs42l81_write(c, 0x0405, 0x00); - cs42l81_write(c, 0x0406, 0x00); + if (!graph_force_gate) { + dev_warn(&c->spi->dev, + "graph_force_gate=0 — dynamic path may tear down via 42D364(0)\n"); + } + + ret = cs42_graph_begin(c, 33); + if (ret) + return ret; + + ret = cs42_5706f4_route_state(c, route_mode); + if (ret) + return ret; + + ret = cs42_174e7c_compute_taps(c); + if (ret) + return ret; + + ret = cs42_174e38_program_range(c, 0); + if (ret) + return ret; + ret = cs42_174e38_program_range(c, 1); + if (ret) + return ret; + + ret = cs42l81_rmw(c, 0x054f, 0xf0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0401, 0x01, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0401, 0x02, 0x00); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0402, 0x00); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0403, c->graph.tap_l); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0404, c->graph.tap_r); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0405, 0x00); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0406, 0x00); + if (ret) + return ret; + + ret = cs42_165bd4_build_slots(c, 0); + if (ret) + return ret; + ret = cs42_165bd4_build_slots(c, 1); + if (ret) + return ret; + + ret = cs42l81_rmw(c, 0x0401, 0x02, 0x02); + if (ret) + return ret; + msleep(100); - cs42l81_write(c, 0x0500, 0x05); - /* sub_F141C(1): unmute/level. Mute path writes 0xFF. */ - cs42l81_write(c, 0x0527, 0x60); - /* 26DDDE → 416440 → 40C028(2): 42A5D6(117, 63, 60). Not 0x75 bit7. */ + + ret = cs42l81_read(c, 0x0528, &c->graph.status_528); + if (ret) + return ret; + + ret = cs42_graph_end(c, 33); + if (ret) + return ret; + + dev_info(&c->spi->dev, + "CS42 graph built: mode=%d tap_l=%u tap_r=%u status528=%02x\n", + route_mode, c->graph.tap_l, c->graph.tap_r, c->graph.status_528); + return 0; +} + +static void cs42_maybe_debug_slot2_gain(struct cs42l81 *c) +{ + if (!graph_slot2_gain) + return; + cs42l81_write(c, 0x0410, 0x02); + cs42l81_write(c, 0x0411, 0x01); + cs42l81_write(c, 0x0412, 0xe0); + dev_info(&c->spi->dev, "CS42 DEBUG graph_slot2_gain baked at 0x410\n"); +} + + +/* OSOS sub_F141C — HP analog mute via 0x527 (sub_43CDB4 reg 1319). */ +static int cs42_f141c(struct cs42l81 *c, int on) +{ + return cs42l81_write(c, 0x0527, on ? 0x60 : 0xff); +} + +/* + * Play-side F141C(1): 527=0x60 before 570620. Final 401&3=2 at post_iis. + * Stop-side F141C(0): 527=0xFF; 401 bit0=1 handled in 42D364(0). + */ +static int cs42_f141c_play_unmute(struct cs42l81 *c, bool play) +{ + if (play) + return cs42_f141c(c, 1); + return cs42_f141c(c, 0); +} + +/* OSOS sub_F1444 — meter soft-ramp pulse on 0x51E/0x523 bit5. */ +static void cs42_f1444(struct cs42l81 *c) +{ + cs42l81_rmw(c, 0x051e, 0x20, 0x20); + cs42l81_rmw(c, 0x051e, 0x20, 0x00); + cs42l81_rmw(c, 0x0523, 0x20, 0x20); + cs42l81_rmw(c, 0x0523, 0x20, 0x00); +} + +/* + * OSOS sub_42D364(0) stop path: + * F141C(0); 401 bit0=1; 401 bit1=0; F1444; (500E14 = CoreAudio state) + */ +static int cs42_42d364_stop(struct cs42l81 *c) +{ + int ret; + + ret = cs42_f141c(c, 0); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0401, 0x01, 0x01); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0401, 0x02, 0x00); + if (ret) + return ret; + cs42_f1444(c); + c->route_playing = false; + c->play_started = false; + dev_info_ratelimited(&c->spi->dev, + "42D364(0) stop: 527=FF 401 bit0=1 bit1=0 F1444\n"); + return 0; +} + +/* ASP lock + final play latch: F141C(1); optional 401&3=2. */ +static int cs42_play_unmute(struct cs42l81 *c) +{ + int ret; + + ret = cs42_f141c_play_unmute(c, true); + if (ret) + return ret; + if (post_iis_401_rmw) { + ret = cs42l81_rmw(c, 0x0401, 0x03, 0x02); + if (ret) + return ret; + } + c->route_playing = true; + return 0; +} + +/* + * OSOS sub_570620(1) play graph — static 5707D8 (mode 0) or dynamic path. + * Caller must have already done F141C(1) per 42D364(1). + */ +static int cs42_570620_play_graph(struct cs42l81 *c, int mode) +{ + int ret; + + if (audio_route != 0) { + dev_warn(&c->spi->dev, + "audio_route=%d (USB/BT/MB) — forcing HP graph\n", + audio_route); + } + + ret = cs42_build_play_graph_retailos(c, graph_mode); + if (ret) + return ret; + cs42_maybe_debug_slot2_gain(c); + dev_info(&c->spi->dev, + "CS42 570620(play): graph=%s taps=%u/%u latched (mode param=%d)\n", + graph_mode == 0 ? "static" : "dynamic", + c->graph.tap_l, c->graph.tap_r, mode); + return 0; +} + +static bool cs42_headset_ready(void); + +/* + * Do not fold into codec prepare — this is the play lifecycle latch. + */ +static int cs42_retailos_play_start(struct cs42l81 *c) +{ + int ret; + + if (!cs42_headset_ready()) { + dev_warn(&c->spi->dev, + "headset not ready (8925CF4) — RetailOS would 42D364(0)\n"); + if (!force_headset) + return -ENODEV; + } + + ret = cs42_f141c_play_unmute(c, true); + if (ret) + return ret; + + ret = cs42_570620_play_graph(c, 1); + if (ret) + return ret; + + cs42_log_graph_snapshot(c, "post_play_start"); + cs42l81_log_start_state(c, "play_start"); + if (dump_regs) + cs42_log_final_state(c, "play_start"); + c->play_started = true; + c->route_playing = true; + dev_info(&c->spi->dev, "CS42 RetailOS play_start complete\n"); + return 0; +} + +static int cs42_retailos_play_stop(struct cs42l81 *c) +{ + int ret; + + ret = cs42_42d364_stop(c); + if (!ret) + cs42l81_log_start_state(c, "play_stop"); + return ret; +} + +static bool cs42_headset_ready(void) +{ + int (*ready)(void); + int r; + + if (force_headset) + return true; + ready = (int (*)(void))__symbol_get("apple_mikeybus_headset_ready"); + if (!ready) { + ready = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); + if (!ready) + return true; /* no mikey module — analog HP path */ + r = ready(); + __symbol_put("apple_mikeybus_jack_present"); + if (r < 0) + return true; /* loaded but unbound (uart2 disabled) */ + return r > 0; + } + r = ready(); + __symbol_put("apple_mikeybus_headset_ready"); + /* + * -ENODEV: module loaded, serdev never probed (uart2 status=disabled). + * That is not "open circuit". Blocking DAI here is the -19 bug. + * 0: resistor task measured open circuit. + * 1: identified accessory or force_plugged / unmeasured-ready. + */ + if (r < 0) + return true; + return r > 0; +} + +/* RE D3280(3)/audio_on HSDET pulse — tip/ring sense + 0x0B type read. */ +static void cs42_hsdet_pulse(struct cs42l81 *c) +{ + u8 r220 = 0, r2f = 0, r0b = 0, r08 = 0, r09 = 0; + unsigned int j; + + cs42l81_rmw(c, 0x0073, 0xc3, 0x00); + cs42l81_rmw(c, 0x0073, 0xc0, 0xc0); + cs42l81_rmw(c, 0x0079, 0x60, 0x00); + cs42l81_read(c, 0x0220, &r220); + cs42l81_rmw(c, 0x0220, 0x40, 0x40); + msleep(1); + cs42l81_rmw(c, 0x0009, 0xc0, 0xc0); + for (j = 0; j < 3; j++) { + msleep(1); + cs42l81_read(c, 0x002f, &r2f); + if (r2f & 0x40) + break; + } + cs42l81_read(c, 0x000b, &r0b); + cs42l81_rmw(c, 0x0009, 0xc0, 0x80); + cs42l81_rmw(c, 0x0220, 0x40, r220 & 0x40); + cs42l81_read(c, 0x0008, &r08); + cs42l81_read(c, 0x0009, &r09); + dev_info(&c->spi->dev, + "HSDET 0x0B=0x%02x type=%u 0x2F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", + r0b, r0b & 3, r2f, r08, r09); +} + +static void cs42_jack_poll_stop(struct cs42l81 *c) +{ + c->jack_poll_active = false; + cancel_delayed_work_sync(&c->jack_work); +} + +static void cs42_jack_workfn(struct work_struct *work) +{ + struct cs42l81 *c = container_of(work, struct cs42l81, jack_work.work); + bool present; + int jack; + + mutex_lock(&c->lock); + if (!c->jack_poll_active) + goto out_unlock; + + jack = -ENODEV; + { + int (*jp)(void) = (int (*)(void)) + __symbol_get("apple_mikeybus_jack_present"); + + if (jp) { + jack = jp(); + __symbol_put("apple_mikeybus_jack_present"); + } + } + present = force_headset || jack != 0; + if (jack == 0 && c->route_playing) { + dev_info(&c->spi->dev, "jack unplug -> 42D364(0)\n"); + cs42_42d364_stop(c); + cs42_hsdet_pulse(c); + } else if (jack > 0 && !c->jack_last_present && !c->route_playing) { + dev_info(&c->spi->dev, "jack plug -> re-arm HSDET\n"); + cs42_hsdet_pulse(c); + } + c->jack_last_present = present; + if (c->jack_poll_active && jack_poll_ms) + schedule_delayed_work(&c->jack_work, + msecs_to_jiffies(jack_poll_ms)); +out_unlock: + mutex_unlock(&c->lock); +} + +static void cs42_jack_poll_start(struct cs42l81 *c) +{ + if (!jack_poll_ms) + return; + c->jack_poll_active = true; + c->jack_last_present = cs42_headset_ready(); + cancel_delayed_work(&c->jack_work); + schedule_delayed_work(&c->jack_work, msecs_to_jiffies(jack_poll_ms)); +} + +/* + * OSOS sub_400330 → sub_3FA0E0(551=0x227, value&0x7f). + * D3280(4) calls this with 64 then 65 before final 0x229=0x41. + */ +static int cs42l81_set_output_gain(struct cs42l81 *c, u8 val) +{ + if (val & 0x40) + val |= 0x80; + return cs42l81_write(c, 0x0227, val); +} + +/* + * OSOS sub_D2F64(271) — output-path “on” mixer/mode blast (via D2D2C(1)). + * Values from Hex-Rays of osos.dec.bin.ida.c — do not invent. + */ +static int cs42l81_apply_mode_271(struct cs42l81 *c) +{ + int ret; + + ret = cs42l81_rmw(c, 0x0006, 0x04, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x28, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000d, 0x03, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0206, 0x3f, 0x3d); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0x3f, 0x3d); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0205, 0xff, 0x5a); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0204, 0x03, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0206, 0xc0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0xc0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0203, 0xc0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000e, 0x40, 0x40); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x011f, 0x3f, 0x1c); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0120, 0x3f, 0x1c); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x012e, 0xff, 0xaa); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000e, 0x40, 0x00); + if (ret) + return ret; + msleep(105); + return 0; +} + +/* OSOS sub_D2F64(6) — output-path “off” companion (via D2D2C(0)). */ +static int cs42l81_apply_mode_6(struct cs42l81 *c) +{ + int ret; + + ret = cs42l81_rmw(c, 0x0006, 0x04, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x28, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000d, 0x03, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0206, 0x3f, 0x34); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0x3f, 0x34); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0204, 0x03, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0206, 0xc0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0xc0, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0203, 0xc0, 0xc0); + if (ret) + return ret; + msleep(60); + return 0; +} + +/* OSOS sub_D2D2C(1) — enable HP/output path before D3280(4). */ +static int cs42l81_output_path_enable(struct cs42l81 *c) +{ + int ret; + + ret = cs42l81_rmw(c, 0x0206, 0x3f, 0x08); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0x3f, 0x08); + if (ret) + return ret; + ret = cs42l81_apply_mode_271(c); + if (ret) + return ret; + dev_info(&c->spi->dev, "D2D2C(1) / output_path_enable complete\n"); + return 0; +} + +/* OSOS sub_D2D2C(0). Kept for teardown; HP bring-up uses enable only. */ +static int __maybe_unused cs42l81_output_path_disable(struct cs42l81 *c) +{ + int ret; + + ret = cs42l81_apply_mode_6(c); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0206, 0x3f, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0207, 0x3f, 0x00); + if (ret) + return ret; + dev_info(&c->spi->dev, "D2D2C(0) / output_path_disable complete\n"); + return 0; +} + +/* + * OSOS sub_D3280(a1==4) — active HP output configuration (RE-backed). + * Do not treat D3280(3) alone as playback-active. + * Sequence includes sub_400330(64)/(65) before final 0x229=0x41. + */ +static int cs42l81_state_4_output_on(struct cs42l81 *c) +{ + int ret; + + ret = cs42l81_rmw(c, 0x0007, 0x40, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0219, 0x78, 0x78); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0229, 0x40); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0006, 0x01, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0201, 0xe0, 0x40); + if (ret) + return ret; + ret = cs42l81_write(c, 0xc81f, 0xff); + if (ret) + return ret; + ret = cs42l81_write(c, 0xc85f, 0x0f); + if (ret) + return ret; + /* RE state 4 base is 0x0E; glass A/B via c96f_final (try 0x1E). */ + ret = cs42l81_write(c, 0xc96f, (u8)(c96f_final & 0xff)); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0223, 0x08); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0224, 0x09); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0225, 0x00); + if (ret) + return ret; + /* + * Exact D3280(4) RE: 400330(64)/400330(65) then 229=0x41. + * Optional glass rail nudge (c96f_final=0x1e) keeps prior 219 lo3 dance. + */ + if ((c96f_final & 0xff) == 0x1e) { + ret = cs42l81_rmw(c, 0x0219, 0x07, 0x01); + if (ret) + return ret; + msleep(100); + ret = cs42l81_write(c, 0xc96f, 0x1e); + if (ret) + return ret; + } + /* D3280(4): 3FA0E0(553,64) before 400330 pair on 227. */ + ret = cs42l81_write(c, 0x0229, 0x40); + if (ret) + return ret; + ret = cs42l81_set_output_gain(c, 64); + if (ret) + return ret; + ret = cs42l81_set_output_gain(c, 65); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0229, 0x41); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + if (ret) + return ret; + + dev_info(&c->spi->dev, "D3280(4) / output_on complete\n"); + return 0; +} + +/* + * OSOS sub_D3280(a1==3) — headset detect / pre-output (not full play). + */ +static int cs42l81_state_3_headset_detect(struct cs42l81 *c) +{ + u8 r74 = 0, r7b = 0, r7c = 0, r0f = 0, r2f = 0; + int ret; + + ret = cs42l81_rmw(c, 0x0007, 0x40, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0006, 0x40, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x28, 0x28); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000f, 0x80, 0x80); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0075, 0x40, 0x40); + if (ret) + return ret; + cs42l81_read(c, 0x0074, &r74); + cs42l81_write(c, 0x0074, (r74 & 0xe7) | 0x08); + cs42l81_read(c, 0x007b, &r7b); + cs42l81_read(c, 0x007c, &r7c); + cs42l81_write(c, 0x0074, r74); + cs42l81_rmw(c, 0x0075, 0x40, 0x00); + cs42l81_rmw(c, 0x0075, 0x80, 0x80); + cs42l81_read(c, 0x000f, &r0f); + cs42l81_read(c, 0x002f, &r2f); + dev_info(&c->spi->dev, + "D3280(3) 0x0F=0x%02x 0x2F=0x%02x 0x7B=0x%02x 0x7C=0x%02x\n", + r0f, r2f, r7b, r7c); + return 0; +} + +/* OSOS sub_D34C0 / 183138 rate programming. */ +static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) +{ + const struct n31_rate_cfg *r = n31_find_rate(rate); + u8 code; + int ret; + + if (!r) + return -EINVAL; + code = r->cs42_rate_code; + + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000f, 0x0f, code); + if (ret) + return ret; + ret = cs42l81_write(c, 0x012f, (u8)(code | (code << 4))); + if (ret) + return ret; + + /* + * sub_183138 branches on rate code: + * code==12 (48 kHz): 0x10B=8, 0x10C=9, 0x131 bit0=1 + * else (e.g. 10=44.1): 0x121=8, 0x122=9, 0x130 lo=code, + * 0x131 bit0=0, 0x10B=4, 0x10C=0x33 + * Linux previously always took the 48 kHz arm while IIS ran + * 44.1 (CLKDIV 272) → ASP/SRC mismatch → pulsed noise. + */ + if (code == 12) { + ret = cs42l81_write(c, 0x010b, 0x08); + if (ret) + return ret; + ret = cs42l81_write(c, 0x010c, 0x09); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0131, 0x01, 0x01); + if (ret) + return ret; + } else { + ret = cs42l81_write(c, 0x0121, 0x08); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0122, 0x09); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0130, 0x0f, code); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0131, 0x01, 0x00); + if (ret) + return ret; + ret = cs42l81_write(c, 0x010b, 0x04); + if (ret) + return ret; + ret = cs42l81_write(c, 0x010c, 0x33); + if (ret) + return ret; + } + + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + if (ret) + return ret; + + c->rate = rate; + dev_info(&c->spi->dev, + "CS42 set_rate %u code=%u 10B=%02x 10C=%02x 131bit0=%d\n", + rate, code, code == 12 ? 0x08 : 0x04, + code == 12 ? 0x09 : 0x33, code == 12 ? 1 : 0); + return 0; +} + +/* + * Codec prepare — rails, rate, D2D2C, D3280(4). No 42D364 play graph. + * RetailOS play latch is cs42_retailos_play_start() at transport START. + */ +static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) +{ + u8 st = 0, r219 = 0; + int ret; + int jack = -ENODEV; + int (*mikey_jack)(void); + + if (!cs42_headset_ready()) { + dev_warn(&c->spi->dev, + "headset not ready (8925CF4) — RetailOS gates 570620\n"); + if (!force_headset) + return -ENODEV; + } + + mikey_jack = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); + if (mikey_jack) { + jack = mikey_jack(); + __symbol_put("apple_mikeybus_jack_present"); + } + if (jack < 0) + dev_info(&c->spi->dev, + "MikeyBus unbound (uart2 disabled) — analog HP not gated\n"); + else if (jack == 0) + dev_warn(&c->spi->dev, + "MikeyBus open circuit — force_headset=1 to override\n"); + else + dev_info(&c->spi->dev, "MikeyBus jack present\n"); + + ret = cs42l81_state_3_headset_detect(c); + if (ret) + return ret; + + if (!rate) + rate = cs42_pick_rate(c, 0); + ret = cs42l81_set_rate(c, rate); + if (ret) + return ret; + + ret = cs42l81_output_path_enable(c); + if (ret) + return ret; + + ret = cs42l81_state_4_output_on(c); + if (ret) + return ret; + + /* Play-object companions (40C028/54F/220) — safe before graph latch. */ cs42l81_rmw(c, 0x0075, 0x3f, 0x3c); - /* 570620: 42A5D6(1359, 240, 0) */ cs42l81_rmw(c, 0x054f, 0xf0, 0x00); cs42l81_rmw(c, 0x0220, 0x28, 0x28); - /* - * D3280(3) after mixer: 0x0F bit7 (pad drive) and 0x220. - * D2EFC/9901 already ran in bringup — do not unlock again - * (that wiped 0x527/mixer on glass). 41CBD8(9,1) is IIS ungate. - */ - cs42l81_rmw(c, 0x0007, 0x40, 0x00); - cs42l81_rmw(c, 0x0006, 0x40, 0x00); - cs42l81_rmw(c, 0x0220, 0x28, 0x28); - cs42l81_rmw(c, 0x000f, 0x80, 0x80); - cs42l81_rmw(c, 0x0075, 0x40, 0x40); - { - u8 r74 = 0, r7b = 0, r7c = 0, r0f = 0, r2f = 0; - - cs42l81_read(c, 0x0074, &r74); - cs42l81_write(c, 0x0074, (r74 & 0xe7) | 0x08); - cs42l81_read(c, 0x007b, &r7b); - cs42l81_read(c, 0x007c, &r7c); - cs42l81_write(c, 0x0074, r74); - cs42l81_rmw(c, 0x0075, 0x40, 0x00); - cs42l81_rmw(c, 0x0075, 0x80, 0x80); - cs42l81_read(c, 0x000f, &r0f); - cs42l81_read(c, 0x002f, &r2f); - dev_info(&c->spi->dev, - "D3280(3) 0x0F=0x%02x 0x2F=0x%02x 0x7B=0x%02x 0x7C=0x%02x\n", - r0f, r2f, r7b, r7c); - } - - /* - * D34C0 with 892A038=0x28 (D3280(3)): short serial, not 183138. - * 0x0E bits7-6 = 11 then 01, 0x0F low=12, 0x12F=0xCC. - * Short path does not touch 0x131 — 183138 already set bit0. - */ - cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); - cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); - cs42l81_write(c, 0x012f, 0xcc); - cs42l81_rmw(c, 0x000e, 0xc0, 0x40); - - /* - * 4F08: enable tip/ring sense. 7984: Class-H charge-pump kick - * plus 0x0B headset-type (CS42L73-class HP stays Hi-Z until this). - * 40C028(2) already programmed 0x75=0x3C; D3280(3) set bit7 (HP). - */ - cs42l81_rmw(c, 0x0073, 0xc3, 0x00); - cs42l81_rmw(c, 0x0073, 0xc0, 0xc0); - cs42l81_rmw(c, 0x0079, 0x60, 0x00); - { - u8 r220 = 0, r2f = 0, r0b = 0, r08 = 0, r09 = 0; - unsigned int i; - - cs42l81_read(c, 0x0220, &r220); - cs42l81_rmw(c, 0x0220, 0x40, 0x40); - msleep(1); - cs42l81_rmw(c, 0x0009, 0xc0, 0xc0); - for (i = 0; i < 3; i++) { - msleep(1); - cs42l81_read(c, 0x002f, &r2f); - if (r2f & 0x40) - break; - } - cs42l81_read(c, 0x000b, &r0b); - cs42l81_rmw(c, 0x0009, 0xc0, 0x80); - cs42l81_rmw(c, 0x0220, 0x40, r220 & 0x40); - cs42l81_read(c, 0x0008, &r08); - cs42l81_read(c, 0x0009, &r09); - dev_info(&c->spi->dev, - "HSDET 0x0B=0x%02x type=%u 0x2F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", - r0b, r0b & 3, r2f, r08, r09); - } - /* 42D364(1) play: unmute HP amp + mixer bit1. */ - cs42l81_write(c, 0x0527, 0x60); - cs42l81_rmw(c, 0x0401, 0x03, 0x02); + cs42_hsdet_pulse(c); cs42l81_read(c, 0x0227, &st); cs42l81_read(c, 0x0219, &r219); - cs42l81_apply_user_vol(c); + cs42l81_push_pcm_q8(c->user_vol); + cs42_log_graph_snapshot(c, "pre_play"); + cs42l81_log_start_state(c, "codec_prepare"); dev_info(&c->spi->dev, - "CS42 audio_on C96F=0x1E status 0x227=0x%02x 0x219=0x%02x vol=%u/%u\n", - st, r219, c->user_vol, CS42L81_USER_VOL_MAX); + "codec_prepare 0x227=0x%02x 0x219=0x%02x rate=%u graph_mode=%d\n", + st, r219, rate, graph_mode); + c->codec_prepared = true; + cs42_jack_poll_start(c); + + /* audio_path_mode=0: legacy debug — graph folded into prepare. */ + if (audio_path_mode == 0) { + ret = cs42_f141c_play_unmute(c, true); + if (ret) + return ret; + ret = cs42_570620_play_graph(c, 1); + if (ret) + return ret; + c->play_started = true; + c->route_playing = true; + } return 0; } -/* 42D364(0/1) + F141C: play unmute 0x527=0x60, mute 0xFF. */ -static int cs42l81_set_mute(struct cs42l81 *c, int mute) +/* Sysfs / legacy: full prepare + play_start (ALSA uses split lifecycle). */ +static int cs42l81_audio_on(struct cs42l81 *c) +{ + int ret; + + ret = cs42_codec_prepare(c, cs42_pick_rate(c, 0)); + if (ret) + return ret; + if (audio_path_mode == 0) { + /* Legacy debug: graph in prepare (non-RetailOS order). */ + ret = cs42_f141c_play_unmute(c, true); + if (ret) + return ret; + ret = cs42_570620_play_graph(c, 1); + if (ret) + return ret; + c->play_started = true; + c->route_playing = true; + return 0; + } + return cs42_retailos_play_start(c); +} + +/* Legacy mute helper — prefer cs42_f141c_play_unmute / cs42_retailos_play_stop. */ +static int __maybe_unused cs42l81_set_mute(struct cs42l81 *c, int mute) { if (mute) { cs42l81_write(c, 0x0527, 0xff); @@ -333,13 +1639,33 @@ static void cs42l81_push_pcm_q8(unsigned int vol) } } -/* RetailOS 0 = analog mute; 1..256 = unmute + Q8 PCM scalar (256 = unity). */ +/* User vol mute during play: F141C(0) only — not full 42D364 teardown. */ static int cs42l81_apply_user_vol(struct cs42l81 *c) { unsigned int q8 = c->dai_mute ? 0 : c->user_vol; cs42l81_push_pcm_q8(q8); - return cs42l81_set_mute(c, q8 == 0); + if (q8 == 0) + return cs42_f141c_play_unmute(c, false); + if (c->play_started) + return cs42_play_unmute(c); + return cs42_f141c_play_unmute(c, true); +} + +/* CONFIRMED_N31 readback set from CURSOR-N31-AUDIO-FIRST-SOUND-HANDOFF.md P0.1 */ +static void cs42l81_log_start_state(struct cs42l81 *c, const char *tag) +{ + u8 r2f = 0, r401 = 0, r527 = 0, r219 = 0, rc96f = 0; + + cs42l81_read(c, 0x002f, &r2f); + cs42l81_read(c, 0x0401, &r401); + cs42l81_read(c, 0x0527, &r527); + cs42l81_read(c, 0x0219, &r219); + cs42l81_read(c, 0xc96f, &rc96f); + dev_info(&c->spi->dev, + "CS42 %s: vol=%u/%u dai_mute=%d 2F=%02x 527=%02x 401=%02x 219=%02x C96F=%02x\n", + tag, c->user_vol, CS42L81_USER_VOL_MAX, c->dai_mute, + r2f, r527, r401, r219, rc96f); } static ssize_t reg_store(struct device *dev, struct device_attribute *attr, @@ -397,20 +1723,69 @@ static ssize_t bringup_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(bringup); +static ssize_t dump_graph_regs_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + int n = 0, ret; + u16 r; + u8 val; + + mutex_lock(&c->lock); + n += scnprintf(buf + n, PAGE_SIZE - n, + "mode=%d tap_l=%u tap_r=%u status528=%02x\n", + c->graph.mode, c->graph.tap_l, c->graph.tap_r, + c->graph.status_528); + for (r = 0x400; r <= 0x448; r++) { + ret = cs42l81_read(c, r, &val); + if (ret) + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: ERR %d\n", r, ret); + else + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: 0x%02x\n", r, val); + if (n >= PAGE_SIZE - 64) + break; + } + for (r = 0x529; r <= 0x534; r++) { + ret = cs42l81_read(c, r, &val); + if (ret) + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: ERR %d\n", r, ret); + else + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: 0x%02x\n", r, val); + } + for (r = 0x54f; r <= 0x54f; r++) { + ret = cs42l81_read(c, r, &val); + if (!ret) + n += scnprintf(buf + n, PAGE_SIZE - n, + "0x%04x: 0x%02x\n", r, val); + } + ret = cs42l81_read(c, 0x528, &val); + if (!ret) + n += scnprintf(buf + n, PAGE_SIZE - n, "0x0528: 0x%02x\n", val); + mutex_unlock(&c->lock); + return n; +} +static DEVICE_ATTR_RO(dump_graph_regs); + static ssize_t dump_key_regs_show(struct device *dev, struct device_attribute *attr, char *buf) { struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); static const u16 regs[] = { - 0x0227, 0x0219, 0xc96f, 0xc81f, 0xc85f, + 0x0227, 0x0229, 0x0219, 0xc96f, 0xc81f, 0xc85f, 0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x000f, 0x0012, 0x0019, 0x0070, 0x0071, 0x0073, 0x0074, 0x0075, 0x0076, 0x0079, 0x007a, 0x007b, 0x007c, - 0x002f, 0x0131, 0x0220, 0x0201, 0x0222, - 0x0223, 0x0224, 0x0121, 0x0122, 0x012f, + 0x002f, 0x0131, 0x0220, 0x0201, 0x0203, 0x0204, + 0x0205, 0x0206, 0x0207, 0x0222, 0x0223, 0x0224, 0x0225, + 0x011f, 0x0120, 0x012e, 0x0121, 0x0122, 0x012f, 0x010b, 0x010c, 0x0529, 0x052a, 0x0533, 0x0534, - 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, + 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, + 0x0407, 0x040a, 0x040d, 0x0410, 0x0425, 0x0428, 0x042b, 0x042e, 0x0527, 0x051e, 0x0523, 0x054f, 0x0500, 0x051f, 0x0520, 0x0521, 0x0524, 0x0525, 0x0528, 0x001a, 0x001b, 0x001e, 0x001f, @@ -547,7 +1922,7 @@ static DEVICE_ATTR_WO(rreg); /* * After IIS BCLK/LRCK run (RetailOS 26DDDE: 414FAE before sustained PCM). - * Re-run 183138 clock regs and poll 0x2F bit6 (ASP sync / LOS clear). + * Re-run 183138 clock regs and poll 0x2F bit6 for ASP sync (see asp_bit6_is_los). * LOS does not always self-recover — pulse 0x220 and retry clock prog. */ static void cs42l81_asp_clock_pulse(struct cs42l81 *c) @@ -557,40 +1932,90 @@ static void cs42l81_asp_clock_pulse(struct cs42l81 *c) cs42l81_rmw(c, 0x0220, 0x20, 0x20); } -static void cs42l81_asp_program_48k(struct cs42l81 *c) +/* Re-apply full 183138 during ASP lock (not just 0x0E/0x0F/0x12F). */ +static void cs42l81_asp_program_rate(struct cs42l81 *c) { - cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); - cs42l81_rmw(c, 0x000f, 0x0f, 0x0c); - cs42l81_write(c, 0x012f, 0xcc); - cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + cs42l81_set_rate(c, cs42_pick_rate(c, c->rate)); } +int cs42l81_asp_hold_light(void); + static int cs42l81_asp_lock(struct cs42l81 *c) { unsigned int attempt, i; - u8 r2f = 0, r0e = 0, r0f = 0; + u8 r2f = 0, r0e = 0, r0f = 0, r08 = 0, r09 = 0; - for (attempt = 0; attempt < 3; attempt++) { + for (attempt = 0; attempt < 5; attempt++) { if (attempt) cs42l81_asp_clock_pulse(c); - cs42l81_asp_program_48k(c); - for (i = 0; i < 50; i++) { + cs42l81_asp_program_rate(c); + for (i = 0; i < 80; i++) { cs42l81_read(c, 0x002f, &r2f); - if (r2f & 0x40) + if (cs42l81_asp_synced(r2f)) break; - usleep_range(1000, 2000); + usleep_range(500, 1000); } - if (r2f & 0x40) + if (cs42l81_asp_synced(r2f)) break; } + cs42l81_read(c, 0x0008, &r08); + cs42l81_read(c, 0x0009, &r09); cs42l81_read(c, 0x000e, &r0e); cs42l81_read(c, 0x000f, &r0f); dev_info(&c->spi->dev, - "asp_lock 0x2F=0x%02x 0x0E=0x%02x 0x0F=0x%02x (need bit6, IIS running)\n", - r2f, r0e, r0f); - return (r2f & 0x40) ? 0 : -EAGAIN; + "asp_lock 0x2F=0x%02x los=%d synced=%d 0x0E=0x%02x 0x0F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", + r2f, asp_bit6_is_los, cs42l81_asp_synced(r2f), r0e, r0f, r08, r09); + return cs42l81_asp_synced(r2f) ? 0 : -EAGAIN; } +/* + * Mid-stream LOS recovery: one 183138 pulse + short poll (~10 ms). + * dma_tone calls this when 0x2F bit6 asserts LOS mid-tone. + */ +int cs42l81_asp_hold_light(void) +{ + struct cs42l81 *c = cs42l81_dev; + unsigned int i; + u8 r2f = 0, before = 0; + int ret = -ENODEV; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + cs42l81_read(c, 0x002f, &before); + if (cs42l81_asp_synced(before)) { + ret = 0; + goto out; + } + cs42l81_asp_program_rate(c); + for (i = 0; i < 24; i++) { + cs42l81_read(c, 0x002f, &r2f); + if (cs42l81_asp_synced(r2f)) + break; + usleep_range(400, 800); + } + if (cs42l81_asp_synced(r2f)) { + ret = 0; + } else { + cs42l81_asp_clock_pulse(c); + cs42l81_asp_program_rate(c); + for (i = 0; i < 16; i++) { + cs42l81_read(c, 0x002f, &r2f); + if (cs42l81_asp_synced(r2f)) + break; + usleep_range(400, 800); + } + ret = cs42l81_asp_synced(r2f) ? 0 : -EAGAIN; + } + dev_info(&c->spi->dev, + "asp_hold_light 0x2F 0x%02x->0x%02x ret=%d\n", + before, r2f, ret); +out: + mutex_unlock(&c->lock); + return ret; +} +EXPORT_SYMBOL_GPL(cs42l81_asp_hold_light); + int cs42l81_play_prepare(void) { struct cs42l81 *c = cs42l81_dev; @@ -599,33 +2024,184 @@ int cs42l81_play_prepare(void) if (!c) return -ENODEV; mutex_lock(&c->lock); - ret = cs42l81_audio_on(c); - if (!ret && !c->dai_mute) - cs42l81_set_mute(c, 0); + if (c->codec_prepared && !force_full_prepare) { + mutex_unlock(&c->lock); + return 0; + } + if (force_full_prepare && c->play_started) + cs42_retailos_play_stop(c); + ret = cs42_codec_prepare(c, cs42_pick_rate(c, c->rate)); mutex_unlock(&c->lock); return ret; } EXPORT_SYMBOL_GPL(cs42l81_play_prepare); +int cs42l81_play_start(void) +{ + struct cs42l81 *c = cs42l81_dev; + int ret; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + if (!c->codec_prepared) { + ret = cs42_codec_prepare(c, cs42_pick_rate(c, c->rate)); + if (ret) + goto out; + } + if (c->play_started && !force_full_prepare) { + ret = 0; + goto out; + } + if (force_full_prepare && c->play_started) + cs42_retailos_play_stop(c); + ret = cs42_retailos_play_start(c); +out: + mutex_unlock(&c->lock); + return ret; +} +EXPORT_SYMBOL_GPL(cs42l81_play_start); + +int cs42l81_play_stop(void) +{ + struct cs42l81 *c = cs42l81_dev; + int ret; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + ret = cs42_retailos_play_stop(c); + if (!ret && force_full_prepare) + c->codec_prepared = false; + mutex_unlock(&c->lock); + return ret; +} +EXPORT_SYMBOL_GPL(cs42l81_play_stop); + +int cs42l81_get_audio_path_mode(void) +{ + return audio_path_mode; +} +EXPORT_SYMBOL_GPL(cs42l81_get_audio_path_mode); + /* - * Called after IIS TXCOM kick. Clears LOS mute and ensures HP path unmuted. + * Called after IIS TXCOM kick. Telemetry on 0x2F; unmute unless asp_gate_unmute. */ +int cs42l81_pre_iis_start(void) +{ + struct cs42l81 *c = cs42l81_dev; + + if (!c) + return -ENODEV; + mutex_lock(&c->lock); + if (of_asp_slave) + cs42l81_rmw(c, 0x000f, 0x80, 0x00); + mutex_unlock(&c->lock); + return 0; +} +EXPORT_SYMBOL_GPL(cs42l81_pre_iis_start); + int cs42l81_post_iis_start(void) { struct cs42l81 *c = cs42l81_dev; - int ret; + int probe; if (!c) return -ENODEV; mutex_lock(&c->lock); - ret = cs42l81_asp_lock(c); - if (!ret && !c->dai_mute) - cs42l81_set_mute(c, 0); + probe = cs42l81_asp_lock(c); + /* + * Checkpoint-010 / handoff: do not gate HP unmute on dai_mute. + * ALSA mute_stream(1) on a prior close left dai_mute stuck, so + * dma_tone/post_iis kept 0x527=0xFF while ASP was locked and + * TXCOM=6 — silent jack with "perfect" digital telemetry. + * asp_gate_unmute=0 (default): always force 0x527=0x60 / 0x401&3=2. + */ + if (!asp_gate_unmute || !probe) { + c->dai_mute = false; + cs42l81_write(c, 0x0229, 0x41); + cs42l81_write(c, 0xc96f, (u8)(c96f_final & 0xff)); + cs42_play_unmute(c); + cs42l81_apply_user_vol(c); + } + cs42l81_log_start_state(c, "post_iis"); + cs42_log_final_state(c, "post_iis"); mutex_unlock(&c->lock); - return ret; + return asp_gate_unmute ? probe : 0; } EXPORT_SYMBOL_GPL(cs42l81_post_iis_start); +static void cs42l81_asp_post_workfn(struct work_struct *work) +{ + cs42l81_post_iis_start(); +} + +void cs42l81_schedule_post_iis(void) +{ + struct cs42l81 *c = cs42l81_dev; + + if (!c) + return; + cancel_delayed_work(&c->asp_post_work); + schedule_delayed_work(&c->asp_post_work, msecs_to_jiffies(25)); +} +EXPORT_SYMBOL_GPL(cs42l81_schedule_post_iis); + +void cs42l81_cancel_post_iis(void) +{ + struct cs42l81 *c = cs42l81_dev; + + if (!c) + return; + cancel_delayed_work_sync(&c->asp_post_work); +} +EXPORT_SYMBOL_GPL(cs42l81_cancel_post_iis); + +static ssize_t probe_2f_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + unsigned int n, i; + u8 r2f; + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + n = 10; + if (kstrtouint(buf + 1, 0, &n) == 0 && n > 0 && n <= 32) + ; /* optional count after '1' */ + else + n = 10; + mutex_lock(&c->lock); + for (i = 0; i < n; i++) { + cs42l81_read(c, 0x002f, &r2f); + dev_info(&c->spi->dev, "probe_2f[%u]=0x%02x synced=%d\n", + i, r2f, cs42l81_asp_synced(r2f)); + } + mutex_unlock(&c->lock); + return count; +} +static DEVICE_ATTR_WO(probe_2f); + +static ssize_t force_play_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct cs42l81 *c = spi_get_drvdata(to_spi_device(dev)); + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + mutex_lock(&c->lock); + c->dai_mute = false; + cs42l81_write(c, 0x0229, 0x41); + cs42l81_write(c, 0xc96f, 0x1e); + cs42l81_write(c, 0x0527, 0x60); + cs42l81_rmw(c, 0x0401, 0x03, 0x02); + cs42l81_apply_user_vol(c); + cs42l81_log_start_state(c, "force_play"); + mutex_unlock(&c->lock); + return count; +} +static DEVICE_ATTR_WO(force_play); + static ssize_t asp_lock_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { @@ -646,8 +2222,11 @@ static struct attribute *cs42l81_attrs[] = { &dev_attr_status_227.attr, &dev_attr_bringup.attr, &dev_attr_dump_key_regs.attr, + &dev_attr_dump_graph_regs.attr, &dev_attr_volume.attr, &dev_attr_audio_on.attr, + &dev_attr_probe_2f.attr, + &dev_attr_force_play.attr, &dev_attr_asp_lock.attr, &dev_attr_mute.attr, &dev_attr_rreg.attr, @@ -660,13 +2239,15 @@ static int cs42l81_dai_hw_params(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); + unsigned int rate = params_rate(params); int ret; mutex_lock(&c->lock); - ret = cs42l81_audio_on(c); + c->rate = rate; + ret = cs42_codec_prepare(c, rate); mutex_unlock(&c->lock); - dev_info(&c->spi->dev, "DAI hw_params rate=%u ret=%d\n", - params_rate(params), ret); + dev_info(&c->spi->dev, "DAI hw_params rate=%u ret=%d (prepare only)\n", + rate, ret); return ret; } @@ -674,7 +2255,6 @@ static int cs42l81_dai_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai) { struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); - int ret = 0; if (substream->stream != SNDRV_PCM_STREAM_PLAYBACK) return 0; @@ -682,22 +2262,23 @@ static int cs42l81_dai_trigger(struct snd_pcm_substream *substream, int cmd, case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: - /* CPU IIS trigger runs first — BCLK/LRCK should be toggling. */ - mutex_lock(&c->lock); - ret = cs42l81_asp_lock(c); - if (!ret && !c->dai_mute) - cs42l81_set_mute(c, 0); - mutex_unlock(&c->lock); - dev_info(&c->spi->dev, "DAI trigger START asp=%d\n", ret); + /* CPU IIS + DMA kick first; ASP lock runs sleepable in workqueue. */ + cs42l81_schedule_post_iis(); + dev_info(&c->spi->dev, "DAI trigger START (asp work scheduled)\n"); break; case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + /* IIS CPU DAI owns 42D364 and post_iis cancel. Cancelling + * here aborts unmute if ALSA xruns 20-50ms after START. + */ + dev_info_ratelimited(&c->spi->dev, + "DAI trigger STOP (codec leave to IIS)\n"); break; default: return -EINVAL; } - return ret; + return 0; } static int cs42l81_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream) @@ -708,9 +2289,15 @@ static int cs42l81_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream return 0; mutex_lock(&c->lock); c->dai_mute = mute ? 1 : 0; - cs42l81_apply_user_vol(c); + /* Do not F141C-mute on ALSA mute(1). Short START/STOP bursts were + * remuting HP 20-50ms after play_start. Real stop is 42D364 on IIS. + */ + if (!mute) + cs42l81_apply_user_vol(c); mutex_unlock(&c->lock); - dev_info(&c->spi->dev, "DAI mute=%d user_vol=%u\n", mute, c->user_vol); + dev_info_ratelimited(&c->spi->dev, + "DAI mute=%d user_vol=%u%s\n", mute, c->user_vol, + mute ? " (deferred)" : ""); return 0; } @@ -740,6 +2327,138 @@ static int cs42l81_vol_get(struct snd_kcontrol *kcontrol, return 0; } +static void cs42l81_notify_master_vol(struct cs42l81 *c) +{ + struct snd_soc_component *comp = c->component; + struct snd_kcontrol *kctl; + + if (!comp || !comp->card || !comp->card->snd_card) + return; + kctl = snd_soc_component_get_kcontrol(comp, "Master Playback Volume"); + if (!kctl) + return; + snd_ctl_notify(comp->card->snd_card, SNDRV_CTL_EVENT_MASK_VALUE, + &kctl->id); +} + +/* + * Vol± from gpio-s5l8740 (KEY_VOLUMEUP/DOWN) → Master Playback Volume. + * Input softirq must not SPI; defer apply + ALSA notify to process context. + */ +static void cs42l81_vol_workfn(struct work_struct *work) +{ + struct cs42l81 *c = container_of(work, struct cs42l81, vol_work); + int steps = atomic_xchg(&c->vol_steps, 0); + int delta; + unsigned int vol, prev; + bool unmute = false; + + if (!steps) + return; + delta = steps * (int)CS42L81_VOL_STEP; + + mutex_lock(&c->lock); + prev = c->user_vol; + if (delta > 0) { + vol = prev + (unsigned int)delta; + if (vol > CS42L81_USER_VOL_MAX) + vol = CS42L81_USER_VOL_MAX; + if (c->dai_mute && vol > 0) { + c->dai_mute = false; + unmute = true; + } + } else { + unsigned int down = (unsigned int)(-delta); + + vol = (prev > down) ? prev - down : 0; + } + if (vol != prev || unmute) { + c->user_vol = vol; + cs42l81_apply_user_vol(c); + } + mutex_unlock(&c->lock); + + if (vol != prev || unmute) { + cs42l81_notify_master_vol(c); + dev_info(&c->spi->dev, + "Vol%c → Master %u/%u%s\n", + delta > 0 ? '+' : '-', vol, CS42L81_USER_VOL_MAX, + unmute ? " (unmuted)" : ""); + } +} + +static void cs42l81_input_event(struct input_handle *handle, + unsigned int type, unsigned int code, int value) +{ + struct cs42l81 *c = handle->private; + + /* value 1 = press, 2 = autorepeat; ignore release */ + if (type != EV_KEY || value == 0 || !c) + return; + if (code == KEY_VOLUMEUP) + atomic_add(1, &c->vol_steps); + else if (code == KEY_VOLUMEDOWN) + atomic_add(-1, &c->vol_steps); + else + return; + schedule_work(&c->vol_work); +} + +static int cs42l81_input_connect(struct input_handler *handler, + struct input_dev *dev, + const struct input_device_id *id) +{ + struct cs42l81 *c = container_of(handler, struct cs42l81, + input_handler); + struct input_handle *handle; + int err; + + handle = kzalloc(sizeof(*handle), GFP_KERNEL); + if (!handle) + return -ENOMEM; + handle->dev = dev; + handle->handler = handler; + handle->name = "cs42l81-vol"; + handle->private = c; + + err = input_register_handle(handle); + if (err) + goto err_free; + err = input_open_device(handle); + if (err) + goto err_unregister; + + dev_info(&c->spi->dev, "Vol± keys → Master Playback Volume (%s)\n", + dev->name ? dev->name : "input"); + return 0; + +err_unregister: + input_unregister_handle(handle); +err_free: + kfree(handle); + return err; +} + +static void cs42l81_input_disconnect(struct input_handle *handle) +{ + input_close_device(handle); + input_unregister_handle(handle); + kfree(handle); +} + +static const struct input_device_id cs42l81_input_ids[] = { + { + .flags = INPUT_DEVICE_ID_MATCH_EVBIT | + INPUT_DEVICE_ID_MATCH_KEYBIT, + .evbit = { BIT_MASK(EV_KEY) }, + .keybit = { + [BIT_WORD(KEY_VOLUMEUP)] = + BIT_MASK(KEY_VOLUMEUP) | BIT_MASK(KEY_VOLUMEDOWN), + }, + }, + { }, +}; + static int cs42l81_vol_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol) { @@ -758,6 +2477,43 @@ static int cs42l81_vol_put(struct snd_kcontrol *kcontrol, return changed; } +/* 1 = unmuted (ALSA convention), 0 = muted — mirrors sysfs mute / dai_mute. */ +static int cs42l81_sw_info(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_info *uinfo) +{ + uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN; + uinfo->count = 1; + uinfo->value.integer.min = 0; + uinfo->value.integer.max = 1; + return 0; +} + +static int cs42l81_sw_get(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *comp = snd_soc_kcontrol_component(kcontrol); + struct cs42l81 *c = snd_soc_component_get_drvdata(comp); + + ucontrol->value.integer.value[0] = c->dai_mute ? 0 : 1; + return 0; +} + +static int cs42l81_sw_put(struct snd_kcontrol *kcontrol, + struct snd_ctl_elem_value *ucontrol) +{ + struct snd_soc_component *comp = snd_soc_kcontrol_component(kcontrol); + struct cs42l81 *c = snd_soc_component_get_drvdata(comp); + bool mute = !ucontrol->value.integer.value[0]; + int changed; + + mutex_lock(&c->lock); + changed = mute != c->dai_mute; + c->dai_mute = mute; + cs42l81_apply_user_vol(c); + mutex_unlock(&c->lock); + return changed; +} + static const struct snd_kcontrol_new cs42l81_controls[] = { { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, @@ -766,6 +2522,13 @@ static const struct snd_kcontrol_new cs42l81_controls[] = { .get = cs42l81_vol_get, .put = cs42l81_vol_put, }, + { + .iface = SNDRV_CTL_ELEM_IFACE_MIXER, + .name = "Master Playback Switch", + .info = cs42l81_sw_info, + .get = cs42l81_sw_get, + .put = cs42l81_sw_put, + }, }; static struct snd_soc_dai_driver cs42l81_dai = { @@ -780,7 +2543,16 @@ static struct snd_soc_dai_driver cs42l81_dai = { .ops = &cs42l81_dai_ops, }; +static int cs42l81_component_probe(struct snd_soc_component *component) +{ + struct cs42l81 *c = snd_soc_component_get_drvdata(component); + + c->component = component; + return 0; +} + static const struct snd_soc_component_driver cs42l81_component = { + .probe = cs42l81_component_probe, .idle_bias_on = 1, .endianness = 1, .controls = cs42l81_controls, @@ -797,7 +2569,12 @@ static int cs42l81_probe(struct spi_device *spi) return -ENOMEM; c->spi = spi; c->user_vol = CS42L81_USER_VOL_MAX; + c->dai_mute = false; mutex_init(&c->lock); + atomic_set(&c->vol_steps, 0); + INIT_WORK(&c->vol_work, cs42l81_vol_workfn); + INIT_DELAYED_WORK(&c->asp_post_work, cs42l81_asp_post_workfn); + INIT_DELAYED_WORK(&c->jack_work, cs42_jack_workfn); spi_set_drvdata(spi, c); mutex_lock(&c->lock); @@ -822,13 +2599,38 @@ static int cs42l81_probe(struct spi_device *spi) return ret; } - dev_info(&spi->dev, "CS42L81 SPI + ASoC DAI cs42l81-hifi\n"); + c->input_handler.event = cs42l81_input_event; + c->input_handler.connect = cs42l81_input_connect; + c->input_handler.disconnect = cs42l81_input_disconnect; + c->input_handler.name = "cs42l81-vol"; + c->input_handler.id_table = cs42l81_input_ids; + ret = input_register_handler(&c->input_handler); + if (ret) + dev_warn(&spi->dev, "Vol± input handler: %d\n", ret); + else + c->input_handler_reg = true; + + dev_info(&spi->dev, + "CS42L81 SPI + ASoC DAI cs42l81-hifi (Vol±→Master step=%u)\n", + CS42L81_VOL_STEP); return 0; } static void cs42l81_remove(struct spi_device *spi) { - if (cs42l81_dev == spi_get_drvdata(spi)) + struct cs42l81 *c = spi_get_drvdata(spi); + + if (c) { + if (c->input_handler_reg) { + input_unregister_handler(&c->input_handler); + c->input_handler_reg = false; + } + cancel_work_sync(&c->vol_work); + cancel_delayed_work_sync(&c->asp_post_work); + cs42_jack_poll_stop(c); + c->component = NULL; + } + if (cs42l81_dev == c) cs42l81_dev = NULL; sysfs_remove_groups(&spi->dev.kobj, cs42l81_groups); } diff --git a/sound/soc/apple/n31-audio-rates.h b/sound/soc/apple/n31-audio-rates.h new file mode 100755 index 00000000000000..606a9f05ed052e --- /dev/null +++ b/sound/soc/apple/n31-audio-rates.h @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * N31 sample-rate table — OSOS sub_D34C0 (osos.dec.bin.ida.c). + * + * RetailOS local music observed CLKDIV=272 → 44100 (code 10). + * 48000 (code 12, CLKDIV 250) stays in the table for ALSA requests; + * do not assume 48 kHz. Unspecified rate → N31_RATE_DEFAULT. + */ +#ifndef N31_AUDIO_RATES_H +#define N31_AUDIO_RATES_H + +#include +#include +#include + +#define N31_RATE_DEFAULT 44100u + +struct n31_rate_cfg { + unsigned int rate; + u8 cs42_rate_code; + u16 clkdiv; +}; + +static const struct n31_rate_cfg n31_rates[] = { + { 8000, 1, 1500 }, + { 11025, 2, 1088 }, + { 12000, 4, 1000 }, + { 16000, 5, 750 }, + { 22050, 6, 544 }, + { 24000, 8, 500 }, + { 32000, 9, 375 }, + { 44100, 10, 272 }, + { 48000, 12, 250 }, +}; + +static inline const struct n31_rate_cfg *n31_find_rate(unsigned int rate) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(n31_rates); i++) + if (n31_rates[i].rate == rate) + return &n31_rates[i]; + return NULL; +} + +/* Use the requested OSOS rate, else RetailOS 44.1 kHz. */ +static inline unsigned int n31_pick_rate(unsigned int rate) +{ + if (rate && n31_find_rate(rate)) + return rate; + return N31_RATE_DEFAULT; +} + +/* Exact 1 kHz period group: rate / gcd(rate, 1000) frames. */ +static inline unsigned int n31_tone_period_frames(unsigned int rate) +{ + unsigned int a, b, t; + + rate = n31_pick_rate(rate); + a = rate; + b = 1000; + while (b) { + t = a % b; + a = b; + b = t; + } + return rate / a; +} + +/* 256-point sine, peak ≈ 0.7 * 32767. 1 kHz via DDS at any table rate. */ +static const s16 n31_sin256[256] = { + 0, 563, 1125, 1687, 2248, 2808, 3366, 3921, + 4475, 5026, 5573, 6118, 6658, 7195, 7727, 8255, + 8778, 9295, 9807, 10313, 10812, 11306, 11792, 12271, + 12743, 13207, 13664, 14112, 14551, 14982, 15404, 15816, + 16219, 16612, 16995, 17368, 17731, 18082, 18423, 18753, + 19071, 19378, 19674, 19957, 20229, 20488, 20735, 20969, + 21191, 21400, 21596, 21779, 21949, 22106, 22250, 22380, + 22496, 22599, 22689, 22765, 22827, 22875, 22909, 22930, + 22937, 22930, 22909, 22875, 22827, 22765, 22689, 22599, + 22496, 22380, 22250, 22106, 21949, 21779, 21596, 21400, + 21191, 20969, 20735, 20488, 20229, 19957, 19674, 19378, + 19071, 18753, 18423, 18082, 17731, 17368, 16995, 16612, + 16219, 15816, 15404, 14982, 14551, 14112, 13664, 13207, + 12743, 12271, 11792, 11306, 10812, 10313, 9807, 9295, + 8778, 8255, 7727, 7195, 6658, 6118, 5573, 5026, + 4475, 3921, 3366, 2808, 2248, 1687, 1125, 563, + 0, -563, -1125, -1687, -2248, -2808, -3366, -3921, + -4475, -5026, -5573, -6118, -6658, -7195, -7727, -8255, + -8778, -9295, -9807, -10313, -10812, -11306, -11792, -12271, + -12743, -13207, -13664, -14112, -14551, -14982, -15404, -15816, + -16219, -16612, -16995, -17368, -17731, -18082, -18423, -18753, + -19071, -19378, -19674, -19957, -20229, -20488, -20735, -20969, + -21191, -21400, -21596, -21779, -21949, -22106, -22250, -22380, + -22496, -22599, -22689, -22765, -22827, -22875, -22909, -22930, + -22937, -22930, -22909, -22875, -22827, -22765, -22689, -22599, + -22496, -22380, -22250, -22106, -21949, -21779, -21596, -21400, + -21191, -20969, -20735, -20488, -20229, -19957, -19674, -19378, + -19071, -18753, -18423, -18082, -17731, -17368, -16995, -16612, + -16219, -15816, -15404, -14982, -14551, -14112, -13664, -13207, + -12743, -12271, -11792, -11306, -10812, -10313, -9807, -9295, + -8778, -8255, -7727, -7195, -6658, -6118, -5573, -5026, + -4475, -3921, -3366, -2808, -2248, -1687, -1125, -563, +}; + +static inline s16 n31_tone_s16(unsigned int sample, unsigned int rate) +{ + u32 idx; + + rate = n31_pick_rate(rate); + idx = (u32)div_u64((u64)sample * 1000ull * 256ull, rate); + return n31_sin256[idx & 255]; +} + +#endif /* N31_AUDIO_RATES_H */ diff --git a/sound/soc/apple/nano7-audio.c b/sound/soc/apple/nano7-audio.c index f59057a6338ded..0c571f370eff0a 100755 --- a/sound/soc/apple/nano7-audio.c +++ b/sound/soc/apple/nano7-audio.c @@ -1,9 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * N31 ASoC machine — IIS0 CPU DAI + CS42L81 SPI codec + PL080 PCM. + * N31 ASoC machine — IIS0+CS42 playback + IIS2 FM capture (local only). * - * CS42 path has no snd_soc_dapm_route table — analog routing is explicit - * register writes in cs42l81_audio_on(). dai_fmt = I2S NB_NF CBS_CFS + * Playback: IIS0 CPU DAI + CS42L81 SPI codec (peri 10 TX). + * Capture: IIS2 CPU DAI + snd-soc-dummy (peri 13 RX). Userspace loops + * arecord/tinycap → aplay/tinyplay. No FM→BT / A2DP path. + * + * CS42 path has no snd_soc_dapm_route table — analog routing uses explicit + * register writes: cs42_codec_prepare() + cs42_retailos_play_start/stop(). * (SoC master, codec slave, 16-bit S16_LE). */ #include @@ -16,6 +20,11 @@ SND_SOC_DAILINK_DEFS(playback, DAILINK_COMP_ARRAY(COMP_CODEC(NULL, "cs42l81-hifi")), DAILINK_COMP_ARRAY(COMP_PLATFORM("snd-soc-dummy"))); +SND_SOC_DAILINK_DEFS(fm_capture, + DAILINK_COMP_ARRAY(COMP_CPU("bcm2078-pcm")), + DAILINK_COMP_ARRAY(COMP_CODEC("snd-soc-dummy", "snd-soc-dummy-dai")), + DAILINK_COMP_ARRAY(COMP_PLATFORM("snd-soc-dummy"))); + static struct snd_soc_dai_link nano7_dais[] = { { .name = "CS42L81", @@ -25,6 +34,14 @@ static struct snd_soc_dai_link nano7_dais[] = { .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, }, + { + .name = "BCM2078-PCM", + .stream_name = "BCM2078 PCM Capture", + SND_SOC_DAILINK_REG(fm_capture), + .capture_only = 1, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | + SND_SOC_DAIFMT_CBS_CFS, + }, }; static struct snd_soc_card nano7_card = { @@ -37,7 +54,7 @@ static struct snd_soc_card nano7_card = { static int nano7_audio_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; - struct device_node *cpu_np, *codec_np; + struct device_node *cpu_np, *codec_np, *fm_np; int ret; cpu_np = of_parse_phandle(dev->of_node, "apple,cpu", 0); @@ -63,18 +80,34 @@ static int nano7_audio_probe(struct platform_device *pdev) nano7_dais[0].codecs->name = NULL; nano7_dais[0].codecs->dai_name = "cs42l81-hifi"; + fm_np = of_parse_phandle(dev->of_node, "apple,fm-cpu", 0); + if (!fm_np) + fm_np = of_find_compatible_node(NULL, NULL, "apple,s5l8740-iis2"); + if (fm_np) { + nano7_dais[1].cpus->of_node = fm_np; + nano7_dais[1].cpus->dai_name = NULL; + nano7_dais[1].platforms->of_node = fm_np; + nano7_dais[1].platforms->name = NULL; + nano7_card.num_links = 2; + } else { + dev_warn(dev, "no IIS2 fm-cpu — playback-only card\n"); + nano7_card.num_links = 1; + } + nano7_card.dev = dev; ret = devm_snd_soc_register_card(dev, &nano7_card); if (ret) { of_node_put(cpu_np); of_node_put(codec_np); + of_node_put(fm_np); if (ret == -EPROBE_DEFER) return ret; dev_err(dev, "snd_soc_register_card failed: %d\n", ret); return ret; } - dev_info(dev, "nano7g-audio: IIS0 + CS42L81 DAI (no dummy codec)\n"); + dev_info(dev, "nano7g-audio: IIS0+CS42 play%s (no FM→BT)\n", + nano7_card.num_links > 1 ? " + BCM2078 PCM capture (IIS2 RX)" : ""); return 0; } @@ -95,5 +128,5 @@ static struct platform_driver nano7_audio_driver = { module_platform_driver(nano7_audio_driver); MODULE_LICENSE("GPL"); -MODULE_DESCRIPTION("iPod nano 7G ASoC machine"); -MODULE_SOFTDEP("pre: cs42l81_spi s5l8740_i2s"); +MODULE_DESCRIPTION("iPod nano 7G ASoC machine (play + FM capture)"); +MODULE_SOFTDEP("pre: cs42l81_spi s5l8740_i2s s5l8740_iis2"); diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c index c3f444a265b72d..2bd35854ed18c6 100755 --- a/sound/soc/apple/s5l8740-i2s.c +++ b/sound/soc/apple/s5l8740-i2s.c @@ -11,18 +11,22 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include +#include "n31-audio-rates.h" + #define S5L8740_I2S_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) #define S5L8740_I2S_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) #define I2SCLKCON 0x00 @@ -33,8 +37,17 @@ #define I2SRXCOM 0x34 #define I2SSTATUS 0x3c #define I2SCLKDIV 0x40 /* OSOS 4F716: *(base+64). Not Rockbox +0x24. */ +/* RetailOS music IIS0+0x44 readback 0x00010007 (oracle 2026-08-25). */ +#define I2SREG44 0x44 +/* + * OSOS sub_C095E(port, ch): STATUS W1C — TX sticky is bit15 (1<<15). + * Linux never cleared this; silent dumps always show 0x8xxx. + */ +#define I2SSTATUS_TX_W1C 0x8000u #define MCLK_ASSUME_HZ 12000000u -/* BCB60 a3!=0 a5!=24: 1048728|50331649 = 0x100098|0x03000001. Not 0x03100219. */ +/* BCB60 a3!=0 a5!=24: 1048728|50331649 = 0x100098|0x03000001. Not 0x03100219. + * NEVER leave Rockbox 0x0B100019 in txcon — glass SRC sticks at +0x1a and + * FIFO never drains (status 0x82a4). */ #define I2STXCON_N31_16 0x03100099u #define I2SRXCON_N31 0x1000u /* OSOS enable ORs 0x100218 (bit20). Live: that bit holds STATUS at @@ -42,31 +55,55 @@ * Override via txcon= for bring-up; default stays OSOS. */ static uint txcon = I2STXCON_N31_16; module_param(txcon, uint, 0644); -MODULE_PARM_DESC(txcon, "I2STXCON (default 0x03100099 BCB60 16-bit)"); +MODULE_PARM_DESC(txcon, "I2STXCON (default 0x03100099; NOT Rockbox 0x0B100019)"); /* - * D34C0 → 4F716(port, div). 48 kHz is 250, or 125 if 892A02C==6000. - * 0 = 12 MHz / rate (250 @ 48 kHz). + * D34C0 → 4F716(port, div). Table in n31-audio-rates.h. + * 0 = 12 MHz / rate (272 @ 44.1 kHz RetailOS music). */ static uint clkdiv; module_param(clkdiv, uint, 0644); -MODULE_PARM_DESC(clkdiv, "I2SCLKDIV override; 0 = 12000000/rate"); +MODULE_PARM_DESC(clkdiv, "I2SCLKDIV override; 0 = OSOS table / 12000000/rate"); +/* + * Default IIS program rate for clk_run / dma_tone when no ALSA hw_params. + * 0 = RetailOS 44100. ALSA playback uses the PCM rate, not this. + */ +static uint default_rate; +module_param(default_rate, uint, 0644); +MODULE_PARM_DESC(default_rate, "clk_run/dma_tone rate; 0 = 44100 OSOS default"); /* * dma_tone FIFO beat width. Rockbox s5l8702 PCM is 16-bit (WIDTH_16). * Default 2 = 16-bit stereo interleaved (Rockbox/OSOS BCB60 16-bit). - * 4 = packed LR 32-bit beats for pio_tone CPU path. */ static int tone_width = 2; module_param(tone_width, int, 0644); MODULE_PARM_DESC(tone_width, "dma_tone dst width bytes 2 or 4 (default 2)"); /* - * OSOS B6620(port,0) does TXCOM |= 6 after PL080 is armed (peri 12). - * That is a DMA kick. CPU PIO has no DMA: Rockbox-family bit 3 must be - * set or the serializer never leaves STATUS 0x24. Default 0xC = PIO. + * dma_tone / pio_tone sample rate. 0 = default_rate / OSOS 44100. + */ +static uint tone_rate; +module_param(tone_rate, uint, 0644); +MODULE_PARM_DESC(tone_rate, "dma_tone/pio_tone rate; 0 = OSOS 44100"); + +/* Keep TX/codec up this long after START even if ALSA xruns. */ +static uint sustain_ms = 5000; +module_param(sustain_ms, uint, 0644); +MODULE_PARM_DESC(sustain_ms, "ignore ALSA STOP for this many ms after START (default 5000)"); + +static uint fifo_prefill = 16; +module_param(fifo_prefill, uint, 0644); +MODULE_PARM_DESC(fifo_prefill, "stereo words to push into TX FIFO before TXCOM kick"); +/* + * OSOS B6620(port,0) does TXCOM |= 6 after PL080 is armed (peri 10). + * RE body: sub_B6620 only ORs 0x6 — not 0xC. Hybrid 0xE was Linux invention. */ #define I2STXCOM_DMA 0x6 #define I2STXCOM_PIO 0xc #define I2STXCOM_STOP 0x0 #define CLKCON_PHYS 0x3c500000ul +/* RetailOS oracle dwords at CLKCON+0x30 (music vs idle/A2DP). */ +#define CLKCON_AUDIO_OFF 0x30 +#define CLKCON_AUDIO_PLAY 0x32190u +#define CLKCON_AUDIO_IDLE 0x1c20u #define GPIO_PHYS 0x3cf00000ul #define GPIOCMD_PHYS 0x3cf001e0ul @@ -117,18 +154,55 @@ static u32 s5l8740_scale_lr(u32 sample) static int use_pio; module_param(use_pio, int, 0644); -MODULE_PARM_DESC(use_pio, "1 = CPU FIFO PCM; 0 = OSOS PL080 M2P peri 12 (default)"); +MODULE_PARM_DESC(use_pio, "1 = CPU FIFO PCM; 0 = PL080 M2P peri 10 from DT (default)"); static int txcom_pio = I2STXCOM_PIO; module_param(txcom_pio, int, 0644); MODULE_PARM_DESC(txcom_pio, "TXCOM when use_pio=1 (default 0xC; OSOS DMA is 0x6)"); +/* + * TXCOM kick mode (checkpoint-003 / handoff P0.3): + * 0 = retail: TXCOM |= 0x6 after DMA armed + * 1 = pio: TXCOM = txcom_pio (0xC) + * 2 = hybrid: bit3 then |0x6 (glass experimental default) + */ +/* + * RetailOS music-playing SCSI oracle 2026-08-25: TXCOM readback = 0x6. + * sub_B6620 does |= 6 only. Hybrid 0xE was a Linux false lead. + */ +static int txcom_mode; +module_param(txcom_mode, int, 0644); +MODULE_PARM_DESC(txcom_mode, "0=retail |=6 (default), 1=pio 0xC, 2=hybrid |C|6"); + +/* + * txcom_exact: when >=0, tx_kick writes this value exactly (no OR). -1=use mode. + * Isolation tests: 0x6 / 0xC / 0xE from TXCOM=0 baseline. + */ +static int txcom_exact = -1; +module_param(txcom_exact, int, 0644); +MODULE_PARM_DESC(txcom_exact, "Exact TXCOM write when >=0; -1=txcom_mode (default)"); + +/* RetailOS local music CLKDIV=0x110 (272) ≈ 12 MHz / 44100. */ + /* BCB60 sets DIR. Live pad_oe=0: GPIO 7/20 stop, GPIO 6 still * toggles — BCLK/LRCK are SoC-driven, not codec-master. */ static int pad_oe = 1; module_param(pad_oe, int, 0644); MODULE_PARM_DESC(pad_oe, "1 = OSOS DIR out (default); 0 = mode 3, DIR in"); +/* + * Pad bring-up variants (exhaust RE before RetailOS GPIO oracle): + * 0 = local 43D38C(7,3)(20,3) only [BCB60 — CONFIRMED OSOS body] + * 1 = +43D38C(6,3) — NO OSOS 43D38C call site for GPIO6; debug only + * 2 = gpio-s5l8740 s5l8740_iis0_pads_enable(3) — SEC pinmux + GPIOCMD + * 3 = gpio driver mode 2 (BCB60 teardown) + * 4 = SEC pinmux 6/7/20 + local mode3 on all three + * 5 = mode2 + SEC pinmux refresh (func2 only, no mode3) + */ +static int pad_mode; +module_param(pad_mode, int, 0644); +MODULE_PARM_DESC(pad_mode, "0=7/20 local; 1=+gpio6; 2=gpio drv; 3=mode2; 4=SEC+6/7/20; 5=SEC func2 only"); + struct s5l8740_i2s { void __iomem *base; void __iomem *clkcon; @@ -139,6 +213,7 @@ struct s5l8740_i2s { int num_clks; bool has_dma; struct dma_chan *tx_chan; /* cached — avoid dma:tx symlink churn */ + u8 tx_chan_borrowed; /* 1 = lookup_peri, do not dma_release */ struct mutex dma_lock; struct snd_dmaengine_dai_dma_data play_dma; struct snd_pcm_substream *ss; @@ -146,15 +221,36 @@ struct s5l8740_i2s { bool pio_run; unsigned int pio_hw_ptr; unsigned int rate; + struct delayed_work dma_watch; + unsigned long play_jiffies; + u32 last_dma_src; + u8 watch_ticks; }; /* * SEC sub_2034 leftovers. OSOS 983430 never programs clock 9; * it does program clocks 6/20 into +0x1C after SEC. If U-Boot * zeroed the pair, IIS has no parent. Do not write +00/+04/+44. + * + * RetailOS music-playing oracle (checkpoint-010): +0x1C = 0xD0052003. + * Leaving the SEC bring-up value 0x10122003 yields IIS STATUS 0x82A0 + * and a silent jack even with TXCOM=6 / CS42 unmuted. Always force + * the stock music parent when force_stock_audio_parent=1 (default). */ -#define SEC_CLKCON_18 0x20012001u -#define SEC_CLKCON_1C 0x10122003u +#define SEC_CLKCON_18 0x20012001u +#define SEC_CLKCON_1C 0x10122003u +/* artifacts/retailos-mmio/music-playing/CLKCON.bin — checkpoint-010 */ +#define STOCK_CLKCON_08 0xa009200au +#define STOCK_CLKCON_0C 0x80000001u +#define STOCK_CLKCON_10 0x00008000u +#define STOCK_CLKCON_14 0x80002200u +#define STOCK_CLKCON_18 0x20012001u +#define STOCK_CLKCON_1C 0xD0052003u + +static bool force_stock_audio_parent = true; +module_param(force_stock_audio_parent, bool, 0644); +MODULE_PARM_DESC(force_stock_audio_parent, + "1=force CLKCON+0x08..0x1C to RetailOS music-playing snapshot"); /* sub_41CBD8(9,1): CLKCON+0x0C bit 15 clear = IIS0 CG16 on. */ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) @@ -165,36 +261,178 @@ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) return; r18 = readl(i2s->clkcon + 0x18); r1c = readl(i2s->clkcon + 0x1c); - if (!r18) - writel(SEC_CLKCON_18, i2s->clkcon + 0x18); - if (!r1c) - writel(SEC_CLKCON_1C, i2s->clkcon + 0x1c); - v = readl(i2s->clkcon + 0x0c); - if (v & 0x8000u) - writel(v & ~0x8000u, i2s->clkcon + 0x0c); + if (force_stock_audio_parent) { + /* + * +0x1C alone left STATUS at 0x82A0. Push the rest of the + * music-playing parent snapshot (checkpoint-010 §5.B). + */ + writel(STOCK_CLKCON_08, i2s->clkcon + 0x08); + writel(STOCK_CLKCON_0C, i2s->clkcon + 0x0c); + writel(STOCK_CLKCON_10, i2s->clkcon + 0x10); + writel(STOCK_CLKCON_14, i2s->clkcon + 0x14); + writel(STOCK_CLKCON_18, i2s->clkcon + 0x18); + writel(STOCK_CLKCON_1C, i2s->clkcon + 0x1c); + } else { + if (!r18) + writel(SEC_CLKCON_18, i2s->clkcon + 0x18); + if (!r1c) + writel(SEC_CLKCON_1C, i2s->clkcon + 0x1c); + v = readl(i2s->clkcon + 0x0c); + if (v & 0x8000u) + writel(v & ~0x8000u, i2s->clkcon + 0x0c); + } +} + +/* RetailOS absolute dword — better than sticky play when idle. */ +static void s5l8740_i2s_clkcon_audio(struct s5l8740_i2s *i2s, u32 val) +{ + if (!i2s || !i2s->clkcon) + return; + writel(val, i2s->clkcon + CLKCON_AUDIO_OFF); } -/* sub_43D38C(7,3) and (20,3) — IIS0 pads only (do not touch 0x0A061010 / GPIO86). */ +/* + * STOP teardown (beats RetailOS sticky TX): TXCOM stop → terminate DMA → + * clear I2SCLKCON → CLKCON+0x30 idle dword. + */ +static void s5l8740_i2s_hw_stop(struct s5l8740_i2s *i2s, + struct snd_pcm_substream *substream) +{ + struct dma_chan *chan = NULL; + + if (!i2s || !i2s->base) + return; + + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + + if (substream && i2s->has_dma && !use_pio) + chan = snd_dmaengine_pcm_get_chan(substream); + if (!chan) + chan = i2s->tx_chan; + if (chan) + dmaengine_terminate_sync(chan); + + writel(0, i2s->base + I2SCLKCON); + s5l8740_i2s_clkcon_audio(i2s, CLKCON_AUDIO_IDLE); +} + +/* Packed pinmux word — same as gpio-s5l8740 sub_223C / sub_47CC. */ +static void s5l8740_i2s_pinmux_word(struct s5l8740_i2s *i2s, u32 word) +{ + unsigned int bank = (word >> 24) & 0xff; + unsigned int pin = (word >> 16) & 0xff; + void __iomem *base; + u32 v; + + if (!i2s->gpio) + return; + base = i2s->gpio + 32u * bank; + v = readl(base + 0x00); + writel(((word & 0xfu) << (4u * pin)) | (v & ~(15u << (4u * pin))), + base + 0x00); + v = readl(base + 0x14); + writel((((word >> 12) & 1u) << pin) | (v & ~BIT(pin)), base + 0x14); + v = readl(base + 0x0c); + writel((((word >> 4) & 1u) << pin) | (v & ~BIT(pin)), base + 0x0c); + v = readl(base + 0x10); + writel((((word >> 8) & 1u) << pin) | (v & ~BIT(pin)), base + 0x10); +} + +static void s5l8740_i2s_gpiocmd(struct s5l8740_i2s *i2s, unsigned int gpio, u8 cmd) +{ + unsigned int bank = gpio >> 3; + unsigned int pin = gpio & 7; + void __iomem *b; + u32 dir; + + if (!i2s->gpio || !i2s->gpiocmd) + return; + b = i2s->gpio + 32 * bank; + dir = readl(b + 0x14); + writel(dir | BIT(pin), b + 0x14); + writel((bank << 16) | (pin << 8) | cmd, i2s->gpiocmd); +} + +static void s5l8740_i2s_log_iis_gpio(struct s5l8740_i2s *i2s, const char *tag) +{ + void (*logpads)(const char *); + + logpads = (void (*)(const char *))__symbol_get("s5l8740_gpio_log_iis0_pads"); + if (logpads) { + logpads(tag); + __symbol_put("s5l8740_gpio_log_iis0_pads"); + } else if (i2s->dev) { + dev_info(i2s->dev, "%s pads (no gpio export)\n", tag); + } +} + +/* sub_43D38C(7,3) and (20,3) — IIS0 pads. Optional (6,3) in pad_mode 1/4. */ static void s5l8740_i2s_pads(struct s5l8740_i2s *i2s) { - static const u8 gpios[] = { 7, 20 }; + static const u8 sec_words[] = { 6, 7, 20 }; unsigned int i; - if (!i2s->gpio || !i2s->gpiocmd) + if (pad_mode == 2) { + void (*en)(unsigned int); + + en = (void (*)(unsigned int))__symbol_get("s5l8740_iis0_pads_enable"); + if (en) { + en(3); + __symbol_put("s5l8740_iis0_pads_enable"); + } + s5l8740_i2s_log_iis_gpio(i2s, "pads-mode2-gpio"); return; - for (i = 0; i < ARRAY_SIZE(gpios); i++) { - unsigned int gpio = gpios[i]; - unsigned int bank = gpio >> 3; - unsigned int pin = gpio & 7; - void __iomem *b = i2s->gpio + 32 * bank; - u32 dir = readl(b + 0x14); - - if (pad_oe) - writel(dir | BIT(pin), b + 0x14); - else - writel(dir & ~BIT(pin), b + 0x14); - writel((bank << 16) | (pin << 8) | 3, i2s->gpiocmd); } + + if (pad_mode == 3) { + void (*en)(unsigned int); + + en = (void (*)(unsigned int))__symbol_get("s5l8740_iis0_pads_enable"); + if (en) { + en(2); + __symbol_put("s5l8740_iis0_pads_enable"); + } + s5l8740_i2s_log_iis_gpio(i2s, "pads-mode3-off"); + return; + } + + if (pad_mode >= 4) { + s5l8740_i2s_pinmux_word(i2s, 0x00061002u); + s5l8740_i2s_pinmux_word(i2s, 0x00071002u); + s5l8740_i2s_pinmux_word(i2s, 0x02041002u); + } + + if (pad_mode == 5) { + s5l8740_i2s_log_iis_gpio(i2s, "pads-mode5-sec-only"); + return; + } + + for (i = 0; i < ARRAY_SIZE(sec_words); i++) { + unsigned int g = sec_words[i]; + + if (g == 6 && pad_mode != 1 && pad_mode != 4) + continue; + if (pad_oe || g == 7 || g == 20) + s5l8740_i2s_gpiocmd(i2s, g, 3); + } + + /* + * RetailOS music/idle gpio.bin bank0 PCON = 0x32112224 + * (pins4/5 = func1). Linux often left 0x32222224 — force stock. + */ + if (i2s->gpio) { + u32 p0 = readl(i2s->gpio); + + if (p0 != 0x32112224u) { + writel(0x32112224u, i2s->gpio); + if (i2s->dev) + dev_info(i2s->dev, + "PCON0 %08x -> 32112224 (RetailOS music)\n", + p0); + } + } + + s5l8740_i2s_log_iis_gpio(i2s, "pads-applied"); } static void s5l8740_i2s_c09ac_start(struct s5l8740_i2s *i2s); @@ -217,7 +455,41 @@ static int s5l8740_i2s_codec_prepare(void) return ret; } -static int s5l8740_i2s_asp_lock(void) +static int s5l8740_i2s_codec_play_start(void) +{ + int (*start)(void) = __symbol_get("cs42l81_play_start"); + int ret = 0; + + if (start) { + ret = start(); + __symbol_put("cs42l81_play_start"); + } + return ret; +} + +static void s5l8740_i2s_codec_play_stop(void) +{ + void (*stop)(void) = __symbol_get("cs42l81_play_stop"); + + if (stop) { + stop(); + __symbol_put("cs42l81_play_stop"); + } +} + +static int s5l8740_i2s_audio_path_mode(void) +{ + int (*mode)(void) = __symbol_get("cs42l81_get_audio_path_mode"); + int m = 1; + + if (mode) { + m = mode(); + __symbol_put("cs42l81_get_audio_path_mode"); + } + return m; +} + +static int __maybe_unused s5l8740_i2s_asp_lock(void) { int (*asp)(void) = __symbol_get("cs42l81_post_iis_start"); int ret = -ENOENT; @@ -230,64 +502,147 @@ static int s5l8740_i2s_asp_lock(void) return ret; } -static void s5l8740_i2s_fifo_write(struct s5l8740_i2s *i2s, s16 s) +static int __maybe_unused s5l8740_i2s_asp_hold_light(void) { - unsigned int n; - u32 status; + int (*hold)(void) = __symbol_get("cs42l81_asp_hold_light"); + int ret = -ENOENT; - for (n = 0; n < fifo_wait_loops; n++) { + if (hold) { + ret = hold(); + __symbol_put("cs42l81_asp_hold_light"); + } + return ret; +} + +static void s5l8740_i2s_log_clocks(struct s5l8740_i2s *i2s, const char *tag) +{ + u32 c0c = 0, c18 = 0, c1c = 0; + u32 clkcon = 0, txcon = 0, txcom = 0, rxcon = 0, rxcom = 0; + u32 status = 0, clkdiv = 0; + + if (!i2s || !i2s->dev) + return; + if (i2s->clkcon) { + c0c = readl(i2s->clkcon + 0x0c); + c18 = readl(i2s->clkcon + 0x18); + c1c = readl(i2s->clkcon + 0x1c); + } + if (i2s->base) { + clkcon = readl(i2s->base + I2SCLKCON); + txcon = readl(i2s->base + I2STXCON); + txcom = readl(i2s->base + I2STXCOM); + rxcon = readl(i2s->base + I2SRXCON); + rxcom = readl(i2s->base + I2SRXCOM); status = readl(i2s->base + I2SSTATUS); - if (!(status & 0x20)) - break; - cpu_relax(); + clkdiv = readl(i2s->base + I2SCLKDIV); } - writel((u32)(u16)s | ((u32)(u16)s << 16), i2s->base + I2STXFIFO); + dev_info(i2s->dev, + "%s CLKCON+0C=%08x +18=%08x +1C=%08x IIS0 +00=%08x +04=%08x +08=%08x +30=%08x +34=%08x +3C=%08x +40=%08x\n", + tag, c0c, c18, c1c, clkcon, txcon, txcom, rxcon, rxcom, status, + clkdiv); } -static int s5l8740_i2s_play_start(struct s5l8740_i2s *i2s, bool dma) +static void s5l8740_i2s_pre_codec(void) { - int ret; + void (*pre)(void); - ret = s5l8740_i2s_codec_prepare(); - if (ret && i2s->dev) - dev_warn(i2s->dev, "codec prepare: %d\n", ret); - s5l8740_i2s_program(i2s, 48000); - s5l8740_i2s_tx_kick(i2s, dma); - ret = s5l8740_i2s_asp_lock(); - if (i2s->dev) - dev_info(i2s->dev, "play_start dma=%d asp=%d status=0x%x txcom=0x%x\n", - dma, ret, readl(i2s->base + I2SSTATUS), - readl(i2s->base + I2STXCOM)); - return ret; + pre = (void (*)(void))__symbol_get("cs42l81_pre_iis_start"); + if (pre) { + pre(); + __symbol_put("cs42l81_pre_iis_start"); + } +} + +static void s5l8740_i2s_schedule_asp(void) +{ + void (*sched)(void); + + sched = (void (*)(void))__symbol_get("cs42l81_schedule_post_iis"); + if (sched) { + sched(); + __symbol_put("cs42l81_schedule_post_iis"); + } +} + +static void s5l8740_i2s_cancel_asp(void) +{ + void (*cancel)(void); + + cancel = (void (*)(void))__symbol_get("cs42l81_cancel_post_iis"); + if (cancel) { + cancel(); + __symbol_put("cs42l81_cancel_post_iis"); + } +} +static void s5l8740_i2s_log_txcon(struct device *dev, u32 v, const char *tag) +{ + dev_info(dev, + "%s txcon=0x%08x %s%s%s%s%s\n", + tag, v, + v == 0x0B100019u ? "ROCKBOX-POISON " : "", + (v & 0x03000001u) ? "BCB60-low " : "", + (v & 0x00100098u) ? "16bit " : "", + v == 0x03100219u ? "extclk " : "", + v == 0x03100099u ? "intclk " : ""); } /* * 345D70 is JUMPOUT 0x22000350 = bootloader sub_350 (SCTLR C-bit). * Play 414FAE only starts — it does not C09AC-stop first. + * RetailOS music: I2SCLKCON = 0x1 (not 0x2 stop-ack). */ static void s5l8740_i2s_c09ac_start(struct s5l8740_i2s *i2s) { writel(1, i2s->base + I2SCLKCON); } +/* OSOS sub_C095E(port, 0): clear TX sticky STATUS bit15 (W1C). */ +static void s5l8740_i2s_status_w1c_tx(struct s5l8740_i2s *i2s) +{ + u32 before, after; + + if (!i2s || !i2s->base) + return; + before = readl(i2s->base + I2SSTATUS); + writel(I2SSTATUS_TX_W1C, i2s->base + I2SSTATUS); + after = readl(i2s->base + I2SSTATUS); + if (i2s->dev && (before & I2SSTATUS_TX_W1C)) + dev_info(i2s->dev, "STATUS W1C tx sticky %08x->%08x\n", + before, after); +} + /* 26DDDE: 41CBD8(9,1), 5705DC RX, 414FAE (C09AC + BCB60), D34C0 CLKDIV. */ static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate) { - u32 div = clkdiv ? clkdiv : MCLK_ASSUME_HZ / (rate ? rate : 48000); + const struct n31_rate_cfg *r = n31_find_rate(rate); + u32 div; u32 rxcom; + if (clkdiv) + div = clkdiv; + else if (r) + div = r->clkdiv; + else + div = MCLK_ASSUME_HZ / n31_pick_rate(rate); if (div < 1) div = 1; s5l8740_i2s_ungate(i2s); + s5l8740_i2s_clkcon_audio(i2s, CLKCON_AUDIO_PLAY); s5l8740_i2s_c09ac_start(i2s); s5l8740_i2s_pads(i2s); + /* sub_BCB60 a3!=0 a5=16 → TXCON; +0x30 = 0x1000 */ writel(txcon, i2s->base + I2STXCON); + if (i2s->dev) + s5l8740_i2s_log_txcon(i2s->dev, txcon, "program"); writel(I2SRXCON_N31, i2s->base + I2SRXCON); rxcom = readl(i2s->base + I2SRXCOM); writel(rxcom & ~4u, i2s->base + I2SRXCOM); writel(div, i2s->base + I2SCLKDIV); - /* C095E/BB9F8 is not on the 26DDDE play path. Bit15 looks W1C. */ - i2s->rate = rate ? rate : 48000; + /* RetailOS music IIS0+0x44 = 0x00010007 (IIS2 already programs this). */ + writel(0x00010007u, i2s->base + I2SREG44); + /* Setup only — TXCOM stays 0 until .trigger START (OSOS B6620). */ + writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + i2s->rate = n31_pick_rate(rate); } /* @@ -296,18 +651,92 @@ static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate) */ static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma) { - u32 txcom; + u32 txcom, before, after; if (!i2s || !i2s->base) return; - if (dma) { - txcom = readl(i2s->base + I2STXCOM); - writel(txcom | I2STXCOM_PIO, i2s->base + I2STXCOM); - writel(txcom | I2STXCOM_PIO | I2STXCOM_DMA, - i2s->base + I2STXCOM); + s5l8740_i2s_pre_codec(); + { + unsigned int n = fifo_prefill, i; + unsigned int rate = i2s->rate ? i2s->rate : N31_RATE_DEFAULT; + s16 s; + + if (n > 64) + n = 64; + for (i = 0; i < n; i++) { + s = s5l8740_scale_s16(n31_tone_s16(i, rate)); + writel(((u32)(u16)s << 16) | (u16)s, + i2s->base + I2STXFIFO); + } + } + before = readl(i2s->base + I2STXCOM); + if (txcom_exact >= 0) { + writel((u32)txcom_exact, i2s->base + I2STXCOM); + } else if (dma) { + switch (txcom_mode) { + case 0: /* retail: OSOS B6620 TXCOM = 0x6 after DMA armed */ + writel(I2STXCOM_DMA, i2s->base + I2STXCOM); + break; + case 1: /* pio-only kick (debug) */ + writel(txcom_pio, i2s->base + I2STXCOM); + break; + default: /* hybrid: glass bit3 + DMA */ + txcom = before; + writel(txcom | I2STXCOM_PIO, i2s->base + I2STXCOM); + writel(txcom | I2STXCOM_PIO | I2STXCOM_DMA, + i2s->base + I2STXCOM); + break; + } } else { writel(txcom_pio, i2s->base + I2STXCOM); } + after = readl(i2s->base + I2STXCOM); + /* RetailOS music never leaves TX sticky 0x8000 set — clear after kick. */ + s5l8740_i2s_status_w1c_tx(i2s); + /* Keep C09AC start bit; 0x2 is stop-ack class (sub_C09AC wait). */ + if ((readl(i2s->base + I2SCLKCON) & 1u) == 0) + writel(1, i2s->base + I2SCLKCON); + if (i2s->dev) + dev_info_ratelimited(i2s->dev, + "tx_kick dma=%d mode=%d txcom %08x->%08x status=%08x clkcon=%08x\n", + dma, txcom_mode, before, after, + readl(i2s->base + I2SSTATUS), + readl(i2s->base + I2SCLKCON)); +} + +int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en); + +static void s5l8740_i2s_dma_watch(struct work_struct *work) +{ + struct s5l8740_i2s *i2s = container_of(work, struct s5l8740_i2s, + dma_watch.work); + u32 src = 0, dst = 0, en = 0, st, txcom; + int ret; + + if (!i2s || !i2s->base) + return; + ret = s5l_pl080_peri_snapshot(10, &src, &dst, &en); + st = readl(i2s->base + I2SSTATUS); + txcom = readl(i2s->base + I2STXCOM); + { + const char *tag; + + if (ret) + tag = "NOPERI"; + else if (i2s->watch_ticks == 0) + tag = "BASE"; + else if (src != i2s->last_dma_src) + tag = "WALK"; + else + tag = "STUCK"; + dev_info(i2s->dev, + "dma_watch t=%ums src=%08x %s dst=%08x en=%x status=%08x txcom=%08x\n", + i2s->watch_ticks * 100, src, tag, dst, en, st, txcom); + } + i2s->last_dma_src = src; + i2s->watch_ticks++; + if (i2s->watch_ticks < 50) + schedule_delayed_work(&i2s->dma_watch, msecs_to_jiffies(100)); } static int s5l8740_i2s_hw_params(struct snd_pcm_substream *substream, @@ -316,15 +745,24 @@ static int s5l8740_i2s_hw_params(struct snd_pcm_substream *substream, { struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); unsigned int rate = params_rate(params); + const struct n31_rate_cfg *r = n31_find_rate(rate); u32 div; + int ret; if (!i2s || !i2s->base) return -ENODEV; + if (!r && !clkdiv) + return -EINVAL; + ret = s5l8740_i2s_codec_prepare(); + if (ret && i2s->dev) + dev_warn(i2s->dev, "codec prepare in hw_params: %d\n", ret); s5l8740_i2s_program(i2s, rate); - div = MCLK_ASSUME_HZ / (i2s->rate ? i2s->rate : 48000); - dev_info(dai->dev, "IIS hw_params rate=%u clkdiv=%u dma=%d pio=%d txcom=0x%x\n", - rate, div, i2s->has_dma, use_pio, - use_pio ? txcom_pio : I2STXCOM_DMA); + div = clkdiv ? clkdiv : (r ? r->clkdiv : 0); + s5l8740_i2s_log_clocks(i2s, "hw_params"); + dev_info(dai->dev, + "IIS hw_params rate=%u code=%u clkdiv=%u dma=%d pio=%d txcom=%08x\n", + rate, r ? r->cs42_rate_code : 0, div, i2s->has_dma, use_pio, + readl(i2s->base + I2STXCOM)); return 0; } @@ -332,6 +770,7 @@ static int s5l8740_i2s_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai) { struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); + int path_mode; if (!i2s || !i2s->base) return -ENODEV; @@ -339,14 +778,41 @@ static int s5l8740_i2s_trigger(struct snd_pcm_substream *substream, int cmd, case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + path_mode = s5l8740_i2s_audio_path_mode(); + if (path_mode == 1) + s5l8740_i2s_codec_play_start(); s5l8740_i2s_tx_kick(i2s, !use_pio); + if (path_mode == 2) + s5l8740_i2s_codec_play_start(); + s5l8740_i2s_log_clocks(i2s, "trigger_start"); + s5l8740_i2s_schedule_asp(); i2s->pio_run = use_pio; + i2s->play_jiffies = jiffies; + i2s->watch_ticks = 0; + i2s->last_dma_src = 0; + mod_delayed_work(system_wq, &i2s->dma_watch, msecs_to_jiffies(100)); + dev_info(dai->dev, + "DAI trigger START path_mode=%d txcom=%08x sustain=%ums\n", + path_mode, readl(i2s->base + I2STXCOM), sustain_ms); return 0; case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + if (sustain_ms && + time_before(jiffies, + i2s->play_jiffies + + msecs_to_jiffies(sustain_ms))) { + dev_info_ratelimited(dai->dev, + "DAI trigger STOP ignored (%ums sustain)\n", + sustain_ms); + return 0; + } i2s->pio_run = false; - writel(I2STXCOM_STOP, i2s->base + I2STXCOM); + cancel_delayed_work(&i2s->dma_watch); + s5l8740_i2s_cancel_asp(); + s5l8740_i2s_codec_play_stop(); + s5l8740_i2s_hw_stop(i2s, substream); + dev_info_ratelimited(dai->dev, "DAI trigger STOP txcom=0\n"); return 0; default: return -EINVAL; @@ -420,7 +886,7 @@ static int s5l8740_pio_thread(void *data) * (~10 ms) and a 3s tone hung for a minute. Burst ~4 ms of * realtime writes, then cond_resched so RNDIS still runs. */ - rate = i2s->rate ? i2s->rate : 48000; + rate = i2s->rate ? i2s->rate : N31_RATE_DEFAULT; burst = rate / 250; /* ~4 ms */ if (burst < 16) burst = 16; @@ -515,7 +981,7 @@ static ssize_t regs_show(struct device *dev, struct device_attribute *attr, struct s5l8740_i2s *i2s = dev_get_drvdata(dev); static const u32 offs[] = { I2SCLKCON, I2STXCON, I2STXCOM, I2STXFIFO, - I2SRXCON, I2SRXCOM, I2SSTATUS, I2SCLKDIV, + I2SRXCON, I2SRXCOM, I2SSTATUS, I2SCLKDIV, I2SREG44, }; int i, n = 0; @@ -527,7 +993,7 @@ static ssize_t regs_show(struct device *dev, struct device_attribute *attr, if (i2s->clkcon) { static const u32 clk_offs[] = { 0x00, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c, - 0x44, 0x48, 0x4c, 0x58, 0x68, 0x6c, + 0x30, 0x44, 0x48, 0x4c, 0x58, 0x68, 0x6c, }; int c; @@ -569,65 +1035,113 @@ static ssize_t volume_show(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RW(volume); -/* 1 kHz sine @ 48 kHz, 48 samples/period, peak 32767. No FP in the loop. */ -static const s16 sine_1khz_48k[48] = { - 0, 4277, 8481, 12540, 16384, 19948, 23170, 25997, - 28378, 30274, 31651, 32487, 32767, 32487, 31651, 30274, - 28378, 25997, 23170, 19948, 16384, 12540, 8481, 4277, - 0, -4277, -8481, -12540, -16384, -19948, -23170, -25997, - -28378, -30274, -31651, -32487, -32767, -32487, -31651, -30274, - -28378, -25997, -23170, -19948, -16384, -12540, -8481, -4277, -}; - -/* CPU-paced FIFO write. TXCOM 6 = OSOS B6620 TX start. */ -static ssize_t pio_tone_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t pad_scan_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct s5l8740_i2s *i2s = dev_get_drvdata(dev); - unsigned int frames, i; - s16 s; + u32 xor[8] = { }, pcon[8] = { }, last[8] = { }; + unsigned int b, i, n = 0; - if (!i2s || !i2s->base) + if (!i2s || !i2s->gpio) return -ENODEV; - if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') - return -EINVAL; - - s5l8740_i2s_play_start(i2s, false); + for (b = 0; b < 8; b++) { + pcon[b] = readl(i2s->gpio + 32 * b); + last[b] = readl(i2s->gpio + 32 * b + 4); + } + for (i = 0; i < 40000; i++) { + for (b = 0; b < 8; b++) { + u32 d = readl(i2s->gpio + 32 * b + 4); - frames = 48000 * 2; /* ~2 s */ - for (i = 0; i < frames; i++) { - s = s5l8740_scale_s16(sine_1khz_48k[i % 48]); - s5l8740_i2s_fifo_write(i2s, s); - udelay(20); + xor[b] |= d ^ last[b]; + last[b] = d; + } } - writel(I2STXCOM_STOP, i2s->base + I2STXCOM); - dev_info(dev, "pio_tone 2s done status=0x%08x\n", - readl(i2s->base + I2SSTATUS)); - return count; + n += scnprintf(buf + n, PAGE_SIZE - n, + "clkcon=0x%x txcon=0x%x txcom=0x%x status=0x%x\n", + readl(i2s->base + I2SCLKCON), + readl(i2s->base + I2STXCON), + readl(i2s->base + I2STXCOM), + readl(i2s->base + I2SSTATUS)); + for (b = 0; b < 8; b++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "b%u pcon=%08x xor=%02x\n", b, pcon[b], xor[b]); + return n; } -static DEVICE_ATTR_WO(pio_tone); +static DEVICE_ATTR_RO(pad_scan); struct dma_chan *s5l_pl080_request_slave(struct device *consumer, unsigned int idx); +struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri); -static struct dma_chan *s5l8740_i2s_tx_get(struct s5l8740_i2s *i2s, - struct device *dev) +static struct dma_chan *s5l8740_i2s_tx_get(struct s5l8740_i2s *i2s) { + struct dma_chan *chan; + if (i2s->tx_chan) return i2s->tx_chan; - i2s->tx_chan = s5l_pl080_request_slave(dev, 0); - return i2s->tx_chan; + chan = s5l_pl080_lookup_peri(10); + if (chan) { + i2s->tx_chan = chan; + i2s->tx_chan_borrowed = 1; + return chan; + } + chan = s5l_pl080_request_slave(i2s->dev, 0); + if (IS_ERR_OR_NULL(chan)) + return chan; + i2s->tx_chan = chan; + i2s->tx_chan_borrowed = 0; + return chan; } static void s5l8740_i2s_tx_put(struct s5l8740_i2s *i2s) { if (!i2s || !i2s->tx_chan) return; - dma_release_channel(i2s->tx_chan); + if (!i2s->tx_chan_borrowed) + dma_release_channel(i2s->tx_chan); i2s->tx_chan = NULL; + i2s->tx_chan_borrowed = 0; } -/* One-shot OSOS path: PL080 M2P -> +0x10, then TXCOM=0xE. */ +static unsigned int s5l8740_i2s_tone_rate(void) +{ + if (tone_rate) + return n31_pick_rate(tone_rate); + return n31_pick_rate(default_rate); +} + +static ssize_t pio_tone_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + unsigned int rate, frames, i; + s16 s; + + if (!i2s || !i2s->base) + return -ENODEV; + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + + rate = s5l8740_i2s_tone_rate(); + s5l8740_i2s_codec_prepare(); + s5l8740_i2s_program(i2s, rate); + s5l8740_i2s_codec_play_start(); + s5l8740_i2s_tx_kick(i2s, false); + + frames = rate * 2; + for (i = 0; i < frames; i++) { + s = s5l8740_scale_s16(n31_tone_s16(i, rate)); + writel(((u32)(u16)s << 16) | (u16)s, i2s->base + I2STXFIFO); + udelay(1000000 / rate); + } + s5l8740_i2s_codec_play_stop(); + s5l8740_i2s_hw_stop(i2s, NULL); + dev_info(dev, "pio_tone 2s rate=%u status=0x%08x\n", + rate, readl(i2s->base + I2SSTATUS)); + return count; +} +static DEVICE_ATTR_WO(pio_tone); + static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { @@ -638,8 +1152,8 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, dma_cookie_t cookie; dma_addr_t dma; s16 *tone; - size_t bytes = 48000 * 2 * 2 * 2; /* 2 s stereo S16 */ - unsigned int i; + unsigned int rate, frames, i; + size_t bytes; s16 s; int ret; @@ -648,10 +1162,14 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') return -EINVAL; + rate = s5l8740_i2s_tone_rate(); + frames = n31_tone_period_frames(rate); + bytes = frames * 2 * sizeof(s16); + mutex_lock(&i2s->dma_lock); - chan = s5l8740_i2s_tx_get(i2s, dev); - if (IS_ERR(chan)) { - ret = PTR_ERR(chan); + chan = s5l8740_i2s_tx_get(i2s); + if (IS_ERR_OR_NULL(chan)) { + ret = chan ? PTR_ERR(chan) : -ENODEV; dev_err(dev, "dma_tone request tx: %d\n", ret); mutex_unlock(&i2s->dma_lock); return ret; @@ -662,8 +1180,8 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, ret = -ENOMEM; goto out_unlock; } - for (i = 0; i < bytes / 4; i++) { - s = s5l8740_scale_s16(sine_1khz_48k[i % 48]); + for (i = 0; i < frames; i++) { + s = s5l8740_scale_s16(n31_tone_s16(i, rate)); tone[i * 2] = s; tone[i * 2 + 1] = s; } @@ -671,105 +1189,45 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, cfg.direction = DMA_MEM_TO_DEV; cfg.dst_addr = i2s->play_dma.addr; - if (tone_width == 2) - cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; - else - cfg.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + cfg.dst_addr_width = (tone_width == 2) ? + DMA_SLAVE_BUSWIDTH_2_BYTES : DMA_SLAVE_BUSWIDTH_4_BYTES; cfg.dst_maxburst = 1; ret = dmaengine_slave_config(chan, &cfg); if (ret) { dev_err(dev, "dma_tone slave_config: %d\n", ret); - goto out_chan; + goto out_buf; } s5l8740_i2s_codec_prepare(); - s5l8740_i2s_program(i2s, 48000); - desc = dmaengine_prep_slave_single(chan, dma, bytes, DMA_MEM_TO_DEV, - DMA_PREP_INTERRUPT); + s5l8740_i2s_program(i2s, rate); + desc = dmaengine_prep_dma_cyclic(chan, dma, bytes, bytes, + DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); if (!desc) { - dev_err(dev, "dma_tone prep_slave_single failed\n"); - ret = -EIO; - goto out_chan; + dev_err(dev, "dma_tone prep_dma_cyclic failed\n"); + ret = -ENOMEM; + goto out_buf; } cookie = dmaengine_submit(desc); if (dma_submit_error(cookie)) { ret = cookie; - goto out_chan; + goto out_buf; } dma_async_issue_pending(chan); + s5l8740_i2s_codec_play_start(); s5l8740_i2s_tx_kick(i2s, true); - { - int asp = s5l8740_i2s_asp_lock(); - - dev_info(dev, "dma_tone asp_lock=%d\n", asp); - } - { - void __iomem *pl = ioremap(0x38200000ul, 0x200); - unsigned int t, i; - - if (i2s->gpio) { - u32 xor[8] = { }, last[8] = { }, pcon[8] = { }; - unsigned int b; - - for (b = 0; b < 8; b++) { - pcon[b] = readl(i2s->gpio + 32 * b); - last[b] = readl(i2s->gpio + 32 * b + 4); - } - for (i = 0; i < 20000; i++) { - for (b = 0; b < 8; b++) { - u32 d = readl(i2s->gpio + 32 * b + 4); - - xor[b] |= d ^ last[b]; - last[b] = d; - } - } - dev_info(dev, - "dma_tone pads xor %02x %02x %02x %02x %02x %02x %02x %02x\n", - xor[0], xor[1], xor[2], xor[3], - xor[4], xor[5], xor[6], xor[7]); - dev_info(dev, - "dma_tone pcon %08x %08x %08x %08x\n", - pcon[0], pcon[1], pcon[2], pcon[3]); - } - - if (pl) { - for (t = 0; t < 3; t++) { - u32 en = readl(pl + 0x1c); - u32 st = readl(i2s->base + I2SSTATUS); - int ch; - - dev_info(dev, - "dma_tone t=%ums status=0x%x txcom=0x%x en=0x%x rawtc=0x%x\n", - t * 100, st, - readl(i2s->base + I2STXCOM), en, - readl(pl + 0x14)); - for (ch = 0; ch < 8; ch++) { - u32 dst = readl(pl + 0x104 + ch * 0x20); - u32 src = readl(pl + 0x100 + ch * 0x20); - u32 cfg = readl(pl + 0x110 + ch * 0x20); - u32 c2 = readl(pl + 0x114 + ch * 0x20); - - if (!(en & BIT(ch)) && dst != 0x3ca00010) - continue; - dev_info(dev, - " ch%u src=0x%x dst=0x%x cfg=0x%x c2=0x%x\n", - ch, src, dst, cfg, c2); - } - if (t == 0) - msleep(100); - else if (t == 1) - msleep(1900); - } - iounmap(pl); - } - } - dev_info(dev, "dma_tone 1kHz 2s status=0x%x txcom=0x%x\n", + s5l8740_i2s_schedule_asp(); + dev_info(dev, "dma_tone 1kHz rate=%u frames=%u bytes=%zu cyclic 2s\n", + rate, frames, bytes); + msleep(2000); + s5l8740_i2s_cancel_asp(); + s5l8740_i2s_codec_play_stop(); + dmaengine_terminate_sync(chan); + s5l8740_i2s_hw_stop(i2s, NULL); + dev_info(dev, "dma_tone done status=0x%x txcom=0x%x\n", readl(i2s->base + I2SSTATUS), readl(i2s->base + I2STXCOM)); - dmaengine_terminate_sync(chan); - writel(I2STXCOM_STOP, i2s->base + I2STXCOM); ret = 0; -out_chan: +out_buf: dma_free_coherent(dev, bytes, tone, dma); out_unlock: mutex_unlock(&i2s->dma_lock); @@ -777,41 +1235,6 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(dma_tone); -/* Sample GPIO DIN xor across banks 0-7. Use after clk_run or at idle. */ -static ssize_t pad_scan_show(struct device *dev, struct device_attribute *attr, - char *buf) -{ - struct s5l8740_i2s *i2s = dev_get_drvdata(dev); - u32 xor[8] = { }, pcon[8] = { }, last[8] = { }; - unsigned int b, i, n = 0; - - if (!i2s || !i2s->gpio) - return -ENODEV; - for (b = 0; b < 8; b++) { - pcon[b] = readl(i2s->gpio + 32 * b); - last[b] = readl(i2s->gpio + 32 * b + 4); - } - for (i = 0; i < 40000; i++) { - for (b = 0; b < 8; b++) { - u32 d = readl(i2s->gpio + 32 * b + 4); - - xor[b] |= d ^ last[b]; - last[b] = d; - } - } - n += scnprintf(buf + n, PAGE_SIZE - n, - "clkcon=0x%x txcon=0x%x txcom=0x%x status=0x%x\n", - readl(i2s->base + I2SCLKCON), - readl(i2s->base + I2STXCON), - readl(i2s->base + I2STXCOM), - readl(i2s->base + I2SSTATUS)); - for (b = 0; b < 8; b++) - n += scnprintf(buf + n, PAGE_SIZE - n, - "b%u pcon=%08x xor=%02x\n", b, pcon[b], xor[b]); - return n; -} -static DEVICE_ATTR_RO(pad_scan); - /* Program IIS and leave TXCOM running so BCLK/LRCK (and MCLK if any) stay up. */ static ssize_t clk_run_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) @@ -824,7 +1247,7 @@ static ssize_t clk_run_store(struct device *dev, struct device_attribute *attr, if (kstrtouint(buf, 0, &v)) return -EINVAL; if (v) { - s5l8740_i2s_program(i2s, 48000); + s5l8740_i2s_program(i2s, n31_pick_rate(default_rate)); s5l8740_i2s_tx_kick(i2s, false); } else { writel(I2STXCOM_STOP, i2s->base + I2STXCOM); @@ -868,12 +1291,13 @@ static int s5l8740_i2s_probe(struct platform_device *pdev) if (res) { i2s->play_dma.addr = res->start + I2STXFIFO; i2s->play_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; - i2s->play_dma.maxburst = 1; + i2s->play_dma.maxburst = 4; } platform_set_drvdata(pdev, i2s); dev_set_drvdata(dev, i2s); mutex_init(&i2s->dma_lock); + INIT_DELAYED_WORK(&i2s->dma_watch, s5l8740_i2s_dma_watch); if (!use_pio && of_property_present(dev->of_node, "dmas")) { ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); @@ -936,6 +1360,7 @@ static void s5l8740_i2s_remove(struct platform_device *pdev) i2s->kthread = NULL; } s5l8740_i2s_tx_put(i2s); + cancel_delayed_work_sync(&i2s->dma_watch); if (i2s && i2s->num_clks) clk_bulk_disable_unprepare(i2s->num_clks, i2s->clks); } diff --git a/sound/soc/apple/s5l8740-iis2.c b/sound/soc/apple/s5l8740-iis2.c new file mode 100755 index 00000000000000..12792264d863fb --- /dev/null +++ b/sound/soc/apple/s5l8740-iis2.c @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * S5L8740 IIS2 @ 0x3D400000 — BCM2078 digital PCM port (N31). + * + * RetailOS oracles (fm/, bt-*-scsi-live/): + * IIS1 @ 0x3CD00000 = XSP, always zero — NOT BCM TX. + * IIS2 = shared BCM2078 I²S: RX FIFO @ +0x38 (PL080 peri 13, FM + module PCM in), + * TXCON @ +0x04 = 0x0b000099 programmed for music/FM/BT (TXCOM often 0 on BT). + * BT A2DP over-the-air = UART1 @ 0x3DB HCI → BCM2078 (no IIS0/CS42). + * CLKCON +0x00 = 0x1 + * TXCON +0x04 = 0x0b000099 (RetailOS programs this on IIS2 too) + * RXCON +0x30 = 0x1000 + * RXCOM +0x34 = 0x6 (DMA kick; idle/stopped often 0x2) + * RXFIFO +0x38 ← PL080 peri 13 P2M + * STATUS +0x3c = 0x10804 live + * CLKDIV +0x40 = 0x96 (FM oracle; IIS0 play uses 0x177/375) + * REG44 +0x44 = 0x00010007 (same as IIS0 music oracle) + * + * SoC clocks: CLKCON+0x30 = 0x32190 play; FM also +0x10 = 0x4 + * (vs music/idle 0x8004). No FM→BT / A2DP path here — local PCM only. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "n31-audio-rates.h" + +#define S5L8740_IIS2_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +#define S5L8740_IIS2_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) + +#define I2SCLKCON 0x00 +#define I2STXCON 0x04 +#define I2SRXCON 0x30 +#define I2SRXCOM 0x34 +#define I2SRXFIFO 0x38 +#define I2SSTATUS 0x3c +#define I2SCLKDIV 0x40 +#define I2SREG44 0x44 + +#define MCLK_ASSUME_HZ 12000000u + +/* fm/20260826T203834Z oracle */ +#define IIS2_CLKCON_ON 0x1u +#define IIS2_TXCON_FM 0x0b000099u +#define IIS2_RXCON_FM 0x1000u +#define IIS2_RXCOM_DMA 0x6u +#define IIS2_RXCOM_IDLE 0x2u +#define IIS2_CLKDIV_FM_ORACLE 0x96u +#define IIS2_REG44_ORACLE 0x00010007u + +static uint iis2_clkdiv; +module_param(iis2_clkdiv, uint, 0644); +MODULE_PARM_DESC(iis2_clkdiv, "IIS2 CLKDIV override; 0 = FM oracle 0x96"); + +#define CLKCON_PHYS 0x3c500000ul +#define CLKCON_AUDIO_OFF 0x30 +#define CLKCON_FM_GATE_OFF 0x10 +#define CLKCON_AUDIO_PLAY 0x32190u +#define CLKCON_AUDIO_IDLE 0x1c20u +#define CLKCON_FM_GATE_ON 0x4u +#define CLKCON_FM_GATE_OFF_VAL 0x8004u + +#define IIS2_REGS_LEN 0x48 + +struct s5l8740_iis2 { + void __iomem *base; + void __iomem *clkcon; + struct device *dev; + struct clk_bulk_data *clks; + int num_clks; + bool has_dma; + struct snd_dmaengine_dai_dma_data cap_dma; + u32 clkcon10_saved; + bool clkcon10_held; + unsigned int rate; +}; + +static u32 iis2_pick_clkdiv(unsigned int rate) +{ + const struct n31_rate_cfg *r; + + if (iis2_clkdiv) + return iis2_clkdiv; + /* + * FM IIS2 oracle differs from IIS0: 0x96 while IIS0 HP path uses + * 0x177 (32 kHz table entry) during the same FM session. + */ + if (rate == 44100 || rate == 48000) + return IIS2_CLKDIV_FM_ORACLE; + r = n31_find_rate(rate); + if (r) + return r->clkdiv; + if (!rate) + rate = 44100; + return MCLK_ASSUME_HZ / rate; +} + +static void iis2_clkcon_audio(struct s5l8740_iis2 *iis2, u32 val) +{ + if (!iis2 || !iis2->clkcon) + return; + writel(val, iis2->clkcon + CLKCON_AUDIO_OFF); +} + +static void iis2_clkcon_fm_gate(struct s5l8740_iis2 *iis2, bool on) +{ + u32 cur; + + if (!iis2 || !iis2->clkcon) + return; + cur = readl(iis2->clkcon + CLKCON_FM_GATE_OFF); + if (on) { + if (!iis2->clkcon10_held) { + iis2->clkcon10_saved = cur; + iis2->clkcon10_held = true; + } + writel(CLKCON_FM_GATE_ON, iis2->clkcon + CLKCON_FM_GATE_OFF); + } else if (iis2->clkcon10_held) { + writel(iis2->clkcon10_saved ? + iis2->clkcon10_saved : CLKCON_FM_GATE_OFF_VAL, + iis2->clkcon + CLKCON_FM_GATE_OFF); + iis2->clkcon10_held = false; + } +} + +/* + * Program IIS2 RX from fm-playing dump. Peri 13 DMA must be armed by + * dmaengine before RXCOM |= 0x6 (same kick model as IIS0 TXCOM). + */ +static void iis2_program_rx(struct s5l8740_iis2 *iis2) +{ + u32 div; + + iis2_clkcon_fm_gate(iis2, true); + iis2_clkcon_audio(iis2, CLKCON_AUDIO_PLAY); + writel(IIS2_CLKCON_ON, iis2->base + I2SCLKCON); + writel(IIS2_TXCON_FM, iis2->base + I2STXCON); + writel(IIS2_RXCON_FM, iis2->base + I2SRXCON); + div = iis2_pick_clkdiv(iis2->rate); + writel(div, iis2->base + I2SCLKDIV); + writel(IIS2_REG44_ORACLE, iis2->base + I2SREG44); +} + +static void iis2_rx_kick(struct s5l8740_iis2 *iis2) +{ + writel(IIS2_RXCOM_DMA, iis2->base + I2SRXCOM); +} + +static void iis2_hw_stop(struct s5l8740_iis2 *iis2) +{ + if (!iis2 || !iis2->base) + return; + writel(IIS2_RXCOM_IDLE, iis2->base + I2SRXCOM); + iis2_clkcon_audio(iis2, CLKCON_AUDIO_IDLE); + iis2_clkcon_fm_gate(iis2, false); +} + +static int s5l8740_iis2_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + iis2->rate = params_rate(params); + iis2_program_rx(iis2); + dev_info(dai->dev, + "IIS2 hw_params rate=%u ch=%u clkdiv=0x%x reg44=0x%x status=0x%x\n", + iis2->rate, params_channels(params), + readl(iis2->base + I2SCLKDIV), readl(iis2->base + I2SREG44), + readl(iis2->base + I2SSTATUS)); + return 0; +} + +static int s5l8740_iis2_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + iis2_program_rx(iis2); + iis2_rx_kick(iis2); + dev_info(dai->dev, "IIS2 capture start rxcom=0x%x status=0x%x\n", + readl(iis2->base + I2SRXCOM), + readl(iis2->base + I2SSTATUS)); + return 0; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + iis2_hw_stop(iis2); + return 0; + default: + return -EINVAL; + } +} + +static int s5l8740_iis2_dai_probe(struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (iis2->has_dma) + snd_soc_dai_init_dma_data(dai, NULL, &iis2->cap_dma); + return 0; +} + +static const struct snd_soc_dai_ops s5l8740_iis2_dai_ops = { + .probe = s5l8740_iis2_dai_probe, + .hw_params = s5l8740_iis2_hw_params, + .trigger = s5l8740_iis2_trigger, +}; + +static struct snd_soc_dai_driver s5l8740_iis2_dai = { + .name = "bcm2078-pcm", + .capture = { + .stream_name = "BCM2078 PCM Capture", + .channels_min = 1, + .channels_max = 2, + .rates = S5L8740_IIS2_RATES, + .formats = S5L8740_IIS2_FORMATS, + }, + .ops = &s5l8740_iis2_dai_ops, +}; + +static const struct snd_soc_component_driver s5l8740_iis2_component = { + .name = "bcm2078-pcm", + .legacy_dai_naming = 1, +}; + +static ssize_t regs_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct s5l8740_iis2 *iis2 = dev_get_drvdata(dev); + unsigned int i; + ssize_t n = 0; + + if (!iis2 || !iis2->base) + return sysfs_emit(buf, "not mapped\n"); + + for (i = 0; i < IIS2_REGS_LEN; i += 4) { + n += sysfs_emit_at(buf, n, "%02x: %08x\n", i, + readl(iis2->base + i)); + if (n >= PAGE_SIZE - 32) + break; + } + if (iis2->clkcon) { + n += sysfs_emit_at(buf, n, "clk+10: %08x\n", + readl(iis2->clkcon + CLKCON_FM_GATE_OFF)); + n += sysfs_emit_at(buf, n, "clk+30: %08x\n", + readl(iis2->clkcon + CLKCON_AUDIO_OFF)); + } + return n; +} +static DEVICE_ATTR_RO(regs); + +static int s5l8740_iis2_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_iis2 *iis2; + struct resource *res; + int ret; + + iis2 = devm_kzalloc(dev, sizeof(*iis2), GFP_KERNEL); + if (!iis2) + return -ENOMEM; + iis2->dev = dev; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + iis2->base = devm_ioremap_resource(dev, res); + if (IS_ERR(iis2->base)) + return PTR_ERR(iis2->base); + + iis2->clkcon = devm_ioremap(dev, CLKCON_PHYS, 0x80); + + ret = devm_clk_bulk_get_all(dev, &iis2->clks); + if (ret > 0) { + iis2->num_clks = ret; + ret = clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); + if (ret) + dev_warn(dev, "clk_bulk: %d\n", ret); + } + + if (res) { + iis2->cap_dma.addr = res->start + I2SRXFIFO; + iis2->cap_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; + iis2->cap_dma.maxburst = 1; + } + + platform_set_drvdata(pdev, iis2); + dev_set_drvdata(dev, iis2); + + if (of_property_present(dev->of_node, "dmas")) { + ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); + if (ret) { + dev_err(dev, "dmaengine_pcm: %d\n", ret); + return ret; + } + iis2->has_dma = true; + } else { + dev_err(dev, "missing dmas (need peri 13 rx)\n"); + return -EINVAL; + } + + ret = devm_snd_soc_register_component(dev, &s5l8740_iis2_component, + &s5l8740_iis2_dai, 1); + if (ret) + return ret; + + ret = device_create_file(dev, &dev_attr_regs); + if (ret) + dev_warn(dev, "regs sysfs: %d\n", ret); + + dev_info(dev, + "BCM2078 PCM RX @%pR peri13 FIFO@+0x38 (IIS2; FM/A2DP PCM in)\n", + res); + return 0; +} + +static void s5l8740_iis2_remove(struct platform_device *pdev) +{ + struct s5l8740_iis2 *iis2 = platform_get_drvdata(pdev); + + device_remove_file(&pdev->dev, &dev_attr_regs); + iis2_hw_stop(iis2); + if (iis2 && iis2->num_clks) + clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); +} + +static const struct of_device_id s5l8740_iis2_of_match[] = { + { .compatible = "apple,s5l8740-bcm2078-pcm" }, + { .compatible = "apple,s5l8740-iis2" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_iis2_of_match); + +static struct platform_driver s5l8740_iis2_driver = { + .probe = s5l8740_iis2_probe, + .remove = s5l8740_iis2_remove, + .driver = { + .name = "s5l8740-iis2", + .of_match_table = s5l8740_iis2_of_match, + }, +}; +module_platform_driver(s5l8740_iis2_driver); + +MODULE_DESCRIPTION("S5L8740 BCM2078 PCM capture DAI (IIS2 @0x3D400000, peri 13 RX)"); +MODULE_LICENSE("GPL"); +MODULE_SOFTDEP("pre: dma_s5l8740_pl080"); From 12865cbfa8d51005b7a64c0880472386dd574e57 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Thu, 27 Aug 2026 08:36:07 -0230 Subject: [PATCH 17/31] N31: sync glass Tristar/FMSS/FTL/PMIC/CS42 from live bring-up Quiet Tristar I2C -110 storm (poll_ms=0, NACK stops poll), DMA-meta storage path with meta_dma_read default off until CS kick is safe, PMIC/CS42 glass fixes, and nodrm DTS. --- arch/arm/boot/dts/samsung/Makefile | 3 +- .../boot/dts/samsung/s5l8740-n31-nodrm.dts | 465 +++++++++ arch/arm/boot/dts/samsung/s5l8740-n31.dts | 3 +- drivers/gpio/gpio-d1830.c | 286 ++++-- drivers/misc/apple-tristar-cbtl1609.c | 899 ++++++++++++++++-- drivers/misc/fmss-s5l8740.c | 109 ++- drivers/misc/ftl-s5l8740.c | 190 +++- drivers/misc/s5l8740-iis2-mmio.c | 301 +++++- drivers/misc/whimory-s5l8740.h | 7 + sound/soc/apple/cs42l81-spi.c | 32 + 10 files changed, 2052 insertions(+), 243 deletions(-) create mode 100755 arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts diff --git a/arch/arm/boot/dts/samsung/Makefile b/arch/arm/boot/dts/samsung/Makefile index 31eb090c63f2c8..83b5c1e22f3a7c 100644 --- a/arch/arm/boot/dts/samsung/Makefile +++ b/arch/arm/boot/dts/samsung/Makefile @@ -50,7 +50,8 @@ dtb-$(CONFIG_ARCH_S3C64XX) += \ dtb-$(CONFIG_ARCH_S5L87XX) += \ s5l8702-n46.dtb \ s5l8723-n20.dtb \ - s5l8740-n31.dtb + s5l8740-n31.dtb \ + s5l8740-n31-nodrm.dtb dtb-$(CONFIG_ARCH_S5PV210) += \ s5pv210-aquila.dtb \ s5pv210-fascinate4g.dtb \ diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts new file mode 100755 index 00000000000000..cffb246c368bcc --- /dev/null +++ b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Apple S5L8740 SoC + * Board name: N31 + * Product name: iPod nano (7th generation) + */ +/dts-v1/; + +#include +#include +#include +#include + +/ { + #address-cells = <1>; + #size-cells = <1>; + model = "Apple iPod nano (7th generation)"; + compatible = "apple,n31", "samsung,s5l8740"; + + aliases { + serial0 = &uart3; + console = &uart3; + }; + + chosen { + /* quiet: suppress non-critical boot spam; nimbus uses explicit prints */ + bootargs = "panic=-1 loglevel=8 nohlt"; + stdout-path = "serial0"; + }; + + nclk: external_clock { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <24000000>; + clock-output-names = "nclk"; + }; + + // pclk: clock-ref { + // compatible = "fixed-clock"; + // #clock-cells = <0>; + // clock-frequency = <6000000>; + // clock-output-names = "pclk"; + // }; + + cpus { + #address-cells = <1>; + #size-cells = <0>; + + cpu@0 { + device_type = "cpu"; + reg = <0>; + compatible = "arm,cortex-a5"; + }; + }; + + memory@8000000 { + device_type = "memory"; + reg = <0x08000000 0x04000000>; + }; + + soc { + compatible = "simple-bus"; + #address-cells = <1>; + #size-cells = <1>; + ranges; + + vic0: interrupt-controller@38e00000 { + compatible = "arm,pl192-vic"; + interrupt-controller; + reg = <0x38e00000 0x1000>; + #interrupt-cells = <1>; + }; + + vic1: interrupt-controller@38e01000 { + compatible = "arm,pl192-vic"; + interrupt-controller; + reg = <0x38e01000 0x1000>; + #interrupt-cells = <1>; + }; + + clkctrl: clock-controller@3c500000 { + compatible = "samsung,s5l8740-clock", "samsung,s5l8702-clock"; + reg = <0x3c500000 0x100>; + #clock-cells = <1>; + /* boot-minimal: WTF already ungated clocks — skip SEC mass reprogram */ + apple,skip-clkcon-ensure; + apple,skip-clkcon-bringup; + status = "okay"; + }; + eic: interrupt-controller@39700000 { + compatible = "apple,s5l8740-eic", "samsung,s5l8740-eic"; + reg = <0x39700000 0x1000>; + interrupt-controller; + #interrupt-cells = <2>; + /* + * Chain ONLY VIC EXT1 (group1 / GPIOs 32-63): Vol 40/41, Nimbus 38. + * Chaining all EXT0-6 hung boot previously. + */ + interrupt-parent = <&vic0>; + interrupts = <1>; /* EXT1 */ + /* boot-minimal v2: no EIC chain (IRQ storm risk without Nimbus) */ + status = "disabled"; + }; + + timer: timer@3c700000 { + compatible = "samsung,s5l8720-timer"; + reg = <0x3c700000 0x20000>; + interrupt-parent = <&vic0>; + interrupts = <7>; + }; + + /* Probe lcdif early — before SPI/Nimbus (boot-minimal bisect) */ + lcdif: lcdif@38300000 { + compatible = "samsung,s5l8740-lcdif"; + reg = <0x38300000 0x10000>; + status = "disabled"; + }; + + backlight: backlight@3e000000 { + compatible = "apple,s5l8740-backlight", "samsung,s5l8740-backlight"; + reg = <0x3e000000 0x100>; + default-brightness = <62>; + status = "disabled"; + }; + + /* if this one is not first, pointers get overwritten + in the driver and it fails to initialize */ + uart3: serial@3dd00000 { + compatible = "apple,s5l-uart"; + reg = <0x3dd00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <27>; + status = "okay"; + }; + + uart0: serial@3cc00000 { + compatible = "apple,s5l-uart"; + reg = <0x3cc00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <24>; + status = "okay"; + }; + + uart1: serial@3db00000 { + compatible = "apple,s5l-uart"; + reg = <0x3db00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <25>; + status = "okay"; + + /* BCM2078KUBG on UART1 @115200 — Vincent CodePatches → BCM2076B1.hcd */ + bluetooth { + compatible = "brcm,bcm2078", "brcm,bcm4329-bt"; + max-speed = <115200>; + shutdown-gpios = <&gpio 97 GPIO_ACTIVE_LOW>; + device-wakeup-gpios = <&gpio 98 GPIO_ACTIVE_HIGH>; + host-wakeup-gpios = <&gpio 119 GPIO_ACTIVE_HIGH>; + firmware-name = "brcm/BCM2076B1.hcd"; + /* Boot-safe: defer BCM until init up — see bcm2078-bt.c */ + status = "disabled"; + }; + }; + + uart2: serial@3dc00000 { + compatible = "apple,s5l-uart"; + reg = <0x3dc00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <26>; + status = "okay"; + }; + + usbphy: usbphy@3c400000 { + /* N31 uses s5l87xx PHY sequence (NOT Nano3 8702 ramp) */ + compatible = "apple,s5l8740-otgphy", "apple,s5l87xx-otgphy"; + reg = <0x3c400000 0x100>; + status = "disabled"; + #phy-cells = <0>; + }; + + usbotg_hs: usb@38400000 { + compatible = "apple,s5l8740-usb", "apple,s5l87xx-usb"; + reg = <0x38400000 0x40000>; + interrupt-parent = <&vic0>; + interrupts = <19>; + phys = <&usbphy>; + phy-names = "usb2-phy"; + clocks = <&clkctrl CLK_USBOTG>, <&clkctrl CLK_USBPHY>, <&nclk>; + clock-names = "otg", "phy", "ref"; + /* rx=256 + np-tx=256 leaves room for periodic TX FIFOs (536+256 overflowed) */ + g-rx-fifo-size = <256>; + g-np-tx-fifo-size = <256>; + status = "disabled"; + dr_mode = "peripheral"; + }; + + + spi2: spi@3d200000 { + /* N31 Nimbus on SPI2 — CLK_SPI2 (+ alt/secondary) + pinmux in driver */ + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; + reg = <0x3d200000 0x100>; + clocks = <&clkctrl CLK_SPI2>, <&clkctrl CLK_SPI2_2>, <&clkctrl CLK_SPI2_ALT>; + clock-names = "spi", "spi-2", "spi-alt"; + /* SPI DMA peri IDs OPEN in N31 RE — Nimbus uses proven PIO path */ + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + nimbus: touchscreen@0 { + compatible = "apple,nimbus"; + reg = <0>; + spi-max-frequency = <1000000>; + enable-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; + reset-gpios = <&gpio 39 GPIO_ACTIVE_LOW>; + attn-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; + /* GPIO38 → EIC group1 → VIC EXT1 */ + interrupts-extended = <&eic 38 IRQ_TYPE_LEVEL_LOW>; + status = "disabled"; + }; + }; + + /* + * SPI0 @0x3C300000: panel in IpodSec, CS42 in RetailOS. + * Keep disabled so LCD SPI ownership is undisturbed; enable only + * after panel/CS42 mux is sequenced. + */ + /* SPI0: CS42 control. Panel pixels are LCDIF@383 — not this bus. */ + spi0: spi@3c300000 { + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; + reg = <0x3c300000 0x100>; + clocks = <&clkctrl CLK_SPI0>, <&clkctrl CLK_SPI0_2>; + clock-names = "spi", "spi-2"; + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + cs42l81: codec@0 { + compatible = "cirrus,cs42l81", "apple,338s1146"; + reg = <0>; + spi-max-frequency = <1000000>; + status = "okay"; + }; + }; + + i2s0: i2s@3ca00000 { + compatible = "apple,s5l8740-i2s", "samsung,s5l8740-i2s"; + reg = <0x3ca00000 0x1000>; + clocks = <&clkctrl CLK_I2S0>, <&clkctrl CLK_CG16_9>; + clock-names = "i2s", "cg16"; + dmas = <&dmac 12 0>, <&dmac 13 0>; + dma-names = "tx", "rx"; + #sound-dai-cells = <0>; + status = "disabled"; + }; + + i2s2: i2s@3d400000 { + compatible = "apple,s5l8740-iis2"; + reg = <0x3d400000 0x1000>; + clocks = <&clkctrl CLK_I2S2>, <&clkctrl CLK_CG16_11>; + clock-names = "i2s", "cg16"; + dmas = <&dmac 16 0>, <&dmac 17 0>; + dma-names = "tx", "rx"; + status = "disabled"; + }; + + nano7_audio: audio { + compatible = "apple,n31-audio"; + apple,cpu = <&i2s0>; + status = "disabled"; + }; + + wdt: watchdog@3c800000 { + compatible = "apple,s5l8740-syscon", "syscon", "simple-mfd"; + reg = <0x3c800000 0x8>; + + reboot: syscon-reboot@3c800000 { + compatible = "syscon-reboot"; + offset = <0x0>; + value = <0x100000>; + }; + }; + + /* + * Full S5L8740 banked GPIO (was: 2-line bcm6345 hack at 0x3cf000a4). + * Keep label gpio5 disabled so old phandles fail closed if any remain. + */ + gpio5: gpio-hack@3cf000a4 { + compatible = "brcm,bcm6345-gpio"; + reg-names = "dat"; + reg = <0x3cf000a4 0x4>; + #gpio-cells = <2>; + gpio-controller; + ngpios = <2>; + status = "disabled"; + }; + + gpio: gpio@3cf00000 { + compatible = "apple,s5l8740-gpio", "samsung,s5l8740-gpio"; + reg = <0x3cf00000 0x400>; + clocks = <&clkctrl CLK_GPIO>; + clock-names = "gpio"; + apple,eic = <&eic>; + /* boot-minimal: skip SEC sub_223C mass pinmux (WTF handoff) */ + apple,skip-sec-pinmux; + #gpio-cells = <2>; + gpio-controller; + ngpios = <128>; /* BT host-wake GPIO 119 */ + status = "disabled"; + }; + + i2c0: i2c@3c600000 { + compatible = "samsung,s5l8702-i2c"; + samsung,write-busy-poll; + #address-cells = <1>; + #size-cells = <0>; + reg = <0x3c600000 0x100>; + clocks = <&clkctrl CLK_I2C0>, <&clkctrl CLK_I2C0_2>; + clock-names = "i2c", "i2c-2"; + clock-frequency = <100000>; + interrupt-parent = <&vic0>; + interrupts = <21>; + status = "disabled"; + + /* + * Tristar CBTL1609A1 — public "0x34 write / 0x35 read" is 8-bit; + * Linux DT 7-bit address is 0x1a. + * RetailOS RE: zero Dx/mux register writes observed — dump stays + * flat until accessory; only apple,init-sequence may write. + */ + tristar: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", + "apple,n31-tristar"; + reg = <0x1a>; + /* Parent i2c0 is disabled in this DTB; child cannot probe. */ + status = "okay"; + }; + }; + + i2c1: i2c@3c900000 { + compatible = "samsung,s5l8702-i2c"; + samsung,write-busy-poll; + #address-cells = <1>; + #size-cells = <0>; + reg = <0x3c900000 0x100>; + clocks = <&clkctrl CLK_I2C1>, <&clkctrl CLK_I2C1_2>; + clock-names = "i2c", "i2c-2"; + clock-frequency = <100000>; /* UPDATE ME */ + interrupt-parent = <&vic0>; + interrupts = <22>; + status = "disabled"; + + /* ST lis3lv02d binding; mount-matrix optional (skip until board orient known) */ + lis3dc: lis331dlh@18 { + compatible = "st,lis3lv02d"; + reg = <0x18>; + /* Identity until board orientation proven on HW */ + mount-matrix = "1", "0", "0", + "0", "1", "0", + "0", "0", "1"; + status = "okay"; + }; + + /* Duplicate of i2c0 tristar@1a — keep disabled; primary is i2c0 only */ + tristar_i2c1: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + reg = <0x1a>; + status = "disabled"; + }; + /* Also try literal 7-bit 0x34 in case public notes meant that */ + /* 0x34 is 8-bit write addr form — NOT a Linux 7-bit address */ + tristar_i2c1_34: lightning-mux@34 { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + reg = <0x34>; + status = "disabled"; + }; + + d1830: pmic@73 { + compatible = "dlg,d1830-gpio"; + reg = <0x73>; + gpio-controller; + #gpio-cells = <2>; + /* N31: no Home/Play. Sleep/Wake is PMIC — map TBD. */ + dlg,gpio-map = <0x07 5>; /* SEC sub_27F4: Sleep/Wake status bit5 */ + }; + }; + + sha1: sha1@38000000 { + compatible = "samsung,s5l8702-sha1"; + reg = <0x38000000 0x100>; + clocks = <&clkctrl CLK_SHA1>; + clock-names = "sha1"; + status = "disabled"; + }; + + /* + * PL080 DMAC0/1 — OSOS pairs 0x38200000 + 0x38700000. + * NOT 0x384 (DWC OTG). Peri IDs: N31 RE IIS0 12/13, IIS2 16/17. + */ + dmac: dma-controller@38200000 { + compatible = "apple,s5l8740-pl080", "arm,pl080"; + reg = <0x38200000 0x1000>, <0x38700000 0x1000>; + clocks = <&clkctrl CLK_DMAC0>, <&clkctrl CLK_DMAC1>; + clock-names = "dmac0", "dmac1"; + interrupt-parent = <&vic0>; + interrupts = <16>, <17>; + #dma-cells = <2>; + status = "disabled"; + }; + + aes: aes@38c00000 { + compatible = "samsung,s5l8702-aes"; + reg = <0x38c00000 0x100>; + clocks = <&clkctrl CLK_AES>; + clock-names = "aes"; + status = "disabled"; + }; + + prng: prng@3c100000 { + compatible = "samsung,s5l8702-prng"; + reg = <0x3c100000 0x100>; + clocks = <&clkctrl CLK_PRNG>; + clock-names = "prng"; + status = "disabled"; + }; + }; + gpio-keys { + compatible = "gpio-keys"; + status = "disabled"; + + button-power { + label = "Sleep/Wake"; + gpios = <&d1830 0 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <50>; + wakeup-source; + status = "okay"; + }; + + /* Active-low CONFIRMED; Vol+ = GPIO40 provisional (swap with 41 if inverted) */ + button-volup { + label = "Volume Up"; + gpios = <&gpio 40 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <30>; + status = "okay"; + }; + + button-voldown { + label = "Volume Down"; + gpios = <&gpio 41 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <30>; + status = "okay"; + }; + }; +}; diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 591867ebdf9327..0181b7da6cf0b0 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -291,7 +291,8 @@ * skips I2C ACK (reads still return the address byte). */ tristar: lightning-mux@1a { - compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", + "apple,n31-tristar"; reg = <0x1a>; status = "okay"; }; diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index 37a6a649ccf6db..a8b9206c0ec41c 100755 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -14,6 +14,11 @@ * 158C82(3/5) writes bitfields in 87/88 — not the ADC. Keep poweroff * unchanged. Do not enable dlg,apply-sec-rails from here. * + * Analog HP needs SEC sub_23EC sibling LDOs (regs 21–23 bit4) plus + * reg16 bit4. Default probe stays gpio-only. CS42 calls + * d1830_audio_rails() on prepare. Never replay hibernate cookie + * writes (regs 1/2/73/96) unless boot_mode bit7 is actually set. + * * Copyright (C) 2026 Vencislav Atanasov */ #include @@ -54,6 +59,18 @@ extern void (*d1830_n31_din_nirq_hook)(void); #define D1830_MV_EMPTY 3300 #define D1830_MV_FULL 4200 +static bool dump_only; +module_param(dump_only, bool, 0644); +MODULE_PARM_DESC(dump_only, + "Log PMIC rail ops, do not write (docs-internal n31-pmic dummies)"); + +static bool allow_audio_rails = true; +module_param(allow_audio_rails, bool, 0644); +MODULE_PARM_DESC(allow_audio_rails, + "Apply sub_23EC LDO trim from d1830_audio_rails() (default on)"); + +int d1830_audio_rails(void); + struct d1830_gpio_map { u8 reg; u8 bit; @@ -207,6 +224,19 @@ static ssize_t do_poweroff_store(struct device *dev, } static DEVICE_ATTR_WO(do_poweroff); +static ssize_t audio_rails_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + int ret; + + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + ret = d1830_audio_rails(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(audio_rails); + /* * Chain from RE (do not invert without new ARM): * user key → PMIC status r5-r8 (sub_26520, read-only in OSOS) @@ -737,13 +767,55 @@ static enum power_supply_property d1830_usb_props[] = { POWER_SUPPLY_PROP_SCOPE, }; +static int d1830_write8(struct i2c_client *client, u8 reg, u8 val) +{ + int ret; + + dev_info(&client->dev, "n31-pmic: WR %02x <- %02x%s\n", + reg, val, dump_only ? " (suppressed)" : ""); + if (dump_only) + return 0; + ret = i2c_smbus_write_byte_data(client, reg, val); + if (ret) + dev_err(&client->dev, "n31-pmic: WR %02x failed ret=%d\n", + reg, ret); + return ret; +} + static int d1830_rmw(struct i2c_client *client, u8 reg, u8 clear, u8 set) { int v = i2c_smbus_read_byte_data(client, reg); + u8 newv; if (v < 0) return v; - return i2c_smbus_write_byte_data(client, reg, (u8)((v & ~clear) | set)); + newv = (u8)((v & ~clear) | set); + dev_info(&client->dev, + "n31-pmic: RMW reg=%02x old=%02x clear=%02x set=%02x new=%02x%s\n", + reg, v, clear, set, newv, dump_only ? " (suppressed)" : ""); + if (dump_only) + return 0; + return i2c_smbus_write_byte_data(client, reg, newv); +} + +static void d1830_log_audio_regs(struct i2c_client *client, const char *tag) +{ + static const u8 regs[] = { + 1, 2, 3, 5, 13, 14, 16, 17, 19, 20, 21, 22, 23, 26, 35, + 36, 37, 38, 41, 42, 43, 48, 89, 96, 110, 111 + }; + char hex[8]; + int i, v; + + for (i = 0; i < ARRAY_SIZE(regs); i++) { + v = i2c_smbus_read_byte_data(client, regs[i]); + if (v < 0) + snprintf(hex, sizeof(hex), "ERR"); + else + snprintf(hex, sizeof(hex), "%02x", v); + dev_info(&client->dev, "n31-pmic: %s RD %02x -> %s\n", + tag, regs[i], hex); + } } /* @@ -768,80 +840,139 @@ int d1830_nimbus_rail(bool on) EXPORT_SYMBOL_GPL(d1830_nimbus_rail); /* - * IpodSec PMIC rail / charge bring-up: - * sub_23EC — regs 20–23,26,16,17,19,35 (charge/rail-ish) - * sub_27F4 — IIC1 init already done by i2c driver; apply safe RMW sequence - * (skip hibernate Stpr cookie / fatal halt paths). + * SEC sub_23EC trim — sibling LDOs used by analog HP. + * + * Bootloader writes computed 5-bit values from tables at 0x22004B70 / + * 0x22004B80 (not recovered). Live Linux (gpio-only): r20=0x1a already + * has bit4; r21=0x0a (no bit4); r22=r23=0x00. Match the group by + * setting bit4 on 20–23 and forcing 21/22/23 to the same value, then + * the documented r16/r17/r19 RMWs. Skip r26=0xB2 (charge). Never + * touch POWEROFF (reg 13). + * + * Cold-boot sub_23EC also sets r16 bit5 (same bit Nimbus uses). Keep + * that — OSOS 7484 will still toggle it for BT. */ -static int d1830_sec_rail_seq(struct i2c_client *client) +static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) { - struct device *dev = &client->dev; - int ret, v; - u8 b; + int v20, v21, v35; + u8 fill, r16; + + dev_info(&client->dev, + "n31-pmic: sub_23EC-equivalent begin boot_mode=0x%02x\n", + boot_mode); + + v35 = i2c_smbus_read_byte_data(client, 35); + if (v35 >= 0) + d1830_write8(client, 35, (u8)(v35 & 0xFC)); + + v20 = i2c_smbus_read_byte_data(client, 20); + v21 = i2c_smbus_read_byte_data(client, 21); + if (v20 < 0 || v21 < 0) + return v20 < 0 ? v20 : v21; + + /* Keep r20 extras (live 0x1a). 21–23 share one 5-bit field + bit4. */ + d1830_write8(client, 20, (u8)(v20 | 0x10)); + fill = (u8)((v21 & 0x1f) | 0x10); + d1830_write8(client, 21, fill); + d1830_write8(client, 22, fill); + d1830_write8(client, 23, fill); + + v21 = i2c_smbus_read_byte_data(client, 16); + if (v21 < 0) + return v21; + r16 = (u8)((v21 & 0x2f) | 0x10); + if (!boot_mode) + r16 |= 0x20; + d1830_write8(client, 16, r16); + + d1830_rmw(client, 17, 0, 0x07); + d1830_rmw(client, 19, 0, 0x02); + + dev_info(&client->dev, "n31-pmic: sub_23EC-equivalent complete\n"); + return 0; +} - /* --- sub_23EC (rail/charge) --- */ - /* Reg20 ← (delay-derived) & 0x1F: use 0x10 as safe mid rail enable-ish */ - ret = i2c_smbus_write_byte_data(client, 20, 0x10); - if (ret) - dev_warn(dev, "rail reg20: %d\n", ret); +/* + * sub_23EC analog-adjacent trim only. Called from CS42 prepare. + * Does not run hibernate cookie / POWEROFF / charger 0xB2. + */ +int d1830_audio_rails(void) +{ + struct i2c_client *client = d1830_poweroff_client; + int r02, ret; - v = i2c_smbus_read_byte_data(client, 35); - if (v >= 0) { - b = (u8)(v & 0xFC); - ret = i2c_smbus_write_byte_data(client, 35, b); - if (ret) - dev_warn(dev, "rail reg35: %d\n", ret); + if (!client) + return -ENODEV; + if (!allow_audio_rails) { + dev_info(&client->dev, "n31-pmic: audio rails skipped (allow_audio_rails=0)\n"); + d1830_log_audio_regs(client, "audio-skip"); + return 0; } - /* Regs 21–23 same pattern as 20 in SEC loop — use 0x10 */ - i2c_smbus_write_byte_data(client, 21, 0x10); - i2c_smbus_write_byte_data(client, 22, 0x10); - i2c_smbus_write_byte_data(client, 23, 0x10); - - /* Reg26 ← 0xB2 (-78) twice in SEC */ - i2c_smbus_write_byte_data(client, 26, 0xB2); - i2c_smbus_write_byte_data(client, 26, 0xB2); + d1830_log_audio_regs(client, "audio-before"); + r02 = i2c_smbus_read_byte_data(client, 2); + if (r02 < 0) + return r02; + ret = d1830_sec_trim_seq(client, (r02 & 0x80) ? 0x11 : 0x00); + /* sub_27F4 analog-adjacent: r14 fixed 0x20. Not POWEROFF. */ + if (!ret) + d1830_write8(client, 14, 0x20); + d1830_log_audio_regs(client, "audio-after"); + return ret; +} +EXPORT_SYMBOL_GPL(d1830_audio_rails); - v = i2c_smbus_read_byte_data(client, 16); - if (v >= 0) { - b = (u8)((v & 0x2F) | 0x10); - /* optional |0x20 path when a1 set — keep base */ - i2c_smbus_write_byte_data(client, 16, b); +/* + * IpodSec PMIC rail / charge bring-up. Opt-in via dlg,apply-sec-rails. + * + * The old Linux seq always wrote the hibernate-to-standby cluster + * (reg2=0x80, reg73=0, reg1=0, cookie@96) and historically POWEROFF. + * Bootloader only does that when reg2 bit7 is set. Match the dummies + * doc: detect boot_mode, skip reset/hibernate unless that branch, + * never write reg 13 here. + */ +static int d1830_sec_rail_seq(struct i2c_client *client) +{ + struct device *dev = &client->dev; + int r02, r01; + u8 boot_mode = 0; + + r02 = i2c_smbus_read_byte_data(client, 2); + if (r02 < 0) + return r02; + dev_info(dev, "n31-pmic: RD 02 -> %02x\n", r02); + if (r02 & 0x80) { + boot_mode = 0x11; + d1830_write8(client, 2, 0x80); + } + dev_info(dev, "n31-pmic: boot_mode=0x%02x\n", boot_mode); + + r01 = i2c_smbus_read_byte_data(client, 1); + if (r01 < 0) + return r01; + dev_info(dev, "n31-pmic: RD 01 -> %02x\n", r01); + + if (boot_mode == 0x11) { + dev_warn(dev, + "n31-pmic: hibernate-to-standby path detected, reset suppressed (reg01=%02x)\n", + r01); + /* Do not write reg 0x49/0x60/0x01/0x0d or call sub_1130. */ } - v = i2c_smbus_read_byte_data(client, 17); - if (v >= 0) - i2c_smbus_write_byte_data(client, 17, (u8)(v | 0x07)); - - v = i2c_smbus_read_byte_data(client, 19); - if (v >= 0) - i2c_smbus_write_byte_data(client, 19, (u8)(v | 0x02)); + d1830_sec_trim_seq(client, boot_mode); - /* --- sub_27F4 safe subset (non-fatal) --- */ - i2c_smbus_write_byte_data(client, 2, 0x80); - i2c_smbus_write_byte_data(client, 73, 0x00); - i2c_smbus_write_byte_data(client, 1, 0x00); - /* clear 4-byte cookie @96 without hibernate SPI (SEC writes 4 bytes) */ - if (i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_I2C_BLOCK)) { - u8 z[4] = { 0, 0, 0, 0 }; + /* sub_27F4 post-trim, non-fatal. NEVER POWEROFF (reg 13). */ + d1830_rmw(client, 48, 0, 0x40); + d1830_rmw(client, 89, 0x1C, 0); + d1830_write8(client, 60, 0x01); + if (boot_mode != 0x11) + d1830_rmw(client, 41, 0x13, 0x10); + d1830_rmw(client, 42, 0x3F, 0x14); + d1830_rmw(client, 43, 0x0F, 0x01); + d1830_write8(client, 14, 0x20); + d1830_rmw(client, 38, 0x01, 0); - i2c_smbus_write_i2c_block_data(client, 96, 4, z); - } else { - i2c_smbus_write_byte_data(client, 96, 0); - } - /* NEVER write reg 13 here — bit0 is D1830_POWEROFF_BIT (cuts Vbat). */ - d1830_rmw(client, 48, 0, 0x40); /* |= 0x40 */ - d1830_rmw(client, 89, 0x1C, 0); /* &= 0xE3 */ - i2c_smbus_write_byte_data(client, 60, 0x01); - d1830_rmw(client, 41, 0x13, 0x10); /* (x & 0xEC) | 0x10 */ - d1830_rmw(client, 42, 0x3F, 0x14); /* (x & 0xC0) | 0x14 */ - d1830_rmw(client, 43, 0x0F, 0x01); /* (x & 0xF0) | 0x01 */ - i2c_smbus_write_byte_data(client, 14, 0x20); - /* 36/37 depend on ADC helper — leave unread defaults */ - d1830_rmw(client, 38, 0x01, 0); /* clear bit0 */ - /* do not RMW reg 13 — poweroff register */ - - dev_info(dev, "SEC PMIC rail seq applied (sub_23EC + sub_27F4 safe)\n"); + dev_info(dev, "n31-pmic: sub_27F4-equivalent complete (POWEROFF skipped)\n"); return 0; } @@ -867,30 +998,17 @@ static int d1830_gpio_probe(struct i2c_client *client) if (ret) return ret; + d1830_poweroff_client = client; + /* Opt-in only. Default probe is GPIO + VBAT reads — no rail writes. * The old default seq wrote reg 13 = 0x01 (POWEROFF bit) at boot. */ if (of_property_read_bool(dev->of_node, "dlg,apply-sec-rails")) d1830_sec_rail_seq(client); else - dev_info(dev, "d1830 gpio-only (rail seq off; set dlg,apply-sec-rails to enable)\n"); - - { - static const u8 dump_regs[] = { - 1, 2, 3, 5, 13, 14, 16, 17, 19, 20, 21, 22, 23, - 26, 35, 36, 37, 41, 48, 49, 50, 96, 110, 111 - }; - int i, v; + dev_info(dev, "d1830 gpio-only (rail seq off; CS42 calls d1830_audio_rails)\n"); - dev_dbg(dev, "PMIC identity dump @0x%02x:\n", client->addr); - for (i = 0; i < ARRAY_SIZE(dump_regs); i++) { - v = i2c_smbus_read_byte_data(client, dump_regs[i]); - if (v < 0) - dev_dbg(dev, " reg 0x%02u: ERR %d\n", dump_regs[i], v); - else - dev_dbg(dev, " reg 0x%02u = 0x%02x\n", dump_regs[i], v); - } - } + d1830_log_audio_regs(client, "probe"); gpio_dev->gpio_chip.label = dev_name(dev); gpio_dev->gpio_chip.parent = dev; @@ -914,11 +1032,14 @@ static int d1830_gpio_probe(struct i2c_client *client) if (ret) dev_warn(dev, "sysfs do_poweroff unavailable: %d\n", ret); + ret = device_create_file(dev, &dev_attr_audio_rails); + if (ret) + dev_warn(dev, "sysfs audio_rails unavailable: %d\n", ret); + ret = device_create_file(dev, &dev_attr_vbat_raw); if (ret) dev_warn(dev, "sysfs vbat_raw unavailable: %d\n", ret); - d1830_poweroff_client = client; if (!pm_power_off) { pm_power_off = d1830_pm_power_off; dev_info(dev, "registered pm_power_off (SEC reg %u bit0)\n", @@ -1042,6 +1163,7 @@ static void d1830_gpio_remove(struct i2c_client *client) if (gpio_dev) cancel_delayed_work_sync(&gpio_dev->trace); device_remove_file(&client->dev, &dev_attr_vbat_raw); + device_remove_file(&client->dev, &dev_attr_audio_rails); device_remove_file(&client->dev, &dev_attr_do_poweroff); if (pm_power_off == d1830_pm_power_off) pm_power_off = NULL; diff --git a/drivers/misc/apple-tristar-cbtl1609.c b/drivers/misc/apple-tristar-cbtl1609.c index e37d9d2e91a905..12300bc11fab83 100755 --- a/drivers/misc/apple-tristar-cbtl1609.c +++ b/drivers/misc/apple-tristar-cbtl1609.c @@ -2,30 +2,150 @@ /* * Apple Lightning Tristar mux — NXP CBTL1609A1 (iPod nano 7G / N31) * - * Public “0x34 write / 0x35 read” is 8-bit; Linux 7-bit address is 0x1a. - * THS7383 Dx/ACCx pin tables are public (nyansatan); CBTL1609 I2C indices - * that program them are still OPEN in public docs. RetailOS RE shows - * **zero** Dx/mux register writes — dump is flat until accessory attaches. - * Only apple,init-sequence from DT may write. UDC soft reconnect is done - * from initramfs via sysfs udc soft_connect. + * Transport (proven): I2C0 7-bit 0x1a. Public 0x34 write / 0x35 read is + * the 8-bit form of that address (nyansatan). + * + * Routing is IDBUS inside the chip, not Linux Dx register writes. + * RetailOS N31 RE observed zero Dx/mux I2C writes. The 0x75 accessory + * ID byte programs ACCx/Dx per the THS7383 tables (nyansatan; first-gen + * CBTL1608 is documented as backwards compatible with those tables). + * CBTL1609 is the nano7 first-gen part — same IDBUS decode, no invented + * I2C mux map. + * + * N31 analog 3.5 mm jack is CS42 + MikeyBus UART2 (accessoryMgr type 1, + * sub_35A4). It is not a Tristar Dx path. Lightning analog EarPods use + * ID 04 F1 00 00 00 00 (nyansatan) — a different connector. CS42 still + * refreshes Tristar on prepare so we log whether Lightning is USB, + * analog, idle, or I2C-echo; we do not mute the jack because USB is + * routed. + * + * OSOS software lane (sub_11C8C): TriStarID/VBUS/CONDET tasks wait on + * event source 13 and branch on raw bits 0x01/0x04/0x08/0x10/0x20. + * Those bits are not mapped to I2C registers (TODO RE). Linux keeps + * osos_event=0 and reports hardware observations separately. Do not + * treat dump-not-flat as OSOS bit 0x04. + * + * Register 0x11 is CBTL1610 "configuration status" (Lina/nyansatan). + * First-gen may NAK it or the bus may echo 0x35 — the read result is + * reported, never invented. + * + * accessoryMgr type 2 is TODO RE — logged, not stub-handled. */ -#include +#include +#include +#include #include +#include +#include #include -#include +#include +#include +#include #include +#include #define TRISTAR_DUMP_LEN 0x40 +#define TRISTAR_LOG_LEN 64 + +/* OSOS sub_11C8C masks — names only. Not produced from I2C until RE maps them. */ +#define N31_TS_EVENT_01 BIT(0) +#define N31_TS_EVENT_04 BIT(2) +#define N31_TS_EVENT_08 BIT(3) +#define N31_TS_EVENT_10 BIT(4) +#define N31_TS_EVENT_20 BIT(5) + +/* nyansatan 0x75 first byte: ACCx[7:6] Dx[5:4] DATA[3:0] */ +#define TS_ID_ACCX(id0) (((id0) >> 6) & 3) +#define TS_ID_DX(id0) (((id0) >> 4) & 3) + +struct tristar_id_sig { + u8 bytes[6]; + const char *name; +}; + +/* nyansatan Lightning ID table (HOSTID=1). */ +static const struct tristar_id_sig tristar_known_ids[] = { + { { 0x10, 0x0c, 0x00, 0x00, 0x00, 0x00 }, "usb-cable" }, + { { 0x04, 0xf1, 0x00, 0x00, 0x00, 0x00 }, "lightning-analog" }, + { { 0x0b, 0xf0, 0x00, 0x00, 0x00, 0x00 }, "haywire-hdmi" }, + { { 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 }, "dcsd-or-uart-charge" }, + { { 0x20, 0x02, 0x00, 0x00, 0x00, 0x00 }, "kong-swd-idle" }, + { { 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00 }, "kong-swd-astris" }, + { { 0x20, 0x0e, 0x00, 0x00, 0x00, 0x00 }, "kanzi-swd-idle" }, + { { 0xa0, 0x0c, 0x00, 0x00, 0x00, 0x00 }, "kanzi-swd-astris" }, + { { 0x20, 0x00, 0x10, 0x00, 0x00, 0x00 }, "uart-charge" }, +}; struct apple_tristar { struct i2c_client *client; + struct mutex lock; + struct delayed_work poll; + struct dentry *debug_root; u8 last_dump[TRISTAR_DUMP_LEN]; u8 read_reg; u8 read_val; bool dump_ok; bool dump_flat; + bool i2c_echo; + int reg11_ret; + u8 reg11; + bool id_valid; + u8 id75[6]; + unsigned int id_off; + const char *id_name; + u8 accx; + u8 dx; + /* OSOS v36 — stays 0. Not synthesized from I2C. */ + u8 osos_event; + u8 prev_osos_event; + bool cf9_latch; + u8 cfa_state; + u32 seen_mask; + u32 polls; + u32 deltas; + u32 writes; + u32 i2c_fail_streak; + bool poll_disabled; + char log[TRISTAR_LOG_LEN][112]; + unsigned int log_head; + unsigned int log_count; }; +static struct apple_tristar *tristar_singleton; +static DEFINE_MUTEX(tristar_singleton_lock); + +int apple_tristar_refresh(void); +int apple_tristar_connected(void); +int apple_tristar_usb_routed(void); +int apple_tristar_lightning_analog(void); +int apple_tristar_vbus(void); +int apple_tristar_config_reg11(u8 *val); +void apple_tristar_log_audio_path(struct device *audio_dev); + +static bool read_only = true; +module_param(read_only, bool, 0644); +MODULE_PARM_DESC(read_only, + "Refuse I2C writes (default 1). IDBUS routing does not need them."); + +static bool unsafe_acks; +module_param(unsafe_acks, bool, 0600); +MODULE_PARM_DESC(unsafe_acks, + "Unused: OSOS event ack path not recovered. Default 0."); + +static bool unsafe_writes; +module_param(unsafe_writes, bool, 0600); +MODULE_PARM_DESC(unsafe_writes, + "Allow poke / DT init-sequence writes. Default 0."); + +/* + * Glass: I2C0 often -110 / echo 0x35. Polling reg0 every 250ms floods dmesg + * and burns the bus. Default off; enable only when I2C0 ACKs for real. + */ +static int poll_ms; +module_param(poll_ms, int, 0644); +MODULE_PARM_DESC(poll_ms, + "Status poll interval ms (0=off). Default 0 — I2C0 often times out."); + static int tristar_read_reg(struct apple_tristar *ts, u8 reg, u8 *val) { int ret = i2c_smbus_read_byte_data(ts->client, reg); @@ -38,7 +158,14 @@ static int tristar_read_reg(struct apple_tristar *ts, u8 reg, u8 *val) static int tristar_write_reg(struct apple_tristar *ts, u8 reg, u8 val) { - return i2c_smbus_write_byte_data(ts->client, reg, val); + int ret; + + if (read_only && !unsafe_writes) + return -EPERM; + ret = i2c_smbus_write_byte_data(ts->client, reg, val); + if (!ret) + ts->writes++; + return ret; } static bool tristar_dump_is_flat(const u8 *dump, size_t len) @@ -52,35 +179,443 @@ static bool tristar_dump_is_flat(const u8 *dump, size_t len) return true; } -static int tristar_dump(struct apple_tristar *ts) +static u8 tristar_read_addr_echo(struct apple_tristar *ts) +{ + return (u8)((ts->client->addr << 1) | 1); +} + +static const char *tristar_dx_usb_id0(u8 dx) +{ + switch (dx) { + case 0: + return "hiz"; + case 1: + return "usb0-on-dp1dn1"; + case 2: + return "usb0-on-dp1dn1+uart-on-dp2dn2"; + default: + return "hiz"; + } +} + +static const char *tristar_dx_usb_id1(u8 dx) +{ + switch (dx) { + case 0: + return "hiz"; + case 1: + return "usb0-on-dp2dn2"; + case 2: + return "usb0-on-dp1dn1+uart-on-dp2dn2"; + default: + return "hiz"; + } +} + +static const char *tristar_accx_name(u8 accx) +{ + switch (accx) { + case 0: + return "hiz-idbus"; + case 1: + return "uart1"; + case 2: + return "jtag-swd"; + default: + return "host-reset"; + } +} + +static bool tristar_usb_dp_from_dx(u8 dx) +{ + return dx == 1 || dx == 2; +} + +static bool tristar_is_lightning_analog(struct apple_tristar *ts) +{ + return ts->id_valid && ts->id_name && + !strcmp(ts->id_name, "lightning-analog"); +} + +static const char *tristar_id_label(struct apple_tristar *ts) +{ + if (ts->i2c_echo) + return "i2c-echo"; + if (ts->id_name) + return ts->id_name; + if (!ts->dump_ok) + return "unread"; + if (ts->dump_flat) + return "idle"; + return "unknown"; +} + +static void tristar_log_line(struct apple_tristar *ts, const char *why) +{ + unsigned int i = ts->log_head; + + snprintf(ts->log[i], sizeof(ts->log[i]), + "%s osos=0x%02x (unmapped) flat=%d echo=%d id=%s accx=%u dx=%u r11=%s%02x", + why, ts->osos_event, ts->dump_flat, ts->i2c_echo, + tristar_id_label(ts), ts->accx, ts->dx, + ts->reg11_ret ? "ERR" : "", + ts->reg11_ret ? 0 : ts->reg11); + ts->log_head = (i + 1) % TRISTAR_LOG_LEN; + if (ts->log_count < TRISTAR_LOG_LEN) + ts->log_count++; +} + +static void tristar_clear_id(struct apple_tristar *ts) +{ + ts->id_valid = false; + ts->id_name = NULL; + ts->id_off = 0; + memset(ts->id75, 0, sizeof(ts->id75)); + ts->accx = 0; + ts->dx = 0; +} + +static void tristar_find_id(struct apple_tristar *ts) +{ + unsigned int s, off; + const struct tristar_id_sig *sig; + + tristar_clear_id(ts); + + if (!ts->dump_ok || ts->dump_flat || ts->i2c_echo) + return; + + for (s = 0; s < ARRAY_SIZE(tristar_known_ids); s++) { + sig = &tristar_known_ids[s]; + for (off = 0; off + 6 <= TRISTAR_DUMP_LEN; off++) { + if (memcmp(ts->last_dump + off, sig->bytes, 6)) + continue; + memcpy(ts->id75, sig->bytes, 6); + ts->id_name = sig->name; + ts->id_valid = true; + ts->id_off = off; + ts->accx = TS_ID_ACCX(sig->bytes[0]); + ts->dx = TS_ID_DX(sig->bytes[0]); + dev_info(&ts->client->dev, + "tristar: IDBUS id %s @dump+0x%x accx=%u(%s) dx=%u id0=%s id1=%s\n", + sig->name, off, ts->accx, + tristar_accx_name(ts->accx), ts->dx, + tristar_dx_usb_id0(ts->dx), + tristar_dx_usb_id1(ts->dx)); + return; + } + } +} + +static int tristar_refresh_locked(struct apple_tristar *ts, const char *why) { int i, ret; u8 v; + u8 prior[TRISTAR_DUMP_LEN]; + unsigned int n = 0; + u8 echo = tristar_read_addr_echo(ts); + bool echo_now; + const char *prev_id; + + memcpy(prior, ts->last_dump, sizeof(prior)); + prev_id = tristar_id_label(ts); for (i = 0; i < TRISTAR_DUMP_LEN; i++) { - ret = tristar_read_reg(ts, i, &v); + ret = tristar_read_reg(ts, (u8)i, &v); if (ret) { - dev_warn(&ts->client->dev, - "read 0x%02x failed: %d\n", i, ret); ts->dump_ok = false; + ts->i2c_echo = false; + tristar_clear_id(ts); + ts->reg11_ret = ret; + ts->i2c_fail_streak++; + /* + * One line per outage, not one per poll. I2C0 -110 is + * common until the mux/bus is actually live. Kill poll + * immediately so a leftover poll_ms=250 never storms. + */ + ts->poll_disabled = true; + if (ts->i2c_fail_streak == 1) + dev_warn(&ts->client->dev, + "tristar: I2C read 0x%02x failed %d (poll off until bus recovers)\n", + i, ret); return ret; } + ts->i2c_fail_streak = 0; ts->last_dump[i] = v; + /* + * I2C0 often echoes the 8-bit read address (0x35). Four + * identical echo bytes means the dump is not chip SRAM — + * stop before a 64-register storm. + */ + if (i == 3 && ts->last_dump[0] == echo && + ts->last_dump[1] == echo && + ts->last_dump[2] == echo && + ts->last_dump[3] == echo) { + memset(ts->last_dump, echo, TRISTAR_DUMP_LEN); + break; + } } - ts->dump_ok = true; + echo_now = tristar_dump_is_flat(ts->last_dump, TRISTAR_DUMP_LEN) && + ts->last_dump[0] == echo; + ts->i2c_echo = echo_now; ts->dump_flat = tristar_dump_is_flat(ts->last_dump, TRISTAR_DUMP_LEN); - dev_dbg(&ts->client->dev, - "CBTL1609 dump[0..0x3f] on %s:\n", - ts->client->adapter->name); - dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump); - dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 16); - dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 32); - dev_dbg(&ts->client->dev, " %*ph\n", 16, ts->last_dump + 48); + if (echo_now) { + ts->reg11_ret = -ENOTSUPP; + ts->reg11 = echo; + tristar_clear_id(ts); + } else { + ts->reg11 = ts->last_dump[0x11]; + ts->reg11_ret = 0; + tristar_find_id(ts); + } + + ts->polls++; + for (i = 0; i < TRISTAR_DUMP_LEN; i++) { + if (prior[i] != ts->last_dump[i]) + n++; + } + ts->deltas += n; + + if (n || strcmp(prev_id, tristar_id_label(ts))) { + dev_info(&ts->client->dev, + "tristar: %s osos=0x00 (unmapped) flat=%d echo=%d deltas=%u id=%s r11=%d/%02x CONDET=%s VBUS=ENOTSUPP accmgr_type2=TODO\n", + why, ts->dump_flat, ts->i2c_echo, n, + tristar_id_label(ts), ts->reg11_ret, + ts->reg11_ret ? 0 : ts->reg11, + (!ts->dump_ok || ts->i2c_echo) ? "unknown" : + (ts->dump_flat ? "idle" : "not-flat")); + if (!ts->dump_flat && !ts->i2c_echo && !ts->id_valid) + dev_info(&ts->client->dev, + "tristar: unknown non-flat dump[0..15] %*ph\n", + 16, ts->last_dump); + tristar_log_line(ts, why); + } return 0; } +static int tristar_poll_cheap_locked(struct apple_tristar *ts) +{ + u8 v, echo = tristar_read_addr_echo(ts); + int ret; + + if (ts->poll_disabled) + return -ENOTSUPP; + + ret = tristar_read_reg(ts, 0x00, &v); + if (ret) { + ts->i2c_fail_streak++; + ts->dump_ok = false; + ts->poll_disabled = true; + if (ts->i2c_fail_streak == 1) + dev_warn(&ts->client->dev, + "tristar: poll read 0x00 failed %d — disabling poll\n", + ret); + /* Do not call full 64-reg refresh on NACK — that is the storm. */ + return ret; + } + ts->i2c_fail_streak = 0; + if (ts->i2c_echo && v == echo) { + ts->polls++; + return 0; + } + if (ts->dump_ok && ts->dump_flat && !ts->i2c_echo && v == ts->last_dump[0]) { + ts->polls++; + return 0; + } + return tristar_refresh_locked(ts, "poll"); +} + +static int tristar_refresh(struct apple_tristar *ts, const char *why) +{ + int ret; + + mutex_lock(&ts->lock); + ret = tristar_refresh_locked(ts, why); + mutex_unlock(&ts->lock); + return ret; +} + +int apple_tristar_refresh(void) +{ + struct apple_tristar *ts; + int ret; + + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + ret = tristar_refresh(ts, "export"); + mutex_unlock(&tristar_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_tristar_refresh); + +int apple_tristar_connected(void) +{ + struct apple_tristar *ts; + int ret; + + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + mutex_lock(&ts->lock); + if (!ts->dump_ok) + ret = -EIO; + else if (ts->i2c_echo) + ret = -ENOTSUPP; + else + ret = ts->dump_flat ? 0 : 1; + mutex_unlock(&ts->lock); + mutex_unlock(&tristar_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_tristar_connected); + +int apple_tristar_usb_routed(void) +{ + struct apple_tristar *ts; + int ret; + + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + mutex_lock(&ts->lock); + if (!ts->dump_ok) + ret = -EIO; + else if (ts->i2c_echo) + ret = -ENOTSUPP; + else if (!ts->id_valid) + ret = 0; + else + ret = tristar_usb_dp_from_dx(ts->dx) ? 1 : 0; + mutex_unlock(&ts->lock); + mutex_unlock(&tristar_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_tristar_usb_routed); + +int apple_tristar_lightning_analog(void) +{ + struct apple_tristar *ts; + int ret; + + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + mutex_lock(&ts->lock); + if (!ts->dump_ok) + ret = -EIO; + else if (ts->i2c_echo) + ret = -ENOTSUPP; + else + ret = tristar_is_lightning_analog(ts) ? 1 : 0; + mutex_unlock(&ts->lock); + mutex_unlock(&tristar_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_tristar_lightning_analog); + +int apple_tristar_vbus(void) +{ + mutex_lock(&tristar_singleton_lock); + if (!tristar_singleton) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + mutex_unlock(&tristar_singleton_lock); + /* TriStarVBUSProcessTask exists; I2C register is TODO RE. */ + return -ENOTSUPP; +} +EXPORT_SYMBOL_GPL(apple_tristar_vbus); + +int apple_tristar_config_reg11(u8 *val) +{ + struct apple_tristar *ts; + int ret; + + if (!val) + return -EINVAL; + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + return -ENODEV; + } + mutex_lock(&ts->lock); + if (!ts->dump_ok) + ret = -EIO; + else if (ts->i2c_echo) + ret = -ENOTSUPP; + else if (ts->reg11_ret) + ret = ts->reg11_ret; + else { + *val = ts->reg11; + ret = 0; + } + mutex_unlock(&ts->lock); + mutex_unlock(&tristar_singleton_lock); + return ret; +} +EXPORT_SYMBOL_GPL(apple_tristar_config_reg11); + +void apple_tristar_log_audio_path(struct device *audio_dev) +{ + struct apple_tristar *ts; + + if (!audio_dev) + return; + + mutex_lock(&tristar_singleton_lock); + ts = tristar_singleton; + if (!ts) { + mutex_unlock(&tristar_singleton_lock); + dev_info(audio_dev, + "tristar unbound — 3.5mm is Mikey/CS42, not Lightning Dx\n"); + return; + } + mutex_lock(&ts->lock); + tristar_refresh_locked(ts, "audio-path"); + dev_info(audio_dev, + "tristar audio-path: lightning=%s flat=%d echo=%d usb_dx=%u accx=%u analog_lightning=%d — 3.5mm jack is CS42+Mikey not Dx; USB route does not mute jack\n", + tristar_id_label(ts), ts->dump_flat, ts->i2c_echo, ts->dx, + ts->accx, tristar_is_lightning_analog(ts)); + if (tristar_is_lightning_analog(ts)) + dev_warn(audio_dev, + "Lightning analog ID 04 F1 present — that is EarPods-on-Lightning, not the onboard 3.5mm CS42 jack\n"); + mutex_unlock(&ts->lock); + mutex_unlock(&tristar_singleton_lock); +} +EXPORT_SYMBOL_GPL(apple_tristar_log_audio_path); + +static void tristar_poll_work(struct work_struct *work) +{ + struct apple_tristar *ts = container_of(to_delayed_work(work), + struct apple_tristar, poll); + + mutex_lock(&ts->lock); + tristar_poll_cheap_locked(ts); + if (ts->poll_disabled) { + mutex_unlock(&ts->lock); + return; + } + mutex_unlock(&ts->lock); + if (poll_ms > 0) + schedule_delayed_work(&ts->poll, msecs_to_jiffies(poll_ms)); +} + static int tristar_apply_init_sequence(struct apple_tristar *ts) { struct device *dev = &ts->client->dev; @@ -94,6 +629,11 @@ static int tristar_apply_init_sequence(struct apple_tristar *ts) n = of_property_count_u32_elems(np, "apple,init-sequence"); if (n <= 0) return 0; + if (!unsafe_writes && read_only) { + dev_warn(dev, + "tristar: apple,init-sequence present but read_only=1 unsafe_writes=0 — not applied\n"); + return 0; + } if (n % 2) { dev_err(dev, "apple,init-sequence must be reg,val pairs\n"); return -EINVAL; @@ -108,37 +648,26 @@ static int tristar_apply_init_sequence(struct apple_tristar *ts) reg, val, ret); return ret; } - dev_dbg(dev, "init 0x%02x <= 0x%02x\n", reg, val); + dev_info(dev, "init 0x%02x <= 0x%02x\n", reg, val); udelay(100); } return 0; } -/* - * Mode heuristic from dump only — no invented mux map. - * Flat dump → unknown; non-flat → "active" (register diversity seen). - */ -static const char *tristar_mode_name(struct apple_tristar *ts) -{ - if (!ts->dump_ok) - return "unknown"; - if (ts->dump_flat) - return "unknown"; - return "active"; -} - static ssize_t dump_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); int i, n = 0; - if (tristar_dump(ts)) + if (tristar_refresh(ts, "sysfs-dump")) return -EIO; + mutex_lock(&ts->lock); for (i = 0; i < TRISTAR_DUMP_LEN; i++) n += scnprintf(buf + n, PAGE_SIZE - n, "%02x%s", ts->last_dump[i], (i + 1) % 16 ? " " : "\n"); + mutex_unlock(&ts->lock); return n; } static DEVICE_ATTR_RO(dump); @@ -154,7 +683,9 @@ static ssize_t poke_store(struct device *dev, struct device_attribute *attr, return -EINVAL; if (reg > 0xff || val > 0xff) return -EINVAL; - ret = tristar_write_reg(ts, reg, val); + if (!unsafe_writes) + return -EPERM; + ret = tristar_write_reg(ts, (u8)reg, (u8)val); if (ret) return ret; dev_info(dev, "poke 0x%02x <= 0x%02x\n", reg, val); @@ -166,13 +697,66 @@ static ssize_t mode_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + const char *name; - if (tristar_dump(ts)) - return sysfs_emit(buf, "unknown\n"); - return sysfs_emit(buf, "%s\n", tristar_mode_name(ts)); + if (tristar_refresh(ts, "sysfs-mode")) + return sysfs_emit(buf, "unread\n"); + mutex_lock(&ts->lock); + name = tristar_id_label(ts); + mutex_unlock(&ts->lock); + return sysfs_emit(buf, "%s\n", name); } static DEVICE_ATTR_RO(mode); +static ssize_t route_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + int n; + + if (tristar_refresh(ts, "sysfs-route")) + return -EIO; + mutex_lock(&ts->lock); + n = sysfs_emit(buf, + "id=%s valid=%d accx=%u (%s) dx=%u id0=%s id1=%s usb=%d lightning_analog=%d echo=%d jack_3v5=cs42+mikey vbus=ENOTSUPP osos_event=0x00\n", + tristar_id_label(ts), ts->id_valid, ts->accx, + tristar_accx_name(ts->accx), ts->dx, + tristar_dx_usb_id0(ts->dx), tristar_dx_usb_id1(ts->dx), + ts->id_valid && tristar_usb_dp_from_dx(ts->dx), + tristar_is_lightning_analog(ts), ts->i2c_echo); + mutex_unlock(&ts->lock); + return n; +} +static DEVICE_ATTR_RO(route); + +static ssize_t audio_path_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + int n; + + if (tristar_refresh(ts, "sysfs-audio-path")) + return -EIO; + mutex_lock(&ts->lock); + if (tristar_is_lightning_analog(ts)) + n = sysfs_emit(buf, + "selected=lightning-analog (04 F1) — not onboard 3.5mm\n"); + else + n = sysfs_emit(buf, + "selected=onboard-3.5mm cs42+mikey lightning=%s\n", + tristar_id_label(ts)); + mutex_unlock(&ts->lock); + return n; +} +static DEVICE_ATTR_RO(audio_path); + +static ssize_t vbus_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + return sysfs_emit(buf, "ENOTSUPP (TriStarVBUSProcessTask I2C map TODO RE)\n"); +} +static DEVICE_ATTR_RO(vbus); + static ssize_t read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { @@ -214,12 +798,22 @@ static ssize_t verify_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + int n; - if (tristar_dump(ts)) + if (tristar_refresh(ts, "sysfs-verify")) return sysfs_emit(buf, "FAIL read\n"); - if (ts->dump_flat) - return sysfs_emit(buf, "FAIL flat 0x%02x\n", ts->last_dump[0]); - return sysfs_emit(buf, "STATUS_OK non-flat\n"); + mutex_lock(&ts->lock); + if (ts->i2c_echo) + n = sysfs_emit(buf, + "I2C_ECHO 0x%02x (8-bit read addr; chip SRAM not visible)\n", + ts->last_dump[0]); + else if (ts->dump_flat) + n = sysfs_emit(buf, "IDLE flat 0x%02x (no Lightning IDBUS accessory)\n", + ts->last_dump[0]); + else + n = sysfs_emit(buf, "STATUS_OK id=%s\n", tristar_id_label(ts)); + mutex_unlock(&ts->lock); + return n; } static DEVICE_ATTR_RO(verify); @@ -227,33 +821,26 @@ static ssize_t poll_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); - u8 prior[TRISTAR_DUMP_LEN]; - unsigned int i, deltas = 0; int ret; - memcpy(prior, ts->last_dump, sizeof(prior)); - ret = tristar_dump(ts); - if (ret) - return ret; - - for (i = 0; i < TRISTAR_DUMP_LEN; i++) { - if (prior[i] != ts->last_dump[i]) - deltas++; - } - - dev_info(dev, "Tristar poll: flat=%d deltas=%u mode=%s (mux map OPEN)\n", - ts->dump_flat, deltas, tristar_mode_name(ts)); - return count; + if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') + return -EINVAL; + ret = tristar_refresh(ts, "sysfs-poll"); + return ret ? ret : count; } static ssize_t poll_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_tristar *ts = i2c_get_clientdata(to_i2c_client(dev)); + int n; - return sysfs_emit(buf, - "flat=%d last_ok=%d — echo 1 > poll to re-dump\n", - ts->dump_flat, ts->dump_ok); + mutex_lock(&ts->lock); + n = sysfs_emit(buf, "flat=%d echo=%d last_ok=%d polls=%u id=%s\n", + ts->dump_flat, ts->i2c_echo, ts->dump_ok, ts->polls, + tristar_id_label(ts)); + mutex_unlock(&ts->lock); + return n; } static DEVICE_ATTR_RW(poll); @@ -261,6 +848,9 @@ static struct attribute *tristar_attrs[] = { &dev_attr_dump.attr, &dev_attr_poke.attr, &dev_attr_mode.attr, + &dev_attr_route.attr, + &dev_attr_audio_path.attr, + &dev_attr_vbus.attr, &dev_attr_read.attr, &dev_attr_value.attr, &dev_attr_verify.attr, @@ -269,6 +859,127 @@ static struct attribute *tristar_attrs[] = { }; ATTRIBUTE_GROUPS(tristar); +static int tristar_dbg_anchors_show(struct seq_file *m, void *p) +{ + seq_puts(m, + "OSOS central loop: sub_11C8C\n" + "OSOS tasks: TriStarIDProcessTask, TriStarVBUSProcessTask, TriStarCONDETProcessTask\n" + "OSOS accessory manager: accessoryMgr.cpp:716 type1=MikeyBus UART2, type2=TODO RE\n" + "OSOS MikeyBus: CMikeyBusUartReadTask, CMikeyBusUartResistorTask, mikeyTask.cpp:169\n" + "I2C: 7-bit 0x1a (8-bit WR 0x34 / RD 0x35)\n" + "Routing: IDBUS 0x74/0x75 inside CBTL1609 — Linux does not write Dx\n" + "0x75 ACCx/Dx tables: THS7383 datasheet via nyansatan\n" + "reg 0x11: CBTL1610 config status (Lina); may NAK or echo on CBTL1609\n" + "OSOS v36 bits 0x01/0x04/0x08/0x10/0x20: logged as unmapped, not synthesized\n" + "3.5mm analog: CS42 + apple-mikeybus, not Tristar Dx\n" + "Lightning analog EarPods: ID 04 F1 00 00 00 00\n" + "Bootloader PMIC sub_3F40/3F60 is not Tristar\n" + "Candidate index pmic-tristar-ida-out.txt is not a write recipe\n"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_anchors); + +static int tristar_dbg_status_show(struct seq_file *m, void *p) +{ + struct apple_tristar *ts = m->private; + + mutex_lock(&ts->lock); + seq_printf(m, + "osos_event=0x%02x\nprev_osos_event=0x%02x\nseen_mask=0x%02x\n" + "osos_masks=0x%02x,0x%02x,0x%02x,0x%02x,0x%02x (I2C producer TODO RE)\n" + "cf9_latch=%d\ncfa_state=%u\n" + "dump_ok=%d dump_flat=%d i2c_echo=%d\nreg11_ret=%d reg11=0x%02x\n" + "id=%s usb_routed=%d lightning_analog=%d\n" + "read_only=%d unsafe_writes=%d unsafe_acks=%d poll_ms=%d\n" + "polls=%u writes=%u\n", + ts->osos_event, ts->prev_osos_event, ts->seen_mask & 0xff, + (unsigned int)N31_TS_EVENT_01, (unsigned int)N31_TS_EVENT_04, + (unsigned int)N31_TS_EVENT_08, (unsigned int)N31_TS_EVENT_10, + (unsigned int)N31_TS_EVENT_20, + ts->cf9_latch, ts->cfa_state, + ts->dump_ok, ts->dump_flat, ts->i2c_echo, ts->reg11_ret, + ts->reg11, tristar_id_label(ts), + ts->id_valid && tristar_usb_dp_from_dx(ts->dx), + tristar_is_lightning_analog(ts), + read_only, unsafe_writes, unsafe_acks, poll_ms, ts->polls, + ts->writes); + mutex_unlock(&ts->lock); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_status); + +static int tristar_dbg_log_show(struct seq_file *m, void *p) +{ + struct apple_tristar *ts = m->private; + unsigned int i, n, idx; + + mutex_lock(&ts->lock); + n = ts->log_count; + idx = (ts->log_head + TRISTAR_LOG_LEN - n) % TRISTAR_LOG_LEN; + for (i = 0; i < n; i++) { + seq_printf(m, "%s\n", ts->log[idx]); + idx = (idx + 1) % TRISTAR_LOG_LEN; + } + mutex_unlock(&ts->lock); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_log); + +static int tristar_dbg_counters_show(struct seq_file *m, void *p) +{ + struct apple_tristar *ts = m->private; + + mutex_lock(&ts->lock); + seq_printf(m, "polls=%u\ndeltas=%u\nwrites=%u\nacks=0\n", + ts->polls, ts->deltas, ts->writes); + mutex_unlock(&ts->lock); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_counters); + +static int tristar_dbg_mode_show(struct seq_file *m, void *p) +{ + struct apple_tristar *ts = m->private; + + mutex_lock(&ts->lock); + seq_printf(m, + "read_only=%d\nunsafe_writes=%d\nunsafe_acks=%d\n" + "i2c_echo=%d\nid=%s\n", + read_only, unsafe_writes, unsafe_acks, ts->i2c_echo, + tristar_id_label(ts)); + mutex_unlock(&ts->lock); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_mode); + +static int tristar_dbg_unsafe_show(struct seq_file *m, void *p) +{ + seq_printf(m, "%d\n", unsafe_writes); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(tristar_dbg_unsafe); + +static void tristar_debugfs_init(struct apple_tristar *ts) +{ + ts->debug_root = debugfs_create_dir("n31_tristar", NULL); + if (IS_ERR_OR_NULL(ts->debug_root)) { + ts->debug_root = NULL; + return; + } + debugfs_create_file("source_anchors", 0444, ts->debug_root, ts, + &tristar_dbg_anchors_fops); + debugfs_create_file("raw_status", 0444, ts->debug_root, ts, + &tristar_dbg_status_fops); + debugfs_create_file("event_log", 0444, ts->debug_root, ts, + &tristar_dbg_log_fops); + debugfs_create_file("counters", 0444, ts->debug_root, ts, + &tristar_dbg_counters_fops); + debugfs_create_file("mode", 0444, ts->debug_root, ts, + &tristar_dbg_mode_fops); + debugfs_create_file("unsafe_writes_enabled", 0444, ts->debug_root, ts, + &tristar_dbg_unsafe_fops); +} + static int apple_tristar_probe(struct i2c_client *client) { struct apple_tristar *ts; @@ -280,14 +991,21 @@ static int apple_tristar_probe(struct i2c_client *client) if (!ts) return -ENOMEM; ts->client = client; + mutex_init(&ts->lock); + INIT_DELAYED_WORK(&ts->poll, tristar_poll_work); i2c_set_clientdata(client, ts); - /* - * I2C RX still returns the address byte (DS=0x31/0xe1). Do not - * require a read ACK at probe — that either fails the bind or - * storms IIC0. U-Boot DFU already routed Lightning USB. Only - * apple,init-sequence may write; RetailOS has no Dx mux map. - */ + dev_info(dev, + "N31 Tristar: OSOS sub_11C8C ID/VBUS/CONDET; I2C 7-bit 0x%02x on %s\n", + client->addr, client->adapter->name); + dev_info(dev, + "N31 Tristar: IDBUS routes USB/UART/SWD; read_only=%d unsafe_writes=%d poll_ms=%d\n", + read_only, unsafe_writes, poll_ms); + dev_info(dev, + "N31 Tristar: 3.5mm analog is CS42+MikeyBus — not a Dx write; type2 accessoryMgr TODO RE\n"); + dev_info(dev, + "N31 Tristar: OSOS v36 bits 0x01/0x04/0x08/0x10/0x20 unmapped; VBUS I2C map ENOTSUPP\n"); + if (of_property_read_bool(dev->of_node, "apple,require-ack")) { ret = tristar_read_reg(ts, 0x00, &id0); if (ret) { @@ -296,35 +1014,60 @@ static int apple_tristar_probe(struct i2c_client *client) client->addr, client->adapter->name, ret); return -ENODEV; } - dev_info(dev, - "Lightning Tristar ACK @7bit=0x%02x on %s reg0=0x%02x\n", - client->addr, client->adapter->name, id0); - } else { - dev_info(dev, - "Tristar bound @7bit=0x%02x on %s (skip ACK)\n", - client->addr, client->adapter->name); + dev_info(dev, "Lightning Tristar ACK @7bit=0x%02x reg0=0x%02x\n", + client->addr, id0); } - /* Full 64-byte dump deferred to sysfs (poll/dump) — avoid boot I2C storm */ - - /* DT-only init — never invent mux register writes in driver */ tristar_apply_init_sequence(ts); + tristar_debugfs_init(ts); ret = sysfs_create_groups(&dev->kobj, tristar_groups); if (ret) dev_warn(dev, "sysfs groups failed: %d\n", ret); + mutex_lock(&tristar_singleton_lock); + tristar_singleton = ts; + mutex_unlock(&tristar_singleton_lock); + + /* + * Probe with a single cheap ACK. Full 64-reg refresh only if the + * chip answers — otherwise one warn and stay quiet (I2C0 often + * NACKs until Lightning/mux is up). + */ + ret = tristar_read_reg(ts, 0x00, &id0); + if (ret) { + ts->dump_ok = false; + ts->poll_disabled = true; + ts->i2c_fail_streak = 1; + dev_warn(dev, + "tristar: probe ACK failed %d — leaving unbound quiet (poll off)\n", + ret); + } else { + tristar_refresh(ts, "probe"); + if (poll_ms > 0 && !ts->poll_disabled) + schedule_delayed_work(&ts->poll, + msecs_to_jiffies(poll_ms)); + } return 0; } static void apple_tristar_remove(struct i2c_client *client) { + struct apple_tristar *ts = i2c_get_clientdata(client); + + cancel_delayed_work_sync(&ts->poll); + mutex_lock(&tristar_singleton_lock); + if (tristar_singleton == ts) + tristar_singleton = NULL; + mutex_unlock(&tristar_singleton_lock); + debugfs_remove_recursive(ts->debug_root); sysfs_remove_groups(&client->dev.kobj, tristar_groups); } static const struct of_device_id apple_tristar_of_match[] = { { .compatible = "apple,tristar-cbtl1609" }, { .compatible = "nxp,cbtl1609a1" }, + { .compatible = "apple,n31-tristar" }, { }, }; MODULE_DEVICE_TABLE(of, apple_tristar_of_match); diff --git a/drivers/misc/fmss-s5l8740.c b/drivers/misc/fmss-s5l8740.c index 7068bfb30bb118..8bc8d0e51ff1dc 100755 --- a/drivers/misc/fmss-s5l8740.c +++ b/drivers/misc/fmss-s5l8740.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -418,11 +419,26 @@ static unsigned int dma_d14 = 7; module_param(dma_d14, uint, 0644); MODULE_PARM_DESC(dma_d14, "FMSS D14 address-cycles-1 for DMA (default 7 = PPN v40)"); -/* One 4096-byte host sector first (oracle G1); raise to 4 for full 16K page. */ +/* One 4096-byte host sector first (oracle G1); FTL callers override via span. */ static unsigned int dma_nsect = 1; module_param(dma_nsect, uint, 0644); MODULE_PARM_DESC(dma_nsect, "DMA span (# logical LBAs) per CS read (default 1)"); +/* + * Decomp wants command-list META. Live CS kick (C00=0xFFF5) still wedges + * the SoC on glass — keep default off until that path is safe. Callers that + * ask for meta with this off get -EOPNOTSUPP (never PIO fake spare). + */ +static bool meta_dma_read; +module_param(meta_dma_read, bool, 0644); +MODULE_PARM_DESC(meta_dma_read, + "Use command-list data+meta read for metadata callers (default N — CS kick wedges)"); + +static bool meta_dma_reset_before = true; +module_param(meta_dma_reset_before, bool, 0644); +MODULE_PARM_DESC(meta_dma_reset_before, + "Reset NAND controller before command-list metadata read"); + /* * PPN physical page = N × (4096 DATA + 16 META) records (N=2 or 4). * 5172A0: qword.lo = (rec*span) | ((rec*slot) << 16); qword.hi = encoded_ppn. @@ -5955,6 +5971,36 @@ u32 s5l8740_fmss_fil_get_info(u32 selector) } EXPORT_SYMBOL_GPL(s5l8740_fmss_fil_get_info); +static int fmss_dma_page_read_records(struct fmss_n31 *f, + unsigned int ce, u32 addr, + unsigned int slot, + unsigned int span) +{ + unsigned int saved_slot = dma_slot; + unsigned int saved_nsect = dma_nsect; + unsigned int saved_rec = dma_rec; + int ret; + + if (slot > 3) + return -EINVAL; + if (!span || span > 4 || slot + span > 4) + return -EINVAL; + + dma_slot = slot; + dma_nsect = span; + dma_rec = FMSS_PPN_REC; /* 4096 data + 16 meta */ + + if (meta_dma_reset_before) + fmss_nand_reset(f); + + ret = fmss_dma_page_read(f, ce, addr); + + dma_rec = saved_rec; + dma_nsect = saved_nsect; + dma_slot = saved_slot; + return ret; +} + int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, unsigned int slc, unsigned int chunks, @@ -5963,6 +6009,7 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, { struct fmss_n31 *f = fmss_dev; unsigned int saved; + unsigned int span; u32 addr; int ret; @@ -5974,39 +6021,69 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, if (!chunks || chunks > FMSS_MAX_CHUNKS) chunks = FMSS_MAX_CHUNKS; + /* + * PIO chunks are 1024-byte units. + * Metadata records are 4096-byte units. + */ + span = DIV_ROUND_UP(chunks, 4); + if (!span) + span = 1; + if (span > 4) + span = 4; + mutex_lock(&f->lock); + if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); f->pages_since_reset = 0; } + saved = page_chunks; page_chunks = chunks; addr = fmss_ppn_addr(cau, block, page, slc); - /* - * PIO last_spare is not proven Sogeti/Whimory META (glass 2026-08-27: - * 53-byte beat is FIFO garbage). Only take the second pass when the - * caller actually asked for a meta buffer. - */ - if (meta && meta_len) - ret = fmss_page_read_with_meta(f, ce, addr); - else + + if (meta && meta_len) { + /* + * Real metadata path. Do not fall back to PIO metadata, + * because fake metadata poisons FPart/VFL/FTL/L2V. + */ + if (!meta_dma_read || !f->dma_ok) { + ret = -EOPNOTSUPP; + goto out_restore; + } + + ret = fmss_dma_page_read_records(f, ce, addr, 0, span); + } else { ret = fmss_page_read(f, ce, addr); + } + f->pages_since_reset++; - page_chunks = saved; + if (!ret) { if (data && data_len) { - if (data_len > f->last_page_len) - data_len = f->last_page_len; + size_t have = f->last_page_len; + + if (have > span * FMSS_SECTOR_LEN) + have = span * FMSS_SECTOR_LEN; + if (data_len > have) + data_len = have; memcpy(data, f->last_page, data_len); } + if (meta && meta_len) { + size_t have = span * 16; + memset(meta, 0xff, meta_len); - if (f->last_spare_len) - memcpy(meta, f->last_spare, - min_t(size_t, meta_len, - f->last_spare_len)); + if (have > f->last_spare_len) + have = f->last_spare_len; + if (have > meta_len) + have = meta_len; + memcpy(meta, f->last_spare, have); } } + +out_restore: + page_chunks = saved; mutex_unlock(&f->lock); return ret; } diff --git a/drivers/misc/ftl-s5l8740.c b/drivers/misc/ftl-s5l8740.c index 0c93a3dfc94628..50440bf167f03a 100755 --- a/drivers/misc/ftl-s5l8740.c +++ b/drivers/misc/ftl-s5l8740.c @@ -73,20 +73,20 @@ module_param(sig_scan_blocks, uint, 0644); MODULE_PARM_DESC(sig_scan_blocks, "FPart assignment scan: tail blocks (0 = vfl_tail)"); -static unsigned int fpart_assign_pages = 16; +static unsigned int fpart_assign_pages = 1; module_param(fpart_assign_pages, uint, 0644); MODULE_PARM_DESC(fpart_assign_pages, - "Pages per tail block to scan for META 0x30 assignment (default 16)"); + "Pages per tail block to scan for META 0x30 assignment (default 1 = page0)"); static bool sig_brute_scan; module_param(sig_brute_scan, bool, 0644); MODULE_PARM_DESC(sig_brute_scan, "META 0x30 page0 brute (default N — PIO spare is not Sogeti)"); -static bool payload_magic_scan = true; +static bool payload_magic_scan; module_param(payload_magic_scan, bool, 0644); MODULE_PARM_DESC(payload_magic_scan, - "Data-only xrmw/wrmx hunt; skip META locate and sigless classify (default Y)"); + "Debug-only data xrmw/wrmx hunt (default N — FPart uses META 0x30)"); /* Kept so existing insmod lines do not fail. Recovery always runs at probe. */ static bool ftl_auto_map __maybe_unused; @@ -127,6 +127,41 @@ static bool whimory_page_blank(const u8 *p, unsigned int n) return all_ff == 0xff || all_00 == 0; } +static bool whimory_meta_erased(const u8 *m, unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) { + if (m[i] != 0xff) + return false; + } + return true; +} + +static bool whimory_meta_is_user_data(const struct whimory_meta *m) +{ + return m->type == WHIMORY_META_TYPE_DATA || + m->type == WHIMORY_META_TYPE_DATA2; +} + +static bool whimory_meta_is_cxt_base(const u8 *m, u32 vba_ofs) +{ + return m[0] == WHIMORY_META_TYPE_SFTL_CXT && + m[1] == WHIMORY_CXT_TAG_BASE && + vba_ofs == 0; +} + +static bool whimory_meta_is_btoc(const u8 *m) +{ + return m[0] == WHIMORY_META_TYPE_BTOC; +} + +static bool whimory_meta_is_data_raw(const u8 *m) +{ + return m[0] == WHIMORY_META_TYPE_DATA || + m[0] == WHIMORY_META_TYPE_DATA2; +} + static bool whimory_special_lba(u32 lba) { return (lba & 0xFFFF0000u) == WHIMORY_SPECIAL_LBA || @@ -345,6 +380,7 @@ static int whimory_range_split(struct whimory *w, struct whimory_range *r, right->start = at; right->len = r->len - left_len; right->vba = r->vba + left_len; + right->weave = r->weave; r->len = left_len; whimory_range_link(&w->ranges, right); w->sftl.range_nodes++; @@ -372,6 +408,7 @@ static int whimory_range_insert_new(struct whimory *w, u32 start, u32 len, n->start = start; n->len = len; n->vba = vba; + n->weave = w->sftl.claim_weave; whimory_range_link(&w->ranges, n); w->sftl.range_nodes++; return 0; @@ -389,7 +426,8 @@ static void whimory_range_coalesce_at(struct whimory *w, u32 start) if (p) { prev = rb_entry(p, struct whimory_range, rb); if (prev->start + prev->len == r->start && - prev->vba + prev->len == r->vba) { + prev->vba + prev->len == r->vba && + prev->weave == r->weave) { prev->len += r->len; whimory_range_erase(w, r); r = prev; @@ -399,7 +437,8 @@ static void whimory_range_coalesce_at(struct whimory *w, u32 start) if (q) { next = rb_entry(q, struct whimory_range, rb); if (r->start + r->len == next->start && - r->vba + r->len == next->vba) { + r->vba + r->len == next->vba && + r->weave == next->weave) { r->len += next->len; whimory_range_erase(w, next); } @@ -416,6 +455,25 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) if (!span || whimory_special_lba(lba)) return 0; + { + u32 end = lba + span; + struct rb_node *node = rb_first(&w->ranges); + + while (node) { + struct whimory_range *r = rb_entry(node, + struct whimory_range, + rb); + u32 r_end = r->start + r->len; + + if (r->start >= end) + break; + if (r_end > lba && r->start < end && + r->weave > w->sftl.claim_weave) + return 0; + node = rb_next(node); + } + } + hit = whimory_range_find(&w->ranges, lba); if (hit) { ret = whimory_range_split(w, hit, lba); @@ -1417,6 +1475,7 @@ static bool fpart_type_class1(u16 type_word) } static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out); +static bool fpart_meta_is_assign(const u8 *meta, u16 *type_out); static bool fpart_has_xrmw(const u8 *page); /* @@ -1445,7 +1504,10 @@ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, if (ret) continue; last = 0; - if (fpart_meta_special(meta, 0, NULL) || fpart_has_xrmw(data)) + if (fpart_meta_special(meta, 0, NULL) || + fpart_meta_is_assign(meta, NULL)) + return 0; + if (payload_magic_scan && fpart_has_xrmw(data)) return 0; } return last; @@ -1472,6 +1534,33 @@ static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out) return false; } +/* + * Scanner: META tag 0x30 and class 1. Chunk-0 assignment pages use m[1]==0 + * with class in type_word[15:8]. Do not treat payload magic as a hit. + */ +static bool fpart_meta_is_assign(const u8 *meta, u16 *type_out) +{ + unsigned int slot; + + if (!meta) + return false; + for (slot = 0; slot < 4; slot++) { + const u8 *m = meta + slot * WHIMORY_META_SIZE; + u16 tw; + + if (m[0] != FPART_SPECIAL_TAG) + continue; + tw = get_unaligned_le16(m + 2); + if ((m[1] & FPART_SPECIAL_CLASS_MASK) == FPART_SPECIAL_CLASS || + (m[1] == 0 && fpart_type_class1(tw))) { + if (type_out) + *type_out = tw; + return true; + } + } + return false; +} + static int fpart_meta_special_slot(const u8 *meta, u8 want_chunk, u16 *type_out) { unsigned int slot; @@ -1707,19 +1796,18 @@ static int fpart_scan_region(struct whimory *w, u16 type, w->fpart_ctx.slot_logs++; } } - special = fpart_meta_special(meta, 0, &type_word); + special = fpart_meta_is_assign(meta, &type_word); + if (!special) + special = fpart_meta_special(meta, 0, + &type_word); magic = fpart_has_xrmw(page); if (fpart_has_wrmx(page)) wrmx++; - if (!special && !magic) - continue; - if (special) - tag30++; - if (magic) { + if (magic) xrmw++; - if (!special) - type_word = WHIMORY_SIG_TYPE; - } + if (!special) + continue; + tag30++; fpart_bank_to_ce_cau(w, bank, &ce, &cau); dev_info(w->dev, "FPART_ASSIGN_SCAN bank=%u ce=%u cau=%u block=%u page=%u slot=%d type_word=0x%04x blank=%d m0=%16ph m1=%16ph m2=%16ph m3=%16ph data00=%32ph data80=%32ph\n", @@ -2198,18 +2286,8 @@ static int whimory_read_signature(struct whimory *w) ret = w->fpart->init(w); if (ret) return ret; - if (payload_magic_scan) { + if (payload_magic_scan) whimory_payload_magic_scan(w); - if (w->sig_ok) - return 0; - dev_warn(w->dev, - "PAYLOAD_SCAN: no usable xrmw. PIO META locate skipped; sigless classify off.\n"); - if (!allow_sigless_debug) { - dev_err(w->dev, - "sig=0 true_meta=unproven: stopping (no VFL/FTL/L2V)\n"); - return -ENOENT; - } - } ret = w->fpart->read_signature(w, w->sig.raw, WHIMORY_SIG_SIZE); if (ret) { dev_warn(w->dev, @@ -2851,6 +2929,8 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) continue; if (m[1] & 0x02) continue; + if (whimory_meta_erased(m, WHIMORY_META_SIZE)) + continue; lba = get_unaligned_le32(m + 8); w->sftl.open_slots_valid_meta++; if (whimory_special_lba(lba) || lba >= 0x01000000u) @@ -2862,8 +2942,12 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) m[0], m[1], data + slot * WHIMORY_LBA_SIZE); vba = whimory_pack_vba(w, sb->ce, sb->cau, vblock, pg, slot); - if (whimory_l2v_update(w, lba, 1, vba)) + w->sftl.claim_weave = whimory_weave48(m); + if (whimory_l2v_update(w, lba, 1, vba)) { + w->sftl.claim_weave = 0; return -ENOMEM; + } + w->sftl.claim_weave = 0; w->sftl.open_l2v_updates++; hits++; } @@ -3063,7 +3147,9 @@ static int whimory_cxt_load(struct whimory *w) dev_info(w->dev, "s_cxt_load base sb=%u weave=%llu\n", sb, w->cxt[i].weave); + w->sftl.claim_weave = w->cxt[i].weave; ret = whimory_cxt_load_sb(w, sb); + w->sftl.claim_weave = 0; if (ret) { dev_warn(w->dev, "cxt sb=%u failed %d\n", sb, ret); continue; @@ -3130,7 +3216,7 @@ static void whimory_print_recovery_stats(struct whimory *w) dev_info(w->dev, "RECOVERY_STATS:\n" " fpart_sig=%u vfl_ctx_hits=%u vfl_cxt_loc=%u vfl_bitmap=%u\n" - " classified_empty=%u classified_closed=%u classified_open=%u classified_cxt=%u\n" + " classified_empty=%u classified_closed=%u classified_open=%u classified_cxt=%u classified_unknown=%u\n" " cxt_blocks_seen=%u cxt_records_seen=%u cxt_l2v_updates=%u\n" " btoc_pages_read=%u btoc_pages_valid=%u btoc_entries_seen=%u btoc_l2v_updates=%u\n" " btoc_token_ffff0000=%u btoc_token_ffffff00=%u btoc_token_ffffffff=%u btoc_holelist_ffff0001=%u\n" @@ -3139,6 +3225,7 @@ static void whimory_print_recovery_stats(struct whimory *w) w->sig_ok, w->vfl.ctx_hits, w->vfl.cxt_loc_count, w->vfl.bitmap_loaded, s->empty_sbs, s->btoc_sbs, s->open_sbs, s->cxt_sbs, + s->unknown_sbs, s->cxt_blocks_seen, s->cxt_records_seen, s->cxt_l2v_updates, s->btoc_pages_read, s->btoc_pages_valid, s->btoc_entries_seen, s->btoc_l2v_updates, @@ -3273,9 +3360,9 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (r0 && r127) continue; if ((!r0 && whimory_page_blank(w->sftl.data_page, 64) && - whimory_page_blank(meta0, 16)) && + whimory_meta_erased(meta0, 16)) && (r127 || (whimory_page_blank(p127, 64) && - whimory_page_blank(meta127, 16)))) { + whimory_meta_erased(meta127, 16)))) { s->empty_sbs++; continue; } @@ -3284,30 +3371,31 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) sb->cau = cau; sb->block = b; sb->weave = 0; - if (!r0 && (meta0[0] == WHIMORY_META_TYPE_DATA || - meta0[0] == WHIMORY_META_TYPE_DATA2 || + if (!r0 && (whimory_meta_is_data_raw(meta0) || meta0[0] == WHIMORY_META_TYPE_SFTL_CXT)) sb->weave = whimory_weave48(meta0); - if ((!r0 && meta0[0] == WHIMORY_META_TYPE_SFTL_CXT) || - (!r127 && meta127[0] == WHIMORY_META_TYPE_SFTL_CXT)) { + if (!r0 && whimory_meta_is_cxt_base(meta0, 0)) { u32 vblock = whimory_vfl_virt(w, cau, b); u32 sb_idx = whimory_sb_index(w, ce, cau, vblock); sb->kind = WHIMORY_SB_CXT; s->cxt_sbs++; - if ((!r0 && meta0[1] == 1) || - (!r127 && meta127[1] == 1)) - whimory_cxt_add_base(w, sb_idx, - sb->weave); - } else if (!r127 && - (meta127[0] == WHIMORY_META_TYPE_BTOC || - !whimory_page_blank(p127, 64))) { + whimory_cxt_add_base(w, sb_idx, sb->weave); + } else if (!r0 && + meta0[0] == WHIMORY_META_TYPE_SFTL_CXT) { + sb->kind = WHIMORY_SB_CXT; + s->cxt_sbs++; + } else if (!r127 && whimory_meta_is_btoc(meta127)) { sb->kind = WHIMORY_SB_CLOSED; s->btoc_sbs++; - } else { + } else if ((!r0 && whimory_meta_is_data_raw(meta0)) || + (!r127 && whimory_meta_is_data_raw(meta127))) { sb->kind = WHIMORY_SB_OPEN; s->open_sbs++; + } else { + s->unknown_sbs++; + continue; } nsb++; } @@ -3316,8 +3404,9 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) classify_done: sort(s->sbs, nsb, sizeof(s->sbs[0]), whimory_sb_cmp, NULL); dev_info(w->dev, - "SFTL classified nsb=%u closed=%u open=%u cxt=%u empty=%u\n", - nsb, s->btoc_sbs, s->open_sbs, s->cxt_sbs, s->empty_sbs); + "SFTL classified nsb=%u closed=%u open=%u cxt=%u empty=%u unknown=%u\n", + nsb, s->btoc_sbs, s->open_sbs, s->cxt_sbs, s->empty_sbs, + s->unknown_sbs); ret = whimory_cxt_load(w); if (ret) @@ -3353,9 +3442,11 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) s->btoc_page, meta127); s->btoc_dumps_left--; } + s->claim_weave = sb->weave; ingested = whimory_ingest_btoc_page(w, sb->ce, sb->cau, vblock, s->btoc_page, S5L8740_FMSS_PAGE_SIZE); + s->claim_weave = 0; if (ingested) s->btoc_pages_valid++; } else if (sb->kind == WHIMORY_SB_OPEN) { @@ -3700,18 +3791,27 @@ static int whimory_validate_meta(struct whimory *w, { u32 meta_lba = le32_to_cpu(m->lba); + if (!whimory_meta_is_user_data(m)) { + dev_err(w->dev, + "sftl non-data meta want=0x%x type=%02x flags=%02x lba=0x%x\n", + expected_lba, m->type, m->flags, meta_lba); + return -EIO; + } + if (meta_lba != expected_lba) { dev_err(w->dev, "sftl lba mismatch want=0x%x meta=0x%x type=%02x flags=%02x\n", expected_lba, meta_lba, m->type, m->flags); return -EIO; } + if (m->flags & 0x02) { dev_err(w->dev, "sftl uECC flag lba=0x%x type=%02x flags=%02x\n", expected_lba, m->type, m->flags); return -EIO; } + return 0; } diff --git a/drivers/misc/s5l8740-iis2-mmio.c b/drivers/misc/s5l8740-iis2-mmio.c index 100f5f180586c0..12792264d863fb 100755 --- a/drivers/misc/s5l8740-iis2-mmio.c +++ b/drivers/misc/s5l8740-iis2-mmio.c @@ -1,24 +1,252 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * S5L8740 IIS2 MMIO hook — FM digital RX @ 0x3D400000 (N31 RE). - * Register model OPEN: probe + regs sysfs only, no invented capture PCM. + * S5L8740 IIS2 @ 0x3D400000 — BCM2078 digital PCM port (N31). + * + * RetailOS oracles (fm/, bt-*-scsi-live/): + * IIS1 @ 0x3CD00000 = XSP, always zero — NOT BCM TX. + * IIS2 = shared BCM2078 I²S: RX FIFO @ +0x38 (PL080 peri 13, FM + module PCM in), + * TXCON @ +0x04 = 0x0b000099 programmed for music/FM/BT (TXCOM often 0 on BT). + * BT A2DP over-the-air = UART1 @ 0x3DB HCI → BCM2078 (no IIS0/CS42). + * CLKCON +0x00 = 0x1 + * TXCON +0x04 = 0x0b000099 (RetailOS programs this on IIS2 too) + * RXCON +0x30 = 0x1000 + * RXCOM +0x34 = 0x6 (DMA kick; idle/stopped often 0x2) + * RXFIFO +0x38 ← PL080 peri 13 P2M + * STATUS +0x3c = 0x10804 live + * CLKDIV +0x40 = 0x96 (FM oracle; IIS0 play uses 0x177/375) + * REG44 +0x44 = 0x00010007 (same as IIS0 music oracle) + * + * SoC clocks: CLKCON+0x30 = 0x32190 play; FM also +0x10 = 0x4 + * (vs music/idle 0x8004). No FM→BT / A2DP path here — local PCM only. */ #include +#include #include #include #include #include #include +#include +#include +#include +#include -#define IIS2_MMIO_LEN 0x40 +#include "n31-audio-rates.h" + +#define S5L8740_IIS2_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +#define S5L8740_IIS2_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) + +#define I2SCLKCON 0x00 +#define I2STXCON 0x04 +#define I2SRXCON 0x30 +#define I2SRXCOM 0x34 +#define I2SRXFIFO 0x38 +#define I2SSTATUS 0x3c +#define I2SCLKDIV 0x40 +#define I2SREG44 0x44 + +#define MCLK_ASSUME_HZ 12000000u + +/* fm/20260826T203834Z oracle */ +#define IIS2_CLKCON_ON 0x1u +#define IIS2_TXCON_FM 0x0b000099u +#define IIS2_RXCON_FM 0x1000u +#define IIS2_RXCOM_DMA 0x6u +#define IIS2_RXCOM_IDLE 0x2u +#define IIS2_CLKDIV_FM_ORACLE 0x96u +#define IIS2_REG44_ORACLE 0x00010007u + +static uint iis2_clkdiv; +module_param(iis2_clkdiv, uint, 0644); +MODULE_PARM_DESC(iis2_clkdiv, "IIS2 CLKDIV override; 0 = FM oracle 0x96"); + +#define CLKCON_PHYS 0x3c500000ul +#define CLKCON_AUDIO_OFF 0x30 +#define CLKCON_FM_GATE_OFF 0x10 +#define CLKCON_AUDIO_PLAY 0x32190u +#define CLKCON_AUDIO_IDLE 0x1c20u +#define CLKCON_FM_GATE_ON 0x4u +#define CLKCON_FM_GATE_OFF_VAL 0x8004u + +#define IIS2_REGS_LEN 0x48 struct s5l8740_iis2 { void __iomem *base; + void __iomem *clkcon; + struct device *dev; struct clk_bulk_data *clks; int num_clks; + bool has_dma; + struct snd_dmaengine_dai_dma_data cap_dma; + u32 clkcon10_saved; + bool clkcon10_held; + unsigned int rate; }; -static ssize_t regs_show(struct device *dev, struct device_attribute *a, char *buf) +static u32 iis2_pick_clkdiv(unsigned int rate) +{ + const struct n31_rate_cfg *r; + + if (iis2_clkdiv) + return iis2_clkdiv; + /* + * FM IIS2 oracle differs from IIS0: 0x96 while IIS0 HP path uses + * 0x177 (32 kHz table entry) during the same FM session. + */ + if (rate == 44100 || rate == 48000) + return IIS2_CLKDIV_FM_ORACLE; + r = n31_find_rate(rate); + if (r) + return r->clkdiv; + if (!rate) + rate = 44100; + return MCLK_ASSUME_HZ / rate; +} + +static void iis2_clkcon_audio(struct s5l8740_iis2 *iis2, u32 val) +{ + if (!iis2 || !iis2->clkcon) + return; + writel(val, iis2->clkcon + CLKCON_AUDIO_OFF); +} + +static void iis2_clkcon_fm_gate(struct s5l8740_iis2 *iis2, bool on) +{ + u32 cur; + + if (!iis2 || !iis2->clkcon) + return; + cur = readl(iis2->clkcon + CLKCON_FM_GATE_OFF); + if (on) { + if (!iis2->clkcon10_held) { + iis2->clkcon10_saved = cur; + iis2->clkcon10_held = true; + } + writel(CLKCON_FM_GATE_ON, iis2->clkcon + CLKCON_FM_GATE_OFF); + } else if (iis2->clkcon10_held) { + writel(iis2->clkcon10_saved ? + iis2->clkcon10_saved : CLKCON_FM_GATE_OFF_VAL, + iis2->clkcon + CLKCON_FM_GATE_OFF); + iis2->clkcon10_held = false; + } +} + +/* + * Program IIS2 RX from fm-playing dump. Peri 13 DMA must be armed by + * dmaengine before RXCOM |= 0x6 (same kick model as IIS0 TXCOM). + */ +static void iis2_program_rx(struct s5l8740_iis2 *iis2) +{ + u32 div; + + iis2_clkcon_fm_gate(iis2, true); + iis2_clkcon_audio(iis2, CLKCON_AUDIO_PLAY); + writel(IIS2_CLKCON_ON, iis2->base + I2SCLKCON); + writel(IIS2_TXCON_FM, iis2->base + I2STXCON); + writel(IIS2_RXCON_FM, iis2->base + I2SRXCON); + div = iis2_pick_clkdiv(iis2->rate); + writel(div, iis2->base + I2SCLKDIV); + writel(IIS2_REG44_ORACLE, iis2->base + I2SREG44); +} + +static void iis2_rx_kick(struct s5l8740_iis2 *iis2) +{ + writel(IIS2_RXCOM_DMA, iis2->base + I2SRXCOM); +} + +static void iis2_hw_stop(struct s5l8740_iis2 *iis2) +{ + if (!iis2 || !iis2->base) + return; + writel(IIS2_RXCOM_IDLE, iis2->base + I2SRXCOM); + iis2_clkcon_audio(iis2, CLKCON_AUDIO_IDLE); + iis2_clkcon_fm_gate(iis2, false); +} + +static int s5l8740_iis2_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + iis2->rate = params_rate(params); + iis2_program_rx(iis2); + dev_info(dai->dev, + "IIS2 hw_params rate=%u ch=%u clkdiv=0x%x reg44=0x%x status=0x%x\n", + iis2->rate, params_channels(params), + readl(iis2->base + I2SCLKDIV), readl(iis2->base + I2SREG44), + readl(iis2->base + I2SSTATUS)); + return 0; +} + +static int s5l8740_iis2_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + iis2_program_rx(iis2); + iis2_rx_kick(iis2); + dev_info(dai->dev, "IIS2 capture start rxcom=0x%x status=0x%x\n", + readl(iis2->base + I2SRXCOM), + readl(iis2->base + I2SSTATUS)); + return 0; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + iis2_hw_stop(iis2); + return 0; + default: + return -EINVAL; + } +} + +static int s5l8740_iis2_dai_probe(struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (iis2->has_dma) + snd_soc_dai_init_dma_data(dai, NULL, &iis2->cap_dma); + return 0; +} + +static const struct snd_soc_dai_ops s5l8740_iis2_dai_ops = { + .probe = s5l8740_iis2_dai_probe, + .hw_params = s5l8740_iis2_hw_params, + .trigger = s5l8740_iis2_trigger, +}; + +static struct snd_soc_dai_driver s5l8740_iis2_dai = { + .name = "bcm2078-pcm", + .capture = { + .stream_name = "BCM2078 PCM Capture", + .channels_min = 1, + .channels_max = 2, + .rates = S5L8740_IIS2_RATES, + .formats = S5L8740_IIS2_FORMATS, + }, + .ops = &s5l8740_iis2_dai_ops, +}; + +static const struct snd_soc_component_driver s5l8740_iis2_component = { + .name = "bcm2078-pcm", + .legacy_dai_naming = 1, +}; + +static ssize_t regs_show(struct device *dev, struct device_attribute *a, + char *buf) { struct s5l8740_iis2 *iis2 = dev_get_drvdata(dev); unsigned int i; @@ -27,24 +255,22 @@ static ssize_t regs_show(struct device *dev, struct device_attribute *a, char *b if (!iis2 || !iis2->base) return sysfs_emit(buf, "not mapped\n"); - for (i = 0; i < IIS2_MMIO_LEN; i += 4) { + for (i = 0; i < IIS2_REGS_LEN; i += 4) { n += sysfs_emit_at(buf, n, "%02x: %08x\n", i, readl(iis2->base + i)); if (n >= PAGE_SIZE - 32) break; } + if (iis2->clkcon) { + n += sysfs_emit_at(buf, n, "clk+10: %08x\n", + readl(iis2->clkcon + CLKCON_FM_GATE_OFF)); + n += sysfs_emit_at(buf, n, "clk+30: %08x\n", + readl(iis2->clkcon + CLKCON_AUDIO_OFF)); + } return n; } static DEVICE_ATTR_RO(regs); -static struct attribute *iis2_attrs[] = { - &dev_attr_regs.attr, - NULL, -}; -static const struct attribute_group iis2_attr_group = { - .attrs = iis2_attrs, -}; - static int s5l8740_iis2_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -55,24 +281,56 @@ static int s5l8740_iis2_probe(struct platform_device *pdev) iis2 = devm_kzalloc(dev, sizeof(*iis2), GFP_KERNEL); if (!iis2) return -ENOMEM; + iis2->dev = dev; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); iis2->base = devm_ioremap_resource(dev, res); if (IS_ERR(iis2->base)) return PTR_ERR(iis2->base); + iis2->clkcon = devm_ioremap(dev, CLKCON_PHYS, 0x80); + ret = devm_clk_bulk_get_all(dev, &iis2->clks); if (ret > 0) { iis2->num_clks = ret; - clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); + ret = clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); + if (ret) + dev_warn(dev, "clk_bulk: %d\n", ret); } - ret = sysfs_create_group(&dev->kobj, &iis2_attr_group); - if (ret) - dev_warn(dev, "sysfs: %d\n", ret); + if (res) { + iis2->cap_dma.addr = res->start + I2SRXFIFO; + iis2->cap_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; + iis2->cap_dma.maxburst = 1; + } + platform_set_drvdata(pdev, iis2); dev_set_drvdata(dev, iis2); - dev_info(dev, "IIS2 FM hook @%pR — regs sysfs; capture PCM OPEN\n", res); + + if (of_property_present(dev->of_node, "dmas")) { + ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); + if (ret) { + dev_err(dev, "dmaengine_pcm: %d\n", ret); + return ret; + } + iis2->has_dma = true; + } else { + dev_err(dev, "missing dmas (need peri 13 rx)\n"); + return -EINVAL; + } + + ret = devm_snd_soc_register_component(dev, &s5l8740_iis2_component, + &s5l8740_iis2_dai, 1); + if (ret) + return ret; + + ret = device_create_file(dev, &dev_attr_regs); + if (ret) + dev_warn(dev, "regs sysfs: %d\n", ret); + + dev_info(dev, + "BCM2078 PCM RX @%pR peri13 FIFO@+0x38 (IIS2; FM/A2DP PCM in)\n", + res); return 0; } @@ -80,12 +338,14 @@ static void s5l8740_iis2_remove(struct platform_device *pdev) { struct s5l8740_iis2 *iis2 = platform_get_drvdata(pdev); - sysfs_remove_group(&pdev->dev.kobj, &iis2_attr_group); + device_remove_file(&pdev->dev, &dev_attr_regs); + iis2_hw_stop(iis2); if (iis2 && iis2->num_clks) clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); } static const struct of_device_id s5l8740_iis2_of_match[] = { + { .compatible = "apple,s5l8740-bcm2078-pcm" }, { .compatible = "apple,s5l8740-iis2" }, { } }; @@ -101,5 +361,6 @@ static struct platform_driver s5l8740_iis2_driver = { }; module_platform_driver(s5l8740_iis2_driver); -MODULE_DESCRIPTION("S5L8740 IIS2 FM MMIO hook (N31)"); +MODULE_DESCRIPTION("S5L8740 BCM2078 PCM capture DAI (IIS2 @0x3D400000, peri 13 RX)"); MODULE_LICENSE("GPL"); +MODULE_SOFTDEP("pre: dma_s5l8740_pl080"); diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h index bb5e25e3b18b8e..4faeaacff0ab44 100755 --- a/drivers/misc/whimory-s5l8740.h +++ b/drivers/misc/whimory-s5l8740.h @@ -77,10 +77,14 @@ struct whimory_fpart { #define WHIMORY_SB_OPEN 2 #define WHIMORY_SB_CXT 7 /* s_cxt_diff.c type 7 */ +#define WHIMORY_SB_UNKNOWN 3 + #define WHIMORY_CXT_MAX_SB 32 +#define WHIMORY_CXT_TAG_BASE 1 #define WHIMORY_CXT_TAG_STATS 2 #define WHIMORY_CXT_TAG_L2V 4 #define WHIMORY_CXT_TAG_END 255 +#define WHIMORY_CXT_TAG_CLEAN 0xff #define WHIMORY_CXT_CONTIG_SPAN 0xfffffff0u #define WHIMORY_FIL_META_BYTES 16 /* FIL GetInfo(105); sub_12ED9C */ @@ -180,6 +184,7 @@ struct whimory_range { u32 start; u32 len; u32 vba; + u64 weave; }; struct whimory_vfl { @@ -259,6 +264,8 @@ struct whimory_sftl { u32 l2v_repack_roots; u32 meta0_hits; u32 btoc_dumps_left; + u32 unknown_sbs; + u64 claim_weave; }; struct whimory_cxt_base { diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c index 3014bc5583744e..c4059abb3dd7f9 100755 --- a/sound/soc/apple/cs42l81-spi.c +++ b/sound/soc/apple/cs42l81-spi.c @@ -1523,6 +1523,24 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) return -ENODEV; } + { + int (*rails)(void); + + rails = (int (*)(void))__symbol_get("d1830_audio_rails"); + if (rails) { + int rr = rails(); + + __symbol_put("d1830_audio_rails"); + if (rr) + dev_warn(&c->spi->dev, + "d1830_audio_rails ret=%d (continuing)\n", + rr); + } else { + dev_warn(&c->spi->dev, + "d1830 unbound — sibling LDOs 21-23 not trimmed\n"); + } + } + mikey_jack = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); if (mikey_jack) { jack = mikey_jack(); @@ -1537,6 +1555,20 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) else dev_info(&c->spi->dev, "MikeyBus jack present\n"); + { + void (*ts_path)(struct device *); + + ts_path = (void (*)(struct device *)) + __symbol_get("apple_tristar_log_audio_path"); + if (ts_path) { + ts_path(&c->spi->dev); + __symbol_put("apple_tristar_log_audio_path"); + } else { + dev_info(&c->spi->dev, + "tristar unbound — 3.5mm path is CS42+Mikey, not Lightning Dx\n"); + } + } + ret = cs42l81_state_3_headset_detect(c); if (ret) return ret; From 5c0bf2e1117ef7437efe555909136785b99e8bac Mon Sep 17 00:00:00 2001 From: andrew867 Date: Thu, 27 Aug 2026 12:39:20 -0230 Subject: [PATCH 18/31] N31: CS-map FTL recover + nand rename, MikeyBus/nimbus/DTS sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace retired fmss monolith with nand-s5l8740 FIL and multi-object ftl (core/csmap/vecmap) including CXT→BTOC→L2V recover on CS META. Sync MikeyBus, Tristar, Nimbus, and N31 DTS from live glass bring-up. --- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 35 +- drivers/input/touchscreen/apple-nimbus.c | 12 +- drivers/misc/Makefile | 5 +- drivers/misc/apple-mikeybus.c | 671 +++-- drivers/misc/apple-tristar-cbtl1609.c | 6 +- drivers/misc/fmss-s5l8740-api.h | 63 - .../{ftl-s5l8740.c => ftl-s5l8740-core.c} | 643 +++-- drivers/misc/ftl-s5l8740-csmap.c | 2393 +++++++++++++++++ drivers/misc/ftl-s5l8740-csmap.h | 49 + drivers/misc/ftl-s5l8740-vecmap.c | 321 +++ drivers/misc/ftl-s5l8740-vecmap.h | 99 + .../{fmss-seq-read.h => nand-s5l8740-seq.h} | 0 .../misc/{fmss-s5l8740.c => nand-s5l8740.c} | 1574 ++++++++--- drivers/misc/nand-s5l8740.h | 152 ++ drivers/misc/whimory-s5l8740.h | 13 +- 15 files changed, 5236 insertions(+), 800 deletions(-) delete mode 100755 drivers/misc/fmss-s5l8740-api.h rename drivers/misc/{ftl-s5l8740.c => ftl-s5l8740-core.c} (88%) create mode 100755 drivers/misc/ftl-s5l8740-csmap.c create mode 100755 drivers/misc/ftl-s5l8740-csmap.h create mode 100755 drivers/misc/ftl-s5l8740-vecmap.c create mode 100755 drivers/misc/ftl-s5l8740-vecmap.h rename drivers/misc/{fmss-seq-read.h => nand-s5l8740-seq.h} (100%) rename drivers/misc/{fmss-s5l8740.c => nand-s5l8740.c} (79%) create mode 100755 drivers/misc/nand-s5l8740.h diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 0181b7da6cf0b0..28d69fa70a415c 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -175,10 +175,9 @@ interrupt-parent = <&vic0>; interrupts = <26>; /* - * MikeyBus (GPIO 66/67). uart3 stays first (samsung port 0). - * Do NOT serdev_device_open() at probe. Do NOT live-add s5l-uart - * (pinmux-then-probe locked glass 2026-08-27). uart_open sysfs - * only for remote RX after boot. + * MikeyBus on UART2. GPIO 66/67 = pad mux ONLY (not DIN detect). + * Resistor/model = OSOS cmd 3/0x8D/ch3 (backend TBD). + * force_plugged until resistor backend; RX = raw+osos rings. */ status = "okay"; @@ -282,20 +281,12 @@ clock-frequency = <100000>; /* UPDATE ME */ interrupt-parent = <&vic0>; interrupts = <21>; - status = "okay"; - /* - * CBTL1609A1 — public 0x34/0x35 is 8-bit; Linux 7-bit is 0x1a. - * RetailOS writes no Dx mux map. U-Boot DFU already routes - * Lightning USB; do not invent apple,init-sequence. Probe - * skips I2C ACK (reads still return the address byte). + * Glass 2026-08-27: IIC0 emcore-TX never completes (VIC21 irq + * count stays 0). Keep the controller for RE; no children until + * pinmux/clock path is proven. */ - tristar: lightning-mux@1a { - compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", - "apple,n31-tristar"; - reg = <0x1a>; - status = "okay"; - }; + status = "okay"; }; i2c1: i2c@3c900000 { @@ -308,6 +299,18 @@ interrupts = <22>; status = "okay"; + /* + * CBTL1609A1 — public 0x34/0x35 is 8-bit; Linux 7-bit is 0x1a. + * Glass: lives on IIC1 (3c900000), NOT IIC0. Non-flat dump with + * IDBUS 0x75 proven via i2cdump. RetailOS writes no Dx mux map. + */ + tristar: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", + "apple,n31-tristar"; + reg = <0x1a>; + status = "okay"; + }; + lis3dc: lis331dlh@18 { compatible = "st,lis3lv02d"; /* Linux DT 7-bit. Wire 8-bit is 0x30 write / 0x31 read. */ diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c index e2f85488a29fc6..cda75496962dc7 100755 --- a/drivers/input/touchscreen/apple-nimbus.c +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -103,7 +103,7 @@ #define NIMBUS_A34_BASE 0x2202fe00UL /* sub_A34(idx) = 0x2202FE00+idx */ #define NIMBUS_A34_ISYS_DESC (NIMBUS_A34_BASE + 0x18) /* sub_A34(24) */ -/* Whimory FTL (fmss-s5l8740.ko) — optional cal/FW from device NAND */ +/* Whimory FTL (nand-s5l8740.ko) — optional cal/FW from device NAND */ #define NIMBUS_FTL_SECTOR_SIZE 4096U #define NIMBUS_GPFW_TAG 0x67706677u /* 'gpfw' LE */ @@ -230,9 +230,9 @@ static unsigned int exec_word1 = 0x00000100; module_param(exec_word1, uint, 0644); MODULE_PARM_DESC(exec_word1, "2D54C EXEC word1 (OSOS 0x00000100)"); -/* fmss-s5l8740.ko exports (optional link). */ -bool fmss_ftl_present(void); -int fmss_ftl_read_sector(u64 logical_sector, void *buf); +/* nand-s5l8740.ko exports (optional link). */ +bool nand_ftl_present(void); +int nand_ftl_read_sector(u64 logical_sector, void *buf); static bool nimbus_verbose = true; @@ -1193,8 +1193,8 @@ static void nimbus_ftl_init_once(void) if (nimbus_ftl_inited) return; nimbus_ftl_inited = true; - nimbus_ftl_present_fn = symbol_get(fmss_ftl_present); - nimbus_ftl_read_fn = symbol_get(fmss_ftl_read_sector); + nimbus_ftl_present_fn = symbol_get(nand_ftl_present); + nimbus_ftl_read_fn = symbol_get(nand_ftl_read_sector); } static bool nimbus_ftl_ready(void) diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile index e554cd659478cd..f9162ce4841ac6 100644 --- a/drivers/misc/Makefile +++ b/drivers/misc/Makefile @@ -1,7 +1,6 @@ # SPDX-License-Identifier: GPL-2.0 # # Makefile for misc devices that really don't fit anywhere else. -# obj-$(CONFIG_IBM_ASM) += ibmasm/ obj-$(CONFIG_IBMVMC) += ibmvmc.o @@ -78,5 +77,7 @@ obj-y += keba/ obj-$(CONFIG_APPLE_MIKEYBUS) += apple-mikeybus.o obj-$(CONFIG_APPLE_TRISTAR_CBTL1609) += apple-tristar-cbtl1609.o obj-$(CONFIG_S5L8740_IIS2_MMIO) += s5l8740-iis2-mmio.o -obj-$(CONFIG_FMSS_S5L8740) += fmss-s5l8740.o obj-$(CONFIG_FTL_S5L8740) += ftl-s5l8740.o +obj-m += nand-s5l8740.o +obj-m += ftl-s5l8740.o +ftl-s5l8740-y := ftl-s5l8740-core.o ftl-s5l8740-csmap.o ftl-s5l8740-vecmap.o diff --git a/drivers/misc/apple-mikeybus.c b/drivers/misc/apple-mikeybus.c index 20f7d69d0d9153..09929b3a02bf70 100755 --- a/drivers/misc/apple-mikeybus.c +++ b/drivers/misc/apple-mikeybus.c @@ -1,23 +1,26 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Apple MikeyBus — N31 headset jack model / remote (UART2 @ 0x3DC00000) + * Apple MikeyBus — N31 headset / remote (UART2 @ 0x3DC00000) * - * RetailOS (osos 1.0.2): - * Tasks: CMikeyBusUartReadTask / CMikeyBusUartResistorTask (sub_35A4) - * UART open pinmux: sub_5714EE case 2 → GPIOCMD(0x42,2) + (0x43,2) - * = GPIO 66/67 func mode 2, then sub_428F70(0x42,1) - * Model table: sub_DCEC / mikeyTask.cpp — MEMORY[0x8925CD3] - * 1=A18 … 0xB=open circuit (unplugged) … 0x10=B187 - * headsetHasMikey: sub_40BE5C + * OSOS decomp (finish-line — grounded): + * Read open: sub_570BA8 → cmd 9/0x71/channel4 (bit4 @ 0x8A9239C) + * RX producer: sub_500ECC — packet type 0x70 appends to 1024B ring + * (@0x8AE5298, index @0x8AE5294, wrap &0x3FF) + * ReadTask: sub_2542F0 drains ring via sub_570C1C/sub_150A38; + * on byte 0xAA appends synthetic 0x01 (no button decode) + * Resistor: sub_410DB0 — cmd 3/0x8D/channel3; wait timeout 100; + * result @0x8A92444 (sample 15→100 if flag clear); + * NOT GPIO66/67 DIN polling; NOT raw UART decode + * ResistorTask: 0x00254382 — measure → sub_41F0D8(0x80, sample, 1) + * Status pkts: 0x76 / 0x8A → v=pkt[3]; bit4→0, bit5→128 * - * This is jack *identity* + remote (resistor/UART), not Tristar Lightning mux - * and not the CS42 HP amp itself. RetailOS HP mute is CS42 0x527; mixer - * bring-up sub_570620 is gated on headset state 0x8925CF4==1 — Linux CS42 - * audio_on already applies the HP sequence, but jack model must still be - * tracked so we do not treat open-circuit as headphones. + * Linux layers: + * 1) RX byte trace — raw + osos-shaped (0xAA→+0x01); no button decode + * 2) Resistor/model — force_plugged default; measure = -EOPNOTSUPP + * until command backend mapped; never flap jack on unknown + * 3) Event/jack — ALSA/export may use force_plugged / force_model only * - * Baud / byte protocol: OPEN until accessory MMIO snap. Default trial - * 115200 8N1 (family heuristic). force_model sysfs for glass bring-up. + * GPIO 66/67 = UART2 pad mux only (sub_5714EE case 2). Not resistor detect. */ #include #include @@ -30,33 +33,57 @@ #include #include -#define MIKEY_UART_PHYS 0x3dc00000ul -#define MIKEY_UART_LEN 0x3c #define GPIO_PHYS 0x3cf00000ul #define GPIOCMD_PHYS 0x3cf001e0ul -/* sub_5714EE case 2 */ -#define MIKEY_GPIO_TX 0x42u /* 66 */ -#define MIKEY_GPIO_RX 0x43u /* 67 */ +/* UART2 pad mux only — NOT DIN / resistor detect. */ +#define MIKEY_GPIO_TX 0x42u /* 66 — UART TX mux */ +#define MIKEY_GPIO_RX 0x43u /* 67 — UART RX mux */ #define MIKEY_MODEL_OPEN 0x0Bu -#define MIKEY_MODEL_A18 0x01u /* passive HP family */ +#define MIKEY_MODEL_A18 0x01u -/* Glass: analog HP is in. Resistor protocol is still OPEN. */ +/* OSOS RX ring is 1024 bytes (index wrap & 0x3FF). */ +#define MIKEY_RX_RING_SIZE 1024 + +/* + * force_plugged until command 3/0x8D resistor backend is mapped. + * Do not invent DIN thresholds; do not treat missing RX as unplug. + */ static bool force_plugged_param = true; module_param_named(force_plugged, force_plugged_param, bool, 0644); MODULE_PARM_DESC(force_plugged, - "Treat jack as plugged until resistor task works (default 1)"); + "Force jack present until resistor cmd backend (default 1)"); + +static u8 force_model_param = MIKEY_MODEL_A18; +module_param_named(force_model, force_model_param, byte, 0644); +MODULE_PARM_DESC(force_model, + "Model reported under force_plugged (default 0x01 A18)"); + +static bool uart_auto_open = true; +module_param(uart_auto_open, bool, 0644); +MODULE_PARM_DESC(uart_auto_open, + "Open UART2 for raw RX trace (default 1)"); /* - * Live s5l-uart instantiate (pinmux then platform_device_add) locked - * glass 2026-08-27 — CPU died during samsung probe. UART2 is enabled - * from DT at boot (uart3 remains first). This param is ignored. + * ResistorTask loop period placeholder. OSOS: wait timeout 100, default + * sample 100, fail-path delay 10 — none proven as Linux ms period. */ +static unsigned int resistor_period_ms = 100; +module_param(resistor_period_ms, uint, 0644); +MODULE_PARM_DESC(resistor_period_ms, + "Resistor worker period (NOT proven ms; 0=off)"); + static bool instantiate_uart2; module_param(instantiate_uart2, bool, 0444); MODULE_PARM_DESC(instantiate_uart2, - "ignored; live s5l-uart add locked glass — use DT uart2 okay"); + "ignored; use DT uart2 okay"); + +struct mikey_rx_ring { + u8 buf[MIKEY_RX_RING_SIZE]; + unsigned int head; /* next write */ + unsigned int count; +}; struct apple_mikeybus { struct device *dev; @@ -64,14 +91,36 @@ struct apple_mikeybus { void __iomem *gpio; void __iomem *gpiocmd; struct mutex lock; - u8 model; /* 0x8925CD3 mirror */ - bool force_plugged; /* glass: ignore open-circuit until resistor RE */ + + u8 model; + u8 force_model; + bool force_plugged; bool pinmux_on; u32 baud; + + /* Layer 1: dual RX rings (Linux shadows of OSOS ring). */ + struct mikey_rx_ring raw_rx; /* exact serdev bytes */ + struct mikey_rx_ring osos_rx; /* ReadTask-shaped (+0x01 after 0xAA) */ u32 rx_bytes; u8 rx_last[64]; unsigned int rx_last_len; + + /* + * Linux shadows of OSOS globals (NOT literal addresses): + * rx_status ↔ 0x892A2C8-ish status from 0x76/0x8A + * channel_mask ↔ 0x8A9239C feature bits (4=read, 3=resistor) + * model_sample ↔ last sub_410DB0 sample (or forced) + */ + u8 rx_status; /* 0 / 128 / unchanged */ + u32 channel_mask_shadow; /* bits we "would" enable */ + u8 model_sample; + bool resistor_backend_ready; /* false until cmd 3/0x8D mapped */ + bool uart_opened; + struct delayed_work uart_open_work; + struct delayed_work resistor_work; + bool resistor_active; + u32 resistor_ticks; }; static struct apple_mikeybus *mikeybus_singleton; @@ -80,7 +129,6 @@ static struct platform_device *mikey_plat_pdev; static void mikey_ensure_plat(struct work_struct *work); static DECLARE_WORK(mikey_plat_work, mikey_ensure_plat); -/* sub_DCEC name table (non-LVTM branch). */ static const char *mikey_model_name(u8 model) { switch (model) { @@ -103,21 +151,11 @@ static const char *mikey_model_name(u8 model) } } -/* sub_40BE5C — models expected to speak Mikey UART remote. */ static bool mikey_headset_has_remote(u8 model) { switch (model) { - case 2: - case 4: - case 5: - case 6: - case 7: - case 8: - case 9: - case 0xA: - case 0xD: - case 0xE: - case 0x10: + case 2: case 4: case 5: case 6: case 7: case 8: case 9: + case 0xA: case 0xD: case 0xE: case 0x10: return true; default: return false; @@ -128,15 +166,15 @@ static bool mikey_headset_ready_locked(struct apple_mikeybus *m) { if (m->force_plugged) return true; - /* - * 0xB is RetailOS "open circuit" only after the resistor task. - * Unmeasured model 0 is not unplugged — analog HP may already be in. - */ if (m->model == MIKEY_MODEL_OPEN) return false; return true; } +/* + * Jack present: force_plugged wins. Unknown / open must NOT flap to + * unplugged (false PLUG lesson). Only clear when measure proves open. + */ static bool mikey_jack_present_locked(struct apple_mikeybus *m) { if (m->force_plugged) @@ -146,10 +184,6 @@ static bool mikey_jack_present_locked(struct apple_mikeybus *m) return true; } -/** - * apple_mikeybus_jack_present - headphones / headset tip present - * Return: 1 present, 0 open circuit / unknown, -ENODEV if no driver - */ int apple_mikeybus_jack_present(void) { int ret; @@ -167,10 +201,6 @@ int apple_mikeybus_jack_present(void) } EXPORT_SYMBOL_GPL(apple_mikeybus_jack_present); -/** - * apple_mikeybus_headset_ready - RetailOS 0x8925CF4 gate for sub_570620 - * Return: 1 ready, 0 not ready, -ENODEV if no driver - */ int apple_mikeybus_headset_ready(void) { int ret; @@ -188,6 +218,39 @@ int apple_mikeybus_headset_ready(void) } EXPORT_SYMBOL_GPL(apple_mikeybus_headset_ready); +static void mikey_ring_put(struct mikey_rx_ring *r, u8 b) +{ + r->buf[r->head] = b; + r->head = (r->head + 1) & (MIKEY_RX_RING_SIZE - 1); + if (r->count < MIKEY_RX_RING_SIZE) + r->count++; +} + +static void mikey_ring_reset(struct mikey_rx_ring *r) +{ + r->head = 0; + r->count = 0; +} + +/* + * Snapshot newest bytes into @dst (up to @max), oldest→newest order among + * the retained window. + */ +static unsigned int mikey_ring_snapshot(const struct mikey_rx_ring *r, + u8 *dst, unsigned int max) +{ + unsigned int n, i, start; + + n = min(r->count, max); + if (!n) + return 0; + start = (r->head - n) & (MIKEY_RX_RING_SIZE - 1); + for (i = 0; i < n; i++) + dst[i] = r->buf[(start + i) & (MIKEY_RX_RING_SIZE - 1)]; + return n; +} + +/* UART pad mux only — never used as DIN sample / resistor path. */ static void mikey_gpiocmd(struct apple_mikeybus *m, u8 gpio, u8 mode) { u32 bank = gpio >> 3; @@ -196,7 +259,6 @@ static void mikey_gpiocmd(struct apple_mikeybus *m, u8 gpio, u8 mode) writel((bank << 16) | (pin << 8) | mode, m->gpiocmd); } -/* sub_5714EE(UART2): mode 2 on 66/67. Close path uses mode 0xFFFE (65534). */ static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) { u32 bank, pin, dir; @@ -206,7 +268,6 @@ static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) return; if (on) { - /* mode 2 → DIR out + GPIOCMD mode byte (sub_43D38C) */ bank = MIKEY_GPIO_TX >> 3; pin = MIKEY_GPIO_TX & 7; b = m->gpio + 32 * bank; @@ -222,7 +283,6 @@ static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) mikey_gpiocmd(m, MIKEY_GPIO_RX, 2); m->pinmux_on = true; } else { - /* mode 0xFFFE: clear DIR, cmd 0 (sub_571374 close) */ bank = MIKEY_GPIO_TX >> 3; pin = MIKEY_GPIO_TX & 7; b = m->gpio + 32 * bank; @@ -241,19 +301,184 @@ static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) } /* - * When the running DTB still has uart2 disabled, serdev never probes. - * Bind a platform device so headset_ready() is 1 (force_plugged) instead - * of -ENODEV. Do NOT platform_device_add("s5l-uart") — that locked glass - * (samsung probe after GPIO 66/67 pinmux, 2026-08-27). UART2 itself is - * enabled from DT at boot, uart3 first. + * ReadTask-shaped append (sub_2542F0): put byte; if 0xAA also put 0x01. + * Raw ring keeps exact wire bytes separately. + */ +static void mikey_rx_append_byte(struct apple_mikeybus *m, u8 b) +{ + mikey_ring_put(&m->raw_rx, b); + mikey_ring_put(&m->osos_rx, b); + if (b == 0xaa) + mikey_ring_put(&m->osos_rx, 0x01); +} + +/* + * Lower-packet dispatcher (sub_500ECC shape). Only call with proven + * packet-framed envelopes — never feed raw serdev bytes here. + */ +static void mikey_lower_packet_rx(struct apple_mikeybus *m, + const u8 *pkt, size_t len) +{ + u8 type, v; + size_t i, count; + + if (len < 3) + return; + + type = pkt[1]; + switch (type) { + case 0x70: + /* payload = packet+3; count = packet[0]-3 (OSOS). */ + count = pkt[0]; + if (count < 3 || count > len) + count = len; + count -= 3; + for (i = 0; i < count; i++) + mikey_rx_append_byte(m, pkt[3 + i]); + break; + case 0x76: + case 0x8a: + /* sub_18911C / sub_182AFC status shadow. */ + v = pkt[3]; + if (v & 0x10) + m->rx_status = 0; + else if (v & 0x20) + m->rx_status = 128; + break; + default: + break; + } +} + +/* + * Command-backend resistor measure (sub_410DB0). Not implemented until + * channel3 / cmd 3/0x8D / wait@0x8A92448 are mapped to Linux. */ +static int mikey_measure_model(struct apple_mikeybus *m, u8 *sample) +{ + (void)m; + (void)sample; + return -EOPNOTSUPP; +} + +static int mikey_uart_open_locked(struct apple_mikeybus *m) +{ + int ret; + + if (m->uart_opened) + return 0; + if (!m->serdev) + return -ENODEV; + ret = serdev_device_open(m->serdev); + if (ret) + return ret; + serdev_device_set_baudrate(m->serdev, m->baud); + serdev_device_set_flow_control(m->serdev, false); + m->uart_opened = true; + /* Shadow: Read open enables channel bit 4. */ + m->channel_mask_shadow |= BIT(4); + dev_info(m->dev, + "Mikey UART opened baud=%u (raw RX trace; no button decode; " + "channel4 shadow set)\n", + m->baud); + return 0; +} + +static void mikey_uart_close_locked(struct apple_mikeybus *m) +{ + if (!m->uart_opened || !m->serdev) + return; + serdev_device_close(m->serdev); + m->uart_opened = false; + m->channel_mask_shadow &= ~BIT(4); + dev_info(m->dev, "Mikey UART closed\n"); +} + +static void mikey_uart_open_workfn(struct work_struct *work) +{ + struct apple_mikeybus *m = + container_of(work, struct apple_mikeybus, uart_open_work.work); + int ret; + + mutex_lock(&m->lock); + ret = mikey_uart_open_locked(m); + mutex_unlock(&m->lock); + if (ret) + dev_warn(m->dev, "Mikey UART auto-open failed: %d\n", ret); +} + +/* + * ResistorTask-shaped worker (0x00254382): + * force_plugged → keep force_model, stay plugged, return + * measure EOPNOTSUPP → unknown, do NOT flap jack + * on change → update model_sample / model (when backend ready) + */ +static void mikey_resistor_workfn(struct work_struct *work) +{ + struct apple_mikeybus *m = + container_of(work, struct apple_mikeybus, resistor_work.work); + u8 sample = 0; + int ret; + + if (!m->resistor_active || !resistor_period_ms) + return; + + mutex_lock(&m->lock); + m->resistor_ticks++; + + if (m->force_plugged) { + m->model = m->force_model ? m->force_model : MIKEY_MODEL_A18; + m->model_sample = m->model; + if (m->resistor_ticks == 1) + dev_info(m->dev, + "Mikey resistor: force_plugged model=0x%02x " + "(%s); backend_ready=%d\n", + m->model, mikey_model_name(m->model), + m->resistor_backend_ready); + goto resched; + } + + ret = mikey_measure_model(m, &sample); + if (ret == -EOPNOTSUPP) { + /* + * Backend not mapped. Report unknown sample shadow only; + * do not clear plugged / do not set OPEN from lack of RX. + */ + if (m->resistor_ticks == 1) + dev_info(m->dev, + "Mikey resistor: measure -EOPNOTSUPP " + "(cmd 3/0x8D/ch3 not mapped); jack unchanged\n"); + goto resched; + } + if (ret) { + /* OSOS fail path uses sample=100 then delay 10 — shadow only. */ + sample = 100; + m->model_sample = sample; + goto resched; + } + + m->channel_mask_shadow |= BIT(3); + if (sample != m->model_sample) { + m->model_sample = sample; + m->model = sample; + dev_info(m->dev, + "Mikey model_sample=%u (0x%02x %s) via measure\n", + sample, sample, mikey_model_name(sample)); + } + +resched: + mutex_unlock(&m->lock); + if (m->resistor_active && resistor_period_ms) + schedule_delayed_work(&m->resistor_work, + msecs_to_jiffies(resistor_period_ms)); +} + static void mikey_ensure_plat(struct work_struct *work) { struct device_node *uart_np, *mikey_np = NULL; int ret; (void)work; - if (mikeybus_singleton) return; @@ -277,8 +502,6 @@ static void mikey_ensure_plat(struct work_struct *work) pr_warn("apple-mikeybus: plat add %d\n", ret); platform_device_put(mikey_plat_pdev); mikey_plat_pdev = NULL; - } else { - pr_info("apple-mikeybus: platform bind (uart2 still DT-disabled)\n"); } out: if (mikey_np) @@ -287,16 +510,14 @@ static void mikey_ensure_plat(struct work_struct *work) of_node_put(uart_np); } +/* -------------------- sysfs (Linux shadows) -------------------- */ + static ssize_t model_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - u8 model; - mutex_lock(&m->lock); - model = m->model; - mutex_unlock(&m->lock); - return sysfs_emit(buf, "0x%02x %s\n", model, mikey_model_name(model)); + return sysfs_emit(buf, "0x%02x %s\n", m->model, mikey_model_name(m->model)); } static ssize_t model_store(struct device *dev, struct device_attribute *attr, @@ -311,15 +532,54 @@ static ssize_t model_store(struct device *dev, struct device_attribute *attr, return -EINVAL; mutex_lock(&m->lock); m->model = (u8)v; + m->model_sample = (u8)v; + dev_info(dev, "Mikey model set 0x%02x (%s) via sysfs\n", + m->model, mikey_model_name(m->model)); mutex_unlock(&m->lock); - dev_info(dev, "model set 0x%02x (%s) has_remote=%d plugged=%d\n", - m->model, mikey_model_name(m->model), - mikey_headset_has_remote(m->model), - mikey_jack_present_locked(m)); return count; } static DEVICE_ATTR_RW(model); +static ssize_t force_model_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "0x%02x %s\n", m->force_model, + mikey_model_name(m->force_model)); +} + +static ssize_t force_model_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int v; + int ret; + + ret = kstrtouint(buf, 0, &v); + if (ret || v > 0xff) + return -EINVAL; + mutex_lock(&m->lock); + m->force_model = (u8)v; + if (m->force_plugged) { + m->model = m->force_model; + m->model_sample = m->force_model; + } + mutex_unlock(&m->lock); + return count; +} +static DEVICE_ATTR_RW(force_model); + +static ssize_t model_sample_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", m->model_sample); +} +static DEVICE_ATTR_RO(model_sample); + static ssize_t plugged_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -354,80 +614,159 @@ static ssize_t force_plugged_store(struct device *dev, return ret; mutex_lock(&m->lock); m->force_plugged = !!v; - if (m->force_plugged && - (m->model == 0 || m->model == MIKEY_MODEL_OPEN)) - m->model = MIKEY_MODEL_A18; + if (m->force_plugged) { + m->model = m->force_model ? m->force_model : MIKEY_MODEL_A18; + m->model_sample = m->model; + } mutex_unlock(&m->lock); return count; } static DEVICE_ATTR_RW(force_plugged); -static ssize_t pinmux_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t baud_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - return sysfs_emit(buf, "%d\n", m->pinmux_on); + return sysfs_emit(buf, "%u\n", m->baud); } -static ssize_t pinmux_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t baud_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); unsigned int v; int ret; ret = kstrtouint(buf, 0, &v); - if (ret) - return ret; + if (ret || !v) + return -EINVAL; mutex_lock(&m->lock); - mikey_pinmux_uart(m, !!v); + m->baud = v; + if (m->serdev && m->uart_opened) + serdev_device_set_baudrate(m->serdev, v); mutex_unlock(&m->lock); return count; } -static DEVICE_ATTR_RW(pinmux); +static DEVICE_ATTR_RW(baud); -static ssize_t baud_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t rx_bytes_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - return sysfs_emit(buf, "%u\n", m->baud); + return sysfs_emit(buf, "%u\n", m->rx_bytes); } +static DEVICE_ATTR_RO(rx_bytes); -static ssize_t baud_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t rx_raw_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; - int ret; + u8 tmp[64]; + unsigned int n; + ssize_t out; - ret = kstrtouint(buf, 0, &v); - if (ret || !v) - return -EINVAL; mutex_lock(&m->lock); - m->baud = v; - if (m->serdev) - serdev_device_set_baudrate(m->serdev, v); + n = mikey_ring_snapshot(&m->raw_rx, tmp, sizeof(tmp)); + out = sysfs_emit(buf, "count=%u last=%*ph\n", m->raw_rx.count, n, tmp); mutex_unlock(&m->lock); - return count; + return out; } -static DEVICE_ATTR_RW(baud); +static DEVICE_ATTR_RO(rx_raw); -static ssize_t rx_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t rx_task_stream_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - ssize_t n; + u8 tmp[64]; + unsigned int n; + ssize_t out; mutex_lock(&m->lock); - n = sysfs_emit(buf, "bytes=%u last_len=%u last=%*ph\n", - m->rx_bytes, m->rx_last_len, - m->rx_last_len, m->rx_last); + n = mikey_ring_snapshot(&m->osos_rx, tmp, sizeof(tmp)); + out = sysfs_emit(buf, + "count=%u (ReadTask-shaped; 0xAA→+0x01) last=%*ph\n", + m->osos_rx.count, n, tmp); mutex_unlock(&m->lock); - return n; + return out; +} +static DEVICE_ATTR_RO(rx_task_stream); + +static ssize_t rx_status_892A2C8_shadow_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%u\n", m->rx_status); +} +static DEVICE_ATTR_RO(rx_status_892A2C8_shadow); + +static ssize_t decomp_channel_mask_shadow_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, + "0x%08x (bit3=resistor cmd shadow, bit4=read open)\n", + m->channel_mask_shadow); +} +static DEVICE_ATTR_RO(decomp_channel_mask_shadow); + +static ssize_t resistor_backend_ready_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + + return sysfs_emit(buf, "%d\n", m->resistor_backend_ready); +} +static DEVICE_ATTR_RO(resistor_backend_ready); + +/* + * Debug inject of a framed lower packet (hex bytes). Does NOT accept + * unframed serdev streams — operator must supply OSOS envelopes. + * Format: echo "len type ..." with decimal/hex tokens, e.g. + * echo "6 0x70 0 aa 01 02" > lower_packet_inject + * First token is OSOS packet[0] length field. + */ +static ssize_t lower_packet_inject_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 pkt[64]; + unsigned int vals[64]; + int n = 0, i; + const char *p = buf; + + while (n < 64 && *p) { + unsigned int v; + int matched; + + while (*p == ' ' || *p == '\t' || *p == '\n') + p++; + if (!*p) + break; + matched = sscanf(p, "%i%n", &v, &i); + if (matched < 1) + break; + vals[n++] = v & 0xff; + p += i; + } + if (n < 3) + return -EINVAL; + for (i = 0; i < n; i++) + pkt[i] = (u8)vals[i]; + + mutex_lock(&m->lock); + mikey_lower_packet_rx(m, pkt, n); + mutex_unlock(&m->lock); + return count; } -static DEVICE_ATTR_RO(rx); +static DEVICE_ATTR_WO(lower_packet_inject); static ssize_t uart_open_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -447,29 +786,15 @@ static ssize_t uart_open_store(struct device *dev, struct device_attribute *attr ret = kstrtouint(buf, 0, &v); if (ret) return ret; - mutex_lock(&m->lock); - if (v && !m->uart_opened) { - if (!m->serdev) { - mutex_unlock(&m->lock); - return -ENODEV; - } - ret = serdev_device_open(m->serdev); - if (ret) { - mutex_unlock(&m->lock); - return ret; - } - serdev_device_set_baudrate(m->serdev, m->baud); - serdev_device_set_flow_control(m->serdev, false); - m->uart_opened = true; - dev_info(dev, "Mikey UART opened baud=%u\n", m->baud); - } else if (!v && m->uart_opened) { - serdev_device_close(m->serdev); - m->uart_opened = false; - dev_info(dev, "Mikey UART closed\n"); + if (v) + ret = mikey_uart_open_locked(m); + else { + mikey_uart_close_locked(m); + ret = 0; } mutex_unlock(&m->lock); - return count; + return ret ? ret : count; } static DEVICE_ATTR_RW(uart_open); @@ -479,24 +804,40 @@ static ssize_t info_show(struct device *dev, struct device_attribute *attr, struct apple_mikeybus *m = dev_get_drvdata(dev); return sysfs_emit(buf, - "MikeyBus UART2 @0x3DC GPIO 66/67\n" - "model=0x%02x (%s) remote=%d plugged=%d force=%d\n" - "pinmux=%d baud=%u uart_open=%d rx_bytes=%u\n" - "protocol baud OPEN — resistor task RE pending\n", - m->model, mikey_model_name(m->model), + "MikeyBus UART2 @0x3DC (pads GPIO66/67 mux ONLY)\n" + "decomp: ReadTask drains 0x70 ring; resistor=cmd " + "3/0x8D/ch3 (NOT DIN poll)\n" + "model=0x%02x (%s) sample=%u remote=%d plugged=%d " + "force=%d force_model=0x%02x\n" + "pinmux=%d baud=%u uart_open=%d rx_bytes=%u " + "raw_ring=%u osos_ring=%u\n" + "rx_status_shadow=%u channel_mask_shadow=0x%x " + "resistor_backend_ready=%d ticks=%u\n" + "NO button decode; NO GPIO66/67 DIN detect claim\n", + m->model, mikey_model_name(m->model), m->model_sample, mikey_headset_has_remote(m->model), mikey_jack_present_locked(m), m->force_plugged, - m->pinmux_on, m->baud, m->uart_opened, m->rx_bytes); + m->force_model, m->pinmux_on, m->baud, m->uart_opened, + m->rx_bytes, m->raw_rx.count, m->osos_rx.count, + m->rx_status, m->channel_mask_shadow, + m->resistor_backend_ready, m->resistor_ticks); } static DEVICE_ATTR_RO(info); static struct attribute *mikey_attrs[] = { &dev_attr_model.attr, + &dev_attr_force_model.attr, + &dev_attr_model_sample.attr, &dev_attr_plugged.attr, &dev_attr_force_plugged.attr, - &dev_attr_pinmux.attr, &dev_attr_baud.attr, - &dev_attr_rx.attr, + &dev_attr_rx_bytes.attr, + &dev_attr_rx_raw.attr, + &dev_attr_rx_task_stream.attr, + &dev_attr_rx_status_892A2C8_shadow.attr, + &dev_attr_decomp_channel_mask_shadow.attr, + &dev_attr_resistor_backend_ready.attr, + &dev_attr_lower_packet_inject.attr, &dev_attr_uart_open.attr, &dev_attr_info.attr, NULL, @@ -507,17 +848,22 @@ static size_t mikey_serdev_receive(struct serdev_device *serdev, const u8 *data, size_t count) { struct apple_mikeybus *m = serdev_device_get_drvdata(serdev); - size_t n; + size_t i, n; if (!m || !count) return count; mutex_lock(&m->lock); m->rx_bytes += count; + for (i = 0; i < count; i++) + mikey_rx_append_byte(m, data[i]); n = min(count, sizeof(m->rx_last)); memcpy(m->rx_last, data + count - n, n); m->rx_last_len = n; - /* Protocol OPEN — log only until resistor/remote decode lands. */ + /* + * Raw serdev bytes → rings only. Do NOT run lower_packet_rx here + * until the wire stream is proven packet-framed. + */ dev_info(m->dev, "Mikey RX %zu: %*ph\n", count, (int)min(count, 16), data); mutex_unlock(&m->lock); @@ -542,20 +888,28 @@ static int mikey_bind(struct device *dev, struct serdev_device *serdev) m->serdev = serdev; m->baud = baud; m->model = 0; + m->force_model = force_model_param ? force_model_param : MIKEY_MODEL_A18; m->force_plugged = force_plugged_param || of_property_read_bool(dev->of_node, "apple,force-plugged"); + m->resistor_backend_ready = false; + m->channel_mask_shadow = 0; + m->rx_status = 0; if (dev->of_node && !of_property_read_u32(dev->of_node, "current-speed", &baud)) m->baud = baud; - if (m->force_plugged) - m->model = MIKEY_MODEL_A18; + if (m->force_plugged) { + m->model = m->force_model; + m->model_sample = m->force_model; + } mutex_init(&m->lock); + mikey_ring_reset(&m->raw_rx); + mikey_ring_reset(&m->osos_rx); + INIT_DELAYED_WORK(&m->uart_open_work, mikey_uart_open_workfn); + INIT_DELAYED_WORK(&m->resistor_work, mikey_resistor_workfn); m->gpio = devm_ioremap(dev, GPIO_PHYS, 0x200); m->gpiocmd = devm_ioremap(dev, GPIOCMD_PHYS, 4); - if (!m->gpio || !m->gpiocmd) - dev_warn(dev, "GPIO/GPIOCMD map failed — pinmux sysfs limited\n"); dev_set_drvdata(dev, m); if (serdev) { @@ -574,10 +928,21 @@ static int mikey_bind(struct device *dev, struct serdev_device *serdev) mutex_unlock(&mikeybus_singleton_lock); dev_info(dev, - "MikeyBus ready (%s) baud=%u model=0x%02x (%s) force_plugged=%d\n", - serdev ? "serdev, UART not opened" : "platform, uart2 bound", + "MikeyBus ready (%s) baud=%u model=0x%02x (%s) force_plugged=%d " + "(RX=raw+osos rings; resistor=EOPNOTSUPP; no DIN detect; " + "no button decode)\n", + serdev ? "serdev" : "platform", m->baud, m->model, mikey_model_name(m->model), m->force_plugged); + + if (serdev && uart_auto_open) + schedule_delayed_work(&m->uart_open_work, msecs_to_jiffies(50)); + + if (resistor_period_ms) { + m->resistor_active = true; + schedule_delayed_work(&m->resistor_work, + msecs_to_jiffies(resistor_period_ms)); + } return 0; } @@ -587,17 +952,21 @@ static void mikey_unbind(struct device *dev) if (!m) return; + + m->resistor_active = false; + cancel_delayed_work_sync(&m->resistor_work); + cancel_delayed_work_sync(&m->uart_open_work); + mutex_lock(&mikeybus_singleton_lock); if (mikeybus_singleton == m) mikeybus_singleton = NULL; mutex_unlock(&mikeybus_singleton_lock); sysfs_remove_groups(&dev->kobj, mikey_groups); + mutex_lock(&m->lock); + mikey_uart_close_locked(m); mikey_pinmux_uart(m, false); - if (m->uart_opened && m->serdev) { - serdev_device_close(m->serdev); - m->uart_opened = false; - } + mutex_unlock(&m->lock); } static int mikey_serdev_probe(struct serdev_device *serdev) @@ -657,7 +1026,7 @@ static int __init mikey_init(void) return ret; } if (instantiate_uart2) - pr_warn("apple-mikeybus: instantiate_uart2 ignored (live s5l-uart add locked glass)\n"); + pr_warn("apple-mikeybus: instantiate_uart2 ignored\n"); schedule_work(&mikey_plat_work); return 0; } @@ -676,6 +1045,6 @@ static void __exit mikey_exit(void) module_init(mikey_init); module_exit(mikey_exit); -MODULE_DESCRIPTION("Apple MikeyBus headset jack model/remote (N31 UART2)"); +MODULE_DESCRIPTION("Apple MikeyBus N31 (UART2 RX rings + force jack; resistor cmd TBD)"); MODULE_AUTHOR("FreeMyiPod"); MODULE_LICENSE("GPL"); diff --git a/drivers/misc/apple-tristar-cbtl1609.c b/drivers/misc/apple-tristar-cbtl1609.c index 12300bc11fab83..b42bd14302032a 100755 --- a/drivers/misc/apple-tristar-cbtl1609.c +++ b/drivers/misc/apple-tristar-cbtl1609.c @@ -2,8 +2,10 @@ /* * Apple Lightning Tristar mux — NXP CBTL1609A1 (iPod nano 7G / N31) * - * Transport (proven): I2C0 7-bit 0x1a. Public 0x34 write / 0x35 read is - * the 8-bit form of that address (nyansatan). + * Transport (proven on glass 2026-08-27): I2C1 7-bit 0x1a + * (controller 3c900000). I2C0/IIC0 does not complete emcore-TX here — + * do not bind Tristar there. Public 0x34 write / 0x35 read is the + * 8-bit form of that address (nyansatan). * * Routing is IDBUS inside the chip, not Linux Dx register writes. * RetailOS N31 RE observed zero Dx/mux I2C writes. The 0x75 accessory diff --git a/drivers/misc/fmss-s5l8740-api.h b/drivers/misc/fmss-s5l8740-api.h deleted file mode 100755 index 539e98c6fe9cb2..00000000000000 --- a/drivers/misc/fmss-s5l8740-api.h +++ /dev/null @@ -1,63 +0,0 @@ -/* SPDX-License-Identifier: GPL-2.0-only */ -/* - * S5L8740 FMSS FIL export — raw PPN page I/O for the Whimory stack. - * - * fmss-s5l8740.ko owns the controller. whimory / ftl-s5l8740.ko owns - * FPart, VFL, SFTL, L2V, and the block device. - * - * fmss_ftl_read_sector() is a compatibility hook for apple-nimbus.ko. - * After Whimory opens successfully it registers the real LBA reader. - */ -#ifndef FMSS_S5L8740_API_H -#define FMSS_S5L8740_API_H - -#include -#include - -#define FMSS_FTL_SECTOR_SIZE 4096U -#define FMSS_FTL_SECTORS_PER_LPN 4U -#define FMSS_FTL_DEFAULT_CAPACITY 3856968U - -#define S5L8740_FMSS_MAX_CE 2U -#define S5L8740_FMSS_MAX_CAU 2U -#define S5L8740_FMSS_PAGE_SIZE 16384U -#define S5L8740_FMSS_META_SIZE 64U /* 4 × 16-byte SFTL slots */ - -struct s5l8740_fmss_geom { - u32 num_ce; - u32 num_cau; - u32 blocks_per_cau; - u32 pages_per_block; - u32 pages_per_block_slc; - u32 page_size; - u32 vfl_tail; - u32 page_bits; - u32 block_bits; - u32 cau_bits; - u32 caus_per_channel; - u32 dev_id; /* FIL selector 101 analogue */ - u32 geom_104; /* FIL selector 104 analogue */ - u32 geom_105; /* FIL selector 105 analogue */ - u32 geom_135; /* FIL selector 135 analogue */ - bool from_param_page; -}; - -bool fmss_ftl_present(void); -struct device *fmss_ftl_device(void); -unsigned int fmss_ftl_lpn_count(void); -int fmss_ftl_build_map(unsigned int max_lpn); -int fmss_ftl_read_sector(u64 logical_sector, void *buf); - -u32 s5l8740_fmss_fil_get_info(u32 selector); -int s5l8740_fmss_available(void); -int s5l8740_fmss_hw_init(void); -int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g); -int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, - unsigned int block, unsigned int page, - unsigned int slc, unsigned int chunks, - void *data, size_t data_len, - void *meta, size_t meta_len); -int s5l8740_fmss_nand_reset(void); -void s5l8740_fmss_register_ftl_read(int (*fn)(u64 lba, void *buf)); - -#endif /* FMSS_S5L8740_API_H */ diff --git a/drivers/misc/ftl-s5l8740.c b/drivers/misc/ftl-s5l8740-core.c similarity index 88% rename from drivers/misc/ftl-s5l8740.c rename to drivers/misc/ftl-s5l8740-core.c index 50440bf167f03a..ccc6be53d3b4b5 100755 --- a/drivers/misc/ftl-s5l8740.c +++ b/drivers/misc/ftl-s5l8740-core.c @@ -1,18 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * S5L8740 Whimory PPN read-only block driver (N31). + * S5L8740 Whimory FTL — read-only block path (N31). * - * FIL (fmss-s5l8740.ko) - * → FPart ReadSpecial type 0xC101 len 0x600 (chunked object; xrmw at payload+0) - * → GetInfo(101) vs sig[+0x34] hard gate - * → VFL_Open (type 0x20 CXT, identity VBN, bank bitmap) - * → FTL_Open / s_boot: classify → s_cxt_load → BTOC/META - * → L2V_Search (sub_428694) - * → VFL read + s_read META check (sub_56C328) - * → /dev/s5l8740-ftl (4096-byte logical, read-only) + * Layers: NAND FIL → FPart → VFL → FTL/L2V → optional /dev/s5l8740-ftl. + * The CS-map front-end in ftl-s5l8740-csmap.c is the preferred RO disk path. + * This module retains the classic Whimory open/boot helpers. * - * The disk is registered only after read_lba_4k(0) returns a metadata- - * validated FAT32 boot sector. Empty L2V never yields a block device. + * The block device is registered only after FAT-critical validation succeeds. + * Empty or inconsistent maps never expose a disk. */ #include #include @@ -32,6 +27,7 @@ #include #include "whimory-s5l8740.h" +#include "ftl-s5l8740-csmap.h" #define FTL_DISK_NAME "s5l8740-ftl" #define FTL_IPOD_NAME "s5l8740-ipod" @@ -48,15 +44,15 @@ module_param(import_l2v_oracle, bool, 0644); MODULE_PARM_DESC(import_l2v_oracle, "Load L2V root/nodes/globals from /lib/firmware/apple/"); -static unsigned int scan_blocks; -module_param(scan_blocks, uint, 0644); -MODULE_PARM_DESC(scan_blocks, - "User blocks per CE/CAU to classify (0 = all user blocks)"); - -static unsigned int max_open_sbs; +static unsigned int max_open_sbs = 16; module_param(max_open_sbs, uint, 0644); MODULE_PARM_DESC(max_open_sbs, - "Max open superblocks to META-rebuild (0 = all)"); + "Max open superblocks to META-rebuild (0 = all; default 16)"); + +static unsigned int scan_blocks = 256; +module_param(scan_blocks, uint, 0644); +MODULE_PARM_DESC(scan_blocks, + "User blocks per CE/CAU to classify (0 = all; default 256)"); static unsigned int meta0_scan_sbs = 4; module_param(meta0_scan_sbs, uint, 0644); @@ -113,6 +109,74 @@ static u64 whimory_weave48(const u8 *m) ((u64)get_unaligned_le32(m + 4) << 16); } +/* + * CS span4/rec4112 page read — real 4× META (glass-proven). Used for + * classify / BTOC / open-SB / CXT / VBA reads when meta_dma_read=0. + */ +static int whimory_cs_read_page(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, void *data, size_t data_len, + void *meta, size_t meta_len) +{ + struct s5l8740_cs_page *csp; + unsigned int s; + int ret; + + if (!w || !data || data_len < S5L8740_NAND_PAGE_SIZE) + return -EINVAL; + csp = w->sftl.cs_page; + if (!csp) + return -ENOMEM; + + ret = s5l8740_nand_cs_phys_read((u8)ce, (u8)cau, (u16)block, (u8)page, + csp); + if (ret) + return ret; + + for (s = 0; s < S5L8740_NAND_SLOTS_PER_PAGE; s++) + memcpy((u8 *)data + s * S5L8740_NAND_SLOT_DATA, + csp->data[s], S5L8740_NAND_SLOT_DATA); + + if (meta && meta_len) { + size_t copy = min_t(size_t, meta_len, S5L8740_NAND_META_SIZE); + + memset(meta, 0xff, meta_len); + for (s = 0; s < S5L8740_NAND_SLOTS_PER_PAGE && + (s + 1) * WHIMORY_META_SIZE <= copy; s++) + memcpy((u8 *)meta + s * WHIMORY_META_SIZE, + csp->meta_raw[s], WHIMORY_META_SIZE); + } + return 0; +} + +static bool whimory_meta_any_btoc(const u8 *meta64) +{ + unsigned int s; + + if (!meta64) + return false; + for (s = 0; s < WHIMORY_VBAS_PER_PAGE; s++) { + if (meta64[s * WHIMORY_META_SIZE] == WHIMORY_META_TYPE_BTOC) + return true; + } + return false; +} + +static bool whimory_meta_slot0_or_any_cxt(const u8 *meta64) +{ + unsigned int s; + + if (!meta64) + return false; + if (meta64[0] == WHIMORY_META_TYPE_SFTL_CXT) + return true; + for (s = 1; s < WHIMORY_VBAS_PER_PAGE; s++) { + if (meta64[s * WHIMORY_META_SIZE] == WHIMORY_META_TYPE_SFTL_CXT) + return true; + } + return false; +} + static bool whimory_page_blank(const u8 *p, unsigned int n) { unsigned int i; @@ -171,12 +235,12 @@ static bool whimory_special_lba(u32 lba) static u32 whimory_vfl_phys(struct whimory *w, u32 cau, u32 virt) { /* - * sub_4EAE40: PBN = VBN (identity over blocks_per_cau). - * The u16 table at CXT +0x200 is a VFL CXT copy journal - * (sub_3D26D8: value = index | (gen<<15), 0xC070 = free), not - * virt→phys. Failed user blocks keep the same VBN and switch - * CAU via the bank bitmap (sub_3D1438 / sub_4EAD34). - */ + *: PBN = VBN (identity over blocks_per_cau). + * The u16 table at CXT +0x200 is a VFL CXT copy journal + * : value = index | (gen<<15), 0xC070 = free), not + * virt→phys. Failed user blocks keep the same VBN and switch + * CAU via the bank bitmap. + */ if (cau >= w->geom.num_cau || !w->vfl.remap[cau]) return virt; if (virt >= w->geom.blocks_per_cau) @@ -184,7 +248,7 @@ static u32 whimory_vfl_phys(struct whimory *w, u32 cau, u32 virt) return w->vfl.remap[cau][virt]; } -/* sub_3D1438: banks that participate in this VBN. */ +/*: banks that participate in this VBN. */ static u32 whimory_vfl_banks_in_vbn(struct whimory *w, u32 vbn, u8 *out, u32 out_max) { @@ -217,7 +281,7 @@ static u32 whimory_vfl_banks_in_vbn(struct whimory *w, u32 vbn, u8 *out, continue; if (out && n < out_max) out[n] = (u8)b; - if (n < S5L8740_FMSS_MAX_CAU) + if (n < S5L8740_NAND_MAX_CAU) w->vfl.cached_banks[n] = (u8)b; n++; } @@ -228,7 +292,7 @@ static u32 whimory_vfl_banks_in_vbn(struct whimory *w, u32 vbn, u8 *out, static u32 whimory_vfl_bank(struct whimory *w, u32 cau, u32 vblock) { - u8 banks[S5L8740_FMSS_MAX_CAU]; + u8 banks[S5L8740_NAND_MAX_CAU]; u32 n, i; n = whimory_vfl_banks_in_vbn(w, vblock, banks, ARRAY_SIZE(banks)); @@ -326,7 +390,7 @@ static void whimory_set_status(struct whimory *w, const char *fmt, ...) } /* ------------------------------------------------------------------ */ -/* Interval map: weave-order LBA→VBA, then packed into the L2V tree. */ +/* Interval map: weave-order LBA→VBA, then packed into the L2V tree. */ /* ------------------------------------------------------------------ */ static struct whimory_range *whimory_range_find(struct rb_root *root, u32 lba) @@ -510,7 +574,7 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) } /* - * sub_3F8958 L2V_Update.c: split at 0x8000 root boundaries, then insert. + *L2V_Update.c: split at 0x8000 root boundaries, then insert. * The interval map is the RO observable of the live tree. */ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) @@ -537,9 +601,9 @@ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) ver = 0; put_unaligned_le16(ver + 1, rec + 4); /* - * sub_110734: whole-root unmap (off=0, - * span=0x8000, vba=invalid) frees the tree. - */ + *: whole-root unmap (off=0, + * span=0x8000, vba=invalid) frees the tree. + */ if (!(lba & 0x7fff) && chunk == WHIMORY_L2V_ROOT_SPAN && vba >= w->l2v.invalid_vba) { @@ -597,7 +661,7 @@ static void whimory_range_free(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* L2V init / lookup / tree pack (sub_E8CA0, sub_428694) */ +/* L2V init / lookup / tree pack , */ /* ------------------------------------------------------------------ */ static void whimory_l2v_free(struct whimory *w) @@ -614,7 +678,7 @@ static void whimory_l2v_free(struct whimory *w) w->l2v.free_count = 0; } -/* L2V_Mem.c sub_3EB0DC / sub_3EAEC8 — intrusive free list in node[0]. */ +/* L2V_Mem.c— intrusive free list in node[0]. */ static void whimory_l2v_mem_free(struct whimory_l2v *l2v, u32 idx) { u8 *node; @@ -920,7 +984,7 @@ static u32 whimory_l2v_collect_root(struct whimory *w, u32 ridx, return nleaf; } -/* sub_E8EC0 analogue: free this root's tree, pack from the interval map. */ +/*analogue: free this root's tree, pack from the interval map. */ static int whimory_l2v_pack_root(struct whimory *w, u32 ridx) { struct whimory_l2v *l2v = &w->l2v; @@ -960,7 +1024,7 @@ static int whimory_l2v_pack_root(struct whimory *w, u32 ridx) return 0; } -/* sub_10FE4C: first insert into an empty root — one node, up to 3 leaves. */ +/*: first insert into an empty root — one node, up to 3 leaves. */ static int whimory_l2v_grow_empty(struct whimory *w, u32 ridx, u32 off, u32 span, u32 vba) { @@ -1084,7 +1148,7 @@ static int whimory_l2v_build_from_ranges(struct whimory *w) return mapped_roots ? 0 : -ENOENT; } -/* L2V_FindFrag.c sub_10C344 — walk leaves, record fragment stats. */ +/* L2V_FindFrag.c— walk leaves, record fragment stats. */ static void whimory_l2v_find_frag_node(struct whimory *w, u32 node_idx, u32 *count, u32 *maxspan, int depth) { @@ -1297,18 +1361,18 @@ static int whimory_l2v_search(struct whimory *w, u32 lba, } /* ------------------------------------------------------------------ */ -/* FIL */ +/* FIL */ /* ------------------------------------------------------------------ */ static int whimory_fil_init(struct whimory *w) { - struct s5l8740_fmss_geom g; + struct s5l8740_nand_geom g; int ret; - ret = s5l8740_fmss_hw_init(); + ret = s5l8740_nand_hw_init(); if (ret) return ret; - ret = s5l8740_fmss_query_geometry(&g); + ret = s5l8740_nand_query_geometry(&g); if (ret) return ret; if (!g.dev_id) @@ -1321,10 +1385,10 @@ static int whimory_fil_init(struct whimory *w) w->geom.page_size = g.page_size; w->geom.vfl_tail = g.vfl_tail; w->geom.user_blocks = g.blocks_per_cau - g.vfl_tail; - w->geom.dev_id = s5l8740_fmss_fil_get_info(101); - w->geom.geom_104 = s5l8740_fmss_fil_get_info(104); - w->geom.geom_105 = s5l8740_fmss_fil_get_info(105); - w->geom.geom_135 = s5l8740_fmss_fil_get_info(135); + w->geom.dev_id = s5l8740_nand_fil_get_info(101); + w->geom.geom_104 = s5l8740_nand_fil_get_info(104); + w->geom.geom_105 = s5l8740_nand_fil_get_info(105); + w->geom.geom_135 = s5l8740_nand_fil_get_info(135); if (!w->geom.dev_id) return -ENODEV; if (w->geom.geom_104 && w->geom.geom_104 != w->geom.page_size) { @@ -1350,11 +1414,11 @@ static int whimory_fil_init(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* FPart — signature from media (or oracle firmware file) */ +/* FPart — signature from media (or oracle firmware file) */ /* ------------------------------------------------------------------ */ /* - * sub_12F368 / sub_1122FC — FPart signature is NOT a user-page hunt. + *— FPart signature is NOT a user-page hunt. * OSOS: memset(sig, 0xA5, 0x600) then _fpart->op80(sig, 0x600, 0xC101). * READ ONLY — never AllocateSpecialBlock / WriteSpecial / erase. * Validate: magic 0x776d7278, ver<=6, +0x34 == FIL GetInfo(101). @@ -1382,7 +1446,7 @@ static void whimory_log_sig_fields(struct whimory *w, const u8 *s, geom, w->geom.dev_id, vfl_arg, fpt_a, extra, cfg, s); } -/* OSOS sub_1122FC checks — not the old ver>=1 / major<=16 heuristic. */ +/* OSOSchecks — not the old ver>=1 / major<=16 heuristic. */ static int whimory_validate_signature(struct whimory *w, const u8 *sig) { u32 magic = whimory_sig32(sig, 0x00); @@ -1479,7 +1543,7 @@ static bool fpart_meta_is_assign(const u8 *meta, u16 *type_out); static bool fpart_has_xrmw(const u8 *page); /* - * sub_3E5650 op=1 analogue. Special objects often live on SLC; try SLC + *op=1 analogue. Special objects often live on SLC; try SLC * then MLC. Full 16 KiB data + 64B META; special uses first 16 META bytes. */ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, @@ -1498,9 +1562,9 @@ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, for (i = 0; i < 2; i++) { int ret; - ret = s5l8740_fmss_page_read(ce, cau, block, page, slc_order[i], + ret = s5l8740_nand_page_read(ce, cau, block, page, slc_order[i], 16, data, w->geom.page_size, - meta, S5L8740_FMSS_META_SIZE); + meta, S5L8740_NAND_META_SIZE); if (ret) continue; last = 0; @@ -1513,7 +1577,7 @@ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, return last; } -/* sub_4EB0CC — 16-byte META copy. LE type_word at +2 (RE). */ +/*— 16-byte META copy. LE type_word at +2 (RE). */ static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out) { unsigned int slot; @@ -1536,7 +1600,7 @@ static bool fpart_meta_special(const u8 *meta, u8 want_chunk, u16 *type_out) /* * Scanner: META tag 0x30 and class 1. Chunk-0 assignment pages use m[1]==0 - * with class in type_word[15:8]. Do not treat payload magic as a hit. + * with class in type_word[15:8]. Avoid treat payload magic as a hit. */ static bool fpart_meta_is_assign(const u8 *meta, u16 *type_out) { @@ -1723,7 +1787,7 @@ static int fpart_scan_region(struct whimory *w, u16 type, bool *matched) { u8 *page; - u8 meta[S5L8740_FMSS_META_SIZE]; + u8 meta[S5L8740_NAND_META_SIZE]; u16 bank, nbanks = fpart_num_banks(w); u32 b, p; int ret, reads = 0, tag30 = 0, xrmw = 0, wrmx = 0, fail = 0; @@ -1739,7 +1803,7 @@ static int fpart_scan_region(struct whimory *w, u16 type, if (page_hi >= w->geom.pages_per_block) page_hi = w->geom.pages_per_block - 1; - s5l8740_fmss_nand_reset(); + s5l8740_nand_reset(); for (bank = 0; bank < nbanks; bank++) { for (b = block_hi; b > block_lo; b--) { @@ -1864,7 +1928,7 @@ static int fpart_scan_region(struct whimory *w, u16 type, /* * fpart_locate_special_4EBBDC: cache by low byte, else scan tail assignment * pages (META 0x30 chunk 0). scanned=true after a full miss so we do not - * rescan. sub_3E5650 op=4 bitmap is not ported — every tail block is read. + * rescan.op=4 bitmap is not ported — every tail block is read. */ static bool fpart_locate_special(struct whimory *w, u16 *index, u16 type) { @@ -1918,7 +1982,7 @@ static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, { struct fpart_special_entry *e; u8 *page; - u8 meta[S5L8740_FMSS_META_SIZE]; + u8 meta[S5L8740_NAND_META_SIZE]; u32 page_size, chunk_count = 1, copy_slots, chunk, slot; u32 object_len = 0, copy_len = 0, generation = 0; int ret = -ENOENT; @@ -2144,7 +2208,7 @@ static int whimory_payload_read_page(struct whimory *w, u16 bank, u32 block, for (i = 0; i < 2; i++) { int ret; - ret = s5l8740_fmss_page_read(ce, cau, block, page, slc_order[i], + ret = s5l8740_nand_page_read(ce, cau, block, page, slc_order[i], 16, data, w->geom.page_size, NULL, 0); if (ret) { @@ -2261,7 +2325,7 @@ static int whimory_payload_magic_scan(struct whimory *w) if (!page) return -ENOMEM; - s5l8740_fmss_nand_reset(); + s5l8740_nand_reset(); hit = whimory_payload_scan_range(w, page, user, nblk, 0, w->geom.pages_per_block - 1, "tail", &reads); @@ -2316,7 +2380,7 @@ static u32 n31_sftl_minor(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* VFL */ +/* VFL */ /* ------------------------------------------------------------------ */ static int n31_vfl_init(struct whimory *w) @@ -2382,11 +2446,11 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, w->vfl.ctx_block[cau] = block; /* - * sub_4EB7E4: memcpy(cxt_copies, data+0x100, 4 * num_copies). - * Each record is {le16 phys_block, u8 bank, u8 flags} — VFL CXT - * copy locations in the tail, not a user virt→phys table. - * Live glass: first u32 is often 0x827 (block 2087). - */ + *: memcpy(cxt_copies, data+0x100, 4 * num_copies). + * Each record is {le16 phys_block, u8 bank, u8 flags} — VFL CXT + * copy locations in the tail, not a user virt→phys table. + * Live glass: first u32 is often 0x827 (block 2087). + */ tab = page + 0x100; for (i = 0; i < 64 && 0x100 + 4 * (i + 1) <= 0x200; i++) { u16 blk = get_unaligned_le16(tab + i * 4); @@ -2399,7 +2463,7 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } w->vfl.cxt_loc_count += loc; - /* sub_4EB098: per-bank u16 CXT copy journal at +0x200 + 32*bank */ + /*: per-bank u16 CXT copy journal at +0x200 + 32*bank */ if (page_len >= WHIMORY_VFL_CXT_HDR + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau + 2) { unsigned int b, j, n16 = w->vfl.cxt_u16_len; @@ -2424,11 +2488,11 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } /* - * sub_3D1438 bitmap: one byte per VBN (stride 0x8D0D0F0 = 1 on N31), - * bit = bank. Not in the 0x200 header / spare journal. Try the - * remainder of this CXT page; reject if any byte has bits outside - * num_cau (would be unrelated payload). - */ + *bitmap: one byte per VBN (stride 0x8D0D0F0 = 1 on N31), + * bit = bank. Not in the 0x200 header / spare journal. Try the + * remainder of this CXT page; reject if any byte has bits outside + * num_cau (would be unrelated payload). + */ { unsigned int off = WHIMORY_VFL_CXT_HDR + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau; @@ -2454,10 +2518,10 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } /* - * User VBN→PBN is identity over blocks_per_cau (sub_4EAE40: - * vbn < mcxt.dev.blocks_per_cau). Failed-block replacement lives - * in the u16 tables, not in a 256-entry slice of +0x100. - */ + * User VBN→PBN is identity over blocks_per_cau : + * vbn < mcxt.dev.blocks_per_cau). Failed-block replacement lives + * in the u16 tables, not in a 256-entry slice of +0x100. + */ w->vfl.remap_count = w->geom.blocks_per_cau; dev_info(w->dev, "VFL ingest ce=%u cau=%u blk=%u magic=%d type20=%d cxt_loc=%u identity=%u\n", @@ -2469,15 +2533,15 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, static int n31_vfl_open(struct whimory *w) { u8 *page; - u8 meta[S5L8740_FMSS_META_SIZE]; + u8 meta[S5L8740_NAND_META_SIZE]; unsigned int ce, cau, b, start, pg, slc; int hits = 0; - page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); + page = kvmalloc(S5L8740_NAND_PAGE_SIZE, GFP_KERNEL); if (!page) return -ENOMEM; start = w->geom.blocks_per_cau - w->geom.vfl_tail; - s5l8740_fmss_nand_reset(); + s5l8740_nand_reset(); for (ce = 0; ce < w->geom.num_ce; ce++) { for (cau = 0; cau < w->geom.num_cau; cau++) { for (b = start; b < w->geom.blocks_per_cau; b++) { @@ -2486,10 +2550,10 @@ static int n31_vfl_open(struct whimory *w) cond_resched(); for (slc = 0; slc < 2; slc++) { - got = s5l8740_fmss_page_read(ce, + got = s5l8740_nand_page_read(ce, cau, b, pg, slc, 16, page, - S5L8740_FMSS_PAGE_SIZE, + S5L8740_NAND_PAGE_SIZE, meta, sizeof(meta)); if (!got) break; @@ -2498,7 +2562,7 @@ static int n31_vfl_open(struct whimory *w) continue; if (n31_vfl_ingest_ctx(w, ce, cau, b, page, - S5L8740_FMSS_PAGE_SIZE, + S5L8740_NAND_PAGE_SIZE, meta)) hits++; } @@ -2531,7 +2595,7 @@ static int n31_vfl_read_vba(struct whimory *w, u32 vba, u32 count, u32 i, ce, cau, vblock, page, slot, pblock; u32 last_ce = ~0u, last_cau = ~0u, last_pblock = ~0u, last_page = ~0u; u8 *pagebuf; - u8 spare[S5L8740_FMSS_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; int ret; if (!count || count > WHIMORY_VBAS_PER_PAGE) @@ -2554,10 +2618,10 @@ static int n31_vfl_read_vba(struct whimory *w, u32 vba, u32 count, pblock = whimory_vfl_phys(w, cau, vblock); if (ce != last_ce || cau != last_cau || pblock != last_pblock || page != last_page) { - ret = s5l8740_fmss_page_read(ce, cau, pblock, page, 0, - 16, pagebuf, - S5L8740_FMSS_PAGE_SIZE, - spare, sizeof(spare)); + ret = whimory_cs_read_page(w, ce, cau, pblock, page, + pagebuf, + S5L8740_NAND_PAGE_SIZE, + spare, sizeof(spare)); if (ret) return ret; last_ce = ce; @@ -2603,10 +2667,10 @@ static int whimory_vfl_open(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* SFTL recovery — classify SBs, replay BTOC/META by weave */ +/* SFTL recovery — classify SBs, replay BTOC/META by weave */ /* ------------------------------------------------------------------ */ -/* sub_50CFA0: FFFF0001 payload is {count, [lba,span]...} → unmap. */ +/*: FFFF0001 payload is {count, [lba,span]...} → unmap. */ static int whimory_sftl_apply_list(struct whimory *w, u32 vba) { u8 *buf; @@ -2906,14 +2970,14 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) { unsigned int pg, slot, vblock; u8 *data = w->sftl.data_page; - u8 spare[S5L8740_FMSS_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; int ret, hits = 0; vblock = whimory_vfl_virt(w, sb->cau, sb->block); for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; pg++) { - ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, pg, 0, - 16, data, S5L8740_FMSS_PAGE_SIZE, - spare, sizeof(spare)); + ret = whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, pg, + data, S5L8740_NAND_PAGE_SIZE, + spare, sizeof(spare)); if (ret) break; if (whimory_page_blank(data, 64) && @@ -2972,7 +3036,7 @@ static int whimory_sb_cmp(const void *a, const void *b) return 0; } -/* sub_569D18 analogue: CXT SB VBAs are not L2V_Update'd (sub_5884D4). */ +/*analogue: CXT SB VBAs are not L2V_Update'd. */ static bool whimory_vba_is_cxt(struct whimory *w, u32 vba) { u32 ce, cau, vblock, page, slot, phys, i; @@ -3071,7 +3135,7 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) u32 last_ce = ~0u, last_cau = ~0u, last_pblock = ~0u, last_page = ~0u; u32 zone, n, i; u8 *data, *gmeta; - u8 spare[S5L8740_FMSS_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; int ret, done = 0; if (sb_idx >= s->num_sb) @@ -3085,7 +3149,7 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) w->cxt_lba_valid = false; w->cxt_next_lba = 0; - /* sub_4FDBE8: VFL_Read in chunks of sftl.gc.zoneSize into ED7C/ED80. */ + /*: VFL_Read in chunks of sftl.gc.zoneSize into ED7C/ED80. */ for (ofs = 0; ofs < s->vbas_per_sb && !done; ofs += zone) { n = min(zone, s->vbas_per_sb - ofs); for (i = 0; i < n; i++) { @@ -3098,12 +3162,11 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) pblock = whimory_vfl_phys(w, cau, vblock); if (ce != last_ce || cau != last_cau || pblock != last_pblock || page != last_page) { - ret = s5l8740_fmss_page_read(ce, cau, pblock, - page, 0, 16, - s->data_page, - S5L8740_FMSS_PAGE_SIZE, - spare, - sizeof(spare)); + ret = whimory_cs_read_page(w, ce, cau, pblock, + page, s->data_page, + S5L8740_NAND_PAGE_SIZE, + spare, + sizeof(spare)); if (ret) return ret; last_ce = ce; @@ -3240,7 +3303,7 @@ static void whimory_print_recovery_stats(struct whimory *w) static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) { unsigned int i, pg, scanned = 0, cap; - u8 spare[S5L8740_FMSS_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; u8 *data = w->sftl.data_page; cap = meta0_scan_sbs; @@ -3258,10 +3321,10 @@ static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; pg++) { int ret; - ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, - pg, 0, 16, data, - S5L8740_FMSS_PAGE_SIZE, - spare, sizeof(spare)); + ret = whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, + pg, data, + S5L8740_NAND_PAGE_SIZE, + spare, sizeof(spare)); if (ret) break; whimory_note_meta0(w, sb->ce, sb->cau, sb->block, pg, @@ -3275,7 +3338,7 @@ static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) static void whimory_dump_vba_page(struct whimory *w, u32 vba) { u32 ce, cau, vblock, page, slot, pblock; - u8 spare[S5L8740_FMSS_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; u8 *data = w->sftl.data_page; int ret; @@ -3291,9 +3354,9 @@ static void whimory_dump_vba_page(struct whimory *w, u32 vba) "BAD_VBA vba=%u sb=%u ofs=%u -> ce=%u cau=%u vblock=%u pbn=%u page=%u map_slot=%u\n", vba, s_g_vba_to_sb(w, vba), s_g_vba_to_ofs(w, vba), ce, cau, vblock, pblock, page, slot); - ret = s5l8740_fmss_page_read(ce, cau, pblock, page, 0, 16, data, - S5L8740_FMSS_PAGE_SIZE, spare, - sizeof(spare)); + ret = whimory_cs_read_page(w, ce, cau, pblock, page, data, + S5L8740_NAND_PAGE_SIZE, spare, + sizeof(spare)); if (ret) { dev_warn(w->dev, "BAD_VBA page read %d\n", ret); return; @@ -3314,8 +3377,8 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) { struct whimory_sftl *s = &w->sftl; unsigned int ce, cau, b, nscan, nsb = 0, i, open_done = 0; - u8 meta0[S5L8740_FMSS_META_SIZE]; - u8 meta127[S5L8740_FMSS_META_SIZE]; + u8 meta0[S5L8740_NAND_META_SIZE]; + u8 meta127[S5L8740_NAND_META_SIZE]; u8 *p127; int ret; @@ -3343,16 +3406,16 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) dev_info(w->dev, "SFTL classify ce=%u cau=%u blk=%u/%u nsb=%u\n", ce, cau, b, nscan, nsb); - r0 = s5l8740_fmss_page_read(ce, cau, b, 0, 0, 16, - w->sftl.data_page, - S5L8740_FMSS_PAGE_SIZE, - meta0, sizeof(meta0)); - r127 = s5l8740_fmss_page_read(ce, cau, b, - WHIMORY_BTOC_PAGE, - 0, 16, p127, - S5L8740_FMSS_PAGE_SIZE, - meta127, - sizeof(meta127)); + r0 = whimory_cs_read_page(w, ce, cau, b, 0, + w->sftl.data_page, + S5L8740_NAND_PAGE_SIZE, + meta0, sizeof(meta0)); + r127 = whimory_cs_read_page(w, ce, cau, b, + WHIMORY_BTOC_PAGE, + p127, + S5L8740_NAND_PAGE_SIZE, + meta127, + sizeof(meta127)); if (!r0) whimory_note_meta0(w, ce, cau, b, 0, w->sftl.data_page, @@ -3382,11 +3445,10 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) sb->kind = WHIMORY_SB_CXT; s->cxt_sbs++; whimory_cxt_add_base(w, sb_idx, sb->weave); - } else if (!r0 && - meta0[0] == WHIMORY_META_TYPE_SFTL_CXT) { + } else if (!r0 && whimory_meta_slot0_or_any_cxt(meta0)) { sb->kind = WHIMORY_SB_CXT; s->cxt_sbs++; - } else if (!r127 && whimory_meta_is_btoc(meta127)) { + } else if (!r127 && whimory_meta_any_btoc(meta127)) { sb->kind = WHIMORY_SB_CLOSED; s->btoc_sbs++; } else if ((!r0 && whimory_meta_is_data_raw(meta0)) || @@ -3427,11 +3489,11 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (sb->kind == WHIMORY_SB_CLOSED) { int ingested; - ret = s5l8740_fmss_page_read(sb->ce, sb->cau, sb->block, - WHIMORY_BTOC_PAGE, 0, 16, - s->btoc_page, - S5L8740_FMSS_PAGE_SIZE, - meta127, sizeof(meta127)); + ret = whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, + WHIMORY_BTOC_PAGE, + s->btoc_page, + S5L8740_NAND_PAGE_SIZE, + meta127, sizeof(meta127)); if (ret) continue; s->btoc_pages_read++; @@ -3445,7 +3507,7 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) s->claim_weave = sb->weave; ingested = whimory_ingest_btoc_page(w, sb->ce, sb->cau, vblock, s->btoc_page, - S5L8740_FMSS_PAGE_SIZE); + S5L8740_NAND_PAGE_SIZE); s->claim_weave = 0; if (ingested) s->btoc_pages_valid++; @@ -3498,22 +3560,24 @@ static int whimory_sftl_alloc(struct whimory *w) s->vba_factor_b = s->vbas_per_sb; s->nodepool_bytes = WHIMORY_MIN_NODEPOOL_BYTES; - s->btoc_page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); - s->data_page = kvmalloc(S5L8740_FMSS_PAGE_SIZE, GFP_KERNEL); + s->btoc_page = kvmalloc(S5L8740_NAND_PAGE_SIZE, GFP_KERNEL); + s->data_page = kvmalloc(S5L8740_NAND_PAGE_SIZE, GFP_KERNEL); s->meta_page = kvmalloc(WHIMORY_META_SIZE * WHIMORY_VBAS_PER_PAGE * (WHIMORY_DATA_PAGES_PER_SB + 1), GFP_KERNEL); + s->cs_page = kvmalloc(sizeof(*s->cs_page), GFP_KERNEL); s->sbs = kvcalloc(nsb, sizeof(*s->sbs), GFP_KERNEL); - if (!s->btoc_page || !s->data_page || !s->meta_page || !s->sbs) + if (!s->btoc_page || !s->data_page || !s->meta_page || !s->cs_page || + !s->sbs) return -ENOMEM; /* - * sub_56863C: max_pages_per_btoc = - * div(page_bytes + 16 * vbas_per_sb - 1, page_bytes) + 1 - * 16×512 BTE bytes fit in a 16KiB NAND page → 1; OSOS adds 1 → 2. - */ + *: max_pages_per_btoc = + * div(page_bytes + 16 * vbas_per_sb - 1, page_bytes) + 1 + * 16×512 BTE bytes fit in a 16KiB NAND page → 1; OSOS adds 1 → 2. + */ { u32 page_bytes = w->geom.page_size ? - w->geom.page_size : S5L8740_FMSS_PAGE_SIZE; + w->geom.page_size : S5L8740_NAND_PAGE_SIZE; u32 i; s->max_pages_per_btoc = @@ -3532,11 +3596,11 @@ static int whimory_sftl_alloc(struct whimory *w) } /* - * sub_56A328: zoneSize starts at 0x8D0EC98 * vbas_per_page and - * doubles until >= 16. Minimum from the loop is 16; must be a - * multiple of vbas_per_page. CXT load (sub_4FDBE8) reads this - * many VBAs into gc_data / gc_meta. - */ + *: zoneSize starts at 0x8D0EC98 * vbas_per_page and + * doubles until >= 16. Minimum from the loop is 16; must be a + * multiple of vbas_per_page. CXT load reads this + * many VBAs into gc_data / gc_meta. + */ s->gc_zone_size = WHIMORY_GC_ZONE_MIN; if (s->gc_zone_size % s->vbas_per_page) return -EINVAL; @@ -3547,13 +3611,13 @@ static int whimory_sftl_alloc(struct whimory *w) if (!s->gc_data || !s->gc_meta) return -ENOMEM; /* - * sub_130158 full-size FTL: num_superblocks * user VBAs per SB. - * BTOC page is not host LBA space (DATA_VBAS_PER_SB). - */ + *full-size FTL: num_superblocks * user VBAs per SB. + * BTOC page is not host LBA space (DATA_VBAS_PER_SB). + */ { u64 cap = (u64)nsb * WHIMORY_DATA_VBAS_PER_SB; - w->total_4k_sectors = cap ? cap : FMSS_FTL_DEFAULT_CAPACITY; + w->total_4k_sectors = cap ? cap : NAND_FTL_DEFAULT_CAPACITY; } return 0; } @@ -3692,12 +3756,13 @@ static int whimory_l2v_selftest(struct whimory *w) static int n31_sftl_open(struct whimory *w) { int ret; + int sess; /* - * OSOS FTL_Open: sub_56863C BTOC (6 slots / 2 open LBA maps), - * sub_56A328 GC zone, sub_56B56C block tables, sub_56C7B8 SB - * state, nodepool ≥ 0x80000, sub_E8CA0 L2V_Init, then s_boot. - */ + * OSOS FTL_Open:BTOC (6 slots / 2 open LBA maps), + *GC zone,block tables,SB + * state, nodepool ≥ 0x80000,L2V_Init, then s_boot. + */ ret = whimory_sftl_alloc(w); if (ret) return ret; @@ -3718,7 +3783,14 @@ static int n31_sftl_open(struct whimory *w) return ret; whimory_l2v_selftest(w); + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + dev_warn(w->dev, "SFTL recover: DMA session %d\n", sess); + /* Continue — cs_phys_read may still one-shot arm. */ + } ret = whimory_sftl_recover_l2v_from_media(w); + if (sess == 0) + s5l8740_nand_dma_session_end(); if (ret) return ret; return 0; @@ -3735,11 +3807,11 @@ static const struct whimory_ftl_ops n31_sftl_ops = { static int whimory_select_ops(struct whimory *w) { /* - * OSOS dispatches VFL/FTL by signature major through a table that - * is not named in the static dump. N31 media is PPN VFL + SFTL; - * those are the only ops this module implements. Log the majors - * from the signature (when present) and bind the N31 ops. - */ + * OSOS dispatches VFL/FTL by signature major through a table that + * is not named in the static dump. N31 media is PPN VFL + SFTL; + * those are the only ops this module implements. Log the majors + * from the signature (when present) and bind the N31 ops. + */ w->vfl_ops = &n31_vfl_ops; w->ftl = &n31_sftl_ops; if (w->sig_ok) { @@ -3782,7 +3854,7 @@ static int whimory_ftl_open(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* Read path (sub_56AB3C / sub_56C328) */ +/* Read path */ /* ------------------------------------------------------------------ */ static int whimory_validate_meta(struct whimory *w, @@ -3917,7 +3989,7 @@ static int whimory_check_lba0(struct whimory *w) } /* ------------------------------------------------------------------ */ -/* Block device */ +/* Block device */ /* ------------------------------------------------------------------ */ static void whimory_submit_bio_range(struct bio *bio, u64 start_4k, @@ -4048,7 +4120,7 @@ static int whimory_register_disk(struct whimory *w) gd = whimory_alloc_disk(w, FTL_IPOD_NAME, &whimory_ipod_ops); if (!IS_ERR(gd)) w->ipod_disk = gd; - s5l8740_fmss_register_ftl_read(whimory_ftl_read_hook); + s5l8740_nand_register_ftl_read(whimory_ftl_read_hook); dev_info(w->dev, "/dev/%s registered read-only (%llu x %uB)\n", FTL_DISK_NAME, w->total_4k_sectors, WHIMORY_LBA_SIZE); @@ -4057,7 +4129,7 @@ static int whimory_register_disk(struct whimory *w) static void whimory_unregister_disk(struct whimory *w) { - s5l8740_fmss_register_ftl_read(NULL); + s5l8740_nand_register_ftl_read(NULL); if (w->ipod_disk) { del_gendisk(w->ipod_disk); put_disk(w->ipod_disk); @@ -4074,16 +4146,23 @@ static ssize_t whimory_status_show(struct device *dev, struct device_attribute *attr, char *buf) { struct whimory *w = whimory_dev; + int meta_ok = s5l8740_nand_meta_transport_ok(); if (!w) return sysfs_emit(buf, "no device\n"); return sysfs_emit(buf, "fil=%d sig=%d vfl=%d ftl=%d l2v=%d lba0=%d oracle=%d\n" + "meta_transport=%s cs_dma_safe=%d pio_meta_trusted=0 " + "disk_gate=%s\n" "mapped_roots=%u mapped_lbas=%u btoc_sbs=%u open_sbs=%u cxt_sbs=%u empty=%u recs=%u cxt_loaded=%d packed=%d\n" "lba0_vba=%u cap=%llu vbas_per_sb=%u hole=%u list=%u\n" "spare_applied=%u bitmap=%u frag=%u/%u gc_zone=%u btoc_pages=%u updates=%u gen=%u free=%u list_unmapped=%u\n%s\n", w->fil_ok, w->sig_ok, w->vfl_ok, w->ftl_ok, w->l2v_ok, w->lba0_ok, w->oracle_used, + meta_ok ? "enabled" : "disabled", + meta_ok ? 1 : 0, + w->disk ? "registered" : + (meta_ok ? "blocked_open" : "blocked_cs_phys_only"), w->sftl.mapped_roots, w->sftl.mapped_lbas, w->sftl.btoc_sbs, w->sftl.open_sbs, w->sftl.cxt_sbs, w->sftl.empty_sbs, w->sftl.btoc_recs, @@ -4119,13 +4198,14 @@ static void whimory_free(struct whimory *w) kvfree(w->sftl.btoc_page); kvfree(w->sftl.data_page); kvfree(w->sftl.meta_page); + kvfree(w->sftl.cs_page); kvfree(w->sftl.sbs); kvfree(w->sftl.gc_data); kvfree(w->sftl.gc_meta); kvfree(w->vfl.bank_mask); for (cau = 0; cau < WHIMORY_BTOC_OPEN; cau++) kvfree(w->sftl.btoc_lba[cau]); - for (cau = 0; cau < S5L8740_FMSS_MAX_CAU; cau++) { + for (cau = 0; cau < S5L8740_NAND_MAX_CAU; cau++) { kvfree(w->vfl.remap[cau]); kvfree(w->vfl.cxt_u16[cau]); } @@ -4142,6 +4222,23 @@ static int whimory_open_stack(struct whimory *w) whimory_set_status(w, "FIL_Init failed %d", ret); return ret; } + + /* + * Without CS metadata DMA, classic Whimory open cannot validate + * META via page_read. Recover is available via CS phys reads: + * echo 1 > .../ftl_sftl_recover (binds csmap disks to L2V). + */ + if (!s5l8740_nand_meta_transport_ok()) { + whimory_set_status(w, + "CS metadata DMA disabled; " + "use ftl_sftl_recover (CS META path) " + "or meta_dma_read=1"); + pr_info("s5l8740-ftl: Whimory auto-open deferred " + "(meta_dma_read=0); run ftl_sftl_recover for " + "CXT→BTOC→L2V on CS META\n"); + return -EOPNOTSUPP; + } + ret = whimory_read_signature(w); if (ret) { whimory_set_status(w, "signature failed %d", ret); @@ -4174,20 +4271,221 @@ static int whimory_open_stack(struct whimory *w) return 0; } +bool whimory_l2v_ready(void) +{ + return whimory_dev && whimory_dev->l2v_ok; +} + +int whimory_read_fmss_lba(u32 lba, void *buf) +{ + struct whimory *w = whimory_dev; + int sess, ret; + + if (!w || !buf) + return -EINVAL; + if (!w->l2v_ok || !w->ftl || !w->ftl->read_lba) + return -ENODEV; + sess = s5l8740_nand_dma_session_begin(); + mutex_lock(&w->tree_lock); + ret = w->ftl->read_lba(w, lba, buf, false); + mutex_unlock(&w->tree_lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return ret; +} + +int whimory_range_walk(int (*fn)(u32 start, u32 len, u32 vba, u64 weave, + void *ctx), + void *ctx) +{ + struct whimory *w = whimory_dev; + struct rb_node *n; + struct whimory_range *snap; + unsigned int i, count = 0; + int ret = 0; + + if (!w || !fn) + return -EINVAL; + + mutex_lock(&w->tree_lock); + count = w->sftl.range_nodes; + if (!count) { + mutex_unlock(&w->tree_lock); + return 0; + } + snap = kvmalloc_array(count, sizeof(*snap), GFP_KERNEL); + if (!snap) { + mutex_unlock(&w->tree_lock); + return -ENOMEM; + } + i = 0; + for (n = rb_first(&w->ranges); n && i < count; n = rb_next(n)) { + struct whimory_range *r = rb_entry(n, struct whimory_range, rb); + + snap[i].start = r->start; + snap[i].len = r->len; + snap[i].vba = r->vba; + snap[i].weave = r->weave; + i++; + } + count = i; + mutex_unlock(&w->tree_lock); + + for (i = 0; i < count; i++) { + ret = fn(snap[i].start, snap[i].len, snap[i].vba, + snap[i].weave, ctx); + if (ret) + break; + } + kvfree(snap); + return ret; +} + +int whimory_l2v_search_phys(u32 lba, u8 *ce, u8 *cau, u16 *blk, u8 *page, + u8 *slot, u64 *weave) +{ + struct whimory *w = whimory_dev; + u32 vba = ~0u, span = 0, vce, vcau, vblock, vpage, vslot, pblock; + struct whimory_range *r; + int ret; + + if (!w || !w->l2v_ok) + return -ENODEV; + mutex_lock(&w->tree_lock); + ret = whimory_l2v_search(w, lba, &vba, &span); + if (ret || vba >= w->l2v.invalid_vba) { + mutex_unlock(&w->tree_lock); + return ret ? ret : -ENOENT; + } + r = whimory_range_find(&w->ranges, lba); + if (weave) + *weave = r ? r->weave : 0; + ret = whimory_unpack_vba(w, vba, &vce, &vcau, &vblock, &vpage, &vslot); + if (ret) { + mutex_unlock(&w->tree_lock); + return ret; + } + vcau = whimory_vfl_bank(w, vcau, vblock); + pblock = whimory_vfl_phys(w, vcau, vblock); + mutex_unlock(&w->tree_lock); + if (ce) + *ce = (u8)vce; + if (cau) + *cau = (u8)vcau; + if (blk) + *blk = (u16)pblock; + if (page) + *page = (u8)vpage; + if (slot) + *slot = (u8)vslot; + return 0; +} + +int whimory_sftl_recover_cs(void) +{ + struct whimory *w = whimory_dev; + int ret, sess; + + if (!w) + return -ENODEV; + + ret = whimory_fil_init(w); + if (ret) { + whimory_set_status(w, "FIL_Init failed %d", ret); + return ret; + } + + ret = whimory_read_signature(w); + if (ret) { + dev_warn(w->dev, + "signature %d; CS recover continues (identity VFL)\n", + ret); + } + + ret = whimory_select_ops(w); + if (ret) + return ret; + + if (!w->vfl_ok) { + ret = whimory_vfl_open(w); + if (ret) { + whimory_set_status(w, "VFL_Open failed %d", ret); + return ret; + } + } + + if (!w->ftl_ok) { + ret = whimory_ftl_open(w); + if (ret) { + whimory_set_status(w, "FTL_Open/recover failed %d", + ret); + return ret; + } + } else { + /* Re-run recover on CS META (clear prior L2V). */ + whimory_range_free(w); + if (w->l2v.root && w->l2v.num_roots) + memset(w->l2v.root, 0xff, + WHIMORY_L2V_ROOT_REC_SIZE * w->l2v.num_roots); + whimory_l2v_mem_reset(&w->l2v); + w->l2v_ok = false; + w->sftl.cxt_loaded = false; + w->sftl.packed_ok = false; + w->n_cxt = 0; + w->cxt_base_weave = 0; + w->sftl.btoc_sbs = 0; + w->sftl.open_sbs = 0; + w->sftl.empty_sbs = 0; + w->sftl.cxt_sbs = 0; + w->sftl.unknown_sbs = 0; + w->sftl.btoc_pages_read = 0; + w->sftl.btoc_pages_valid = 0; + w->sftl.btoc_entries_seen = 0; + w->sftl.btoc_l2v_updates = 0; + w->sftl.open_slots_seen = 0; + w->sftl.open_slots_valid_meta = 0; + w->sftl.open_l2v_updates = 0; + w->sftl.range_nodes = 0; + w->sftl.cxt_l2v_updates = 0; + + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) + dev_warn(w->dev, "re-recover DMA session %d\n", sess); + ret = whimory_sftl_recover_l2v_from_media(w); + if (sess == 0) + s5l8740_nand_dma_session_end(); + if (ret) { + whimory_set_status(w, "re-recover failed %d", ret); + return ret; + } + } + + if (!w->l2v_ok) { + whimory_set_status(w, "recover OK but l2v_ok=0"); + return -EIO; + } + whimory_set_status(w, + "CS recover OK mapped_ranges=%u btoc=%u open=%u", + w->sftl.range_nodes, w->sftl.btoc_pages_valid, + w->sftl.open_l2v_updates); + dev_info(w->dev, "%s\n", w->status); + return 0; +} + static int __init ftl_init(void) { struct whimory *w; int ret; - if (!s5l8740_fmss_available()) { - pr_err("s5l8740-ftl: load fmss-s5l8740.ko first\n"); + if (!s5l8740_nand_available()) { + pr_err("s5l8740-ftl: load nand_s5l8740 first\n"); return -ENODEV; } w = kzalloc(sizeof(*w), GFP_KERNEL); if (!w) return -ENOMEM; - w->dev = fmss_ftl_device(); + w->dev = nand_ftl_device(); mutex_init(&w->bounce_lock); mutex_init(&w->tree_lock); w->ranges = RB_ROOT; @@ -4196,7 +4494,7 @@ static int __init ftl_init(void) kfree(w); return -ENOMEM; } - w->total_4k_sectors = FMSS_FTL_DEFAULT_CAPACITY; + w->total_4k_sectors = NAND_FTL_DEFAULT_CAPACITY; whimory_dev = w; ftl_pdev = platform_device_register_simple("s5l8740-ftl", -1, NULL, 0); @@ -4218,6 +4516,10 @@ static int __init ftl_init(void) return ret; } + ret = ftl_s5l8740_csmap_init(&ftl_pdev->dev); + if (ret) + dev_warn(&ftl_pdev->dev, "CS map init failed %d\n", ret); + ret = whimory_open_stack(w); if (ret) { dev_err(w->dev, @@ -4225,9 +4527,9 @@ static int __init ftl_init(void) ret, FTL_DISK_NAME, w->fil_ok, w->sig_ok, w->vfl_ok, w->ftl_ok, w->l2v_ok, w->lba0_ok); /* - * Keep the platform device so sysfs status is visible. - * The block disk is absent until LBA0 works. - */ + * Keep the platform device so sysfs status is visible. + * The block disk is absent until LBA0 works. + */ return 0; } return 0; @@ -4238,6 +4540,7 @@ static void __exit ftl_exit(void) struct whimory *w = whimory_dev; if (ftl_pdev) { + ftl_s5l8740_csmap_exit(&ftl_pdev->dev); sysfs_remove_group(&ftl_pdev->dev.kobj, &ftl_attr_group); platform_device_unregister(ftl_pdev); ftl_pdev = NULL; @@ -4252,7 +4555,7 @@ module_exit(ftl_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("S5L8740 Whimory PPN SFTL read-only block driver"); MODULE_AUTHOR("n31"); -MODULE_SOFTDEP("pre: fmss_s5l8740"); +MODULE_SOFTDEP("pre: nand_s5l8740"); MODULE_FIRMWARE(WHIMORY_ORACLE_SIG); MODULE_FIRMWARE(WHIMORY_ORACLE_ROOT); MODULE_FIRMWARE(WHIMORY_ORACLE_NODES); diff --git a/drivers/misc/ftl-s5l8740-csmap.c b/drivers/misc/ftl-s5l8740-csmap.c new file mode 100755 index 00000000000000..eca4d2af9386b3 --- /dev/null +++ b/drivers/misc/ftl-s5l8740-csmap.c @@ -0,0 +1,2393 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * S5L8740 FTL CS LBA map and read-only VFAT block front-end (N31). + * + * Builds a sparse LBA→physical cache from CS page metadata (4096+16 × 4 + * slots per page), compresses it into dual L2V/V2L vector tables, and + * registers read-only disks after FAT-critical validation: + * /dev/s5l8740-ipod — user FAT (disk_lba 0 @ fat_base_lba) + * /dev/s5l8740-ftl — same FAT (compat alias) + * /dev/s5l8740-firmware — pre-FAT fmss range, if mapped content found + * + * Authoritative map after `ftl_sftl_recover`: Whimory CXT→BTOC→L2V_Update + * (CS META), with reads via L2V_Search. `ftl_map_build` (blk 62–66) is a + * debug fallback only — not a journal replay. + * + * Every data return re-validates the on-media metadata LBA. Read-only: + * no program, erase, or GC. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nand-s5l8740.h" +#include "ftl-s5l8740-csmap.h" +#include "ftl-s5l8740-vecmap.h" + +#define N31_FAT_BASE_DEFAULT 49279u +#define N31_FAT_TOTAL_DEFAULT 3856968u +#define N31_FMSS_LBA_MAX (N31_FAT_BASE_DEFAULT + \ + N31_FAT_TOTAL_DEFAULT + 65536u) +#define N31_BPB_CANDIDATES_MAX 16 +#define N31_MAP_HASH_BITS 12 +#define N31_EXTENT_MAX 2048 +#define N31_PAGES_PER_BLOCK 128 + +#define N31_PHYS_KEY_INVALID 0xffffffffu + +/* Sparse hash + extents for construction; L2V/V2L preferred on read. */ + +struct n31_map_entry { + u32 fmss_lba; + u32 phys_key; + u64 weave; /* full 48-bit weaveSeq — do not truncate */ + u8 type; + u8 valid; +}; + +struct n31_map_node { + struct hlist_node hnode; + struct n31_map_entry e; +}; + +struct n31_lba_extent { + u32 start_lba; + u32 len; + u32 start_phys_key; + u64 weave_first; + u8 type; +}; + +struct n31_fat_layout { + u16 bytes_per_sector; + u8 sectors_per_cluster; + u16 reserved_sectors; + u8 num_fats; + u32 fat_size_32; + u32 root_cluster; + u16 fsinfo_sector; + u16 backup_boot_sector; + u16 ext_flags; /* BPB_ExtFlags @ 0x28 */ + u32 total_sectors; + u32 fat_start; + u32 data_start; + u32 root_dir_lba; + u32 fsinfo_lba; + u32 backup_boot_lba; + bool valid; +}; + +enum n31_slice_kind { + N31_SLICE_IPOD = 0, + N31_SLICE_FTL_ALIAS, + N31_SLICE_FIRMWARE, +}; + +struct n31_ftl_cs; + +struct n31_ftl_slice { + struct n31_ftl_cs *ftl; + struct gendisk *gd; + u32 base_fmss; + u32 nsectors; + enum n31_slice_kind kind; +}; + +struct n31_ftl_cs { + struct device *dev; + struct mutex lock; + + DECLARE_HASHTABLE(map, N31_MAP_HASH_BITS); + unsigned int map_entries; + unsigned int map_updates; + unsigned int map_skips; + unsigned int map_collisions; + unsigned int map_pages; + unsigned int map_data_recs; + unsigned int newer_replacements; + u32 lba_min, lba_max; + bool map_built; + + struct n31_vecmap vec; + char vec_log[384]; + + struct n31_lba_extent *extents; + unsigned int extent_count; + unsigned int extent_largest; + u32 open_ext_lba; + u32 open_ext_phys; + u32 open_ext_start_lba; + u32 open_ext_start_phys; + u64 open_ext_weave; + u8 open_ext_type; + u32 open_ext_len; + + u32 fat_base_lba; + bool fat_base_valid; + bool fat_base_autodetect; + u32 fat_total_sectors; + struct n31_fat_layout layout; + u8 bpb_sector[N31_DATA_SLOT_SIZE]; + bool bpb_cached; + + u32 bpb_candidates[N31_BPB_CANDIDATES_MAX]; + u64 bpb_cand_weave[N31_BPB_CANDIDATES_MAX]; + u32 bpb_cand_total[N31_BPB_CANDIDATES_MAX]; + char bpb_cand_oem[N31_BPB_CANDIDATES_MAX][9]; + u8 bpb_cand_sector[N31_BPB_CANDIDATES_MAX][N31_DATA_SLOT_SIZE]; + unsigned int bpb_ncand; + unsigned int fat_crit_ok_n; + unsigned int fat_crit_need_n; + + u32 fw_base_fmss; + u32 fw_nsectors; + bool fw_valid; + unsigned int fw_mapped; + unsigned int fw_magic_hits; + char fw_log[320]; + + u8 last_sector[N31_DATA_SLOT_SIZE]; + u32 last_fmss_lba; + u32 last_disk_lba; + int last_ret; + char last_log[768]; + char extents_log[512]; + char range_log[512]; + char layout_log[512]; + char bpb_log[384]; + + unsigned int range_ok; + unsigned int range_fail; + unsigned int range_miss; + unsigned int demand_scans; + + bool disk0_ok; + bool fat_critical_ok; + bool enable_gate_ok; + bool block_enable; + bool dma_session_held; + bool whimory_backed; /* L2V_Search via Whimory recover */ + + struct n31_ftl_slice ipod; + struct n31_ftl_slice ftl_alias; + struct n31_ftl_slice firmware; + u8 *bounce; +}; + +static int n31_ftl_find_bpb(struct n31_ftl_cs *ftl); +static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl); +static int n31_validate_fat_critical(struct n31_ftl_cs *ftl); +static int n31_ftl_register_disk(struct n31_ftl_cs *ftl); +static void n31_ftl_unregister_disk(struct n31_ftl_cs *ftl); +static int n31_ftl_apply_bpb(struct n31_ftl_cs *ftl, u32 fmss_lba, + u32 total, const u8 *sector); +static bool n31_bpb_looks_valid(const u8 *d, u32 *total_out); + +static struct n31_ftl_cs *n31_ftl; + +static bool ftl_block_enable = true; +module_param(ftl_block_enable, bool, 0644); +MODULE_PARM_DESC(ftl_block_enable, + "Register RO ipod/firmware disks after FAT-critical gates (default Y)"); + +static bool ftl_demand_scan; +module_param(ftl_demand_scan, bool, 0644); +MODULE_PARM_DESC(ftl_demand_scan, + "On map miss, scan a nearby block for the LBA (default N; unsafe under mount I/O)"); + +static int fw_start_lba = -1; +module_param(fw_start_lba, int, 0644); +MODULE_PARM_DESC(fw_start_lba, + "Firmware slice start fmss_lba (-1 = auto below fat_base)"); + +static int fw_nsectors = -1; +module_param(fw_nsectors, int, 0644); +MODULE_PARM_DESC(fw_nsectors, + "Firmware slice length in 4096-byte sectors (-1 = auto)"); + +static bool fw_force; +module_param(fw_force, bool, 0644); +MODULE_PARM_DESC(fw_force, + "Register firmware disk even without magic/mapped hits (default N)"); + +/* -------------------- packed physical key -------------------- */ + +static u32 n31_phys_pack(u8 ce, u8 cau, u16 blk, u8 page, u8 slot) +{ + return ((u32)(ce & 0x3) << 30) | + ((u32)(cau & 0x3) << 28) | + ((u32)(blk & 0x3fff) << 14) | + ((u32)(page & 0xff) << 6) | + ((u32)(slot & 0x3)); +} + +static void n31_phys_unpack(u32 key, u8 *ce, u8 *cau, u16 *blk, + u8 *page, u8 *slot) +{ + *ce = (key >> 30) & 0x3; + *cau = (key >> 28) & 0x3; + *blk = (key >> 14) & 0x3fff; + *page = (key >> 6) & 0xff; + *slot = key & 0x3; +} + +/* Advance one CS data slot: slot→…→3 → next page slot0 → next block. */ +static u32 n31_phys_succ(u32 key) +{ + u8 ce, cau, page, slot; + u16 blk; + + if (key == N31_PHYS_KEY_INVALID) + return N31_PHYS_KEY_INVALID; + n31_phys_unpack(key, &ce, &cau, &blk, &page, &slot); + if (slot < 3) + return n31_phys_pack(ce, cau, blk, page, slot + 1); + if (page + 1 < N31_PAGES_PER_BLOCK) + return n31_phys_pack(ce, cau, blk, page + 1, 0); + return n31_phys_pack(ce, cau, blk + 1, 0, 0); +} + +static u32 n31_phys_pred(u32 key) +{ + u8 ce, cau, page, slot; + u16 blk; + + if (key == N31_PHYS_KEY_INVALID) + return N31_PHYS_KEY_INVALID; + n31_phys_unpack(key, &ce, &cau, &blk, &page, &slot); + if (slot > 0) + return n31_phys_pack(ce, cau, blk, page, slot - 1); + if (page > 0) + return n31_phys_pack(ce, cau, blk, page - 1, 3); + if (blk > 0) + return n31_phys_pack(ce, cau, blk - 1, + N31_PAGES_PER_BLOCK - 1, 3); + return N31_PHYS_KEY_INVALID; +} + +static u32 n31_phys_advance(u32 key, u32 delta) +{ + while (delta--) { + key = n31_phys_succ(key); + if (key == N31_PHYS_KEY_INVALID) + break; + } + return key; +} + +static void n31_entry_from_phys(struct n31_map_entry *e, u32 fmss_lba, + u32 phys_key, u64 weave, u8 type) +{ + e->fmss_lba = fmss_lba; + e->phys_key = phys_key; + e->weave = weave; + e->type = type; + e->valid = 1; +} + +static void n31_entry_to_legacy(const struct n31_map_entry *e, + struct n31_lba_map_entry *out) +{ + u8 ce, cau, page, slot; + u16 blk; + + memset(out, 0, sizeof(*out)); + if (!e || !e->valid) + return; + n31_phys_unpack(e->phys_key, &ce, &cau, &blk, &page, &slot); + out->ce = ce; + out->cau = cau; + out->block = blk; + out->page = page; + out->slot = slot; + out->type = e->type; + out->weave = e->weave; + out->fmss_lba = e->fmss_lba; + out->present = true; +} + +/* -------------------- BPB / layout -------------------- */ + +static bool n31_bpb_looks_valid(const u8 *d, u32 *total_out) +{ + u16 bps, root_ents, fat16; + u32 total32, total16, fat32; + u8 spc, fats; + + if (!d) + return false; + if (d[0] != 0xeb && d[0] != 0xe9) + return false; + if (d[0] == 0xeb && d[2] != 0x90) + return false; + bps = get_unaligned_le16(d + 11); + spc = d[13]; + fats = d[16]; + root_ents = get_unaligned_le16(d + 17); + fat16 = get_unaligned_le16(d + 22); + total16 = get_unaligned_le16(d + 19); + total32 = get_unaligned_le32(d + 32); + fat32 = get_unaligned_le32(d + 36); + if (bps != 4096 || spc != 4 || fats == 0 || fats > 2) + return false; + if (!total32) + total32 = total16; + if (!total32 || total32 < 1000u || total32 > 16u * 1024u * 1024u) + return false; + /* + * FAT32: root_ents==0 and FATsz16==0 → require FATsz32. + * Reject MSDOS5.0-style stubs that advertise FAT32 in the type + * string but leave FATsz32=0 (not a mountable volume). + */ + if (root_ents == 0 && fat16 == 0 && fat32 == 0) + return false; + if (total_out) + *total_out = total32; + return true; +} + +static int n31_parse_bpb(const u8 *d, struct n31_fat_layout *L) +{ + u32 total = 0; + + memset(L, 0, sizeof(*L)); + if (!n31_bpb_looks_valid(d, &total)) + return -EINVAL; + + L->bytes_per_sector = get_unaligned_le16(d + 11); + L->sectors_per_cluster = d[13]; + L->reserved_sectors = get_unaligned_le16(d + 14); + L->num_fats = d[16]; + L->total_sectors = total; + L->fat_size_32 = get_unaligned_le32(d + 36); + L->ext_flags = get_unaligned_le16(d + 0x28); + L->root_cluster = get_unaligned_le32(d + 44); + L->fsinfo_sector = get_unaligned_le16(d + 48); + L->backup_boot_sector = get_unaligned_le16(d + 50); + + L->fat_start = L->reserved_sectors; + L->data_start = L->reserved_sectors + + (u32)L->num_fats * L->fat_size_32; + if (L->root_cluster >= 2) + L->root_dir_lba = L->data_start + + (L->root_cluster - 2) * L->sectors_per_cluster; + else + L->root_dir_lba = L->data_start; + L->fsinfo_lba = L->fsinfo_sector; + L->backup_boot_lba = L->backup_boot_sector; + L->valid = true; + return 0; +} + +/* -------------------- sparse map + extents -------------------- */ + +static void n31_extent_close(struct n31_ftl_cs *ftl) +{ + struct n31_lba_extent *ex; + + if (ftl->open_ext_len == 0) + return; + if (ftl->extent_count >= N31_EXTENT_MAX) { + ftl->open_ext_len = 0; + return; + } + if (!ftl->extents) { + ftl->extents = kcalloc(N31_EXTENT_MAX, + sizeof(*ftl->extents), GFP_KERNEL); + if (!ftl->extents) { + ftl->open_ext_len = 0; + return; + } + } + ex = &ftl->extents[ftl->extent_count++]; + ex->start_lba = ftl->open_ext_start_lba; + ex->len = ftl->open_ext_len; + ex->start_phys_key = ftl->open_ext_start_phys; + ex->weave_first = ftl->open_ext_weave; + ex->type = ftl->open_ext_type; + if (ex->len > ftl->extent_largest) + ftl->extent_largest = ex->len; + ftl->open_ext_len = 0; +} + +static void n31_extent_feed(struct n31_ftl_cs *ftl, u32 fmss_lba, + u32 phys_key, u64 weave, u8 type) +{ + if (ftl->open_ext_len == 0) { + ftl->open_ext_start_lba = fmss_lba; + ftl->open_ext_start_phys = phys_key; + ftl->open_ext_lba = fmss_lba; + ftl->open_ext_phys = phys_key; + ftl->open_ext_weave = weave; + ftl->open_ext_type = type; + ftl->open_ext_len = 1; + return; + } + + if (fmss_lba == ftl->open_ext_lba + 1 && + phys_key == n31_phys_succ(ftl->open_ext_phys) && + (type == ftl->open_ext_type || + (type <= 2 && ftl->open_ext_type <= 2))) { + ftl->open_ext_lba = fmss_lba; + ftl->open_ext_phys = phys_key; + ftl->open_ext_len++; + return; + } + + n31_extent_close(ftl); + ftl->open_ext_start_lba = fmss_lba; + ftl->open_ext_start_phys = phys_key; + ftl->open_ext_lba = fmss_lba; + ftl->open_ext_phys = phys_key; + ftl->open_ext_weave = weave; + ftl->open_ext_type = type; + ftl->open_ext_len = 1; +} + +static void n31_extents_reset(struct n31_ftl_cs *ftl) +{ + n31_extent_close(ftl); + ftl->extent_count = 0; + ftl->extent_largest = 0; + ftl->open_ext_len = 0; +} + +static int n31_extent_lookup(struct n31_ftl_cs *ftl, u32 fmss_lba, + struct n31_map_entry *out) +{ + unsigned int i; + + for (i = 0; i < ftl->extent_count; i++) { + struct n31_lba_extent *ex = &ftl->extents[i]; + u32 off; + + if (fmss_lba < ex->start_lba || + fmss_lba >= ex->start_lba + ex->len) + continue; + off = fmss_lba - ex->start_lba; + n31_entry_from_phys(out, fmss_lba, + n31_phys_advance(ex->start_phys_key, off), + ex->weave_first, ex->type); + return 0; + } + return -ENOENT; +} + +static void n31_map_free(struct n31_ftl_cs *ftl) +{ + unsigned int bkt; + struct n31_map_node *n; + struct hlist_node *tmp; + + hash_for_each_safe(ftl->map, bkt, tmp, n, hnode) { + hash_del(&n->hnode); + kfree(n); + } + ftl->map_entries = 0; + ftl->map_updates = 0; + ftl->map_skips = 0; + ftl->map_collisions = 0; + ftl->map_pages = 0; + ftl->map_data_recs = 0; + ftl->newer_replacements = 0; + ftl->lba_min = ~0u; + ftl->lba_max = 0; + ftl->map_built = false; + ftl->disk0_ok = false; + ftl->fat_critical_ok = false; + ftl->enable_gate_ok = false; + n31_extents_reset(ftl); + kfree(ftl->extents); + ftl->extents = NULL; + n31_vecmap_free(&ftl->vec); + ftl->vec_log[0] = '\0'; + ftl->fw_valid = false; + ftl->fw_mapped = 0; + ftl->fw_magic_hits = 0; + ftl->fw_nsectors = 0; + ftl->fw_log[0] = '\0'; +} + +static struct n31_map_node *n31_map_find(struct n31_ftl_cs *ftl, u32 fmss_lba) +{ + struct n31_map_node *n; + + hash_for_each_possible(ftl->map, n, hnode, fmss_lba) { + if (n->e.valid && n->e.fmss_lba == fmss_lba) + return n; + } + return NULL; +} + +static void n31_map_ingest(struct n31_ftl_cs *ftl, u8 ce, u8 cau, + u16 block, u8 page, u8 slot, + const struct s5l8740_meta_decoded *m) +{ + struct n31_map_node *n; + u32 phys; + u64 weave; + + if (!n31_meta_is_data_record(m)) { + ftl->map_skips++; + return; + } + if (m->lba >= N31_FMSS_LBA_MAX) { + ftl->map_skips++; + return; + } + + phys = n31_phys_pack(ce, cau, block, page, slot); + weave = m->weave; + ftl->map_data_recs++; + if (m->lba < ftl->lba_min) + ftl->lba_min = m->lba; + if (m->lba > ftl->lba_max) + ftl->lba_max = m->lba; + + n = n31_map_find(ftl, m->lba); + if (!n) { + n = kzalloc(sizeof(*n), GFP_KERNEL); + if (!n) { + ftl->map_skips++; + return; + } + n31_entry_from_phys(&n->e, m->lba, phys, weave, m->type); + hash_add(ftl->map, &n->hnode, m->lba); + ftl->map_entries++; + n31_extent_feed(ftl, m->lba, phys, weave, m->type); + return; + } + + if (weave == n->e.weave && n->e.phys_key != phys) { + ftl->map_collisions++; + dev_dbg(ftl->dev, + "lba=%u weave collision old_phys=%08x new_phys=%08x\n", + m->lba, n->e.phys_key, phys); + return; + } + if (!n31_weave_newer(weave, n->e.weave)) { + /* Stale or equal — never feed extents from losers. */ + ftl->map_skips++; + return; + } + + ftl->newer_replacements++; + ftl->map_updates++; + n31_entry_from_phys(&n->e, m->lba, phys, weave, m->type); + n31_extent_feed(ftl, m->lba, phys, weave, m->type); +} + +static int n31_scan_page(struct n31_ftl_cs *ftl, u8 ce, u8 cau, + u16 block, u8 page) +{ + struct s5l8740_cs_page *pg; + int ret, s; + + pg = kzalloc(sizeof(*pg), GFP_KERNEL); + if (!pg) + return -ENOMEM; + ret = s5l8740_nand_cs_phys_read(ce, cau, block, page, pg); + if (!ret) { + ftl->map_pages++; + for (s = 0; s < N31_DATA_SLOTS; s++) + n31_map_ingest(ftl, ce, cau, block, page, s, + &pg->meta[s]); + } + kfree(pg); + return ret; +} + +/* + * Compress newest-weave hash entries into L2V/V2L. Pair scratch is freed + * after build; the hash remains available for misses and rebuilds. + */ +static int n31_vecmap_rebuild_from_hash(struct n31_ftl_cs *ftl) +{ + struct n31_vec_pair *pairs; + struct n31_map_node *n; + unsigned int bkt, i = 0, nent; + int ret; + u8 ce, cau, page, slot; + u16 blk; + + nent = ftl->map_entries; + if (!nent) { + n31_vecmap_free(&ftl->vec); + scnprintf(ftl->vec_log, sizeof(ftl->vec_log), + "ready=0 pairs=0\n"); + return 0; + } + + pairs = vmalloc(array_size(nent, sizeof(*pairs))); + if (!pairs) { + scnprintf(ftl->vec_log, sizeof(ftl->vec_log), + "ready=0 err=-ENOMEM pairs=%u\n", nent); + return -ENOMEM; + } + + hash_for_each(ftl->map, bkt, n, hnode) { + if (i >= nent) + break; + n31_phys_unpack(n->e.phys_key, &ce, &cau, &blk, &page, &slot); + pairs[i].l = n->e.fmss_lba; + pairs[i].p = n31_phys_to_ordinal(ce, cau, blk, page, slot); + pairs[i].weave = n->e.weave; + i++; + } + + ret = n31_vecmap_build(&ftl->vec, pairs, i); + vfree(pairs); + if (ret) { + scnprintf(ftl->vec_log, sizeof(ftl->vec_log), + "ready=0 err=%d pairs=%u\n", ret, i); + return ret; + } + + scnprintf(ftl->vec_log, sizeof(ftl->vec_log), + "ready=1 pairs=%u l_base=%u l_count=%u p_base=%u p_count=%u " + "l2v_groups=%u v2l_groups=%u compact_ok=%u esc=%u miss=%u " + "l2v_esc_n=%u v2l_esc_n=%u\n", + i, ftl->vec.l_base, ftl->vec.l_count, + ftl->vec.p_base, ftl->vec.p_count, + ftl->vec.l2v_groups, ftl->vec.v2l_groups, + ftl->vec.compact_ok, ftl->vec.compact_esc, + ftl->vec.compact_miss, ftl->vec.l2v_esc_n, + ftl->vec.v2l_esc_n); + return 0; +} + +static void n31_scan_finish_stats(struct n31_ftl_cs *ftl, const char *tag, + unsigned int pages_ok, + unsigned int pages_fail, + bool rebuild_vec) +{ + int vret = 0; + + n31_extent_close(ftl); + ftl->map_built = ftl->map_entries > 0; + if (rebuild_vec) + vret = n31_vecmap_rebuild_from_hash(ftl); + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "%s pages_ok=%u pages_fail=%u records=%u " + "lba_min=%u lba_max=%u extents=%u largest=%u " + "collisions=%u replacements=%u has_fat_base=%d " + "vec_ready=%d vec_ret=%d\n", + tag, pages_ok, pages_fail, ftl->map_data_recs, + ftl->lba_min == ~0u ? 0 : ftl->lba_min, ftl->lba_max, + ftl->extent_count, ftl->extent_largest, + ftl->map_collisions, ftl->newer_replacements, + n31_map_find(ftl, ftl->fat_base_lba) ? 1 : 0, + ftl->vec.ready ? 1 : 0, vret); + scnprintf(ftl->extents_log, sizeof(ftl->extents_log), + "extent_count=%u largest=%u open_len=%u\n", + ftl->extent_count, ftl->extent_largest, ftl->open_ext_len); + dev_dbg(ftl->dev, "%s", ftl->last_log); + if (rebuild_vec && ftl->vec_log[0]) + dev_dbg(ftl->dev, "vecmap %s", ftl->vec_log); +} + +/* + * Scan CE/CAU × blk_lo..blk_hi × pages. Appends unless @reset. + * Full vector rebuild only when @rebuild_vec (not on demand I/O fills). + */ +static int n31_scan_block_window(struct n31_ftl_cs *ftl, u8 ce, u8 cau, + u16 blk_lo, u16 blk_hi, bool reset, + bool rebuild_vec) +{ + u16 blk; + u8 pg; + unsigned int pages_ok = 0, pages_fail = 0; + int ret; + + if (reset) { + n31_map_free(ftl); + hash_init(ftl->map); + ftl->lba_min = ~0u; + ftl->lba_max = 0; + } + + for (blk = blk_lo; blk <= blk_hi; blk++) { + for (pg = 0; pg < N31_PAGES_PER_BLOCK; pg++) { + ret = n31_scan_page(ftl, ce, cau, blk, pg); + if (ret) + pages_fail++; + else + pages_ok++; + cond_resched(); + } + } + n31_scan_finish_stats(ftl, "scan_window", pages_ok, pages_fail, + rebuild_vec); + return 0; +} + +/* Scan blk_lo..blk_hi on every CE/CAU; reset once; compress vectors once. */ +static int n31_scan_banks_window(struct n31_ftl_cs *ftl, u16 blk_lo, + u16 blk_hi, bool reset) +{ + u8 ce, cau; + bool do_reset = reset; + + for (ce = 0; ce < N31_VEC_NUM_CE; ce++) { + for (cau = 0; cau < N31_VEC_NUM_CAU; cau++) { + bool last = (ce == N31_VEC_NUM_CE - 1) && + (cau == N31_VEC_NUM_CAU - 1); + + n31_scan_block_window(ftl, ce, cau, blk_lo, blk_hi, + do_reset, last); + do_reset = false; + } + } + return 0; +} + +static int n31_map_lookup_hint(struct n31_ftl_cs *ftl, u32 fmss_lba, + struct n31_map_entry *out) +{ + struct n31_map_node *n; + + n = n31_map_find(ftl, fmss_lba); + if (n) { + *out = n->e; + return 0; + } + return n31_extent_lookup(ftl, fmss_lba, out); +} + +/* + * Optional nearby-block fill for map misses. Must not run under block I/O + * with a multi-block window or vector rebuild — that softlocks the system. + */ +static int n31_demand_scan_for(struct n31_ftl_cs *ftl, u32 fmss_lba) +{ + struct n31_map_entry hint; + u8 ce = 0, cau = 0; + u16 blk = 63; + u8 page = 88, slot; + int ret; + u16 lo, hi; + + ftl->demand_scans++; + if (fmss_lba > 0 && + !n31_map_lookup_hint(ftl, fmss_lba - 1, &hint)) { + n31_phys_unpack(hint.phys_key, &ce, &cau, &blk, &page, &slot); + } else if (!n31_map_lookup_hint(ftl, fmss_lba + 1, &hint)) { + n31_phys_unpack(hint.phys_key, &ce, &cau, &blk, &page, &slot); + } else if (!n31_map_lookup_hint(ftl, ftl->fat_base_lba, &hint)) { + n31_phys_unpack(hint.phys_key, &ce, &cau, &blk, &page, &slot); + } + (void)slot; + + dev_dbg(ftl->dev, + "demand_scan lba=%u ce=%u cau=%u blk=%u\n", + fmss_lba, ce, cau, blk); + + lo = blk; + hi = blk; + + /* Home bank first, then the other CE/CAU at the same block. */ + ret = n31_scan_block_window(ftl, ce, cau, lo, hi, false, false); + if (n31_map_find(ftl, fmss_lba) || + !n31_extent_lookup(ftl, fmss_lba, &hint)) + return 0; + { + u8 tce, tcau; + + for (tce = 0; tce < N31_VEC_NUM_CE; tce++) { + for (tcau = 0; tcau < N31_VEC_NUM_CAU; tcau++) { + if (tce == ce && tcau == cau) + continue; + n31_scan_block_window(ftl, tce, tcau, lo, hi, + false, false); + if (n31_map_find(ftl, fmss_lba) || + !n31_extent_lookup(ftl, fmss_lba, &hint)) + return 0; + } + } + } + return ret ? ret : -ENOENT; +} + +static void n31_map_ingest_page(struct n31_ftl_cs *ftl, u8 ce, u8 cau, + u16 block, u8 page, + const struct s5l8740_cs_page *pg) +{ + int s; + + for (s = 0; s < N31_DATA_SLOTS; s++) + n31_map_ingest(ftl, ce, cau, block, page, s, &pg->meta[s]); +} + +/* + * One-page fill from a mapped neighbour's CS page. Safe under block I/O. + * Also tries the predicted successor/predecessor slot's page when distinct. + */ +static int n31_neighbor_probe(struct n31_ftl_cs *ftl, u32 fmss_lba, + struct n31_map_entry *out) +{ + struct n31_map_entry hint; + struct s5l8740_cs_page *page; + u32 try_key[4]; + unsigned int ntry = 0, i, j; + u8 ce, cau, pg, sl; + u16 blk; + int slot, ret; + + if (fmss_lba > 0 && + !n31_map_lookup_hint(ftl, fmss_lba - 1, &hint)) { + try_key[ntry++] = hint.phys_key; + try_key[ntry++] = n31_phys_succ(hint.phys_key); + } + if (!n31_map_lookup_hint(ftl, fmss_lba + 1, &hint)) { + try_key[ntry++] = hint.phys_key; + try_key[ntry++] = n31_phys_pred(hint.phys_key); + } + if (!ntry) + return -ENOENT; + + page = kzalloc(sizeof(*page), GFP_KERNEL); + if (!page) + return -ENOMEM; + + for (i = 0; i < ntry; i++) { + if (try_key[i] == N31_PHYS_KEY_INVALID) + continue; + /* Skip duplicate pages already tried. */ + for (j = 0; j < i; j++) { + u8 ce2, cau2, pg2, sl2; + u16 blk2; + + if (try_key[j] == N31_PHYS_KEY_INVALID) + continue; + n31_phys_unpack(try_key[i], &ce, &cau, &blk, &pg, &sl); + n31_phys_unpack(try_key[j], &ce2, &cau2, &blk2, &pg2, + &sl2); + if (ce == ce2 && cau == cau2 && blk == blk2 && + pg == pg2) + goto next; + } + n31_phys_unpack(try_key[i], &ce, &cau, &blk, &pg, &sl); + (void)sl; + ret = s5l8740_nand_cs_phys_read(ce, cau, blk, pg, page); + if (ret) + continue; + n31_map_ingest_page(ftl, ce, cau, blk, pg, page); + slot = s5l8740_nand_meta_pick_lba(page, fmss_lba); + if (slot >= 0) { + kfree(page); + return n31_map_lookup_hint(ftl, fmss_lba, out); + } +next: + ; + } + kfree(page); + return -ENOENT; +} + +static int n31_ftl_read_fmss_lba_flags(struct n31_ftl_cs *ftl, u32 fmss_lba, + void *dst, bool allow_demand) +{ + struct n31_map_entry e; + struct n31_lba_map_entry leg; + struct s5l8740_cs_page *page; + int slot, ret; + u8 ce, cau, pg, sl; + u16 blk; + u32 p_ord; + bool have_hint = false; + + if (!ftl || !dst) + return -EINVAL; + + /* Whimory L2V is authoritative after CXT→BTOC recover. */ + if (ftl->whimory_backed && whimory_l2v_ready()) { + ret = whimory_read_fmss_lba(fmss_lba, dst); + if (!ret) + return 0; + /* Fall through to hash/vec if Search miss (sparse holes). */ + } + + if (ftl->vec.ready) { + ret = n31_vecmap_lookup(&ftl->vec, fmss_lba, &p_ord); + if (!ret) { + n31_ordinal_to_phys(p_ord, &ce, &cau, &blk, &pg, &sl); + n31_entry_from_phys(&e, fmss_lba, + n31_phys_pack(ce, cau, blk, pg, sl), + 0, 0x01); + have_hint = true; + } else if (ret == -EUCLEAN) { + return -EUCLEAN; + } + } + + if (!have_hint) { + ret = n31_map_lookup_hint(ftl, fmss_lba, &e); + if (ret) + ret = n31_neighbor_probe(ftl, fmss_lba, &e); + if (ret && allow_demand && ftl_demand_scan) { + ret = n31_demand_scan_for(ftl, fmss_lba); + if (!ret) + ret = n31_map_lookup_hint(ftl, fmss_lba, &e); + } + if (ret) + return -ENOENT; + } + + n31_entry_to_legacy(&e, &leg); + ce = leg.ce; + cau = leg.cau; + blk = leg.block; + pg = leg.page; + + page = kzalloc(sizeof(*page), GFP_KERNEL); + if (!page) + return -ENOMEM; + + ret = s5l8740_nand_cs_phys_read(ce, cau, blk, pg, page); + if (ret) { + ret = -EIO; + goto out; + } + + n31_map_ingest_page(ftl, ce, cau, blk, pg, page); + + slot = s5l8740_nand_meta_pick_lba(page, fmss_lba); + if (slot < 0) { + ret = -EUCLEAN; + goto out; + } + n31_phys_unpack(e.phys_key, &ce, &cau, &blk, &pg, &sl); + if ((u8)slot != sl) + dev_dbg(ftl->dev, + "lba=%u map_slot=%u meta_slot=%u\n", + fmss_lba, sl, slot); + + memcpy(dst, page->data[slot], N31_DATA_SLOT_SIZE); + ret = 0; +out: + kfree(page); + return ret; +} + +int n31_ftl_read_fmss_lba(struct n31_ftl_cs *ftl, u32 fmss_lba, void *dst) +{ + return n31_ftl_read_fmss_lba_flags(ftl, fmss_lba, dst, true); +} + +static int n31_ftl_read_disk_lba_flags(struct n31_ftl_cs *ftl, u32 disk_lba, + void *dst, bool allow_demand) +{ + u32 fmss_lba; + + if (!ftl || !dst) + return -EINVAL; + if (!ftl->fat_base_valid) + return -ENODEV; + if (disk_lba >= ftl->fat_total_sectors) + return -ERANGE; + + fmss_lba = ftl->fat_base_lba + disk_lba; + return n31_ftl_read_fmss_lba_flags(ftl, fmss_lba, dst, allow_demand); +} + +int n31_ftl_read_disk_lba(struct n31_ftl_cs *ftl, u32 disk_lba, void *dst) +{ + return n31_ftl_read_disk_lba_flags(ftl, disk_lba, dst, true); +} + +static int n31_ftl_apply_bpb(struct n31_ftl_cs *ftl, u32 fmss_lba, + u32 total, const u8 *sector) +{ + if (!sector) + return -EINVAL; + ftl->fat_base_lba = fmss_lba; + ftl->fat_total_sectors = total; + ftl->fat_base_valid = true; + memcpy(ftl->bpb_sector, sector, N31_DATA_SLOT_SIZE); + n31_parse_bpb(sector, &ftl->layout); + ftl->bpb_cached = true; + return 0; +} + +static int n31_ftl_find_bpb(struct n31_ftl_cs *ftl) +{ + unsigned int bkt; + struct n31_map_node *n; + u8 *buf; + int ret, sess; + unsigned int found = 0; + char cand_log[512]; + unsigned int cand_n = 0; + + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + kfree(buf); + return sess; + } + + mutex_lock(&ftl->lock); + ftl->bpb_ncand = 0; + ftl->fat_base_valid = false; + ftl->bpb_cached = false; + cand_log[0] = '\0'; + + hash_for_each(ftl->map, bkt, n, hnode) { + u32 total = 0; + unsigned int i; + + if (!n->e.valid) + continue; + if (n->e.type != S5L8740_NAND_META_TYPE_DATA && + n->e.type != S5L8740_NAND_META_TYPE_DATA2) + continue; + ret = n31_ftl_read_fmss_lba(ftl, n->e.fmss_lba, buf); + if (ret) + continue; + if (!n31_bpb_looks_valid(buf, &total)) + continue; + + /* Dedup same fmss_lba (keep higher weave). */ + for (i = 0; i < ftl->bpb_ncand; i++) { + if (ftl->bpb_candidates[i] != n->e.fmss_lba) + continue; + if (n31_weave_newer(n->e.weave, ftl->bpb_cand_weave[i])) { + ftl->bpb_cand_weave[i] = n->e.weave; + ftl->bpb_cand_total[i] = total; + memcpy(ftl->bpb_cand_oem[i], buf + 3, 8); + ftl->bpb_cand_oem[i][8] = '\0'; + memcpy(ftl->bpb_cand_sector[i], buf, + N31_DATA_SLOT_SIZE); + } + goto next; + } + if (ftl->bpb_ncand < N31_BPB_CANDIDATES_MAX) { + i = ftl->bpb_ncand++; + ftl->bpb_candidates[i] = n->e.fmss_lba; + ftl->bpb_cand_weave[i] = n->e.weave; + ftl->bpb_cand_total[i] = total; + memcpy(ftl->bpb_cand_oem[i], buf + 3, 8); + ftl->bpb_cand_oem[i][8] = '\0'; + memcpy(ftl->bpb_cand_sector[i], buf, N31_DATA_SLOT_SIZE); + } + found++; +next: + ; + } + + for (bkt = 0; bkt < ftl->bpb_ncand && cand_n < sizeof(cand_log) - 96; + bkt++) + cand_n += scnprintf(cand_log + cand_n, sizeof(cand_log) - cand_n, + " cand%u fmss=%u weave=%012llx oem='%.8s' " + "total=%u\n", + bkt + 1, ftl->bpb_candidates[bkt], + (unsigned long long)ftl->bpb_cand_weave[bkt], + ftl->bpb_cand_oem[bkt], + ftl->bpb_cand_total[bkt]); + + scnprintf(ftl->bpb_log, sizeof(ftl->bpb_log), + "bpb_scan candidates=%u\n%s", ftl->bpb_ncand, cand_log); + scnprintf(ftl->last_log, sizeof(ftl->last_log), "%s", ftl->bpb_log); + dev_info(ftl->dev, "%s", ftl->last_log); + mutex_unlock(&ftl->lock); + + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + return found || ftl->bpb_ncand ? 0 : -ENOENT; +} + +/* + * Newest weave first among candidates that pass FAT-critical. A slightly + * older *UOKJIHC volume that mounts beats a newer BPB with a broken FAT. + */ +static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl) +{ + unsigned int i, j; + int best = -1; + unsigned int best_ok = 0; + bool best_apple = false; + u64 best_weave = 0; + + if (!ftl->bpb_ncand) + return -ENOENT; + + /* Insertion-sort candidates by weave descending (n is tiny). */ + for (i = 1; i < ftl->bpb_ncand; i++) { + u32 fmss = ftl->bpb_candidates[i]; + u64 weave = ftl->bpb_cand_weave[i]; + u32 total = ftl->bpb_cand_total[i]; + char oem[9]; + u8 sector[N31_DATA_SLOT_SIZE]; + + memcpy(oem, ftl->bpb_cand_oem[i], 9); + memcpy(sector, ftl->bpb_cand_sector[i], N31_DATA_SLOT_SIZE); + j = i; + while (j > 0 && + n31_weave_newer(weave, ftl->bpb_cand_weave[j - 1])) { + ftl->bpb_candidates[j] = ftl->bpb_candidates[j - 1]; + ftl->bpb_cand_weave[j] = ftl->bpb_cand_weave[j - 1]; + ftl->bpb_cand_total[j] = ftl->bpb_cand_total[j - 1]; + memcpy(ftl->bpb_cand_oem[j], ftl->bpb_cand_oem[j - 1], + 9); + memcpy(ftl->bpb_cand_sector[j], + ftl->bpb_cand_sector[j - 1], + N31_DATA_SLOT_SIZE); + j--; + } + ftl->bpb_candidates[j] = fmss; + ftl->bpb_cand_weave[j] = weave; + ftl->bpb_cand_total[j] = total; + memcpy(ftl->bpb_cand_oem[j], oem, 9); + memcpy(ftl->bpb_cand_sector[j], sector, N31_DATA_SLOT_SIZE); + } + + for (i = 0; i < ftl->bpb_ncand; i++) { + bool apple; + int vret; + + n31_ftl_apply_bpb(ftl, ftl->bpb_candidates[i], + ftl->bpb_cand_total[i], + ftl->bpb_cand_sector[i]); + vret = n31_validate_fat_critical(ftl); + apple = !memcmp(ftl->bpb_cand_oem[i], "*UOKJIHC", 8); + + dev_info(ftl->dev, + "bpb_try fmss=%u weave=%012llx oem='%.8s' " + "crit=%u/%u ret=%d\n", + ftl->bpb_candidates[i], + (unsigned long long)ftl->bpb_cand_weave[i], + ftl->bpb_cand_oem[i], ftl->fat_crit_ok_n, + ftl->fat_crit_need_n, vret); + + /* Perfect critical set: newest weave wins immediately. */ + if (!vret && ftl->fat_crit_ok_n == ftl->fat_crit_need_n && + ftl->fat_crit_need_n > 0) { + best = i; + break; + } + + if (ftl->fat_crit_ok_n < 3) + continue; + if (best < 0 || ftl->fat_crit_ok_n > best_ok || + (ftl->fat_crit_ok_n == best_ok && apple && !best_apple) || + (ftl->fat_crit_ok_n == best_ok && apple == best_apple && + n31_weave_newer(ftl->bpb_cand_weave[i], best_weave))) { + best = i; + best_ok = ftl->fat_crit_ok_n; + best_apple = apple; + best_weave = ftl->bpb_cand_weave[i]; + } + } + + if (best < 0) + best = 0; + + n31_ftl_apply_bpb(ftl, ftl->bpb_candidates[best], + ftl->bpb_cand_total[best], + ftl->bpb_cand_sector[best]); + n31_validate_fat_critical(ftl); + + scnprintf(ftl->bpb_log, sizeof(ftl->bpb_log), + "fat_base_lba=%u valid=%d total=%u candidates=%u " + "oem='%.8s' weave=%012llx ext_flags=0x%04x " + "active_fat=%u mirror=%s selected=%u crit=%u/%u\n", + ftl->fat_base_lba, ftl->fat_base_valid, ftl->fat_total_sectors, + ftl->bpb_ncand, ftl->bpb_cand_oem[best], + (unsigned long long)ftl->bpb_cand_weave[best], + ftl->layout.ext_flags, ftl->layout.ext_flags & 0xF, + (ftl->layout.ext_flags & 0x80) ? "off" : "on", + best + 1, ftl->fat_crit_ok_n, ftl->fat_crit_need_n); + dev_info(ftl->dev, "%s", ftl->bpb_log); + return ftl->fat_critical_ok ? 0 : -EAGAIN; +} + +static int n31_read_disk_checked(struct n31_ftl_cs *ftl, u32 disk_lba, + u8 *buf, const char *tag) +{ + int ret = n31_ftl_read_disk_lba(ftl, disk_lba, buf); + + if (ret) + dev_warn_ratelimited(ftl->dev, + "FAT-critical %s disk_lba=%u fail %d\n", + tag, disk_lba, ret); + else + dev_dbg(ftl->dev, "FAT-critical %s disk_lba=%u OK\n", + tag, disk_lba); + return ret; +} + +/* + * Validate BPB, FSInfo, FAT, and root-directory sectors before registering + * the block device. + */ +static int n31_validate_fat_critical(struct n31_ftl_cs *ftl) +{ + u8 *buf; + u32 total = 0; + int ret, sess; + struct n31_fat_layout *L; + unsigned int ok = 0, need = 0; + + if (!ftl->fat_base_valid) + return -ENODEV; + + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + kfree(buf); + return sess; + } + + mutex_lock(&ftl->lock); + ftl->fat_critical_ok = false; + ftl->enable_gate_ok = false; + ftl->disk0_ok = false; + + ret = n31_ftl_read_disk_lba(ftl, 0, buf); + ftl->last_ret = ret; + ftl->last_disk_lba = 0; + ftl->last_fmss_lba = ftl->fat_base_lba; + if (ret || !n31_bpb_looks_valid(buf, &total)) { + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "disk_lba0 BPB fail ret=%d\n", ret); + goto out; + } + memcpy(ftl->last_sector, buf, N31_DATA_SLOT_SIZE); + memcpy(ftl->bpb_sector, buf, N31_DATA_SLOT_SIZE); + ftl->bpb_cached = true; + ftl->disk0_ok = true; + ftl->fat_total_sectors = total; + n31_parse_bpb(buf, &ftl->layout); + L = &ftl->layout; + + scnprintf(ftl->layout_log, sizeof(ftl->layout_log), + "bps=%u spc=%u reserved=%u fats=%u fat_size32=%u " + "root_cluster=%u fsinfo=%u backup=%u total=%u " + "ext_flags=0x%04x active_fat=%u mirror=%s\n" + "fat_start=%u fat1_start=%u data_start=%u root_dir_lba=%u " + "fsinfo_lba=%u backup_boot_lba=%u\n", + L->bytes_per_sector, L->sectors_per_cluster, + L->reserved_sectors, L->num_fats, L->fat_size_32, + L->root_cluster, L->fsinfo_sector, L->backup_boot_sector, + L->total_sectors, L->ext_flags, L->ext_flags & 0xF, + (L->ext_flags & 0x80) ? "off" : "on", + L->fat_start, + L->fat_start + L->fat_size_32, + L->data_start, L->root_dir_lba, L->fsinfo_lba, + L->backup_boot_lba); + scnprintf(ftl->bpb_log, sizeof(ftl->bpb_log), + "oem='%.8s' jump=%02x%02x%02x %s", + buf + 3, buf[0], buf[1], buf[2], ftl->layout_log); + + need = 0; + ok = 0; +#define CRIT(lba, tag) do { \ + need++; \ + if ((lba) < ftl->fat_total_sectors && \ + !n31_read_disk_checked(ftl, (lba), buf, (tag))) \ + ok++; \ +} while (0) + + CRIT(0, "BPB"); + if (L->fsinfo_lba) + CRIT(L->fsinfo_lba, "FSInfo"); + if (L->backup_boot_lba && L->backup_boot_lba != L->fsinfo_lba) + CRIT(L->backup_boot_lba, "backup_BPB"); + CRIT(L->fat_start, "FAT0"); + if (L->fat_start + 1 < ftl->fat_total_sectors) + CRIT(L->fat_start + 1, "FAT1"); + CRIT(L->data_start, "root0"); + if (L->sectors_per_cluster >= 2) + CRIT(L->data_start + 1, "root1"); + if (L->sectors_per_cluster >= 3) + CRIT(L->data_start + 2, "root2"); + if (L->sectors_per_cluster >= 4) + CRIT(L->data_start + 3, "root3"); +#undef CRIT + + /* Require BPB + FAT0 + root0 at minimum; prefer full set. */ + ftl->fat_critical_ok = (ok >= 3 && ftl->disk0_ok && + (ftl->whimory_backed || + n31_map_find(ftl, ftl->fat_base_lba))); + ftl->enable_gate_ok = ftl->fat_critical_ok; + ftl->fat_crit_ok_n = ok; + ftl->fat_crit_need_n = need; + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "fat_critical ok=%u/%u gate=%d fat_base=%u\n%s", + ok, need, ftl->enable_gate_ok, ftl->fat_base_lba, + ftl->layout_log); + dev_info(ftl->dev, "%s", ftl->last_log); + ret = ftl->fat_critical_ok ? 0 : -EAGAIN; +out: + mutex_unlock(&ftl->lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + return ret; +} + +static int n31_read_disk_range(struct n31_ftl_cs *ftl, u32 start, u32 count) +{ + u8 *buf; + u32 i; + int ret, sess; + u32 prev_phys = N31_PHYS_KEY_INVALID; + + if (!count) + return -EINVAL; + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + kfree(buf); + return sess; + } + + mutex_lock(&ftl->lock); + ftl->range_ok = 0; + ftl->range_fail = 0; + ftl->range_miss = 0; + for (i = 0; i < count; i++) { + u32 disk_lba = start + i; + u32 fmss_lba; + struct n31_map_entry e; + + if (!ftl->fat_base_valid) { + ftl->range_fail++; + break; + } + fmss_lba = ftl->fat_base_lba + disk_lba; + if (n31_map_lookup_hint(ftl, fmss_lba, &e)) + ftl->range_miss++; + ret = n31_ftl_read_disk_lba(ftl, disk_lba, buf); + if (ret) { + ftl->range_fail++; + dev_dbg(ftl->dev, + "range miss disk_lba=%u ret=%d\n", + disk_lba, ret); + continue; + } + ftl->range_ok++; + if (!n31_map_lookup_hint(ftl, fmss_lba, &e)) { + if (prev_phys != N31_PHYS_KEY_INVALID && + e.phys_key != n31_phys_succ(prev_phys) && + e.phys_key != prev_phys) + dev_dbg(ftl->dev, + "range gap disk_lba=%u phys=%08x prev=%08x\n", + disk_lba, e.phys_key, prev_phys); + prev_phys = e.phys_key; + } + } + scnprintf(ftl->range_log, sizeof(ftl->range_log), + "range start=%u count=%u ok=%u fail=%u pre_miss=%u " + "demand_scans=%u\n", + start, count, ftl->range_ok, ftl->range_fail, + ftl->range_miss, ftl->demand_scans); + scnprintf(ftl->last_log, sizeof(ftl->last_log), "%s", ftl->range_log); + dev_dbg(ftl->dev, "%s", ftl->last_log); + mutex_unlock(&ftl->lock); + + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + return ftl->range_fail ? -EIO : 0; +} + +/* -------------------- block device -------------------- */ + +static bool n31_looks_like_firmware(const u8 *d) +{ + u32 total; + + if (!d) + return false; + /* Classic Apple IMG1 / 8900 header */ + if (d[0] == '8' && d[1] == '9' && d[2] == '0' && d[3] == '0') + return true; + if (d[0] == 'I' && d[1] == 'm' && d[2] == 'g') + return true; + /* WinPod-style volume marker */ + if (!memcmp(d + 0x100, "[hi]", 4)) + return true; + /* Nested FAT inside the pre-user range */ + if (n31_bpb_looks_valid(d, &total) && total > 0) + return true; + return false; +} + +static void n31_firmware_probe(struct n31_ftl_cs *ftl) +{ + unsigned int bkt; + struct n31_map_node *n; + u32 min_l = ~0u, max_l = 0; + unsigned int mapped = 0, magic = 0, sampled = 0; + u8 *buf; + + ftl->fw_valid = false; + ftl->fw_mapped = 0; + ftl->fw_magic_hits = 0; + ftl->fw_base_fmss = 0; + ftl->fw_nsectors = 0; + ftl->fw_log[0] = '\0'; + + if (!ftl->fat_base_valid || !ftl->fat_base_lba) + return; + + hash_for_each(ftl->map, bkt, n, hnode) { + u32 l = n->e.fmss_lba; + + if (l >= ftl->fat_base_lba) + continue; + mapped++; + if (l < min_l) + min_l = l; + if (l > max_l) + max_l = l; + } + ftl->fw_mapped = mapped; + + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (buf) { + hash_for_each(ftl->map, bkt, n, hnode) { + u32 l = n->e.fmss_lba; + + if (l >= ftl->fat_base_lba) + continue; + if (sampled >= 24) + break; + sampled++; + if (!n31_ftl_read_fmss_lba_flags(ftl, l, buf, false) && + n31_looks_like_firmware(buf)) + magic++; + } + kfree(buf); + } + ftl->fw_magic_hits = magic; + + if (fw_start_lba >= 0) + ftl->fw_base_fmss = (u32)fw_start_lba; + else + ftl->fw_base_fmss = 0; + + if (fw_nsectors > 0) + ftl->fw_nsectors = (u32)fw_nsectors; + else if (ftl->fat_base_lba > ftl->fw_base_fmss) + ftl->fw_nsectors = ftl->fat_base_lba - ftl->fw_base_fmss; + else + ftl->fw_nsectors = 0; + + ftl->fw_valid = fw_force || magic > 0 || mapped >= 4; + if (!ftl->fw_nsectors) + ftl->fw_valid = false; + + scnprintf(ftl->fw_log, sizeof(ftl->fw_log), + "fw_valid=%d base=%u nsectors=%u mapped=%u magic=%u " + "min_lba=%u max_lba=%u force=%d\n", + ftl->fw_valid, ftl->fw_base_fmss, ftl->fw_nsectors, + mapped, magic, + mapped ? min_l : 0, mapped ? max_l : 0, fw_force); + dev_info(ftl->dev, "%s", ftl->fw_log); +} + +static void n31_ftl_submit_bio(struct bio *bio) +{ + struct n31_ftl_slice *sl = bio->bi_bdev->bd_disk->private_data; + struct n31_ftl_cs *ftl; + struct bvec_iter iter; + struct bio_vec bvec; + sector_t sector = bio->bi_iter.bi_sector; + int ret = 0; + + if (!sl || !sl->ftl || !sl->nsectors) { + bio_io_error(bio); + return; + } + ftl = sl->ftl; + if (sl->kind != N31_SLICE_FIRMWARE && !ftl->enable_gate_ok) { + bio_io_error(bio); + return; + } + if (op_is_write(bio_op(bio)) || bio_op(bio) == REQ_OP_DISCARD || + bio_op(bio) == REQ_OP_WRITE_ZEROES) { + bio->bi_status = BLK_STS_IOERR; + bio_endio(bio); + return; + } + if ((sector & 7) != 0) { + bio_io_error(bio); + return; + } + + bio_for_each_segment(bvec, bio, iter) { + u8 *dst = kmap_local_page(bvec.bv_page) + bvec.bv_offset; + unsigned int done = 0; + + while (done < bvec.bv_len) { + u32 off = (u32)(sector >> 3); + u32 fmss_lba; + unsigned int n = min_t(unsigned int, + bvec.bv_len - done, + N31_DATA_SLOT_SIZE); + + if (off >= sl->nsectors) { + ret = -ERANGE; + kunmap_local(dst); + goto done; + } + fmss_lba = sl->base_fmss + off; + mutex_lock(&ftl->lock); + ret = n31_ftl_read_fmss_lba_flags(ftl, fmss_lba, + ftl->bounce, false); + if (!ret) + memcpy(dst + done, ftl->bounce, n); + else + dev_err_ratelimited(ftl->dev, + "read miss %s fmss_lba=%u ret=%d\n", + sl->gd ? sl->gd->disk_name : "?", + fmss_lba, ret); + mutex_unlock(&ftl->lock); + if (ret) { + kunmap_local(dst); + goto done; + } + done += n; + sector += n / 512; + } + kunmap_local(dst); + } +done: + if (ret) + bio_io_error(bio); + else + bio_endio(bio); +} + +static const struct block_device_operations n31_ftl_bd_ops = { + .owner = THIS_MODULE, + .submit_bio = n31_ftl_submit_bio, +}; + +static int n31_slice_register(struct n31_ftl_slice *sl, const char *name, + u32 base_fmss, u32 nsectors, + enum n31_slice_kind kind) +{ + struct queue_limits lim = { + .logical_block_size = N31_DATA_SLOT_SIZE, + .physical_block_size = N31_DATA_SLOT_SIZE, + .io_min = N31_DATA_SLOT_SIZE, + }; + struct gendisk *gd; + int ret; + + if (!sl || !sl->ftl || !nsectors) + return -EINVAL; + if (sl->gd) + return 0; + + gd = blk_alloc_disk(&lim, NUMA_NO_NODE); + if (IS_ERR(gd)) + return PTR_ERR(gd); + + sl->base_fmss = base_fmss; + sl->nsectors = nsectors; + sl->kind = kind; + sl->gd = gd; + + gd->first_minor = 0; + gd->flags = GENHD_FL_NO_PART; + gd->fops = &n31_ftl_bd_ops; + gd->private_data = sl; + snprintf(gd->disk_name, DISK_NAME_LEN, "%s", name); + set_capacity(gd, (sector_t)nsectors * 8); + set_disk_ro(gd, 1); + ret = add_disk(gd); + if (ret) { + put_disk(gd); + sl->gd = NULL; + return ret; + } + dev_info(sl->ftl->dev, + "/dev/%s read-only, %u × 4096-byte sectors, base_fmss=%u\n", + name, nsectors, base_fmss); + return 0; +} + +static void n31_slice_unregister(struct n31_ftl_slice *sl) +{ + if (!sl || !sl->gd) + return; + del_gendisk(sl->gd); + put_disk(sl->gd); + sl->gd = NULL; + sl->nsectors = 0; +} + +static void n31_ftl_unregister_disk(struct n31_ftl_cs *ftl); + +static int n31_ftl_register_disk(struct n31_ftl_cs *ftl) +{ + int ret; + + if (!ftl_block_enable && !ftl->block_enable) + return -EPERM; + if (!ftl->enable_gate_ok || !ftl->fat_critical_ok) + return -EAGAIN; + if (ftl->ipod.gd) + return 0; + + if (!ftl->dma_session_held) { + ret = s5l8740_nand_dma_session_begin(); + if (ret && ret != -EBUSY) + return ret; + ftl->dma_session_held = (ret == 0); + } + + ftl->ipod.ftl = ftl; + ftl->ftl_alias.ftl = ftl; + ftl->firmware.ftl = ftl; + + n31_firmware_probe(ftl); + + ret = n31_slice_register(&ftl->ipod, N31_IPOD_DISK_NAME, + ftl->fat_base_lba, ftl->fat_total_sectors, + N31_SLICE_IPOD); + if (ret) + goto fail; + + ret = n31_slice_register(&ftl->ftl_alias, N31_FTL_DISK_NAME, + ftl->fat_base_lba, ftl->fat_total_sectors, + N31_SLICE_FTL_ALIAS); + if (ret) + goto fail; + + if (ftl->fw_valid) { + ret = n31_slice_register(&ftl->firmware, N31_FW_DISK_NAME, + ftl->fw_base_fmss, ftl->fw_nsectors, + N31_SLICE_FIRMWARE); + if (ret) + dev_warn(ftl->dev, + "firmware disk register failed %d\n", ret); + } + return 0; + +fail: + n31_ftl_unregister_disk(ftl); + return ret; +} + +static void n31_ftl_unregister_disk(struct n31_ftl_cs *ftl) +{ + if (!ftl) + return; + n31_slice_unregister(&ftl->firmware); + n31_slice_unregister(&ftl->ftl_alias); + n31_slice_unregister(&ftl->ipod); + if (ftl->dma_session_held) { + s5l8740_nand_dma_session_end(); + ftl->dma_session_held = false; + } +} + +/* -------------------- sysfs -------------------- */ + +static ssize_t ftl_map_stats_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, + "built=%d entries=%u pages=%u valid_records=%u " + "lba_min=%u lba_max=%u extents=%u largest=%u " + "duplicates=%u newer=%u has_49279=%d " + "demand_scans=%u\n%s", + ftl->map_built, ftl->map_entries, ftl->map_pages, + ftl->map_data_recs, + ftl->lba_min == ~0u ? 0 : ftl->lba_min, ftl->lba_max, + ftl->extent_count, ftl->extent_largest, + ftl->map_collisions, ftl->newer_replacements, + n31_map_find(ftl, N31_FAT_BASE_DEFAULT) ? 1 : 0, + ftl->demand_scans, ftl->last_log); +} +static DEVICE_ATTR_RO(ftl_map_stats); + +static ssize_t ftl_extents_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int i, n = 0; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + n = sysfs_emit(buf, "%s", ftl->extents_log); + for (i = 0; i < ftl->extent_count && n < PAGE_SIZE - 80; i++) { + struct n31_lba_extent *ex = &ftl->extents[i]; + + n += scnprintf(buf + n, PAGE_SIZE - n, + "ex%u start_lba=%u len=%u phys=%08x type=%02x\n", + i, ex->start_lba, ex->len, ex->start_phys_key, + ex->type); + } + return n; +} +static DEVICE_ATTR_RO(ftl_extents); + +/* echo "CE CAU BLK_LO BLK_HI" > ftl_scan_block_window */ +static ssize_t ftl_scan_block_window_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int ce, cau, lo, hi; + int nf, sess, ret; + + if (!ftl) + return -ENODEV; + nf = sscanf(buf, "%u %u %u %u", &ce, &cau, &lo, &hi); + if (nf < 4) + return -EINVAL; + if (hi < lo) + return -EINVAL; + if (hi - lo > 32) + return -E2BIG; /* glass safety */ + + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) + return sess; + mutex_lock(&ftl->lock); + ret = n31_scan_block_window(ftl, ce, cau, lo, hi, true, true); + mutex_unlock(&ftl->lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_scan_block_window); + +static ssize_t ftl_map_build_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int stage = 1; + int sess, ret; + u16 lo, hi; + + if (!ftl) + return -ENODEV; + if (sscanf(buf, "%u", &stage) < 1) + stage = 1; + /* stage1 = blk63; stage2 = 62..66; all CE/CAU. stage3 refused. */ + if (stage >= 3) + return -EPERM; + lo = (stage <= 1) ? 63 : 62; + hi = (stage <= 1) ? 63 : 66; + + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) + return sess; + mutex_lock(&ftl->lock); + ret = n31_scan_banks_window(ftl, lo, hi, true); + mutex_unlock(&ftl->lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_map_build); + +static ssize_t ftl_fat_base_lba_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, + "fat_base_lba=%u valid=%d total=%u disk0_ok=%d " + "fat_critical_ok=%d gate_ok=%d\n", + ftl->fat_base_lba, ftl->fat_base_valid, + ftl->fat_total_sectors, ftl->disk0_ok, + ftl->fat_critical_ok, ftl->enable_gate_ok); +} + +static ssize_t ftl_fat_base_lba_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + u32 v; + + if (!ftl || kstrtou32(buf, 0, &v)) + return -EINVAL; + mutex_lock(&ftl->lock); + ftl->fat_base_lba = v; + ftl->fat_base_valid = true; + ftl->fat_base_autodetect = false; + ftl->disk0_ok = false; + ftl->fat_critical_ok = false; + ftl->enable_gate_ok = false; + mutex_unlock(&ftl->lock); + return count; +} +static DEVICE_ATTR_RW(ftl_fat_base_lba); + +static ssize_t ftl_bpb_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", ftl->bpb_log[0] ? ftl->bpb_log : "none\n"); +} +static DEVICE_ATTR_RO(ftl_bpb); + +static ssize_t ftl_layout_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", + ftl->layout_log[0] ? ftl->layout_log : "none\n"); +} +static DEVICE_ATTR_RO(ftl_layout); + +static ssize_t ftl_find_bpb_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + int ret; + + if (!ftl) + return -ENODEV; + ftl->fat_base_autodetect = true; + ret = n31_ftl_find_bpb(ftl); + if (!ret) + ret = n31_ftl_select_bpb(ftl); + if (!ret && ftl_block_enable) { + ftl->block_enable = true; + ret = n31_ftl_register_disk(ftl); + } + return (ret && ret != -ENOENT && ret != -EAGAIN) ? ret : count; +} +static DEVICE_ATTR_WO(ftl_find_bpb); + +static ssize_t ftl_read_last_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + size_t n = N31_DATA_SLOT_SIZE; + + if (!ftl) + return -ENODEV; + if (n > PAGE_SIZE) + n = PAGE_SIZE; + memcpy(buf, ftl->last_sector, n); + return n; +} +static DEVICE_ATTR_RO(ftl_read_last); + +static ssize_t ftl_read_fmss_lba_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + u32 fmss_lba; + int ret, sess; + + if (!ftl || kstrtou32(buf, 0, &fmss_lba)) + return -EINVAL; + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) + return sess; + mutex_lock(&ftl->lock); + ret = n31_ftl_read_fmss_lba(ftl, fmss_lba, ftl->last_sector); + ftl->last_ret = ret; + ftl->last_fmss_lba = fmss_lba; + ftl->last_disk_lba = ~0u; + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "ftl_read_fmss_lba=%u ret=%d first8=%*ph\n", + fmss_lba, ret, 8, ftl->last_sector); + dev_dbg(ftl->dev, "%s", ftl->last_log); + mutex_unlock(&ftl->lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_read_fmss_lba); + +static ssize_t ftl_read_disk_lba_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + u32 disk_lba; + int ret, sess; + + if (!ftl || kstrtou32(buf, 0, &disk_lba)) + return -EINVAL; + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) + return sess; + mutex_lock(&ftl->lock); + ret = n31_ftl_read_disk_lba(ftl, disk_lba, ftl->last_sector); + ftl->last_ret = ret; + ftl->last_disk_lba = disk_lba; + ftl->last_fmss_lba = ftl->fat_base_valid ? + ftl->fat_base_lba + disk_lba : 0; + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "ftl_read_disk_lba=%u fmss_lba=%u ret=%d first8=%*ph\n", + disk_lba, ftl->last_fmss_lba, ret, 8, ftl->last_sector); + dev_dbg(ftl->dev, "%s", ftl->last_log); + mutex_unlock(&ftl->lock); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_read_disk_lba); + +/* echo "START COUNT" > ftl_read_disk_range */ +static ssize_t ftl_read_disk_range_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int start, n; + int ret; + + if (!ftl) + return -ENODEV; + if (sscanf(buf, "%u %u", &start, &n) < 2) + return -EINVAL; + if (n > 512) + return -E2BIG; + ret = n31_read_disk_range(ftl, start, n); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_read_disk_range); + +static ssize_t ftl_read_range_stats_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", + ftl->range_log[0] ? ftl->range_log : "none\n"); +} +static DEVICE_ATTR_RO(ftl_read_range_stats); + +static ssize_t ftl_enable_block_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int v = 0; + int ret; + + if (!ftl || kstrtouint(buf, 0, &v)) + return -EINVAL; + if (!v) { + n31_ftl_unregister_disk(ftl); + ftl->block_enable = false; + return count; + } + ret = n31_validate_fat_critical(ftl); + if (ret) + return ret; + ftl->block_enable = true; + ret = n31_ftl_register_disk(ftl); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_enable_block); + +static ssize_t ftl_vec_stats_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + struct n31_vecmap *v; + u32 p = N31_INVALID_P; + int cross = -ENOENT; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + v = &ftl->vec; + if (v->ready) + cross = n31_vecmap_lookup(v, N31_FAT_BASE_DEFAULT, &p); + return sysfs_emit(buf, + "%s" + "cross_49279_ret=%d p=%u\n", + ftl->vec_log[0] ? ftl->vec_log : "ready=0\n", + cross, p); +} +static DEVICE_ATTR_RO(ftl_vec_stats); + +/* echo 1 > ftl_vec_build — recompress from current sparse hash */ +static ssize_t ftl_vec_build_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + int ret; + + if (!ftl) + return -ENODEV; + mutex_lock(&ftl->lock); + ret = n31_vecmap_rebuild_from_hash(ftl); + mutex_unlock(&ftl->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_vec_build); + +static ssize_t ftl_firmware_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", + ftl->fw_log[0] ? ftl->fw_log : "fw_valid=0\n"); +} +static DEVICE_ATTR_RO(ftl_firmware); + +struct n31_bpb_walk_ctx { + struct n31_ftl_cs *ftl; + u8 *buf; + unsigned int tried; + unsigned int max_try; +}; + +static void n31_ftl_note_bpb_cand(struct n31_ftl_cs *ftl, u32 fmss_lba, + u64 weave, const u8 *buf, u32 total) +{ + unsigned int i; + + for (i = 0; i < ftl->bpb_ncand; i++) { + if (ftl->bpb_candidates[i] != fmss_lba) + continue; + if (n31_weave_newer(weave, ftl->bpb_cand_weave[i])) { + ftl->bpb_cand_weave[i] = weave; + ftl->bpb_cand_total[i] = total; + memcpy(ftl->bpb_cand_oem[i], buf + 3, 8); + ftl->bpb_cand_oem[i][8] = '\0'; + memcpy(ftl->bpb_cand_sector[i], buf, + N31_DATA_SLOT_SIZE); + } + return; + } + if (ftl->bpb_ncand >= N31_BPB_CANDIDATES_MAX) + return; + i = ftl->bpb_ncand++; + ftl->bpb_candidates[i] = fmss_lba; + ftl->bpb_cand_weave[i] = weave; + ftl->bpb_cand_total[i] = total; + memcpy(ftl->bpb_cand_oem[i], buf + 3, 8); + ftl->bpb_cand_oem[i][8] = '\0'; + memcpy(ftl->bpb_cand_sector[i], buf, N31_DATA_SLOT_SIZE); +} + +static int n31_bpb_walk_fn(u32 start, u32 len, u32 vba, u64 weave, void *opaque) +{ + struct n31_bpb_walk_ctx *c = opaque; + u32 try_lba[2]; + unsigned int ntry = 0, i; + u32 total = 0; + int ret; + + (void)vba; + if (!c || !c->ftl || !c->buf || !len) + return 0; + if (c->tried >= c->max_try) + return 1; /* stop walk */ + + try_lba[ntry++] = start; + if (start < N31_FAT_BASE_DEFAULT && + start + len > N31_FAT_BASE_DEFAULT) + try_lba[ntry++] = N31_FAT_BASE_DEFAULT; + + for (i = 0; i < ntry; i++) { + if (c->tried >= c->max_try) + break; + c->tried++; + ret = whimory_read_fmss_lba(try_lba[i], c->buf); + if (ret) + continue; + if (!n31_bpb_looks_valid(c->buf, &total)) + continue; + mutex_lock(&c->ftl->lock); + n31_ftl_note_bpb_cand(c->ftl, try_lba[i], weave, c->buf, + total); + mutex_unlock(&c->ftl->lock); + } + return 0; +} + +bool n31_ftl_cs_whimory_backed(void) +{ + return n31_ftl && n31_ftl->whimory_backed; +} + +int n31_ftl_cs_bind_whimory(void) +{ + struct n31_ftl_cs *ftl = n31_ftl; + struct n31_bpb_walk_ctx ctx; + u8 *buf; + u8 ce, cau, page, slot; + u16 blk; + u64 weave = 0; + u32 total = 0; + int ret, sess; + static const u32 probes[] = { + N31_FAT_BASE_DEFAULT, 49279u, 49285u, 0u + }; + unsigned int i; + + if (!ftl) + return -ENODEV; + if (!whimory_l2v_ready()) + return -ENODEV; + + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + sess = s5l8740_nand_dma_session_begin(); + mutex_lock(&ftl->lock); + ftl->whimory_backed = true; + ftl->map_built = true; + ftl->bpb_ncand = 0; + ftl->fat_base_valid = false; + mutex_unlock(&ftl->lock); + + /* Prefer known BPB LBA probes via L2V_Search. */ + for (i = 0; i < ARRAY_SIZE(probes); i++) { + ret = whimory_read_fmss_lba(probes[i], buf); + if (ret) + continue; + if (!n31_bpb_looks_valid(buf, &total)) + continue; + weave = 0; + whimory_l2v_search_phys(probes[i], &ce, &cau, &blk, &page, + &slot, &weave); + mutex_lock(&ftl->lock); + n31_ftl_note_bpb_cand(ftl, probes[i], weave, buf, total); + mutex_unlock(&ftl->lock); + } + + ctx.ftl = ftl; + ctx.buf = buf; + ctx.tried = 0; + ctx.max_try = 512; + whimory_range_walk(n31_bpb_walk_fn, &ctx); + + mutex_lock(&ftl->lock); + if (!ftl->bpb_ncand) { + dev_err(ftl->dev, + "whimory bind: no BPB found in L2V ranges\n"); + ftl->whimory_backed = false; + mutex_unlock(&ftl->lock); + ret = -ENOENT; + goto out_sess; + } + /* Snapshot ncand; select_bpb/validate take the lock themselves. */ + mutex_unlock(&ftl->lock); + + ret = n31_ftl_select_bpb(ftl); + if (ret) { + mutex_lock(&ftl->lock); + ftl->whimory_backed = false; + mutex_unlock(&ftl->lock); + goto out_sess; + } + + /* Self-check: Search(fat_base) must resolve. */ + ret = whimory_l2v_search_phys(ftl->fat_base_lba, &ce, &cau, &blk, + &page, &slot, &weave); + dev_info(ftl->dev, + "whimory_bind fat_base=%u search=%d phys=%u/%u/%u/%u/%u " + "weave=%012llx cand=%u\n", + ftl->fat_base_lba, ret, ce, cau, blk, page, slot, + (unsigned long long)weave, ftl->bpb_ncand); + if (ret) { + mutex_lock(&ftl->lock); + ftl->whimory_backed = false; + mutex_unlock(&ftl->lock); + goto out_sess; + } + + if (ftl->enable_gate_ok && ftl_block_enable) { + mutex_lock(&ftl->lock); + n31_ftl_unregister_disk(ftl); + ret = n31_ftl_register_disk(ftl); + mutex_unlock(&ftl->lock); + } else { + ret = ftl->enable_gate_ok ? 0 : -EAGAIN; + } + + scnprintf(ftl->last_log, sizeof(ftl->last_log), + "whimory_bind ok backed=%d fat_base=%u gate=%d disk=%d\n", + ftl->whimory_backed, ftl->fat_base_lba, ftl->enable_gate_ok, + ret); + dev_info(ftl->dev, "%s", ftl->last_log); +out_sess: + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + return ret; +} + +/* echo 1 > ftl_sftl_recover — CXT→BTOC→L2V on CS META, then bind disks */ +static ssize_t ftl_sftl_recover_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int v = 1; + int ret; + + (void)dev; + (void)attr; + if (sscanf(buf, "%u", &v) >= 1 && !v) + return count; + + ret = whimory_sftl_recover_cs(); + if (ret) + return ret; + ret = n31_ftl_cs_bind_whimory(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_sftl_recover); + +static ssize_t ftl_finishline_status_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, + "map_built=%d whimory_backed=%d entries=%u extents=%u " + "vec_ready=%d fat_base_lba=%u valid=%d disk0_ok=%d " + "fat_critical_ok=%d gate_ok=%d " + "ipod=%s ftl_alias=%s firmware=%s\n" + "last_ret=%d last_fmss=%u last_disk=%u\n%s%s%s", + ftl->map_built, ftl->whimory_backed ? 1 : 0, + ftl->map_entries, ftl->extent_count, + ftl->vec.ready ? 1 : 0, + ftl->fat_base_lba, ftl->fat_base_valid, ftl->disk0_ok, + ftl->fat_critical_ok, ftl->enable_gate_ok, + ftl->ipod.gd ? ftl->ipod.gd->disk_name : "(none)", + ftl->ftl_alias.gd ? ftl->ftl_alias.gd->disk_name : + "(none)", + ftl->firmware.gd ? ftl->firmware.gd->disk_name : + "(none)", + ftl->last_ret, ftl->last_fmss_lba, ftl->last_disk_lba, + ftl->last_log, + ftl->vec_log, + ftl->fw_log); +} +static DEVICE_ATTR_RO(ftl_finishline_status); + +static struct attribute *n31_ftl_finish_attrs[] = { + &dev_attr_ftl_sftl_recover.attr, + &dev_attr_ftl_map_build.attr, + &dev_attr_ftl_scan_block_window.attr, + &dev_attr_ftl_map_stats.attr, + &dev_attr_ftl_extents.attr, + &dev_attr_ftl_vec_stats.attr, + &dev_attr_ftl_vec_build.attr, + &dev_attr_ftl_firmware.attr, + &dev_attr_ftl_find_bpb.attr, + &dev_attr_ftl_fat_base_lba.attr, + &dev_attr_ftl_bpb.attr, + &dev_attr_ftl_layout.attr, + &dev_attr_ftl_read_fmss_lba.attr, + &dev_attr_ftl_read_disk_lba.attr, + &dev_attr_ftl_read_disk_range.attr, + &dev_attr_ftl_read_range_stats.attr, + &dev_attr_ftl_read_last.attr, + &dev_attr_ftl_enable_block.attr, + &dev_attr_ftl_finishline_status.attr, + NULL, +}; + +static const struct attribute_group n31_ftl_finish_group = { + .attrs = n31_ftl_finish_attrs, +}; + +int ftl_s5l8740_csmap_init(struct device *dev) +{ + struct n31_ftl_cs *ftl; + int ret; + + if (!dev) + return -EINVAL; + if (n31_ftl) + return -EBUSY; + + ftl = kzalloc(sizeof(*ftl), GFP_KERNEL); + if (!ftl) + return -ENOMEM; + ftl->bounce = kzalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!ftl->bounce) { + kfree(ftl); + return -ENOMEM; + } + ftl->dev = dev; + mutex_init(&ftl->lock); + hash_init(ftl->map); + ftl->lba_min = ~0u; + ftl->fat_base_lba = N31_FAT_BASE_DEFAULT; + ftl->fat_total_sectors = N31_FAT_TOTAL_DEFAULT; + ftl->fat_base_autodetect = true; + + ret = sysfs_create_group(&dev->kobj, &n31_ftl_finish_group); + if (ret) { + kfree(ftl->bounce); + kfree(ftl); + return ret; + } + n31_ftl = ftl; + dev_info(dev, + "CS map ready (scan → find_bpb → /dev/%s + /dev/%s)\n", + N31_IPOD_DISK_NAME, N31_FW_DISK_NAME); + return 0; +} + +void ftl_s5l8740_csmap_exit(struct device *dev) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return; + n31_ftl_unregister_disk(ftl); + if (dev) + sysfs_remove_group(&dev->kobj, &n31_ftl_finish_group); + mutex_lock(&ftl->lock); + n31_map_free(ftl); + mutex_unlock(&ftl->lock); + kfree(ftl->bounce); + kfree(ftl); + n31_ftl = NULL; +} diff --git a/drivers/misc/ftl-s5l8740-csmap.h b/drivers/misc/ftl-s5l8740-csmap.h new file mode 100755 index 00000000000000..f676e4067f4fdc --- /dev/null +++ b/drivers/misc/ftl-s5l8740-csmap.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef FTL_S5L8740_CSMAP_H +#define FTL_S5L8740_CSMAP_H + +#include +#include + +#define N31_FTL_DISK_NAME "s5l8740-ftl" /* FAT alias (compat) */ +#define N31_IPOD_DISK_NAME "s5l8740-ipod" /* user FAT volume */ +#define N31_FW_DISK_NAME "s5l8740-firmware" + +struct n31_lba_map_entry { + u8 ce; + u8 cau; + u16 block; + u8 page; + u8 slot; + u8 type; + u64 weave; + u32 fmss_lba; + bool present; +}; + +struct n31_ftl_cs; + +int ftl_s5l8740_csmap_init(struct device *dev); +void ftl_s5l8740_csmap_exit(struct device *dev); + +int n31_ftl_read_fmss_lba(struct n31_ftl_cs *ftl, u32 fmss_lba, void *dst); +int n31_ftl_read_disk_lba(struct n31_ftl_cs *ftl, u32 disk_lba, void *dst); + +/* + * After Whimory CXT→BTOC→L2V recover: bind csmap disks to L2V_Search + * (no full hash import — avoids multi-million node RAM). + */ +int n31_ftl_cs_bind_whimory(void); +bool n31_ftl_cs_whimory_backed(void); + +/* Implemented in ftl-s5l8740-core.c (same module). */ +int whimory_sftl_recover_cs(void); +bool whimory_l2v_ready(void); +int whimory_read_fmss_lba(u32 lba, void *buf); +int whimory_range_walk(int (*fn)(u32 start, u32 len, u32 vba, u64 weave, + void *ctx), + void *ctx); +int whimory_l2v_search_phys(u32 lba, u8 *ce, u8 *cau, u16 *blk, u8 *page, + u8 *slot, u64 *weave); + +#endif /* FTL_S5L8740_CSMAP_H */ diff --git a/drivers/misc/ftl-s5l8740-vecmap.c b/drivers/misc/ftl-s5l8740-vecmap.c new file mode 100755 index 00000000000000..21119376e7221e --- /dev/null +++ b/drivers/misc/ftl-s5l8740-vecmap.c @@ -0,0 +1,321 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Dual vector LBA maps for S5L8740 FTL. + * + * L2V maps logical LBA → physical record ordinal; V2L maps the reverse. + * Each axis uses a u32 group anchor (256 entries) plus an s8 residual. + * Sentinels: 127 = missing, -128 = sparse escape. Callers must still + * validate on-media metadata after a successful cross-check. + */ +#include +#include +#include +#include + +#include "ftl-s5l8740-vecmap.h" + +void n31_vecmap_init(struct n31_vecmap *v) +{ + memset(v, 0, sizeof(*v)); +} + +void n31_vecmap_free(struct n31_vecmap *v) +{ + if (!v) + return; + vfree(v->l2v_base_p); + vfree(v->l2v_delta); + vfree(v->v2l_base_l); + vfree(v->v2l_delta); + kfree(v->l2v_esc); + kfree(v->v2l_esc); + memset(v, 0, sizeof(*v)); +} + +static int n31_s32_cmp(const void *a, const void *b) +{ + s32 da = *(const s32 *)a; + s32 db = *(const s32 *)b; + + return da < db ? -1 : da > db ? 1 : 0; +} + +static s32 n31_median_s32(s32 *tmp, unsigned int n) +{ + if (!n) + return 0; + sort(tmp, n, sizeof(*tmp), n31_s32_cmp, NULL); + return tmp[n / 2]; +} + +static int n31_esc_add(struct n31_vec_escape **arr, unsigned int *n, + unsigned int *cap, u32 key, u32 value, u32 weave) +{ + struct n31_vec_escape *e; + unsigned int i; + + for (i = 0; i < *n; i++) { + if ((*arr)[i].key == key) { + (*arr)[i].value = value; + (*arr)[i].weave = weave; + return 0; + } + } + if (*n >= *cap) { + unsigned int ncap = *cap ? *cap * 2 : 64; + struct n31_vec_escape *na = + krealloc(*arr, ncap * sizeof(*e), GFP_KERNEL); + + if (!na) + return -ENOMEM; + *arr = na; + *cap = ncap; + } + e = &(*arr)[(*n)++]; + e->key = key; + e->value = value; + e->weave = weave; + return 0; +} + +static int n31_esc_find(const struct n31_vec_escape *arr, unsigned int n, + u32 key, u32 *value) +{ + unsigned int i; + + for (i = 0; i < n; i++) { + if (arr[i].key == key) { + *value = arr[i].value; + return 0; + } + } + return -ENOENT; +} + +static int n31_compress_axis(u32 *base_out, s8 *delta_out, u32 count, + u32 groups, bool is_l2v, + const struct n31_vec_pair *pairs, unsigned int np, + u32 key_base, + struct n31_vec_escape **esc, unsigned int *esc_n, + unsigned int *esc_cap, + unsigned int *ok, unsigned int *escapes, + unsigned int *miss) +{ + s32 *scratch; + unsigned int g; + + scratch = kmalloc(N31_VEC_GROUP_SIZE * sizeof(*scratch), GFP_KERNEL); + if (!scratch) + return -ENOMEM; + + for (g = 0; g < groups; g++) { + u32 gbase = g << N31_VEC_GROUP_SHIFT; + unsigned int nobs = 0; + unsigned int i; + s32 anchor; + u32 idx; + + for (i = 0; i < np && nobs < N31_VEC_GROUP_SIZE; i++) { + u32 key = is_l2v ? pairs[i].l : pairs[i].p; + u32 val = is_l2v ? pairs[i].p : pairs[i].l; + + if (key < key_base) + continue; + idx = key - key_base; + if ((idx >> N31_VEC_GROUP_SHIFT) != g) + continue; + scratch[nobs++] = (s32)val - (s32)(idx & + (N31_VEC_GROUP_SIZE - 1)); + } + + if (!nobs) { + base_out[g] = 0; + for (idx = 0; idx < N31_VEC_GROUP_SIZE && + gbase + idx < count; idx++) { + delta_out[gbase + idx] = N31_VEC_MISS; + (*miss)++; + } + continue; + } + + anchor = n31_median_s32(scratch, nobs); + base_out[g] = (u32)anchor; + + /* Fill miss first, then overwrite observed. */ + for (idx = 0; idx < N31_VEC_GROUP_SIZE && + gbase + idx < count; idx++) + delta_out[gbase + idx] = N31_VEC_MISS; + + for (i = 0; i < np; i++) { + u32 key = is_l2v ? pairs[i].l : pairs[i].p; + u32 val = is_l2v ? pairs[i].p : pairs[i].l; + s32 expected, delta; + u32 off; + + if (key < key_base) + continue; + off = key - key_base; + if ((off >> N31_VEC_GROUP_SHIFT) != g) + continue; + if (off >= count) + continue; + + expected = (s32)anchor + (s32)(off & (N31_VEC_GROUP_SIZE - 1)); + delta = (s32)val - expected; + if (delta >= -127 && delta <= 126) { + delta_out[off] = (s8)delta; + (*ok)++; + } else { + delta_out[off] = N31_VEC_ESC; + if (n31_esc_add(esc, esc_n, esc_cap, key, val, + pairs[i].weave)) { + kfree(scratch); + return -ENOMEM; + } + (*escapes)++; + } + } + + for (idx = 0; idx < N31_VEC_GROUP_SIZE && + gbase + idx < count; idx++) { + if (delta_out[gbase + idx] == N31_VEC_MISS) + (*miss)++; + } + } + + kfree(scratch); + return 0; +} + +int n31_vecmap_build(struct n31_vecmap *v, const struct n31_vec_pair *pairs, + unsigned int n) +{ + u32 l_min = ~0u, l_max = 0, p_min = ~0u, p_max = 0; + unsigned int i; + int ret; + + n31_vecmap_free(v); + n31_vecmap_init(v); + + if (!pairs || !n) + return -EINVAL; + + for (i = 0; i < n; i++) { + if (pairs[i].l < l_min) + l_min = pairs[i].l; + if (pairs[i].l > l_max) + l_max = pairs[i].l; + if (pairs[i].p < p_min) + p_min = pairs[i].p; + if (pairs[i].p > p_max) + p_max = pairs[i].p; + } + + /* Align bases down to group boundary for clean indexing. */ + v->l_base = l_min & ~(N31_VEC_GROUP_SIZE - 1); + v->p_base = p_min & ~(N31_VEC_GROUP_SIZE - 1); + v->l_count = l_max - v->l_base + 1; + v->p_count = p_max - v->p_base + 1; + /* Pad counts to full groups. */ + v->l_count = (v->l_count + N31_VEC_GROUP_SIZE - 1) & + ~(N31_VEC_GROUP_SIZE - 1); + v->p_count = (v->p_count + N31_VEC_GROUP_SIZE - 1) & + ~(N31_VEC_GROUP_SIZE - 1); + v->l2v_groups = v->l_count >> N31_VEC_GROUP_SHIFT; + v->v2l_groups = v->p_count >> N31_VEC_GROUP_SHIFT; + + v->l2v_base_p = vmalloc(array_size(v->l2v_groups, sizeof(u32))); + v->l2v_delta = vmalloc(v->l_count); + v->v2l_base_l = vmalloc(array_size(v->v2l_groups, sizeof(u32))); + v->v2l_delta = vmalloc(v->p_count); + if (!v->l2v_base_p || !v->l2v_delta || !v->v2l_base_l || !v->v2l_delta) { + n31_vecmap_free(v); + return -ENOMEM; + } + memset(v->l2v_delta, N31_VEC_MISS, v->l_count); + memset(v->v2l_delta, N31_VEC_MISS, v->p_count); + + ret = n31_compress_axis(v->l2v_base_p, v->l2v_delta, v->l_count, + v->l2v_groups, true, pairs, n, v->l_base, + &v->l2v_esc, &v->l2v_esc_n, &v->l2v_esc_cap, + &v->compact_ok, &v->compact_esc, + &v->compact_miss); + if (ret) { + n31_vecmap_free(v); + return ret; + } + + ret = n31_compress_axis(v->v2l_base_l, v->v2l_delta, v->p_count, + v->v2l_groups, false, pairs, n, v->p_base, + &v->v2l_esc, &v->v2l_esc_n, &v->v2l_esc_cap, + &v->compact_ok, &v->compact_esc, + &v->compact_miss); + if (ret) { + n31_vecmap_free(v); + return ret; + } + + v->ready = true; + return 0; +} + +static u32 n31_vec_predict(u32 base, s8 delta, u32 idx_in_group) +{ + return (u32)((s32)base + (s32)idx_in_group + (s32)delta); +} + +int n31_vecmap_lookup(const struct n31_vecmap *v, u32 lba, u32 *p_out) +{ + u32 off, group, idx, p, back; + s8 d; + + if (!v || !v->ready || !p_out) + return -EINVAL; + if (lba < v->l_base || lba >= v->l_base + v->l_count) + return -ENOENT; + + off = lba - v->l_base; + d = v->l2v_delta[off]; + if (d == N31_VEC_MISS) + return -ENOENT; + if (d == N31_VEC_ESC) { + if (n31_esc_find(v->l2v_esc, v->l2v_esc_n, lba, &p)) + return -ENOENT; + } else { + group = off >> N31_VEC_GROUP_SHIFT; + idx = off & (N31_VEC_GROUP_SIZE - 1); + p = n31_vec_predict(v->l2v_base_p[group], d, idx); + } + + /* Cross-check V2L(P) == L */ + back = n31_vecmap_v2l(v, p); + if (back != lba) + return -EUCLEAN; + + *p_out = p; + return 0; +} + +u32 n31_vecmap_v2l(const struct n31_vecmap *v, u32 p) +{ + u32 off, group, idx, l; + s8 d; + + if (!v || !v->ready) + return N31_INVALID_L; + if (p < v->p_base || p >= v->p_base + v->p_count) + return N31_INVALID_L; + + off = p - v->p_base; + d = v->v2l_delta[off]; + if (d == N31_VEC_MISS) + return N31_INVALID_L; + if (d == N31_VEC_ESC) { + if (n31_esc_find(v->v2l_esc, v->v2l_esc_n, p, &l)) + return N31_INVALID_L; + return l; + } + group = off >> N31_VEC_GROUP_SHIFT; + idx = off & (N31_VEC_GROUP_SIZE - 1); + return n31_vec_predict(v->v2l_base_l[group], d, idx); +} diff --git a/drivers/misc/ftl-s5l8740-vecmap.h b/drivers/misc/ftl-s5l8740-vecmap.h new file mode 100755 index 00000000000000..a51621c42d8276 --- /dev/null +++ b/drivers/misc/ftl-s5l8740-vecmap.h @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef FTL_S5L8740_VECMAP_H +#define FTL_S5L8740_VECMAP_H + +#include + +#define N31_VEC_GROUP_SHIFT 8 /* 256 entries per group */ +#define N31_VEC_GROUP_SIZE (1u << N31_VEC_GROUP_SHIFT) +#define N31_VEC_MISS 127 /* unknown / missing */ +#define N31_VEC_ESC (-128) /* escape to full table */ +#define N31_INVALID_P 0xffffffffu +#define N31_INVALID_L 0xffffffffu + +#define N31_VEC_NUM_CE 2u +#define N31_VEC_NUM_CAU 2u +#define N31_VEC_BLOCKS_PER_CAU 2088u +#define N31_VEC_PAGES_PER_BLOCK 128u +#define N31_VEC_SLOTS_PER_PAGE 4u + +struct n31_vec_pair { + u32 l; /* fmss_lba */ + u32 p; /* physical record ordinal */ + u32 weave; +}; + +struct n31_vec_escape { + u32 key; + u32 value; + u32 weave; +}; + +struct n31_vecmap { + /* L2V: L -> P */ + u32 l_base; + u32 l_count; + u32 *l2v_base_p; /* per group */ + s8 *l2v_delta; /* per L index */ + u32 l2v_groups; + + /* V2L: P -> L */ + u32 p_base; + u32 p_count; + u32 *v2l_base_l; + s8 *v2l_delta; + u32 v2l_groups; + + struct n31_vec_escape *l2v_esc; + unsigned int l2v_esc_n; + unsigned int l2v_esc_cap; + struct n31_vec_escape *v2l_esc; + unsigned int v2l_esc_n; + unsigned int v2l_esc_cap; + + unsigned int compact_ok; + unsigned int compact_esc; + unsigned int compact_miss; + bool ready; +}; + +static inline u32 n31_phys_to_ordinal(u8 ce, u8 cau, u16 blk, u8 page, u8 slot) +{ + u32 bank = (u32)ce * N31_VEC_NUM_CAU + cau; + u32 page_i = (u32)blk * N31_VEC_PAGES_PER_BLOCK + page; + + return ((bank * N31_VEC_BLOCKS_PER_CAU * + N31_VEC_PAGES_PER_BLOCK + page_i) * + N31_VEC_SLOTS_PER_PAGE) + (slot & 3); +} + +static inline void n31_ordinal_to_phys(u32 p, u8 *ce, u8 *cau, u16 *blk, + u8 *page, u8 *slot) +{ + u32 slot_i = p % N31_VEC_SLOTS_PER_PAGE; + u32 page_i = p / N31_VEC_SLOTS_PER_PAGE; + u32 pages_per_bank = N31_VEC_BLOCKS_PER_CAU * N31_VEC_PAGES_PER_BLOCK; + u32 bank = page_i / pages_per_bank; + u32 rem = page_i % pages_per_bank; + + *slot = (u8)slot_i; + *page = (u8)(rem % N31_VEC_PAGES_PER_BLOCK); + *blk = (u16)(rem / N31_VEC_PAGES_PER_BLOCK); + *cau = (u8)(bank % N31_VEC_NUM_CAU); + *ce = (u8)(bank / N31_VEC_NUM_CAU); +} + +void n31_vecmap_init(struct n31_vecmap *v); +void n31_vecmap_free(struct n31_vecmap *v); + +/* + * Build L2V/V2L from newest-weave pairs (caller already chose weave). + * Allocates delta tables covering [l_min..l_max] and [p_min..p_max]. + */ +int n31_vecmap_build(struct n31_vecmap *v, const struct n31_vec_pair *pairs, + unsigned int n); + +int n31_vecmap_lookup(const struct n31_vecmap *v, u32 lba, u32 *p_out); +u32 n31_vecmap_v2l(const struct n31_vecmap *v, u32 p); + +#endif /* FTL_S5L8740_VECMAP_H */ diff --git a/drivers/misc/fmss-seq-read.h b/drivers/misc/nand-s5l8740-seq.h similarity index 100% rename from drivers/misc/fmss-seq-read.h rename to drivers/misc/nand-s5l8740-seq.h diff --git a/drivers/misc/fmss-s5l8740.c b/drivers/misc/nand-s5l8740.c similarity index 79% rename from drivers/misc/fmss-s5l8740.c rename to drivers/misc/nand-s5l8740.c index 8bc8d0e51ff1dc..7048ca372d1a44 100755 --- a/drivers/misc/fmss-s5l8740.c +++ b/drivers/misc/nand-s5l8740.c @@ -1,20 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * S5L8740 FMSS/FMC NAND peek — N31 + * S5L8740 FMSS/FMC NAND controller driver (N31). * - * Stage A: dump controller regs and issue OSOS READ ID (cmd 0x90). - * Stage B: PPN FIL page read (sub_50D960 / 12ECCC). 3 x 1024 PIO. - * Does not write NAND array data. Do not call the 10453C reset - * sequence from probe — peek first. Do not port 50DC34 (program) - * or 4ED258 (erase). + * Provides FIL primitives used by the FTL: READ ID, parameter page, page + * read (PIO and CS command-list paths), and geometry helpers. Array + * program and erase are not implemented. * - * Cookbook (RetailOS 1.0.2): - * 41C738: *(0x38A00008) = cmd - * 4F11F4: READ ID — FMCTRL0 CE bit, cmd 0x90, addr via 106594, - * 8 bytes via 41AE38 / D622C @ +0x80 - * 50D960: page read — cmd 0x0A, addr, cmd 0x37, 4F1CE8 ready, - * cmd 0x7A, 3 x (optional 53-byte parity + 1024 data) - * D6388: PIO data port is +0x80 (write path; unused here) + * CS physical reads use span-4 / 4112-byte records with four 4096+16 slots + * per page. True metadata DMA remains optional and disabled by default. */ #include #include @@ -35,8 +28,8 @@ #include #include -#include "fmss-seq-read.h" -#include "fmss-s5l8740-api.h" +#include "nand-s5l8740-seq.h" +#include "nand-s5l8740.h" #include "whimory-ftl.h" #define FMSS_PHYS 0x38A00000ul @@ -80,7 +73,7 @@ /* * PPN address packing — OSOS 5173CA / Sogeti PPNVFL: - * page | (block << page_bits) | (cau << (page_bits+block_bits)) | (slc << ...) + * page | (block << page_bits) | (cau << (page_bits+block_bits)) | (slc <<...) * Param page: page_bits=7, block_bits=12, cau_bits=1. * VFL context lives in the last ~5% of each CAU, SLC page 0 * (spare type 0x20, index 0xFFFF). SFTL context spare type 0x1F. @@ -101,7 +94,7 @@ #define FMSS_L2V_DEFAULT_BLOCKS 256 /* Map is keyed by page LPN (YaFTL); full LBA map preferred for block I/O. */ #define FMSS_L2V_DEFAULT_MAX_LPN \ - ((FMSS_FTL_DEFAULT_CAPACITY / FMSS_FTL_SECTORS_PER_LPN) + 64) + ((NAND_FTL_DEFAULT_CAPACITY / NAND_FTL_SECTORS_PER_LPN) + 64) /* Classic Whimory mount (freemyipod) adapted for N31 PPN geometry. */ #define WMR_PAGES_PER_BLOCK 128u @@ -110,7 +103,7 @@ #define WMR_FTLCTRL_MAX 3u /* Packed L2V entry: - * valid|ce[1:0]|cau[1:0]|PHYS|sec[1:0]|block[11:0]|page[6:0] + * valid|ce[1:0]|cau[1:0]|PHYS|sec[1:0]|block[11:0]|page[6:0] * sec = 4K index within the NAND page (SFTL VBA). 0x3 = “use LBA%4”. * PHYS: block is already physical (BTOC/BTE/META/carve) — do NOT VFL-remap. */ @@ -171,9 +164,9 @@ module_param(quiet, int, 0644); MODULE_PARM_DESC(quiet, "1=minimal logs, skip ECC diag (faster FTL scans, default on)"); #define fmss_info(fmt, ...) \ - do { if (!quiet) pr_info("s5l8740-fmss: " fmt, ##__VA_ARGS__); } while (0) + do { if (!quiet) pr_info("s5l8740-nand: " fmt, ##__VA_ARGS__); } while (0) -#define fmss_dev_info(dev, fmt, ...) \ +#define nand_dev_info(dev, fmt, ...) \ do { if (!quiet) dev_info(dev, fmt, ##__VA_ARGS__); } while (0) static unsigned int vfl_build_blocks = 32; @@ -212,11 +205,11 @@ static unsigned int l2v_meta_hits; * Full LBA → packed phys+sec map (SFTL BTE / boot carve). Preferred over LPN * dense map for block I/O. * - * WARNING: FMSS_FTL_DEFAULT_CAPACITY is ~3.8M *sectors* (~15GB media). A dense + * WARNING: NAND_FTL_DEFAULT_CAPACITY is ~3.8M *sectors* (~15GB media). A dense * map of that size is ~50MB+ RAM (u32+u64+u8) and OOMs N31 before RNDIS. * Cap with lba_map_max (default 262144 ≈ 1GB LBA space ≈ 3.4MB RAM). */ -#define FMSS_LBA_MAP_HARDMAX FMSS_FTL_DEFAULT_CAPACITY +#define FMSS_LBA_MAP_HARDMAX NAND_FTL_DEFAULT_CAPACITY static unsigned int lba_map_max = 262144; module_param(lba_map_max, uint, 0644); MODULE_PARM_DESC(lba_map_max, @@ -273,7 +266,7 @@ MODULE_PARM_DESC(l2v_auto_blocks, */ static bool fmss_legacy_meta_ingest; -static int (*fmss_ftl_read_hook)(u64 lba, void *buf); +static int (*nand_ftl_read_hook)(u64 lba, void *buf); /* Root-dir physical page when LPN(DataStart) is otherwise unmapped. */ static bool root_dir_valid; @@ -288,7 +281,7 @@ static unsigned int vfl_map_count; static unsigned int vfl_ctx_cau[FMSS_NUM_CAU]; static unsigned int vfl_ctx_block[FMSS_NUM_CAU]; -/* Classic Whimory FTL block map + mount status (Stage C). */ +/* Classic Whimory FTL block map + mount status (phase). */ static u16 *wmr_block_map; static unsigned int wmr_block_map_n; static unsigned int wmr_dis_hits; @@ -310,7 +303,7 @@ static u32 fmss_ppn_addr(unsigned int cau, unsigned int block, | ((slc ? 1u : 0u) << (FMSS_PAGE_BITS + FMSS_BLOCK_BITS + FMSS_CAU_BITS)); } -struct fmss_n31 { +struct nand_s5l8740 { void __iomem *base; struct mutex lock; u8 last_id[8]; @@ -355,7 +348,7 @@ struct fmss_n31 { u32 last_vic_en; }; -static struct fmss_n31 *fmss_dev; +static struct nand_s5l8740 *nand_dev; /* 12ED9C: PPN >= 0x10500 uses 4 address cycles. */ static unsigned int addr_cycles = 4; @@ -426,7 +419,7 @@ MODULE_PARM_DESC(dma_nsect, "DMA span (# logical LBAs) per CS read (default 1)") /* * Decomp wants command-list META. Live CS kick (C00=0xFFF5) still wedges - * the SoC on glass — keep default off until that path is safe. Callers that + * the SoC in testing — keep default off until that path is safe. Callers that * ask for meta with this off get -EOPNOTSUPP (never PIO fake spare). */ static bool meta_dma_read; @@ -434,11 +427,21 @@ module_param(meta_dma_read, bool, 0644); MODULE_PARM_DESC(meta_dma_read, "Use command-list data+meta read for metadata callers (default N — CS kick wedges)"); -static bool meta_dma_reset_before = true; +/* + * PIO page path uses the legacy reset/reinit sequence. + * CS command-list path is a separate sequencer path and should not assume + * hard reset immediately before kick. in testing this sequence can wedge. + */ +static bool meta_dma_reset_before; module_param(meta_dma_reset_before, bool, 0644); MODULE_PARM_DESC(meta_dma_reset_before, "Reset NAND controller before command-list metadata read"); +static bool dma_reset_before; +module_param(dma_reset_before, bool, 0644); +MODULE_PARM_DESC(dma_reset_before, + "Reset NAND controller before manual CS DMA read"); + /* * PPN physical page = N × (4096 DATA + 16 META) records (N=2 or 4). * 5172A0: qword.lo = (rec*span) | ((rec*slot) << 16); qword.hi = encoded_ppn. @@ -469,13 +472,26 @@ static unsigned int dma_kick = 0xfff5; module_param(dma_kick, uint, 0644); MODULE_PARM_DESC(dma_kick, "FMSEQ (C00) kick value (OSOS D39EC = 0xFFF5)"); -static bool dma_dry; +/* Default Y: program descriptors without C00 kick until glass preflight passes. */ +static bool dma_dry = true; module_param(dma_dry, bool, 0644); -MODULE_PARM_DESC(dma_dry, "program CS regs/descriptors but do not write C00 (default N)"); +MODULE_PARM_DESC(dma_dry, + "program CS regs/descriptors but do not write C00 (default Y)"); + +static bool dma_armed; +module_param(dma_armed, bool, 0644); +MODULE_PARM_DESC(dma_armed, "Allow one hazardous CS DMA kick"); + +static bool dma_one_shot = true; +module_param(dma_one_shot, bool, 0644); +MODULE_PARM_DESC(dma_one_shot, "Disarm CS DMA after one kick"); + +/* Canary path: one page, no FTL/lba_map ingest. */ +static bool dma_skip_ingest; /* * D39EC: if 0x8982448 then C6C=0 + pulse C60; else C6C=16. - * Pulse never raised C64 on glass. Default matches the else path. + * Pulse never raised C64 in testing. Default matches the else path. */ static bool dma_pulse; module_param(dma_pulse, bool, 0644); @@ -505,7 +521,7 @@ static u32 fmss_page_ctrl0(unsigned int ce) } /* 1858DC: poll FMSTAT48 bit(s), then W1C. */ -static int fmss_wait48_n(struct fmss_n31 *f, u32 mask, unsigned int loops) +static int fmss_wait48_n(struct nand_s5l8740 *f, u32 mask, unsigned int loops) { unsigned int i; u32 st; @@ -526,19 +542,19 @@ static int fmss_wait48_n(struct fmss_n31 *f, u32 mask, unsigned int loops) return -ETIMEDOUT; } -static int fmss_wait48(struct fmss_n31 *f, u32 mask) +static int fmss_wait48(struct nand_s5l8740 *f, u32 mask) { return fmss_wait48_n(f, mask, 20000); } -static int fmss_cmd(struct fmss_n31 *f, u8 cmd) +static int fmss_cmd(struct nand_s5l8740 *f, u8 cmd) { writel(cmd, f->base + FMCMD); return fmss_wait48(f, 2); } /* D622C: drain PIO FIFO at +0x80. Must read +0x80 first (OSOS + live ID). */ -static int fmss_pio_read(struct fmss_n31 *f, void *dst, unsigned int len) +static int fmss_pio_read(struct nand_s5l8740 *f, void *dst, unsigned int len) { u8 *p = dst; unsigned int n = 0, words = len >> 2, spins = 0; @@ -565,7 +581,7 @@ static int fmss_pio_read(struct fmss_n31 *f, void *dst, unsigned int len) } /* 41AE38: NAND→controller beat then D622C PIO. len <= M2_BYTES_PER_SECTOR. */ -static int fmss_data_in(struct fmss_n31 *f, void *dst, unsigned int len) +static int fmss_data_in(struct nand_s5l8740 *f, void *dst, unsigned int len) { if (len < 1 || len > 0x400) return -EINVAL; @@ -574,7 +590,7 @@ static int fmss_data_in(struct fmss_n31 *f, void *dst, unsigned int len) writel(0, f->base + FMUNK24); writel(34, f->base + FMCTRL1); if (fmss_wait48(f, 8)) { - pr_info("s5l8740-fmss: data_in wait48(8) timeout len=%u st=%08x\n", + pr_info("s5l8740-nand: data_in wait48(8) timeout len=%u st=%08x\n", len, f->last_stat48); return -ETIMEDOUT; } @@ -582,7 +598,7 @@ static int fmss_data_in(struct fmss_n31 *f, void *dst, unsigned int len) } /* 41DA70: wait NANDSTAT ready after 0x77/0x7D. */ -static int fmss_wait_status(struct fmss_n31 *f, unsigned int loops, u8 *nandstat) +static int fmss_wait_status(struct nand_s5l8740 *f, unsigned int loops, u8 *nandstat) { int ret; @@ -595,7 +611,7 @@ static int fmss_wait_status(struct fmss_n31 *f, unsigned int loops, u8 *nandstat return ret; } -static int fmss_addr_n(struct fmss_n31 *f, u32 addr, unsigned int cycles) +static int fmss_addr_n(struct nand_s5l8740 *f, u32 addr, unsigned int cycles) { if (cycles < 1 || cycles > 8) cycles = 1; @@ -605,7 +621,7 @@ static int fmss_addr_n(struct fmss_n31 *f, u32 addr, unsigned int cycles) return fmss_wait48(f, 4); } -static int fmss_addr1(struct fmss_n31 *f, u32 addr) +static int fmss_addr1(struct nand_s5l8740 *f, u32 addr) { return fmss_addr_n(f, addr, 1); } @@ -614,7 +630,7 @@ static int fmss_addr1(struct fmss_n31 *f, u32 addr) * D6388 + 112A7C: PIO write to +0x80 then kick. Used only for SET FEATURES * (NAND device registers), never for array program (50DC34). */ -static int fmss_pio_write(struct fmss_n31 *f, const void *src, unsigned int len) +static int fmss_pio_write(struct nand_s5l8740 *f, const void *src, unsigned int len) { const u32 *p = src; unsigned int i, words; @@ -640,7 +656,7 @@ static int fmss_pio_write(struct fmss_n31 *f, const void *src, unsigned int len) } /* 1303B4: PPN SET FEATURES (cmd 0xEF). */ -static int fmss_set_feature(struct fmss_n31 *f, unsigned int ce, u16 feat, u32 val) +static int fmss_set_feature(struct nand_s5l8740 *f, unsigned int ce, u16 feat, u32 val) { u8 st = 0; u32 feat_word = feat; @@ -670,7 +686,7 @@ static u16 last_feat_id; static int last_feat_ce = -1; static int last_feat_ret = -1; -static int fmss_get_feature(struct fmss_n31 *f, unsigned int ce, u16 feat, +static int fmss_get_feature(struct nand_s5l8740 *f, unsigned int ce, u16 feat, void *dst, unsigned int len) { u8 st = 0; @@ -703,8 +719,8 @@ static int fmss_get_feature(struct fmss_n31 *f, unsigned int ce, u16 feat, return ret; } -/* sub_4F11F4(ce, addr=0, buf): READ ID, no 10453C reset. */ -static int fmss_read_id(struct fmss_n31 *f, unsigned int ce) +/*(ce, addr=0, buf): READ ID, no 10453C reset. */ +static int fmss_read_id(struct nand_s5l8740 *f, unsigned int ce) { u8 *dst = f->last_id; @@ -719,7 +735,7 @@ static int fmss_read_id(struct fmss_n31 *f, unsigned int ce) fmss_cmd(f, 0x90); if (fmss_addr1(f, 0)) - pr_info("s5l8740-fmss: wait48(4) after addr timed out st=%08x\n", + pr_info("s5l8740-nand: wait48(4) after addr timed out st=%08x\n", readl(f->base + FMSTAT48)); if (fmss_data_in(f, dst, 8)) { @@ -733,7 +749,7 @@ static int fmss_read_id(struct fmss_n31 *f, unsigned int ce) } /* 4F1CE8: cmd 0x77/0x7D then wait NAND ready (STAT48 bit 0x800000). */ -static int fmss_wait_ready(struct fmss_n31 *f) +static int fmss_wait_ready(struct nand_s5l8740 *f) { int ret; @@ -745,7 +761,7 @@ static int fmss_wait_ready(struct fmss_n31 *f) writel(32, f->base + FMCTRL1); writel(0x800000u, f->base + FMSTAT48); if (ret) - pr_info("s5l8740-fmss: ready timeout NANDSTAT=%08x STAT48=%08x\n", + pr_info("s5l8740-nand: ready timeout NANDSTAT=%08x STAT48=%08x\n", f->last_nandstat, f->last_stat48); return ret; } @@ -756,7 +772,7 @@ MODULE_PARM_DESC(ecc_before_drain, "Run OSOS 4EB458 ECC/descramble before PIO drain (default Y)"); /* - * 0 = diagnostic sub_50D960: FMLEN=52 FMCE=16 CTRL1=34 then data FMCE=1 + * 0 = diagnostic: FMLEN=52 FMCE=16 CTRL1=34 then data FMCE=1 * 1 = production-seq style: FMLEN=15 FMCE=0x102 CTRL1=0x1E2, data FMCE=0x201 +0x18=2 */ static unsigned int xfer_style; @@ -783,8 +799,8 @@ module_param(xfer_ctrl1, uint, 0644); MODULE_PARM_DESC(xfer_ctrl1, "FMCTRL1 for parity/data beats (default 34)"); /* - * OSOS sub_4EB458(a1=0, len=1024): kick FMSS ECC/descramble engine. - * Must run AFTER parity+data transfer waits, BEFORE FIFO drain (sub_D622C). + * OSOS(a1=0, len=1024): kick FMSS ECC/descramble engine. + * Must run AFTER parity+data transfer waits, BEFORE FIFO drain. * * Exact RetailOS order — do NOT clear +0x810 before kick (preload stays * 0x3f1f73af). Poll bit0 of +0x810 for completion; if preload already has @@ -792,7 +808,7 @@ MODULE_PARM_DESC(xfer_ctrl1, "FMCTRL1 for parity/data beats (default 34)"); * * Returns 0 OK, 1 clean/erased page, -ETIMEDOUT / -EIO on hard fail. */ -static int fmss_ecc_chunk(struct fmss_n31 *f, unsigned int seed_a1) +static int fmss_ecc_chunk(struct nand_s5l8740 *f, unsigned int seed_a1) { u32 ecc, hist, st; unsigned int t; @@ -813,7 +829,7 @@ static int fmss_ecc_chunk(struct fmss_n31 *f, unsigned int seed_a1) udelay(1); } if (t >= 20000) { - pr_info("s5l8740-fmss: 4EB458 timeout st=%08x\n", st); + pr_info("s5l8740-nand: 4EB458 timeout st=%08x\n", st); return -ETIMEDOUT; } ecc = readl(f->base + 0x80c); @@ -830,15 +846,15 @@ static int fmss_ecc_chunk(struct fmss_n31 *f, unsigned int seed_a1) } /* - * sub_50D960(ce, page_addr, buf, with_parity). + *(ce, page_addr, buf, with_parity). * Per 1KiB chunk: parity beat → data xfer wait → 4EB458 → PIO drain. * Linux previously drained before ECC — that left DATA pages whitened. */ -static void fmss_meta_ingest_spare(struct fmss_n31 *f, unsigned int ce, +static void fmss_meta_ingest_spare(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, unsigned int slot0); -static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) +static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) { int i, ret = -EIO, ecc_ret; u8 *dst = f->last_page; @@ -871,19 +887,19 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) writel(addr, f->base + FMADDR); writel(1, f->base + FMCTRL1); if (fmss_wait48(f, 4)) { - pr_info("s5l8740-fmss: addr cycle timeout ce=%u addr=%08x st=%08x nand=%08x\n", + pr_info("s5l8740-nand: addr cycle timeout ce=%u addr=%08x st=%08x nand=%08x\n", ce, addr, f->last_stat48, f->last_nandstat); goto fail_ctrl0; } if (fmss_cmd(f, 0x37)) { - pr_info("s5l8740-fmss: cmd 0x37 timeout ce=%u addr=%08x st=%08x\n", + pr_info("s5l8740-nand: cmd 0x37 timeout ce=%u addr=%08x st=%08x\n", ce, addr, f->last_stat48); goto fail_ctrl0; } if (fmss_wait_ready(f)) { - pr_info("s5l8740-fmss: not ready after read cmd ce=%u addr=%08x\n", + pr_info("s5l8740-nand: not ready after read cmd ce=%u addr=%08x\n", ce, addr); goto fail_ctrl0; } @@ -902,24 +918,24 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) writel(0x1e2, f->base + FMCTRL1); } else { /* OSOS 50D960 parity: FMLEN=52 FMCE=16 +0x24=0 CTRL1=34 - * — do not touch +0x18 or +0x28 here. - */ + * — do not touch +0x18 or +0x28 here. + */ writel(52, f->base + FMLEN); writel(parity_fmce, f->base + FMCE); writel(0, f->base + FMUNK24); writel(xfer_ctrl1, f->base + FMCTRL1); } if (fmss_wait48(f, 8)) { - pr_info("s5l8740-fmss: parity xfer timeout ce=%u addr=%08x chunk=%d st=%08x style=%u\n", + pr_info("s5l8740-nand: parity xfer timeout ce=%u addr=%08x chunk=%d st=%08x style=%u\n", ce, addr, i, f->last_stat48, xfer_style); goto fail_ctrl0; } /* - * Live: after FMCE=16 FMLEN=52, D622C can read bytes - * (head often 02 …) — parity lands on the DATA FIFO. - * Never drain it when ecc_before_drain=1 (OSOS leaves - * it for 4EB458). Optional strip only when ECC off. - */ + * Live: after FMCE=16 FMLEN=52, D622C can read bytes + * (head often 02 …) — parity lands on the DATA FIFO. + * Never drain it when ecc_before_drain=1 (OSOS leaves + * it for 4EB458). Optional strip only when ECC off. + */ if (xfer_style == 0 && !ecc_before_drain) { u8 dig[64]; @@ -928,9 +944,9 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) memcpy(f->last_parity[i], dig, 53); f->last_parity_len[i] = 53; /* - * Host-visible spare is the first 16 of - * the 53-byte beat, once per 4K slot. - */ + * Host-visible spare is the first 16 of + * the 53-byte beat, once per 4K slot. + */ if ((i & 3) == 0) { unsigned int slot = (unsigned int)i / 4u; unsigned int pick = 0; @@ -966,7 +982,7 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) writel(xfer_ctrl1, f->base + FMCTRL1); } if (fmss_wait48(f, 8)) { - pr_info("s5l8740-fmss: data xfer timeout ce=%u addr=%08x chunk=%d st=%08x nand=%08x style=%u\n", + pr_info("s5l8740-nand: data xfer timeout ce=%u addr=%08x chunk=%d st=%08x nand=%08x style=%u\n", ce, addr, i, f->last_stat48, f->last_nandstat, xfer_style); goto fail_ctrl0; @@ -976,11 +992,11 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) ecc_ret = fmss_ecc_chunk(f, 0); if (ecc_ret == 1) { /* - * Chunk is erased/clean — leave zeros and - * keep going. Aborting the whole page on - * chunk0 made FPart/VFL miss SLC specials - * (glass: 4096 tail reads, tag30=0). - */ + * Chunk is erased/clean — leave zeros and + * keep going. Aborting the whole page on + * chunk0 made FPart/VFL miss SLC specials + * (glass: 4096 tail reads, tag30=0). + */ f->last_clean_chunks++; continue; } @@ -993,17 +1009,17 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) } } if (fmss_pio_read(f, dst + i * FMSS_CHUNK, FMSS_CHUNK)) { - pr_info("s5l8740-fmss: PIO drain timeout ce=%u addr=%08x chunk=%d\n", + pr_info("s5l8740-nand: PIO drain timeout ce=%u addr=%08x chunk=%d\n", ce, addr, i); goto fail_ctrl0; } } /* - * Trailing fmss_data_in(64) after 16×1K is an empty FIFO (zeros) and - * must not be treated as META (that polluted lba_map with type 0x00). - * Extra style-1 FMLEN=15 beats after this path desynced the next page. - */ + * Trailing fmss_data_in(64) after 16×1K is an empty FIFO (zeros) and + * must not be treated as META (that polluted lba_map with type 0x00). + * Extra style-1 FMLEN=15 beats after this path desynced the next page. + */ fmss_cmd(f, 0x77); writel(0, f->base + FMCTRL0); @@ -1030,7 +1046,7 @@ static int fmss_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) * FIL needs both descrambled data (pass 1, ECC) and 16B Sogeti META * (pass 2, drain 53-byte parity FIFO). Mutex is already held. */ -static int fmss_page_read_with_meta(struct fmss_n31 *f, unsigned int ce, +static int fmss_page_read_with_meta(struct nand_s5l8740 *f, unsigned int ce, u32 addr) { static u8 page_bak[FMSS_PAGE_LEN]; @@ -1044,9 +1060,9 @@ static int fmss_page_read_with_meta(struct fmss_n31 *f, unsigned int ce, if (ret || !meta_pass) return ret; /* - * Erased PPN page: all 16 chunks ECC-clean. Spare is 0xFF; a second - * 50D960 only burns the controller (tail+brute are mostly empty). - */ + * Erased PPN page: all 16 chunks ECC-clean. Spare is 0xFF; a second + * 50D960 only burns the controller (tail+brute are mostly empty). + */ if (f->last_clean_chunks && f->last_clean_chunks >= (page_chunks ? page_chunks : 16)) { memset(f->last_spare, 0xff, sizeof(f->last_spare)); @@ -1071,7 +1087,7 @@ static int fmss_page_read_with_meta(struct fmss_n31 *f, unsigned int ce, f->last_page_chunk = saved_chunk; f->last_page_ret = ret; if (ret2 && !quiet) - pr_info("s5l8740-fmss: meta pass ce=%u addr=%08x ret=%d (data kept)\n", + pr_info("s5l8740-nand: meta pass ce=%u addr=%08x ret=%d (data kept)\n", ce, addr, ret2); return ret; } @@ -1081,16 +1097,22 @@ static int fmss_page_read_with_meta(struct fmss_n31 *f, unsigned int ce, * Sequence program is the OSOS blob at 0x8980EA0 (embedded). * 16-byte PPN spare per 4K sector lands in the spare DMA buffer * (Sogeti type/bank/weaveSeq/lpn). Status bytes go to stbuf. + * + * Treat CS as a peripheral sequencer (not PL080): clear status, prove + * idle, program pointers, read back posted MMIO, kick last. USB OTG + * already proves SoC bus-master DMA / coherent buffers work. */ -static int fmss_wait_cs(struct fmss_n31 *f, unsigned int loops) +static int fmss_wait_cs_poll(struct nand_s5l8740 *f, unsigned int loops) { unsigned int i; u32 irq; for (i = 0; i < loops; i++) { irq = readl(f->base + FMSEQIRQ); - if ((irq & 0xd) == 1) + if ((irq & 0xd) == 1) { + f->last_dma_c0c = irq; return 0; + } if (irq & 0xc) { f->last_dma_c0c = irq; return -EIO; @@ -1101,7 +1123,40 @@ static int fmss_wait_cs(struct fmss_n31 *f, unsigned int loops) return -ETIMEDOUT; } -static void fmss_dma_teardown(struct fmss_n31 *f) +static bool fmss_cs_preflight(struct nand_s5l8740 *f) +{ + u32 c08, c0c, c00; + + c00 = readl(f->base + FMSEQ); + c08 = readl(f->base + FMSEQSTAT); + c0c = readl(f->base + FMSEQIRQ); + + f->last_dma_c00 = c00; + f->last_dma_c0c = c0c; + + /* Clear stale completion/error before programming. */ + if (c0c & 0x0d) { + writel(c0c & 0x0d, f->base + FMSEQIRQ); + readl(f->base + FMSEQIRQ); + udelay(10); + c0c = readl(f->base + FMSEQIRQ); + f->last_dma_c0c = c0c; + } + + /* + * Conservative idle gate. Adjust allowed states only after glass logs. + * Avoid kick if status already advertises completion/error/busy noise. + */ + if (c0c & 0x0d) + return false; + + if (c08 != 0 && c08 != 3) + return false; + + return true; +} + +static void fmss_dma_teardown(struct nand_s5l8740 *f) { struct device *dev = f->dev; @@ -1128,14 +1183,27 @@ static void fmss_dma_teardown(struct fmss_n31 *f) static irqreturn_t fmss_cs_irq(int irq, void *data) { - struct fmss_n31 *f = data; + struct nand_s5l8740 *f = data; + u32 st; + + st = readl(f->base + FMSEQIRQ); + f->last_dma_c0c = st; + + /* + * Level-style VIC source: clear peripheral before parent EOI, or it + * can retrigger/stick. Snapshot first — waiter must not require C0C + * to remain asserted after W1C. + */ + if (st & 0x0d) { + writel(st & 0x0d, f->base + FMSEQIRQ); + readl(f->base + FMSEQIRQ); + } - f->last_dma_c0c = readl(f->base + FMSEQIRQ); complete(&f->cs_irq); return IRQ_HANDLED; } -static void fmss_peek_vic1(struct fmss_n31 *f) +static void fmss_peek_vic1(struct nand_s5l8740 *f) { void __iomem *vic1; @@ -1147,7 +1215,7 @@ static void fmss_peek_vic1(struct fmss_n31 *f) iounmap(vic1); } -static int fmss_dma_setup(struct fmss_n31 *f, struct device *dev) +static int fmss_dma_setup(struct nand_s5l8740 *f, struct device *dev) { int ret; @@ -1163,7 +1231,7 @@ static int fmss_dma_setup(struct fmss_n31 *f, struct device *dev) f->spare = dma_alloc_coherent(dev, FMSS_DMA_SPARE_LEN, &f->spare_dma, GFP_KERNEL); f->stbuf = dma_alloc_coherent(dev, FMSS_DMA_STATUS_LEN, &f->stbuf_dma, GFP_KERNEL); if (!f->seq || !f->cmdl || !f->data || !f->spare || !f->stbuf) { - fmss_dev_info(dev, "DMA coherent alloc failed, PIO only\n"); + nand_dev_info(dev, "DMA coherent alloc failed, PIO only\n"); fmss_dma_teardown(f); return -ENOMEM; } @@ -1182,11 +1250,11 @@ static int fmss_dma_setup(struct fmss_n31 *f, struct device *dev) f->irq = 0; } else { f->irq = dma_irq; - fmss_dev_info(dev, "CS IRQ %d (OSOS 54 / VIC1 22)\n", f->irq); + nand_dev_info(dev, "CS IRQ %d (OSOS 54 / VIC1 22)\n", f->irq); } } - fmss_dev_info(dev, "DMA seq_phys=0x%08lx cmdl=0x%08lx data=0x%08lx seq0=%02x %02x %02x %02x coherent=1\n", + nand_dev_info(dev, "DMA seq_phys=0x%08lx cmdl=0x%08lx data=0x%08lx seq0=%02x %02x %02x %02x coherent=1\n", (unsigned long)f->seq_dma, (unsigned long)f->cmdl_dma, (unsigned long)f->data_dma, ((u8 *)f->seq)[0], ((u8 *)f->seq)[1], @@ -1194,16 +1262,18 @@ static int fmss_dma_setup(struct fmss_n31 *f, struct device *dev) return 0; } -static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) +static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) { u32 *cl; u32 ce_bit; unsigned int nsect = dma_nsect; unsigned int i, spare_bytes; u8 *dp; - int ret; + int ret = 0; u32 dregs[32]; + memset(dregs, 0, sizeof(dregs)); + if (!f->dma_ok) return -ENODEV; if (ce > 7 || nsect < 1 || nsect > 4) @@ -1220,18 +1290,18 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) ce_bit = 1u << (16 + ce); /* - * 4EDDDC one-page descriptor list: - * desc0: CE-select dword0=1<<(ce+16), later |0x80000000 if last for CE - * dword2/3 = packed physical address (v40: low in [2]) - * desc1: transfer dword0=(1<<(ce+16))|1, [1]=span, [2]=meta, [3]=data - * term: 0x00010002 - */ + * 4EDDDC one-page descriptor list: + * desc0: CE-select dword0=1<<(ce+16), later |0x80000000 if last for CE + * dword2/3 = packed physical address (v40: low in [2]) + * desc1: transfer dword0=(1<<(ce+16))|1, [1]=span, [2]=meta, [3]=data + * term: 0x00010002 + */ /* - * 4EDDDC address qword from 5172A0 (READ, v40 / multi-LBA page): - * lo = (rec * span) | ((rec * slot) << 16) // length | column<<16 - * hi = encoded_ppn (5173CA, mode 0) - * desc[2]=lo, desc[3]=hi when dma_d14>=7 (v40). - */ + * 4EDDDC address qword from 5172A0 (READ, v40 / multi-LBA page): + * lo = (rec * span) | ((rec * slot) << 16) // length | column<<16 + * hi = encoded_ppn (5173CA, mode 0) + * desc[2]=lo, desc[3]=hi when dma_d14>=7 (v40). + */ cl[0] = ce_bit; cl[1] = 0; { @@ -1294,6 +1364,15 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) writel(0x0b00, f->base + 0xc4c); } + /* + * Clear/idle before programming D-regs / C04 (USB/PL080 pattern: + * clear status, prove idle, program pointers, kick last). + */ + if (!fmss_cs_preflight(f)) { + ret = -EBUSY; + goto dma_done; + } + /* 4EDDDC register skeleton — bus addresses only. */ writel(page_ctrl0_or, f->base + FMGEN1); /* D04 timing template */ writel((u32)f->cmdl_dma, f->base + FMGEN2); /* D08 descriptor list */ @@ -1302,14 +1381,15 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) writel(dma_d14, f->base + FMGEN5); /* D14 = addr_cycles-1 */ writel((u32)f->seq_dma, f->base + FMSEQBASE); /* C04 = seq program */ /* - * Do NOT poke +0x81C here — that is 4EB458 ECC, not in 4EDDDC/D39EC. - * A spurious 81C write before CS previously correlated with SoC wedges. - */ + * Do NOT poke +0x81C here — that is 4EB458 ECC, not in 4EDDDC/D39EC. + * A spurious 81C write before CS previously correlated with SoC wedges. + */ /* D39EC: C00 = 0xFFF5. Do NOT use 0x80000 (reset) here. */ reinit_completion(&f->cs_irq); - fmss_info("dma kick ce=%u addr=%08x seq=%08x cmdl=%08x data=%08x meta=%08x st=%08x d14=%u kick=%04x dry=%d\n", + fmss_info("dma kick ce=%u addr=%08x seq=%08x cmdl=%08x data=%08x meta=%08x st=%08x d14=%u kick=%04x dry=%d armed=%d\n", ce, addr, (u32)f->seq_dma, (u32)f->cmdl_dma, (u32)f->data_dma, - (u32)f->spare_dma, (u32)f->stbuf_dma, dma_d14, dma_kick, dma_dry); + (u32)f->spare_dma, (u32)f->stbuf_dma, dma_d14, dma_kick, + dma_dry, dma_armed); if (dma_dry) { f->last_dma_c0c = readl(f->base + FMSEQIRQ); f->last_dma_d00 = readl(f->base + FMGEN0); @@ -1317,17 +1397,53 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) ret = -EAGAIN; goto dma_done; } + if (!dma_armed) { + ret = -EPERM; + goto dma_done; + } + if (dma_one_shot) + dma_armed = false; + + /* + * Device-visible descriptor/data/status buffers before CS fetch. + * Coherent allocs do not need explicit cache flushes; ordering does. + */ + dma_wmb(); + wmb(); + + /* Flush posted MMIO programming before the sequencer kick. */ + readl(f->base + FMGEN2); + readl(f->base + FMGEN3); + readl(f->base + FMGEN4); + readl(f->base + FMGEN5); + readl(f->base + FMSEQBASE); + writel(dma_kick, f->base + FMSEQ); + readl(f->base + FMSEQ); /* posted write flush */ /* - * Prefer short poll of C0C. IRQ wait alone can hang the process context - * if VIC routing is wrong; poll always bounds the wait. - */ - ret = fmss_wait_cs(f, 20000); - if (ret && f->irq > 0 && - try_wait_for_completion(&f->cs_irq)) - ret = ((f->last_dma_c0c & 0xd) == 1) ? 0 : ret; - f->last_dma_c0c = readl(f->base + FMSEQIRQ); + * Prefer IRQ completion snapshot (ISR already W1C'd C0C). Fall back + * to short poll only when no IRQ or completion never arrived. + */ + if (f->irq > 0) { + if (wait_for_completion_timeout(&f->cs_irq, + msecs_to_jiffies(200))) { + ret = 0; + } else { + ret = fmss_wait_cs_poll(f, 2000); + } + } else { + ret = fmss_wait_cs_poll(f, 20000); + } + + /* + * Avoid require C0C to still be live — ISR clears it for VIC EOI. + * Trust the saved snapshot from ISR or poll. + */ + if (!ret && f->last_dma_c0c && + ((f->last_dma_c0c & 0x0d) != 1)) + ret = -EIO; + f->last_dma_d00 = readl(f->base + FMGEN0); f->last_dma_c00 = readl(f->base + FMSEQ); fmss_peek_vic1(f); @@ -1395,7 +1511,7 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) cl[0], cl[1], cl[2], cl[3], cl[4], cl[5], cl[6], cl[7], cl[8], (u32)f->seq_dma, (u32)f->cmdl_dma, (u32)f->data_dma, (u32)f->spare_dma); - if (!ret && f->last_spare_len >= 16) { + if (!ret && !dma_skip_ingest && f->last_spare_len >= 16) { unsigned int pg = addr & L2V_PAGE_MASK; unsigned int blk = (addr >> FMSS_PAGE_BITS) & L2V_BLOCK_MASK; unsigned int cau = (addr >> (FMSS_PAGE_BITS + FMSS_BLOCK_BITS)) & @@ -1407,10 +1523,10 @@ static int fmss_dma_page_read(struct fmss_n31 *f, unsigned int ce, u32 addr) } /* - * sub_12FA84: PPN parameter page, 512 bytes via 41AE38. + *: PPN parameter page, 512 bytes via 41AE38. * cmd 0x92, addr 0, cmd 0x97, 0x77/0x7D, 41DA70, cmd 0x7A, 512 PIO. */ -static int fmss_param_read(struct fmss_n31 *f, unsigned int ce) +static int fmss_param_read(struct nand_s5l8740 *f, unsigned int ce) { u8 st = 0; int ret; @@ -1422,24 +1538,24 @@ static int fmss_param_read(struct fmss_n31 *f, unsigned int ce) writel(fmss_ctrl0(ce), f->base + FMCTRL0); if (fmss_cmd(f, 0x92)) { - pr_info("s5l8740-fmss: param cmd 0x92 timeout\n"); + pr_info("s5l8740-nand: param cmd 0x92 timeout\n"); ret = -ETIMEDOUT; goto out_idle; } if (fmss_addr1(f, 0)) { - pr_info("s5l8740-fmss: param addr timeout st=%08x\n", f->last_stat48); + pr_info("s5l8740-nand: param addr timeout st=%08x\n", f->last_stat48); ret = -ETIMEDOUT; goto out_idle; } if (fmss_cmd(f, 0x97)) { - pr_info("s5l8740-fmss: param cmd 0x97 timeout\n"); + pr_info("s5l8740-nand: param cmd 0x97 timeout\n"); ret = -ETIMEDOUT; goto out_idle; } fmss_cmd(f, 0x77); fmss_cmd(f, 0x7d); if (fmss_wait_status(f, 100000, &st)) { - pr_info("s5l8740-fmss: param ready timeout NANDSTAT=%02x st48=%08x\n", + pr_info("s5l8740-nand: param ready timeout NANDSTAT=%02x st48=%08x\n", st, f->last_stat48); ret = -ETIMEDOUT; goto out_idle; @@ -1458,10 +1574,10 @@ static int fmss_param_read(struct fmss_n31 *f, unsigned int ce) } /* - * sub_10453C: controller reset only (no NAND array write). + *: controller reset only (no NAND array write). * Followed by per-CE cmd 0xFF as in 130060(a3=1). */ -static int fmss_ctrl_reset(struct fmss_n31 *f) +static int fmss_ctrl_reset(struct nand_s5l8740 *f) { unsigned int i; u32 v; @@ -1504,7 +1620,7 @@ static int fmss_ctrl_reset(struct fmss_n31 *f) udelay(1); } if (!(readl(f->base + FMCTRL1) & 0x40000000u)) { - pr_info("s5l8740-fmss: 10453C FMCTRL1 bit30 timeout v=%08x\n", + pr_info("s5l8740-nand: 10453C FMCTRL1 bit30 timeout v=%08x\n", readl(f->base + FMCTRL1)); return -ETIMEDOUT; } @@ -1514,7 +1630,7 @@ static int fmss_ctrl_reset(struct fmss_n31 *f) } /* 130060: 10453C, then NAND RESET (0xFF) on CE0/CE1. */ -static int fmss_nand_reset(struct fmss_n31 *f) +static int fmss_nand_reset(struct nand_s5l8740 *f) { unsigned int ce; int ret; @@ -1533,7 +1649,7 @@ static int fmss_nand_reset(struct fmss_n31 *f) writel((2u * (1u << ce)) | 0xFF001u, f->base + FMCTRL0); writel(2, f->base + FMSTAT48); if (fmss_cmd(f, 0xff)) - pr_info("s5l8740-fmss: cmd 0xFF timeout ce=%u st=%08x\n", + pr_info("s5l8740-nand: cmd 0xFF timeout ce=%u st=%08x\n", ce, f->last_stat48); } msleep(50); @@ -1558,7 +1674,7 @@ static u32 fmss_le32(const u8 *p, unsigned int off) static ssize_t regs_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; static const u32 offs[] = { FMCTRL0, FMCTRL1, FMCMD, FMADDR, FMCE, FMUNK18, FMUNK24, FMUNK28, FMCYCLES, FMLEN, FMUNK38, FMSTAT48, NANDSTAT, FMDATA, FMSEQ, FMSEQSTAT, @@ -1578,7 +1694,7 @@ static DEVICE_ATTR_RO(regs); static ssize_t id_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; if (!f) return -ENODEV; @@ -1593,7 +1709,7 @@ static DEVICE_ATTR_RO(id); static ssize_t read_id_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce; int ret; @@ -1604,7 +1720,7 @@ static ssize_t read_id_store(struct device *dev, struct device_attribute *attr, mutex_lock(&f->lock); ret = fmss_read_id(f, ce); mutex_unlock(&f->lock); - fmss_dev_info(dev, "read_id ce=%u ret=%d id=%02x%02x%02x%02x%02x%02x%02x%02x\n", + nand_dev_info(dev, "read_id ce=%u ret=%d id=%02x%02x%02x%02x%02x%02x%02x%02x\n", ce, ret, f->last_id[0], f->last_id[1], f->last_id[2], f->last_id[3], f->last_id[4], f->last_id[5], f->last_id[6], f->last_id[7]); @@ -1615,7 +1731,7 @@ static DEVICE_ATTR_WO(read_id); static ssize_t page_status_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; if (!f) return -ENODEV; @@ -1631,7 +1747,7 @@ static DEVICE_ATTR_RO(page_status); static ssize_t page_hex_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; int i, n = 0; if (!f) @@ -1647,7 +1763,7 @@ static DEVICE_ATTR_RO(page_hex); static ssize_t spare_hex_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; int i, n = 0; if (!f) @@ -1677,7 +1793,7 @@ static u8 fmss_meta_type(const u8 *m, unsigned int len) static ssize_t parity_hex_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int i, j, n = 0; if (!f) @@ -1703,7 +1819,7 @@ static DEVICE_ATTR_RO(parity_hex); static ssize_t page_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, a, b, c, d, cycles; u32 addr; int nf, ret; @@ -1738,7 +1854,7 @@ static DEVICE_ATTR_WO(page_read); static ssize_t dma_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, a, b, c, d, cycles; u32 addr; int nf, ret; @@ -1758,8 +1874,10 @@ static ssize_t dma_read_store(struct device *dev, struct device_attribute *attr, return -EINVAL; } mutex_lock(&f->lock); - /* Always re-init PPN before CS — cold CS kick can bus-hang the SoC. */ - fmss_nand_reset(f); + + if (dma_reset_before) + fmss_nand_reset(f); + ret = fmss_dma_page_read(f, ce, addr); f->pages_since_reset++; mutex_unlock(&f->lock); @@ -1767,11 +1885,481 @@ static ssize_t dma_read_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(dma_read); +/* + * CS descriptor ABI canary — no FTL ingest / no lba_map mutation. + * + * Physical page: ce=0 cau=0 block=63 page=88 slc=0 + * + * PIO may show *UOKJIHC at non-aligned offset 7816 — that is a raw + * positive-control artifact only. CS full-page shows sector-aligned + * boot-like payloads at 0 and 8192 (= 2×4096). Prefer rec=4112 + * (4096 data + 16 meta). Span4 is the primary trusted CS path; + * sub-slot is optional. + */ +#define CS_CANARY_MARK "*UOKJIHC" +#define CS_CANARY_MARK_LEN 8 +#define CS_CANARY_CE 0u +#define CS_CANARY_CAU 0u +#define CS_CANARY_BLOCK 63u +#define CS_CANARY_PAGE 88u +#define CS_CANARY_SLC 0u +#define CS_CANARY_PIO_OFF 7816u /* PIO-only artifact; not CS expect */ +#define CS_CANARY_SLOT2_OFF 8192u +#define CS_CANARY_OEM_DELTA 3u +#define CS_CANARY_SECTOR 4096u +#define CS_CANARY_META 16u + +static char cs_canary_log[8192]; + +static int fmss_find_bytes(const u8 *p, unsigned int n, + const char *s, unsigned int sl) +{ + unsigned int i; + + if (!sl || sl > n) + return -1; + for (i = 0; i + sl <= n; i++) { + if (!memcmp(p + i, s, sl)) + return (int)i; + } + return -1; +} + +static bool fmss_meta_nonblank(const u8 *m, unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) { + if (m[i] != 0x00 && m[i] != 0xff) + return true; + } + return false; +} + +static bool fmss_looks_like_bpb(const u8 *p) +{ + return (p[0] == 0xeb || p[0] == 0xe9) && + (p[2] == 0x90 || p[2] == 0x00); +} + +/* Sector-aligned CS hit: BPB jump at expect_off and/or OEM at +3. */ +static bool fmss_cs_canary_hit(const u8 *dp, unsigned int data_len, + unsigned int expect_off, int *mark_off) +{ + int hit; + + hit = fmss_find_bytes(dp, data_len, CS_CANARY_MARK, CS_CANARY_MARK_LEN); + if (mark_off) + *mark_off = hit; + if (expect_off + 3 <= data_len && fmss_looks_like_bpb(dp + expect_off)) + return true; + if (hit == (int)(expect_off + CS_CANARY_OEM_DELTA)) + return true; + return false; +} + +static u64 fmss_le48(const u8 *p) +{ + return (u64)p[0] | ((u64)p[1] << 8) | ((u64)p[2] << 16) | + ((u64)p[3] << 24) | ((u64)p[4] << 32) | ((u64)p[5] << 40); +} + +static int fmss_cs_slot_report(const u8 *dp, const u8 *mp, + unsigned int slot, char *out, size_t out_sz) +{ + const u8 *d = dp + slot * CS_CANARY_SECTOR; + const u8 *m = mp + slot * CS_CANARY_META; + u16 bps = (u16)d[11] | ((u16)d[12] << 8); + u8 spc = d[13]; + u32 lba = (u32)m[8] | ((u32)m[9] << 8) | + ((u32)m[10] << 16) | ((u32)m[11] << 24); + int mark = fmss_find_bytes(d, CS_CANARY_SECTOR, + CS_CANARY_MARK, CS_CANARY_MARK_LEN); + + return scnprintf(out, out_sz, + " slot%u data[0..15]=%*ph oem=%*ph bpb=%d " + "bps=%u spc=%u mark_off=%d\n" + " slot%u meta=%*ph type=%02x flags=%02x " + "weave=%012llx lba=%u aux=%*ph nb=%d\n", + slot, 16, d, 8, d + 3, fmss_looks_like_bpb(d), + bps, spc, mark, + slot, 16, m, m[0], m[1], + (unsigned long long)fmss_le48(m + 2), lba, + 4, m + 12, fmss_meta_nonblank(m, 16)); +} + +static int fmss_cs_canary_one(struct nand_s5l8740 *f, unsigned int ce, u32 addr, + unsigned int slot, unsigned int span, + unsigned int rec, unsigned int expect_off, + bool slot_report, char *out, size_t out_sz) +{ + unsigned int saved_slot = dma_slot; + unsigned int saved_nsect = dma_nsect; + unsigned int saved_rec = dma_rec; + bool saved_armed = dma_armed; + int ret, hit, n, s; + bool pass; + u8 *dp, *sp, *st; + unsigned int data_len; + + dma_slot = slot; + dma_nsect = span; + dma_rec = rec; + + if (dma_reset_before) + fmss_nand_reset(f); + + if (!dma_dry) + dma_armed = true; + + dma_skip_ingest = true; + ret = fmss_dma_page_read(f, ce, addr); + dma_skip_ingest = false; + + dma_slot = saved_slot; + dma_nsect = saved_nsect; + dma_rec = saved_rec; + if (dma_dry) + dma_armed = saved_armed; + + dp = f->last_page; + sp = f->last_spare; + st = f->stbuf ? f->stbuf : f->last_spare; + data_len = f->last_page_len; + if (data_len > FMSS_PAGE_LEN) + data_len = FMSS_PAGE_LEN; + + pass = (expect_off != 0xffffffffu) && + fmss_cs_canary_hit(dp, data_len, expect_off, &hit); + if (expect_off == 0xffffffffu) + fmss_cs_canary_hit(dp, data_len, 0, &hit); + + n = scnprintf(out, out_sz, + "case slot=%u span=%u rec=%u ret=%d dry=%d expect_off=%u " + "mark_off=%d meta_nb=%d\n" + " c00=%08x c04=%08x c08=%08x c0c=%08x c6c=%08x\n" + " d00=%08x d04=%08x d08=%08x d0c=%08x d10=%08x d14=%08x\n" + " st[0..15]=%*ph\n" + " meta[0..15]=%*ph\n" + " data[0..15]=%*ph\n", + slot, span, rec, ret, dma_dry, + expect_off == 0xffffffffu ? 0 : expect_off, hit, + fmss_meta_nonblank(sp, 16), + f->last_dma_c00, readl(f->base + FMSEQBASE), + readl(f->base + FMSEQSTAT), f->last_dma_c0c, + readl(f->base + 0xc6c), + f->last_dma_d00, readl(f->base + FMGEN1), + readl(f->base + FMGEN2), readl(f->base + FMGEN3), + readl(f->base + FMGEN4), readl(f->base + FMGEN5), + 16, st, 16, sp, 16, dp); + + if (expect_off != 0xffffffffu && expect_off < data_len) { + n += scnprintf(out + n, out_sz - n, + " data@expect=%*ph%s\n", + 16, dp + expect_off, + pass ? " HIT" : + (hit >= 0) ? " ELSEWHERE" : " MISS"); + } + if (slot_report && span == 4 && data_len >= FMSS_PAGE_LEN) { + for (s = 0; s < 4; s++) + n += fmss_cs_slot_report(dp, sp, s, + out + n, out_sz - n); + } + if (pass) + n += scnprintf(out + n, out_sz - n, " RESULT=PASS_MARKER\n"); + else if (hit >= 0) + n += scnprintf(out + n, out_sz - n, + " RESULT=MARKER_AT_%d\n", hit); + else if (!ret || ret == -EAGAIN) + n += scnprintf(out + n, out_sz - n, " RESULT=NO_MARKER\n"); + else + n += scnprintf(out + n, out_sz - n, " RESULT=ERR\n"); + + return n; +} + +static ssize_t cs_canary_read_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (!cs_canary_log[0]) + return sysfs_emit(buf, "no canary yet\n"); + return sysfs_emit(buf, "%s", cs_canary_log); +} + +static ssize_t cs_canary_read_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nand_s5l8740 *f = nand_dev; + unsigned int ce, a, b, c, d; + unsigned int slot = 0, span = 1, rec = FMSS_PPN_REC; + unsigned int expect_off = 0xffffffffu; + u32 addr; + int nf, n; + + if (!f) + return -ENODEV; + if (!f->dma_ok) + return -ENODEV; + + nf = sscanf(buf, "%u %i %u %u %u %u %u %u", + &ce, &a, &b, &c, &d, &slot, &span, &rec); + if (nf >= 5) { + if (a > 1 || b >= FMSS_BLOCKS_PER_CAU || c > FMSS_BTOC_PAGE) + return -EINVAL; + addr = fmss_ppn_addr(a, b, c, d); + if (nf < 6) + slot = dma_slot; + if (nf < 7) + span = 1; + if (nf < 8) + rec = dma_rec ? dma_rec : FMSS_PPN_REC; + } else { + nf = sscanf(buf, "%u %i %u %u %u", + &ce, &addr, &slot, &span, &rec); + if (nf < 2) + return -EINVAL; + if (nf < 3) + slot = dma_slot; + if (nf < 4) + span = 1; + if (nf < 5) + rec = dma_rec ? dma_rec : FMSS_PPN_REC; + } + + if (slot > 3 || span < 1 || span > 4 || slot + span > 4) + return -EINVAL; + if (rec != 4096 && rec != 4112) + return -EINVAL; + + /* Sector-aligned expects only. */ + if (span == 1) + expect_off = 0; + else if (span == 4 && slot == 0) + expect_off = CS_CANARY_SLOT2_OFF; /* Apple boot in slot2 */ + + if (!dma_dry && !dma_armed) + return -EPERM; + + mutex_lock(&f->lock); + n = scnprintf(cs_canary_log, sizeof(cs_canary_log), + "cs_canary ce=%u addr=%08x slot=%u span=%u rec=%u\n", + ce, addr, slot, span, rec); + n += fmss_cs_canary_one(f, ce, addr, slot, span, rec, expect_off, + span == 4, cs_canary_log + n, + sizeof(cs_canary_log) - n); + f->pages_since_reset++; + nand_dev_info(dev, "%s", cs_canary_log); + mutex_unlock(&f->lock); + return count; +} +static DEVICE_ATTR_RW(cs_canary_read); + +static ssize_t cs_canary_matrix_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + size_t len; + + if (!cs_canary_log[0]) + return sysfs_emit(buf, + "usage: echo 1 > cs_canary_matrix\n" + "page ce=%u cau=%u blk=%u pg=%u\n" + "PIO artifact off=%u (not CS expect)\n" + "E slot2/span1/rec4112 expect data@0 BPB/OEM\n" + "F slot2/span1/rec4096 (meta-in-data check)\n" + "G slot0/span4/rec4112 per-slot BPB+meta\n" + "A/B slot1 legacy; C/D span4 legacy\n" + "(full log also in dmesg)\n", + CS_CANARY_CE, CS_CANARY_CAU, CS_CANARY_BLOCK, + CS_CANARY_PAGE, CS_CANARY_PIO_OFF); + len = strnlen(cs_canary_log, sizeof(cs_canary_log)); + if (len >= PAGE_SIZE) + len = PAGE_SIZE - 1; + memcpy(buf, cs_canary_log, len); + buf[len] = '\0'; + return len; +} + +static ssize_t cs_canary_matrix_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nand_s5l8740 *f = nand_dev; + u32 addr; + unsigned int on = 0; + int n; + struct { + unsigned int slot, span, rec, expect; + bool slot_report; + char tag; + } cases[] = { + /* Primary: sector-aligned slot2 + span4 decode. */ + { 2, 1, 4112, 0, false, 'E' }, + { 2, 1, 4096, 0, false, 'F' }, + { 0, 4, 4112, CS_CANARY_SLOT2_OFF, true, 'G' }, + }; + unsigned int i; + + if (!f) + return -ENODEV; + if (!f->dma_ok) + return -ENODEV; + if (kstrtouint(buf, 0, &on) || !on) + return -EINVAL; + if (!dma_dry && !dma_armed) + return -EPERM; + + addr = fmss_ppn_addr(CS_CANARY_CAU, CS_CANARY_BLOCK, + CS_CANARY_PAGE, CS_CANARY_SLC); + + mutex_lock(&f->lock); + n = scnprintf(cs_canary_log, sizeof(cs_canary_log), + "cs_canary_matrix ce=%u cau=%u blk=%u pg=%u addr=%08x " + "dry=%d\n" + "note: PIO off=%u is artifact; CS expect sector @0/@8192\n", + CS_CANARY_CE, CS_CANARY_CAU, CS_CANARY_BLOCK, + CS_CANARY_PAGE, addr, dma_dry, CS_CANARY_PIO_OFF); + + /* PIO control — proves page exists; do not require CS at 7816. */ + { + int pret, phit; + unsigned int saved_chunks = page_chunks; + unsigned int plen; + + page_chunks = FMSS_MAX_CHUNKS; + pret = fmss_page_read(f, CS_CANARY_CE, addr); + page_chunks = saved_chunks; + plen = f->last_page_len; + if (plen > FMSS_PAGE_LEN) + plen = FMSS_PAGE_LEN; + phit = fmss_find_bytes(f->last_page, plen, + CS_CANARY_MARK, CS_CANARY_MARK_LEN); + n += scnprintf(cs_canary_log + n, sizeof(cs_canary_log) - n, + "PIO ret=%d mark_off=%d (artifact expect~%u) %s\n", + pret, phit, CS_CANARY_PIO_OFF, + (phit >= 0) ? "PASS_CONTROL" : "FAIL_CONTROL"); + if (pret || phit < 0) { + nand_dev_info(dev, "%s", cs_canary_log); + mutex_unlock(&f->lock); + return -EIO; + } + } + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + n += scnprintf(cs_canary_log + n, sizeof(cs_canary_log) - n, + "\n=== %c ===\n", cases[i].tag); + n += fmss_cs_canary_one(f, CS_CANARY_CE, addr, + cases[i].slot, cases[i].span, + cases[i].rec, cases[i].expect, + cases[i].slot_report, + cs_canary_log + n, + sizeof(cs_canary_log) - n); + f->pages_since_reset++; + } + + nand_dev_info(dev, "%s", cs_canary_log); + mutex_unlock(&f->lock); + return count; +} +static DEVICE_ATTR_RW(cs_canary_matrix); + +static char cs_phys_log[2048]; + +static ssize_t cs_phys_read_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (!cs_phys_log[0]) + return sysfs_emit(buf, + "usage: echo CE CAU BLK PG [fmss_lba] > cs_phys_read\n" + "CS span4/rec4112 physical page; optional fmss_lba pick\n" + "requires dma_dry=0 dma_armed=1; no lba_map ingest\n"); + return sysfs_emit(buf, "%s", cs_phys_log); +} + +static ssize_t cs_phys_last_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return cs_phys_read_show(dev, attr, buf); +} +static DEVICE_ATTR_RO(cs_phys_last); + +static ssize_t cs_phys_read_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nand_s5l8740 *f = nand_dev; + unsigned int ce, cau, block, page; + u32 want_lba = 0xffffffffu; + struct s5l8740_cs_page *pg; + int nf, ret, n, s, pick; + + if (!f || !f->dma_ok) + return -ENODEV; + /* Accept optional trailing SLC (ignored; CS phys is always MLC=0). */ + nf = sscanf(buf, "%u %u %u %u %u", + &ce, &cau, &block, &page, &want_lba); + if (nf < 4) + return -EINVAL; + /* + * Legacy: "ce cau blk pg slc [lba]" — if 5th token is 0/1 treat as + * SLC and take optional 6th as fmss_lba. + */ + if (nf == 5 && want_lba <= 1) { + unsigned int slc_ignored = want_lba; + u32 lba6 = 0xffffffffu; + int n6 = sscanf(buf, "%u %u %u %u %u %u", + &ce, &cau, &block, &page, &slc_ignored, &lba6); + (void)slc_ignored; + want_lba = (n6 >= 6) ? lba6 : 0xffffffffu; + } + + pg = kzalloc(sizeof(*pg), GFP_KERNEL); + if (!pg) + return -ENOMEM; + + ret = s5l8740_nand_cs_phys_read(ce, cau, block, page, pg); + n = scnprintf(cs_phys_log, sizeof(cs_phys_log), + "cs_phys_read ce=%u cau=%u blk=%u pg=%u ret=%d " + "rec=%u span=4\n", + ce, cau, block, page, ret, N31_CS_REC_SIZE); + if (!ret) { + for (s = 0; s < N31_DATA_SLOTS; s++) { + const struct s5l8740_meta_decoded *sm = &pg->meta[s]; + + n += scnprintf(cs_phys_log + n, sizeof(cs_phys_log) - n, + "slot%u data=%*ph bpb=%d meta type=%02x " + "weave=%012llx fmss_lba=%u valid=%d " + "data_rec=%d\n", + s, 8, pg->data[s], + (pg->data[s][0] == 0xeb || + pg->data[s][0] == 0xe9), + sm->type, (unsigned long long)sm->weave, + sm->lba, sm->valid, + n31_meta_is_data_record(sm)); + } + if (want_lba != 0xffffffffu) { + pick = s5l8740_nand_meta_pick_lba(pg, want_lba); + n += scnprintf(cs_phys_log + n, sizeof(cs_phys_log) - n, + "pick_fmss_lba=%u -> slot=%d weave=%012llx " + "type=%02x\n", + want_lba, pick, + pick >= 0 ? + (unsigned long long)pg->meta[pick].weave : + 0ULL, + pick >= 0 ? pg->meta[pick].type : 0); + } + } + nand_dev_info(dev, "%s", cs_phys_log); + kfree(pg); + return ret && ret != -EAGAIN && ret != -EPERM ? ret : count; +} +static DEVICE_ATTR_RW(cs_phys_read); + static int fmss_page_blankish(const u8 *p, unsigned int n); /* * PPN DATA spare (CS META stream, type 0x01): - * +0 type, +1 bank/flags, +2..+7 weaveSeq48, +8..+11 LBA LE, +12..+15 aux. + * +0 type, +1 bank/flags, +2..+7 weaveSeq48, +8..+11 LBA LE, +12..+15 aux. * Whimory chooses the newest weave claimant before FMSS; manual phys reads * can hit stale historical LBA copies. lba_weave_scan lists them newest-first. */ @@ -1801,7 +2389,7 @@ static unsigned int lba_claim_be_alt; /* BE@+8 matched a target */ static char lba_claim_log[PAGE_SIZE]; static unsigned int lba_claim_log_len; -/* N31 META: weave[15:0]@+2 LE16 | weave[47:16]@+4 LE32 (5688C4 / 568ED4) */ +/* N31 META: weave[15:0]@+2 LE16 | weave[47:16]@+4 LE32 (5688C4 / 568ED4) */ static u64 fmss_ppn_weave48(const u8 *m) { return (u64)get_unaligned_le16(m + 2) | @@ -1849,7 +2437,7 @@ static void fmss_lba_claim_insert(const struct fmss_lba_claim *c) lba_claims[i] = *c; } -static void fmss_lba_claim_note_page(struct fmss_n31 *f, unsigned int ce, +static void fmss_lba_claim_note_page(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, u32 ppn) { @@ -1867,9 +2455,9 @@ static void fmss_lba_claim_note_page(struct fmss_n31 *f, unsigned int ce, meta = f->last_spare + s * 16; /* - * Diagnostic: any type whose LE LBA matches a target. - * Production mapping still prefers type 0x01. - */ + * Diagnostic: any type whose LE LBA matches a target. + * Production mapping still prefers type 0x01. + */ lba_le = fmss_ppn_meta_lba(meta); lba_be = get_unaligned_be32(meta + 8); if (meta[0] == 0x01 && lba_le < 4096u) @@ -1890,7 +2478,7 @@ static void fmss_lba_claim_note_page(struct fmss_n31 *f, unsigned int ce, fmss_lba_claim_insert(&c); lba_claim_hits++; /* Raw META for positive-control / endian debug. */ - pr_info("s5l8740-fmss: META hit lba=%u typ=%02x weave=%012llx ce=%u cau=%u blk=%u pg=%u slot=%u meta=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x\n", + pr_info("s5l8740-nand: META hit lba=%u typ=%02x weave=%012llx ce=%u cau=%u blk=%u pg=%u slot=%u meta=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x\n", lba_le, meta[0], (unsigned long long)c.weave, ce, cau, block, page, s, meta[0], meta[1], meta[2], meta[3], @@ -1918,7 +2506,7 @@ static int fmss_page_erased(const u8 *p, unsigned int n) * echo "121,122,123 [START [NBLOCKS]]" > lba_weave_scan * Default: START=0 NBLOCKS=256 (user area; skip VFL tail). */ -static int fmss_lba_weave_scan(struct fmss_n31 *f, unsigned int start, +static int fmss_lba_weave_scan(struct nand_s5l8740 *f, unsigned int start, unsigned int nblocks) { unsigned int ce, cau, b, pg; @@ -1979,7 +2567,7 @@ static int fmss_lba_weave_scan(struct fmss_n31 *f, unsigned int start, break; } if ((b & 7) == 0) { - pr_info("s5l8740-fmss: lba_weave_scan prog targets=%u scanned=%u hits=%u small=%u be_alt=%u ce=%u cau=%u blk=%u\n", + pr_info("s5l8740-nand: lba_weave_scan prog targets=%u scanned=%u hits=%u small=%u be_alt=%u ce=%u cau=%u blk=%u\n", lba_claim_ntargets, lba_claim_scanned, lba_claim_hits, lba_claim_small, lba_claim_be_alt, @@ -2018,7 +2606,7 @@ static int fmss_lba_weave_scan(struct fmss_n31 *f, unsigned int start, b, c->lba, (unsigned long long)c->weave, c->ce, c->cau, c->block, c->page, c->slot, c->ppn); } - pr_info("s5l8740-fmss: lba_weave_scan %s", lba_claim_log); + pr_info("s5l8740-nand: lba_weave_scan %s", lba_claim_log); return 0; } @@ -2063,7 +2651,7 @@ static ssize_t lba_weave_scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int start = 0, nblocks = 256; int ret; @@ -2091,7 +2679,7 @@ static DEVICE_ATTR_RW(lba_weave_scan); static ssize_t seq_kick_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int cmd; if (!f) @@ -2099,10 +2687,16 @@ static ssize_t seq_kick_store(struct device *dev, struct device_attribute *attr, if (kstrtouint(buf, 0, &cmd)) return -EINVAL; mutex_lock(&f->lock); + if (!dma_armed) { + mutex_unlock(&f->lock); + return -EPERM; + } + if (dma_one_shot) + dma_armed = false; writel(cmd, f->base + FMSEQ); f->last_dma_c00 = readl(f->base + FMSEQ); f->last_dma_c0c = readl(f->base + FMSEQIRQ); - fmss_dev_info(dev, "seq_kick wrote 0x%x now c00=%08x c08=%08x c0c=%08x c04=%08x c38=%08x\n", + nand_dev_info(dev, "seq_kick wrote 0x%x now c00=%08x c08=%08x c0c=%08x c04=%08x c38=%08x\n", cmd, f->last_dma_c00, readl(f->base + FMSEQSTAT), f->last_dma_c0c, readl(f->base + FMSEQBASE), readl(f->base + 0xc38)); @@ -2284,7 +2878,7 @@ static bool fmss_btoc_prefer_le(const u8 *btoc) return false; } -/* D: probe 2026-08-24: EB 3C 90 OEM "*UOKJIHC", vol "AISPOD FAT32" +/* D: probe 2026-08-24: EB 3C 90 OEM "*UOKJIHC", vol "AISPOD FAT32" * Live copy may have 55AA at 0x1C9 rather than 510 — match OEM, patch on carve. */ static bool fmss_apple_fat_sig(const u8 *s) @@ -2346,7 +2940,7 @@ static unsigned int fmss_bpb_data_start(const u8 *bpb) if (!nfats || nfats > 4 || !fatz) return 1916; start = (unsigned int)rsvd + (unsigned int)nfats * fatz; - if (!start || start > FMSS_FTL_DEFAULT_CAPACITY) + if (!start || start > NAND_FTL_DEFAULT_CAPACITY) return 1916; return start; } @@ -2448,7 +3042,7 @@ static int fmss_lba_map_ensure(void) fmss_lba_map_free(); return -ENOMEM; } - pr_info("fmss-s5l8740: LBA dense map cap=%u (~%u KiB)\n", + pr_info("nand-s5l8740: LBA dense map cap=%u (~%u KiB)\n", cap, (cap * (4 + 8 + 1)) / 1024); return 0; } @@ -2559,7 +3153,7 @@ static void fmss_l2v_set_ex(unsigned int lpn, unsigned int ce, unsigned int cau, l2v_mapped++; fmss_l2v_index_note(lpn, ce, cau, block, page); if (!quiet && l2v_mapped <= 8) - pr_info("s5l8740-fmss: l2v_set lpn=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u weave=%llx\n", + pr_info("s5l8740-nand: l2v_set lpn=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u weave=%llx\n", lpn, src, phys, ce, cau, block, page, (unsigned long long)weave); } @@ -2587,7 +3181,7 @@ static void fmss_lba_set(unsigned int lba, unsigned int ce, unsigned int cau, if (!(prev & L2V_VALID)) lba_mapped++; if (!quiet && lba_mapped <= 8) - pr_info("s5l8740-fmss: lba_set lba=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u sec=%u weave=%llx\n", + pr_info("s5l8740-nand: lba_set lba=%u src=%u phys=%d ce=%u cau=%u blk=%u pg=%u sec=%u weave=%llx\n", lba, src, phys, ce, cau, block, page, sec & 3u, (unsigned long long)weave); } @@ -2642,7 +3236,7 @@ static void fmss_early_lba_free(void) * Pass 2: promote PIO/DMA META slots into full lba_map. * type 0x01 data records; weave newest-wins via fmss_lba_set. */ -static void fmss_meta_ingest_spare(struct fmss_n31 *f, unsigned int ce, +static void fmss_meta_ingest_spare(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, unsigned int slot0) { @@ -2798,11 +3392,11 @@ static bool fmss_btoc_ingestible(const u8 *btoc) return false; } -static int fmss_boot_carve_try(struct fmss_n31 *f, unsigned int ce, +static int fmss_boot_carve_try(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page); -static void fmss_l2v_ingest_btoc(struct fmss_n31 *f, unsigned int ce, +static void fmss_l2v_ingest_btoc(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, const u8 *btoc, unsigned int max_lpn) { @@ -2822,10 +3416,10 @@ static void fmss_l2v_ingest_btoc(struct fmss_n31 *f, unsigned int ce, continue; if (lpn == 0) { /* - * Do not fmss_boot_carve_try here — nested full-page - * reads during the BTOC walk wedge FMSS. Discover - * handles BTOC[0]==0 after the walk. - */ + * Avoid fmss_boot_carve_try here — nested full-page + * reads during the BTOC walk wedge FMSS. Discover + * handles BTOC[0]==0 after the walk. + */ continue; } fmss_l2v_set_ex(lpn, ce, cau, block, p, true, L2V_SRC_BTOC, 0); @@ -2843,8 +3437,8 @@ static void fmss_l2v_ingest_btoc(struct fmss_n31 *f, unsigned int ce, /* * SFTL on-flash BTOC (meta type 28): 16-byte BE records - * +0 weaveSeqAdd, +4 aux, +8 lba, +12 … +15 span in low byte (live: - * 00 00 00 00 | a7 00 00 1d | 00 00 00 79 | 05 00 00 02 → lba=121 span=2). + * +0 weaveSeqAdd, +4 aux, +8 lba, +12 … +15 span in low byte (live: + * 00 00 00 00 | a7 00 00 1d | 00 00 00 79 | 05 00 00 02 → lba=121 span=2). * Used when YaFTL u32 LPN table is not ingestible. */ static bool fmss_page_looks_bte(const u8 *page) @@ -2869,7 +3463,7 @@ static bool fmss_page_looks_bte(const u8 *page) return true; } -static void fmss_l2v_ingest_bte(struct fmss_n31 *f, unsigned int ce, +static void fmss_l2v_ingest_bte(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, const u8 *page, unsigned int max_lpn) { @@ -2964,9 +3558,9 @@ static unsigned int fmss_vfl_phys(unsigned int cau, unsigned int virt); static unsigned int fmss_vfl_resolve(unsigned int cau, unsigned int virt); static unsigned int fmss_map_to_phys(unsigned int cau, unsigned int block, u32 packed); -static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, +static int fmss_vfl_ingest(struct nand_s5l8740 *f, unsigned int cau, unsigned int block, const u8 *hdr); -static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, +static int fmss_read_lpn_page(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, u8 *dst, unsigned int dst_len); @@ -3087,7 +3681,7 @@ static bool fmss_wmr_vpage_to_phys(u32 vpage, unsigned int *ce, } } -static void fmss_wmr_maybe_reset(struct fmss_n31 *f) +static void fmss_wmr_maybe_reset(struct nand_s5l8740 *f) { if (reset_every && f->pages_since_reset >= reset_every) { fmss_nand_reset(f); @@ -3096,7 +3690,7 @@ static void fmss_wmr_maybe_reset(struct fmss_n31 *f) } /* Bounded DEVICEINFOSIGN hunt: early blocks + VFL tail, page 0 only. */ -static void fmss_wmr_scan_deviceinfo(struct fmss_n31 *f, unsigned int nblocks) +static void fmss_wmr_scan_deviceinfo(struct nand_s5l8740 *f, unsigned int nblocks) { unsigned int ce, cau, b, saved; unsigned int start_tail; @@ -3158,7 +3752,7 @@ static void fmss_wmr_scan_deviceinfo(struct fmss_n31 *f, unsigned int nblocks) } /* Tail VFL wrmx/xrmw ingest + optional classic ftlctrlblocks. */ -static void fmss_wmr_scan_vfl(struct fmss_n31 *f, unsigned int nblocks) +static void fmss_wmr_scan_vfl(struct nand_s5l8740 *f, unsigned int nblocks) { unsigned int ce, cau, i, saved, start; u16 ctrl[3]; @@ -3207,7 +3801,7 @@ static void fmss_wmr_scan_vfl(struct fmss_n31 *f, unsigned int nblocks) page_chunks = saved; } -static void fmss_wmr_try_load_bmap(struct fmss_n31 *f, unsigned int ce, +static void fmss_wmr_try_load_bmap(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int *dst_off) { @@ -3228,7 +3822,7 @@ static void fmss_wmr_try_load_bmap(struct fmss_n31 *f, unsigned int ce, } /* Probe ftlctrl blocks + bounded early/tail for type-0x44-like maps. */ -static void fmss_wmr_scan_block_maps(struct fmss_n31 *f, unsigned int nblocks) +static void fmss_wmr_scan_block_maps(struct nand_s5l8740 *f, unsigned int nblocks) { unsigned int ce, cau, b, i, saved, dst = 0; unsigned int start_tail; @@ -3298,7 +3892,7 @@ static unsigned int fmss_wmr_fill_l2v(unsigned int max_lpn) continue; if (!fmss_wmr_vpage_to_phys(vpage, &ce, &cau, &block, &page)) continue; - /* Do not clobber denser BTOC hits already present. */ + /* Avoid clobber denser BTOC hits already present. */ if (l2v_map && lpn < l2v_map_size && (l2v_map[lpn] & L2V_VALID)) continue; @@ -3314,9 +3908,9 @@ static unsigned int fmss_wmr_fill_l2v(unsigned int max_lpn) * Classic freemyipod Whimory mount adapted for N31 PPN. * Does not clear existing l2v_build / boot_carve results. * Usage: echo 1 > whimory_mount - * echo "NBLOCKS [MAX_LPN]" > whimory_mount + * echo "NBLOCKS [MAX_LPN]" > whimory_mount */ -static int fmss_whimory_mount(struct fmss_n31 *f, unsigned int nblocks, +static int fmss_whimory_mount(struct nand_s5l8740 *f, unsigned int nblocks, unsigned int max_lpn) { if (!max_lpn) @@ -3338,7 +3932,7 @@ static int fmss_whimory_mount(struct fmss_n31 *f, unsigned int nblocks, else wmr_mount_ret = 0; - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "whimory_mount n=%u max_lpn=%u dis=%u vfl=%u ftlctrl=%u bmap_pages=%u map_ents=%u filled=%u ret=%d\n", nblocks, max_lpn, wmr_dis_hits, wmr_vfl_hits, wmr_ftlctrl_hits, wmr_bmap_pages, wmr_block_map_n, @@ -3384,7 +3978,7 @@ static unsigned int fmss_vfl_resolve(unsigned int cau, unsigned int virt) if (vfl_map[i].cau == cau && vfl_map[i].virt == virt) { vfl_remap_applied++; if (!quiet && vfl_remap_applied <= 32) - pr_info("s5l8740-fmss: vfl_remap mode=%s cau=%u in=%u out=%u idx=%u\n", + pr_info("s5l8740-nand: vfl_remap mode=%s cau=%u in=%u out=%u idx=%u\n", vfl_remap_mode, cau, virt, phys, i); return phys; } @@ -3406,7 +4000,7 @@ static unsigned int fmss_map_to_phys(unsigned int cau, unsigned int block, * wrmx/xrmw VFLCxt: 512-byte header, u32 remap table begins @ +0x100. * Live pod: entries are LE phys block numbers (e.g. 0x827 = 2087). */ -static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, +static int fmss_vfl_ingest(struct nand_s5l8740 *f, unsigned int cau, unsigned int block, const u8 *hdr) { unsigned int i, virt, phys, added = 0; @@ -3440,12 +4034,12 @@ static int fmss_vfl_ingest(struct fmss_n31 *f, unsigned int cau, added++; } } - fmss_dev_info(f->dev, "vfl_ingest cau=%u blk=%u magic=%s entries=%u total=%u\n", + nand_dev_info(f->dev, "vfl_ingest cau=%u blk=%u magic=%s entries=%u total=%u\n", cau, block, magic, added, vfl_map_count); return added; } -static void fmss_vfl_format_log(struct fmss_n31 *f, unsigned int ce, +static void fmss_vfl_format_log(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, u32 addr) { unsigned int i, off = 0; @@ -3530,12 +4124,12 @@ static DEVICE_ATTR_RO(lpn_index); /* * Read one VFL context page (SLC page 0) and capture 512-byte header in vfl_log. - * Usage: echo "CE CAU BLOCK" > vfl_dump (CE/CAU default 0) + * Usage: echo "CE CAU BLOCK" > vfl_dump (CE/CAU default 0) */ static ssize_t vfl_dump_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce = 0, cau = 0, block; unsigned int saved; u32 addr; @@ -3571,16 +4165,16 @@ static ssize_t vfl_dump_store(struct device *dev, struct device_attribute *attr, if (!ret) fmss_vfl_ingest(f, cau, block, f->last_page); mutex_unlock(&f->lock); - fmss_dev_info(dev, "vfl_dump ce=%u cau=%u blk=%u ret=%d\n", ce, cau, block, ret); + nand_dev_info(dev, "vfl_dump ce=%u cau=%u blk=%u ret=%d\n", ce, cau, block, ret); return ret ? ret : count; } static DEVICE_ATTR_WO(vfl_dump); -static int fmss_find_lpn(struct fmss_n31 *f, unsigned int target_lpn, +static int fmss_find_lpn(struct nand_s5l8740 *f, unsigned int target_lpn, unsigned int *oce, unsigned int *ocau, unsigned int *oblock, unsigned int *opage); -static int fmss_lpn_resolve(struct fmss_n31 *f, unsigned int target_lpn, +static int fmss_lpn_resolve(struct nand_s5l8740 *f, unsigned int target_lpn, unsigned int *oce, unsigned int *ocau, unsigned int *oblock, unsigned int *opage) { @@ -3601,7 +4195,7 @@ static int fmss_lpn_resolve(struct fmss_n31 *f, unsigned int target_lpn, return fmss_find_lpn(f, target_lpn, oce, ocau, oblock, opage); } -static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, +static int fmss_read_lpn_page(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, u8 *dst, unsigned int dst_len) { @@ -3609,7 +4203,7 @@ static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, u32 addr; int ret; - /* Callers pass physical scan blocks (find_lpn / BTOC). Do not VFL-remap. */ + /* Callers pass physical scan blocks (find_lpn / BTOC). Avoid VFL-remap. */ pblock = fmss_map_to_phys(cau, block, L2V_PHYS); saved = page_chunks; page_chunks = 16; @@ -3635,7 +4229,7 @@ static int fmss_read_lpn_page(struct fmss_n31 *f, unsigned int ce, return ret; } -static int fmss_find_lpn(struct fmss_n31 *f, unsigned int target_lpn, +static int fmss_find_lpn(struct nand_s5l8740 *f, unsigned int target_lpn, unsigned int *oce, unsigned int *ocau, unsigned int *oblock, unsigned int *opage) { @@ -3701,7 +4295,7 @@ static int fmss_find_lpn(struct fmss_n31 *f, unsigned int target_lpn, return found ? 0 : -ENOENT; } -static void fmss_boot_apply_bpb(struct fmss_n31 *f, unsigned int ce, +static void fmss_boot_apply_bpb(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, const u8 *bpb) { @@ -3725,7 +4319,7 @@ static void fmss_boot_apply_bpb(struct fmss_n31 *f, unsigned int ce, /* L2V[0] / LBA0 must point at this real boot page (physical). */ fmss_l2v_set_ex(0, ce, cau, block, page, true, L2V_SRC_CARVE, 0); fmss_lba_set(0, ce, cau, block, page, 0, true, L2V_SRC_CARVE, 0); - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "boot_sb ce=%u cau=%u blk=%u pg=%u DataStart=%u rsv=%u fatz=%u\n", ce, cau, block, page, boot_data_start, boot_reserved_sects, boot_fat_sects); @@ -3735,7 +4329,7 @@ static void fmss_boot_apply_bpb(struct fmss_n31 *f, unsigned int ce, * Accept ONLY an aligned live boot sector: BPB at page offset 0 with * 55AA@510. Mid-page *UOKJIHC (e.g. off=7816) is a file copy — reject. */ -static int fmss_boot_carve_try(struct fmss_n31 *f, unsigned int ce, +static int fmss_boot_carve_try(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page) { @@ -3755,7 +4349,7 @@ static int fmss_boot_carve_try(struct fmss_n31 *f, unsigned int ce, /* * Try aligned BPB on page p; on success ingest the saved BTOC table. */ -static int fmss_boot_try_btoc_page(struct fmss_n31 *f, unsigned int ce, +static int fmss_boot_try_btoc_page(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, const u8 *btoc_page, unsigned int page) { @@ -3780,7 +4374,7 @@ static int fmss_boot_try_btoc_page(struct fmss_n31 *f, unsigned int ce, * BTOC[0]==0 && BTOC[1] in {1,vbas_per_page}), then aligned BPB on that page. * Falls back to page-0 aligned BPB scan (never mid-page OEM). */ -static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, +static int fmss_boot_carve_discover(struct nand_s5l8740 *f, unsigned int start, unsigned int nblocks) { unsigned int ce, cau, b, p, saved; @@ -3796,7 +4390,7 @@ static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, boot_carve_page_param ? boot_carve_page_param : 0)) return 0; - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "cached boot_sb blk%u miss — scanning BTOC LPN0\n", boot_carve_block_param); } @@ -3829,10 +4423,10 @@ static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, l1 = use_le ? fmss_btoc_entry_le(f->last_page, 1) : fmss_btoc_entry_be(f->last_page, 1); /* - * LPN0 candidate: BTOC[0]==0 (even if [1] is junk — - * live ce1/cau1/blk63). Do not scan every random - * zero dword in non-ingestible pages (wedges NAND). - */ + * LPN0 candidate: BTOC[0]==0 (even if [1] is junk — + * live ce1/cau1/blk63). Avoid scan every random + * zero dword in non-ingestible pages (wedges NAND). + */ if (l0 == 0) { page_chunks = 16; if (fmss_boot_try_btoc_page(f, ce, cau, b, @@ -3866,9 +4460,9 @@ static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, } /* - * Aligned BPB: page0 of each block, then all pages of open SBs - * (page0 programmed && page127 not closed BTOC/BTE). Never mid-page OEM. - */ + * Aligned BPB: page0 of each block, then all pages of open SBs + * (page0 programmed && page127 not closed BTOC/BTE). Never mid-page OEM. + */ page_chunks = 16; { unsigned int boot_scan = nblocks ? nblocks : 256; @@ -3941,7 +4535,7 @@ static int fmss_boot_carve_discover(struct fmss_n31 *f, unsigned int start, fmss_lba_set(0, ce, cau, b, pg, sec, true, L2V_SRC_CARVE, 0); page_chunks = saved; - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "boot_sb open-SB sec=%u ce=%u cau=%u blk=%u pg=%u\n", sec, ce, cau, b, pg); return 0; @@ -3980,14 +4574,14 @@ static bool fmss_page_has_n31os_dirent(const u8 *page, unsigned int len) return false; } -static int fmss_root_dir_discover(struct fmss_n31 *f, unsigned int start, +static int fmss_root_dir_discover(struct nand_s5l8740 *f, unsigned int start, unsigned int nblocks) { unsigned int ce, cau, b, p, lpn; unsigned int saved, pages = 0; const unsigned int page_cap = 4096; - lpn = boot_data_start / FMSS_FTL_SECTORS_PER_LPN; + lpn = boot_data_start / NAND_FTL_SECTORS_PER_LPN; if (!fmss_l2v_lookup(lpn, &ce, &cau, &b, &p)) { root_dir_valid = true; root_dir_ce = ce; @@ -4029,7 +4623,7 @@ static int fmss_root_dir_discover(struct fmss_n31 *f, unsigned int start, fmss_l2v_set_ex(lpn, ce, cau, b, p, true, L2V_SRC_CARVE, 0); page_chunks = saved; - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "root_dir N31OS ce=%u cau=%u blk=%u pg=%u lpn=%u\n", ce, cau, b, p, lpn); return 0; @@ -4042,7 +4636,7 @@ static int fmss_root_dir_discover(struct fmss_n31 *f, unsigned int start, return -ENOENT; } -static void fmss_l2v_try_block_map_page(struct fmss_n31 *f, unsigned int ce, +static void fmss_l2v_try_block_map_page(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int max_lpn) { @@ -4098,7 +4692,7 @@ static void fmss_l2v_try_block_map_page(struct fmss_n31 *f, unsigned int ce, * Build dense L2V from BTOC page 127 (+ optional classic block-map pages). * Bounded: [start, start+nblocks) per CE/CAU. Also carve boot + root dir. */ -static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, +static int fmss_l2v_build(struct nand_s5l8740 *f, unsigned int max_lpn, unsigned int start, unsigned int nblocks) { unsigned int ce, cau, b, saved; @@ -4175,10 +4769,10 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, max_lpn); else if (fmss_page_looks_bte(f->last_page)) { /* - * Pass 2: BTE needs the full - * 16 KiB page; walk used 1-chunk - * probe — re-read full page. - */ + * Pass 2: BTE needs the full + * 16 KiB page; walk used 1-chunk + * probe — re-read full page. + */ unsigned int saved2 = page_chunks; page_chunks = 16; @@ -4194,9 +4788,9 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, } /* - * Classic block-map heuristic only when BTOC is - * blank — capped probes to avoid wedging. - */ + * Classic block-map heuristic only when BTOC is + * blank — capped probes to avoid wedging. + */ if (!btoc_ok && bmap_probes < 32) { page_chunks = 16; fmss_l2v_try_block_map_page(f, ce, cau, @@ -4216,7 +4810,7 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, fmss_legacy_meta_ingest = false; - fmss_dev_info(f->dev, + nand_dev_info(f->dev, "l2v_build max_lpn=%u range=%u+%u mapped=%u btoc=%u bmap=%u boot=%d root=%d ecc_soft=%u\n", max_lpn, start, nblocks, l2v_mapped, l2v_btoc_hits, l2v_bmap_hits, boot_carve_valid, root_dir_valid, @@ -4224,7 +4818,7 @@ static int fmss_l2v_build(struct fmss_n31 *f, unsigned int max_lpn, return 0; } -static int fmss_build_lpn_index(struct fmss_n31 *f, unsigned int max_lpn) +static int fmss_build_lpn_index(struct nand_s5l8740 *f, unsigned int max_lpn) { unsigned int nblocks = l2v_scan_blocks; @@ -4238,7 +4832,7 @@ static int fmss_build_lpn_index(struct fmss_n31 *f, unsigned int max_lpn) static ssize_t lpn_build_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int max_lpn = 32; int ret; @@ -4255,14 +4849,14 @@ static DEVICE_ATTR_WO(lpn_build); /* * Explicit dense L2V build (bounded). Usage: - * echo 1 > l2v_build - * echo "NBLOCKS" > l2v_build - * echo "START NBLOCKS [MAX_LPN]" > l2v_build + * echo 1 > l2v_build + * echo "NBLOCKS" > l2v_build + * echo "START NBLOCKS [MAX_LPN]" > l2v_build */ static ssize_t l2v_build_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int start = 0, nblocks = 0, max_lpn = 0; int nf, ret; @@ -4347,15 +4941,15 @@ static DEVICE_ATTR_RO(whimory_status); /* * Classic Whimory mount (bounded). Usage: - * echo 1 > whimory_mount - * echo "NBLOCKS" > whimory_mount - * echo "NBLOCKS MAX_LPN" > whimory_mount + * echo 1 > whimory_mount + * echo "NBLOCKS" > whimory_mount + * echo "NBLOCKS MAX_LPN" > whimory_mount */ static ssize_t whimory_mount_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int nblocks = 0, max_lpn = 0; int nf, ret; @@ -4390,14 +4984,14 @@ static DEVICE_ATTR_WO(whimory_mount); * Uses dense L2V / root-dir cache only — no on-demand BTOC scan (avoids wedging * the device on sparse unmapped reads). Returns -ENOENT if unmapped. */ -static int fmss_ftl_read_lpn_locked(struct fmss_n31 *f, unsigned int target_lpn, +static int nand_ftl_read_lpn_locked(struct nand_s5l8740 *f, unsigned int target_lpn, unsigned int sector, u8 *buf) { unsigned int ce, cau, block, page, pblock, off, saved; u32 addr, packed = L2V_PHYS; int ret; - if (sector > FMSS_FTL_SECTORS_PER_LPN - 1) + if (sector > NAND_FTL_SECTORS_PER_LPN - 1) return -EINVAL; if (root_dir_valid && target_lpn == root_dir_lpn) { @@ -4437,12 +5031,12 @@ static int fmss_ftl_read_lpn_locked(struct fmss_n31 *f, unsigned int target_lpn, /* * Read logical page N (BTOC LPN) and expose 4 KiB sector via sector_hex. - * Usage: echo "LPN [sector_in_page]" > lpn_read (sector 0..3, default 0) + * Usage: echo "LPN [sector_in_page]" > lpn_read (sector 0..3, default 0) */ static ssize_t lpn_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int target_lpn, sector = 0; unsigned int i, ce, cau, block, page, poff, saved; u8 *secbuf; @@ -4511,7 +5105,7 @@ static ssize_t lpn_read_store(struct device *dev, struct device_attribute *attr, ((i + 1) % 16) ? " " : "\n"); } - fmss_dev_info(dev, + nand_dev_info(dev, "lpn=%u sector=%u head=%02x%02x%02x%02x\n", target_lpn, sector, secbuf[0], secbuf[1], secbuf[2], secbuf[3]); kfree(secbuf); @@ -4532,7 +5126,7 @@ static ssize_t read_sector_dense_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int lba; u8 *secbuf; int ret; @@ -4544,7 +5138,7 @@ static ssize_t read_sector_dense_store(struct device *dev, secbuf = kmalloc(FMSS_SECTOR_LEN, GFP_KERNEL); if (!secbuf) return -ENOMEM; - ret = fmss_ftl_read_sector(lba, secbuf); + ret = nand_ftl_read_sector(lba, secbuf); dev_info(dev, "read_sector_dense LBA=%u ret=%d %s", lba, ret, resolve_log); kfree(secbuf); @@ -4556,7 +5150,7 @@ static ssize_t read_sector_slow_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int lba, lpn, sec, ce, cau, block, page, pblock, saved; u8 *secbuf; u32 addr, packed = L2V_PHYS; @@ -4571,15 +5165,15 @@ static ssize_t read_sector_slow_store(struct device *dev, return -ENOMEM; /* Try dense first. */ - ret = fmss_ftl_read_sector(lba, secbuf); + ret = nand_ftl_read_sector(lba, secbuf); if (!ret) { dev_info(dev, "read_sector_slow LBA=%u via dense OK\n", lba); kfree(secbuf); return count; } - lpn = lba / FMSS_FTL_SECTORS_PER_LPN; - sec = lba % FMSS_FTL_SECTORS_PER_LPN; + lpn = lba / NAND_FTL_SECTORS_PER_LPN; + sec = lba % NAND_FTL_SECTORS_PER_LPN; mutex_lock(&f->lock); ret = fmss_l2v_lookup_ex(lpn, &ce, &cau, &block, &page, &packed); if (ret) { @@ -4615,7 +5209,7 @@ static ssize_t read_sector_phys_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, block, page, sec = 0, saved; u8 *secbuf; u32 addr; @@ -4671,13 +5265,13 @@ static DEVICE_ATTR_RO(grep_log); /* * Walk FTL superblocks and search page data for a short ASCII needle. * Usage: echo "START N_BLOCKS NEEDLE" > ftl_grep - * echo "32 24 N31OS" > ftl_grep (defaults: start=32, n=24, N31OS) + * echo "32 24 N31OS" > ftl_grep (defaults: start=32, n=24, N31OS) * Caps N_BLOCKS at FMSS_GREP_MAX_BLOCKS per call to avoid RetailOS watchdog. */ static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, b, p, saved, start = 32, nblocks = 16; unsigned int pages_done = 0, hits = 0; char needle[48] = "N31OS"; @@ -4713,10 +5307,10 @@ static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, f->pages_since_reset = 0; } /* - * Do not require BTOC page 127 — Apple FAT clusters - * live in data pages even when BTOC looks blank. - * (Root cause: old code skipped whole superblocks.) - */ + * Avoid require BTOC page 127 — Apple FAT clusters + * live in data pages even when BTOC looks blank. + * (Root cause: old code skipped whole superblocks.) + */ for (p = 0; p < FMSS_BTOC_PAGE; p++) { unsigned int off = 0, show, flags; u32 lpn = ~0u; @@ -4733,7 +5327,7 @@ static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, if (!flags) continue; hits++; - fmss_dev_info(dev, + nand_dev_info(dev, "grep hit ce=%u cau=%u blk=%u pg=%u off=%u enc=%s\n", ce, cau, b, p, off, fmss_match_enc_name(flags)); @@ -4771,7 +5365,7 @@ static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, grep_log_len = scnprintf(grep_log, sizeof(grep_log), "NO HIT needle=%s pages=%u (tried ascii/utf16le/utf16be/bswap16)\n", needle, pages_done); - fmss_dev_info(dev, "ftl_grep start=%u n=%u needle=%s pages=%u hits=%u\n", + nand_dev_info(dev, "ftl_grep start=%u n=%u needle=%s pages=%u hits=%u\n", start, nblocks, needle, pages_done, hits); return count; } @@ -4779,12 +5373,12 @@ static DEVICE_ATTR_WO(ftl_grep); /* * Dump ASCII from a specific FTL page offset (after ftl_grep locates a file). - * Usage: echo "CE CAU BLK PG OFF LEN" > ftl_ascii (LEN default 512, max 2048) + * Usage: echo "CE CAU BLK PG OFF LEN" > ftl_ascii (LEN default 512, max 2048) */ static ssize_t readme_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, b, p, saved, start = 32, nblocks = 48; unsigned int pages_done = 0; const char *needle = "N31OS boot files on the RetailOS FAT volume"; @@ -4841,7 +5435,7 @@ static ssize_t readme_read_store(struct device *dev, struct device_attribute *at "OK ce=%u cau=%u blk=%u pg=%u off=%u len=%u pages=%u\n%.*s\n", ce, cau, b, p, off, dump, pages_done, dump, f->last_page + off); - fmss_dev_info(dev, "readme_read FOUND ce=%u cau=%u blk=%u pg=%u off=%u\n", + nand_dev_info(dev, "readme_read FOUND ce=%u cau=%u blk=%u pg=%u off=%u\n", ce, cau, b, p, off); } } @@ -4863,10 +5457,10 @@ static DEVICE_ATTR_WO(readme_read); /* * Locate Apple FAT32 boot sector by scanning FTL pages for EB3C90 *UOKJIHC. - * PIO only (fast). Usage: echo 1 > boot_read or echo "START N_BLOCKS" > boot_read + * PIO only (fast). Usage: echo 1 > boot_read or echo "START N_BLOCKS" > boot_read * Result in grep_log + sector_hex (512 B boot sector). */ -static int fmss_read_ftl_page_pio(struct fmss_n31 *f, unsigned int ce, +static int fmss_read_ftl_page_pio(struct nand_s5l8740 *f, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page) { @@ -4892,7 +5486,7 @@ static int fmss_read_ftl_page_pio(struct fmss_n31 *f, unsigned int ce, static ssize_t boot_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, b, saved, start = 32, nblocks = 32; unsigned int fat_off = 0, sector_off = 0; unsigned int pages_done = 0; @@ -4969,7 +5563,7 @@ static DEVICE_ATTR_WO(boot_read); static ssize_t ftl_ascii_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, block, page, off = 0, len = 512; int ret; @@ -5002,7 +5596,7 @@ static ssize_t ftl_ascii_store(struct device *dev, struct device_attribute *attr "%.*s\n", len, f->last_page + off); mutex_unlock(&f->lock); - fmss_dev_info(dev, "ftl_ascii ce=%u cau=%u blk=%u pg=%u off=%u len=%u\n", + nand_dev_info(dev, "ftl_ascii ce=%u cau=%u blk=%u pg=%u off=%u len=%u\n", ce, cau, block, page, off, len); return count; } @@ -5016,7 +5610,7 @@ static DEVICE_ATTR_WO(ftl_ascii); static ssize_t vfl_scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, i, saved_chunks; unsigned int start = FMSS_BLOCKS_PER_CAU - FMSS_VFL_TAIL; int hits = 0, xrmw = 0; @@ -5062,7 +5656,7 @@ static ssize_t vfl_scan_store(struct device *dev, struct device_attribute *attr, f->last_page[2] == 'm' && f->last_page[3] == 'x') xrmw++; fmss_vfl_ingest(f, cau, i, f->last_page); - fmss_dev_info(dev, + nand_dev_info(dev, "vfl ce=%u cau=%u blk=%u addr=0x%08x %02x %02x %02x %02x %02x %02x %02x %02x +256 %08x %08x %08x %08x\n", ce, cau, i, addr, f->last_page[0], f->last_page[1], @@ -5076,7 +5670,7 @@ static ssize_t vfl_scan_store(struct device *dev, struct device_attribute *attr, } page_chunks = saved_chunks; mutex_unlock(&f->lock); - fmss_dev_info(dev, "vfl_scan start_blk=%u hits=%d xrmw=%d\n", start, hits, xrmw); + nand_dev_info(dev, "vfl_scan start_blk=%u hits=%d xrmw=%d\n", start, hits, xrmw); return count; } static DEVICE_ATTR_WO(vfl_scan); @@ -5102,13 +5696,13 @@ static DEVICE_ATTR_RO(vfl_map); /* * Walk VFL tail (SLC page 0) on each CAU and ingest wrmx/xrmw remap tables. - * Usage: echo 1 > vfl_build (last vfl_build_blocks blocks, default 32) - * echo "START COUNT" > vfl_build (COUNT capped at FMSS_VFL_TAIL) + * Usage: echo 1 > vfl_build (last vfl_build_blocks blocks, default 32) + * echo "START COUNT" > vfl_build (COUNT capped at FMSS_VFL_TAIL) */ static ssize_t vfl_build_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, i, saved, start, nblocks, scanned = 0; int ingested = 0, ret; @@ -5165,7 +5759,7 @@ static ssize_t vfl_build_store(struct device *dev, struct device_attribute *attr } page_chunks = saved; mutex_unlock(&f->lock); - fmss_dev_info(dev, "vfl_build start=%u n=%u scanned=%u ingested=%d map=%u\n", + nand_dev_info(dev, "vfl_build start=%u n=%u scanned=%u ingested=%d map=%u\n", start, nblocks, scanned, ingested, vfl_map_count); return count; } @@ -5192,7 +5786,7 @@ static DEVICE_ATTR_RO(btoc_log); static ssize_t btoc_scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, i, saved, start = 0, n = FMSS_BLOCKS_PER_CAU; int hits = 0, lpn0 = 0; u32 addr, a, b; @@ -5234,7 +5828,7 @@ static ssize_t btoc_scan_store(struct device *dev, struct device_attribute *attr if (a == 0) lpn0++; if (a < 0x100000 && (b == a + 1 || a == 0)) { - fmss_dev_info(dev, + nand_dev_info(dev, "btoc ce=%u cau=%u blk=%u addr=0x%08x lpn0=%u lpn1=%u %02x %02x %02x %02x\n", ce, cau, i, addr, a, b, f->last_page[0], f->last_page[1], @@ -5251,7 +5845,7 @@ static ssize_t btoc_scan_store(struct device *dev, struct device_attribute *attr } page_chunks = saved; mutex_unlock(&f->lock); - fmss_dev_info(dev, "btoc_scan start=%u n=%u hits=%d lpn0=%d\n", start, n, hits, lpn0); + nand_dev_info(dev, "btoc_scan start=%u n=%u hits=%d lpn0=%d\n", start, n, hits, lpn0); return count; } static DEVICE_ATTR_WO(btoc_scan); @@ -5259,7 +5853,7 @@ static DEVICE_ATTR_WO(btoc_scan); static ssize_t fat_scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, i, saved, start = 0, n = FMSS_BLOCKS_PER_CAU; int hits = 0; u32 addr; @@ -5297,7 +5891,7 @@ static ssize_t fat_scan_store(struct device *dev, struct device_attribute *attr, (f->last_page_len >= 512 && fmss_apple_fat_boot(f->last_page))) { hits++; - fmss_dev_info(dev, + nand_dev_info(dev, "fat ce=%u cau=%u blk=%u addr=0x%08x %02x %02x %02x %02x %02x %02x %02x %02x\n", ce, cau, i, addr, f->last_page[0], f->last_page[1], @@ -5318,7 +5912,7 @@ static ssize_t fat_scan_store(struct device *dev, struct device_attribute *attr, } page_chunks = saved; mutex_unlock(&f->lock); - fmss_dev_info(dev, "fat_scan start=%u n=%u hits=%d\n", start, n, hits); + nand_dev_info(dev, "fat_scan start=%u n=%u hits=%d\n", start, n, hits); return count; } static DEVICE_ATTR_WO(fat_scan); @@ -5330,7 +5924,7 @@ static DEVICE_ATTR_WO(fat_scan); static ssize_t fat_boot_scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, cau, b, p, saved, start = 32, nblocks = 24; int hits = 0; @@ -5366,7 +5960,7 @@ static ssize_t fat_boot_scan_store(struct device *dev, struct device_attribute * "FAT32", 5)) continue; hits++; - fmss_dev_info(dev, + nand_dev_info(dev, "fat_boot ce=%u cau=%u blk=%u pg=%u off=%u oem=%.8s\n", ce, cau, b, p, off, s + 3); if (grep_log_len < sizeof(grep_log) - 96) @@ -5383,7 +5977,7 @@ static ssize_t fat_boot_scan_store(struct device *dev, struct device_attribute * } page_chunks = saved; mutex_unlock(&f->lock); - fmss_dev_info(dev, "fat_boot_scan start=%u n=%u hits=%d\n", start, nblocks, hits); + nand_dev_info(dev, "fat_boot_scan start=%u n=%u hits=%d\n", start, nblocks, hits); return count; } static DEVICE_ATTR_WO(fat_boot_scan); @@ -5391,7 +5985,7 @@ static DEVICE_ATTR_WO(fat_boot_scan); static ssize_t scan_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, start, n, i; u32 addr; int nonempty = 0; @@ -5410,14 +6004,14 @@ static ssize_t scan_store(struct device *dev, struct device_attribute *attr, } addr = (start + i) * 128u; if (fmss_page_read(f, ce, addr)) { - fmss_dev_info(dev, "scan ce=%u blk=%u FAIL\n", ce, start + i); + nand_dev_info(dev, "scan ce=%u blk=%u FAIL\n", ce, start + i); f->pages_since_reset++; continue; } f->pages_since_reset++; if (f->last_page[0] != 0xff && f->last_page[0] != 0x00) { nonempty++; - fmss_dev_info(dev, + nand_dev_info(dev, "scan ce=%u blk=%u p0 %02x %02x %02x %02x %02x %02x %02x %02x\n", ce, start + i, f->last_page[0], f->last_page[1], f->last_page[2], @@ -5426,7 +6020,7 @@ static ssize_t scan_store(struct device *dev, struct device_attribute *attr, } } mutex_unlock(&f->lock); - fmss_dev_info(dev, "scan ce=%u start=%u n=%u nonempty_head=%d\n", + nand_dev_info(dev, "scan ce=%u start=%u n=%u nonempty_head=%d\n", ce, start, n, nonempty); return count; } @@ -5435,7 +6029,7 @@ static DEVICE_ATTR_WO(scan); static ssize_t param_hex_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; int i, n = 0; if (!f) @@ -5451,7 +6045,7 @@ static DEVICE_ATTR_RO(param_hex); static ssize_t param_info_show(struct device *dev, struct device_attribute *attr, char *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; const u8 *p; if (!f) @@ -5476,7 +6070,7 @@ static DEVICE_ATTR_RO(param_info); static ssize_t param_read_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce = 0; int ret; @@ -5494,7 +6088,7 @@ static DEVICE_ATTR_WO(param_read); static ssize_t nand_reset_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; int ret; if (!f) @@ -5509,7 +6103,7 @@ static DEVICE_ATTR_WO(nand_reset); static ssize_t set_feature_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, feat, val; int ret; @@ -5527,7 +6121,7 @@ static DEVICE_ATTR_WO(set_feature); static ssize_t get_feature_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int ce, feat, len = 16; int nf, ret; @@ -5564,7 +6158,7 @@ static ssize_t page_data_read(struct file *filp, struct kobject *kobj, struct bin_attribute *attr, char *buf, loff_t off, size_t count) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; if (!f) return -ENODEV; @@ -5583,6 +6177,10 @@ static struct attribute *fmss_attrs[] = { &dev_attr_read_id.attr, &dev_attr_page_read.attr, &dev_attr_dma_read.attr, + &dev_attr_cs_canary_read.attr, + &dev_attr_cs_canary_matrix.attr, + &dev_attr_cs_phys_read.attr, + &dev_attr_cs_phys_last.attr, &dev_attr_lba_weave_scan.attr, &dev_attr_seq_kick.attr, &dev_attr_scan.attr, @@ -5630,15 +6228,15 @@ static struct bin_attribute *fmss_bin_attrs[] = { NULL, }; -static const struct attribute_group fmss_group = { +static const struct attribute_group nand_group = { .attrs = fmss_attrs, .bin_attrs = fmss_bin_attrs, }; -static const struct attribute_group *fmss_groups[] = { &fmss_group, NULL }; +static const struct attribute_group *nand_groups[] = { &nand_group, NULL }; static int fmss_probe(struct platform_device *pdev) { - struct fmss_n31 *f; + struct nand_s5l8740 *f; f = devm_kzalloc(&pdev->dev, sizeof(*f), GFP_KERNEL); if (!f) @@ -5653,7 +6251,7 @@ static int fmss_probe(struct platform_device *pdev) f->last_param_ce = -1; f->last_param_ret = -1; fmss_dma_setup(f, &pdev->dev); - fmss_dev = f; + nand_dev = f; platform_set_drvdata(pdev, f); dev_info(&pdev->dev, "FMSS peek FMCTRL0=0x%08x NANDSTAT=0x%08x quiet=%d (read-only until read_id/page_read)\n", @@ -5663,9 +6261,9 @@ static int fmss_probe(struct platform_device *pdev) static void fmss_remove(struct platform_device *pdev) { - struct fmss_n31 *f = platform_get_drvdata(pdev); + struct nand_s5l8740 *f = platform_get_drvdata(pdev); - fmss_dev = NULL; + nand_dev = NULL; fmss_l2v_free(); fmss_early_lba_free(); boot_carve_valid = false; @@ -5674,61 +6272,61 @@ static void fmss_remove(struct platform_device *pdev) fmss_dma_teardown(f); } -static struct platform_driver fmss_driver = { +static struct platform_driver nand_driver = { .probe = fmss_probe, .remove = fmss_remove, .driver = { - .name = "s5l8740-fmss", - .dev_groups = fmss_groups, + .name = "s5l8740-nand", + .dev_groups = nand_groups, }, }; -static struct platform_device *fmss_pdev; +static struct platform_device *nand_pdev; -static int __init fmss_init(void) +static int __init nand_s5l8740_init(void) { int ret; - ret = platform_driver_register(&fmss_driver); + ret = platform_driver_register(&nand_driver); if (ret) return ret; - fmss_pdev = platform_device_register_simple("s5l8740-fmss", -1, NULL, 0); - if (IS_ERR(fmss_pdev)) { - platform_driver_unregister(&fmss_driver); - return PTR_ERR(fmss_pdev); + nand_pdev = platform_device_register_simple("s5l8740-nand", -1, NULL, 0); + if (IS_ERR(nand_pdev)) { + platform_driver_unregister(&nand_driver); + return PTR_ERR(nand_pdev); } return 0; } -static void __exit fmss_exit(void) +static void __exit nand_s5l8740_exit(void) { - platform_device_unregister(fmss_pdev); - platform_driver_unregister(&fmss_driver); + platform_device_unregister(nand_pdev); + platform_driver_unregister(&nand_driver); } /* --- Exported read-only FTL sector API (ftl-s5l8740.ko) --- */ -bool fmss_ftl_present(void) +bool nand_ftl_present(void) { - return fmss_dev != NULL; + return nand_dev != NULL; } -EXPORT_SYMBOL_GPL(fmss_ftl_present); +EXPORT_SYMBOL_GPL(nand_ftl_present); -struct device *fmss_ftl_device(void) +struct device *nand_ftl_device(void) { - return fmss_dev ? fmss_dev->dev : NULL; + return nand_dev ? nand_dev->dev : NULL; } -EXPORT_SYMBOL_GPL(fmss_ftl_device); +EXPORT_SYMBOL_GPL(nand_ftl_device); -unsigned int fmss_ftl_lpn_count(void) +unsigned int nand_ftl_lpn_count(void) { return l2v_mapped ? l2v_mapped : lpn_index_count; } -EXPORT_SYMBOL_GPL(fmss_ftl_lpn_count); +EXPORT_SYMBOL_GPL(nand_ftl_lpn_count); -int fmss_ftl_build_map(unsigned int max_lpn) +int nand_ftl_build_map(unsigned int max_lpn) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int nblocks; int ret; @@ -5740,11 +6338,11 @@ int fmss_ftl_build_map(unsigned int max_lpn) mutex_unlock(&f->lock); return ret; } -EXPORT_SYMBOL_GPL(fmss_ftl_build_map); +EXPORT_SYMBOL_GPL(nand_ftl_build_map); -int fmss_ftl_read_sector(u64 logical_sector, void *buf) +int nand_ftl_read_sector(u64 logical_sector, void *buf) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int lpn, sec, ce, cau, block, page, pblock, off, saved; u32 addr, packed; int ret; @@ -5754,10 +6352,10 @@ int fmss_ftl_read_sector(u64 logical_sector, void *buf) return -ENODEV; /* - * Whimory registers the real LBA reader after FTL_Open. Call it - * without the FMSS mutex — the FIL page_read wrapper takes that lock. - */ - hook = READ_ONCE(fmss_ftl_read_hook); + * Whimory registers the real LBA reader after FTL_Open. Call it + * without the FMSS mutex — the FIL page_read wrapper takes that lock. + */ + hook = READ_ONCE(nand_ftl_read_hook); if (hook) return hook(logical_sector, buf); @@ -5828,10 +6426,10 @@ int fmss_ftl_read_sector(u64 logical_sector, void *buf) } } - lpn = (unsigned int)(logical_sector / FMSS_FTL_SECTORS_PER_LPN); - sec = (unsigned int)(logical_sector % FMSS_FTL_SECTORS_PER_LPN); + lpn = (unsigned int)(logical_sector / NAND_FTL_SECTORS_PER_LPN); + sec = (unsigned int)(logical_sector % NAND_FTL_SECTORS_PER_LPN); - ret = fmss_ftl_read_lpn_locked(f, lpn, sec, buf); + ret = nand_ftl_read_lpn_locked(f, lpn, sec, buf); if (!ret) resolve_log_len = scnprintf( resolve_log, sizeof(resolve_log), @@ -5847,17 +6445,30 @@ int fmss_ftl_read_sector(u64 logical_sector, void *buf) mutex_unlock(&f->lock); return ret; } -EXPORT_SYMBOL_GPL(fmss_ftl_read_sector); +EXPORT_SYMBOL_GPL(nand_ftl_read_sector); -int s5l8740_fmss_available(void) +int s5l8740_nand_available(void) { - return fmss_dev != NULL; + return nand_dev != NULL; } -EXPORT_SYMBOL_GPL(s5l8740_fmss_available); +EXPORT_SYMBOL_GPL(s5l8740_nand_available); -int s5l8740_fmss_hw_init(void) +int s5l8740_nand_meta_transport_ok(void) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; + + /* + * Disk registration still requires meta_dma_read=1. + * Early CS phys helpers use s5l8740_nand_cs_phys_read() instead — + * glass-proven span4/rec4112, but FTL must not auto-open yet. + */ + return f && f->dma_ok && meta_dma_read; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_meta_transport_ok); + +int s5l8740_nand_hw_init(void) +{ + struct nand_s5l8740 *f = nand_dev; int ret; if (!f) @@ -5869,11 +6480,11 @@ int s5l8740_fmss_hw_init(void) mutex_unlock(&f->lock); return ret; } -EXPORT_SYMBOL_GPL(s5l8740_fmss_hw_init); +EXPORT_SYMBOL_GPL(s5l8740_nand_hw_init); -int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g) +int s5l8740_nand_query_geometry(struct s5l8740_nand_geom *g) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; const u8 *p; u32 caus, cau_bits, blocks, block_bits, pages, pages_slc; u32 page_bits, page_size; @@ -5931,15 +6542,15 @@ int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g) mutex_unlock(&f->lock); /* - * FIL GetInfo (vtable +80, sub_12F83C): - * 101 — NAND present / signature +0x34 geometry (WhimoryBoot.c:169,260) - * 0 → "No NAND device found". Compared to sig[+0x34]. - * Value stored at format is blocks_per_cau (sub_12ED9C → 0x8D102CC - * is the first geometry word copied from the param page). - * 104 — BUF_Init data bytes (sub_D1960 first arg) = physical page size - * 105 — BUF_Init meta bytes (sub_D1960 second arg) = 16 (sub_12ED9C) - * 135 — stored at 0x8D0CE2C and unused after GetInfo - */ + * FIL GetInfo (vtable +80,: + * 101 — NAND present / signature +0x34 geometry (WhimoryBoot.c:169,260) + * 0 → "No NAND device found". Compared to sig[+0x34]. + * Value stored at format is blocks_per_cau → 0x8D102CC + * is the first geometry word copied from the param page). + * 104 — BUF_Init data bytes first arg) = physical page size + * 105 — BUF_Init meta bytes second arg) = 16 + * 135 — stored at 0x8D0CE2C and unused after GetInfo + */ g->dev_id = g->blocks_per_cau; g->geom_104 = g->page_size; g->geom_105 = 16; @@ -5948,13 +6559,13 @@ int s5l8740_fmss_query_geometry(struct s5l8740_fmss_geom *g) return -ENODEV; return 0; } -EXPORT_SYMBOL_GPL(s5l8740_fmss_query_geometry); +EXPORT_SYMBOL_GPL(s5l8740_nand_query_geometry); -u32 s5l8740_fmss_fil_get_info(u32 selector) +u32 s5l8740_nand_fil_get_info(u32 selector) { - struct s5l8740_fmss_geom g; + struct s5l8740_nand_geom g; - if (s5l8740_fmss_query_geometry(&g)) + if (s5l8740_nand_query_geometry(&g)) return 0; switch (selector) { case 101: @@ -5969,9 +6580,9 @@ u32 s5l8740_fmss_fil_get_info(u32 selector) return 0; } } -EXPORT_SYMBOL_GPL(s5l8740_fmss_fil_get_info); +EXPORT_SYMBOL_GPL(s5l8740_nand_fil_get_info); -static int fmss_dma_page_read_records(struct fmss_n31 *f, +static int fmss_dma_page_read_records(struct nand_s5l8740 *f, unsigned int ce, u32 addr, unsigned int slot, unsigned int span) @@ -5988,7 +6599,7 @@ static int fmss_dma_page_read_records(struct fmss_n31 *f, dma_slot = slot; dma_nsect = span; - dma_rec = FMSS_PPN_REC; /* 4096 data + 16 meta */ + dma_rec = FMSS_PPN_REC; /* 4096 data + 16 meta — glass ABI */ if (meta_dma_reset_before) fmss_nand_reset(f); @@ -6001,15 +6612,212 @@ static int fmss_dma_page_read_records(struct fmss_n31 *f, return ret; } -int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, +void s5l8740_nand_meta_decode(const u8 *m16, + struct s5l8740_meta_decoded *out) +{ + unsigned int i; + bool blank = true; + + if (!out) + return; + memset(out, 0, sizeof(*out)); + if (!m16) + return; + + out->type = m16[0]; + out->flags = m16[1]; + out->weave = (u64)m16[2] | ((u64)m16[3] << 8) | + ((u64)m16[4] << 16) | ((u64)m16[5] << 24) | + ((u64)m16[6] << 32) | ((u64)m16[7] << 40); + out->lba = (u32)m16[8] | ((u32)m16[9] << 8) | + ((u32)m16[10] << 16) | ((u32)m16[11] << 24); + + for (i = 0; i < N31_META_SLOT_SIZE; i++) { + if (m16[i] != 0x00 && m16[i] != 0xff) { + blank = false; + break; + } + } + out->blank = blank; + out->valid = !blank; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_meta_decode); + +void s5l8740_nand_meta_decode_legacy(const u8 *m16, + struct s5l8740_nand_slot_meta *out) +{ + struct s5l8740_meta_decoded d; + + if (!out) + return; + memset(out, 0, sizeof(*out)); + s5l8740_nand_meta_decode(m16, &d); + out->type = d.type; + out->flags = d.flags; + out->weave48 = d.weave; + out->lba = d.lba; + if (m16) + memcpy(out->aux, m16 + 12, 4); + out->data_like = n31_meta_is_data_record(&d); +} +EXPORT_SYMBOL_GPL(s5l8740_nand_meta_decode_legacy); + +int s5l8740_nand_meta_pick_lba(const struct s5l8740_cs_page *page, + u32 fmss_lba) +{ + int best = -ENOENT; + u64 best_weave = 0; + unsigned int s; + + if (!page) + return -EINVAL; + + for (s = 0; s < N31_DATA_SLOTS; s++) { + const struct s5l8740_meta_decoded *cur = &page->meta[s]; + + if (!n31_meta_is_data_record(cur) || cur->lba != fmss_lba) + continue; + if (best < 0 || n31_weave_newer(cur->weave, best_weave)) { + if (best >= 0 && page->meta[best].weave > cur->weave) + pr_info("s5l8740-nand: weave moved backward " + "fmss_lba=%u slot%u->%u " + "%012llx -> %012llx\n", + fmss_lba, best, s, + (unsigned long long)page->meta[best].weave, + (unsigned long long)cur->weave); + best = (int)s; + best_weave = cur->weave; + } else if (best >= 0 && cur->weave < best_weave) { + pr_info("s5l8740-nand: weave older skipped " + "fmss_lba=%u slot=%u weave=%012llx " + "best=%012llx\n", + fmss_lba, s, + (unsigned long long)cur->weave, + (unsigned long long)best_weave); + } + } + return best; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_meta_pick_lba); + +/* + * FTL map CS physical read: always slot0/span4/rec4112. + * Fills struct s5l8740_cs_page. No lba_map ingest. + */ +int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out) +{ + struct nand_s5l8740 *f = nand_dev; + u32 addr; + bool saved_armed; + int ret; + unsigned int s; + + if (!f || !out) + return -ENODEV; + if (!f->dma_ok) + return -ENODEV; + if (ce >= FMSS_NUM_CE || cau >= FMSS_NUM_CAU || + block >= FMSS_BLOCKS_PER_CAU || page > FMSS_BTOC_PAGE) + return -EINVAL; + if (dma_dry) + return -EAGAIN; + if (!dma_armed) + return -EPERM; + + memset(out, 0, sizeof(*out)); + addr = fmss_ppn_addr(cau, block, page, 0); + + mutex_lock(&f->lock); + saved_armed = dma_armed; + /* One-shot friendly: re-arm for this kick; disarm after if one_shot. */ + dma_armed = true; + dma_skip_ingest = true; + ret = fmss_dma_page_read_records(f, ce, addr, 0, 4); + dma_skip_ingest = false; + if (!dma_one_shot) + dma_armed = saved_armed; + f->pages_since_reset++; + + if (!ret) { + size_t dn = f->last_page_len; + size_t mn = f->last_spare_len; + + if (dn > FMSS_PAGE_LEN) + dn = FMSS_PAGE_LEN; + if (mn > S5L8740_NAND_META_SIZE) + mn = S5L8740_NAND_META_SIZE; + + for (s = 0; s < N31_DATA_SLOTS; s++) { + size_t doff = (size_t)s * N31_DATA_SLOT_SIZE; + size_t moff = (size_t)s * N31_META_SLOT_SIZE; + + memset(out->data[s], 0xff, N31_DATA_SLOT_SIZE); + if (dn > doff) { + size_t n = N31_DATA_SLOT_SIZE; + + if (dn - doff < n) + n = dn - doff; + memcpy(out->data[s], f->last_page + doff, n); + } + memset(out->meta_raw[s], 0xff, N31_META_SLOT_SIZE); + if (mn > moff) { + size_t n = N31_META_SLOT_SIZE; + + if (mn - moff < n) + n = mn - moff; + memcpy(out->meta_raw[s], f->last_spare + moff, n); + } + s5l8740_nand_meta_decode(out->meta_raw[s], &out->meta[s]); + } + } + mutex_unlock(&f->lock); + return ret; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_cs_phys_read); + +/* Batch CS sessions for map build: keep armed across many phys reads. */ +static struct { + bool saved; + bool dry; + bool armed; + bool one_shot; +} fmss_dma_batch; + +int s5l8740_nand_dma_session_begin(void) +{ + if (fmss_dma_batch.saved) + return -EBUSY; + fmss_dma_batch.dry = dma_dry; + fmss_dma_batch.armed = dma_armed; + fmss_dma_batch.one_shot = dma_one_shot; + fmss_dma_batch.saved = true; + dma_dry = false; + dma_one_shot = false; + dma_armed = true; + return 0; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_dma_session_begin); + +void s5l8740_nand_dma_session_end(void) +{ + if (!fmss_dma_batch.saved) + return; + dma_dry = fmss_dma_batch.dry; + dma_armed = fmss_dma_batch.armed; + dma_one_shot = fmss_dma_batch.one_shot; + fmss_dma_batch.saved = false; +} +EXPORT_SYMBOL_GPL(s5l8740_nand_dma_session_end); + +int s5l8740_nand_page_read(unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, unsigned int slc, unsigned int chunks, void *data, size_t data_len, void *meta, size_t meta_len) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; unsigned int saved; - unsigned int span; u32 addr; int ret; @@ -6021,16 +6829,6 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, if (!chunks || chunks > FMSS_MAX_CHUNKS) chunks = FMSS_MAX_CHUNKS; - /* - * PIO chunks are 1024-byte units. - * Metadata records are 4096-byte units. - */ - span = DIV_ROUND_UP(chunks, 4); - if (!span) - span = 1; - if (span > 4) - span = 4; - mutex_lock(&f->lock); if (reset_every && f->pages_since_reset >= reset_every) { @@ -6044,15 +6842,23 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, if (meta && meta_len) { /* - * Real metadata path. Do not fall back to PIO metadata, - * because fake metadata poisons FPart/VFL/FTL/L2V. - */ + * Real metadata: always full-page CS span4/rec4112, then + * software-slice. Never invent PIO spare as Whimory meta. + */ if (!meta_dma_read || !f->dma_ok) { ret = -EOPNOTSUPP; goto out_restore; } - - ret = fmss_dma_page_read_records(f, ce, addr, 0, span); + if (dma_dry) { + ret = -EAGAIN; + goto out_restore; + } + if (!dma_armed) { + ret = -EPERM; + goto out_restore; + } + dma_armed = true; + ret = fmss_dma_page_read_records(f, ce, addr, 0, 4); } else { ret = fmss_page_read(f, ce, addr); } @@ -6063,15 +6869,15 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, if (data && data_len) { size_t have = f->last_page_len; - if (have > span * FMSS_SECTOR_LEN) - have = span * FMSS_SECTOR_LEN; + if (have > FMSS_PAGE_LEN) + have = FMSS_PAGE_LEN; if (data_len > have) data_len = have; memcpy(data, f->last_page, data_len); } if (meta && meta_len) { - size_t have = span * 16; + size_t have = S5L8740_NAND_META_SIZE; memset(meta, 0xff, meta_len); if (have > f->last_spare_len) @@ -6087,11 +6893,11 @@ int s5l8740_fmss_page_read(unsigned int ce, unsigned int cau, mutex_unlock(&f->lock); return ret; } -EXPORT_SYMBOL_GPL(s5l8740_fmss_page_read); +EXPORT_SYMBOL_GPL(s5l8740_nand_page_read); -int s5l8740_fmss_nand_reset(void) +int s5l8740_nand_reset(void) { - struct fmss_n31 *f = fmss_dev; + struct nand_s5l8740 *f = nand_dev; int ret; if (!f) @@ -6101,16 +6907,16 @@ int s5l8740_fmss_nand_reset(void) mutex_unlock(&f->lock); return ret; } -EXPORT_SYMBOL_GPL(s5l8740_fmss_nand_reset); +EXPORT_SYMBOL_GPL(s5l8740_nand_reset); -void s5l8740_fmss_register_ftl_read(int (*fn)(u64 lba, void *buf)) +void s5l8740_nand_register_ftl_read(int (*fn)(u64 lba, void *buf)) { - WRITE_ONCE(fmss_ftl_read_hook, fn); + WRITE_ONCE(nand_ftl_read_hook, fn); } -EXPORT_SYMBOL_GPL(s5l8740_fmss_register_ftl_read); +EXPORT_SYMBOL_GPL(s5l8740_nand_register_ftl_read); -module_init(fmss_init); -module_exit(fmss_exit); +module_init(nand_s5l8740_init); +module_exit(nand_s5l8740_exit); MODULE_LICENSE("GPL"); -MODULE_DESCRIPTION("S5L8740 FMSS NAND controller (Whimory FIL, read-only)"); +MODULE_DESCRIPTION("S5L8740 NAND/FMSS controller (Whimory FIL, read-only)"); MODULE_AUTHOR("n31"); diff --git a/drivers/misc/nand-s5l8740.h b/drivers/misc/nand-s5l8740.h new file mode 100755 index 00000000000000..f1f00c5115d4a9 --- /dev/null +++ b/drivers/misc/nand-s5l8740.h @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * S5L8740 NAND controller (FMSS/FIL) — raw PPN page I/O for the Whimory / CS-map stack. + * + * Terminology (FTL map): + * fmss_lba — logical LBA from 16-byte CS metadata + * disk_lba — exported FAT volume LBA (Linux/VFAT) + * fat_base_lba — fmss_lba of the FAT32 BPB (disk_lba 0) + * physical record — ce/cau/block/page/slot/weave/type/lba + * CS page — 4×4096 data + 4×16 meta (rec=4112) + * + * disk_lba 0 → fat_base_lba (proven candidate 49279). + * target_fmss_lba = fat_base_lba + disk_lba + * + * nand-s5l8740.ko owns the controller. ftl-s5l8740.ko owns map / block. + */ +#ifndef NAND_S5L8740_H +#define NAND_S5L8740_H + +#include +#include + +#define NAND_FTL_SECTOR_SIZE 4096U +#define NAND_FTL_SECTORS_PER_LPN 4U +#define NAND_FTL_DEFAULT_CAPACITY 3856968U + +#define S5L8740_NAND_MAX_CE 2U +#define S5L8740_NAND_MAX_CAU 2U +#define S5L8740_NAND_PAGE_SIZE 16384U +#define S5L8740_NAND_META_SIZE 64U /* 4 × 16-byte SFTL slots */ +#define S5L8740_NAND_SLOTS_PER_PAGE 4U +#define S5L8740_NAND_SLOT_DATA 4096U +#define S5L8740_NAND_SLOT_META 16U +#define S5L8740_NAND_REC_BYTES 4112U /* 4096 data + 16 meta */ + +/* FTL map names (aliases of the above). */ +#define N31_DATA_SLOTS S5L8740_NAND_SLOTS_PER_PAGE +#define N31_DATA_SLOT_SIZE S5L8740_NAND_SLOT_DATA +#define N31_META_SLOT_SIZE S5L8740_NAND_SLOT_META +#define N31_CS_REC_SIZE S5L8740_NAND_REC_BYTES + +/* PPN DATA spare (CS META stream): glass-proven with rec=4112. */ +#define S5L8740_NAND_META_TYPE_DATA 0x01u +#define S5L8740_NAND_META_TYPE_DATA2 0x02u + +struct s5l8740_nand_geom { + u32 num_ce; + u32 num_cau; + u32 blocks_per_cau; + u32 pages_per_block; + u32 pages_per_block_slc; + u32 page_size; + u32 vfl_tail; + u32 page_bits; + u32 block_bits; + u32 cau_bits; + u32 caus_per_channel; + u32 dev_id; /* FIL selector 101 analogue */ + u32 geom_104; /* FIL selector 104 analogue */ + u32 geom_105; /* FIL selector 105 analogue */ + u32 geom_135; /* FIL selector 135 analogue */ + bool from_param_page; +}; + +/* Decoded 16-byte CS metadata slot (fmss_lba lives in.lba). */ +struct s5l8740_meta_decoded { + u8 type; + u8 flags; + u64 weave; /* weaveSeq from meta bytes; width-limited */ + u32 lba; /* fmss_lba — NOT disk_lba */ + bool valid; + bool blank; +}; + +/* One CS physical page: four data + four meta records (rec=4112). */ +struct s5l8740_cs_page { + u8 data[N31_DATA_SLOTS][N31_DATA_SLOT_SIZE]; + u8 meta_raw[N31_DATA_SLOTS][N31_META_SLOT_SIZE]; + struct s5l8740_meta_decoded meta[N31_DATA_SLOTS]; +}; + +/* Legacy alias used by older call sites / Whimory. */ +struct s5l8740_nand_slot_meta { + u8 type; + u8 flags; + u64 weave48; + u32 lba; + u8 aux[4]; + bool data_like; +}; + +bool nand_ftl_present(void); +struct device *nand_ftl_device(void); +unsigned int nand_ftl_lpn_count(void); +int nand_ftl_build_map(unsigned int max_lpn); +int nand_ftl_read_sector(u64 logical_sector, void *buf); + +u32 s5l8740_nand_fil_get_info(u32 selector); +int s5l8740_nand_available(void); +int s5l8740_nand_meta_transport_ok(void); +int s5l8740_nand_hw_init(void); +int s5l8740_nand_query_geometry(struct s5l8740_nand_geom *g); + +/* + * CS physical read (FTL map ABI): + * always slot=0, span=4, rec=4112, command-list CS DMA + * fills 4 data + 4 meta slots; no lba_map ingest + * Requires dma_dry=0 and dma_armed=1 (one-shot friendly). + */ +int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out); + +/* Hold dma_armed across a multi-page CS scan; restores prior dry/one_shot. */ +int s5l8740_nand_dma_session_begin(void); +void s5l8740_nand_dma_session_end(void); + +void s5l8740_nand_meta_decode(const u8 *m16, + struct s5l8740_meta_decoded *out); + +/* Legacy decode into slot_meta (aux + data_like). */ +void s5l8740_nand_meta_decode_legacy(const u8 *m16, + struct s5l8740_nand_slot_meta *out); + +static inline bool n31_meta_is_data_record(const struct s5l8740_meta_decoded *m) +{ + if (!m || !m->valid || m->blank) + return false; + return m->type == S5L8740_NAND_META_TYPE_DATA || + m->type == S5L8740_NAND_META_TYPE_DATA2; +} + +static inline bool n31_weave_newer(u64 a, u64 b) +{ + return a > b; +} + +/* + * Among four decoded meta records, pick newest weave claiming @fmss_lba + * with type in {0x01,0x02}. Returns slot 0..3 or -ENOENT. + */ +int s5l8740_nand_meta_pick_lba(const struct s5l8740_cs_page *page, + u32 fmss_lba); + +int s5l8740_nand_page_read(unsigned int ce, unsigned int cau, + unsigned int block, unsigned int page, + unsigned int slc, unsigned int chunks, + void *data, size_t data_len, + void *meta, size_t meta_len); +int s5l8740_nand_reset(void); +void s5l8740_nand_register_ftl_read(int (*fn)(u64 lba, void *buf)); + +#endif /* NAND_S5L8740_H */ diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h index 4faeaacff0ab44..adf2e2e482b31f 100755 --- a/drivers/misc/whimory-s5l8740.h +++ b/drivers/misc/whimory-s5l8740.h @@ -14,7 +14,7 @@ #include #include -#include "fmss-s5l8740-api.h" +#include "nand-s5l8740.h" #define WHIMORY_SIG_SIZE 0x600 #define WHIMORY_SIG_MAGIC 0x776d7278u /* "xrmw" LE — payload, not raw page+0 */ @@ -188,10 +188,10 @@ struct whimory_range { }; struct whimory_vfl { - u32 *remap[S5L8740_FMSS_MAX_CAU]; - u16 *cxt_u16[S5L8740_FMSS_MAX_CAU]; - u32 ctx_ce[S5L8740_FMSS_MAX_CAU]; - u32 ctx_block[S5L8740_FMSS_MAX_CAU]; + u32 *remap[S5L8740_NAND_MAX_CAU]; + u16 *cxt_u16[S5L8740_NAND_MAX_CAU]; + u32 ctx_ce[S5L8740_NAND_MAX_CAU]; + u32 ctx_block[S5L8740_NAND_MAX_CAU]; u32 remap_count; u32 cxt_u16_len; u32 cxt_loc_count; @@ -202,7 +202,7 @@ struct whimory_vfl { u8 *bank_mask; /* [blocks_per_cau] bank bitmask; sub_3D1438 */ u16 cached_vbn; u8 cached_n; - u8 cached_banks[S5L8740_FMSS_MAX_CAU]; + u8 cached_banks[S5L8740_NAND_MAX_CAU]; }; struct whimory_sb { @@ -225,6 +225,7 @@ struct whimory_sftl { u8 *btoc_page; u8 *data_page; u8 *meta_page; + struct s5l8740_cs_page *cs_page; /* CS span4 scratch for recover/read */ struct whimory_sb *sbs; u32 mapped_roots; u32 mapped_lbas; From 00d2c4287387d69b0573b26152f1cbc250013364 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Thu, 27 Aug 2026 20:15:31 -0230 Subject: [PATCH 19/31] N31: CXT fast-path FTL recovery, PMIC button debounce, style cleanup Storage bring-up milestone for the iPod nano 7 (S5L8740), plus the code cleanup that should have accompanied the earlier syncs. FTL / NAND ---------- Use the SFTL context block as the primary map source instead of replaying every open superblock. On the glass this takes a cold recover from 498 s to 69 s while mapping more of the volume (642652 -> 811554 LBAs), and only 8 of 1803 superblocks then need replaying. The key finding is that CXT VBAs are not in the address space whimory_pack_vba() builds. The FTL counts one superblock as the same virtual block across every (ce, cau) plane, so a superblock holds 2048 VBAs and the plane index sits between the page and the slot: vba = vblock * (pages_per_sb * planes * vbas_per_page) + page * (planes * vbas_per_page) + plane * vbas_per_page + slot Untranslated, those VBAs land on unrelated and often erased pages, which is why the context looked unusable. A run of consecutive CXT VBAs is only contiguous here within one 4-slot group, so extents are split at plane boundaries on import. The TREE is also partitioned by logical range across the context superblocks, and only the oldest carries the BASE marker, so all of them are merged rather than taking the first that parses. Two quadratic paths are gone: whimory_range_update() walked the interval map from rb_first() on every L2V update, and the packed L2V repacked an entire root per update while collecting a root walked the whole map. A binary-search lower bound plus deferring the pack until replay finishes took a full brute-force recover from 1005 s to 504 s with byte-identical output. Ranges now coalesce across weaves when they are contiguous in both LBA and VBA, keeping the older weave so a later claim is never wrongly rejected as stale. That cut 208134 interval nodes to 52352 and is what made a full replay fit in 55 MiB of RAM. Recovery is now a small state machine: a rebuild cannot tear down a live map, and re-binding an already-registered disk no longer leaves the gendisk at capacity 0 and fails every read. Diagnostics are quiet by default; diag=1 restores the bring-up dumps. PMIC ---- Reports that looked like kernel crashes were the d1830 100 ms I2C button sweep misreading r7 during NAND activity, emitting KEY_POWER from a single sample, and userspace powering the device off. Buttons are interrupt-driven by default now (btn_poll_ms=0) with a confirming re-read (btn_confirm_ms). DMA --- s5l_pl080_desc_free() ran dma_free_coherent() after an in_atomic() test. That test cannot see spinlock context on a non-preempt build, and the driver openly leaked when it guessed wrong. Descriptors holding a coherent LLI block are now queued to a workqueue; pool-backed ones are still released inline. Compile-tested only: audio on this board is broken for unrelated reasons and is the next work item. Cleanup ------- Cross-driver declarations were repeated as bare externs in six files and had begun to disagree. They now live in include/linux/apple-n31.h. A previous automated pass had mangled comments across these drivers, leaving fragments such as a comment opening with a bare colon, and continuation lines unindented at column 0. Twelve were rewritten as prose and roughly a thousand continuation lines re-aligned; the resulting .ko files are byte-identical, so that part is comment-only. checkpatch on the touched files goes from 2 errors and 172 warnings to 0 errors and 142 warnings. What remains is deliberate: split format strings and deep nesting in the NAND sequencer, both of which need real refactoring; msleep values that come from hardware timing; and sysfs_emit false positives where the trailing newline is inside a %s argument. Verified on hardware: read-only FAT mount with 50 Fxx directories, 496 music files, zero read misses and zero VFAT bread failures. An fsck of the volume reports 702 files, 91 directories, 2.9 GB, clean. Co-Authored-By: Claude Opus 5 --- .../boot/dts/samsung/s5l8740-n31-nodrm.dts | 5 + drivers/dma/dma-s5l8740-pl080.c | 117 +- drivers/gpio/gpio-d1830.c | 175 +- drivers/gpio/gpio-s5l8740.c | 5 +- drivers/input/touchscreen/apple-nimbus.c | 185 +- drivers/misc/apple-mikeybus.c | 1450 +++++++---- drivers/misc/ftl-s5l8740-core.c | 2312 +++++++++++++++-- drivers/misc/ftl-s5l8740-csmap.c | 448 +++- drivers/misc/ftl-s5l8740-csmap.h | 6 + drivers/misc/ftl-s5l8740-vecmap.c | 5 +- drivers/misc/nand-s5l8740.c | 299 ++- drivers/misc/whimory-s5l8740.h | 82 +- include/linux/apple-n31.h | 62 + sound/soc/apple/s5l8740-i2s.c | 8 +- 14 files changed, 4072 insertions(+), 1087 deletions(-) create mode 100755 include/linux/apple-n31.h diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts index cffb246c368bcc..7e3cb6a2efd745 100755 --- a/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts @@ -179,6 +179,11 @@ interrupt-parent = <&vic0>; interrupts = <26>; status = "okay"; + + mikeybus { + compatible = "apple,mikeybus", "apple,n31-mikeybus"; + current-speed = <115200>; + }; }; usbphy: usbphy@3c400000 { diff --git a/drivers/dma/dma-s5l8740-pl080.c b/drivers/dma/dma-s5l8740-pl080.c index 893ea74730319e..47e4985246919f 100755 --- a/drivers/dma/dma-s5l8740-pl080.c +++ b/drivers/dma/dma-s5l8740-pl080.c @@ -21,7 +21,6 @@ * Not the mainline Samsung map (CONTROL2@+0x10 / CFG@+0x14). * Cache: ARM1176 32-byte lines — buffer 32-byte aligned; sync in start(). */ -#include #include #include #include @@ -30,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +38,9 @@ #include #include #include +#include + +#include #include "virt-dma.h" #define PL080_INT_STATUS 0x00 @@ -127,7 +130,7 @@ MODULE_PARM_DESC(m2p_dst_burst, "M2P DBSIZE enc (default 1 = RetailOS music)"); static int retail_prot = 1; module_param(retail_prot, int, 0644); MODULE_PARM_DESC(retail_prot, "1=Prot=0 on M2P/P2M (RetailOS music CTL)"); -static int force_eng = 0; +static int force_eng; module_param(force_eng, int, 0644); MODULE_PARM_DESC(force_eng, "PL080 engine 0/1 for xlate (-1 = either; default 0 = PL080_0)"); /* Prefer physical channel (RetailOS music uses ch2). -1 = first free. */ @@ -135,6 +138,19 @@ static int force_ch = 2; module_param(force_ch, int, 0644); MODULE_PARM_DESC(force_ch, "prefer PL080 channel id 0..7 (-1=any; default 2=RetailOS)"); +/* Verbose transfer/xlate spam off by default; use verbose=1 or dyndbg. */ +static bool verbose; +module_param(verbose, bool, 0644); +MODULE_PARM_DESC(verbose, "Verbose PL080 DMA bring-up logs (default N)"); + +#define pl080_vinfo(dev, fmt, ...) \ + do { \ + if (verbose) \ + dev_info((dev), fmt, ##__VA_ARGS__); \ + else \ + dev_dbg((dev), fmt, ##__VA_ARGS__); \ + } while (0) + /* OSOS B424C descriptor stride is 20 bytes; keep pool 32-byte aligned. */ #define PL080_LLI_ALIGN 32 #define PL080_TERM_POLL_US 10 @@ -188,15 +204,9 @@ struct s5l_pl080_desc { size_t period_len; unsigned int periods; unsigned int periods_done; + struct llist_node free_node; }; -struct s5l_pl080; - -struct dma_chan *s5l_pl080_request_slave(struct device *consumer, - unsigned int idx); -struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri); -int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en); - struct s5l_pl080 { struct device *dev; void __iomem *base[2]; @@ -210,6 +220,11 @@ struct s5l_pl080 { dma_addr_t lli_pool_phys; DECLARE_BITMAP(lli_busy, PL080_LLI_POOL_NODES); struct task_struct *pump; + /* Descriptors whose coherent LLI block must be freed outside + * atomic context; see s5l_pl080_desc_free(). + */ + struct llist_head free_list; + struct work_struct free_work; }; static struct pl080_lli *s5l_pl080_lli_alloc(struct s5l_pl080 *pl, @@ -258,28 +273,34 @@ static struct pl080_lli *s5l_pl080_lli_alloc(struct s5l_pl080 *pl, return lli; } -static void s5l_pl080_lli_release(struct s5l_pl080 *pl, struct s5l_pl080_desc *d) +/* Returning pool slots is just clearing bits, so any context will do. */ +static void s5l_pl080_lli_pool_release(struct s5l_pl080 *pl, + struct s5l_pl080_desc *d) { unsigned long flags; unsigned int j; - if (!pl || !d || !d->lli) - return; - if (d->lli_from_pool) { - spin_lock_irqsave(&pl->lock, flags); - for (j = 0; j < d->nlli && d->lli_off + j < PL080_LLI_POOL_NODES; - j++) - clear_bit(d->lli_off + j, pl->lli_busy); - spin_unlock_irqrestore(&pl->lock, flags); - } else if (!irqs_disabled() && !in_atomic()) { + spin_lock_irqsave(&pl->lock, flags); + for (j = 0; j < d->nlli && d->lli_off + j < PL080_LLI_POOL_NODES; j++) + clear_bit(d->lli_off + j, pl->lli_busy); + spin_unlock_irqrestore(&pl->lock, flags); + d->lli = NULL; +} + +/* Drains descriptors parked by s5l_pl080_desc_free(). */ +static void s5l_pl080_free_work(struct work_struct *work) +{ + struct s5l_pl080 *pl = container_of(work, struct s5l_pl080, + free_work); + struct s5l_pl080_desc *d, *tmp; + struct llist_node *pending; + + pending = llist_del_all(&pl->free_list); + llist_for_each_entry_safe(d, tmp, pending, free_node) { dma_free_coherent(pl->dev, s5l_pl080_lli_size(d->nlli), d->lli, d->lli_phys); - } else { - dev_warn_ratelimited(pl->dev, - "LLI leak nlli=%u (atomic free)\n", - d->nlli); + kfree(d); } - d->lli = NULL; } static int s5l_pl080_need_soft(void) @@ -531,13 +552,28 @@ static void s5l_pl080_free(struct dma_chan *c) vchan_free_chan_resources(&to_s5l_chan(c)->vc); } +/* + * virt-dma calls this from its tasklet, and callers may hold the channel + * lock, so it must not sleep. dma_free_coherent() can, so descriptors + * holding a coherent LLI block are queued for s5l_pl080_free_work() + * instead. Pool-backed descriptors are released inline. + */ static void s5l_pl080_desc_free(struct virt_dma_desc *vd) { struct s5l_pl080_desc *d = to_s5l_desc(vd); - struct s5l_pl080_chan *ch = to_s5l_chan(vd->tx.chan); + struct s5l_pl080 *pl = to_s5l_chan(vd->tx.chan)->host; - s5l_pl080_lli_release(ch->host, d); - kfree(d); + if (!pl || !d->lli) { + kfree(d); + return; + } + if (d->lli_from_pool) { + s5l_pl080_lli_pool_release(pl, d); + kfree(d); + return; + } + llist_add(&d->free_node, &pl->free_list); + schedule_work(&pl->free_work); } static struct dma_async_tx_descriptor * @@ -559,7 +595,7 @@ s5l_pl080_prep_slave_sg(struct dma_chan *c, struct scatterlist *sgl, if (!total) return NULL; if (sg_len > 1) - dev_info(ch->host->dev, "prep_slave_sg sg_len=%u (LLI chain)\n", + pl080_vinfo(ch->host->dev, "prep_slave_sg sg_len=%u (LLI chain)\n", sg_len); /* One LLI node per <= PL080_MAX_XFER_WORDS transfer units */ @@ -679,7 +715,7 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, u32 cfg; if (!buf_len || !period_len || buf_len % period_len) { - dev_info(ch->host->dev, + pl080_vinfo(ch->host->dev, "cyclic reject len=%zu period=%zu\n", buf_len, period_len); return NULL; } @@ -788,7 +824,7 @@ s5l_pl080_prep_dma_cyclic(struct dma_chan *c, dma_addr_t buf_addr, d->period_len = period_len; d->periods = periods; d->periods_done = 0; - dev_info(ch->host->dev, + pl080_vinfo(ch->host->dev, "cyclic ok peri=%u nlli=%u periods=%u period=%zu fifo=0x%x\n", ch->peri, nlli, periods, period_len, (u32)lower_32_bits(dev_addr)); @@ -862,7 +898,7 @@ static int s5l_pl080_terminate(struct dma_chan *c) static irqreturn_t s5l_pl080_irq(int irq, void *data) { struct s5l_pl080 *pl = data; - unsigned eng, i; + unsigned int eng, i; u32 tc, err; for (eng = 0; eng < 2; eng++) { @@ -922,7 +958,7 @@ static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, struct of_phandle_args *spec) { struct s5l_pl080_chan *ch; - unsigned i, peri, eng_lo, eng_hi; + unsigned int i, peri, eng_lo, eng_hi; if (!pl || !spec || spec->args_count < 1) return NULL; @@ -950,7 +986,7 @@ static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, ch->peri = peri; ch->src_burst = clamp(m2p_src_burst, 0, 7); ch->dst_burst = clamp(m2p_dst_burst, 0, 7); - dev_info(pl->dev, + pl080_vinfo(pl->dev, "xlate DT peri=%u -> ch%u (forced) peri=%u\n", spec->args[0] & 0x1f, prefer, ch->peri); return dma_get_slave_channel(&ch->vc.chan); @@ -964,7 +1000,7 @@ static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, ch->peri = peri; ch->src_burst = clamp(m2p_src_burst, 0, 7); ch->dst_burst = clamp(m2p_dst_burst, 0, 7); - dev_info(pl->dev, "xlate DT peri=%u -> ch%u (eng%u) peri=%u\n", + pl080_vinfo(pl->dev, "xlate DT peri=%u -> ch%u (eng%u) peri=%u\n", spec->args[0] & 0x1f, i, i / PL080_CH_COUNT, ch->peri); return dma_get_slave_channel(&ch->vc.chan); } @@ -1121,6 +1157,8 @@ static int s5l_pl080_probe(struct platform_device *pdev) return -ENOMEM; pl->dev = dev; spin_lock_init(&pl->lock); + init_llist_head(&pl->free_list); + INIT_WORK(&pl->free_work, s5l_pl080_free_work); for (i = 0; i < 2; i++) { res = platform_get_resource(pdev, IORESOURCE_MEM, i); @@ -1164,7 +1202,7 @@ static int s5l_pl080_probe(struct platform_device *pdev) if (!b) continue; - dev_info(dev, "selftest eng%u id=%02x\n", + pl080_vinfo(dev, "selftest eng%u id=%02x\n", eng, readl(b + 0xfe0) & 0xff); for (wid = 1; wid <= 2; wid++) { for (as = 0; as <= 1; as++) { @@ -1199,7 +1237,7 @@ static int s5l_pl080_probe(struct platform_device *pdev) writel(BIT(0), b + PL080_SOFT_BREQ); writel(BIT(0), b + PL080_SOFT_SREQ); udelay(50); - dev_info(dev, + pl080_vinfo(dev, "selftest e%u w%u s%d d%d tc=%x err=%x dst=%08x\n", eng, wid, as, ad, readl(b + PL080_RAW_TC), @@ -1277,7 +1315,7 @@ static int s5l_pl080_probe(struct platform_device *pdev) if (!pl->lli_pool) dev_warn(dev, "LLI pool alloc failed — GFP_NOWAIT fallback only\n"); else - dev_info(dev, "LLI pool %u nodes pa=%pad\n", + pl080_vinfo(dev, "LLI pool %u nodes pa=%pad\n", PL080_LLI_POOL_NODES, &pl->lli_pool_phys); pl->pump = kthread_run(s5l_pl080_pump, pl, "n31-pl080-pump"); @@ -1290,7 +1328,7 @@ static int s5l_pl080_probe(struct platform_device *pdev) ret = device_create_file(dev, &dev_attr_chregs); if (ret) dev_warn(dev, "chregs sysfs: %d\n", ret); - dev_info(dev, + pl080_vinfo(dev, "PL080 dmaengine @%pR id=%02x peri IIS0=10/11 IIS2 RX=13 (RetailOS)\n", platform_get_resource(pdev, IORESOURCE_MEM, 0), id0); return 0; @@ -1305,6 +1343,9 @@ static void s5l_pl080_remove(struct platform_device *pdev) device_remove_file(&pdev->dev, &dev_attr_chregs); of_dma_controller_free(pdev->dev.of_node); dma_async_device_unregister(&pl->ddev); + /* Descriptors parked by s5l_pl080_desc_free() must not outlive us. */ + cancel_work_sync(&pl->free_work); + s5l_pl080_free_work(&pl->free_work); } static const struct of_device_id s5l_pl080_of_match[] = { diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index a8b9206c0ec41c..ceb6d9f0251fc9 100755 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -37,6 +37,8 @@ #include #include +#include + #define D1830_REG_POWEROFF 13 #define D1830_POWEROFF_BIT BIT(0) #define D1830_REG_ADC_CFG 48 @@ -50,11 +52,6 @@ #define D1830_DESIGN_MIN_UV 3300000 #define D1830_DESIGN_MAX_UV 4200000 -int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); -void s5l8740_n31_report_key(unsigned int code, int pressed); -int s5l8740_n31_din86(void); -extern void (*d1830_n31_din_nirq_hook)(void); - /* Provisional Li-ion empty/full for capacity % (OPEN scale) */ #define D1830_MV_EMPTY 3300 #define D1830_MV_FULL 4200 @@ -64,12 +61,54 @@ module_param(dump_only, bool, 0644); MODULE_PARM_DESC(dump_only, "Log PMIC rail ops, do not write (docs-internal n31-pmic dummies)"); +/* + * Chatty WR/RMW/reg dumps are off by default. Enable with verbose=1, or + * via dynamic debug (dev_dbg) if the kernel was built with DYNAMIC_DEBUG. + */ +static bool verbose; +module_param(verbose, bool, 0644); +MODULE_PARM_DESC(verbose, "Verbose n31-pmic I2C/reg logging (default N; also gpio_d1830.verbose=1 on cmdline)"); + +#define d1830_vinfo(dev, fmt, ...) \ + do { \ + if (verbose) \ + dev_info((dev), fmt, ##__VA_ARGS__); \ + else \ + dev_dbg((dev), fmt, ##__VA_ARGS__); \ + } while (0) + static bool allow_audio_rails = true; module_param(allow_audio_rails, bool, 0644); MODULE_PARM_DESC(allow_audio_rails, "Apply sub_23EC LDO trim from d1830_audio_rails() (default on)"); -int d1830_audio_rails(void); +/* Off by default: false Sleep during NAND CS storms was cutting power. */ +static bool sleep_poweroff; +module_param(sleep_poweroff, bool, 0644); +MODULE_PARM_DESC(sleep_poweroff, + "Hold Sleep ~500ms → pm_power_off (default N)"); + +/* + * The 100 ms r5-r8 sweep was a bring-up aid, not a product poll: Home / + * Sleep / Play arrive on the PMIC nIRQ (GPIO 86 DIN) via + * d1830_n31_din_nirq(). Polling it during a NAND CS storm is what + * produced the phantom "SLEEP PRESS r7=0x0e" that power-watch turned + * into a poweroff mid-recover. Interrupt-driven by default. + */ +static unsigned int btn_poll_ms; +module_param(btn_poll_ms, uint, 0644); +MODULE_PARM_DESC(btn_poll_ms, + "Fallback button poll period in ms (0=off, interrupt-only)"); + +/* + * A single glitched I2C byte must not reach userspace either: power-watch + * turns one KEY_POWER press into reboot(RB_POWER_OFF). Re-read after + * btn_confirm_ms and only report the press if Sleep is still low. + */ +static unsigned int btn_confirm_ms = 60; +module_param(btn_confirm_ms, uint, 0644); +MODULE_PARM_DESC(btn_confirm_ms, + "Re-read delay confirming a Sleep press before KEY_POWER (0=off)"); struct d1830_gpio_map { u8 reg; @@ -95,6 +134,8 @@ struct d1830_gpio { unsigned long last_adc_jiffies; int psy_ticks; struct delayed_work trace; + struct delayed_work confirm; + u8 sleep_pending; int trace_r[12]; int trace_din; bool trace_inited; @@ -352,7 +393,7 @@ void d1830_n31_din_nirq(void) static irqreturn_t d1830_irq_thread(int irq, void *data) { struct d1830_gpio *gpio_dev = data; - static unsigned hits; + static unsigned int hits; hits++; if (hits <= 8 || (hits & 0x3f) == 0) { @@ -443,7 +484,23 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) if (!sleep) { if (gpio_dev->last_sleep) { - dev_info(&client->dev, + /* + * First low sample only arms the press. A real press easily + * outlives btn_confirm_ms; a glitched I2C byte during a NAND + * CS storm does not, and power-watch turns one KEY_POWER into + * reboot(RB_POWER_OFF). + */ + if (btn_confirm_ms && !gpio_dev->sleep_pending) { + gpio_dev->sleep_pending = 1; + dev_dbg(&client->dev, + "n31-btn SLEEP arm r7=0x%02x (confirm in %ums)\n", + r7, btn_confirm_ms); + schedule_delayed_work(&gpio_dev->confirm, + msecs_to_jiffies(btn_confirm_ms)); + return; + } + gpio_dev->sleep_pending = 0; + d1830_vinfo(&client->dev, "n31-btn SLEEP PRESS r7=0x%02x (bit5 1->0)\n", r7); s5l8740_n31_report_key(KEY_POWER, 1); @@ -456,17 +513,23 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) * Short press still emits KEY_POWER for power-watch / * pm_power_off; hold is the kernel-direct fallback when * userspace is not watching. One noisy I2C byte must not - * hibernate. */ + * hibernate. + */ if (gpio_dev->sleep_hold < 5) gpio_dev->sleep_hold++; if (gpio_dev->sleep_hold == 5) { - dev_warn(&client->dev, - "n31-btn SLEEP held — poweroff\n"); - /* Prefer machine pm_power_off (same cut_power). */ - if (pm_power_off) - pm_power_off(); - else - d1830_cut_power(client); + if (!sleep_poweroff) { + d1830_vinfo(&client->dev, + "n31-btn SLEEP held (poweroff disabled; sleep_poweroff=1 to enable)\n"); + } else { + dev_warn(&client->dev, + "n31-btn SLEEP held — poweroff\n"); + /* Prefer machine pm_power_off (same cut_power). */ + if (pm_power_off) + pm_power_off(); + else + d1830_cut_power(client); + } } } else { if (!gpio_dev->last_sleep) { @@ -478,6 +541,7 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) input_sync(gpio_dev->input); } } + gpio_dev->sleep_pending = 0; gpio_dev->sleep_hold = 0; } gpio_dev->last_sleep = sleep; @@ -496,16 +560,28 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) static void d1830_trace_work(struct work_struct *work) { struct d1830_gpio *gpio_dev = container_of(to_delayed_work(work), - struct d1830_gpio, trace); + struct d1830_gpio, trace); + unsigned int period = btn_poll_ms ? btn_poll_ms : 1000; + unsigned int psy_every = 10000 / period; - d1830_btn_poll_once(gpio_dev); - if (gpio_dev->psy && ++gpio_dev->psy_ticks >= 100) { + if (btn_poll_ms) + d1830_btn_poll_once(gpio_dev); + if (gpio_dev->psy && ++gpio_dev->psy_ticks >= (psy_every ? psy_every : 1)) { gpio_dev->psy_ticks = 0; power_supply_changed(gpio_dev->psy); if (gpio_dev->usb_psy) power_supply_changed(gpio_dev->usb_psy); } - schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(100)); + schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(period)); +} + +/* Second look at Sleep after btn_confirm_ms; see d1830_btn_poll_once(). */ +static void d1830_confirm_work(struct work_struct *work) +{ + struct d1830_gpio *gpio_dev = container_of(to_delayed_work(work), + struct d1830_gpio, confirm); + + d1830_btn_poll_once(gpio_dev); } /* @@ -771,8 +847,8 @@ static int d1830_write8(struct i2c_client *client, u8 reg, u8 val) { int ret; - dev_info(&client->dev, "n31-pmic: WR %02x <- %02x%s\n", - reg, val, dump_only ? " (suppressed)" : ""); + d1830_vinfo(&client->dev, "n31-pmic: WR %02x <- %02x%s\n", + reg, val, dump_only ? " (suppressed)" : ""); if (dump_only) return 0; ret = i2c_smbus_write_byte_data(client, reg, val); @@ -790,9 +866,9 @@ static int d1830_rmw(struct i2c_client *client, u8 reg, u8 clear, u8 set) if (v < 0) return v; newv = (u8)((v & ~clear) | set); - dev_info(&client->dev, - "n31-pmic: RMW reg=%02x old=%02x clear=%02x set=%02x new=%02x%s\n", - reg, v, clear, set, newv, dump_only ? " (suppressed)" : ""); + d1830_vinfo(&client->dev, + "n31-pmic: RMW reg=%02x old=%02x clear=%02x set=%02x new=%02x%s\n", + reg, v, clear, set, newv, dump_only ? " (suppressed)" : ""); if (dump_only) return 0; return i2c_smbus_write_byte_data(client, reg, newv); @@ -813,8 +889,8 @@ static void d1830_log_audio_regs(struct i2c_client *client, const char *tag) snprintf(hex, sizeof(hex), "ERR"); else snprintf(hex, sizeof(hex), "%02x", v); - dev_info(&client->dev, "n31-pmic: %s RD %02x -> %s\n", - tag, regs[i], hex); + d1830_vinfo(&client->dev, "n31-pmic: %s RD %02x -> %s\n", + tag, regs[i], hex); } } @@ -833,8 +909,8 @@ int d1830_nimbus_rail(bool on) if (before < 0) return before; ret = d1830_rmw(client, 16, BIT(5), on ? BIT(5) : 0); - dev_info(&client->dev, "nimbus rail reg16 0x%02x -> bit5=%d ret=%d\n", - before, on, ret); + d1830_vinfo(&client->dev, "nimbus rail reg16 0x%02x -> bit5=%d ret=%d\n", + before, on, ret); return ret; } EXPORT_SYMBOL_GPL(d1830_nimbus_rail); @@ -857,7 +933,7 @@ static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) int v20, v21, v35; u8 fill, r16; - dev_info(&client->dev, + d1830_vinfo(&client->dev, "n31-pmic: sub_23EC-equivalent begin boot_mode=0x%02x\n", boot_mode); @@ -888,7 +964,7 @@ static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) d1830_rmw(client, 17, 0, 0x07); d1830_rmw(client, 19, 0, 0x02); - dev_info(&client->dev, "n31-pmic: sub_23EC-equivalent complete\n"); + d1830_vinfo(&client->dev, "n31-pmic: sub_23EC-equivalent complete\n"); return 0; } @@ -904,7 +980,7 @@ int d1830_audio_rails(void) if (!client) return -ENODEV; if (!allow_audio_rails) { - dev_info(&client->dev, "n31-pmic: audio rails skipped (allow_audio_rails=0)\n"); + d1830_vinfo(&client->dev, "n31-pmic: audio rails skipped (allow_audio_rails=0)\n"); d1830_log_audio_regs(client, "audio-skip"); return 0; } @@ -940,17 +1016,17 @@ static int d1830_sec_rail_seq(struct i2c_client *client) r02 = i2c_smbus_read_byte_data(client, 2); if (r02 < 0) return r02; - dev_info(dev, "n31-pmic: RD 02 -> %02x\n", r02); + d1830_vinfo(dev, "n31-pmic: RD 02 -> %02x\n", r02); if (r02 & 0x80) { boot_mode = 0x11; d1830_write8(client, 2, 0x80); } - dev_info(dev, "n31-pmic: boot_mode=0x%02x\n", boot_mode); + d1830_vinfo(dev, "n31-pmic: boot_mode=0x%02x\n", boot_mode); r01 = i2c_smbus_read_byte_data(client, 1); if (r01 < 0) return r01; - dev_info(dev, "n31-pmic: RD 01 -> %02x\n", r01); + d1830_vinfo(dev, "n31-pmic: RD 01 -> %02x\n", r01); if (boot_mode == 0x11) { dev_warn(dev, @@ -972,7 +1048,7 @@ static int d1830_sec_rail_seq(struct i2c_client *client) d1830_write8(client, 14, 0x20); d1830_rmw(client, 38, 0x01, 0); - dev_info(dev, "n31-pmic: sub_27F4-equivalent complete (POWEROFF skipped)\n"); + d1830_vinfo(dev, "n31-pmic: sub_27F4-equivalent complete (POWEROFF skipped)\n"); return 0; } @@ -1006,7 +1082,7 @@ static int d1830_gpio_probe(struct i2c_client *client) if (of_property_read_bool(dev->of_node, "dlg,apply-sec-rails")) d1830_sec_rail_seq(client); else - dev_info(dev, "d1830 gpio-only (rail seq off; CS42 calls d1830_audio_rails)\n"); + d1830_vinfo(dev, "d1830 gpio-only (rail seq off; CS42 calls d1830_audio_rails)\n"); d1830_log_audio_regs(client, "probe"); @@ -1042,7 +1118,7 @@ static int d1830_gpio_probe(struct i2c_client *client) if (!pm_power_off) { pm_power_off = d1830_pm_power_off; - dev_info(dev, "registered pm_power_off (SEC reg %u bit0)\n", + d1830_vinfo(dev, "registered pm_power_off (SEC reg %u bit0)\n", D1830_REG_POWEROFF); } else { dev_warn(dev, "pm_power_off already set — sysfs do_poweroff only\n"); @@ -1066,7 +1142,7 @@ static int d1830_gpio_probe(struct i2c_client *client) dev_err(dev, "PMIC nIRQ %d failed: %d\n", client->irq, ret); else - dev_info(dev, + d1830_vinfo(dev, "PMIC nIRQ virq=%d hwirq=%lu LEVEL_LOW (OSOS 40641C type=1, EFBB4 DIN=0)\n", client->irq, d ? d->hwirq : 0); } else { @@ -1077,7 +1153,7 @@ static int d1830_gpio_probe(struct i2c_client *client) { int v = i2c_smbus_read_byte_data(client, 7); - dev_info(dev, + d1830_vinfo(dev, "PMIC 7bit=0x%02x wire WR=0x%02x RD=0x%02x reg7 %s (%d)\n", client->addr, client->addr << 1, (client->addr << 1) | 1, @@ -1105,7 +1181,7 @@ static int d1830_gpio_probe(struct i2c_client *client) PTR_ERR(gpio_dev->psy)); gpio_dev->psy = NULL; } else if (!d1830_read_vbat(gpio_dev, &mv)) { - dev_info(dev, + d1830_vinfo(dev, "battery psy OSOS ch3 10-bit*6 mV=%d (design %u mAh)\n", mv, D1830_DESIGN_UAH / 1000); } @@ -1129,7 +1205,7 @@ static int d1830_gpio_probe(struct i2c_client *client) } } - dev_info(dev, "Registered %u read-only GPIOs using Dialog D1830 driver\n", + d1830_vinfo(dev, "Registered %u read-only GPIOs using Dialog D1830 driver\n", gpio_dev->num_gpios); gpio_dev->input = devm_input_allocate_device(dev); @@ -1145,13 +1221,16 @@ static int d1830_gpio_probe(struct i2c_client *client) gpio_dev->input = NULL; } - /* Idle snapshot plus 100ms poll. nIRQ (GPIO 86) still calls - * d1830_n31_din_nirq; poll covers a missed EIC edge so Home / - * Sleep / Play show on n31-btn. Trace prints only on change. + /* Idle snapshot only. Home / Sleep / Play arrive on the PMIC nIRQ + * (GPIO 86) via d1830_n31_din_nirq; btn_poll_ms re-enables the old + * 100ms sweep if an EIC edge is ever missed. The slow tick that + * remains is the battery power_supply refresh. */ d1830_btn_poll_once(gpio_dev); INIT_DELAYED_WORK(&gpio_dev->trace, d1830_trace_work); - schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(100)); + INIT_DELAYED_WORK(&gpio_dev->confirm, d1830_confirm_work); + schedule_delayed_work(&gpio_dev->trace, + msecs_to_jiffies(btn_poll_ms ? btn_poll_ms : 1000)); d1830_n31_din_nirq_hook = d1830_n31_din_nirq; return 0; } @@ -1160,8 +1239,10 @@ static void d1830_gpio_remove(struct i2c_client *client) { struct d1830_gpio *gpio_dev = i2c_get_clientdata(client); - if (gpio_dev) + if (gpio_dev) { cancel_delayed_work_sync(&gpio_dev->trace); + cancel_delayed_work_sync(&gpio_dev->confirm); + } device_remove_file(&client->dev, &dev_attr_vbat_raw); device_remove_file(&client->dev, &dev_attr_audio_rails); device_remove_file(&client->dev, &dev_attr_do_poweroff); diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c index 590081d837f35b..60ff482c7c649e 100644 --- a/drivers/gpio/gpio-s5l8740.c +++ b/drivers/gpio/gpio-s5l8740.c @@ -34,6 +34,8 @@ #include #include +#include + #define S5L8740_GPIO_BANK_STRIDE 32 #define S5L8740_GPIO_DIN_OFF 0x04 #define S5L8740_GPIO_DOUT_OFF 0x08 @@ -44,9 +46,6 @@ #define S5L8740_CMD_OUT_LOW 14 #define S5L8740_CMD_OUT_HIGH 15 -/* From irq-s5l8740-eic.c */ -int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); - /* * IpodSec sub_223C / sub_47CC — packed pinmux word: * [31:24] bank, [23:16] pin, [15] pull?, [12] bit→+0x14?, [8] bit→+0x10, diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c index cda75496962dc7..75da807390cce8 100755 --- a/drivers/input/touchscreen/apple-nimbus.c +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -81,6 +81,8 @@ #include #include +#include + #define NIMBUS_MAGIC 0xEA #define NIMBUS_PING_TYPE 490 #define NIMBUS_BOOTLOAD_WORD 6593 /* 0x19C1 */ @@ -168,9 +170,14 @@ MODULE_PARM_DESC(prepend_z2_hdr, "0=none (N31 default) 1=5A5A+BE len+CRC32 2=c3f static int chunk_spi; module_param(chunk_spi, int, 0644); MODULE_PARM_DESC(chunk_spi, "1=spi_sync chunk xfers (apple_z2-style atomic CS)"); -static int quiet; +/* Default quiet: bring-up spam off; set quiet=0 or verbose=1 for detail. */ +static int quiet = 1; module_param(quiet, int, 0644); -MODULE_PARM_DESC(quiet, "1=minimal logs (auto after GO fail)"); +MODULE_PARM_DESC(quiet, "1=minimal logs (default); 0=verbose bring-up (or apple_nimbus.verbose=1)"); + +static bool verbose; +module_param(verbose, bool, 0644); +MODULE_PARM_DESC(verbose, "Verbose Nimbus logs (overrides quiet; default N)"); static int skip_download; module_param(skip_download, int, 0644); MODULE_PARM_DESC(skip_download, "1=bootload+ping only, no FW chunks"); @@ -230,16 +237,21 @@ static unsigned int exec_word1 = 0x00000100; module_param(exec_word1, uint, 0644); MODULE_PARM_DESC(exec_word1, "2D54C EXEC word1 (OSOS 0x00000100)"); -/* nand-s5l8740.ko exports (optional link). */ -bool nand_ftl_present(void); -int nand_ftl_read_sector(u64 logical_sector, void *buf); -static bool nimbus_verbose = true; +static bool nimbus_verbose; + +#define nimbus_dev_vinfo(dev, fmt, ...) \ + do { \ + if (nimbus_verbose) \ + dev_info((dev), fmt, ##__VA_ARGS__); \ + else \ + dev_dbg((dev), fmt, ##__VA_ARGS__); \ + } while (0) #define nimbus_vinfo(n, fmt, ...) \ do { \ - if (nimbus_verbose && !(n)->parked) \ - dev_info(&(n)->spi->dev, fmt, ##__VA_ARGS__); \ + if (!(n)->parked) \ + nimbus_dev_vinfo(&(n)->spi->dev, fmt, ##__VA_ARGS__); \ } while (0) struct nimbus { @@ -275,10 +287,7 @@ struct nimbus { unsigned int recycle_count; }; -/* From irq-s5l8740-eic.c */ -int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); /* From gpio-d1830.c — OSOS 20766 / 6644(4) / reg16 bit5 */ -int d1830_nimbus_rail(bool on); static u16 nimbus_sum16(const u8 *buf, int len) { @@ -364,7 +373,7 @@ static void nimbus_power_down(struct nimbus *n) nimbus_spi2_pinmux(n, false); d1830_nimbus_rail(false); nimbus_gpiocmd_mode(n, NIMBUS_GPIO_EN, 1, 0); - dev_info(&n->spi->dev, "1A878 power-cut (RST hold, rail off, EN mode 1)\n"); + nimbus_vinfo(n, "1A878 power-cut (RST hold, rail off, EN mode 1)\n"); } /* @@ -392,7 +401,7 @@ static void nimbus_spi2_11b70(struct nimbus *n) n->spi2 + SPI2_CTRL); writel(SPI2_CTRL_ENABLE, n->spi2 + SPI2_CTRL); setup = readl(n->spi2 + SPI2_SETUP); - dev_info(&n->spi->dev, "11B70 SPI2 SETUP=0x%x CLKDIV=%u\n", + nimbus_vinfo(n, "11B70 SPI2 SETUP=0x%x CLKDIV=%u\n", setup, readl(n->spi2 + SPI2_CLKDIV)); } @@ -593,7 +602,7 @@ static int nimbus_probe_26494(struct nimbus *n, const char *tag) ret = nimbus_xfer(n, tx, rx, NIMBUS_FRAME_LEN); w0 = (u16)((rx[0] << 8) | rx[1]); w1 = (u16)((rx[2] << 8) | rx[3]); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "26494 %s ret=%d words 0x%04x 0x%04x known=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", tag, ret, w0, w1, nimbus_opcode_known(w0) && nimbus_opcode_known(w1), rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); @@ -645,7 +654,7 @@ static void nimbus_fwfile_classify(struct nimbus *n, const u8 *data, size_t size u32 le0c = (h8740 && size >= 0x10) ? get_unaligned_le32(data + 0xc) : 0; u8 rev = (h8740 && size >= 5) ? data[4] : 0; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "FWFILE size=%zu first16=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x has_8740=%d rev=%u le32(+0xc)=0x%x arm@0=%d arm@0x400=%d Z2FW=%d\n", size, size > 0 ? data[0] : 0, size > 1 ? data[1] : 0, @@ -669,14 +678,14 @@ static void __maybe_unused nimbus_log_calcand(struct nimbus *n, const char *name u32 s; if (size < off + NIMBUS_FW_HDR_LEN) { - dev_info(&n->spi->dev, "CALCAND %s off=%u OOB (file=%zu)\n", + nimbus_vinfo(n, "CALCAND %s off=%u OOB (file=%zu)\n", name, off, size); return; } memcpy(tmp, data + off, NIMBUS_FW_HDR_LEN); nimbus_bswap32_words(tmp, NIMBUS_FW_HDR_LEN); s = nimbus_sum32(tmp, NIMBUS_FW_HDR_LEN); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "CALCAND %s off=%u sum32=0x%08x first16=%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x (post-bswap)\n", name, off, s, tmp[0], tmp[1], tmp[2], tmp[3], tmp[4], tmp[5], tmp[6], @@ -709,7 +718,7 @@ static void nimbus_fw_audit(struct nimbus *n, const u8 *body, size_t body_len, return; crc = nimbus_crc32_payload(body, body_len); pad = body_len & 3u; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "FW audit %s %zuB arm=%d crc32=0x%08x pad=%zu\n", tag, body_len, nimbus_looks_like_arm(body, body_len), crc, pad); if (body_len >= 16) { @@ -719,7 +728,7 @@ static void nimbus_fw_audit(struct nimbus *n, const u8 *body, size_t body_len, u32 c_le = get_unaligned_le32(body + 8); if (m == NIMBUS_Z2_MAGIC_5A5A || m == NIMBUS_Z2_MAGIC_C3F5) - dev_info(&n->spi->dev, + nimbus_vinfo(n, " z2-dl hdr magic=0x%08x len_le=%u len_be=%u crc=0x%08x\n", m, ln_le, ln_be, c_le); } @@ -756,7 +765,7 @@ static u8 *nimbus_maybe_prepend_z2_hdr(struct nimbus *n, const u8 *body, memset(buf + *out_len, 0, 4 - (*out_len & 3)); *out_len = round_up(*out_len, 4); } - dev_info(&n->spi->dev, + nimbus_vinfo(n, "prepended Z2 dl hdr magic=0x%08x total=%zu\n", magic, *out_len); return buf; } @@ -823,7 +832,7 @@ static int nimbus_hbpp_wake_ee(struct nimbus *n, const char *tag) tx[14] = 0xee; ret = nimbus_burst16(n, tx, rx); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "HBPP 0xEE wake %s ret=%d rx %02x %02x %02x %02x %02x %02x\n", tag, ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5]); return ret; @@ -880,7 +889,7 @@ static void nimbus_fw_readback(struct nimbus *n, const char *tag) buf = kmalloc(0x1000, GFP_KERNEL); if (!buf) return; - dev_info(&n->spi->dev, "NIMBUS FW_READBACK %s:\n", tag); + nimbus_vinfo(n, "NIMBUS FW_READBACK %s:\n", tag); for (i = 0; i < ARRAY_SIZE(addrs); i++) { u32 crc100, crc1000; @@ -892,7 +901,7 @@ static void nimbus_fw_readback(struct nimbus *n, const char *tag) } crc100 = nimbus_crc32_payload(buf, 0x100); crc1000 = nimbus_crc32_payload(buf, 0x1000); - dev_info(&n->spi->dev, + nimbus_vinfo(n, " addr=%08x first32=%32ph crc100=0x%08x crc1000=0x%08x\n", addrs[i], buf, crc100, crc1000); } @@ -915,7 +924,7 @@ static void nimbus_cal_readback(struct nimbus *n, const u8 *upload) } crc_chip = nimbus_crc32_payload(buf, NIMBUS_FW_HDR_LEN); crc_host = nimbus_crc32_payload(upload, NIMBUS_FW_HDR_LEN); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "NIMBUS CAL_READBACK @%08x first64=%32ph %32ph crc200=0x%08x host_crc=0x%08x match=%d\n", NIMBUS_CAL_DEST, buf, buf + 32, crc_chip, crc_host, crc_chip == crc_host && !memcmp(buf, upload, NIMBUS_FW_HDR_LEN)); @@ -938,7 +947,7 @@ static int nimbus_bootload_cmd(struct nimbus *n) } memset(rx, 0, sizeof(rx)); ret = nimbus_xfer(n, tx, rx, NIMBUS_FRAME_LEN); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "bootload 6593 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); return ret; @@ -1016,14 +1025,14 @@ static int nimbus_prepare_cal_from_isys(struct nimbus *n, const u8 *isys, raw = n->isys + NIMBUS_FW_HDR_OFF; memcpy(n->cal_upload, raw, NIMBUS_FW_HDR_LEN); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "cal +350 raw head %02x %02x %02x %02x%s\n", raw[0], raw[1], raw[2], raw[3], nimbus_cal_looks_ni(raw) ? " (NI family — good)" : " (not NI — suspect vs 4S/IOReg cal)"); nimbus_bswap32_words(n->cal_upload, NIMBUS_FW_HDR_LEN); sum = nimbus_sum32(n->cal_upload, NIMBUS_FW_HDR_LEN); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "Nimbus IsyS cal prepared: off=%u len=0x%x sum32=0x%08x upload_first32=%32ph\n", NIMBUS_FW_HDR_OFF, NIMBUS_FW_HDR_LEN, sum, n->cal_upload); if (!sum) { @@ -1062,7 +1071,7 @@ static int nimbus_load_isys_from_a34(struct nimbus *n) ptr = readl(desc_io + 4); iounmap(desc_io); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "A34 IsyS descriptor: magic=0x%08x ptr=0x%08x\n", magic, ptr); @@ -1090,7 +1099,7 @@ static int nimbus_load_isys_from_a34(struct nimbus *n) memcpy_fromio(tmp, src_io, NIMBUS_ISYS_LEN); iounmap(src_io); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "A34 IsyS read: ptr=0x%08x len=0x%x first32=%32ph calraw_first16=%16ph\n", ptr, NIMBUS_ISYS_LEN, tmp, tmp + NIMBUS_FW_HDR_OFF); @@ -1128,7 +1137,7 @@ static int nimbus_load_isys_from_dt(struct nimbus *n) if (ret) goto out; - dev_info(&n->spi->dev, "DT IsyS: addr=0x%08x size=0x%x\n", addr, size); + nimbus_vinfo(n, "DT IsyS: addr=0x%08x size=0x%x\n", addr, size); if (!addr || size != NIMBUS_ISYS_LEN) { ret = -EINVAL; @@ -1148,7 +1157,7 @@ static int nimbus_load_isys_from_dt(struct nimbus *n) goto out; } - dev_info(&n->spi->dev, + nimbus_vinfo(n, "DT IsyS read: addr=0x%08x len=0x%x first32=%32ph calraw_first16=%16ph\n", addr, size, tmp, tmp + NIMBUS_FW_HDR_OFF); @@ -1171,7 +1180,7 @@ static int nimbus_acquire_isys_cal(struct nimbus *n) if (!ret) return 0; - dev_info(&n->spi->dev, "DT IsyS unavailable: %d; trying A34 live\n", + nimbus_vinfo(n, "DT IsyS unavailable: %d; trying A34 live\n", ret); ret = nimbus_load_isys_from_a34(n); @@ -1254,7 +1263,7 @@ static u8 *nimbus_try_gpfw_from_ftl(struct device *dev, size_t *out_len) NIMBUS_FTL_SECTOR_SIZE); } if (got >= 0x410) { - dev_info(dev, + nimbus_dev_vinfo(dev, "gpfw/8740 from FTL lba=%llu off=%u need=%zu got=%zu rev=%u\n", lba, off, need, got, buf[7]); *out_len = got; @@ -1300,10 +1309,10 @@ static int nimbus_acquire_fw(struct device *dev, const u8 **data, static void nimbus_release_fw(const struct firmware *fw, u8 *kbuf) { - if (kbuf) - kfree(kbuf); - else if (fw) + /* Exactly one of the decoded copy and the firmware blob is live. */ + if (!kbuf && fw) release_firmware(fw); + kfree(kbuf); } /* @@ -1403,10 +1412,10 @@ static void nimbus_log_upload_prefix(struct nimbus *n, const char *tag, unsigned int len, const u8 *tx, unsigned int xfer_len, u16 ack, int ack_ret) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "NIMBUS %s chunk=%u dest=%08x len=%04x xfer=%u ACK=0x%04x ret=%d\n", tag, chunk_idx, dest, len, xfer_len, ack, ack_ret); - dev_info(&n->spi->dev, + nimbus_vinfo(n, " tx[0:16] = %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n", tx[0], tx[1], tx[2], tx[3], tx[4], tx[5], tx[6], tx[7], tx[8], tx[9], tx[10], tx[11], @@ -1425,21 +1434,21 @@ static void nimbus_dump_hbpp_tx(struct nimbus *n, const char *tag, nimbus_log_upload_prefix(n, tag, chunk_idx, dest, chunk_len, tx, xfer_len, ack, ack_ret); if (raw && chunk_len >= 64) - dev_info(&n->spi->dev, " raw first64=%32ph %32ph\n", + nimbus_vinfo(n, " raw first64=%32ph %32ph\n", raw, raw + 32); else if (raw) - dev_info(&n->spi->dev, " raw first%u=%*ph\n", + nimbus_vinfo(n, " raw first%u=%*ph\n", chunk_len, chunk_len, raw); if (xfer_len >= 96) - dev_info(&n->spi->dev, + nimbus_vinfo(n, " tx first96=%32ph %32ph %32ph\n", tx, tx + 32, tx + 64); else if (xfer_len > 16) - dev_info(&n->spi->dev, " tx first%u=%*ph\n", + nimbus_vinfo(n, " tx first%u=%*ph\n", xfer_len, xfer_len, tx); if (xfer_len >= 32) { last_off = xfer_len - 32; - dev_info(&n->spi->dev, " tx last32=%32ph\n", tx + last_off); + nimbus_vinfo(n, " tx last32=%32ph\n", tx + last_off); } } @@ -1497,7 +1506,7 @@ static int nimbus_send_chunk_ex(struct nimbus *n, const u8 *data, } if (n->blob16 && try == 0) { n->blob16 = false; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "16-bit DATA no 4BC1 — falling back to 8-bit PIO\n"); } } @@ -1614,12 +1623,12 @@ static int nimbus_post_download(struct nimbus *n) u32 rb = 0; ret = nimbus_cmd_34ad0(n, pokes[i].a1, pokes[i].a2, pokes[i].a3); - dev_info(&n->spi->dev, "34AD0[%d] %d\n", i, ret); + nimbus_vinfo(n, "34AD0[%d] %d\n", i, ret); if (ret) return ret; /* 4AD1 = write ACK only; verify with RDREG while still in HBPP. */ if (nimbus_rdreg(n, pokes[i].a1, &rb) == 0) - dev_info(&n->spi->dev, + nimbus_vinfo(n, "34AD0[%d] RDREG 0x%08x -> 0x%08x (wrote %u)\n", i, pokes[i].a1, rb, pokes[i].a2); } @@ -1631,7 +1640,7 @@ static int nimbus_post_download(struct nimbus *n) msleep(65); /* 2D5B0: 3D5706 success only — does not require 0x4BC1 */ if (nimbus_status_poll(n, &st) == 0) { - dev_info(&n->spi->dev, "post-poke status 0x%04x\n", st); + nimbus_vinfo(n, "post-poke status 0x%04x\n", st); n->requestcal_done = true; return 0; } @@ -1657,7 +1666,7 @@ static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) if (go_spi_setup > 0) { saved_setup = readl(n->spi2 + SPI2_SETUP); writel((u32)go_spi_setup, n->spi2 + SPI2_SETUP); - dev_info(&n->spi->dev, "2D54C GO SETUP 0x%x (was 0x%x)\n", + nimbus_vinfo(n, "2D54C GO SETUP 0x%x (was 0x%x)\n", go_spi_setup, saved_setup); } } @@ -1669,7 +1678,7 @@ static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) ret = nimbus_burst(n, tx, rx, 12); if (saved_setup) writel(saved_setup, n->spi2 + SPI2_SETUP); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "2D54C %08x %08x ret=%d xfer=%d rx %02x %02x %02x %02x %02x %02x\n", word0, word1, ret, go_xfer, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5]); @@ -1698,7 +1707,7 @@ static void nimbus_pre_exec_verify(struct nimbus *n) u32 v = 0; if (nimbus_rdreg(n, addrs[i], &v) == 0) - dev_info(&n->spi->dev, "pre-EXEC RDREG 0x%08x=0x%08x\n", + nimbus_vinfo(n, "pre-EXEC RDREG 0x%08x=0x%08x\n", addrs[i], v); } } @@ -1732,7 +1741,7 @@ static int nimbus_probe_z2_eb(struct nimbus *n) tx[1] = 0x01; put_unaligned_le16(0xeb + 1, tx + 14); ret = nimbus_burst16(n, tx, rx); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "z2-EB ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); return (ret == 0 && rx[0] == 0xe1) ? 0 : -EIO; @@ -1751,7 +1760,7 @@ static int nimbus_probe_ea16(struct nimbus *n) csum = nimbus_sum16(tx, 14); put_unaligned_le16(csum, tx + 14); ret = nimbus_burst16(n, tx, rx); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "EA16 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); return (ret == 0 && rx[0] == NIMBUS_MAGIC) ? 0 : -EIO; @@ -1777,7 +1786,7 @@ static int nimbus_probe_ping16(struct nimbus *n) spi_message_init(&m); spi_message_add_tail(&t, &m); ret = spi_sync(n->spi, &m); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "ping16 ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x csum=%d\n", ret, rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7], nimbus_sum16(rx, 14) == get_unaligned_le16(rx + 14)); @@ -1959,14 +1968,14 @@ static int nimbus_send_preconstructed_hbpp(struct nimbus *n, const u8 *data, if (ret) continue; if (nimbus_wait_ack(n, NIMBUS_ACK_CHUNK, 8) == 0) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "preconstructed HBPP %zuB ACK 0x4BC1 try=%d\n", len, try); return 0; } if (n->blob16 && try == 0) { n->blob16 = false; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "preconstructed HBPP: fall back to 8-bit\n"); } } @@ -2035,7 +2044,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, * disk-shaped image — send the whole 8740+ARM at dest 0. */ if (rev != 3) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "204E0 ARM-at-0 %zuB dest 0 (rev=%u hdr+0x0c=0x%x file=%zu)\n", body_len, rev, get_unaligned_le32(data + 0x0c), size); @@ -2046,7 +2055,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, round_up(min_t(size_t, body_len, size - 0x400) & ~15u, 16), false) == 0 && nimbus_looks_like_arm(try, min_t(size_t, 16, body_len))) { - dev_info(&n->spi->dev, "force_gid 422FFA ARM ok\n"); + nimbus_vinfo(n, "force_gid 422FFA ARM ok\n"); body = try; body_len = min_t(size_t, body_len, size - 0x400); dec = try; @@ -2056,7 +2065,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, } goto send; } - dev_info(&n->spi->dev, + nimbus_vinfo(n, "204E0 ARM-at-0 %zu bytes rev=%u (hdr+0x0c=0x%x file=%zu)\n", body_len, rev, get_unaligned_le32(data + 0x0c), size); @@ -2070,14 +2079,14 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, if (nimbus_gid_crypt(&n->spi->dev, probe, 16, true) == 0 && !memcmp(probe, data + 0x40, 16)) { verified = true; - dev_info(&n->spi->dev, "26CCC GID verify OK\n"); + nimbus_vinfo(n, "26CCC GID verify OK\n"); } else { memcpy(probe, data, 16); if (nimbus_422ffa_mmio(&n->spi->dev, probe, 16, true) == 0 && !memcmp(probe, data + 0x40, 16)) { verified = true; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "26CCC 422FFA verify OK\n"); } else { dev_warn(&n->spi->dev, @@ -2101,12 +2110,12 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, ret = nimbus_gid_crypt(&n->spi->dev, dec, body_len, false); } - dev_info(&n->spi->dev, + nimbus_vinfo(n, "204E0 GID decrypt ret=%d arm=%d head %02x %02x %02x %02x ver=%d\n", ret, nimbus_looks_like_arm(dec, body_len), dec[0], dec[1], dec[2], dec[3], verified); if (!ret && body_len > 0xd210) - dev_info(&n->spi->dev, + nimbus_vinfo(n, "ARM +0x54 %02x%02x%02x%02x +0x100 %02x%02x%02x%02x +0x1000 %02x%02x%02x%02x +0xD208 %02x%02x%02x%02x +0x20=%08x\n", dec[0x54], dec[0x55], dec[0x56], dec[0x57], dec[0x100], dec[0x101], dec[0x102], dec[0x103], @@ -2126,7 +2135,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, dev_warn(&n->spi->dev, "FW empty (%zu) — skip download\n", size); return -EINVAL; } else { - dev_info(&n->spi->dev, "Grape FW download %zu bytes (no 8740)\n", + nimbus_vinfo(n, "Grape FW download %zu bytes (no 8740)\n", size); } @@ -2136,15 +2145,15 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, * send once and run post-download. Do not re-packetize ARM. */ if (nimbus_looks_like_hbpp_data(body, body_len)) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "preconstructed HBPP DATA %zuB — direct SPI (no ARM wrap)\n", body_len); ret = nimbus_send_preconstructed_hbpp(n, body, body_len); if (!ret) { - ret = nimbus_post_download(n); - if (!ret) - ret = nimbus_cmd_2d54c(n); - } + ret = nimbus_post_download(n); + if (!ret) + ret = nimbus_cmd_2d54c(n); + } kfree(dec); return ret; } @@ -2159,7 +2168,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, int try, cal; if (official < body_len) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "cap ARM %zu -> %zu (204E0 +0x0c; strip S/C fill)\n", body_len, official); body_len = official; @@ -2179,7 +2188,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, memset(pad_buf + dl_len, 0, round_up(dl_len, 4) - dl_len); dl_len = round_up(dl_len, 4); dl_body = pad_buf; - dev_info(&n->spi->dev, "FW padded to %zu (4-byte align)\n", + nimbus_vinfo(n, "FW padded to %zu (4-byte align)\n", dl_len); } nimbus_fw_audit(n, dl_body, dl_len, "2D640"); @@ -2207,17 +2216,17 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, return cal; } - dev_info(&n->spi->dev, + nimbus_vinfo(n, "2D640 ARM dest_base=0x%08x EXEC=0x%08x cal=0x%08x\n", fw_dest, exec_addr, NIMBUS_CAL_DEST); if (fw_dest == 0) - dev_info(&n->spi->dev, + nimbus_vinfo(n, "expect FW prefix: 18 e1 30 01 07 fc 00 00 00 00 01 03 (len=0x1ff0)\n"); else dev_warn(&n->spi->dev, "fw_dest override 0x%08x — OSOS uses 0 (A/B only)\n", fw_dest); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "expect CAL prefix: 18 e1 30 01 00 80 02 00 00 40 00 c2\n"); /* 20E94: 273A0 up to 3 times, no 1A878 between. */ @@ -2240,7 +2249,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, continue; } n->cal_uploaded = true; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "2D7A4 512B cal @0x%08x ACK (transport only)\n", NIMBUS_CAL_DEST); nimbus_cal_readback(n, win); @@ -2252,7 +2261,7 @@ static int nimbus_download_fw(struct nimbus *n, const u8 *data, size_t size, } ret = nimbus_cmd_2d54c(n); if (!ret) { - dev_info(&n->spi->dev, + nimbus_vinfo(n, "2D54C EXEC sent (try %d) — await runtime ping\n", try); break; @@ -2446,7 +2455,7 @@ static void nimbus_dump_pad(struct nimbus *n, unsigned int gpio, const char *nam pcon = readl(b); din = readl(b + 0x04); dir = readl(b + 0x14); - dev_info(&n->spi->dev, + nimbus_vinfo(n, "pad %s gpio%u pcon=%x din=%u dir=%u dout=%u punb=%u punc=%u\n", name, gpio, (pcon >> (4 * pin)) & 0xf, !!(din & BIT(pin)), !!(dir & BIT(pin)), @@ -2467,7 +2476,7 @@ static void nimbus_gpio_por_reset(struct nimbus *n) msleep(reset_release_ms); nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); msleep(5); - dev_info(&n->spi->dev, "extra POR RST %dms low / %dms high\n", + nimbus_vinfo(n, "extra POR RST %dms low / %dms high\n", reset_hold_ms, reset_release_ms); } @@ -2525,7 +2534,7 @@ static void nimbus_irq_enable(struct nimbus *n) nimbus_gpiocmd_mode(n, NIMBUS_GPIO_IRQ, 0, 0); /* RetailOS: level, active-low → VIC EXT1 */ if (s5l8740_eic_enable_gpio(NIMBUS_GPIO_IRQ, IRQ_TYPE_LEVEL_LOW) == 0) - dev_info(&n->spi->dev, "EIC enabled GPIO%d level-low\n", + nimbus_vinfo(n, "EIC enabled GPIO%d level-low\n", NIMBUS_GPIO_IRQ); } @@ -2544,7 +2553,7 @@ static int nimbus_1a5ac_and_download(struct nimbus *n, const u8 *data, msleep(15); /* 1A5AC: after 20848, before 2075A(0) */ nimbus_gpio_release_reset(n); if (skip_download) { - dev_info(&n->spi->dev, "skip_download — no 2D640\n"); + nimbus_vinfo(n, "skip_download — no 2D640\n"); return 0; } /* 20E94: 26494 then 273A0; settle already done in release (30ms). */ @@ -2590,7 +2599,7 @@ static void nimbus_recycle(struct nimbus *n) return; } n->recycle_count++; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "1703E8 10 ping fails — 13A20 recycle %u/%u\n", n->recycle_count, NIMBUS_RECYCLE_MAX); nimbus_power_down(n); @@ -2609,7 +2618,7 @@ static void nimbus_recycle(struct nimbus *n) msleep(2); if (!nimbus_ping(n, &st)) { n->spi_ok = true; - dev_info(&n->spi->dev, "recycle ping ok, status=0x%04x\n", st); + nimbus_vinfo(n, "recycle ping ok, status=0x%04x\n", st); } else if (n->recycle_count >= NIMBUS_RECYCLE_MAX) { nimbus_park(n, "still bootloader after GO"); } @@ -2631,7 +2640,7 @@ static void nimbus_service(struct nimbus *n) } n->ping_fails++; if (nimbus_verbose && (n->ping_fails <= 3 || n->ping_fails == 10)) - dev_info(&n->spi->dev, + nimbus_vinfo(n, "188FFC ping still fail (%u) attn=%d\n", n->ping_fails, n->attn ? gpiod_get_value_cansleep(n->attn) : -1); @@ -2690,7 +2699,7 @@ static int nimbus_poll_thread(void *data) } if (!n->fw_loaded && !n->fw_tried) { n->fw_tried = true; - dev_info(&n->spi->dev, + nimbus_vinfo(n, "no apple/grape-nimbus.bin — bootload+ping only\n"); } @@ -2783,7 +2792,7 @@ static int nimbus_probe(struct spi_device *spi) return -ENOMEM; n->spi = spi; n->blob16 = false; - nimbus_verbose = !quiet; + nimbus_verbose = verbose || !quiet; mutex_init(&n->lock); spi_set_drvdata(spi, n); @@ -2861,7 +2870,7 @@ static int nimbus_probe(struct spi_device *spi) n->spi_ok = true; n->runtime_ready = true; n->fw_loaded = true; - dev_info(&spi->dev, + nimbus_dev_vinfo(&spi->dev, "runtime ping ok status=0x%04x (ready)\n", ping_st); } else { @@ -2871,7 +2880,7 @@ static int nimbus_probe(struct spi_device *spi) nimbus_peek(n, "post-go-fail"); /* Diagnostics only after runtime fail. */ if (nimbus_status_poll(n, &st) == 0) - dev_info(&spi->dev, + nimbus_dev_vinfo(&spi->dev, "post-fail HBPP status 0x%04x\n", st); err = 0; /* keep 1A878 retries going */ @@ -2884,7 +2893,7 @@ static int nimbus_probe(struct spi_device *spi) } } nimbus_release_fw(fw, kbuf); - dev_info(&spi->dev, + nimbus_dev_vinfo(&spi->dev, "nimbus state uploaded=%d cal=%d reqcal=%d exec=%d runtime=%d spi_ok=%d\n", n->fw_uploaded, n->cal_uploaded, n->requestcal_done, n->exec_sent, n->runtime_ready, n->spi_ok); @@ -2905,7 +2914,7 @@ static int nimbus_probe(struct spi_device *spi) n->irq = -1; } else { n->use_irq = true; - dev_info(&spi->dev, + nimbus_dev_vinfo(&spi->dev, "IRQ-driven (VIC irq %d + EIC GPIO%d)\n", n->irq, NIMBUS_GPIO_IRQ); } diff --git a/drivers/misc/apple-mikeybus.c b/drivers/misc/apple-mikeybus.c index 09929b3a02bf70..4e045bb0201586 100755 --- a/drivers/misc/apple-mikeybus.c +++ b/drivers/misc/apple-mikeybus.c @@ -1,35 +1,55 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Apple MikeyBus — N31 headset / remote (UART2 @ 0x3DC00000) + * Apple N31 MikeyBus, decomp-aligned Linux driver. * - * OSOS decomp (finish-line — grounded): - * Read open: sub_570BA8 → cmd 9/0x71/channel4 (bit4 @ 0x8A9239C) - * RX producer: sub_500ECC — packet type 0x70 appends to 1024B ring - * (@0x8AE5298, index @0x8AE5294, wrap &0x3FF) - * ReadTask: sub_2542F0 drains ring via sub_570C1C/sub_150A38; - * on byte 0xAA appends synthetic 0x01 (no button decode) - * Resistor: sub_410DB0 — cmd 3/0x8D/channel3; wait timeout 100; - * result @0x8A92444 (sample 15→100 if flag clear); - * NOT GPIO66/67 DIN polling; NOT raw UART decode - * ResistorTask: 0x00254382 — measure → sub_41F0D8(0x80, sample, 1) - * Status pkts: 0x76 / 0x8A → v=pkt[3]; bit4→0, bit5→128 + * RetailOS objects: + * CMikeyBusUartReadTask + * CMikeyBusUartResistorTask * - * Linux layers: - * 1) RX byte trace — raw + osos-shaped (0xAA→+0x01); no button decode - * 2) Resistor/model — force_plugged default; measure = -EOPNOTSUPP - * until command backend mapped; never flap jack on unknown - * 3) Event/jack — ALSA/export may use force_plugged / force_model only + * Read path: + * sub_570BA8 opens channel 4 with command 9/0x71/channel4. + * sub_500ECC handles lower packet type 0x70 and appends payload bytes + * to a 1024-byte RX ring. + * sub_2542F0 drains that RX ring and appends each byte to a parent stream. + * If byte == 0xAA, it appends an extra 0x01 to the stream. * - * GPIO 66/67 = UART2 pad mux only (sub_5714EE case 2). Not resistor detect. + * Resistor/model path: + * sub_410DB0 enables channel 3 and submits command 3/0x8D/channel3. + * It waits with timeout 100 on the backend result object. + * It reads sample byte 0x8A92444 and modifier byte 0x8A9244C. + * If sample == 15 and modifier == 0, sample is remapped to 100. + * + * Presence/model state: + * sub_587F38 consumes 0x7E / 0x8A-like presence events. + * sub_17DD6C toggles the model modifier/state byte and opens/closes + * the read path. + * + * Gate/audio route: + * sub_42D364 and sub_587E60 tie the Mikey state to the CS42/audio route. + * Do not blindly poke audio rails here. + * + * Not implemented yet: + * button input-event mapping. + * + * Never do: + * GPIO66/67 resistor detection. Those are UART pins, not model-detect GPIOs. + * + * Optional UART pad mux (GPIO 66/67 = TX/RX): + * GPIO_PHYS 0x3cf00000, GPIOCMD_PHYS 0x3cf001e0, TX=0x42 RX=0x43, + * GPIOCMD mode 2 for UART function ONLY. Never sample those pins as + * resistor DIN / model detect. */ +#include #include #include +#include #include #include #include #include #include #include +#include #include #include @@ -40,87 +60,109 @@ #define MIKEY_GPIO_TX 0x42u /* 66 — UART TX mux */ #define MIKEY_GPIO_RX 0x43u /* 67 — UART RX mux */ -#define MIKEY_MODEL_OPEN 0x0Bu -#define MIKEY_MODEL_A18 0x01u - -/* OSOS RX ring is 1024 bytes (index wrap & 0x3FF). */ #define MIKEY_RX_RING_SIZE 1024 +#define MIKEY_TASK_RING_SIZE 2048 -/* - * force_plugged until command 3/0x8D resistor backend is mapped. - * Do not invent DIN thresholds; do not treat missing RX as unplug. - */ -static bool force_plugged_param = true; -module_param_named(force_plugged, force_plugged_param, bool, 0644); -MODULE_PARM_DESC(force_plugged, - "Force jack present until resistor cmd backend (default 1)"); +#define MIKEY_SAMPLE_DEFAULT 0x64 +#define MIKEY_SAMPLE_OPEN_CIRCUIT 0x0b +#define MIKEY_SAMPLE_REMAP_FROM 0x0f -static u8 force_model_param = MIKEY_MODEL_A18; -module_param_named(force_model, force_model_param, byte, 0644); -MODULE_PARM_DESC(force_model, - "Model reported under force_plugged (default 0x01 A18)"); +#define MIKEY_CH_RESISTOR 3 +#define MIKEY_CH_READ 4 -static bool uart_auto_open = true; -module_param(uart_auto_open, bool, 0644); -MODULE_PARM_DESC(uart_auto_open, - "Open UART2 for raw RX trace (default 1)"); +#define MIKEY_PKT_RX_BYTES 0x70 +#define MIKEY_PKT_IGNORED_74 0x74 +#define MIKEY_PKT_STATUS_76 0x76 +#define MIKEY_PKT_STATUS_8A 0x8a -/* - * ResistorTask loop period placeholder. OSOS: wait timeout 100, default - * sample 100, fail-path delay 10 — none proven as Linux ms period. - */ -static unsigned int resistor_period_ms = 100; -module_param(resistor_period_ms, uint, 0644); -MODULE_PARM_DESC(resistor_period_ms, - "Resistor worker period (NOT proven ms; 0=off)"); - -static bool instantiate_uart2; -module_param(instantiate_uart2, bool, 0444); -MODULE_PARM_DESC(instantiate_uart2, - "ignored; use DT uart2 okay"); - -struct mikey_rx_ring { - u8 buf[MIKEY_RX_RING_SIZE]; - unsigned int head; /* next write */ - unsigned int count; +#define MIKEY_INJECT_MAX 64 + +/* -------------------- module parameters -------------------- */ + +static bool force_plugged; +module_param(force_plugged, bool, 0644); +MODULE_PARM_DESC(force_plugged, "Force headset plugged state for bring-up"); + +static int force_model = -1; +module_param(force_model, int, 0644); +MODULE_PARM_DESC(force_model, "Force headset model sample, -1 disables"); + +static bool auto_report = true; +module_param(auto_report, bool, 0644); +MODULE_PARM_DESC(auto_report, "Print plug/unplug/model changes to kernel log"); + +static bool active_probe; +module_param(active_probe, bool, 0644); +MODULE_PARM_DESC(active_probe, + "Experimental: actively send model probe command if transport is implemented"); + +static int poll_ms = 500; +module_param(poll_ms, int, 0644); +MODULE_PARM_DESC(poll_ms, "Model poll interval in milliseconds"); + +static int baud = 115200; +module_param(baud, int, 0644); +MODULE_PARM_DESC(baud, "MikeyBus UART baud rate"); + +static bool accept_case3_model = true; +module_param(accept_case3_model, bool, 0644); +MODULE_PARM_DESC(accept_case3_model, + "Accept backend packet class 3 as model sample candidate"); + +/* -------------------- state -------------------- */ + +struct apple_mikey_ring { + u8 data[MIKEY_TASK_RING_SIZE]; + u16 head; + u16 tail; + u32 drops; }; struct apple_mikeybus { struct device *dev; struct serdev_device *serdev; + struct mutex lock; + struct delayed_work poll_work; + void __iomem *gpio; void __iomem *gpiocmd; - struct mutex lock; + bool pinmux_on; + bool uart_opened; + + bool auto_report; + bool active_probe; + bool resistor_backend_ready; + + bool plugged; + bool last_reported_plugged; u8 model; - u8 force_model; + u8 last_reported_model; + u8 model_sample; + u8 model_modifier; + bool force_plugged; - bool pinmux_on; - u32 baud; + int force_model; - /* Layer 1: dual RX rings (Linux shadows of OSOS ring). */ - struct mikey_rx_ring raw_rx; /* exact serdev bytes */ - struct mikey_rx_ring osos_rx; /* ReadTask-shaped (+0x01 after 0xAA) */ - u32 rx_bytes; - u8 rx_last[64]; - unsigned int rx_last_len; + u32 decomp_channel_mask_shadow; + u8 rx_status_shadow; - /* - * Linux shadows of OSOS globals (NOT literal addresses): - * rx_status ↔ 0x892A2C8-ish status from 0x76/0x8A - * channel_mask ↔ 0x8A9239C feature bits (4=read, 3=resistor) - * model_sample ↔ last sub_410DB0 sample (or forced) - */ - u8 rx_status; /* 0 / 128 / unchanged */ - u32 channel_mask_shadow; /* bits we "would" enable */ - u8 model_sample; - bool resistor_backend_ready; /* false until cmd 3/0x8D mapped */ + struct apple_mikey_ring rx_raw; + struct apple_mikey_ring rx_task_stream; - bool uart_opened; - struct delayed_work uart_open_work; - struct delayed_work resistor_work; - bool resistor_active; - u32 resistor_ticks; + u32 rx_bytes; + u32 lower_packets; + u32 lower_rx70_packets; + u32 lower_status_packets; + u32 presence_packets; + u32 aa_stuff_count; + u32 model_changes; + u32 plug_events; + u32 unplug_events; + u32 active_probe_count; + u32 active_probe_fail_count; + + int baud; }; static struct apple_mikeybus *mikeybus_singleton; @@ -129,60 +171,43 @@ static struct platform_device *mikey_plat_pdev; static void mikey_ensure_plat(struct work_struct *work); static DECLARE_WORK(mikey_plat_work, mikey_ensure_plat); +/* -------------------- model tables -------------------- */ + static const char *mikey_model_name(u8 model) { switch (model) { - case 1: return "A18"; - case 2: return "B18"; - case 3: return "A62"; - case 4: return "B15"; - case 5: return "A36"; - case 6: return "Apple noise occluding"; - case 7: return "mfg noise occluding"; - case 8: return "mfg noise occluding w/ mic"; - case 9: return "mfg std"; - case 0xA: return "mfg std w/ mic"; - case 0xB: return "open circuit"; - case 0xD: return "B60f"; - case 0xE: return "B60g"; - case 0xF: return "B149"; + case 0x01: return "A18"; + case 0x02: return "B18"; + case 0x03: return "A62"; + case 0x04: return "B15"; + case 0x05: return "A36"; + case 0x06: return "Apple noise occluding"; + case 0x07: return "mfg noise occluding"; + case 0x08: return "mfg noise occluding w/ mic"; + case 0x09: return "mfg std"; + case 0x0a: return "mfg std w/ mic"; + case 0x0b: return "open circuit"; + case 0x0d: return "B60f"; + case 0x0e: return "B60g"; + case 0x0f: return "B149"; case 0x10: return "B187"; + case 0x64: return "default/open/unknown"; default: return "inscrutable"; } } -static bool mikey_headset_has_remote(u8 model) +static bool mikey_sample_is_plugged(u8 sample) { - switch (model) { - case 2: case 4: case 5: case 6: case 7: case 8: case 9: - case 0xA: case 0xD: case 0xE: case 0x10: - return true; - default: + switch (sample) { + case 0x0b: /* open circuit */ + case 0x64: /* RetailOS default/open/unknown */ return false; - } -} - -static bool mikey_headset_ready_locked(struct apple_mikeybus *m) -{ - if (m->force_plugged) + default: return true; - if (m->model == MIKEY_MODEL_OPEN) - return false; - return true; + } } -/* - * Jack present: force_plugged wins. Unknown / open must NOT flap to - * unplugged (false PLUG lesson). Only clear when measure proves open. - */ -static bool mikey_jack_present_locked(struct apple_mikeybus *m) -{ - if (m->force_plugged) - return true; - if (m->model == 0 || m->model == MIKEY_MODEL_OPEN) - return false; - return true; -} +/* -------------------- CS42 exports -------------------- */ int apple_mikeybus_jack_present(void) { @@ -194,7 +219,7 @@ int apple_mikeybus_jack_present(void) return -ENODEV; } mutex_lock(&mikeybus_singleton->lock); - ret = mikey_jack_present_locked(mikeybus_singleton) ? 1 : 0; + ret = mikeybus_singleton->plugged ? 1 : 0; mutex_unlock(&mikeybus_singleton->lock); mutex_unlock(&mikeybus_singleton_lock); return ret; @@ -203,59 +228,52 @@ EXPORT_SYMBOL_GPL(apple_mikeybus_jack_present); int apple_mikeybus_headset_ready(void) { - int ret; - - mutex_lock(&mikeybus_singleton_lock); - if (!mikeybus_singleton) { - mutex_unlock(&mikeybus_singleton_lock); - return -ENODEV; - } - mutex_lock(&mikeybus_singleton->lock); - ret = mikey_headset_ready_locked(mikeybus_singleton) ? 1 : 0; - mutex_unlock(&mikeybus_singleton->lock); - mutex_unlock(&mikeybus_singleton_lock); - return ret; + /* Same as jack_present for now (analog HP gate). */ + return apple_mikeybus_jack_present(); } EXPORT_SYMBOL_GPL(apple_mikeybus_headset_ready); -static void mikey_ring_put(struct mikey_rx_ring *r, u8 b) -{ - r->buf[r->head] = b; - r->head = (r->head + 1) & (MIKEY_RX_RING_SIZE - 1); - if (r->count < MIKEY_RX_RING_SIZE) - r->count++; -} +/* -------------------- rings -------------------- */ -static void mikey_ring_reset(struct mikey_rx_ring *r) +static void mikey_ring_put(struct apple_mikey_ring *r, u8 b) { - r->head = 0; - r->count = 0; + u16 next = (r->head + 1) % sizeof(r->data); + + if (next == r->tail) { + r->drops++; + r->tail = (r->tail + 1) % sizeof(r->data); + } + + r->data[r->head] = b; + r->head = next; } -/* - * Snapshot newest bytes into @dst (up to @max), oldest→newest order among - * the retained window. - */ -static unsigned int mikey_ring_snapshot(const struct mikey_rx_ring *r, - u8 *dst, unsigned int max) +static size_t mikey_ring_dump_hex(struct apple_mikey_ring *r, + char *buf, size_t max) { - unsigned int n, i, start; - - n = min(r->count, max); - if (!n) - return 0; - start = (r->head - n) & (MIKEY_RX_RING_SIZE - 1); - for (i = 0; i < n; i++) - dst[i] = r->buf[(start + i) & (MIKEY_RX_RING_SIZE - 1)]; + size_t n = 0; + u16 p = r->tail; + + while (p != r->head && n + 4 < max) { + n += scnprintf(buf + n, max - n, "%02x ", r->data[p]); + p = (p + 1) % sizeof(r->data); + } + + if (n && n < max) + buf[n - 1] = '\n'; + return n; } -/* UART pad mux only — never used as DIN sample / resistor path. */ +/* -------------------- UART pad mux (UART function only) -------------------- */ + static void mikey_gpiocmd(struct apple_mikeybus *m, u8 gpio, u8 mode) { u32 bank = gpio >> 3; u32 pin = gpio & 7; + if (!m->gpiocmd) + return; writel((bank << 16) | (pin << 8) | mode, m->gpiocmd); } @@ -300,298 +318,541 @@ static void mikey_pinmux_uart(struct apple_mikeybus *m, bool on) } } -/* - * ReadTask-shaped append (sub_2542F0): put byte; if 0xAA also put 0x01. - * Raw ring keeps exact wire bytes separately. - */ -static void mikey_rx_append_byte(struct apple_mikeybus *m, u8 b) +/* -------------------- RX / state -------------------- */ + +static void mikey_rx_byte_locked(struct apple_mikeybus *m, u8 b) { - mikey_ring_put(&m->raw_rx, b); - mikey_ring_put(&m->osos_rx, b); - if (b == 0xaa) - mikey_ring_put(&m->osos_rx, 0x01); + mikey_ring_put(&m->rx_raw, b); + mikey_ring_put(&m->rx_task_stream, b); + + if (b == 0xaa) { + mikey_ring_put(&m->rx_task_stream, 0x01); + m->aa_stuff_count++; + } + + m->rx_bytes++; } -/* - * Lower-packet dispatcher (sub_500ECC shape). Only call with proven - * packet-framed envelopes — never feed raw serdev bytes here. - */ -static void mikey_lower_packet_rx(struct apple_mikeybus *m, - const u8 *pkt, size_t len) +static void mikey_report_state_locked(struct apple_mikeybus *m, + const char *reason) { - u8 type, v; - size_t i, count; + if (!m->auto_report) + return; - if (len < 3) + if (m->plugged == m->last_reported_plugged && + m->model == m->last_reported_model) return; + if (m->plugged && m->last_reported_plugged && + m->model != m->last_reported_model) { + dev_info(m->dev, + "headset changed: %s sample=0x%02x modifier=%u reason=%s\n", + mikey_model_name(m->model), m->model_sample, + m->model_modifier, reason); + } else if (m->plugged) { + dev_info(m->dev, + "headset plugged: %s sample=0x%02x modifier=%u reason=%s\n", + mikey_model_name(m->model), m->model_sample, + m->model_modifier, reason); + m->plug_events++; + } else { + dev_info(m->dev, + "headset unplugged: %s sample=0x%02x modifier=%u reason=%s\n", + mikey_model_name(m->model), m->model_sample, + m->model_modifier, reason); + m->unplug_events++; + } + + m->last_reported_plugged = m->plugged; + m->last_reported_model = m->model; +} + +static void mikey_apply_model_sample_locked(struct apple_mikeybus *m, + u8 sample, + const char *reason) +{ + bool plugged; + u8 model; + + if (sample == MIKEY_SAMPLE_REMAP_FROM && m->model_modifier == 0) + sample = MIKEY_SAMPLE_DEFAULT; + + if (m->force_plugged) { + plugged = true; + model = (m->force_model >= 0) ? (u8)m->force_model : sample; + } else if (m->force_model >= 0) { + sample = (u8)m->force_model; + model = sample; + plugged = mikey_sample_is_plugged(sample); + } else { + model = sample; + plugged = mikey_sample_is_plugged(sample); + } + + if (m->model_sample != sample || m->model != model || + m->plugged != plugged) + m->model_changes++; + + m->model_sample = sample; + m->model = model; + m->plugged = plugged; + + mikey_report_state_locked(m, reason); +} + +/* -------------------- lower / backend packets -------------------- */ + +static void mikey_handle_lower_packet_locked(struct apple_mikeybus *m, + const u8 *pkt, size_t len) +{ + u8 type; + u8 v; + size_t i; + u8 count; + + if (len < 2) + return; + + m->lower_packets++; type = pkt[1]; + switch (type) { - case 0x70: - /* payload = packet+3; count = packet[0]-3 (OSOS). */ - count = pkt[0]; - if (count < 3 || count > len) - count = len; - count -= 3; + case MIKEY_PKT_RX_BYTES: + if (len < 3 || pkt[0] < 3 || pkt[0] > len) + return; + + count = pkt[0] - 3; for (i = 0; i < count; i++) - mikey_rx_append_byte(m, pkt[3 + i]); + mikey_rx_byte_locked(m, pkt[3 + i]); + + m->lower_rx70_packets++; + break; + + case MIKEY_PKT_IGNORED_74: break; - case 0x76: - case 0x8a: - /* sub_18911C / sub_182AFC status shadow. */ + + case MIKEY_PKT_STATUS_76: + case MIKEY_PKT_STATUS_8A: + if (len < 4) + return; + v = pkt[3]; + if (v & 0x10) - m->rx_status = 0; + m->rx_status_shadow = 0; else if (v & 0x20) - m->rx_status = 128; + m->rx_status_shadow = 0x80; + + m->lower_status_packets++; break; + default: + dev_dbg(m->dev, "unknown lower packet type=0x%02x len=%zu\n", + type, len); break; } } -/* - * Command-backend resistor measure (sub_410DB0). Not implemented until - * channel3 / cmd 3/0x8D / wait@0x8A92448 are mapped to Linux. - */ -static int mikey_measure_model(struct apple_mikeybus *m, u8 *sample) +static bool model_completion_byte_plausible(u8 b) +{ + switch (b) { + case 0x01 ... 0x0b: + case 0x0d ... 0x10: + case 0x64: + return true; + default: + return false; + } +} + +static void mikey_handle_model_completion_candidate_locked( + struct apple_mikeybus *m, const u8 *pkt, size_t len) { - (void)m; - (void)sample; - return -EOPNOTSUPP; + u8 sample = 0xff; + + if (!accept_case3_model) + return; + + /* + * Conservative candidate extraction: + * Try packet[3], packet[4], packet[1] in that order. + * + * Why not hard-code one forever? + * The decomp proves the sample global is written in the case-3 lane, + * but the current dump does not yet show enough local context to + * name the exact source byte with full confidence. + */ + if (len > 3 && model_completion_byte_plausible(pkt[3])) + sample = pkt[3]; + else if (len > 4 && model_completion_byte_plausible(pkt[4])) + sample = pkt[4]; + else if (len > 1 && model_completion_byte_plausible(pkt[1])) + sample = pkt[1]; + + if (sample == 0xff) { + dev_dbg(m->dev, "case3 model candidate ignored len=%zu\n", len); + return; + } + + m->resistor_backend_ready = true; + mikey_apply_model_sample_locked(m, sample, "case3"); } -static int mikey_uart_open_locked(struct apple_mikeybus *m) +static void mikey_handle_presence_candidate_locked(struct apple_mikeybus *m, + const u8 *pkt, size_t len) { - int ret; + u8 code; + u8 arg = 0; - if (m->uart_opened) - return 0; - if (!m->serdev) - return -ENODEV; - ret = serdev_device_open(m->serdev); - if (ret) - return ret; - serdev_device_set_baudrate(m->serdev, m->baud); - serdev_device_set_flow_control(m->serdev, false); - m->uart_opened = true; - /* Shadow: Read open enables channel bit 4. */ - m->channel_mask_shadow |= BIT(4); - dev_info(m->dev, - "Mikey UART opened baud=%u (raw RX trace; no button decode; " - "channel4 shadow set)\n", - m->baud); - return 0; + if (len < 2) + return; + + code = pkt[1]; + if (len > 4) + arg = pkt[4]; + + m->presence_packets++; + + if (code == 0x7e) { + m->model_modifier = arg & 1; + dev_info(m->dev, + "presence event: code=0x7e arg=0x%02x modifier=%u\n", + arg, m->model_modifier); + return; + } + + if (code == 0x8a) { + dev_info(m->dev, "presence event: code=0x8a\n"); + return; + } + + dev_dbg(m->dev, "presence candidate code=0x%02x arg=0x%02x\n", + code, arg); } -static void mikey_uart_close_locked(struct apple_mikeybus *m) +static void mikey_handle_backend_packet_locked(struct apple_mikeybus *m, + const u8 *pkt, size_t len) { - if (!m->uart_opened || !m->serdev) + u8 cls; + + if (len < 3) return; - serdev_device_close(m->serdev); - m->uart_opened = false; - m->channel_mask_shadow &= ~BIT(4); - dev_info(m->dev, "Mikey UART closed\n"); + + cls = pkt[2]; + + switch (cls) { + case 3: + mikey_handle_model_completion_candidate_locked(m, pkt, len); + break; + + case 4: + mikey_handle_lower_packet_locked(m, pkt, len); + break; + + case 5: + dev_dbg(m->dev, "backend case5 len=%zu\n", len); + break; + + case 6: + mikey_handle_presence_candidate_locked(m, pkt, len); + break; + + case 16: + dev_dbg(m->dev, "backend case16 len=%zu\n", len); + break; + + default: + dev_dbg(m->dev, "backend packet class=%u len=%zu\n", cls, len); + break; + } } -static void mikey_uart_open_workfn(struct work_struct *work) +/* -------------------- active probe / poll -------------------- */ + +static int mikey_send_active_probe(struct apple_mikeybus *m) { - struct apple_mikeybus *m = - container_of(work, struct apple_mikeybus, uart_open_work.work); + /* + * Do not write this blindly unless the lower transport framing is known. + * + * RetailOS command-object shape: + * cmd[6] = 3 + * cmd[7] = 0x8D + * cmd[8] = 3 + * cmd[3] = 0xFF + * + * The Linux serial transport may not accept the command object bytes + * directly. Keep this disabled until glass capture proves framing. + */ + u8 cmd[] = { + 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x03, 0x8d, 0x03 + }; int ret; - mutex_lock(&m->lock); - ret = mikey_uart_open_locked(m); - mutex_unlock(&m->lock); - if (ret) - dev_warn(m->dev, "Mikey UART auto-open failed: %d\n", ret); + if (!m->active_probe) + return -EOPNOTSUPP; + + m->active_probe_count++; + + if (!m->serdev || !m->uart_opened) + return -ENODEV; + + m->decomp_channel_mask_shadow |= BIT(MIKEY_CH_RESISTOR); + + ret = serdev_device_write_buf(m->serdev, cmd, sizeof(cmd)); + if (ret < 0) + return ret; + if (ret != sizeof(cmd)) + return -EIO; + return 0; } -/* - * ResistorTask-shaped worker (0x00254382): - * force_plugged → keep force_model, stay plugged, return - * measure EOPNOTSUPP → unknown, do NOT flap jack - * on change → update model_sample / model (when backend ready) - */ -static void mikey_resistor_workfn(struct work_struct *work) +static void mikey_poll_work(struct work_struct *work) { struct apple_mikeybus *m = - container_of(work, struct apple_mikeybus, resistor_work.work); - u8 sample = 0; + container_of(to_delayed_work(work), struct apple_mikeybus, + poll_work); int ret; - - if (!m->resistor_active || !resistor_period_ms) - return; + int interval; mutex_lock(&m->lock); - m->resistor_ticks++; - if (m->force_plugged) { - m->model = m->force_model ? m->force_model : MIKEY_MODEL_A18; - m->model_sample = m->model; - if (m->resistor_ticks == 1) - dev_info(m->dev, - "Mikey resistor: force_plugged model=0x%02x " - "(%s); backend_ready=%d\n", - m->model, mikey_model_name(m->model), - m->resistor_backend_ready); - goto resched; - } - - ret = mikey_measure_model(m, &sample); - if (ret == -EOPNOTSUPP) { - /* - * Backend not mapped. Report unknown sample shadow only; - * do not clear plugged / do not set OPEN from lack of RX. - */ - if (m->resistor_ticks == 1) - dev_info(m->dev, - "Mikey resistor: measure -EOPNOTSUPP " - "(cmd 3/0x8D/ch3 not mapped); jack unchanged\n"); - goto resched; - } - if (ret) { - /* OSOS fail path uses sample=100 then delay 10 — shadow only. */ - sample = 100; - m->model_sample = sample; - goto resched; + if (m->force_plugged || m->force_model >= 0) { + u8 sample = (m->force_model >= 0) ? + (u8)m->force_model : MIKEY_SAMPLE_DEFAULT; + mikey_apply_model_sample_locked(m, sample, "force"); + goto out; } - m->channel_mask_shadow |= BIT(3); - if (sample != m->model_sample) { - m->model_sample = sample; - m->model = sample; - dev_info(m->dev, - "Mikey model_sample=%u (0x%02x %s) via measure\n", - sample, sample, mikey_model_name(sample)); + if (m->active_probe) { + ret = mikey_send_active_probe(m); + if (ret < 0) { + m->active_probe_fail_count++; + dev_dbg(m->dev, "active probe failed ret=%d\n", ret); + } } -resched: +out: mutex_unlock(&m->lock); - if (m->resistor_active && resistor_period_ms) - schedule_delayed_work(&m->resistor_work, - msecs_to_jiffies(resistor_period_ms)); + + interval = poll_ms; + if (interval < 100) + interval = 100; + schedule_delayed_work(&m->poll_work, msecs_to_jiffies(interval)); } -static void mikey_ensure_plat(struct work_struct *work) +/* -------------------- hex inject parser -------------------- */ + +static int mikey_parse_hex_bytes(const char *buf, size_t count, + u8 *out, size_t max, size_t *out_len) { - struct device_node *uart_np, *mikey_np = NULL; - int ret; + const char *p = buf; + const char *end = buf + count; + size_t n = 0; - (void)work; - if (mikeybus_singleton) - return; + while (p < end) { + u8 byte; + char tok[32]; + size_t i = 0; - uart_np = of_find_node_by_path("/soc/serial@3dc00000"); - if (uart_np && of_device_is_available(uart_np)) { - pr_info("apple-mikeybus: uart2 okay in DT — waiting on serdev\n"); - of_node_put(uart_np); - return; - } - if (uart_np) - mikey_np = of_get_child_by_name(uart_np, "mikeybus"); + while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || + *p == '\r' || *p == ',')) + p++; + if (p >= end) + break; - mikey_plat_pdev = platform_device_alloc("apple-mikeybus-plat", - PLATFORM_DEVID_NONE); - if (!mikey_plat_pdev) - goto out; - if (mikey_np) - mikey_plat_pdev->dev.of_node = of_node_get(mikey_np); - ret = platform_device_add(mikey_plat_pdev); - if (ret) { - pr_warn("apple-mikeybus: plat add %d\n", ret); - platform_device_put(mikey_plat_pdev); - mikey_plat_pdev = NULL; + while (p < end && *p != ' ' && *p != '\t' && *p != '\n' && + *p != '\r' && *p != ',' && i + 1 < sizeof(tok)) + tok[i++] = *p++; + tok[i] = '\0'; + if (!i) + break; + + /* Inject ABI is hex bytes (guide: "06 70 00 aa 55 33"). */ + if (kstrtou8(tok, 16, &byte)) + return -EINVAL; + if (n >= max) + return -EINVAL; + out[n++] = byte; } -out: - if (mikey_np) - of_node_put(mikey_np); - if (uart_np) - of_node_put(uart_np); + + *out_len = n; + return n ? 0 : -EINVAL; } -/* -------------------- sysfs (Linux shadows) -------------------- */ +/* -------------------- serdev -------------------- */ + +static size_t mikey_serdev_receive(struct serdev_device *serdev, + const u8 *data, size_t count) +{ + struct apple_mikeybus *m = serdev_device_get_drvdata(serdev); + size_t i; + + if (!m) + return count; + + mutex_lock(&m->lock); + for (i = 0; i < count; i++) + mikey_rx_byte_locked(m, data[i]); + mutex_unlock(&m->lock); + + dev_dbg(&serdev->dev, "Mikey RX %zu: %*ph\n", count, + (int)min(count, (size_t)16), data); + + return count; +} + +static const struct serdev_device_ops mikey_serdev_ops = { + .receive_buf = mikey_serdev_receive, + .write_wakeup = serdev_device_write_wakeup, +}; + +/* -------------------- sysfs -------------------- */ + +static ssize_t info_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + ssize_t n; + + mutex_lock(&m->lock); + n = sysfs_emit(buf, + "N31 MikeyBus\n" + "status=YELLOW\n" + "read_path=channel4 command 9/0x71\n" + "resistor_path=channel3 command 3/0x8D\n" + "auto_report=%d\n" + "active_probe=%d\n" + "plugged=%d\n" + "model=0x%02x %s\n" + "model_sample=0x%02x\n" + "model_modifier=%u\n" + "force_plugged=%d\n" + "force_model=%d\n" + "resistor_backend_ready=%d\n" + "accept_case3_model=%d\n" + "baud=%d\n" + "serdev=%s\n" + "uart_opened=%d\n" + "pinmux_on=%d\n" + "button_decode=raw-only\n", + m->auto_report, m->active_probe, m->plugged, + m->model, mikey_model_name(m->model), + m->model_sample, m->model_modifier, + m->force_plugged, m->force_model, + m->resistor_backend_ready, accept_case3_model, + m->baud, m->serdev ? "yes" : "no", + m->uart_opened, m->pinmux_on); + mutex_unlock(&m->lock); + return n; +} +static DEVICE_ATTR_RO(info); + +static ssize_t plugged_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + bool plugged; + + mutex_lock(&m->lock); + plugged = m->plugged; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "%d\n", plugged); +} +static DEVICE_ATTR_RO(plugged); static ssize_t model_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 model; - return sysfs_emit(buf, "0x%02x %s\n", m->model, mikey_model_name(m->model)); + mutex_lock(&m->lock); + model = m->model; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "0x%02x\n", model); } +static DEVICE_ATTR_RO(model); -static ssize_t model_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t model_name_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; - int ret; + u8 model; - ret = kstrtouint(buf, 0, &v); - if (ret || v > 0xff) - return -EINVAL; mutex_lock(&m->lock); - m->model = (u8)v; - m->model_sample = (u8)v; - dev_info(dev, "Mikey model set 0x%02x (%s) via sysfs\n", - m->model, mikey_model_name(m->model)); + model = m->model; mutex_unlock(&m->lock); - return count; + return sysfs_emit(buf, "%s\n", mikey_model_name(model)); } -static DEVICE_ATTR_RW(model); +static DEVICE_ATTR_RO(model_name); -static ssize_t force_model_show(struct device *dev, - struct device_attribute *attr, char *buf) +static ssize_t model_sample_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 sample; - return sysfs_emit(buf, "0x%02x %s\n", m->force_model, - mikey_model_name(m->force_model)); + mutex_lock(&m->lock); + sample = m->model_sample; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "0x%02x\n", sample); } +static DEVICE_ATTR_RO(model_sample); -static ssize_t force_model_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t model_sample_inject_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; + unsigned int val; int ret; - ret = kstrtouint(buf, 0, &v); - if (ret || v > 0xff) + ret = kstrtouint(buf, 0, &val); + if (ret) + return ret; + if (val > 0xff) return -EINVAL; + mutex_lock(&m->lock); - m->force_model = (u8)v; - if (m->force_plugged) { - m->model = m->force_model; - m->model_sample = m->force_model; - } + mikey_apply_model_sample_locked(m, (u8)val, "sample_inject"); mutex_unlock(&m->lock); + return count; } -static DEVICE_ATTR_RW(force_model); +static DEVICE_ATTR_WO(model_sample_inject); -static ssize_t model_sample_show(struct device *dev, - struct device_attribute *attr, char *buf) +static ssize_t model_modifier_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 mod; - return sysfs_emit(buf, "%u\n", m->model_sample); + mutex_lock(&m->lock); + mod = m->model_modifier; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "%u\n", mod); } -static DEVICE_ATTR_RO(model_sample); -static ssize_t plugged_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t model_modifier_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - int p; + unsigned int val; + int ret; + + ret = kstrtouint(buf, 0, &val); + if (ret) + return ret; + if (val > 1) + return -EINVAL; mutex_lock(&m->lock); - p = mikey_jack_present_locked(m); + m->model_modifier = (u8)val; mutex_unlock(&m->lock); - return sysfs_emit(buf, "%d\n", p); + return count; } -static DEVICE_ATTR_RO(plugged); +static DEVICE_ATTR_RW(model_modifier); static ssize_t force_plugged_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -606,278 +867,305 @@ static ssize_t force_plugged_store(struct device *dev, const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; + bool v; int ret; - ret = kstrtouint(buf, 0, &v); + ret = kstrtobool(buf, &v); if (ret) return ret; + mutex_lock(&m->lock); - m->force_plugged = !!v; - if (m->force_plugged) { - m->model = m->force_model ? m->force_model : MIKEY_MODEL_A18; - m->model_sample = m->model; + m->force_plugged = v; + force_plugged = v; + if (v) { + u8 sample = (m->force_model >= 0) ? + (u8)m->force_model : MIKEY_SAMPLE_DEFAULT; + mikey_apply_model_sample_locked(m, sample, "force"); + } else if (m->force_model < 0) { + mikey_apply_model_sample_locked(m, m->model_sample, "force_clear"); } mutex_unlock(&m->lock); return count; } static DEVICE_ATTR_RW(force_plugged); -static ssize_t baud_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t force_model_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - return sysfs_emit(buf, "%u\n", m->baud); + return sysfs_emit(buf, "%d\n", m->force_model); } -static ssize_t baud_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t force_model_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; + int val; int ret; - ret = kstrtouint(buf, 0, &v); - if (ret || !v) + ret = kstrtoint(buf, 0, &val); + if (ret) + return ret; + if (val < -1 || val > 0xff) return -EINVAL; + mutex_lock(&m->lock); - m->baud = v; - if (m->serdev && m->uart_opened) - serdev_device_set_baudrate(m->serdev, v); + m->force_model = val; + force_model = val; + if (m->force_plugged || val >= 0) { + u8 sample = (val >= 0) ? (u8)val : MIKEY_SAMPLE_DEFAULT; + + mikey_apply_model_sample_locked(m, sample, "force"); + } mutex_unlock(&m->lock); return count; } -static DEVICE_ATTR_RW(baud); +static DEVICE_ATTR_RW(force_model); -static ssize_t rx_bytes_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t auto_report_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - return sysfs_emit(buf, "%u\n", m->rx_bytes); + return sysfs_emit(buf, "%d\n", m->auto_report); } -static DEVICE_ATTR_RO(rx_bytes); -static ssize_t rx_raw_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t auto_report_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - u8 tmp[64]; - unsigned int n; - ssize_t out; + bool v; + int ret; + + ret = kstrtobool(buf, &v); + if (ret) + return ret; mutex_lock(&m->lock); - n = mikey_ring_snapshot(&m->raw_rx, tmp, sizeof(tmp)); - out = sysfs_emit(buf, "count=%u last=%*ph\n", m->raw_rx.count, n, tmp); + m->auto_report = v; + auto_report = v; mutex_unlock(&m->lock); - return out; + return count; } -static DEVICE_ATTR_RO(rx_raw); +static DEVICE_ATTR_RW(auto_report); -static ssize_t rx_task_stream_show(struct device *dev, - struct device_attribute *attr, char *buf) +static ssize_t active_probe_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - u8 tmp[64]; - unsigned int n; - ssize_t out; + + return sysfs_emit(buf, "%d\n", m->active_probe); +} + +static ssize_t active_probe_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + bool v; + int ret; + + ret = kstrtobool(buf, &v); + if (ret) + return ret; mutex_lock(&m->lock); - n = mikey_ring_snapshot(&m->osos_rx, tmp, sizeof(tmp)); - out = sysfs_emit(buf, - "count=%u (ReadTask-shaped; 0xAA→+0x01) last=%*ph\n", - m->osos_rx.count, n, tmp); + m->active_probe = v; + active_probe = v; mutex_unlock(&m->lock); - return out; + return count; } -static DEVICE_ATTR_RO(rx_task_stream); +static DEVICE_ATTR_RW(active_probe); -static ssize_t rx_status_892A2C8_shadow_show(struct device *dev, - struct device_attribute *attr, - char *buf) +static ssize_t resistor_backend_ready_show(struct device *dev, + struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + bool ready; - return sysfs_emit(buf, "%u\n", m->rx_status); + mutex_lock(&m->lock); + ready = m->resistor_backend_ready; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "%d\n", ready); } -static DEVICE_ATTR_RO(rx_status_892A2C8_shadow); +static DEVICE_ATTR_RO(resistor_backend_ready); static ssize_t decomp_channel_mask_shadow_show(struct device *dev, struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u32 mask; - return sysfs_emit(buf, - "0x%08x (bit3=resistor cmd shadow, bit4=read open)\n", - m->channel_mask_shadow); + mutex_lock(&m->lock); + mask = m->decomp_channel_mask_shadow; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "0x%08x\n", mask); } static DEVICE_ATTR_RO(decomp_channel_mask_shadow); -static ssize_t resistor_backend_ready_show(struct device *dev, - struct device_attribute *attr, - char *buf) +static ssize_t rx_status_shadow_show(struct device *dev, + struct device_attribute *attr, char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 st; - return sysfs_emit(buf, "%d\n", m->resistor_backend_ready); + mutex_lock(&m->lock); + st = m->rx_status_shadow; + mutex_unlock(&m->lock); + return sysfs_emit(buf, "0x%02x\n", st); } -static DEVICE_ATTR_RO(resistor_backend_ready); +static DEVICE_ATTR_RO(rx_status_shadow); -/* - * Debug inject of a framed lower packet (hex bytes). Does NOT accept - * unframed serdev streams — operator must supply OSOS envelopes. - * Format: echo "len type ..." with decimal/hex tokens, e.g. - * echo "6 0x70 0 aa 01 02" > lower_packet_inject - * First token is OSOS packet[0] length field. - */ -static ssize_t lower_packet_inject_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t rx_raw_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); - u8 pkt[64]; - unsigned int vals[64]; - int n = 0, i; - const char *p = buf; + size_t n; - while (n < 64 && *p) { - unsigned int v; - int matched; + mutex_lock(&m->lock); + n = mikey_ring_dump_hex(&m->rx_raw, buf, PAGE_SIZE); + mutex_unlock(&m->lock); + return n; +} +static DEVICE_ATTR_RO(rx_raw); - while (*p == ' ' || *p == '\t' || *p == '\n') - p++; - if (!*p) - break; - matched = sscanf(p, "%i%n", &v, &i); - if (matched < 1) - break; - vals[n++] = v & 0xff; - p += i; - } - if (n < 3) - return -EINVAL; - for (i = 0; i < n; i++) - pkt[i] = (u8)vals[i]; +static ssize_t rx_task_stream_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + size_t n; mutex_lock(&m->lock); - mikey_lower_packet_rx(m, pkt, n); + n = mikey_ring_dump_hex(&m->rx_task_stream, buf, PAGE_SIZE); mutex_unlock(&m->lock); - return count; + return n; } -static DEVICE_ATTR_WO(lower_packet_inject); +static DEVICE_ATTR_RO(rx_task_stream); -static ssize_t uart_open_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t rx_stats_show(struct device *dev, struct device_attribute *attr, + char *buf) { struct apple_mikeybus *m = dev_get_drvdata(dev); + ssize_t n; - return sysfs_emit(buf, "%d\n", m->uart_opened); + mutex_lock(&m->lock); + n = sysfs_emit(buf, + "rx_bytes=%u\n" + "aa_stuff_count=%u\n" + "lower_packets=%u\n" + "lower_rx70_packets=%u\n" + "lower_status_packets=%u\n" + "presence_packets=%u\n" + "model_changes=%u\n" + "plug_events=%u\n" + "unplug_events=%u\n" + "active_probe_count=%u\n" + "active_probe_fail_count=%u\n" + "rx_raw_drops=%u\n" + "rx_task_drops=%u\n", + m->rx_bytes, m->aa_stuff_count, + m->lower_packets, m->lower_rx70_packets, + m->lower_status_packets, m->presence_packets, + m->model_changes, m->plug_events, m->unplug_events, + m->active_probe_count, m->active_probe_fail_count, + m->rx_raw.drops, m->rx_task_stream.drops); + mutex_unlock(&m->lock); + return n; } +static DEVICE_ATTR_RO(rx_stats); -static ssize_t uart_open_store(struct device *dev, struct device_attribute *attr, - const char *buf, size_t count) +static ssize_t lower_packet_inject_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); - unsigned int v; + u8 pkt[MIKEY_INJECT_MAX]; + size_t len; int ret; - ret = kstrtouint(buf, 0, &v); + ret = mikey_parse_hex_bytes(buf, count, pkt, sizeof(pkt), &len); if (ret) return ret; + mutex_lock(&m->lock); - if (v) - ret = mikey_uart_open_locked(m); - else { - mikey_uart_close_locked(m); - ret = 0; - } + mikey_handle_lower_packet_locked(m, pkt, len); mutex_unlock(&m->lock); - return ret ? ret : count; + return count; } -static DEVICE_ATTR_RW(uart_open); +static DEVICE_ATTR_WO(lower_packet_inject); -static ssize_t info_show(struct device *dev, struct device_attribute *attr, - char *buf) +static ssize_t backend_packet_inject_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) { struct apple_mikeybus *m = dev_get_drvdata(dev); + u8 pkt[MIKEY_INJECT_MAX]; + size_t len; + int ret; + + ret = mikey_parse_hex_bytes(buf, count, pkt, sizeof(pkt), &len); + if (ret) + return ret; - return sysfs_emit(buf, - "MikeyBus UART2 @0x3DC (pads GPIO66/67 mux ONLY)\n" - "decomp: ReadTask drains 0x70 ring; resistor=cmd " - "3/0x8D/ch3 (NOT DIN poll)\n" - "model=0x%02x (%s) sample=%u remote=%d plugged=%d " - "force=%d force_model=0x%02x\n" - "pinmux=%d baud=%u uart_open=%d rx_bytes=%u " - "raw_ring=%u osos_ring=%u\n" - "rx_status_shadow=%u channel_mask_shadow=0x%x " - "resistor_backend_ready=%d ticks=%u\n" - "NO button decode; NO GPIO66/67 DIN detect claim\n", - m->model, mikey_model_name(m->model), m->model_sample, - mikey_headset_has_remote(m->model), - mikey_jack_present_locked(m), m->force_plugged, - m->force_model, m->pinmux_on, m->baud, m->uart_opened, - m->rx_bytes, m->raw_rx.count, m->osos_rx.count, - m->rx_status, m->channel_mask_shadow, - m->resistor_backend_ready, m->resistor_ticks); + mutex_lock(&m->lock); + mikey_handle_backend_packet_locked(m, pkt, len); + mutex_unlock(&m->lock); + return count; } -static DEVICE_ATTR_RO(info); +static DEVICE_ATTR_WO(backend_packet_inject); static struct attribute *mikey_attrs[] = { + &dev_attr_info.attr, + &dev_attr_plugged.attr, &dev_attr_model.attr, - &dev_attr_force_model.attr, + &dev_attr_model_name.attr, &dev_attr_model_sample.attr, - &dev_attr_plugged.attr, + &dev_attr_model_sample_inject.attr, + &dev_attr_model_modifier.attr, &dev_attr_force_plugged.attr, - &dev_attr_baud.attr, - &dev_attr_rx_bytes.attr, + &dev_attr_force_model.attr, + &dev_attr_auto_report.attr, + &dev_attr_active_probe.attr, + &dev_attr_resistor_backend_ready.attr, + &dev_attr_decomp_channel_mask_shadow.attr, + &dev_attr_rx_status_shadow.attr, &dev_attr_rx_raw.attr, &dev_attr_rx_task_stream.attr, - &dev_attr_rx_status_892A2C8_shadow.attr, - &dev_attr_decomp_channel_mask_shadow.attr, - &dev_attr_resistor_backend_ready.attr, + &dev_attr_rx_stats.attr, &dev_attr_lower_packet_inject.attr, - &dev_attr_uart_open.attr, - &dev_attr_info.attr, + &dev_attr_backend_packet_inject.attr, NULL, }; -ATTRIBUTE_GROUPS(mikey); -static size_t mikey_serdev_receive(struct serdev_device *serdev, - const u8 *data, size_t count) -{ - struct apple_mikeybus *m = serdev_device_get_drvdata(serdev); - size_t i, n; +static const struct attribute_group mikey_attr_group = { + .attrs = mikey_attrs, +}; - if (!m || !count) - return count; +static const struct attribute_group *mikey_groups[] = { + &mikey_attr_group, + NULL, +}; - mutex_lock(&m->lock); - m->rx_bytes += count; - for (i = 0; i < count; i++) - mikey_rx_append_byte(m, data[i]); - n = min(count, sizeof(m->rx_last)); - memcpy(m->rx_last, data + count - n, n); - m->rx_last_len = n; - /* - * Raw serdev bytes → rings only. Do NOT run lower_packet_rx here - * until the wire stream is proven packet-framed. - */ - dev_info(m->dev, "Mikey RX %zu: %*ph\n", count, (int)min(count, 16), - data); - mutex_unlock(&m->lock); - return count; +static int mikey_create_sysfs(struct apple_mikeybus *m) +{ + return sysfs_create_groups(&m->dev->kobj, mikey_groups); } -static const struct serdev_device_ops mikey_serdev_ops = { - .receive_buf = mikey_serdev_receive, -}; +static void mikey_remove_sysfs(struct apple_mikeybus *m) +{ + sysfs_remove_groups(&m->dev->kobj, mikey_groups); +} + +/* -------------------- bind / unbind -------------------- */ static int mikey_bind(struct device *dev, struct serdev_device *serdev) { struct apple_mikeybus *m; - u32 baud = 115200; int ret; m = devm_kzalloc(dev, sizeof(*m), GFP_KERNEL); @@ -887,62 +1175,61 @@ static int mikey_bind(struct device *dev, struct serdev_device *serdev) m->dev = dev; m->serdev = serdev; m->baud = baud; - m->model = 0; - m->force_model = force_model_param ? force_model_param : MIKEY_MODEL_A18; - m->force_plugged = force_plugged_param || - of_property_read_bool(dev->of_node, - "apple,force-plugged"); - m->resistor_backend_ready = false; - m->channel_mask_shadow = 0; - m->rx_status = 0; - if (dev->of_node && - !of_property_read_u32(dev->of_node, "current-speed", &baud)) - m->baud = baud; - if (m->force_plugged) { - m->model = m->force_model; - m->model_sample = m->force_model; - } + m->auto_report = auto_report; + m->active_probe = active_probe; + m->force_plugged = force_plugged; + m->force_model = force_model; mutex_init(&m->lock); - mikey_ring_reset(&m->raw_rx); - mikey_ring_reset(&m->osos_rx); - INIT_DELAYED_WORK(&m->uart_open_work, mikey_uart_open_workfn); - INIT_DELAYED_WORK(&m->resistor_work, mikey_resistor_workfn); + INIT_DELAYED_WORK(&m->poll_work, mikey_poll_work); + + m->model_sample = MIKEY_SAMPLE_DEFAULT; + m->model = MIKEY_SAMPLE_DEFAULT; + m->plugged = false; + m->last_reported_plugged = false; + m->last_reported_model = MIKEY_SAMPLE_DEFAULT; + m->gpio = devm_ioremap(dev, GPIO_PHYS, 0x200); m->gpiocmd = devm_ioremap(dev, GPIOCMD_PHYS, 4); dev_set_drvdata(dev, m); + if (serdev) { serdev_device_set_drvdata(serdev, m); serdev_device_set_client_ops(serdev, &mikey_serdev_ops); + + ret = serdev_device_open(serdev); + if (ret) + return ret; + + serdev_device_set_baudrate(serdev, m->baud); + serdev_device_set_flow_control(serdev, false); + m->uart_opened = true; + m->decomp_channel_mask_shadow |= BIT(MIKEY_CH_READ); } mikey_pinmux_uart(m, true); - ret = sysfs_create_groups(&dev->kobj, mikey_groups); - if (ret) - dev_warn(dev, "sysfs: %d\n", ret); + ret = mikey_create_sysfs(m); + if (ret) { + if (serdev && m->uart_opened) { + serdev_device_close(serdev); + m->uart_opened = false; + } + return ret; + } mutex_lock(&mikeybus_singleton_lock); mikeybus_singleton = m; mutex_unlock(&mikeybus_singleton_lock); dev_info(dev, - "MikeyBus ready (%s) baud=%u model=0x%02x (%s) force_plugged=%d " - "(RX=raw+osos rings; resistor=EOPNOTSUPP; no DIN detect; " - "no button decode)\n", + "N31 MikeyBus loaded (%s): auto_report=%d active_probe=%d force_plugged=%d force_model=%d baud=%d\n", serdev ? "serdev" : "platform", - m->baud, m->model, mikey_model_name(m->model), - m->force_plugged); - - if (serdev && uart_auto_open) - schedule_delayed_work(&m->uart_open_work, msecs_to_jiffies(50)); + m->auto_report, m->active_probe, + m->force_plugged, m->force_model, m->baud); - if (resistor_period_ms) { - m->resistor_active = true; - schedule_delayed_work(&m->resistor_work, - msecs_to_jiffies(resistor_period_ms)); - } + schedule_delayed_work(&m->poll_work, msecs_to_jiffies(100)); return 0; } @@ -953,22 +1240,68 @@ static void mikey_unbind(struct device *dev) if (!m) return; - m->resistor_active = false; - cancel_delayed_work_sync(&m->resistor_work); - cancel_delayed_work_sync(&m->uart_open_work); + cancel_delayed_work_sync(&m->poll_work); mutex_lock(&mikeybus_singleton_lock); if (mikeybus_singleton == m) mikeybus_singleton = NULL; mutex_unlock(&mikeybus_singleton_lock); - sysfs_remove_groups(&dev->kobj, mikey_groups); + mikey_remove_sysfs(m); + mutex_lock(&m->lock); - mikey_uart_close_locked(m); + if (m->uart_opened && m->serdev) { + serdev_device_close(m->serdev); + m->uart_opened = false; + } mikey_pinmux_uart(m, false); mutex_unlock(&m->lock); } +/* -------------------- platform fallback -------------------- */ + +static void mikey_ensure_plat(struct work_struct *work) +{ + struct device_node *uart_np, *mikey_np = NULL; + int ret; + + (void)work; + if (mikeybus_singleton) + return; + + /* + * Prefer serdev when uart2 is okay in DT. Only instantiate the + * platform fallback when uart2 is disabled / missing so exports + * and sysfs still exist for CS42 bring-up. + */ + uart_np = of_find_node_by_path("/soc/serial@3dc00000"); + if (uart_np && of_device_is_available(uart_np)) { + pr_info("apple-mikeybus: uart2 okay in DT — waiting on serdev\n"); + of_node_put(uart_np); + return; + } + if (uart_np) + mikey_np = of_get_child_by_name(uart_np, "mikeybus"); + + mikey_plat_pdev = platform_device_alloc("apple-mikeybus-plat", + PLATFORM_DEVID_NONE); + if (!mikey_plat_pdev) + goto out; + if (mikey_np) + mikey_plat_pdev->dev.of_node = of_node_get(mikey_np); + ret = platform_device_add(mikey_plat_pdev); + if (ret) { + pr_warn("apple-mikeybus: plat add %d\n", ret); + platform_device_put(mikey_plat_pdev); + mikey_plat_pdev = NULL; + } +out: + if (mikey_np) + of_node_put(mikey_np); + if (uart_np) + of_node_put(uart_np); +} + static int mikey_serdev_probe(struct serdev_device *serdev) { return mikey_bind(&serdev->dev, serdev); @@ -1025,8 +1358,6 @@ static int __init mikey_init(void) serdev_device_driver_unregister(&mikey_serdev_driver); return ret; } - if (instantiate_uart2) - pr_warn("apple-mikeybus: instantiate_uart2 ignored\n"); schedule_work(&mikey_plat_work); return 0; } @@ -1045,6 +1376,7 @@ static void __exit mikey_exit(void) module_init(mikey_init); module_exit(mikey_exit); -MODULE_DESCRIPTION("Apple MikeyBus N31 (UART2 RX rings + force jack; resistor cmd TBD)"); +MODULE_DESCRIPTION("Apple N31 MikeyBus (serdev RX, model/jack state, CS42 exports)"); MODULE_AUTHOR("FreeMyiPod"); MODULE_LICENSE("GPL"); +MODULE_ALIAS("platform:apple-mikeybus-plat"); diff --git a/drivers/misc/ftl-s5l8740-core.c b/drivers/misc/ftl-s5l8740-core.c index ccc6be53d3b4b5..0e61b7282bbf5e 100755 --- a/drivers/misc/ftl-s5l8740-core.c +++ b/drivers/misc/ftl-s5l8740-core.c @@ -12,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -54,6 +56,191 @@ module_param(scan_blocks, uint, 0644); MODULE_PARM_DESC(scan_blocks, "User blocks per CE/CAU to classify (0 = all; default 256)"); +/* + * BTOC meta-confirm re-reads every candidate data page over CS. Full-SB + * confirm across closed BTOCs wedges USB (~2min then reset). Cap confirms; + * open-META rebuild remains the bulk authority. 0 = unlimited (raise only + * after correctness passes; stage with scan_blocks). + */ +static bool btoc_meta_confirm = true; +module_param(btoc_meta_confirm, bool, 0644); +MODULE_PARM_DESC(btoc_meta_confirm, + "CS-read data pages to take meta_lba as L2V key (default Y)"); + +static unsigned int btoc_confirm_max = 512; +module_param(btoc_confirm_max, uint, 0644); +MODULE_PARM_DESC(btoc_confirm_max, + "Max BTOC CS page confirms per recover (default 512; 0=unlimited)"); +/* Alias name from bring-up notes. */ +module_param_named(btoc_confirm_pages_cap, btoc_confirm_max, uint, 0644); + +/* Soft wall-clock budget for BTOC confirms (0 = ignore). */ +static unsigned int recover_budget_ms; +module_param(recover_budget_ms, uint, 0644); +MODULE_PARM_DESC(recover_budget_ms, + "Stop BTOC confirms after this many ms (0=off)"); + +/* Keep RNDIS/USB alive during long recover (default 2ms every 4 blocks). */ +static unsigned int recover_yield_us = 2000; +module_param(recover_yield_us, uint, 0644); +MODULE_PARM_DESC(recover_yield_us, + "usleep between classify blocks to keep USB alive (0=off; default 2000)"); + +/* + * Interval-map node budget. N31 glass has ~55 MiB of RAM total; each + * whimory_range is a kmalloc-64 slab object. Running out mid-recover is an + * OOM panic (panic=-1 → reboot to RetailOS), which costs a DFU cycle and + * loses the log. Stop adding mappings at the budget and report instead. + */ +static unsigned int max_range_nodes = 200000; +module_param(max_range_nodes, uint, 0644); +MODULE_PARM_DESC(max_range_nodes, + "Interval-map node ceiling; stop mapping past it (0=unlimited)"); + +/* + * The SFTL context is the authoritative FTL snapshot and only turns up + * once scan_blocks reaches the high blocks that hold it (~1960 on N31). + * It maps 600k+ LBAs in a few thousand coalesced ranges, but its VBAs do + * not yet resolve (sftl lba mismatch → no BPB in L2V). Off skips both the + * CXT load and the weave filter that suppresses replay of older SBs, so + * the brute-force BTOC/open rebuild still yields a mountable disk. + */ +/* + * Open-SB rebuild assigns each VBA its own weave, so adjacent LBAs landing + * in adjacent VBAs never merged and the map cost one 64-byte rb node per + * LBA — 46 MiB to replay every open SB on a 55 MiB device. Merging + * neighbours that are contiguous in both LBA and VBA and keeping the OLDER + * weave collapses sequentially written data; keeping the older weave means + * a later claim is never wrongly rejected as stale, it just splits the + * range again. 0 restores exact per-weave ranges. + */ +/* + * Closed superblocks currently cost the same 127 CS reads as open ones: + * the BTOC is used only to pick pages to re-read, and the per-slot meta is + * taken as the L2V key. Before trusting the BTOC records outright, measure + * whether they actually predict that metadata. + */ +static unsigned int btoc_verify_sbs; +module_param(btoc_verify_sbs, uint, 0644); +MODULE_PARM_DESC(btoc_verify_sbs, + "Closed SBs to probe for BTOC-vs-meta agreement (0=off)"); + +static unsigned int btoc_verify_pages = 6; +module_param(btoc_verify_pages, uint, 0644); +MODULE_PARM_DESC(btoc_verify_pages, + "Pages sampled per verified BTOC (default 6)"); + +/* + * Console logging is not free on this target: every dev_info goes out the + * serial console and measurably lengthens recover. Quiet by default; the + * deep bring-up dumps come back with diag=1. + */ +static bool ftl_diag; +module_param_named(diag, ftl_diag, bool, 0644); +MODULE_PARM_DESC(diag, + "Per-LBA/BTOC/VBA diagnostic dumps (default N)"); + +static bool ftl_progress = true; +module_param_named(progress, ftl_progress, bool, 0644); +MODULE_PARM_DESC(progress, "Periodic recover progress lines (default Y)"); + +static unsigned int progress_ms = 5000; +module_param(progress_ms, uint, 0644); +MODULE_PARM_DESC(progress_ms, "Minimum ms between progress lines"); + +/* Rate-limit: emit at most this many of a repeating diagnostic. */ +static unsigned int diag_max_lines = 3; +module_param(diag_max_lines, uint, 0644); +MODULE_PARM_DESC(diag_max_lines, "Cap on repeated read-miss/winner lines"); + +/* True once per progress_ms window; keeps hot loops from flooding. */ +static bool ftl_progress_due(struct whimory *w) +{ + unsigned long now = jiffies; + + if (!ftl_progress) + return false; + if (w->progress_jiffies && + time_before(now, w->progress_jiffies + msecs_to_jiffies(progress_ms))) + return false; + w->progress_jiffies = now; + return true; +} + +static bool range_coalesce = true; +module_param(range_coalesce, bool, 0644); +MODULE_PARM_DESC(range_coalesce, + "Merge LBA/VBA-contiguous ranges across weaves (default Y)"); + +static bool use_cxt = true; +module_param(use_cxt, bool, 0644); +MODULE_PARM_DESC(use_cxt, + "Load the SFTL CXT snapshot during recover (default Y)"); + +/* + * A full rebuild used to be re-runnable at any time, so a stray write to + * ftl_sftl_recover could tear down a live map underneath a mounted disk. + */ +enum whimory_recovery_state { + RECOVERY_NONE = 0, + RECOVERY_RUNNING, + RECOVERY_VALID, + RECOVERY_FAILED, +}; + +static enum whimory_recovery_state recovery_state; +static u32 recovery_params_key; + +static bool recover_force; +module_param(recover_force, bool, 0644); +MODULE_PARM_DESC(recover_force, + "Allow rebuild when a map is already valid/bound (default N)"); + +/* Changing any of these is a different map, so a repeat is not a no-op. */ +static u32 whimory_recover_key(void) +{ + return scan_blocks * 1000003u + max_open_sbs * 10007u + + btoc_confirm_max * 101u + (use_cxt ? 2u : 0u) + + (range_coalesce ? 1u : 0u) + max_range_nodes; +} + +const char *whimory_recovery_state_name(void) +{ + switch (recovery_state) { + case RECOVERY_RUNNING: + return "running"; + case RECOVERY_VALID: + return "valid"; + case RECOVERY_FAILED: + return "failed"; + default: + return "none"; + } +} +EXPORT_SYMBOL_GPL(whimory_recovery_state_name); + +static bool audit_lba_winners; +module_param(audit_lba_winners, bool, 0644); +MODULE_PARM_DESC(audit_lba_winners, + "Log duplicate-LBA winner decisions for critical fmss LBAs"); + +/* Dump every winner decision for this absolute fmss_lba (0 = off). */ +static unsigned int l2v_trace_lba; +module_param(l2v_trace_lba, uint, 0644); +MODULE_PARM_DESC(l2v_trace_lba, + "Log L2V winner old/new for this fmss_lba (0=off)"); + +/* Apply BTOC FFFF0001 LIST unmaps during recover (default Y). */ +static bool btoc_apply_list = true; +module_param(btoc_apply_list, bool, 0644); +MODULE_PARM_DESC(btoc_apply_list, + "Apply BTOC LIST (FFFF0001) unmap payloads during recover"); + +static bool vba_page_dump; +module_param(vba_page_dump, bool, 0644); +MODULE_PARM_DESC(vba_page_dump, + "Emit VBA_DIAG sibling slot dump on critical reads (default N)"); + static unsigned int meta0_scan_sbs = 4; module_param(meta0_scan_sbs, uint, 0644); MODULE_PARM_DESC(meta0_scan_sbs, @@ -97,11 +284,16 @@ static struct whimory *whimory_dev; static struct platform_device *ftl_pdev; static void whimory_l2v_find_frag(struct whimory *w); +static void whimory_l2v_cache_flush(struct whimory *w); static void whimory_l2v_free_tree(struct whimory *w, u32 node_idx, u32 root_idx); static int whimory_l2v_update_packed(struct whimory *w, u32 ridx, u32 off, u32 span, u32 vba); static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, bool allow_blank); +static bool whimory_audit_fmss_lba(u32 fmss_lba); +static void whimory_note_payload_strings(struct whimory *w, const u8 *data, + unsigned int len); +static void whimory_dump_vba_page(struct whimory *w, u32 vba, u32 fmss_lba); static u64 whimory_weave48(const u8 *m) { @@ -235,12 +427,15 @@ static bool whimory_special_lba(u32 lba) static u32 whimory_vfl_phys(struct whimory *w, u32 cau, u32 virt) { /* - *: PBN = VBN (identity over blocks_per_cau). - * The u16 table at CXT +0x200 is a VFL CXT copy journal - * : value = index | (gen<<15), 0xC070 = free), not - * virt→phys. Failed user blocks keep the same VBN and switch - * CAU via the bank bitmap. - */ + * The N31 VFL is an identity map over blocks_per_cau: physical + * block number equals virtual block number. + * + * The u16 table at CXT +0x200 looks like a remap table but is not + * one — it is the VFL context copy journal, where each value is + * index | (generation << 15) and 0xC070 marks a free slot. A failed + * user block keeps its VBN and moves to another CAU instead, which + * the bank bitmap records. + */ if (cau >= w->geom.num_cau || !w->vfl.remap[cau]) return virt; if (virt >= w->geom.blocks_per_cau) @@ -248,7 +443,7 @@ static u32 whimory_vfl_phys(struct whimory *w, u32 cau, u32 virt) return w->vfl.remap[cau][virt]; } -/*: banks that participate in this VBN. */ +/* Collect the CAU banks that participate in this virtual block. */ static u32 whimory_vfl_banks_in_vbn(struct whimory *w, u32 vbn, u8 *out, u32 out_max) { @@ -410,6 +605,33 @@ static struct whimory_range *whimory_range_find(struct rb_root *root, u32 lba) return NULL; } +/* + * Leftmost range that can overlap [lba, ...) — i.e. the first with + * start + len > lba. Ranges are disjoint and keyed by start, so r_end is + * monotonic in tree order and the predicate is a valid binary search. + * + * Callers used to walk from rb_first(), which made every L2V update + * O(ranges) and the whole recover O(ranges^2). At wide scan_blocks that + * spins the CPU long enough to starve RNDIS and trip the watchdog. + */ +static struct whimory_range *whimory_range_lower(struct rb_root *root, u32 lba) +{ + struct rb_node *n = root->rb_node; + struct whimory_range *best = NULL; + + while (n) { + struct whimory_range *r = rb_entry(n, struct whimory_range, rb); + + if (r->start + r->len > lba) { + best = r; + n = n->rb_left; + } else { + n = n->rb_right; + } + } + return best; +} + static int whimory_range_link(struct rb_root *root, struct whimory_range *n) { struct rb_node **link = &root->rb_node, *parent = NULL; @@ -448,6 +670,7 @@ static int whimory_range_split(struct whimory *w, struct whimory_range *r, r->len = left_len; whimory_range_link(&w->ranges, right); w->sftl.range_nodes++; + w->sftl.map_gen++; return 0; } @@ -455,6 +678,7 @@ static void whimory_range_erase(struct whimory *w, struct whimory_range *r) { rb_erase(&r->rb, &w->ranges); kfree(r); + w->sftl.map_gen++; if (w->sftl.range_nodes) w->sftl.range_nodes--; } @@ -466,6 +690,10 @@ static int whimory_range_insert_new(struct whimory *w, u32 start, u32 len, if (!len) return 0; + if (max_range_nodes && w->sftl.range_nodes >= max_range_nodes) { + w->sftl.range_budget_stop++; + return 0; + } n = kzalloc(sizeof(*n), GFP_KERNEL); if (!n) return -ENOMEM; @@ -475,9 +703,19 @@ static int whimory_range_insert_new(struct whimory *w, u32 start, u32 len, n->weave = w->sftl.claim_weave; whimory_range_link(&w->ranges, n); w->sftl.range_nodes++; + w->sftl.map_gen++; return 0; } +/* Merge r with either neighbour when both LBA and VBA stay contiguous. */ +static bool whimory_range_joinable(const struct whimory_range *a, + const struct whimory_range *b) +{ + if (a->start + a->len != b->start || a->vba + a->len != b->vba) + return false; + return range_coalesce || a->weave == b->weave; +} + static void whimory_range_coalesce_at(struct whimory *w, u32 start) { struct whimory_range *r, *prev, *next; @@ -489,10 +727,10 @@ static void whimory_range_coalesce_at(struct whimory *w, u32 start) p = rb_prev(&r->rb); if (p) { prev = rb_entry(p, struct whimory_range, rb); - if (prev->start + prev->len == r->start && - prev->vba + prev->len == r->vba && - prev->weave == r->weave) { + if (whimory_range_joinable(prev, r)) { prev->len += r->len; + if (r->weave < prev->weave) + prev->weave = r->weave; whimory_range_erase(w, r); r = prev; } @@ -500,10 +738,10 @@ static void whimory_range_coalesce_at(struct whimory *w, u32 start) q = rb_next(&r->rb); if (q) { next = rb_entry(q, struct whimory_range, rb); - if (r->start + r->len == next->start && - r->vba + r->len == next->vba && - r->weave == next->weave) { + if (whimory_range_joinable(r, next)) { r->len += next->len; + if (next->weave < r->weave) + r->weave = next->weave; whimory_range_erase(w, next); } } @@ -520,20 +758,21 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) return 0; { - u32 end = lba + span; - struct rb_node *node = rb_first(&w->ranges); + struct whimory_range *first = whimory_range_lower(&w->ranges, + lba); + struct rb_node *node = first ? &first->rb : NULL; while (node) { struct whimory_range *r = rb_entry(node, struct whimory_range, rb); - u32 r_end = r->start + r->len; if (r->start >= end) break; - if (r_end > lba && r->start < end && - r->weave > w->sftl.claim_weave) - return 0; + if (r->weave > w->sftl.claim_weave) { + w->sftl.stale_mapping_rejected++; + return 1; /* stale — do not touch packed L2V */ + } node = rb_next(node); } } @@ -553,7 +792,8 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) } } - node = rb_first(&w->ranges); + hit = whimory_range_lower(&w->ranges, lba); + node = hit ? &hit->rb : NULL; while (node) { struct whimory_range *r = rb_entry(node, struct whimory_range, rb); @@ -564,6 +804,13 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) if (r->start >= lba && r->start + r->len <= end) whimory_range_erase(w, r); node = next; + cond_resched(); + } + + /* True unmap: erase only; do not insert invalid_vba placeholders. */ + if (vba >= w->l2v.invalid_vba) { + whimory_range_coalesce_at(w, lba); + return 0; } ret = whimory_range_insert_new(w, lba, span, vba); @@ -574,14 +821,14 @@ static int whimory_range_update(struct whimory *w, u32 lba, u32 span, u32 vba) } /* - *L2V_Update.c: split at 0x8000 root boundaries, then insert. + * L2V_Update: split at 0x8000 root boundaries, then insert. * The interval map is the RO observable of the live tree. + * Returns 0 on success (including stale-skip of a chunk), <0 on OOM/error. */ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) { - w->sftl.l2v_update_calls++; - if (vba >= w->l2v.invalid_vba) - w->sftl.l2v_unmap_calls++; + bool is_unmap = vba >= w->l2v.invalid_vba; + while (span) { u32 chunk = WHIMORY_L2V_ROOT_SPAN - (lba & (WHIMORY_L2V_ROOT_SPAN - 1)); @@ -589,6 +836,22 @@ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) if (chunk > span) chunk = span; + ret = whimory_range_update(w, lba, chunk, vba); + if (ret < 0) + return ret; + if (ret > 0) { + /* Stale reject: leave packed L2V alone for this chunk. */ + span -= chunk; + lba += chunk; + if (vba < w->l2v.invalid_vba) + vba += chunk; + continue; + } + + w->sftl.l2v_update_calls++; + if (is_unmap) + w->sftl.l2v_unmap_calls++; + if (w->l2v.root && w->l2v.num_roots) { u32 ridx = lba >> 15; u8 *rec; @@ -601,12 +864,12 @@ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) ver = 0; put_unaligned_le16(ver + 1, rec + 4); /* - *: whole-root unmap (off=0, - * span=0x8000, vba=invalid) frees the tree. - */ + * whole-root unmap (off=0, span=0x8000, + * vba=invalid) frees the tree. + */ if (!(lba & 0x7fff) && chunk == WHIMORY_L2V_ROOT_SPAN && - vba >= w->l2v.invalid_vba) { + is_unmap) { node_idx = get_unaligned_le16(rec); if (node_idx != WHIMORY_L2V_INVALID_ROOT) whimory_l2v_free_tree(w, @@ -621,14 +884,18 @@ static int whimory_l2v_update(struct whimory *w, u32 lba, u32 span, u32 vba) if (w->l2v.updates >= WHIMORY_L2V_UPDATE_REPACK) w->l2v.updates = 0; } - ret = whimory_range_update(w, lba, chunk, vba); - if (ret) - return ret; - if (w->l2v.root && w->l2v.num_roots) { + /* + * whimory_l2v_update_packed() repacks a whole root, and + * collecting a root walks the interval map. Doing that per + * update makes replay O(updates x ranges) — the tail of a full + * recover crawled to ~14 s per superblock. The interval map is + * the lookup authority; pack once at the end instead. + */ + if (w->l2v.root && w->l2v.num_roots && !w->l2v_defer_pack) { u32 ridx = lba >> 15; bool whole_unmap = !(lba & 0x7fff) && chunk == WHIMORY_L2V_ROOT_SPAN && - vba >= w->l2v.invalid_vba; + is_unmap; if (ridx < w->l2v.num_roots && !whole_unmap) { ret = whimory_l2v_update_packed(w, ridx, @@ -658,6 +925,8 @@ static void whimory_range_free(struct whimory *w) } w->ranges = RB_ROOT; w->sftl.range_nodes = 0; + w->sftl.map_gen++; + whimory_l2v_cache_flush(w); } /* ------------------------------------------------------------------ */ @@ -949,9 +1218,10 @@ static u32 whimory_l2v_collect_root(struct whimory *w, u32 ridx, u32 base = ridx * WHIMORY_L2V_ROOT_SPAN; u32 win_end = base + WHIMORY_L2V_ROOT_SPAN; u32 cursor = base, nleaf = 0; + struct whimory_range *first = whimory_range_lower(&w->ranges, base); struct rb_node *n; - for (n = rb_first(&w->ranges); n; n = rb_next(n)) { + for (n = first ? &first->rb : NULL; n; n = rb_next(n)) { struct whimory_range *rg = rb_entry(n, struct whimory_range, rb); u32 s, e, vba, span; @@ -984,7 +1254,7 @@ static u32 whimory_l2v_collect_root(struct whimory *w, u32 ridx, return nleaf; } -/*analogue: free this root's tree, pack from the interval map. */ +/* Discard this root's packed tree and rebuild it from the interval map. */ static int whimory_l2v_pack_root(struct whimory *w, u32 ridx) { struct whimory_l2v *l2v = &w->l2v; @@ -1024,7 +1294,7 @@ static int whimory_l2v_pack_root(struct whimory *w, u32 ridx) return 0; } -/*: first insert into an empty root — one node, up to 3 leaves. */ +/* First insert into an empty root: one node holding up to three leaves. */ static int whimory_l2v_grow_empty(struct whimory *w, u32 ridx, u32 off, u32 span, u32 vba) { @@ -1344,19 +1614,54 @@ static int whimory_l2v_lookup(struct whimory *w, u32 lba, return -ELOOP; } +/* + * L2V_Search keeps a sequential hint; do the same here. VFAT walks a cluster + * one 4 KiB sector at a time, so consecutive lookups land in the same extent + * and the rbtree descent is pure overhead. Invalidated by map generation. + */ +static void whimory_l2v_cache_store(struct whimory *w, + const struct whimory_range *r) +{ + w->search_start = r->start; + w->search_len = r->len; + w->search_vba = r->vba; + w->search_gen = w->sftl.map_gen; + w->search_valid = true; +} + +static void whimory_l2v_cache_flush(struct whimory *w) +{ + if (w) + w->search_valid = false; +} + /* Prefer the interval map (L2V_Update result); packed tree is for Search. */ static int whimory_l2v_search(struct whimory *w, u32 lba, u32 *vba_out, u32 *span_out) { - struct whimory_range *r = whimory_range_find(&w->ranges, lba); + struct whimory_range *r; + + if (w->search_valid && w->search_gen == w->sftl.map_gen && + lba >= w->search_start && lba - w->search_start < w->search_len) { + u32 delta = lba - w->search_start; + + *vba_out = w->search_vba + delta; + *span_out = w->search_len - delta; + w->sftl.search_cache_hits++; + return 0; + } + r = whimory_range_find(&w->ranges, lba); if (r) { u32 delta = lba - r->start; + whimory_l2v_cache_store(w, r); + w->sftl.search_cache_misses++; *vba_out = r->vba + delta; *span_out = r->len - delta; return 0; } + w->sftl.search_cache_misses++; return whimory_l2v_lookup(w, lba, vba_out, span_out); } @@ -2446,11 +2751,12 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, w->vfl.ctx_block[cau] = block; /* - *: memcpy(cxt_copies, data+0x100, 4 * num_copies). - * Each record is {le16 phys_block, u8 bank, u8 flags} — VFL CXT - * copy locations in the tail, not a user virt→phys table. - * Live glass: first u32 is often 0x827 (block 2087). - */ + * Copy locations live at +0x100, one 4-byte record each: + * {le16 phys_block, u8 bank, u8 flags}. These point at the VFL + * context copies in the block tail; they are not a user + * virtual-to-physical table. On the glass the first record is + * usually 0x827 (block 2087). + */ tab = page + 0x100; for (i = 0; i < 64 && 0x100 + 4 * (i + 1) <= 0x200; i++) { u16 blk = get_unaligned_le16(tab + i * 4); @@ -2463,7 +2769,7 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } w->vfl.cxt_loc_count += loc; - /*: per-bank u16 CXT copy journal at +0x200 + 32*bank */ + /* Per-bank u16 context copy journal at +0x200 + 32 * bank. */ if (page_len >= WHIMORY_VFL_CXT_HDR + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau + 2) { unsigned int b, j, n16 = w->vfl.cxt_u16_len; @@ -2488,11 +2794,11 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } /* - *bitmap: one byte per VBN (stride 0x8D0D0F0 = 1 on N31), - * bit = bank. Not in the 0x200 header / spare journal. Try the - * remainder of this CXT page; reject if any byte has bits outside - * num_cau (would be unrelated payload). - */ + *bitmap: one byte per VBN (stride 0x8D0D0F0 = 1 on N31), + * bit = bank. Not in the 0x200 header / spare journal. Try the + * remainder of this CXT page; reject if any byte has bits outside + * num_cau (would be unrelated payload). + */ { unsigned int off = WHIMORY_VFL_CXT_HDR + WHIMORY_VFL_SPARE_STRIDE * w->geom.num_cau; @@ -2518,10 +2824,10 @@ static int n31_vfl_ingest_ctx(struct whimory *w, unsigned int ce, } /* - * User VBN→PBN is identity over blocks_per_cau : - * vbn < mcxt.dev.blocks_per_cau). Failed-block replacement lives - * in the u16 tables, not in a 256-entry slice of +0x100. - */ + * User VBN→PBN is identity over blocks_per_cau : + * vbn < mcxt.dev.blocks_per_cau). Failed-block replacement lives + * in the u16 tables, not in a 256-entry slice of +0x100. + */ w->vfl.remap_count = w->geom.blocks_per_cau; dev_info(w->dev, "VFL ingest ce=%u cau=%u blk=%u magic=%d type20=%d cxt_loc=%u identity=%u\n", @@ -2670,7 +2976,7 @@ static int whimory_vfl_open(struct whimory *w) /* SFTL recovery — classify SBs, replay BTOC/META by weave */ /* ------------------------------------------------------------------ */ -/*: FFFF0001 payload is {count, [lba,span]...} → unmap. */ +/* FFFF0001 payload is {count, [lba,span]...} → unmap. */ static int whimory_sftl_apply_list(struct whimory *w, u32 vba) { u8 *buf; @@ -2678,27 +2984,51 @@ static int whimory_sftl_apply_list(struct whimory *w, u32 vba) struct whimory_meta meta; int ret; + /* + * Must not alias data_page: n31_vfl_read_vba uses data_page as the + * page scratch. Prefer gc_data; else a stack-sized one-shot alloc. + */ buf = w->sftl.gc_data; - if (!buf) - buf = w->sftl.data_page; - if (!buf || !w->vfl_ops || !w->vfl_ops->read_vba) + if (!buf) { + buf = kmalloc(WHIMORY_LBA_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + } + if (!w->vfl_ops || !w->vfl_ops->read_vba) { + if (buf != w->sftl.gc_data) + kfree(buf); return -ENOMEM; + } ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); - if (ret) + if (ret) { + if (buf != w->sftl.gc_data) + kfree(buf); return ret; + } count = get_unaligned_le32(buf); - if (!count || count > (WHIMORY_LBA_SIZE - 4) / 8) + if (!count || count > (WHIMORY_LBA_SIZE - 4) / 8) { + if (buf != w->sftl.gc_data) + kfree(buf); return -EINVAL; + } for (i = 0; i < count; i++) { lba = get_unaligned_le32(buf + 4 + 8 * i); span = get_unaligned_le32(buf + 8 + 8 * i); - if (!span || whimory_special_lba(lba)) + /* Cap runaway garbage spans from misclassified tokens. */ + if (!span || whimory_special_lba(lba) || lba >= 0x01000000u) break; + if (span > WHIMORY_L2V_ROOT_SPAN) + span = WHIMORY_L2V_ROOT_SPAN; ret = whimory_l2v_update(w, lba, span, w->l2v.invalid_vba); - if (ret) + if (ret) { + if (buf != w->sftl.gc_data) + kfree(buf); return ret; + } w->sftl.token_list_applied++; } + if (buf != w->sftl.gc_data) + kfree(buf); return 0; } @@ -2748,6 +3078,142 @@ static bool whimory_btoc_looks_be_bte(const u8 *page) return true; } + +/* + * BTOC is a physical-slot index only. Per-slot CS metadata is the L2V key. + * Never L2V_Update(btoc_lpn) — zeros/holes in BTOC were poisoning L2V[0]. + */ +static int whimory_l2v_update_from_slot_meta(struct whimory *w, + unsigned int ce, unsigned int cau, + unsigned int vblock, + unsigned int page, unsigned int slot, + const u8 *meta16, + u32 btoc_hint_lba) +{ + u32 meta_lba, vba; + + if (!meta16) + return 0; + if (meta16[0] != WHIMORY_META_TYPE_DATA && + meta16[0] != WHIMORY_META_TYPE_DATA2) + return 0; + if (meta16[1] & 0x02) + return 0; + if (whimory_meta_erased(meta16, WHIMORY_META_SIZE)) + return 0; + meta_lba = get_unaligned_le32(meta16 + 8); + if (whimory_special_lba(meta_lba) || meta_lba >= 0x01000000u) + return 0; + if (btoc_hint_lba != 0xffffffffu && btoc_hint_lba != meta_lba) { + w->sftl.btoc_meta_mismatch++; + dev_dbg(w->dev, + "btoc_meta_mismatch hint=%u meta_lba=%u ce=%u cau=%u vblock=%u pg=%u slot=%u\n", + btoc_hint_lba, meta_lba, ce, cau, vblock, page, slot); + /* Still trust metadata as authority. */ + } + vba = whimory_pack_vba(w, ce, cau, vblock, page, slot); + w->sftl.claim_weave = whimory_weave48(meta16); + if (w->sftl.claim_source == 0) + w->sftl.claim_source = 1; + if ((audit_lba_winners && whimory_audit_fmss_lba(meta_lba)) || + (l2v_trace_lba && meta_lba == l2v_trace_lba)) { + struct whimory_range *prev = + whimory_range_find(&w->ranges, meta_lba); + u64 prev_weave = prev ? prev->weave : 0; + u32 prev_vba = prev ? prev->vba : ~0u; + bool win = !prev || + !(prev->weave > w->sftl.claim_weave); + const char *src = w->sftl.claim_source == 2 ? "open" : + w->sftl.claim_source == 3 ? "CXT" : + w->sftl.claim_source == 4 ? "LIST" : + "BTOC/meta"; + + dev_info(w->dev, + "LBA_WINNER fmss_lba=%u candidate vba=%u " + "ce=%u cau=%u vblk=%u pg=%u slot=%u weave=%012llx " + "prev_vba=%u prev_weave=%012llx selected=%s " + "reason=%s source=%s\n", + meta_lba, vba, ce, cau, vblock, page, slot, + (unsigned long long)w->sftl.claim_weave, + prev_vba, (unsigned long long)prev_weave, + win ? "yes" : "no", + win ? (prev ? "newer_or_equal_weave" : "first") + : "older_weave_kept", + src); + } + if (whimory_l2v_update(w, meta_lba, 1, vba)) { + w->sftl.claim_weave = 0; + return -ENOMEM; + } + w->sftl.claim_weave = 0; + w->sftl.btoc_meta_confirmed++; + w->sftl.btoc_l2v_updates++; + w->sftl.btoc_recs++; + return 1; +} + +static int whimory_btoc_confirm_page(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int vblock, + unsigned int page, u32 btoc_hint_base, + bool hint_is_page_lpn) +{ + u8 spare[S5L8740_NAND_META_SIZE]; + u8 *data = w->sftl.data_page; + unsigned int slot; + u32 pblock; + int ret, hits = 0; + + if (!btoc_meta_confirm) + return 0; + if (btoc_confirm_max && + w->sftl.btoc_confirm_pages >= btoc_confirm_max) { + w->sftl.btoc_confirm_capped++; + return 0; + } + if (recover_budget_ms && w->sftl.confirm_start_jiffies && + time_after(jiffies, w->sftl.confirm_start_jiffies + + msecs_to_jiffies(recover_budget_ms))) { + w->sftl.btoc_confirm_budget_stop++; + return 0; + } + if (!data) + return -ENOMEM; + pblock = whimory_vfl_phys(w, cau, vblock); + ret = whimory_cs_read_page(w, ce, cau, pblock, page, data, + S5L8740_NAND_PAGE_SIZE, spare, + sizeof(spare)); + if (ret) + return ret; + w->sftl.btoc_confirm_pages++; + if (recover_yield_us && (w->sftl.btoc_confirm_pages & 0x0f) == 0) { + cond_resched(); + usleep_range(recover_yield_us, recover_yield_us + 500); + } + whimory_note_payload_strings(w, data, S5L8740_NAND_PAGE_SIZE); + if (whimory_page_blank(data, 64) && whimory_meta_erased(spare, 16)) + return 0; + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + u32 hint = 0xffffffffu; + + if (btoc_hint_base != 0xffffffffu) { + if (hint_is_page_lpn) + hint = btoc_hint_base * WHIMORY_VBAS_PER_PAGE + + slot; + else if (slot == 0) + hint = btoc_hint_base; + } + ret = whimory_l2v_update_from_slot_meta(w, ce, cau, vblock, + page, slot, + spare + slot * + WHIMORY_META_SIZE, + hint); + if (ret < 0) + return ret; + hits += ret; + } + return hits; +} + static bool whimory_btoc_parse_be_lpn(struct whimory *w, const u8 *page, unsigned int len, unsigned int ce, unsigned int cau, unsigned int vblock) @@ -2766,7 +3232,8 @@ static bool whimory_btoc_parse_be_lpn(struct whimory *w, const u8 *page, page_gran = valid > 0 && valid <= WHIMORY_DATA_PAGES_PER_SB; if (w->sftl.btoc_pages_valid < 5) dev_info(w->dev, - "BTOC_BE_LPN valid=%u %s ce=%u cau=%u vblock=%u\n", + "BTOC_BE_LPN valid=%u %s ce=%u cau=%u vblock=%u " + "(meta-validated)\n", valid, page_gran ? "page-granularity x4" : "slot-granularity", ce, cau, vblock); @@ -2775,40 +3242,47 @@ static bool whimory_btoc_parse_be_lpn(struct whimory *w, const u8 *page, n = min(valid, (unsigned int)WHIMORY_DATA_PAGES_PER_SB); for (i = 0; i < n; i++) { u32 lpn = get_unaligned_be32(page + i * 4); - u32 vba; - unsigned int slot, pg; + unsigned int pg, slot; + int got; w->sftl.btoc_entries_seen++; - if (lpn == 0xffffffff || lpn == WHIMORY_LBA_BLANK) + if (lpn == 0xffffffff || lpn == WHIMORY_LBA_BLANK) { + w->sftl.btoc_hole_entries++; continue; + } if (whimory_special_lba(lpn)) { + if (lpn == WHIMORY_LBA_HOLE) + w->sftl.btoc_hole_entries++; + else if (lpn == WHIMORY_LBA_DELETED || + lpn == WHIMORY_LBA_LIST) + w->sftl.btoc_unmap_entries++; + else + w->sftl.btoc_unknown_entries++; w->sftl.token_hole++; continue; } if (lpn >= 0x01000000u) continue; + /* Zero BTOC slots are holes — never poison L2V[0]. */ + if (lpn == 0) { + w->sftl.btoc_skipped_zero++; + continue; + } if (page_gran) { - for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { - vba = whimory_pack_vba(w, ce, cau, vblock, i, - slot); - if (whimory_l2v_update(w, - lpn * WHIMORY_VBAS_PER_PAGE + - slot, 1, vba)) - return hit > 0; - w->sftl.btoc_l2v_updates++; - w->sftl.btoc_recs++; - hit++; - } + got = whimory_btoc_confirm_page(w, ce, cau, vblock, i, + lpn, true); } else { pg = i / w->sftl.vbas_per_page; slot = i % w->sftl.vbas_per_page; - vba = whimory_pack_vba(w, ce, cau, vblock, pg, slot); - if (whimory_l2v_update(w, lpn, 1, vba)) - break; - w->sftl.btoc_l2v_updates++; - w->sftl.btoc_recs++; - hit++; + if (slot != 0) + continue; + got = whimory_btoc_confirm_page(w, ce, cau, vblock, pg, + lpn, false); } + if (got < 0) + return hit > 0; + if (got > 0) + hit += got; } return hit > 0; } @@ -2818,27 +3292,48 @@ static bool whimory_btoc_parse_be_bte(struct whimory *w, const u8 *page, unsigned int cau, unsigned int vblock) { unsigned int i, recs, vba_ofs = 0, hit = 0; + unsigned int last_pg = ~0u; recs = len / 16; for (i = 0; i < recs; i++) { const u8 *r = page + i * 16; u32 lba = get_unaligned_be32(r + 8); u32 span = r[15]; - u32 vba; - int upd; + unsigned int s; w->sftl.btoc_entries_seen++; if (!span) break; if (whimory_special_lba(lba)) { - if (lba == WHIMORY_LBA_LIST) + if (lba == WHIMORY_LBA_LIST) { + u32 vba = whimory_pack_vba(w, ce, cau, vblock, + vba_ofs / w->sftl.vbas_per_page, + vba_ofs % w->sftl.vbas_per_page); + w->sftl.btoc_holelist_ffff0001++; - else if (lba == WHIMORY_LBA_HOLE) + w->sftl.btoc_unmap_entries++; + if (btoc_apply_list) { + w->sftl.claim_source = 4; + if (whimory_sftl_apply_list(w, vba)) + dev_warn(w->dev, + "BE BTE list token vba=%u failed\n", + vba); + else + w->sftl.token_list++; + w->sftl.claim_source = 1; + } + } else if (lba == WHIMORY_LBA_HOLE) { w->sftl.btoc_token_ffff0000++; - else if (lba == WHIMORY_LBA_DELETED) + w->sftl.btoc_hole_entries++; + } else if (lba == WHIMORY_LBA_DELETED) { w->sftl.btoc_token_ffffff00++; - else if (lba == WHIMORY_LBA_BLANK) + w->sftl.btoc_unmap_entries++; + } else if (lba == WHIMORY_LBA_BLANK) { w->sftl.btoc_token_ffffffff++; + w->sftl.btoc_hole_entries++; + } else { + w->sftl.btoc_unknown_entries++; + } w->sftl.token_hole++; if (vba_ofs + span > WHIMORY_VBAS_PER_SB) break; @@ -2849,16 +3344,26 @@ static bool whimory_btoc_parse_be_bte(struct whimory *w, const u8 *page, break; if (vba_ofs + span > WHIMORY_DATA_VBAS_PER_SB) break; - vba = whimory_pack_vba(w, ce, cau, vblock, - vba_ofs / w->sftl.vbas_per_page, - vba_ofs % w->sftl.vbas_per_page); - upd = whimory_l2v_update(w, lba, span, vba); - if (upd) - break; - w->sftl.btoc_l2v_updates++; + if (lba == 0) { + w->sftl.btoc_skipped_zero++; + vba_ofs += span; + continue; + } + for (s = 0; s < span; s++) { + unsigned int pg = (vba_ofs + s) / w->sftl.vbas_per_page; + int got; + + if (pg == last_pg) + continue; + last_pg = pg; + got = whimory_btoc_confirm_page(w, ce, cau, vblock, pg, + 0xffffffffu, false); + if (got < 0) + return hit > 0; + if (got > 0) + hit += got; + } vba_ofs += span; - hit++; - w->sftl.btoc_recs++; } return hit > 0; } @@ -2868,6 +3373,7 @@ static bool whimory_btoc_parse_bte(struct whimory *w, const u8 *page, unsigned int cau, unsigned int vblock) { unsigned int i, recs, vba_ofs = 0, hit = 0; + unsigned int last_pg = ~0u; if (len < sizeof(struct whimory_bte) || whimory_page_blank(page, 64)) return false; @@ -2881,59 +3387,176 @@ static bool whimory_btoc_parse_bte(struct whimory *w, const u8 *page, (const struct whimory_bte *)(page + i * sizeof(*bte)); u32 lba = le32_to_cpu(bte->lba); u32 span = le32_to_cpu(bte->span); - u32 vba; - int upd; + unsigned int s; w->sftl.btoc_entries_seen++; if (!span) break; if (whimory_special_lba(lba)) { if (lba == WHIMORY_LBA_LIST) { - vba = whimory_pack_vba(w, ce, cau, vblock, - vba_ofs / w->sftl.vbas_per_page, - vba_ofs % w->sftl.vbas_per_page); - if (whimory_sftl_apply_list(w, vba)) - dev_warn(w->dev, - "list token vba=%u failed\n", - vba); - w->sftl.token_list++; + u32 vba = whimory_pack_vba(w, ce, cau, vblock, + vba_ofs / w->sftl.vbas_per_page, + vba_ofs % w->sftl.vbas_per_page); + w->sftl.btoc_unmap_entries++; w->sftl.btoc_holelist_ffff0001++; + if (btoc_apply_list) { + w->sftl.claim_source = 4; + if (whimory_sftl_apply_list(w, vba)) + dev_warn(w->dev, + "list token vba=%u failed\n", + vba); + else + w->sftl.token_list++; + w->sftl.claim_source = 1; + } } else if (lba == WHIMORY_LBA_HOLE) { w->sftl.btoc_token_ffff0000++; - w->sftl.token_hole++; + w->sftl.btoc_hole_entries++; } else if (lba == WHIMORY_LBA_DELETED) { w->sftl.btoc_token_ffffff00++; - w->sftl.token_hole++; + w->sftl.btoc_unmap_entries++; } else if (lba == WHIMORY_LBA_BLANK) { w->sftl.btoc_token_ffffffff++; - w->sftl.token_hole++; - } else - w->sftl.token_hole++; + w->sftl.btoc_hole_entries++; + } else { + w->sftl.btoc_unknown_entries++; + } + w->sftl.token_hole++; if (vba_ofs + span > WHIMORY_VBAS_PER_SB) break; vba_ofs += span; continue; } - if (span > WHIMORY_DATA_VBAS_PER_SB) - break; - if (lba >= 0x01000000u) + if (span > WHIMORY_DATA_VBAS_PER_SB || lba >= 0x01000000u) break; if (vba_ofs + span > WHIMORY_DATA_VBAS_PER_SB) break; - vba = whimory_pack_vba(w, ce, cau, vblock, - vba_ofs / w->sftl.vbas_per_page, - vba_ofs % w->sftl.vbas_per_page); - upd = whimory_l2v_update(w, lba, span, vba); - if (upd) - break; - w->sftl.btoc_l2v_updates++; + if (lba == 0) { + w->sftl.btoc_skipped_zero++; + vba_ofs += span; + continue; + } + for (s = 0; s < span; s++) { + unsigned int pg = (vba_ofs + s) / w->sftl.vbas_per_page; + int got; + + if (pg == last_pg) + continue; + last_pg = pg; + got = whimory_btoc_confirm_page(w, ce, cau, vblock, pg, + 0xffffffffu, false); + if (got < 0) + return hit > 0; + if (got > 0) + hit += got; + } vba_ofs += span; - hit++; - w->sftl.btoc_recs++; } return hit > 0; } +/* + * Decode a BTOC record stream into a per-VBA LBA table. Records are 16 bytes; + * `be` picks the big-endian form (LBA at +8 BE, span in the last byte) over + * the little-endian struct whimory_bte. Returns how many VBAs were described. + * Special/token LBAs and holes land as WHIMORY_LBA_BLANK. + */ +static unsigned int whimory_btoc_decode_map(const u8 *page, unsigned int len, + bool be, u32 *map) +{ + unsigned int i, recs = len / 16, vba_ofs = 0; + + for (i = 0; i < recs && vba_ofs < WHIMORY_DATA_VBAS_PER_SB; i++) { + const u8 *r = page + i * 16; + u32 lba = be ? get_unaligned_be32(r + 8) : + get_unaligned_le32(r + 8); + u32 span = be ? r[15] : get_unaligned_le32(r + 12); + unsigned int s; + + if (!span || span > WHIMORY_DATA_VBAS_PER_SB) + break; + if (vba_ofs + span > WHIMORY_DATA_VBAS_PER_SB) + break; + for (s = 0; s < span; s++) + map[vba_ofs + s] = whimory_special_lba(lba) ? + WHIMORY_LBA_BLANK : lba + s; + vba_ofs += span; + } + return vba_ofs; +} + +/* + * Sample a few pages of a closed superblock and report how often the BTOC + * prediction matches the per-slot metadata. High agreement means the replay + * can apply the BTOC directly and drop from 127 reads per SB to one. + */ +static void whimory_btoc_verify(struct whimory *w, struct whimory_sb *sb, + unsigned int vblock, const u8 *btoc, + unsigned int len) +{ + static const bool forms[2] = { false, true }; + u8 meta[S5L8740_NAND_META_SIZE]; + unsigned int f; + + if (!w->sftl.btoc_map || !w->sftl.data_page) + return; + + for (f = 0; f < ARRAY_SIZE(forms); f++) { + u32 *map = w->sftl.btoc_map; + unsigned int vbas, pages, step, pg, n = 0; + unsigned int agree = 0, disagree = 0, nodata = 0; + u32 first_hint = 0, first_meta = 0; + + memset(map, 0xff, WHIMORY_DATA_VBAS_PER_SB * sizeof(*map)); + vbas = whimory_btoc_decode_map(btoc, len, forms[f], map); + if (vbas < w->sftl.vbas_per_page) + continue; + pages = vbas / w->sftl.vbas_per_page; + step = pages / (btoc_verify_pages ? btoc_verify_pages : 1); + if (!step) + step = 1; + + for (pg = 0; pg < pages && n < btoc_verify_pages; pg += step) { + unsigned int slot; + + if (whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, + pg, w->sftl.data_page, + S5L8740_NAND_PAGE_SIZE, + meta, sizeof(meta))) + break; + n++; + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { + const u8 *m = meta + slot * WHIMORY_META_SIZE; + u32 hint = map[pg * w->sftl.vbas_per_page + slot]; + u32 mlba; + + if (!whimory_meta_is_data_raw(m) || + whimory_meta_erased(m, WHIMORY_META_SIZE)) { + nodata++; + continue; + } + mlba = get_unaligned_le32(m + 8); + if (hint == mlba) { + agree++; + } else { + if (!disagree) { + first_hint = hint; + first_meta = mlba; + } + disagree++; + } + } + } + dev_info(w->dev, + "BTOC_VERIFY ce=%u cau=%u vblk=%u form=%s vbas=%u " + "pages_probed=%u agree=%u disagree=%u nodata=%u " + "first_hint=%u first_meta=%u\n", + sb->ce, sb->cau, vblock, forms[f] ? "BE" : "LE", + vbas, n, agree, disagree, nodata, + first_hint, first_meta); + } +} + static int whimory_ingest_btoc_page(struct whimory *w, unsigned int ce, unsigned int cau, unsigned int vblock, const u8 *page, unsigned int len) @@ -2959,7 +3582,7 @@ static int whimory_ingest_btoc_page(struct whimory *w, unsigned int ce, verdict = "LE_BTE"; hit = 1; } - if (w->sftl.btoc_pages_read <= 8) + if (ftl_diag && w->sftl.btoc_pages_read <= 8) dev_info(w->dev, "BTOC_VERDICT ce=%u cau=%u vblock=%u %s first32=%32ph\n", ce, cau, vblock, verdict, page); @@ -2974,6 +3597,7 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) int ret, hits = 0; vblock = whimory_vfl_virt(w, sb->cau, sb->block); + w->sftl.claim_source = 2; for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; pg++) { ret = whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, pg, data, S5L8740_NAND_PAGE_SIZE, @@ -2986,6 +3610,8 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { const u8 *m = spare + slot * WHIMORY_META_SIZE; u32 lba, vba; + struct whimory_range *prev; + u64 weave; w->sftl.open_slots_seen++; if (m[0] != WHIMORY_META_TYPE_DATA && @@ -2997,25 +3623,61 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) continue; lba = get_unaligned_le32(m + 8); w->sftl.open_slots_valid_meta++; - if (whimory_special_lba(lba) || lba >= 0x01000000u) + if (whimory_special_lba(lba)) { + w->sftl.open_unmap_entries++; continue; - if (lba == 0 && w->sftl.open_l2v_updates < 8) - dev_info(w->dev, - "OPEN_META_SCAN lba=0 ce=%u cau=%u blk=%u page=%u slot=%u type=%02x flags=%02x first64=%32ph\n", - sb->ce, sb->cau, sb->block, pg, slot, - m[0], m[1], data + slot * WHIMORY_LBA_SIZE); + } + if (lba >= 0x01000000u) { + w->sftl.btoc_unknown_entries++; + continue; + } + if (lba == 0) { + w->sftl.open_skipped_zero++; + continue; + } vba = whimory_pack_vba(w, sb->ce, sb->cau, vblock, pg, slot); - w->sftl.claim_weave = whimory_weave48(m); + weave = whimory_weave48(m); + prev = whimory_range_find(&w->ranges, lba); + if (prev) { + if (weave > prev->weave) + w->sftl.open_overrides_closed++; + else if (weave < prev->weave) + w->sftl.open_rejected_stale++; + else + w->sftl.open_unknown_order++; + } + w->sftl.claim_weave = weave; + if ((l2v_trace_lba && lba == l2v_trace_lba) || + (audit_lba_winners && whimory_audit_fmss_lba(lba))) { + dev_info(w->dev, + "LBA_WINNER fmss_lba=%u candidate vba=%u " + "ce=%u cau=%u vblk=%u pg=%u slot=%u " + "weave=%012llx prev_vba=%u prev_weave=%012llx " + "source=open\n", + lba, vba, sb->ce, sb->cau, vblock, pg, + slot, (unsigned long long)weave, + prev ? prev->vba : ~0u, + prev ? (unsigned long long)prev->weave : + 0ull); + } if (whimory_l2v_update(w, lba, 1, vba)) { w->sftl.claim_weave = 0; + w->sftl.claim_source = 0; return -ENOMEM; } w->sftl.claim_weave = 0; w->sftl.open_l2v_updates++; hits++; } + if ((pg & 0x0f) == 0) { + cond_resched(); + if (recover_yield_us) + usleep_range(recover_yield_us, + recover_yield_us + 500); + } } + w->sftl.claim_source = 0; return hits; } @@ -3036,22 +3698,48 @@ static int whimory_sb_cmp(const void *a, const void *b) return 0; } -/*analogue: CXT SB VBAs are not L2V_Update'd. */ -static bool whimory_vba_is_cxt(struct whimory *w, u32 vba) -{ - u32 ce, cau, vblock, page, slot, phys, i; +/* + * Compact (ce, cau, block) list of the CXT superblocks found by classify. + * whimory_vba_is_cxt() is called once per CXT L2V record; scanning all + * num_sb entries there was O(num_sb) per record (7840 SBs on N31). + */ +static void whimory_cxt_index_build(struct whimory *w, unsigned int nsb) +{ + struct whimory_sftl *s = &w->sftl; + unsigned int i; + + s->n_cxt_idx = 0; + if (!s->sbs) + return; + for (i = 0; i < nsb && s->n_cxt_idx < ARRAY_SIZE(s->cxt_idx); i++) { + struct whimory_sb *sb = &s->sbs[i]; + + if (sb->kind != WHIMORY_SB_CXT) + continue; + s->cxt_idx[s->n_cxt_idx].ce = sb->ce; + s->cxt_idx[s->n_cxt_idx].cau = sb->cau; + s->cxt_idx[s->n_cxt_idx].block = sb->block; + s->n_cxt_idx++; + } +} + +/* + * VBAs belonging to a CXT superblock hold context records, not user data, + * so they must never enter the L2V map. + */ +static bool whimory_vba_is_cxt(struct whimory *w, u32 vba) +{ + u32 ce, cau, vblock, page, slot, phys; + unsigned int i; if (whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot)) return false; cau = whimory_vfl_bank(w, cau, vblock); phys = whimory_vfl_phys(w, cau, vblock); - if (!w->sftl.sbs) - return false; - for (i = 0; i < w->sftl.num_sb; i++) { - struct whimory_sb *sb = &w->sftl.sbs[i]; + for (i = 0; i < w->sftl.n_cxt_idx; i++) { + struct whimory_cxt_sb_id *c = &w->sftl.cxt_idx[i]; - if (sb->kind == WHIMORY_SB_CXT && sb->ce == ce && - sb->cau == cau && sb->block == phys) + if (c->ce == ce && c->cau == cau && c->block == phys) return true; } return false; @@ -3149,7 +3837,7 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) w->cxt_lba_valid = false; w->cxt_next_lba = 0; - /*: VFL_Read in chunks of sftl.gc.zoneSize into ED7C/ED80. */ + /* Read the superblock in gc_zone_size chunks, as the FTL does. */ for (ofs = 0; ofs < s->vbas_per_sb && !done; ofs += zone) { n = min(zone, s->vbas_per_sb - ofs); for (i = 0; i < n; i++) { @@ -3200,7 +3888,7 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) return 0; } -static int whimory_cxt_load(struct whimory *w) +static int __maybe_unused whimory_cxt_load(struct whimory *w) { unsigned int i; int ret, loaded = 0; @@ -3225,6 +3913,850 @@ static int whimory_cxt_load(struct whimory *w) return 0; } +/* ------------------------------------------------------------------ */ +/* CXT scanner / dumper (read-only; never touches the map) */ +/* ------------------------------------------------------------------ */ + + +/* + * Every superblock classify tagged as CXT, newest weave first. + * + * whimory_cxt_add_base() only registers a superblock whose page 0 slot 0 + * carries tag BASE, which on this device is one of the four CXT blocks. The + * BASE payload itself names the others ({count, sb, sb, ...}), and the newest + * generation need not be the one holding the BASE marker, so the candidate + * search has to consider all of them. + */ +static unsigned int whimory_cxt_collect_sbs(struct whimory *w, + struct whimory_cxt_base *out, + unsigned int max) +{ + struct whimory_sftl *s = &w->sftl; + unsigned int i, n = 0; + + if (!s->sbs) + return 0; + for (i = 0; i < s->num_sb && n < max; i++) { + struct whimory_sb *sb = &s->sbs[i]; + u32 vblock, idx, j; + + if (sb->kind != WHIMORY_SB_CXT) + continue; + vblock = whimory_vfl_virt(w, sb->cau, sb->block); + idx = whimory_sb_index(w, sb->ce, sb->cau, vblock); + for (j = 0; j < n; j++) + if (out[j].sb == idx) + break; + if (j < n) + continue; + out[n].sb = idx; + out[n].weave = sb->weave; + n++; + } + /* Insertion sort, newest weave first; n is at most WHIMORY_CXT_MAX_SB. */ + for (i = 1; i < n; i++) { + struct whimory_cxt_base tmp = out[i]; + int j = (int)i - 1; + + while (j >= 0 && out[j].weave < tmp.weave) { + out[j + 1] = out[j]; + j--; + } + out[j + 1] = tmp; + } + return n; +} + +static const char *whimory_cxt_tag_name(u8 tag) +{ + switch (tag) { + case WHIMORY_CXT_TAG_BASE: + return "BASE"; + case WHIMORY_CXT_TAG_STATS: + return "STATS"; + case WHIMORY_CXT_TAG_SB: + return "SB"; + case WHIMORY_CXT_TAG_L2V: + return "TREE"; + case WHIMORY_CXT_TAG_USERSEQ: + return "USERSEQ"; + case WHIMORY_CXT_TAG_READS: + return "READS"; + case WHIMORY_CXT_TAG_CLEAN: + return "CLEAN/END"; + default: + return "?"; + } +} + +/* + * Read one VBA of a CXT superblock. Returns the 4 KiB payload in `data` and + * the 16-byte record metadata in `meta`. Page reads are cached across the + * four slots of a physical page by the caller. + */ +static int whimory_cxt_read_vba(struct whimory *w, u32 sb_idx, u32 ofs, + u8 *data, u8 *meta, u8 *spare, u32 *last_key) +{ + struct whimory_sftl *s = &w->sftl; + u32 ce, cau, vblock, page, slot, pblock, key; + u32 vba = s_g_addr_to_vba(w, sb_idx, ofs); + int ret; + + ret = whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot); + if (ret) + return ret; + cau = whimory_vfl_bank(w, cau, vblock); + pblock = whimory_vfl_phys(w, cau, vblock); + key = ((ce & 0xf) << 28) | ((cau & 0xf) << 24) | + ((pblock & 0xffff) << 8) | (page & 0xff); + if (key != *last_key) { + ret = whimory_cs_read_page(w, ce, cau, pblock, page, + s->data_page, + S5L8740_NAND_PAGE_SIZE, + spare, S5L8740_NAND_META_SIZE); + if (ret) + return ret; + *last_key = key; + } + memcpy(data, s->data_page + slot * WHIMORY_LBA_SIZE, WHIMORY_LBA_SIZE); + memcpy(meta, spare + slot * WHIMORY_META_SIZE, WHIMORY_META_SIZE); + return 0; +} + +/* + * Walk one CXT superblock and report what is actually stored in it: the tag + * of every record, the shape of the BASE header (which carries the CXT + * superblock list), and the header of each TREE record. + */ +static void whimory_cxt_dump_sb(struct whimory *w, u32 sb_idx, u64 weave, + unsigned int max_vbas) +{ + struct whimory_sftl *s = &w->sftl; + u32 counts[8] = {0}, clean = 0, other = 0, end_at = ~0u; + u32 ofs, last_key = ~0u, trees = 0, base_seen = 0; + u8 *data = s->gc_data; + u8 meta[WHIMORY_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; + + if (!data || !s->data_page) { + dev_err(w->dev, "CXT_DUMP sb=%u no scratch\n", sb_idx); + return; + } + if (!max_vbas || max_vbas > s->vbas_per_sb) + max_vbas = s->vbas_per_sb; + + dev_info(w->dev, "CXT_DUMP_BEGIN sb=%u weave=%llu vbas=%u\n", + sb_idx, (unsigned long long)weave, max_vbas); + + for (ofs = 0; ofs < max_vbas; ofs++) { + u8 type, tag; + + if (whimory_cxt_read_vba(w, sb_idx, ofs, data, meta, spare, + &last_key)) + break; + type = meta[0]; + tag = meta[1]; + if (whimory_meta_erased(meta, WHIMORY_META_SIZE)) { + clean++; + continue; + } + if (type != WHIMORY_META_TYPE_SFTL_CXT) { + other++; + continue; + } + if (tag == WHIMORY_CXT_TAG_CLEAN) { + clean++; + if (end_at == ~0u) + end_at = ofs; + break; + } + if (tag < ARRAY_SIZE(counts)) + counts[tag]++; + else + other++; + + /* One compact row per distinct tag, so the record layout of + * a superblock is visible without a full hex dump. + */ + if (tag < ARRAY_SIZE(counts) && counts[tag] == 1) + dev_info(w->dev, + "CXT_REC sb=%u ofs=%u pg=%u slot=%u type=%02x tag=%02x %s\n", + sb_idx, ofs, ofs / s->vbas_per_page, + ofs % s->vbas_per_page, type, tag, + whimory_cxt_tag_name(tag)); + + if (tag == WHIMORY_CXT_TAG_BASE && base_seen++ < 2) { + dev_info(w->dev, + "CXT_BASE_REC sb=%u ofs=%u meta=%16ph\n", + sb_idx, ofs, meta); + dev_info(w->dev, + "CXT_BASE_REC sb=%u ofs=%u first64=%32ph %32ph\n", + sb_idx, ofs, data, data + 32); + } + if (tag == WHIMORY_CXT_TAG_L2V && trees < 1) { + unsigned int b; + + for (b = 0; b < 128; b += 32) + dev_info(w->dev, + "CXT_TREE_HEX sb=%u ofs=%u +%03u %32ph\n", + sb_idx, ofs, b, data + b); + } + if (tag == WHIMORY_CXT_TAG_L2V && trees++ < 3) { + dev_info(w->dev, + "CXT_TREE_REC sb=%u ofs=%u hdr_lba=%u " + "hdr_span=0x%08x p0=(%u,%u) p1=(%u,%u) " + "p2=(%u,%u)\n", + sb_idx, ofs, + get_unaligned_le32(data), + get_unaligned_le32(data + 4), + get_unaligned_le32(data + 8), + get_unaligned_le32(data + 12), + get_unaligned_le32(data + 16), + get_unaligned_le32(data + 20), + get_unaligned_le32(data + 24), + get_unaligned_le32(data + 28)); + } + } + + dev_info(w->dev, + "CXT_DUMP_END sb=%u scanned=%u clean=%u other=%u end_at=%d " + "base=%u stats=%u sbrec=%u tree=%u userseq=%u reads=%u\n", + sb_idx, ofs, clean, other, (int)end_at, + counts[WHIMORY_CXT_TAG_BASE], counts[WHIMORY_CXT_TAG_STATS], + counts[WHIMORY_CXT_TAG_SB], counts[WHIMORY_CXT_TAG_L2V], + counts[WHIMORY_CXT_TAG_USERSEQ], + counts[WHIMORY_CXT_TAG_READS]); +} + +/* + * Phase 2 entry point: report every CXT base candidate newest-weave first, + * then dump what each one contains. Read-only — the L2V map is untouched. + */ +int whimory_cxt_dump(unsigned int max_vbas) +{ + struct whimory *w = whimory_dev; + unsigned int i; + struct whimory_cxt_base all[WHIMORY_CXT_MAX_SB]; + unsigned int n_all; + int sess; + + if (!w) + return -ENODEV; + if (!w->sftl.sbs || !w->sftl.num_sb) + return -ENODATA; + + dev_info(w->dev, + "CXT_SCAN bases=%u cxt_sbs=%u classified_sbs=%u " + "cxt_loaded=%d base_weave=%llu\n", + w->n_cxt, w->sftl.cxt_sbs, w->sftl.num_sb, w->sftl.cxt_loaded, + (unsigned long long)w->cxt_base_weave); + + for (i = 0; i < w->n_cxt; i++) + dev_info(w->dev, "CXT_CAND i=%u sb=%u weave=%llu\n", + i, w->cxt[i].sb, + (unsigned long long)w->cxt[i].weave); + + n_all = whimory_cxt_collect_sbs(w, all, ARRAY_SIZE(all)); + for (i = 0; i < n_all; i++) + dev_info(w->dev, "CXT_SB i=%u sb=%u weave=%llu\n", + i, all[i].sb, (unsigned long long)all[i].weave); + + if (!w->n_cxt) { + dev_warn(w->dev, + "CXT_SCAN no base candidates; classify must reach the " + "high blocks that hold them (scan_blocks=0/1960)\n"); + return -ENOENT; + } + + sess = s5l8740_nand_dma_session_begin(); + for (i = 0; i < n_all; i++) + whimory_cxt_dump_sb(w, all[i].sb, all[i].weave, max_vbas); + if (sess == 0) + s5l8740_nand_dma_session_end(); + return 0; +} +EXPORT_SYMBOL_GPL(whimory_cxt_dump); + +/* ------------------------------------------------------------------ */ +/* Phase 3: CXT TREE -> candidate map, compared against the live map */ +/* ------------------------------------------------------------------ */ + +static unsigned int cxt_max_extents = 1048576; +module_param(cxt_max_extents, uint, 0644); +MODULE_PARM_DESC(cxt_max_extents, + "Candidate-map extent ceiling for the CXT TREE parser"); + +/* + * CXT VBAs are in the FTL native superblock space, which is not the space + * whimory_pack_vba() builds. Apple counts one superblock as the same virtual + * block across every (ce, cau) plane, so a superblock holds + * pages_per_sb * planes * vbas_per_page VBAs and the plane index sits + * between the page and the slot: + * + * vba = vblock * (pages_per_sb * planes * 4) + * + page * (planes * 4) + plane * 4 + slot + * + * whimory_pack_vba() instead gives every (ce, cau, vblock) triple its own + * superblock index, so a raw CXT VBA lands on an unrelated page here. Two + * consequences: translate before use, and a run of consecutive CXT VBAs is + * only contiguous in our space within one 4-slot group, because the next + * group belongs to a different plane. + */ +static int whimory_cxt_vba_translate(struct whimory *w, u32 cxt_vba, u32 *out) +{ + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 per_page, per_sb, vblock, rem, page, plane, slot; + + if (!planes || !w->sftl.vbas_per_page || !w->sftl.pages_per_sb) + return -EINVAL; + per_page = planes * w->sftl.vbas_per_page; + per_sb = w->sftl.pages_per_sb * per_page; + vblock = cxt_vba / per_sb; + rem = cxt_vba % per_sb; + page = rem / per_page; + plane = (rem % per_page) / w->sftl.vbas_per_page; + slot = rem % w->sftl.vbas_per_page; + if (vblock >= w->sftl.user_blocks || page >= w->sftl.pages_per_sb) + return -ERANGE; + *out = whimory_pack_vba(w, plane / w->geom.num_cau, + plane % w->geom.num_cau, vblock, page, slot); + return 0; +} + +static void whimory_cxt_ext_reset(struct whimory *w) +{ + w->n_cxt_ext = 0; + w->cxt_ext_weave = 0; + w->cxt_ext_sb = 0; +} + +static int whimory_cxt_ext_add(struct whimory *w, u32 lba, u32 span, u32 vba) +{ + struct whimory_cxt_extent *e; + + if (w->n_cxt_ext >= w->max_cxt_ext) + return -ENOSPC; + e = &w->cxt_ext[w->n_cxt_ext++]; + e->lba = lba; + e->span = span; + e->vba = vba; + return 0; +} + +/* + * A TREE record is {start_lba, CONTIG_SPAN} followed by (vba, span) pairs, + * each pair advancing the logical cursor by span. Same shape as + * whimory_cxt_load_contig(), but it appends to the candidate map instead of + * touching L2V. + */ +static int whimory_cxt_parse_tree(struct whimory *w, const u8 *data, + unsigned int len, u32 *next_lba, + bool *lba_valid) +{ + u32 lba, span, vba, i, n = len / 8; + + if (len < 16) + return 0; + lba = get_unaligned_le32(data); + span = get_unaligned_le32(data + 4); + if (span == 0xffffffffu) + return 0; + if (span != WHIMORY_CXT_CONTIG_SPAN) + return -EINVAL; + if (*lba_valid && lba != *next_lba) { + dev_warn(w->dev, + "CXT_TREE lba discontinuity want=%u got=%u\n", + *next_lba, lba); + return -EINVAL; + } + *lba_valid = true; + + for (i = 1; i < n; i++) { + vba = get_unaligned_le32(data + 8 * i); + span = get_unaligned_le32(data + 8 * i + 4); + if (vba == 0xffffffffu || !span) + break; + w->sftl.cxt_records_seen++; + if (vba >= WHIMORY_CXT_VBA_HOLE || vba >= w->l2v.invalid_vba) { + /* Hole: consumes logical space, maps nothing. */ + w->sftl.cxt_hole_entries++; + lba += span; + continue; + } + while (span) { + u32 chunk = w->sftl.vbas_per_page - + (vba % w->sftl.vbas_per_page); + u32 tvba; + int ret; + + if (chunk > span) + chunk = span; + if (!whimory_cxt_vba_translate(w, vba, &tvba)) { + ret = whimory_cxt_ext_add(w, lba, chunk, tvba); + if (ret) + return ret; + } else { + w->sftl.cxt_xlate_fail++; + } + lba += chunk; + vba += chunk; + span -= chunk; + } + continue; + } + *next_lba = lba; + return 0; +} + +/* Walk the records of one CXT superblock and collect every TREE extent. */ +static int whimory_cxt_build_from_sb(struct whimory *w, u32 sb_idx) +{ + struct whimory_sftl *s = &w->sftl; + u8 *data = s->gc_data; + u8 meta[WHIMORY_META_SIZE]; + u8 spare[S5L8740_NAND_META_SIZE]; + u32 ofs, last_key = ~0u, next_lba = 0; + bool lba_valid = false; + int ret; + + if (!data || !s->data_page) + return -ENOMEM; + + for (ofs = 0; ofs < s->vbas_per_sb; ofs++) { + ret = whimory_cxt_read_vba(w, sb_idx, ofs, data, meta, spare, + &last_key); + if (ret) + return ret; + if (meta[0] != WHIMORY_META_TYPE_SFTL_CXT) + continue; + if (meta[1] == WHIMORY_CXT_TAG_CLEAN) + break; + if (meta[1] != WHIMORY_CXT_TAG_L2V) + continue; + ret = whimory_cxt_parse_tree(w, data, WHIMORY_LBA_SIZE, + &next_lba, &lba_valid); + if (ret) + return ret; + } + return 0; +} + +/* + * Build the candidate map from the newest CXT base that parses cleanly. + * Never touches w->ranges. + */ +static int whimory_cxt_ext_cmp(const void *a, const void *b) +{ + const struct whimory_cxt_extent *x = a, *y = b; + + if (x->lba != y->lba) + return x->lba < y->lba ? -1 : 1; + return 0; +} + +/* + * Build the candidate map from every CXT superblock. + * + * The TREE is partitioned by logical range across the CXT blocks — on this + * device sb 1702 starts at LBA 0 and sb 3662 picks up at 613939 — so taking + * the first superblock that parses leaves most of the volume, including the + * FAT-critical sectors, unmapped. Merge them all, then sort by LBA so the + * lookup can binary search. Never touches w->ranges. + */ +static int whimory_cxt_build_candidate(struct whimory *w) +{ + struct whimory_cxt_base all[WHIMORY_CXT_MAX_SB]; + unsigned int i, n_all, ok = 0, overlaps = 0; + int ret, sess; + + /* + * Allocated on first use: translation splits every CXT run at 4-slot + * plane boundaries, so the table is roughly one entry per four LBAs + * and is only worth its megabytes when the CXT path is exercised. + */ + if (!w->cxt_ext) { + w->max_cxt_ext = cxt_max_extents; + w->cxt_ext = kvmalloc_array(w->max_cxt_ext, + sizeof(*w->cxt_ext), GFP_KERNEL); + if (!w->cxt_ext) { + w->max_cxt_ext = 0; + return -ENOMEM; + } + } + n_all = whimory_cxt_collect_sbs(w, all, ARRAY_SIZE(all)); + if (!n_all) + return -ENOENT; + + whimory_cxt_ext_reset(w); + w->sftl.cxt_records_seen = 0; + w->sftl.cxt_hole_entries = 0; + w->sftl.cxt_xlate_fail = 0; + + sess = s5l8740_nand_dma_session_begin(); + for (i = 0; i < n_all; i++) { + u32 before = w->n_cxt_ext; + + ret = whimory_cxt_build_from_sb(w, all[i].sb); + if (ret) { + dev_warn(w->dev, + "CXT_CAND_MAP sb=%u parse failed %d\n", + all[i].sb, ret); + continue; + } + if (w->n_cxt_ext == before) + continue; + ok++; + if (all[i].weave > w->cxt_ext_weave) { + w->cxt_ext_weave = all[i].weave; + w->cxt_ext_sb = all[i].sb; + } + dev_info(w->dev, + "CXT_CAND_MAP sb=%u weave=%llu extents=+%u total=%u\n", + all[i].sb, (unsigned long long)all[i].weave, + w->n_cxt_ext - before, w->n_cxt_ext); + } + if (sess == 0) + s5l8740_nand_dma_session_end(); + + if (!w->n_cxt_ext) + return -ENODATA; + + sort(w->cxt_ext, w->n_cxt_ext, sizeof(*w->cxt_ext), + whimory_cxt_ext_cmp, NULL); + for (i = 1; i < w->n_cxt_ext; i++) + if (w->cxt_ext[i].lba < + w->cxt_ext[i - 1].lba + w->cxt_ext[i - 1].span) + overlaps++; + + dev_info(w->dev, + "CXT_MAP sbs_used=%u/%u extents=%u records=%u holes=%u " + "xlate_fail=%u overlaps=%u base_weave=%llu\n", + ok, n_all, w->n_cxt_ext, w->sftl.cxt_records_seen, + w->sftl.cxt_hole_entries, w->sftl.cxt_xlate_fail, overlaps, + (unsigned long long)w->cxt_ext_weave); + return 0; +} + +/* + * Compare the candidate map against the brute-force interval map, which is + * ground truth because it comes from per-slot metadata. Any systematic + * decode error shows up as a repeated (vba_cxt - vba_brute) delta. + */ +#define WHIMORY_CXT_DELTA_SLOTS 8 + +static void whimory_cxt_compare(struct whimory *w) +{ + s64 delta_val[WHIMORY_CXT_DELTA_SLOTS] = {0}; + u32 delta_cnt[WHIMORY_CXT_DELTA_SLOTS] = {0}; + u32 agree = 0, disagree = 0, absent = 0, checked = 0; + u32 i, j, shown = 0; + u64 covered = 0; + + for (i = 0; i < w->n_cxt_ext; i++) { + struct whimory_cxt_extent *e = &w->cxt_ext[i]; + u32 probes[3], np = 0, k; + + covered += e->span; + probes[np++] = e->lba; + if (e->span > 2) + probes[np++] = e->lba + e->span / 2; + if (e->span > 1) + probes[np++] = e->lba + e->span - 1; + + for (k = 0; k < np; k++) { + u32 lba = probes[k]; + u32 want = e->vba + (lba - e->lba); + struct whimory_range *r; + s64 d; + + checked++; + r = whimory_range_find(&w->ranges, lba); + if (!r) { + absent++; + continue; + } + d = (s64)want - (s64)(r->vba + (lba - r->start)); + if (!d) { + agree++; + continue; + } + disagree++; + if (shown++ < diag_max_lines) { + u32 bv = r->vba + (lba - r->start); + u32 per = w->sftl.vbas_per_sb; + u32 dper = WHIMORY_DATA_VBAS_PER_SB; + + /* + * Decompose both VBAs in the 512-per-SB space + * we pack into and in the 508-per-SB data-only + * space, so a units mismatch is visible rather + * than inferred. + */ + dev_info(w->dev, + "CXT_CMP lba=%u cxt_vba=%u brute_vba=%u delta=%lld\n", + lba, want, bv, (long long)d); + dev_info(w->dev, + " cxt sb512=%u ofs512=%u sb508=%u ofs508=%u\n", + want / per, want % per, + want / dper, want % dper); + dev_info(w->dev, + " brute sb512=%u ofs512=%u sb508=%u ofs508=%u\n", + bv / per, bv % per, + bv / dper, bv % dper); + } + for (j = 0; j < WHIMORY_CXT_DELTA_SLOTS; j++) { + if (delta_cnt[j] && delta_val[j] != d) + continue; + delta_val[j] = d; + delta_cnt[j]++; + break; + } + } + } + + dev_info(w->dev, + "CXT_COMPARE extents=%u covered_lbas=%llu checked=%u " + "agree=%u disagree=%u absent_in_brute=%u\n", + w->n_cxt_ext, covered, checked, agree, disagree, absent); + for (j = 0; j < WHIMORY_CXT_DELTA_SLOTS; j++) { + if (!delta_cnt[j]) + continue; + dev_info(w->dev, "CXT_DELTA %lld x%u\n", + (long long)delta_val[j], delta_cnt[j]); + } +} + +/* Resolve an LBA through the candidate map only. */ +static int whimory_cxt_lookup(struct whimory *w, u32 lba, u32 *vba_out) +{ + u32 lo = 0, hi = w->n_cxt_ext; + + while (lo < hi) { + u32 mid = lo + (hi - lo) / 2; + struct whimory_cxt_extent *e = &w->cxt_ext[mid]; + + if (lba < e->lba) + hi = mid; + else if (lba >= e->lba + e->span) + lo = mid + 1; + else { + *vba_out = e->vba + (lba - e->lba); + return 0; + } + } + return -ENOENT; +} + +/* + * Read the BPB and the FAT-critical sectors through the candidate map and + * check that each landed on a page whose metadata claims the LBA we asked + * for. This is the gate that decides whether the CXT map is usable. + */ +static int whimory_cxt_validate(struct whimory *w, u32 fat_base) +{ + static const u32 rel[] = { 0, 1, 2, 6, 7, 8 }; + struct whimory_meta meta; + u8 *buf; + unsigned int i; + u32 ok = 0, bad = 0, miss = 0; + int ret, sess; + + if (!w->n_cxt_ext) + return -ENODATA; + buf = kmalloc(WHIMORY_LBA_SIZE, GFP_KERNEL); + if (!buf) + return -ENOMEM; + + sess = s5l8740_nand_dma_session_begin(); + for (i = 0; i < ARRAY_SIZE(rel); i++) { + u32 lba = fat_base + rel[i]; + u32 vba = 0, mlba; + + ret = whimory_cxt_lookup(w, lba, &vba); + if (ret) { + miss++; + dev_info(w->dev, "CXT_VALID lba=%u UNMAPPED\n", lba); + continue; + } + ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); + if (ret) { + bad++; + dev_info(w->dev, "CXT_VALID lba=%u vba=%u read %d\n", + lba, vba, ret); + continue; + } + mlba = le32_to_cpu(meta.lba); + if (mlba == lba) { + ok++; + } else { + bad++; + dev_info(w->dev, + "CXT_VALID lba=%u vba=%u meta_lba=%u MISMATCH\n", + lba, vba, mlba); + } + } + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + + dev_info(w->dev, + "CXT_VALIDATE fat_base=%u ok=%u bad=%u unmapped=%u verdict=%s\n", + fat_base, ok, bad, miss, + (ok == ARRAY_SIZE(rel)) ? "USABLE" : "NOT_USABLE"); + return (ok == ARRAY_SIZE(rel)) ? 0 : -EBADMSG; +} + + +/* + * Sample extents across the candidate map, read the VBA the CXT claims, and + * print what the page metadata actually says. If the decode is merely stale + * the page still claims the LBA we asked for; if it is wrong, meta_lba is + * unrelated and the (expected, actual) pairs expose the transform. + */ +static void whimory_cxt_probe(struct whimory *w, unsigned int nsamples) +{ + struct whimory_meta meta; + u8 *buf; + u32 step, i, ok = 0, stale = 0, wrong = 0, blank = 0, zero = 0; + int ret, sess; + + if (!w->n_cxt_ext || !nsamples) + return; + buf = kmalloc(WHIMORY_LBA_SIZE, GFP_KERNEL); + if (!buf) + return; + + step = w->n_cxt_ext / nsamples; + if (!step) + step = 1; + + sess = s5l8740_nand_dma_session_begin(); + for (i = 0; i < w->n_cxt_ext; i += step) { + struct whimory_cxt_extent *e = &w->cxt_ext[i]; + u32 mlba; + + ret = w->vfl_ops->read_vba(w, e->vba, 1, buf, &meta); + if (ret) { + wrong++; + continue; + } + mlba = le32_to_cpu(meta.lba); + if (mlba == e->lba) { + ok++; + continue; + } + if (mlba == 0xffffffffu) + blank++; + else if (!mlba) + zero++; + else + wrong++; + if (stale++ < 12) + dev_info(w->dev, + "CXT_PROBE ext=%u lba=%u span=%u vba=%u " + "meta_type=%02x meta_lba=%u diff=%d\n", + i, e->lba, e->span, e->vba, meta.type, mlba, + (int)(mlba - e->lba)); + } + if (sess == 0) + s5l8740_nand_dma_session_end(); + kfree(buf); + + dev_info(w->dev, + "CXT_PROBE_SUM sampled=%u ok=%u wrong=%u blank=%u zero_lba=%u\n", + (w->n_cxt_ext + step - 1) / step, ok, wrong, blank, zero); +} + +/* + * Seed the interval map from the CXT snapshot. + * + * Every extent is claimed at the CXT base weave, so the normal winner rules + * apply unchanged: anything the diff replay finds with a newer weave + * overrides it, and anything older is rejected as stale. + */ +static int whimory_cxt_seed_l2v(struct whimory *w) +{ + u32 i, seeded = 0; + int ret; + + w->sftl.claim_source = 3; + for (i = 0; i < w->n_cxt_ext; i++) { + struct whimory_cxt_extent *e = &w->cxt_ext[i]; + + w->sftl.claim_weave = w->cxt_ext_weave; + ret = whimory_l2v_update(w, e->lba, e->span, e->vba); + w->sftl.claim_weave = 0; + if (ret) { + w->sftl.claim_source = 0; + return ret; + } + seeded += e->span; + w->sftl.cxt_l2v_updates++; + if ((i & 0x3fff) == 0) + cond_resched(); + } + w->sftl.claim_source = 0; + dev_info(w->dev, + "CXT_SEED extents=%u lbas=%u ranges=%u base_weave=%llu\n", + w->n_cxt_ext, seeded, w->sftl.range_nodes, + (unsigned long long)w->cxt_ext_weave); + return 0; +} + +/* + * Fast path: seed from the CXT, then let the caller replay only the + * superblocks newer than the checkpoint. Returns 0 when the map is seeded. + */ +static int whimory_cxt_fast_load(struct whimory *w) +{ + int ret; + + ret = whimory_cxt_build_candidate(w); + if (ret) { + dev_warn(w->dev, + "CXT fast path unavailable (%d); full replay\n", + ret); + return ret; + } + ret = whimory_cxt_seed_l2v(w); + if (ret) + return ret; + w->cxt_base_weave = w->cxt_ext_weave; + w->sftl.cxt_loaded = true; + /* + * The interval map now holds everything the extent table did, and the + * table is ~12 MiB on a 55 MiB device. Drop it; the Phase 3 tools + * reallocate it on demand. + */ + kvfree(w->cxt_ext); + w->cxt_ext = NULL; + w->max_cxt_ext = 0; + w->n_cxt_ext = 0; + return 0; +} + +/* Phase 3 entry point: build the candidate map, compare it, validate it. */ +int whimory_cxt_candidate(u32 fat_base) +{ + struct whimory *w = whimory_dev; + int ret; + + if (!w) + return -ENODEV; + ret = whimory_cxt_build_candidate(w); + if (ret) { + dev_warn(w->dev, "CXT candidate build failed %d\n", ret); + return ret; + } + whimory_cxt_compare(w); + whimory_cxt_probe(w, 24); + if (fat_base) + whimory_cxt_validate(w, fat_base); + return 0; +} +EXPORT_SYMBOL_GPL(whimory_cxt_candidate); + + static void whimory_note_meta0(struct whimory *w, unsigned int ce, unsigned int cau, unsigned int block, unsigned int page, const u8 *data, const u8 *meta) @@ -3246,7 +4778,7 @@ static void whimory_note_meta0(struct whimory *w, unsigned int ce, m[0] != WHIMORY_META_TYPE_DATA2) continue; w->sftl.meta0_hits++; - if (w->sftl.meta0_hits > 24) + if (!ftl_diag || w->sftl.meta0_hits > 24) continue; vblock = whimory_vfl_virt(w, cau, block); vba = whimory_pack_vba(w, ce, cau, vblock, page, slot); @@ -3278,13 +4810,23 @@ static void whimory_print_recovery_stats(struct whimory *w) dev_info(w->dev, "RECOVERY_STATS:\n" + " scan_blocks=%u (param) user_blocks=%u\n" " fpart_sig=%u vfl_ctx_hits=%u vfl_cxt_loc=%u vfl_bitmap=%u\n" " classified_empty=%u classified_closed=%u classified_open=%u classified_cxt=%u classified_unknown=%u\n" " cxt_blocks_seen=%u cxt_records_seen=%u cxt_l2v_updates=%u\n" " btoc_pages_read=%u btoc_pages_valid=%u btoc_entries_seen=%u btoc_l2v_updates=%u\n" + " btoc_meta_confirmed=%u btoc_meta_mismatch=%u btoc_skipped_zero=%u\n" + " btoc_confirm_pages=%u btoc_confirm_capped=%u btoc_confirm_budget_stop=%u\n" + " btoc_unmap_entries=%u btoc_hole_entries=%u btoc_unknown_entries=%u\n" " btoc_token_ffff0000=%u btoc_token_ffffff00=%u btoc_token_ffffffff=%u btoc_holelist_ffff0001=%u\n" " open_slots_seen=%u open_slots_valid_meta=%u open_l2v_updates=%u\n" - " l2v_update_calls=%u l2v_unmap_calls=%u l2v_repack_roots=%u mapped_lbas=%u mapped_roots=%u meta0_hits=%u\n", + " open_unmap_entries=%u open_skipped_zero=%u open_overrides_closed=%u " + "open_rejected_stale=%u open_unknown_order=%u\n" + " mapped_lbas=%u mapped_ranges=%u mapped_roots=%u range_budget_stop=%u\n" + " string_hits itunesdb=%u f00=%u ipod_control=%u music=%u apps=%u mp3=%u m4a=%u\n" + " l2v_update_calls=%u l2v_unmap_calls=%u stale_mapping_rejected=%u " + "l2v_repack_roots=%u meta0_hits=%u\n", + scan_blocks, s->user_blocks, w->sig_ok, w->vfl.ctx_hits, w->vfl.cxt_loc_count, w->vfl.bitmap_loaded, s->empty_sbs, s->btoc_sbs, s->open_sbs, s->cxt_sbs, @@ -3292,12 +4834,27 @@ static void whimory_print_recovery_stats(struct whimory *w) s->cxt_blocks_seen, s->cxt_records_seen, s->cxt_l2v_updates, s->btoc_pages_read, s->btoc_pages_valid, s->btoc_entries_seen, s->btoc_l2v_updates, + s->btoc_meta_confirmed, s->btoc_meta_mismatch, + s->btoc_skipped_zero, + s->btoc_confirm_pages, s->btoc_confirm_capped, + s->btoc_confirm_budget_stop, + s->btoc_unmap_entries, s->btoc_hole_entries, + s->btoc_unknown_entries, s->btoc_token_ffff0000, s->btoc_token_ffffff00, s->btoc_token_ffffffff, s->btoc_holelist_ffff0001, s->open_slots_seen, s->open_slots_valid_meta, s->open_l2v_updates, - s->l2v_update_calls, s->l2v_unmap_calls, s->l2v_repack_roots, - s->mapped_lbas, s->mapped_roots, s->meta0_hits); + s->open_unmap_entries, s->open_skipped_zero, + s->open_overrides_closed, s->open_rejected_stale, + s->open_unknown_order, + s->mapped_lbas, s->range_nodes, s->mapped_roots, + s->range_budget_stop, + s->string_hit_itunesdb, s->string_hit_f00, + s->string_hit_ipod_control, s->string_hit_music, + s->string_hit_apps, s->string_hit_mp3, s->string_hit_m4a, + s->l2v_update_calls, s->l2v_unmap_calls, + s->stale_mapping_rejected, + s->l2v_repack_roots, s->meta0_hits); } static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) @@ -3335,32 +4892,127 @@ static void whimory_scan_closed_meta0(struct whimory *w, unsigned int nsb) } } -static void whimory_dump_vba_page(struct whimory *w, u32 vba) +/* + * Sibling slots on a page need not be contiguous LBAs. Only the selected + * map_slot is judged against requested fmss_lba. Sibling dump is VBA_DIAG. + */ +static bool whimory_audit_fmss_lba(u32 fmss_lba) +{ + /* Known BPB / critical fmss candidates + FAT-relative later. */ + switch (fmss_lba) { + case 49216u: + case 49279u: + case 49280u: + case 49285u: + case 49286u: + case 49311u: + case 49317u: /* FAT0 disk 32 @ base 49285 */ + case 51201u: /* root @ base 49285 */ + return true; + default: + if (fmss_lba >= 49279u && fmss_lba < 49279u + 2048u) + return true; + return false; + } +} + +static bool payload_string_scan; +module_param(payload_string_scan, bool, 0644); +MODULE_PARM_DESC(payload_string_scan, + "Scan confirmed pages for iTunesDB/F00/mp3 strings (default N)"); + +static void whimory_note_payload_strings(struct whimory *w, const u8 *data, + unsigned int len) { - u32 ce, cau, vblock, page, slot, pblock; + if (!payload_string_scan || !data || len < 8) + return; + if (memchr(data, 'i', len) && + strnstr((const char *)data, "iTunesDB", len)) + w->sftl.string_hit_itunesdb++; + if (strnstr((const char *)data, "F00", len) || + strnstr((const char *)data, "F01", len) || + strnstr((const char *)data, "F02", len)) + w->sftl.string_hit_f00++; + if (strnstr((const char *)data, "iPod_Control", len)) + w->sftl.string_hit_ipod_control++; + if (strnstr((const char *)data, "Music", len)) + w->sftl.string_hit_music++; + if (strnstr((const char *)data, "NanoApps", len) || + strnstr((const char *)data, "Apps", len)) + w->sftl.string_hit_apps++; + if (strnstr((const char *)data, ".mp3", len) || + strnstr((const char *)data, ".MP3", len) || + strnstr((const char *)data, "mp3", len)) + w->sftl.string_hit_mp3++; + if (strnstr((const char *)data, ".m4a", len) || + strnstr((const char *)data, ".M4A", len) || + strnstr((const char *)data, "m4a", len)) + w->sftl.string_hit_m4a++; +} + +static void whimory_dump_vba_page(struct whimory *w, u32 vba, u32 fmss_lba) +{ + u32 ce, cau, vblock, page, map_slot, pblock, slot; u8 spare[S5L8740_NAND_META_SIZE]; u8 *data = w->sftl.data_page; + const u8 *sel_m; + u32 sel_meta_lba; + u64 sel_weave; + bool sel_type_ok, sel_lba_ok, bad; int ret; if (!data) return; - if (whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot)) { - dev_warn(w->dev, "BAD_VBA unpack failed vba=%u\n", vba); + if (whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &map_slot)) { + dev_warn(w->dev, + "BAD_VBA unpack failed fmss_lba=%u vba=%u\n", + fmss_lba, vba); return; } cau = whimory_vfl_bank(w, cau, vblock); pblock = whimory_vfl_phys(w, cau, vblock); - dev_info(w->dev, - "BAD_VBA vba=%u sb=%u ofs=%u -> ce=%u cau=%u vblock=%u pbn=%u page=%u map_slot=%u\n", - vba, s_g_vba_to_sb(w, vba), s_g_vba_to_ofs(w, vba), - ce, cau, vblock, pblock, page, slot); ret = whimory_cs_read_page(w, ce, cau, pblock, page, data, S5L8740_NAND_PAGE_SIZE, spare, sizeof(spare)); if (ret) { - dev_warn(w->dev, "BAD_VBA page read %d\n", ret); + dev_warn(w->dev, + "BAD_VBA page read fmss_lba=%u vba=%u ret=%d\n", + fmss_lba, vba, ret); return; } + + sel_m = spare + map_slot * WHIMORY_META_SIZE; + sel_meta_lba = get_unaligned_le32(sel_m + 8); + sel_weave = whimory_weave48(sel_m); + sel_type_ok = (sel_m[0] == WHIMORY_META_TYPE_DATA || + sel_m[0] == WHIMORY_META_TYPE_DATA2) && + !(sel_m[1] & 0x02); + sel_lba_ok = (sel_meta_lba == fmss_lba); + bad = !sel_type_ok || !sel_lba_ok; + + dev_info(w->dev, + "VBA_DIAG fmss_lba=%u vba=%u sb=%u ofs=%u " + "ppn=ce%u/cau%u/vblk%u/pbn%u/pg%u selected_slot=%u " + "selected_meta_lba=%u selected_weave=%012llx " + "type=%02x flags=%02x verdict=%s\n", + fmss_lba, vba, s_g_vba_to_sb(w, vba), s_g_vba_to_ofs(w, vba), + ce, cau, vblock, pblock, page, map_slot, sel_meta_lba, + (unsigned long long)sel_weave, sel_m[0], sel_m[1], + bad ? "BAD" : "OK"); + + if (bad) { + const u8 *d = data + map_slot * WHIMORY_LBA_SIZE; + + dev_warn(w->dev, + "BAD_VBA fmss_lba=%u selected_slot=%u type=%02x " + "flags=%02x meta_lba=%u (want %u) first64=%32ph\n", + fmss_lba, map_slot, sel_m[0], sel_m[1], sel_meta_lba, + fmss_lba, d); + } + + if (!vba_page_dump) + return; + for (slot = 0; slot < WHIMORY_VBAS_PER_PAGE; slot++) { const u8 *m = spare + slot * WHIMORY_META_SIZE; const u8 *d = data + slot * WHIMORY_LBA_SIZE; @@ -3368,8 +5020,10 @@ static void whimory_dump_vba_page(struct whimory *w, u32 vba) u16 bps = get_unaligned_le16(d + 11); dev_info(w->dev, - "BAD_VBA slot=%u type=%02x flags=%02x meta_lba=%u bps=%u first64=%32ph %32ph meta=%16ph\n", - slot, m[0], m[1], meta_lba, bps, d, d + 32, m); + "VBA_DIAG slot=%u%s type=%02x flags=%02x meta_lba=%u " + "bps=%u first64=%32ph meta=%16ph\n", + slot, slot == map_slot ? "*" : "", + m[0], m[1], meta_lba, bps, d, m); } } @@ -3389,10 +5043,24 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) p127 = s->btoc_page; if (!p127) return -ENOMEM; - s->btoc_dumps_left = 5; + s->btoc_dumps_left = ftl_diag ? 5 : 0; + w->l2v_defer_pack = true; + s->btoc_verified = 0; + s->diff_replayed_sbs = 0; + s->diff_skipped_sbs = 0; + s->confirm_start_jiffies = jiffies; + s->btoc_confirm_budget_stop = 0; + s->string_hit_itunesdb = 0; + s->string_hit_f00 = 0; + s->string_hit_apps = 0; + s->string_hit_mp3 = 0; + s->string_hit_m4a = 0; - dev_info(w->dev, "SFTL classify scan ce=%u cau=%u blocks=%u\n", - w->geom.num_ce, w->geom.num_cau, nscan); + dev_info(w->dev, + "SFTL classify scan ce=%u cau=%u blocks=%u " + "btoc_confirm_max=%u recover_budget_ms=%u audit_winners=%d\n", + w->geom.num_ce, w->geom.num_cau, nscan, + btoc_confirm_max, recover_budget_ms, audit_lba_winners); for (ce = 0; ce < w->geom.num_ce; ce++) { for (cau = 0; cau < w->geom.num_cau; cau++) { @@ -3402,10 +5070,15 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (nsb >= s->num_sb) goto classify_done; - if ((b & 0x7f) == 0 && cau == 0 && ce == 0) + if ((b & 0x1f) == 0 && ftl_progress_due(w)) dev_info(w->dev, "SFTL classify ce=%u cau=%u blk=%u/%u nsb=%u\n", ce, cau, b, nscan, nsb); + if (recover_yield_us && (b & 0x3) == 0) { + cond_resched(); + usleep_range(recover_yield_us, + recover_yield_us + 500); + } r0 = whimory_cs_read_page(w, ce, cau, b, 0, w->sftl.data_page, S5L8740_NAND_PAGE_SIZE, @@ -3470,7 +5143,18 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) nsb, s->btoc_sbs, s->open_sbs, s->cxt_sbs, s->empty_sbs, s->unknown_sbs); - ret = whimory_cxt_load(w); + whimory_cxt_index_build(w, nsb); + /* + * The CXT is the FTL own checkpoint: it rebuilds the bulk of the map + * from a handful of pages instead of every open superblock. Replay + * below then adopts only what is newer than its base weave. + */ + ret = use_cxt ? whimory_cxt_fast_load(w) : -ENOENT; + if (ret && use_cxt) + dev_info(w->dev, + "CXT seed failed %d; falling back to full replay\n", + ret); + ret = 0; if (ret) dev_warn(w->dev, "s_cxt_load %d; continuing with BTOC replay\n", ret); @@ -3482,10 +5166,23 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) struct whimory_sb *sb = &s->sbs[i]; u32 vblock = whimory_vfl_virt(w, sb->cau, sb->block); + /* + * Replay is the long pole once scan_blocks is widened. Give RNDIS + * and the watchdog air on every superblock, not just inside + * whimory_rebuild_open_sb(). + */ + cond_resched(); + if (recover_yield_us && (i & 0x3) == 0) + usleep_range(recover_yield_us, recover_yield_us + 500); + if (sb->kind == WHIMORY_SB_CXT) continue; - if (s->cxt_loaded && sb->weave && sb->weave < w->cxt_base_weave) + if (use_cxt && s->cxt_loaded && sb->weave && + sb->weave < w->cxt_base_weave) { + s->diff_skipped_sbs++; continue; + } + s->diff_replayed_sbs++; if (sb->kind == WHIMORY_SB_CLOSED) { int ingested; @@ -3497,6 +5194,11 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (ret) continue; s->btoc_pages_read++; + if (s->btoc_verified < btoc_verify_sbs) { + s->btoc_verified++; + whimory_btoc_verify(w, sb, vblock, s->btoc_page, + S5L8740_NAND_PAGE_SIZE); + } if (s->btoc_dumps_left && (s->btoc_pages_read <= 2 || whimory_btoc_looks_be_lpn(s->btoc_page))) { @@ -3505,12 +5207,24 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) s->btoc_dumps_left--; } s->claim_weave = sb->weave; + s->claim_source = 1; ingested = whimory_ingest_btoc_page(w, sb->ce, sb->cau, vblock, s->btoc_page, S5L8740_NAND_PAGE_SIZE); s->claim_weave = 0; + s->claim_source = 0; if (ingested) s->btoc_pages_valid++; + if (ftl_progress_due(w)) + dev_info(w->dev, + "SFTL replay progress i=%u/%u closed_valid=%u " + "open_updates=%u unmap_calls=%u stale_rej=%u " + "mapped=%u ranges=%u confirm=%u budget_stop=%u\n", + i, nsb, s->btoc_pages_valid, + s->open_l2v_updates, s->l2v_unmap_calls, + s->stale_mapping_rejected, s->mapped_lbas, + s->range_nodes, s->btoc_confirm_pages, + s->range_budget_stop); } else if (sb->kind == WHIMORY_SB_OPEN) { if (max_open_sbs && open_done >= max_open_sbs) continue; @@ -3519,17 +5233,31 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) open_done++; else if (ret < 0) return ret; + if (ftl_progress_due(w)) + dev_info(w->dev, + "SFTL open progress done=%u/%u i=%u/%u " + "open_updates=%u ranges=%u mapped=%u\n", + open_done, s->open_sbs, i, nsb, + s->open_l2v_updates, s->range_nodes, + s->mapped_lbas); } } + dev_info(w->dev, + "SFTL diff replay sbs=%u skipped_by_cxt=%u cxt_seeded=%u\n", + s->diff_replayed_sbs, s->diff_skipped_sbs, s->cxt_l2v_updates); + w->l2v_defer_pack = false; ret = whimory_l2v_build_from_ranges(w); - if (ret && s->range_nodes) { + if (ret) { + /* The interval map is the lookup authority, so a failed pack + * is survivable as long as it holds something. + */ + if (!s->range_nodes) + return ret; dev_warn(w->dev, "L2V pack %d; using interval map (%u ranges)\n", ret, s->range_nodes); ret = 0; - } else if (ret) { - return ret; } else { s->packed_ok = true; } @@ -3565,16 +5293,21 @@ static int whimory_sftl_alloc(struct whimory *w) s->meta_page = kvmalloc(WHIMORY_META_SIZE * WHIMORY_VBAS_PER_PAGE * (WHIMORY_DATA_PAGES_PER_SB + 1), GFP_KERNEL); s->cs_page = kvmalloc(sizeof(*s->cs_page), GFP_KERNEL); + s->btoc_map = kvmalloc_array(WHIMORY_DATA_VBAS_PER_SB, + sizeof(*s->btoc_map), GFP_KERNEL); s->sbs = kvcalloc(nsb, sizeof(*s->sbs), GFP_KERNEL); if (!s->btoc_page || !s->data_page || !s->meta_page || !s->cs_page || - !s->sbs) + !s->sbs || !s->btoc_map) return -ENOMEM; /* - *: max_pages_per_btoc = - * div(page_bytes + 16 * vbas_per_sb - 1, page_bytes) + 1 - * 16×512 BTE bytes fit in a 16KiB NAND page → 1; OSOS adds 1 → 2. - */ + * Pages needed to hold one block table of contents: + * + * ceil(16 * vbas_per_sb / page_bytes) + 1 + * + * 16 bytes per entry x 512 VBAs fits in a single 16 KiB NAND page, + * and the stock firmware reserves one more, giving 2. + */ { u32 page_bytes = w->geom.page_size ? w->geom.page_size : S5L8740_NAND_PAGE_SIZE; @@ -3596,11 +5329,11 @@ static int whimory_sftl_alloc(struct whimory *w) } /* - *: zoneSize starts at 0x8D0EC98 * vbas_per_page and - * doubles until >= 16. Minimum from the loop is 16; must be a - * multiple of vbas_per_page. CXT load reads this - * many VBAs into gc_data / gc_meta. - */ + * The garbage-collection zone size doubles until it reaches at + * least 16 VBAs and must stay a multiple of vbas_per_page, so 16 is + * both the minimum and what N31 uses. Context load reads this many + * VBAs at a time into gc_data / gc_meta. + */ s->gc_zone_size = WHIMORY_GC_ZONE_MIN; if (s->gc_zone_size % s->vbas_per_page) return -EINVAL; @@ -3611,9 +5344,9 @@ static int whimory_sftl_alloc(struct whimory *w) if (!s->gc_data || !s->gc_meta) return -ENOMEM; /* - *full-size FTL: num_superblocks * user VBAs per SB. - * BTOC page is not host LBA space (DATA_VBAS_PER_SB). - */ + *full-size FTL: num_superblocks * user VBAs per SB. + * BTOC page is not host LBA space (DATA_VBAS_PER_SB). + */ { u64 cap = (u64)nsb * WHIMORY_DATA_VBAS_PER_SB; @@ -3759,10 +5492,10 @@ static int n31_sftl_open(struct whimory *w) int sess; /* - * OSOS FTL_Open:BTOC (6 slots / 2 open LBA maps), - *GC zone,block tables,SB - * state, nodepool ≥ 0x80000,L2V_Init, then s_boot. - */ + * OSOS FTL_Open:BTOC (6 slots / 2 open LBA maps), + *GC zone,block tables,SB + * state, nodepool ≥ 0x80000,L2V_Init, then s_boot. + */ ret = whimory_sftl_alloc(w); if (ret) return ret; @@ -3807,11 +5540,11 @@ static const struct whimory_ftl_ops n31_sftl_ops = { static int whimory_select_ops(struct whimory *w) { /* - * OSOS dispatches VFL/FTL by signature major through a table that - * is not named in the static dump. N31 media is PPN VFL + SFTL; - * those are the only ops this module implements. Log the majors - * from the signature (when present) and bind the N31 ops. - */ + * OSOS dispatches VFL/FTL by signature major through a table that + * is not named in the static dump. N31 media is PPN VFL + SFTL; + * those are the only ops this module implements. Log the majors + * from the signature (when present) and bind the N31 ops. + */ w->vfl_ops = &n31_vfl_ops; w->ftl = &n31_sftl_ops; if (w->sig_ok) { @@ -3892,6 +5625,7 @@ static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, { struct whimory_meta meta; u32 vba = 0, span = 0; + u32 ce, cau, vblock, page, slot; int ret; if (!w->l2v_ok) @@ -3912,18 +5646,30 @@ static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, memset(buf, 0xff, WHIMORY_LBA_SIZE); return 0; } - if (lba == 0) { - dev_info(w->dev, "L2V lookup LBA0 -> VBA=%u span=%u\n", - vba, span); - whimory_dump_vba_page(w, vba); + if (ftl_diag && (whimory_audit_fmss_lba(lba) || lba < 16)) { + if (!whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, + &slot)) + dev_info(w->dev, + "L2V lookup fmss_lba=%u -> vba=%u span=%u " + "ppn=ce%u/cau%u/vblk%u/pg%u/slot%u\n", + lba, vba, span, ce, cau, vblock, page, slot); + else + dev_info(w->dev, + "L2V lookup fmss_lba=%u -> vba=%u span=%u\n", + lba, vba, span); + /* Sibling VBA_DIAG only for BPB candidates (avoid spam). */ + if (lba == 49279u || lba == 49285u || lba == 49216u || + lba < 4) + whimory_dump_vba_page(w, vba, lba); } ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); if (ret) return ret; ret = whimory_validate_meta(w, &meta, lba); if (!ret) - dev_dbg(w->dev, "meta OK lba=%u vba=%u type=%02x\n", - lba, vba, meta.type); + dev_dbg(w->dev, + "meta OK fmss_lba=%u vba=%u type=%02x meta_lba=%u\n", + lba, vba, meta.type, le32_to_cpu(meta.lba)); return ret; } @@ -4156,7 +5902,8 @@ static ssize_t whimory_status_show(struct device *dev, "disk_gate=%s\n" "mapped_roots=%u mapped_lbas=%u btoc_sbs=%u open_sbs=%u cxt_sbs=%u empty=%u recs=%u cxt_loaded=%d packed=%d\n" "lba0_vba=%u cap=%llu vbas_per_sb=%u hole=%u list=%u\n" - "spare_applied=%u bitmap=%u frag=%u/%u gc_zone=%u btoc_pages=%u updates=%u gen=%u free=%u list_unmapped=%u\n%s\n", + "spare_applied=%u bitmap=%u frag=%u/%u gc_zone=%u btoc_pages=%u updates=%u gen=%u free=%u list_unmapped=%u\n" + "search_cache hits=%u misses=%u recovery=%s\n%s\n", w->fil_ok, w->sig_ok, w->vfl_ok, w->ftl_ok, w->l2v_ok, w->lba0_ok, w->oracle_used, meta_ok ? "enabled" : "disabled", @@ -4174,6 +5921,9 @@ static ssize_t whimory_status_show(struct device *dev, w->sftl.gc_zone_size, w->sftl.max_pages_per_btoc, w->l2v.updates, w->l2v.gen, w->l2v.free_count, w->sftl.token_list_applied, + w->sftl.search_cache_hits, + w->sftl.search_cache_misses, + whimory_recovery_state_name(), w->status); } static DEVICE_ATTR_RO(whimory_status); @@ -4199,6 +5949,8 @@ static void whimory_free(struct whimory *w) kvfree(w->sftl.data_page); kvfree(w->sftl.meta_page); kvfree(w->sftl.cs_page); + kvfree(w->sftl.btoc_map); + kvfree(w->cxt_ext); kvfree(w->sftl.sbs); kvfree(w->sftl.gc_data); kvfree(w->sftl.gc_meta); @@ -4225,17 +5977,16 @@ static int whimory_open_stack(struct whimory *w) /* * Without CS metadata DMA, classic Whimory open cannot validate - * META via page_read. Recover is available via CS phys reads: - * echo 1 > .../ftl_sftl_recover (binds csmap disks to L2V). + * META via page_read. Recover is still available via CS phys: + * echo 1 > .../ftl_sftl_recover */ if (!s5l8740_nand_meta_transport_ok()) { whimory_set_status(w, "CS metadata DMA disabled; " "use ftl_sftl_recover (CS META path) " - "or meta_dma_read=1"); + "or meta_dma_read=1 dma_dry=0"); pr_info("s5l8740-ftl: Whimory auto-open deferred " - "(meta_dma_read=0); run ftl_sftl_recover for " - "CXT→BTOC→L2V on CS META\n"); + "(meta transport off); run ftl_sftl_recover\n"); return -EOPNOTSUPP; } @@ -4381,7 +6132,106 @@ int whimory_l2v_search_phys(u32 lba, u8 *ce, u8 *cau, u16 *blk, u8 *page, return 0; } -int whimory_sftl_recover_cs(void) +static bool whimory_slot_has_needle(const u8 *slot, unsigned int len, + const char *needle) +{ + return !!strnstr((const char *)slot, needle, len); +} + +/* + * Physical string scanner — ignores L2V. Walks readable CS pages and prints + * hits with ce/cau/block/page/slot + meta_lba. Independent of mount. + */ +int whimory_phys_string_scan(unsigned int max_blocks) +{ + struct whimory *w = whimory_dev; + u8 *data; + u8 spare[S5L8740_NAND_META_SIZE]; + unsigned int ce, cau, b, pg, slot, nscan, hits = 0, pages = 0; + static const char *const needles[] = { + "iTunesDB", "F00", "F01", "F02", "iPod_Control", "Music", + "Apps", "NanoApps", ".mp3", ".m4a", "mp3", "m4a", + }; + int sess; + + if (!w || !w->sftl.data_page) + return -ENODEV; + nscan = max_blocks ? max_blocks : + (scan_blocks ? scan_blocks : w->sftl.user_blocks); + if (!nscan) + nscan = 256; + data = w->sftl.data_page; + sess = s5l8740_nand_dma_session_begin(); + dev_info(w->dev, + "PHYS_STRING_SCAN start blocks=%u (L2V ignored)\n", nscan); + for (ce = 0; ce < w->geom.num_ce; ce++) { + for (cau = 0; cau < w->geom.num_cau; cau++) { + for (b = 0; b < nscan; b++) { + for (pg = 0; pg < WHIMORY_DATA_PAGES_PER_SB; + pg++) { + int ret; + unsigned int ni; + + ret = whimory_cs_read_page(w, ce, cau, + b, pg, data, + S5L8740_NAND_PAGE_SIZE, spare, + sizeof(spare)); + if (ret) + break; + if (whimory_page_blank(data, 64) && + whimory_meta_erased(spare, 16)) + break; + pages++; + for (slot = 0; + slot < WHIMORY_VBAS_PER_PAGE; + slot++) { + const u8 *d = data + + slot * WHIMORY_LBA_SIZE; + const u8 *m = spare + + slot * WHIMORY_META_SIZE; + u32 meta_lba = + get_unaligned_le32(m + 8); + + for (ni = 0; ni < ARRAY_SIZE(needles); + ni++) { + if (!whimory_slot_has_needle( + d, + WHIMORY_LBA_SIZE, + needles[ni])) + continue; + hits++; + if (hits <= 64) + dev_info(w->dev, + "PHYS_STRING hit=%s " + "ce=%u cau=%u blk=%u " + "page=%u slot=%u " + "meta_lba=%u type=%02x\n", + needles[ni], + ce, cau, b, pg, + slot, meta_lba, + m[0]); + break; + } + } + if ((pages & 0x7f) == 0) + cond_resched(); + } + if ((b & 0x7f) == 0) + dev_info(w->dev, + "PHYS_STRING_SCAN progress " + "ce=%u cau=%u blk=%u/%u hits=%u\n", + ce, cau, b, nscan, hits); + } + } + } + if (sess == 0) + s5l8740_nand_dma_session_end(); + dev_info(w->dev, + "PHYS_STRING_SCAN done pages=%u hits=%u\n", pages, hits); + return hits; +} + +static int whimory_sftl_recover_cs_locked(void) { struct whimory *w = whimory_dev; int ret, sess; @@ -4442,11 +6292,25 @@ int whimory_sftl_recover_cs(void) w->sftl.btoc_pages_valid = 0; w->sftl.btoc_entries_seen = 0; w->sftl.btoc_l2v_updates = 0; + w->sftl.btoc_meta_mismatch = 0; + w->sftl.btoc_meta_confirmed = 0; + w->sftl.btoc_skipped_zero = 0; + w->sftl.btoc_confirm_pages = 0; + w->sftl.btoc_confirm_capped = 0; + w->sftl.btoc_confirm_budget_stop = 0; + w->sftl.string_hit_itunesdb = 0; + w->sftl.string_hit_f00 = 0; + w->sftl.string_hit_apps = 0; + w->sftl.string_hit_mp3 = 0; + w->sftl.string_hit_m4a = 0; w->sftl.open_slots_seen = 0; w->sftl.open_slots_valid_meta = 0; w->sftl.open_l2v_updates = 0; w->sftl.range_nodes = 0; w->sftl.cxt_l2v_updates = 0; + w->sftl.range_budget_stop = 0; + w->sftl.stale_mapping_rejected = 0; + w->sftl.n_cxt_idx = 0; sess = s5l8740_nand_dma_session_begin(); if (sess && sess != -EBUSY) @@ -4472,6 +6336,44 @@ int whimory_sftl_recover_cs(void) return 0; } +/* + * One boot should run one recovery. A repeat with the same knobs is a + * no-op; a repeat with different knobs rebuilds, but not while the map is + * already live behind a registered disk unless asked explicitly. + */ +int whimory_sftl_recover_cs(void) +{ + struct whimory *w = whimory_dev; + int ret; + + if (!w) + return -ENODEV; + if (recovery_state == RECOVERY_RUNNING) + return -EBUSY; + if (recovery_state == RECOVERY_VALID) { + if (recovery_params_key == whimory_recover_key()) { + dev_info(w->dev, + "recover: map already valid (same params); skipping\n"); + return 0; + } + if (!recover_force && n31_ftl_cs_disk_registered()) { + dev_warn(w->dev, + "recover: disk bound; set recover_force=1 to rebuild\n"); + return -EBUSY; + } + } + + recovery_state = RECOVERY_RUNNING; + ret = whimory_sftl_recover_cs_locked(); + if (ret) { + recovery_state = RECOVERY_FAILED; + return ret; + } + recovery_state = RECOVERY_VALID; + recovery_params_key = whimory_recover_key(); + return 0; +} + static int __init ftl_init(void) { struct whimory *w; @@ -4527,9 +6429,9 @@ static int __init ftl_init(void) ret, FTL_DISK_NAME, w->fil_ok, w->sig_ok, w->vfl_ok, w->ftl_ok, w->l2v_ok, w->lba0_ok); /* - * Keep the platform device so sysfs status is visible. - * The block disk is absent until LBA0 works. - */ + * Keep the platform device so sysfs status is visible. + * The block disk is absent until LBA0 works. + */ return 0; } return 0; diff --git a/drivers/misc/ftl-s5l8740-csmap.c b/drivers/misc/ftl-s5l8740-csmap.c index eca4d2af9386b3..fd9cb10228f2c5 100755 --- a/drivers/misc/ftl-s5l8740-csmap.c +++ b/drivers/misc/ftl-s5l8740-csmap.c @@ -1,4 +1,4 @@ -/* SPDX-License-Identifier: GPL-2.0-only */ +// SPDX-License-Identifier: GPL-2.0-only /* * S5L8740 FTL CS LBA map and read-only VFAT block front-end (N31). * @@ -169,6 +169,7 @@ struct n31_ftl_cs { unsigned int range_fail; unsigned int range_miss; unsigned int demand_scans; + unsigned int read_miss_count; bool disk0_ok; bool fat_critical_ok; @@ -177,6 +178,17 @@ struct n31_ftl_cs { bool dma_session_held; bool whimory_backed; /* L2V_Search via Whimory recover */ + /* FAT semantic validation (beyond crit sector reads). */ + u32 fat_sem_root_chain_len; + u32 fat_sem_root_entries; + u32 fat_sem_music_dirs; + u32 fat_sem_fat0_fat1_diff; + bool fat_sem_itunesdb; + bool fat_sem_apps; + bool fat_sem_nanoapps; + char fat_sem_log[512]; + char string_scan_log[384]; + struct n31_ftl_slice ipod; struct n31_ftl_slice ftl_alias; struct n31_ftl_slice firmware; @@ -186,6 +198,7 @@ struct n31_ftl_cs { static int n31_ftl_find_bpb(struct n31_ftl_cs *ftl); static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl); static int n31_validate_fat_critical(struct n31_ftl_cs *ftl); +static void n31_fat_semantic_validate(struct n31_ftl_cs *ftl); static int n31_ftl_register_disk(struct n31_ftl_cs *ftl); static void n31_ftl_unregister_disk(struct n31_ftl_cs *ftl); static int n31_ftl_apply_bpb(struct n31_ftl_cs *ftl, u32 fmss_lba, @@ -194,6 +207,16 @@ static bool n31_bpb_looks_valid(const u8 *d, u32 *total_out); static struct n31_ftl_cs *n31_ftl; +/* + * A read miss costs nine L2V probes plus ten console lines. VFAT retries a + * failing directory cluster, so an unmapped chain used to bury the log and + * slow the mount. Describe the first few, then count silently. + */ +static unsigned int read_miss_diag_max = 3; +module_param(read_miss_diag_max, uint, 0644); +MODULE_PARM_DESC(read_miss_diag_max, + "Read misses to describe in full before going quiet (0=all)"); + static bool ftl_block_enable = true; module_param(ftl_block_enable, bool, 0644); MODULE_PARM_DESC(ftl_block_enable, @@ -910,6 +933,9 @@ static int n31_ftl_read_fmss_lba_flags(struct n31_ftl_cs *ftl, u32 fmss_lba, /* Whimory L2V is authoritative after CXT→BTOC recover. */ if (ftl->whimory_backed && whimory_l2v_ready()) { + dev_dbg(ftl->dev, + "read disk? via L2V_Search fmss_lba=%u\n", + fmss_lba); ret = whimory_read_fmss_lba(fmss_lba, dst); if (!ret) return 0; @@ -996,6 +1022,9 @@ static int n31_ftl_read_disk_lba_flags(struct n31_ftl_cs *ftl, u32 disk_lba, return -ERANGE; fmss_lba = ftl->fat_base_lba + disk_lba; + dev_dbg(ftl->dev, + "disk_lba=%u -> fmss_lba=%u (fat_base=%u)\n", + disk_lba, fmss_lba, ftl->fat_base_lba); return n31_ftl_read_fmss_lba_flags(ftl, fmss_lba, dst, allow_demand); } @@ -1202,14 +1231,27 @@ static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl) scnprintf(ftl->bpb_log, sizeof(ftl->bpb_log), "fat_base_lba=%u valid=%d total=%u candidates=%u " "oem='%.8s' weave=%012llx ext_flags=0x%04x " - "active_fat=%u mirror=%s selected=%u crit=%u/%u\n", + "active_fat=%u mirror=%s selected=%u crit=%u/%u " + "itunesdb=%d music_dirs=%u fat1_disk_lba=%u\n", ftl->fat_base_lba, ftl->fat_base_valid, ftl->fat_total_sectors, ftl->bpb_ncand, ftl->bpb_cand_oem[best], (unsigned long long)ftl->bpb_cand_weave[best], ftl->layout.ext_flags, ftl->layout.ext_flags & 0xF, (ftl->layout.ext_flags & 0x80) ? "off" : "on", - best + 1, ftl->fat_crit_ok_n, ftl->fat_crit_need_n); + best + 1, ftl->fat_crit_ok_n, ftl->fat_crit_need_n, + ftl->fat_sem_itunesdb, ftl->fat_sem_music_dirs, + ftl->layout.fat_start + ftl->layout.fat_size_32); dev_info(ftl->dev, "%s", ftl->bpb_log); + for (i = 0; i < ftl->bpb_ncand; i++) + dev_info(ftl->dev, + "BPB_CAND #%u fmss_lba=%u weave=%012llx oem='%.8s' " + "total=%u selected=%s reason=%s\n", + i + 1, ftl->bpb_candidates[i], + (unsigned long long)ftl->bpb_cand_weave[i], + ftl->bpb_cand_oem[i], ftl->bpb_cand_total[i], + i == (unsigned int)best ? "yes" : "no", + i == (unsigned int)best ? + "newest_valid_high_crit" : "not_selected"); return ftl->fat_critical_ok ? 0 : -EAGAIN; } @@ -1228,6 +1270,157 @@ static int n31_read_disk_checked(struct n31_ftl_cs *ftl, u32 disk_lba, return ret; } +/* + * Semantic FAT checks beyond fat_critical N/N sector readability. + * Walk root cluster chain, count dir entries, search for known iPod names. + * FAT1 starts at reserved + fat_size32 (e.g. 32+942=974), not disk_lba=33. + */ +static void n31_fat_semantic_validate(struct n31_ftl_cs *ftl) +{ + struct n31_fat_layout *L = &ftl->layout; + u8 *buf, *fat0 = NULL, *fat1 = NULL; + u32 cluster, chain_len = 0, entries = 0, music_dirs = 0; + u32 fat_diff = 0, i, max_chain = 64, max_fat_cmp = 4; + bool itunesdb = false, apps = false, nanoapps = false; + int ret; + + ftl->fat_sem_root_chain_len = 0; + ftl->fat_sem_root_entries = 0; + ftl->fat_sem_music_dirs = 0; + ftl->fat_sem_fat0_fat1_diff = 0; + ftl->fat_sem_itunesdb = false; + ftl->fat_sem_apps = false; + ftl->fat_sem_nanoapps = false; + ftl->fat_sem_log[0] = '\0'; + + if (!ftl->fat_base_valid || L->sectors_per_cluster == 0 || + L->root_cluster < 2) + return; + + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return; + + /* Compare first few FAT0 vs FAT1 sectors (mirror check). */ + if (L->num_fats >= 2 && L->fat_size_32) { + fat0 = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + fat1 = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (fat0 && fat1) { + u32 n = min(max_fat_cmp, L->fat_size_32); + + for (i = 0; i < n; i++) { + u32 d0 = L->fat_start + i; + u32 d1 = L->fat_start + L->fat_size_32 + i; + + if (n31_ftl_read_disk_lba(ftl, d0, fat0) || + n31_ftl_read_disk_lba(ftl, d1, fat1)) + break; + if (memcmp(fat0, fat1, N31_DATA_SLOT_SIZE)) + fat_diff++; + } + } + kfree(fat0); + kfree(fat1); + } + + cluster = L->root_cluster; + while (cluster >= 2 && cluster < 0x0ffffff8 && chain_len < max_chain) { + u32 disk_lba = L->data_start + + (cluster - 2) * L->sectors_per_cluster; + u32 s; + + for (s = 0; s < L->sectors_per_cluster; s++) { + unsigned int off; + + ret = n31_ftl_read_disk_lba(ftl, disk_lba + s, buf); + if (ret) + goto done; + for (off = 0; off + 32 <= N31_DATA_SLOT_SIZE; off += 32) { + const u8 *ent = buf + off; + char name[13]; + unsigned int n; + + if (ent[0] == 0x00) + goto chain_done; + if (ent[0] == 0xe5 || (ent[11] & 0x0f) == 0x0f) + continue; + entries++; + for (n = 0; n < 11; n++) + name[n] = ent[n] == ' ' ? '\0' : ent[n]; + name[11] = '\0'; + if (strnstr(name, "MUSIC", 11) || + (ent[11] & 0x10)) { + if (strnstr((const char *)ent, "MUSIC", 11) || + strnstr(name, "F00", 11) || + strnstr(name, "F01", 11) || + strnstr(name, "F02", 11)) + music_dirs++; + } + } + if (strnstr((const char *)buf, "iTunesDB", + N31_DATA_SLOT_SIZE) || + strnstr((const char *)buf, "ITUNESDB", + N31_DATA_SLOT_SIZE)) + itunesdb = true; + if (strnstr((const char *)buf, "iPod_Control", + N31_DATA_SLOT_SIZE) || + strnstr((const char *)buf, "IPOD_CON", + N31_DATA_SLOT_SIZE)) + entries++; /* ensure root hit is counted */ + if (strnstr((const char *)buf, "NanoApps", + N31_DATA_SLOT_SIZE) || + strnstr((const char *)buf, "NANOAPPS", + N31_DATA_SLOT_SIZE)) + nanoapps = true; + if (strnstr((const char *)buf, "Apps", + N31_DATA_SLOT_SIZE)) + apps = true; + if (strnstr((const char *)buf, "Music", + N31_DATA_SLOT_SIZE) || + strnstr((const char *)buf, "MUSIC", + N31_DATA_SLOT_SIZE)) + music_dirs++; + if (strnstr((const char *)buf, "F00", + N31_DATA_SLOT_SIZE) || + strnstr((const char *)buf, "F01", + N31_DATA_SLOT_SIZE)) + music_dirs++; + } + + /* Next cluster from active FAT (FAT0). */ + { + u32 fat_off = cluster * 4; + u32 fat_sec = L->fat_start + (fat_off / N31_DATA_SLOT_SIZE); + u32 fat_ent_off = fat_off % N31_DATA_SLOT_SIZE; + + ret = n31_ftl_read_disk_lba(ftl, fat_sec, buf); + if (ret) + break; + cluster = get_unaligned_le32(buf + fat_ent_off) & + 0x0fffffffu; + } + chain_len++; + } +chain_done: +done: + ftl->fat_sem_root_chain_len = chain_len; + ftl->fat_sem_root_entries = entries; + ftl->fat_sem_music_dirs = music_dirs; + ftl->fat_sem_fat0_fat1_diff = fat_diff; + ftl->fat_sem_itunesdb = itunesdb; + ftl->fat_sem_apps = apps; + ftl->fat_sem_nanoapps = nanoapps; + scnprintf(ftl->fat_sem_log, sizeof(ftl->fat_sem_log), + "fat_semantic fat_base=%u root_chain_len=%u root_entries=%u " + "music_dirs=%u itunesdb=%d apps=%d nanoapps=%d " + "fat0_fat1_diff=%u fat1_start_disk_lba=%u\n", + ftl->fat_base_lba, chain_len, entries, music_dirs, + itunesdb, apps, nanoapps, fat_diff, + L->fat_start + L->fat_size_32); + dev_info(ftl->dev, "%s", ftl->fat_sem_log); + kfree(buf); +} + /* * Validate BPB, FSInfo, FAT, and root-directory sectors before registering * the block device. @@ -1319,10 +1512,12 @@ static int n31_validate_fat_critical(struct n31_ftl_cs *ftl) CRIT(L->data_start + 3, "root3"); #undef CRIT - /* Require BPB + FAT0 + root0 at minimum; prefer full set. */ - ftl->fat_critical_ok = (ok >= 3 && ftl->disk0_ok && - (ftl->whimory_backed || - n31_map_find(ftl, ftl->fat_base_lba))); + /* Whimory-backed maps must nearly pass; 5/9 must not register. */ + if (ftl->whimory_backed) + ftl->fat_critical_ok = (ok >= 8 && ftl->disk0_ok); + else + ftl->fat_critical_ok = (ok >= 3 && ftl->disk0_ok && + n31_map_find(ftl, ftl->fat_base_lba)); ftl->enable_gate_ok = ftl->fat_critical_ok; ftl->fat_crit_ok_n = ok; ftl->fat_crit_need_n = need; @@ -1331,6 +1526,8 @@ static int n31_validate_fat_critical(struct n31_ftl_cs *ftl) ok, need, ftl->enable_gate_ok, ftl->fat_base_lba, ftl->layout_log); dev_info(ftl->dev, "%s", ftl->last_log); + if (ftl->fat_critical_ok) + n31_fat_semantic_validate(ftl); ret = ftl->fat_critical_ok ? 0 : -EAGAIN; out: mutex_unlock(&ftl->lock); @@ -1504,6 +1701,71 @@ static void n31_firmware_probe(struct n31_ftl_cs *ftl) dev_info(ftl->dev, "%s", ftl->fw_log); } +/* + * VFAT directory bread failures land here as L2V misses. Print address-space + * math + neighbor map presence so we can tell "not scanned yet" from corruption. + */ +static void n31_log_read_miss(struct n31_ftl_cs *ftl, struct n31_ftl_slice *sl, + u32 fmss_lba, u32 disk_lba, int ret) +{ + struct n31_fat_layout *L = &ftl->layout; + u32 cluster = 0; + int i; + u8 ce = 0, cau = 0, page = 0, slot = 0; + u16 blk = 0; + u64 weave = 0; + int phys_ret; + + ftl->read_miss_count++; + if (read_miss_diag_max && ftl->read_miss_count > read_miss_diag_max) + return; + if (L->valid && L->sectors_per_cluster && + disk_lba >= L->data_start) { + cluster = ((disk_lba - L->data_start) / + L->sectors_per_cluster) + 2; + } + + dev_err_ratelimited(ftl->dev, + "read miss %s fmss_lba=%u disk_lba=%u ret=%d " + "fat_base=%u data_start=%u spc=%u cluster~=%u miss_n=%u\n", + sl->gd ? sl->gd->disk_name : "?", + fmss_lba, disk_lba, ret, + ftl->fat_base_lba, L->data_start, L->sectors_per_cluster, + cluster, ftl->read_miss_count); + + for (i = -4; i <= 4; i++) { + u32 n = fmss_lba + i; + u8 nce = 0, ncau = 0, npg = 0, nslot = 0; + u16 nblk = 0; + u64 nw = 0; + int nr; + + if ((int)fmss_lba + i < 0) + continue; + nr = whimory_l2v_search_phys(n, &nce, &ncau, &nblk, &npg, + &nslot, &nw); + if (!nr) + dev_err_ratelimited(ftl->dev, + " neighbor fmss_lba=%u MAPPED ce=%u cau=%u " + "blk=%u pg=%u slot=%u weave=%012llx\n", + n, nce, ncau, nblk, npg, nslot, + (unsigned long long)nw); + else if (i == 0) + dev_err_ratelimited(ftl->dev, + " neighbor fmss_lba=%u UNMAPPED ret=%d\n", + n, nr); + } + + phys_ret = whimory_l2v_search_phys(fmss_lba, &ce, &cau, &blk, &page, + &slot, &weave); + if (!phys_ret) + dev_err_ratelimited(ftl->dev, + " L2V suddenly mapped after miss? ce=%u cau=%u blk=%u " + "pg=%u slot=%u\n", + ce, cau, blk, page, slot); + (void)phys_ret; +} + static void n31_ftl_submit_bio(struct bio *bio) { struct n31_ftl_slice *sl = bio->bi_bdev->bd_disk->private_data; @@ -1556,10 +1818,7 @@ static void n31_ftl_submit_bio(struct bio *bio) if (!ret) memcpy(dst + done, ftl->bounce, n); else - dev_err_ratelimited(ftl->dev, - "read miss %s fmss_lba=%u ret=%d\n", - sl->gd ? sl->gd->disk_name : "?", - fmss_lba, ret); + n31_log_read_miss(ftl, sl, fmss_lba, off, ret); mutex_unlock(&ftl->lock); if (ret) { kunmap_local(dst); @@ -2034,9 +2293,7 @@ static ssize_t ftl_vec_stats_show(struct device *dev, v = &ftl->vec; if (v->ready) cross = n31_vecmap_lookup(v, N31_FAT_BASE_DEFAULT, &p); - return sysfs_emit(buf, - "%s" - "cross_49279_ret=%d p=%u\n", + return sysfs_emit(buf, "%scross_49279_ret=%d p=%u\n", ftl->vec_log[0] ? ftl->vec_log : "ready=0\n", cross, p); } @@ -2148,6 +2405,13 @@ bool n31_ftl_cs_whimory_backed(void) return n31_ftl && n31_ftl->whimory_backed; } +/* True once /dev/s5l8740-ipod is live; a rebuild under it is destructive. */ +bool n31_ftl_cs_disk_registered(void) +{ + return n31_ftl && n31_ftl->ipod.gd; +} +EXPORT_SYMBOL_GPL(n31_ftl_cs_disk_registered); + int n31_ftl_cs_bind_whimory(void) { struct n31_ftl_cs *ftl = n31_ftl; @@ -2273,11 +2537,58 @@ static ssize_t ftl_sftl_recover_store(struct device *dev, ret = whimory_sftl_recover_cs(); if (ret) return ret; + /* + * Re-binding a disk that is already registered clears the BPB + * candidates and leaves the gendisk at capacity 0, so a live mount + * starts failing every read. Recovery that was a no-op must not + * disturb the disk it just declined to rebuild. + */ + if (n31_ftl_cs_disk_registered()) + return count; ret = n31_ftl_cs_bind_whimory(); return ret ? ret : count; } static DEVICE_ATTR_WO(ftl_sftl_recover); +/* echo > ftl_cxt_dump — report CXT bases/tags; never touches L2V */ +static ssize_t ftl_cxt_dump_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int v = 0; + int ret; + + (void)dev; + (void)attr; + if (sscanf(buf, "%u", &v) < 1) + v = 0; + ret = whimory_cxt_dump(v); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_cxt_dump); + +/* + * echo [fat_base] > ftl_cxt_candidate — build the CXT TREE map into a + * separate candidate array, diff it against the live brute-force map, and + * validate the FAT-critical sectors through it. Never mutates L2V. + */ +static ssize_t ftl_cxt_candidate_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + unsigned int v = 0; + int ret; + + (void)dev; + (void)attr; + if (sscanf(buf, "%u", &v) < 1 || !v) + v = ftl ? ftl->fat_base_lba : 0; + ret = whimory_cxt_candidate(v); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(ftl_cxt_candidate); + static ssize_t ftl_finishline_status_show(struct device *dev, struct device_attribute *attr, char *buf) @@ -2309,8 +2620,113 @@ static ssize_t ftl_finishline_status_show(struct device *dev, } static DEVICE_ATTR_RO(ftl_finishline_status); +static ssize_t ftl_fat_semantic_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", + ftl->fat_sem_log[0] ? ftl->fat_sem_log : + "none\n"); +} +static DEVICE_ATTR_RO(ftl_fat_semantic); + +static ssize_t ftl_phys_string_scan_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int blocks = 0; + int hits; + + if (kstrtouint(buf, 0, &blocks)) + blocks = 0; + hits = whimory_phys_string_scan(blocks); + if (n31_ftl) + scnprintf(n31_ftl->string_scan_log, + sizeof(n31_ftl->string_scan_log), + "phys_string_scan blocks=%u hits=%d\n", blocks, hits); + return hits < 0 ? hits : count; +} +static DEVICE_ATTR_WO(ftl_phys_string_scan); + +static ssize_t ftl_logical_string_scan_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct n31_ftl_cs *ftl = n31_ftl; + u8 *sec; + u32 i, nsectors = 4096, hits = 0; + int ret, sess; + static const char *const needles[] = { + "iTunesDB", "F00", "F01", "F02", "iPod_Control", "Music", + "Apps", "NanoApps", ".mp3", ".m4a", + }; + + if (!ftl || !ftl->fat_base_valid) + return -ENODEV; + if (kstrtouint(buf, 0, &nsectors)) + nsectors = 4096; + if (nsectors > ftl->fat_total_sectors) + nsectors = ftl->fat_total_sectors; + sec = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!sec) + return -ENOMEM; + sess = s5l8740_nand_dma_session_begin(); + dev_info(ftl->dev, + "LOGICAL_STRING_SCAN start disk_lbas=0..%u via L2V\n", + nsectors); + for (i = 0; i < nsectors; i++) { + unsigned int ni; + + ret = n31_ftl_read_disk_lba(ftl, i, sec); + if (ret) + continue; + for (ni = 0; ni < ARRAY_SIZE(needles); ni++) { + if (!strnstr((const char *)sec, needles[ni], + N31_DATA_SLOT_SIZE)) + continue; + hits++; + if (hits <= 64) + dev_info(ftl->dev, + "LOGICAL_STRING hit=%s disk_lba=%u " + "fmss_lba=%u\n", + needles[ni], i, + ftl->fat_base_lba + i); + break; + } + if ((i & 0xff) == 0) + cond_resched(); + } + if (sess == 0) + s5l8740_nand_dma_session_end(); + scnprintf(ftl->string_scan_log, sizeof(ftl->string_scan_log), + "logical_string_scan disk_lbas=%u hits=%u\n", nsectors, hits); + dev_info(ftl->dev, "%s", ftl->string_scan_log); + kfree(sec); + return count; +} +static DEVICE_ATTR_WO(ftl_logical_string_scan); + +static ssize_t ftl_string_scan_log_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct n31_ftl_cs *ftl = n31_ftl; + + if (!ftl) + return sysfs_emit(buf, "no ftl\n"); + return sysfs_emit(buf, "%s", + ftl->string_scan_log[0] ? ftl->string_scan_log : + "none\n"); +} +static DEVICE_ATTR_RO(ftl_string_scan_log); + static struct attribute *n31_ftl_finish_attrs[] = { &dev_attr_ftl_sftl_recover.attr, + &dev_attr_ftl_cxt_dump.attr, + &dev_attr_ftl_cxt_candidate.attr, &dev_attr_ftl_map_build.attr, &dev_attr_ftl_scan_block_window.attr, &dev_attr_ftl_map_stats.attr, @@ -2322,6 +2738,10 @@ static struct attribute *n31_ftl_finish_attrs[] = { &dev_attr_ftl_fat_base_lba.attr, &dev_attr_ftl_bpb.attr, &dev_attr_ftl_layout.attr, + &dev_attr_ftl_fat_semantic.attr, + &dev_attr_ftl_phys_string_scan.attr, + &dev_attr_ftl_logical_string_scan.attr, + &dev_attr_ftl_string_scan_log.attr, &dev_attr_ftl_read_fmss_lba.attr, &dev_attr_ftl_read_disk_lba.attr, &dev_attr_ftl_read_disk_range.attr, diff --git a/drivers/misc/ftl-s5l8740-csmap.h b/drivers/misc/ftl-s5l8740-csmap.h index f676e4067f4fdc..34ea2b1abac586 100755 --- a/drivers/misc/ftl-s5l8740-csmap.h +++ b/drivers/misc/ftl-s5l8740-csmap.h @@ -34,10 +34,13 @@ int n31_ftl_read_disk_lba(struct n31_ftl_cs *ftl, u32 disk_lba, void *dst); * (no full hash import — avoids multi-million node RAM). */ int n31_ftl_cs_bind_whimory(void); +bool n31_ftl_cs_disk_registered(void); bool n31_ftl_cs_whimory_backed(void); /* Implemented in ftl-s5l8740-core.c (same module). */ int whimory_sftl_recover_cs(void); +int whimory_cxt_dump(unsigned int max_vbas); +int whimory_cxt_candidate(u32 fat_base); bool whimory_l2v_ready(void); int whimory_read_fmss_lba(u32 lba, void *buf); int whimory_range_walk(int (*fn)(u32 start, u32 len, u32 vba, u64 weave, @@ -46,4 +49,7 @@ int whimory_range_walk(int (*fn)(u32 start, u32 len, u32 vba, u64 weave, int whimory_l2v_search_phys(u32 lba, u8 *ce, u8 *cau, u16 *blk, u8 *page, u8 *slot, u64 *weave); +/* Physical NAND string scan (ignores L2V). Returns hit count. */ +int whimory_phys_string_scan(unsigned int max_blocks); + #endif /* FTL_S5L8740_CSMAP_H */ diff --git a/drivers/misc/ftl-s5l8740-vecmap.c b/drivers/misc/ftl-s5l8740-vecmap.c index 21119376e7221e..3f35f197b66366 100755 --- a/drivers/misc/ftl-s5l8740-vecmap.c +++ b/drivers/misc/ftl-s5l8740-vecmap.c @@ -1,4 +1,4 @@ -/* SPDX-License-Identifier: GPL-2.0-only */ +// SPDX-License-Identifier: GPL-2.0-only /* * Dual vector LBA maps for S5L8740 FTL. * @@ -104,7 +104,8 @@ static int n31_compress_axis(u32 *base_out, s8 *delta_out, u32 count, s32 *scratch; unsigned int g; - scratch = kmalloc(N31_VEC_GROUP_SIZE * sizeof(*scratch), GFP_KERNEL); + scratch = kmalloc_array(N31_VEC_GROUP_SIZE, sizeof(*scratch), + GFP_KERNEL); if (!scratch) return -ENOMEM; diff --git a/drivers/misc/nand-s5l8740.c b/drivers/misc/nand-s5l8740.c index 7048ca372d1a44..fdfb8a479bf63d 100755 --- a/drivers/misc/nand-s5l8740.c +++ b/drivers/misc/nand-s5l8740.c @@ -383,6 +383,28 @@ module_param(reset_every, uint, 0644); MODULE_PARM_DESC(reset_every, "nand_reset after this many page_read calls (0=off)"); /* 50D960 uses 3; full PPN page is 16 x 1K data (+64 spare not in this PIO). */ +/* + * The CS/command-list path bumps pages_since_reset but historically never + * acted on it — only the PIO page_read paths reset. A recover is thousands + * of back-to-back live C00 kicks with no controller reset in between, and + * the glass reboots partway through a wide scan. Reset every N CS reads. + */ +/* Total live CS kicks since insmod; heartbeat + post-mortem marker. */ +static unsigned int cs_reads_total; +/* Off by default: console writes lengthen recover measurably. */ +static bool cs_heartbeat; +module_param(cs_heartbeat, bool, 0644); +MODULE_PARM_DESC(cs_heartbeat, "Log CS read progress every 1024 pages"); +module_param(cs_reads_total, uint, 0444); + +/* Where CS read wall time actually goes; reported by the heartbeat. */ +static u64 cs_ns_kick, cs_ns_copy; + +static unsigned int cs_reset_every; +module_param(cs_reset_every, uint, 0644); +MODULE_PARM_DESC(cs_reset_every, + "fmss_nand_reset after this many CS phys reads (0=off)"); + static unsigned int page_chunks = 16; module_param(page_chunks, uint, 0644); MODULE_PARM_DESC(page_chunks, "1K PIO chunks per page_read (16=full 16KiB data)"); @@ -425,7 +447,8 @@ MODULE_PARM_DESC(dma_nsect, "DMA span (# logical LBAs) per CS read (default 1)") static bool meta_dma_read; module_param(meta_dma_read, bool, 0644); MODULE_PARM_DESC(meta_dma_read, - "Use command-list data+meta read for metadata callers (default N — CS kick wedges)"); + "Allow page_read META via CS span4 (default N). " + "ftl_sftl_recover uses dma_session live CS without this."); /* * PIO page path uses the legacy reset/reinit sequence. @@ -472,7 +495,9 @@ static unsigned int dma_kick = 0xfff5; module_param(dma_kick, uint, 0644); MODULE_PARM_DESC(dma_kick, "FMSEQ (C00) kick value (OSOS D39EC = 0xFFF5)"); -/* Default Y: program descriptors without C00 kick until glass preflight passes. */ +/* Default dry/disarmed: permanent live C00 kick reboots (glass 2026-08-27). + * ftl_sftl_recover / cs_phys use dma_session_begin to arm live CS temporarily. + */ static bool dma_dry = true; module_param(dma_dry, bool, 0644); MODULE_PARM_DESC(dma_dry, @@ -480,11 +505,12 @@ MODULE_PARM_DESC(dma_dry, static bool dma_armed; module_param(dma_armed, bool, 0644); -MODULE_PARM_DESC(dma_armed, "Allow one hazardous CS DMA kick"); +MODULE_PARM_DESC(dma_armed, "Allow CS DMA kick (default N — session arms for recover)"); static bool dma_one_shot = true; module_param(dma_one_shot, bool, 0644); -MODULE_PARM_DESC(dma_one_shot, "Disarm CS DMA after one kick"); +MODULE_PARM_DESC(dma_one_shot, + "Clear dma_armed after each CS kick (default Y outside sessions)"); /* Canary path: one page, no FTL/lba_map ingest. */ static bool dma_skip_ingest; @@ -918,8 +944,8 @@ static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) writel(0x1e2, f->base + FMCTRL1); } else { /* OSOS 50D960 parity: FMLEN=52 FMCE=16 +0x24=0 CTRL1=34 - * — do not touch +0x18 or +0x28 here. - */ + * — do not touch +0x18 or +0x28 here. + */ writel(52, f->base + FMLEN); writel(parity_fmce, f->base + FMCE); writel(0, f->base + FMUNK24); @@ -931,11 +957,11 @@ static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) goto fail_ctrl0; } /* - * Live: after FMCE=16 FMLEN=52, D622C can read bytes - * (head often 02 …) — parity lands on the DATA FIFO. - * Never drain it when ecc_before_drain=1 (OSOS leaves - * it for 4EB458). Optional strip only when ECC off. - */ + * Live: after FMCE=16 FMLEN=52, D622C can read bytes + * (head often 02 …) — parity lands on the DATA FIFO. + * Never drain it when ecc_before_drain=1 (OSOS leaves + * it for 4EB458). Optional strip only when ECC off. + */ if (xfer_style == 0 && !ecc_before_drain) { u8 dig[64]; @@ -944,9 +970,9 @@ static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) memcpy(f->last_parity[i], dig, 53); f->last_parity_len[i] = 53; /* - * Host-visible spare is the first 16 of - * the 53-byte beat, once per 4K slot. - */ + * Host-visible spare is the first 16 of + * the 53-byte beat, once per 4K slot. + */ if ((i & 3) == 0) { unsigned int slot = (unsigned int)i / 4u; unsigned int pick = 0; @@ -992,11 +1018,11 @@ static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) ecc_ret = fmss_ecc_chunk(f, 0); if (ecc_ret == 1) { /* - * Chunk is erased/clean — leave zeros and - * keep going. Aborting the whole page on - * chunk0 made FPart/VFL miss SLC specials - * (glass: 4096 tail reads, tag30=0). - */ + * Chunk is erased/clean — leave zeros and + * keep going. Aborting the whole page on + * chunk0 made FPart/VFL miss SLC specials + * (glass: 4096 tail reads, tag30=0). + */ f->last_clean_chunks++; continue; } @@ -1016,10 +1042,10 @@ static int fmss_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) } /* - * Trailing fmss_data_in(64) after 16×1K is an empty FIFO (zeros) and - * must not be treated as META (that polluted lba_map with type 0x00). - * Extra style-1 FMLEN=15 beats after this path desynced the next page. - */ + * Trailing fmss_data_in(64) after 16×1K is an empty FIFO (zeros) and + * must not be treated as META (that polluted lba_map with type 0x00). + * Extra style-1 FMLEN=15 beats after this path desynced the next page. + */ fmss_cmd(f, 0x77); writel(0, f->base + FMCTRL0); @@ -1060,9 +1086,9 @@ static int fmss_page_read_with_meta(struct nand_s5l8740 *f, unsigned int ce, if (ret || !meta_pass) return ret; /* - * Erased PPN page: all 16 chunks ECC-clean. Spare is 0xFF; a second - * 50D960 only burns the controller (tail+brute are mostly empty). - */ + * Erased PPN page: all 16 chunks ECC-clean. Spare is 0xFF; a second + * 50D960 only burns the controller (tail+brute are mostly empty). + */ if (f->last_clean_chunks && f->last_clean_chunks >= (page_chunks ? page_chunks : 16)) { memset(f->last_spare, 0xff, sizeof(f->last_spare)); @@ -1144,9 +1170,9 @@ static bool fmss_cs_preflight(struct nand_s5l8740 *f) } /* - * Conservative idle gate. Adjust allowed states only after glass logs. - * Avoid kick if status already advertises completion/error/busy noise. - */ + * Conservative idle gate. Adjust allowed states only after glass logs. + * Avoid kick if status already advertises completion/error/busy noise. + */ if (c0c & 0x0d) return false; @@ -1190,10 +1216,10 @@ static irqreturn_t fmss_cs_irq(int irq, void *data) f->last_dma_c0c = st; /* - * Level-style VIC source: clear peripheral before parent EOI, or it - * can retrigger/stick. Snapshot first — waiter must not require C0C - * to remain asserted after W1C. - */ + * Level-style VIC source: clear peripheral before parent EOI, or it + * can retrigger/stick. Snapshot first — waiter must not require C0C + * to remain asserted after W1C. + */ if (st & 0x0d) { writel(st & 0x0d, f->base + FMSEQIRQ); readl(f->base + FMSEQIRQ); @@ -1290,18 +1316,18 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) ce_bit = 1u << (16 + ce); /* - * 4EDDDC one-page descriptor list: - * desc0: CE-select dword0=1<<(ce+16), later |0x80000000 if last for CE - * dword2/3 = packed physical address (v40: low in [2]) - * desc1: transfer dword0=(1<<(ce+16))|1, [1]=span, [2]=meta, [3]=data - * term: 0x00010002 - */ + * 4EDDDC one-page descriptor list: + * desc0: CE-select dword0=1<<(ce+16), later |0x80000000 if last for CE + * dword2/3 = packed physical address (v40: low in [2]) + * desc1: transfer dword0=(1<<(ce+16))|1, [1]=span, [2]=meta, [3]=data + * term: 0x00010002 + */ /* - * 4EDDDC address qword from 5172A0 (READ, v40 / multi-LBA page): - * lo = (rec * span) | ((rec * slot) << 16) // length | column<<16 - * hi = encoded_ppn (5173CA, mode 0) - * desc[2]=lo, desc[3]=hi when dma_d14>=7 (v40). - */ + * 4EDDDC address qword from 5172A0 (READ, v40 / multi-LBA page): + * lo = (rec * span) | ((rec * slot) << 16) // length | column<<16 + * hi = encoded_ppn (5173CA, mode 0) + * desc[2]=lo, desc[3]=hi when dma_d14>=7 (v40). + */ cl[0] = ce_bit; cl[1] = 0; { @@ -1365,9 +1391,9 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) } /* - * Clear/idle before programming D-regs / C04 (USB/PL080 pattern: - * clear status, prove idle, program pointers, kick last). - */ + * Clear/idle before programming D-regs / C04 (USB/PL080 pattern: + * clear status, prove idle, program pointers, kick last). + */ if (!fmss_cs_preflight(f)) { ret = -EBUSY; goto dma_done; @@ -1381,9 +1407,9 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) writel(dma_d14, f->base + FMGEN5); /* D14 = addr_cycles-1 */ writel((u32)f->seq_dma, f->base + FMSEQBASE); /* C04 = seq program */ /* - * Do NOT poke +0x81C here — that is 4EB458 ECC, not in 4EDDDC/D39EC. - * A spurious 81C write before CS previously correlated with SoC wedges. - */ + * Do NOT poke +0x81C here — that is 4EB458 ECC, not in 4EDDDC/D39EC. + * A spurious 81C write before CS previously correlated with SoC wedges. + */ /* D39EC: C00 = 0xFFF5. Do NOT use 0x80000 (reset) here. */ reinit_completion(&f->cs_irq); fmss_info("dma kick ce=%u addr=%08x seq=%08x cmdl=%08x data=%08x meta=%08x st=%08x d14=%u kick=%04x dry=%d armed=%d\n", @@ -1405,10 +1431,12 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) dma_armed = false; /* - * Device-visible descriptor/data/status buffers before CS fetch. - * Coherent allocs do not need explicit cache flushes; ordering does. - */ + * Publish the descriptor, data and status buffers before the + * sequencer fetches them. Coherent allocations need no explicit + * cache maintenance, but they do need ordering. + */ dma_wmb(); + /* Pair the DMA-visible write above with a full barrier. */ wmb(); /* Flush posted MMIO programming before the sequencer kick. */ @@ -1422,9 +1450,9 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) readl(f->base + FMSEQ); /* posted write flush */ /* - * Prefer IRQ completion snapshot (ISR already W1C'd C0C). Fall back - * to short poll only when no IRQ or completion never arrived. - */ + * Prefer IRQ completion snapshot (ISR already W1C'd C0C). Fall back + * to short poll only when no IRQ or completion never arrived. + */ if (f->irq > 0) { if (wait_for_completion_timeout(&f->cs_irq, msecs_to_jiffies(200))) { @@ -1437,9 +1465,9 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) } /* - * Avoid require C0C to still be live — ISR clears it for VIC EOI. - * Trust the saved snapshot from ISR or poll. - */ + * Avoid require C0C to still be live — ISR clears it for VIC EOI. + * Trust the saved snapshot from ISR or poll. + */ if (!ret && f->last_dma_c0c && ((f->last_dma_c0c & 0x0d) != 1)) ret = -EIO; @@ -1523,8 +1551,10 @@ static int fmss_dma_page_read(struct nand_s5l8740 *f, unsigned int ce, u32 addr) } /* - *: PPN parameter page, 512 bytes via 41AE38. - * cmd 0x92, addr 0, cmd 0x97, 0x77/0x7D, 41DA70, cmd 0x7A, 512 PIO. + * Read the 512-byte PPN parameter page. + * + * Command sequence: 0x92, address 0, 0x97, then 0x77/0x7D to select the + * page, then 0x7A and 512 bytes of PIO. */ static int fmss_param_read(struct nand_s5l8740 *f, unsigned int ce) { @@ -1574,8 +1604,8 @@ static int fmss_param_read(struct nand_s5l8740 *f, unsigned int ce) } /* - *: controller reset only (no NAND array write). - * Followed by per-CE cmd 0xFF as in 130060(a3=1). + * Reset the FMSS controller only; this touches no NAND array state. + * Each CE is then issued command 0xFF, matching the stock reset path. */ static int fmss_ctrl_reset(struct nand_s5l8740 *f) { @@ -2301,9 +2331,9 @@ static ssize_t cs_phys_read_store(struct device *dev, if (nf < 4) return -EINVAL; /* - * Legacy: "ce cau blk pg slc [lba]" — if 5th token is 0/1 treat as - * SLC and take optional 6th as fmss_lba. - */ + * Legacy: "ce cau blk pg slc [lba]" — if 5th token is 0/1 treat as + * SLC and take optional 6th as fmss_lba. + */ if (nf == 5 && want_lba <= 1) { unsigned int slc_ignored = want_lba; u32 lba6 = 0xffffffffu; @@ -2455,9 +2485,9 @@ static void fmss_lba_claim_note_page(struct nand_s5l8740 *f, unsigned int ce, meta = f->last_spare + s * 16; /* - * Diagnostic: any type whose LE LBA matches a target. - * Production mapping still prefers type 0x01. - */ + * Diagnostic: any type whose LE LBA matches a target. + * Production mapping still prefers type 0x01. + */ lba_le = fmss_ppn_meta_lba(meta); lba_be = get_unaligned_be32(meta + 8); if (meta[0] == 0x01 && lba_le < 4096u) @@ -2615,24 +2645,27 @@ static int fmss_lba_parse_targets(const char *s, unsigned int *start, { unsigned int n = 0, a = 0, b = 256; const char *p = s; - char *end; + char *list, *cursor, *tok; + size_t len = 0; lba_claim_ntargets = 0; - while (*p && *p != ' ' && *p != '\t' && *p != '\n') { - unsigned long v = simple_strtoul(p, &end, 0); + while (p[len] && p[len] != ' ' && p[len] != '\t' && p[len] != '\n') + len++; + list = kstrndup(p, len, GFP_KERNEL); + if (!list) + return -ENOMEM; + cursor = list; + while ((tok = strsep(&cursor, ",")) != NULL) { + unsigned int v; - if (end == p) - return -EINVAL; - if (n >= FMSS_LBA_TARGETS_MAX) + if (n >= FMSS_LBA_TARGETS_MAX || kstrtouint(tok, 0, &v)) { + kfree(list); return -EINVAL; - lba_claim_targets[n++] = (unsigned int)v; - p = end; - if (*p == ',') { - p++; - continue; } - break; + lba_claim_targets[n++] = v; } + kfree(list); + p += len; if (!n) return -EINVAL; while (*p == ' ' || *p == '\t') @@ -3416,10 +3449,10 @@ static void fmss_l2v_ingest_btoc(struct nand_s5l8740 *f, unsigned int ce, continue; if (lpn == 0) { /* - * Avoid fmss_boot_carve_try here — nested full-page - * reads during the BTOC walk wedge FMSS. Discover - * handles BTOC[0]==0 after the walk. - */ + * Avoid fmss_boot_carve_try here — nested full-page + * reads during the BTOC walk wedge FMSS. Discover + * handles BTOC[0]==0 after the walk. + */ continue; } fmss_l2v_set_ex(lpn, ce, cau, block, p, true, L2V_SRC_BTOC, 0); @@ -4423,10 +4456,10 @@ static int fmss_boot_carve_discover(struct nand_s5l8740 *f, unsigned int start, l1 = use_le ? fmss_btoc_entry_le(f->last_page, 1) : fmss_btoc_entry_be(f->last_page, 1); /* - * LPN0 candidate: BTOC[0]==0 (even if [1] is junk — - * live ce1/cau1/blk63). Avoid scan every random - * zero dword in non-ingestible pages (wedges NAND). - */ + * LPN0 candidate: BTOC[0]==0 (even if [1] is junk — + * live ce1/cau1/blk63). Avoid scan every random + * zero dword in non-ingestible pages (wedges NAND). + */ if (l0 == 0) { page_chunks = 16; if (fmss_boot_try_btoc_page(f, ce, cau, b, @@ -4460,9 +4493,9 @@ static int fmss_boot_carve_discover(struct nand_s5l8740 *f, unsigned int start, } /* - * Aligned BPB: page0 of each block, then all pages of open SBs - * (page0 programmed && page127 not closed BTOC/BTE). Never mid-page OEM. - */ + * Aligned BPB: page0 of each block, then all pages of open SBs + * (page0 programmed && page127 not closed BTOC/BTE). Never mid-page OEM. + */ page_chunks = 16; { unsigned int boot_scan = nblocks ? nblocks : 256; @@ -4769,10 +4802,10 @@ static int fmss_l2v_build(struct nand_s5l8740 *f, unsigned int max_lpn, max_lpn); else if (fmss_page_looks_bte(f->last_page)) { /* - * Pass 2: BTE needs the full - * 16 KiB page; walk used 1-chunk - * probe — re-read full page. - */ + * Pass 2: BTE needs the full + * 16 KiB page; walk used 1-chunk + * probe — re-read full page. + */ unsigned int saved2 = page_chunks; page_chunks = 16; @@ -4788,9 +4821,9 @@ static int fmss_l2v_build(struct nand_s5l8740 *f, unsigned int max_lpn, } /* - * Classic block-map heuristic only when BTOC is - * blank — capped probes to avoid wedging. - */ + * Classic block-map heuristic only when BTOC is + * blank — capped probes to avoid wedging. + */ if (!btoc_ok && bmap_probes < 32) { page_chunks = 16; fmss_l2v_try_block_map_page(f, ce, cau, @@ -5307,10 +5340,10 @@ static ssize_t ftl_grep_store(struct device *dev, struct device_attribute *attr, f->pages_since_reset = 0; } /* - * Avoid require BTOC page 127 — Apple FAT clusters - * live in data pages even when BTOC looks blank. - * (Root cause: old code skipped whole superblocks.) - */ + * Avoid require BTOC page 127 — Apple FAT clusters + * live in data pages even when BTOC looks blank. + * (Root cause: old code skipped whole superblocks.) + */ for (p = 0; p < FMSS_BTOC_PAGE; p++) { unsigned int off = 0, show, flags; u32 lpn = ~0u; @@ -6346,15 +6379,15 @@ int nand_ftl_read_sector(u64 logical_sector, void *buf) unsigned int lpn, sec, ce, cau, block, page, pblock, off, saved; u32 addr, packed; int ret; - int (*hook)(u64, void *); + int (*hook)(u64 lba, void *buf); if (!buf) return -ENODEV; /* - * Whimory registers the real LBA reader after FTL_Open. Call it - * without the FMSS mutex — the FIL page_read wrapper takes that lock. - */ + * Whimory registers the real LBA reader after FTL_Open. Call it + * without the FMSS mutex — the FIL page_read wrapper takes that lock. + */ hook = READ_ONCE(nand_ftl_read_hook); if (hook) return hook(logical_sector, buf); @@ -6458,10 +6491,10 @@ int s5l8740_nand_meta_transport_ok(void) struct nand_s5l8740 *f = nand_dev; /* - * Disk registration still requires meta_dma_read=1. - * Early CS phys helpers use s5l8740_nand_cs_phys_read() instead — - * glass-proven span4/rec4112, but FTL must not auto-open yet. - */ + * Disk registration still requires meta_dma_read=1. + * Early CS phys helpers use s5l8740_nand_cs_phys_read() instead — + * glass-proven span4/rec4112, but FTL must not auto-open yet. + */ return f && f->dma_ok && meta_dma_read; } EXPORT_SYMBOL_GPL(s5l8740_nand_meta_transport_ok); @@ -6542,15 +6575,15 @@ int s5l8740_nand_query_geometry(struct s5l8740_nand_geom *g) mutex_unlock(&f->lock); /* - * FIL GetInfo (vtable +80,: - * 101 — NAND present / signature +0x34 geometry (WhimoryBoot.c:169,260) - * 0 → "No NAND device found". Compared to sig[+0x34]. - * Value stored at format is blocks_per_cau → 0x8D102CC - * is the first geometry word copied from the param page). - * 104 — BUF_Init data bytes first arg) = physical page size - * 105 — BUF_Init meta bytes second arg) = 16 - * 135 — stored at 0x8D0CE2C and unused after GetInfo - */ + * FIL GetInfo (vtable +80,: + * 101 — NAND present / signature +0x34 geometry (WhimoryBoot.c:169,260) + * 0 → "No NAND device found". Compared to sig[+0x34]. + * Value stored at format is blocks_per_cau → 0x8D102CC + * is the first geometry word copied from the param page). + * 104 — BUF_Init data bytes first arg) = physical page size + * 105 — BUF_Init meta bytes second arg) = 16 + * 135 — stored at 0x8D0CE2C and unused after GetInfo + */ g->dev_id = g->blocks_per_cau; g->geom_104 = g->page_size; g->geom_105 = 16; @@ -6711,6 +6744,7 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, u32 addr; bool saved_armed; int ret; + u64 t0, t1, t2; unsigned int s; if (!f || !out) @@ -6729,11 +6763,15 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, addr = fmss_ppn_addr(cau, block, page, 0); mutex_lock(&f->lock); + if (cs_reset_every && f->pages_since_reset >= cs_reset_every) + fmss_nand_reset(f); saved_armed = dma_armed; /* One-shot friendly: re-arm for this kick; disarm after if one_shot. */ dma_armed = true; dma_skip_ingest = true; + t0 = ktime_get_ns(); ret = fmss_dma_page_read_records(f, ce, addr, 0, 4); + t1 = ktime_get_ns(); dma_skip_ingest = false; if (!dma_one_shot) dma_armed = saved_armed; @@ -6771,6 +6809,21 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, s5l8740_nand_meta_decode(out->meta_raw[s], &out->meta[s]); } } + + /* Where the wall time goes; also the last line the host sees over + * /proc/kmsg if the glass dies mid-recover. + */ + t2 = ktime_get_ns(); + cs_ns_kick += t1 - t0; + cs_ns_copy += t2 - t1; + cs_reads_total++; + if (cs_heartbeat && (cs_reads_total & 0x3ff) == 0) + pr_info("s5l8740-nand: cs_phys_read n=%u since_reset=%u " + "kick_us=%llu copy_us=%llu NANDSTAT=%08x\n", + cs_reads_total, f->pages_since_reset, + div_u64(cs_ns_kick, 1000 * cs_reads_total), + div_u64(cs_ns_copy, 1000 * cs_reads_total), + readl(f->base + NANDSTAT)); mutex_unlock(&f->lock); return ret; } @@ -6842,9 +6895,9 @@ int s5l8740_nand_page_read(unsigned int ce, unsigned int cau, if (meta && meta_len) { /* - * Real metadata: always full-page CS span4/rec4112, then - * software-slice. Never invent PIO spare as Whimory meta. - */ + * Real metadata: always full-page CS span4/rec4112, then + * software-slice. Never invent PIO spare as Whimory meta. + */ if (!meta_dma_read || !f->dma_ok) { ret = -EOPNOTSUPP; goto out_restore; diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h index adf2e2e482b31f..007a3f71d3ccf2 100755 --- a/drivers/misc/whimory-s5l8740.h +++ b/drivers/misc/whimory-s5l8740.h @@ -59,8 +59,10 @@ struct whimory_fpart { #define WHIMORY_PAGES_PER_SB 128 #define WHIMORY_DATA_PAGES_PER_SB 127 #define WHIMORY_BTOC_PAGE 127 -/* s_g_vbas_per_sb includes the BTOC page: addr_to_vba(sb, vfl+76-1) - * is the last VBA of page 127 (sub_56B77C). */ +/* + * VBAs per superblock includes the block table of contents page: the last + * VBA a superblock can address falls on page 127. + */ #define WHIMORY_VBAS_PER_SB (WHIMORY_PAGES_PER_SB * \ WHIMORY_VBAS_PER_PAGE) #define WHIMORY_DATA_VBAS_PER_SB (WHIMORY_DATA_PAGES_PER_SB * \ @@ -82,10 +84,16 @@ struct whimory_fpart { #define WHIMORY_CXT_MAX_SB 32 #define WHIMORY_CXT_TAG_BASE 1 #define WHIMORY_CXT_TAG_STATS 2 +#define WHIMORY_CXT_TAG_SB 3 #define WHIMORY_CXT_TAG_L2V 4 +#define WHIMORY_CXT_TAG_USERSEQ 5 +#define WHIMORY_CXT_TAG_READS 6 +/* 0xff is both "nothing written here" and the record-stream terminator. */ #define WHIMORY_CXT_TAG_END 255 #define WHIMORY_CXT_TAG_CLEAN 0xff #define WHIMORY_CXT_CONTIG_SPAN 0xfffffff0u +/* TREE hole sentinel: consumes logical space, maps nothing. */ +#define WHIMORY_CXT_VBA_HOLE 0x7fffffu #define WHIMORY_FIL_META_BYTES 16 /* FIL GetInfo(105); sub_12ED9C */ /* BTOC / META tokens (sub_5688C4). Occupy VBA stream, not user L2V. */ @@ -213,6 +221,13 @@ struct whimory_sb { u64 weave; }; +/* Compact CXT superblock identity; see whimory_cxt_index_build(). */ +struct whimory_cxt_sb_id { + u16 ce; + u16 cau; + u16 block; +}; + struct whimory_sftl { u32 vba_factor_a; u32 vba_factor_b; @@ -244,11 +259,16 @@ struct whimory_sftl { u8 *gc_data; u8 *gc_meta; u32 *btoc_lba[WHIMORY_BTOC_OPEN]; + u32 *btoc_map; /* per-VBA LBA decoded from one BTOC page */ bool cxt_loaded; bool packed_ok; u32 cxt_blocks_seen; u32 cxt_records_seen; u32 cxt_l2v_updates; + u32 cxt_hole_entries; + u32 cxt_xlate_fail; + u32 diff_replayed_sbs; + u32 diff_skipped_sbs; u32 btoc_pages_read; u32 btoc_pages_valid; u32 btoc_entries_seen; @@ -257,6 +277,29 @@ struct whimory_sftl { u32 btoc_token_ffffff00; u32 btoc_token_ffffffff; u32 btoc_holelist_ffff0001; + /* Classified BTOC/open entry counters (recovery observability). */ + u32 btoc_unmap_entries; /* LIST tokens applied or attempted */ + u32 btoc_hole_entries; /* HOLE / erased physical spans */ + u32 btoc_unknown_entries; + u32 open_unmap_entries; + u32 open_skipped_zero; + u32 open_overrides_closed; + u32 open_rejected_stale; + u32 open_unknown_order; + u32 stale_mapping_rejected; + u32 btoc_meta_mismatch; + u32 btoc_meta_confirmed; + u32 btoc_skipped_zero; + u32 btoc_confirm_pages; + u32 btoc_confirm_capped; + u32 btoc_confirm_budget_stop; + u32 string_hit_itunesdb; + u32 string_hit_f00; + u32 string_hit_apps; + u32 string_hit_mp3; + u32 string_hit_m4a; + u32 string_hit_ipod_control; + u32 string_hit_music; u32 open_slots_seen; u32 open_slots_valid_meta; u32 open_l2v_updates; @@ -267,6 +310,17 @@ struct whimory_sftl { u32 btoc_dumps_left; u32 unknown_sbs; u64 claim_weave; + /* 0=none, 1=BTOC/closed, 2=open, 3=CXT, 4=LIST-unmap */ + u8 claim_source; + unsigned long confirm_start_jiffies; + /* Recover-time replay accounting (widen staging / OOM guard). */ + u32 range_budget_stop; + u32 btoc_verified; + u32 map_gen; /* bumped on every map mutation */ + u32 search_cache_hits; + u32 search_cache_misses; + struct whimory_cxt_sb_id cxt_idx[WHIMORY_CXT_MAX_SB]; + unsigned int n_cxt_idx; }; struct whimory_cxt_base { @@ -274,6 +328,17 @@ struct whimory_cxt_base { u64 weave; }; +/* + * One contiguous (lba, span) -> vba mapping decoded from a CXT TREE record. + * Phase 3 keeps these in a candidate map, separate from the live interval + * map, so the CXT decode can be validated without disturbing a working disk. + */ +struct whimory_cxt_extent { + u32 lba; + u32 span; + u32 vba; +}; + struct whimory_fpart_ops { u32 major; u32 (*minor)(struct whimory *w); @@ -321,6 +386,11 @@ struct whimory { struct platform_device *pdev; u32 lba0_vba; u64 cxt_base_weave; + struct whimory_cxt_extent *cxt_ext; /* candidate map (Phase 3) */ + u32 n_cxt_ext; + u32 max_cxt_ext; + u64 cxt_ext_weave; /* base weave it came from */ + u32 cxt_ext_sb; struct whimory_cxt_base cxt[WHIMORY_CXT_MAX_SB]; u32 n_cxt; u32 cxt_next_lba; @@ -332,6 +402,14 @@ struct whimory { bool l2v_ok; bool lba0_ok; bool oracle_used; + bool l2v_defer_pack; /* pack once after replay, not per update */ + unsigned long progress_jiffies; /* rate-limits recover progress */ + /* L2V_Search sequential hint; see whimory_l2v_search(). */ + u32 search_start; + u32 search_len; + u32 search_vba; + u32 search_gen; + bool search_valid; char status[512]; }; diff --git a/include/linux/apple-n31.h b/include/linux/apple-n31.h new file mode 100755 index 00000000000000..7525a6b94a8393 --- /dev/null +++ b/include/linux/apple-n31.h @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Cross-driver interfaces for the iPod nano 7 (N31, Samsung S5L8740). + * + * A handful of symbols on this board legitimately cross driver boundaries: + * the PMIC gates the touch and audio rails, its interrupt arrives on an SoC + * GPIO owned by another driver, and the touch controller reads its calibration + * blob through the NAND FTL. Collecting the declarations here keeps them in + * one place instead of being repeated as bare externs in each .c file, where + * they were already starting to disagree with each other. + * + * Everything below is exported with EXPORT_SYMBOL_GPL() by the driver named + * in the comment. Consumers must cope with the provider being absent, since + * these are separate modules that can load in any order. + */ +#ifndef __LINUX_APPLE_N31_H +#define __LINUX_APPLE_N31_H + +#include + +struct device; +struct dma_chan; + +/* irq-s5l8740-eic.c — route an SoC GPIO to the external interrupt controller. */ +int s5l8740_eic_enable_gpio(unsigned int gpio, unsigned int irq_type); + +/* gpio-s5l8740.c — report a key press to the board input device. */ +void s5l8740_n31_report_key(unsigned int code, int pressed); + +/* gpio-s5l8740.c — raw level of GPIO 86, the PMIC nIRQ line. */ +int s5l8740_n31_din86(void); + +/* + * gpio-d1830.c — set by the PMIC driver so the GPIO edge handler can fold a + * missed EIC edge back into the button poll. NULL until gpio-d1830 probes. + */ +extern void (*d1830_n31_din_nirq_hook)(void); + +/* gpio-d1830.c — apply the audio LDO trim, for the CS42L81 codec. */ +int d1830_audio_rails(void); + +/* gpio-d1830.c — power the touch controller rail, for the Nimbus driver. */ +int d1830_nimbus_rail(bool on); + +/* nand-s5l8740.c — true once the FTL has a usable logical-to-virtual map. */ +bool nand_ftl_present(void); + +/* nand-s5l8740.c — read one 4096-byte logical sector through the FTL. */ +int nand_ftl_read_sector(u64 logical_sector, void *buf); + +/* + * dma-s5l8740-pl080.c — slave-channel lookup. The I2S and IIS2 request lines + * are fixed by the SoC rather than described in the device tree, so consumers + * ask for them by index or by peripheral number instead of going through the + * usual of_dma path. + */ +struct dma_chan *s5l_pl080_request_slave(struct device *consumer, + unsigned int idx); +struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri); +int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en); + +#endif /* __LINUX_APPLE_N31_H */ diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c index 2bd35854ed18c6..39dc31a70b5b06 100755 --- a/sound/soc/apple/s5l8740-i2s.c +++ b/sound/soc/apple/s5l8740-i2s.c @@ -25,6 +25,8 @@ #include #include +#include + #include "n31-audio-rates.h" #define S5L8740_I2S_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) @@ -704,8 +706,6 @@ static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma) readl(i2s->base + I2SCLKCON)); } -int s5l_pl080_peri_snapshot(unsigned int peri, u32 *src, u32 *dst, u32 *en); - static void s5l8740_i2s_dma_watch(struct work_struct *work) { struct s5l8740_i2s *i2s = container_of(work, struct s5l8740_i2s, @@ -1069,10 +1069,6 @@ static ssize_t pad_scan_show(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RO(pad_scan); -struct dma_chan *s5l_pl080_request_slave(struct device *consumer, - unsigned int idx); -struct dma_chan *s5l_pl080_lookup_peri(unsigned int peri); - static struct dma_chan *s5l8740_i2s_tx_get(struct s5l8740_i2s *i2s) { struct dma_chan *chan; From 42b2c6ae87468b044d08dcae4aa40e8a2e7ebd3f Mon Sep 17 00:00:00 2001 From: andrew867 Date: Fri, 28 Aug 2026 10:18:23 -0230 Subject: [PATCH 20/31] N31: audio bring-up, FM/RDS command surface, I2C and IIS2 pad muxing Audio, two independent faults either of which is silence on its own. GPIO 7 was never muxed: sub_BCB60 claims both IIS0 pads together on every TX enable and releases both on disable, but pad 7 was being skipped in the belief it drove the display. It does not -- the panel is driven entirely from the LCDIF, no display code touches GPIO, and RetailOS holds pad 7 at function 3 while playing music with the panel lit. And register 0x0227, the live output gain, sat at its bring-up minimum forever: sub_D2C98 encodes code = dB from -50 to +12 and -50 + (dB+50)/2 below that, floor -63 = -76 dB, which is where the bring-up sequence leaves it and matches the -66 dBFS measured on glass. The mapping added here reproduces sub_D2C98 exactly across its whole input range. The write also sign-extended bit 6 into bit 7 instead of masking to seven bits as sub_400330 does, and the mixer control only reached a software PCM scaler that the DMA path never consulted. Volume and mute now drive the hardware as Headphones Playback Volume/Switch. IIS2 folded into s5l8740-i2s. The two ports share the audio clock gate at CLKCON+0x30 and each wrote it directly, so stopping FM capture idled the clock under music that was still playing; a per-port wanted flag now idles it only when neither wants it. IIS2 also gains the pad group it never had: sub_15DD5C claims GPIO 97/98/119 at function 2 on FM power-on beside programming audio device 2 and kicking RXCOM, and releases them on power-off. Those pins had been described as BCM control lines and given to hci_bcm, which drove the capture bus as GPIOs. I2C pads are muxed for the first time. Nothing in this port ever did, which is invisible on a bus the bootloader leaves configured and fatal on one it does not: i2c1 (PMIC) works while i2c0 (Tristar) reads as noise. sub_5714EE gives the per-bus pairs and sub_1860 fixes the indices. FM is named from BlueTool's own FM_RDS_Command definition rather than raw hex, and gains a read path, RSSI, SNR, RDS group decoding for PS, RT, PI and PTY, and raw register plus arbitrary-HCI passthrough for userspace. ROUTE_PCM was only ever read back and never written, leaving the route to the port IIS2 captures wherever the last owner left it. Also: the touch download follows the stock tail and no longer interleaves 24 KB of diagnostic reads into the HBPP sequence; SPI2 engine setup has a single owner again instead of two drivers programming different dividers; the playback DAI link is nonatomic because its trigger reaches the codec over SPI; screen sleep, LCD power cycling and PMU rail arbitration land, with the rail-held mask stopping the global repair from dropping a rail a driver is using -- the real cause of the white screen. None of this is hardware-validated yet. Co-Authored-By: Claude Opus 5 --- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 28 +- drivers/bluetooth/bcm2078-bt.c | 674 ++++++++++++++- drivers/gpio/gpio-d1830.c | 886 +++++++++++++++++++- drivers/gpio/gpio-s5l8740.c | 13 +- drivers/gpu/drm/tiny/s5l8740.c | 334 ++++++++ drivers/i2c/busses/i2c-s5l8702.c | 130 +++ drivers/input/touchscreen/apple-nimbus.c | 226 ++++- drivers/spi/spi-s5l8702.c | 41 +- drivers/video/backlight/backlight-s5l8740.c | 103 ++- include/linux/apple-n31.h | 54 ++ sound/soc/apple/Kconfig | 20 +- sound/soc/apple/Makefile | 1 - sound/soc/apple/cs42l81-spi.c | 137 ++- sound/soc/apple/nano7-audio.c | 38 +- sound/soc/apple/s5l8740-i2s.c | 477 ++++++++++- sound/soc/apple/s5l8740-iis2.c | 366 -------- 16 files changed, 2988 insertions(+), 540 deletions(-) delete mode 100755 sound/soc/apple/s5l8740-iis2.c diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 28d69fa70a415c..625458b19b2b29 100644 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -158,9 +158,15 @@ bluetooth { compatible = "brcm,bcm4329-bt", "brcm,bcm2078"; max-speed = <115200>; - shutdown-gpios = <&gpio 97 GPIO_ACTIVE_LOW>; - device-wakeup-gpios = <&gpio 98 GPIO_ACTIVE_HIGH>; - host-wakeup-gpios = <&gpio 119 GPIO_ACTIVE_HIGH>; + /* + * No control GPIOs here on purpose. 97/98/119 were + * listed as shutdown/device-wakeup/host-wakeup, but + * sub_15DD5C claims those three at function 2 when FM + * powers on and releases them when it powers off -- + * they are the IIS2 PCM pads. Handing them to hci_bcm + * made it drive the capture bus as GPIOs. The real BT + * control pins are not identified yet. + */ firmware-name = "brcm/BCM2076B1.hcd"; status = "okay"; }; @@ -219,7 +225,9 @@ lcdif: lcdif@38300000 { compatible = "samsung,s5l8740-lcdif"; - reg = <0x38300000 0x10000>; + /* LCDIF, then the clock controller: an LCDIF reset has to + * cycle two gates in it. */ + reg = <0x38300000 0x10000>, <0x3c500000 0x100>; }; /* MMIO backlight only. Never LCDIF CON/PHTIME. U-Boot already @@ -275,6 +283,10 @@ i2c0: i2c@3c600000 { compatible = "samsung,s5l8702-i2c"; + /* sub_5714EE bus 0. Nothing muxed these before, which is + * why this bus reads as noise while i2c1 works: the + * bootloader leaves i2c1 configured and this one not. */ + apple,pads = <4 5>; #address-cells = <1>; #size-cells = <0>; reg = <0x3c600000 0x100>; @@ -291,6 +303,9 @@ i2c1: i2c@3c900000 { compatible = "samsung,s5l8702-i2c"; + /* sub_5714EE bus 1. Already live at boot; re-applying the + * stock mux is a no-op rather than a change. */ + apple,pads = <78 79>; #address-cells = <1>; #size-cells = <0>; reg = <0x3c900000 0x100>; @@ -411,7 +426,10 @@ nimbus: touchscreen@0 { compatible = "apple,nimbus"; reg = <0>; - spi-max-frequency = <1000000>; + /* Stock runs this bus at 12 MHz (divider 2 from 24 MHz). + * The controller programs its own divider, so this is + * documentation rather than a control. */ + spi-max-frequency = <12000000>; enable-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; reset-gpios = <&gpio 39 GPIO_ACTIVE_LOW>; attn-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; diff --git a/drivers/bluetooth/bcm2078-bt.c b/drivers/bluetooth/bcm2078-bt.c index 049f1ee42ff743..3a0aa989cadd9b 100755 --- a/drivers/bluetooth/bcm2078-bt.c +++ b/drivers/bluetooth/bcm2078-bt.c @@ -6,6 +6,16 @@ * (hci0). This driver must NOT ioremap UART1 or of_platform_device_create the * bluetooth child — that steals the port from hci_bcm. * + * GPIO 97/98/119 are not this driver's business either, but for a different + * reason than the UART. They were taken for BCM shutdown / device-wakeup / + * host-wakeup, and this driver muxed them at function 2 under that name. + * sub_15DD5C shows what they really are: FM power-on claims exactly these + * three at function 2 next to programming audio device 2 (0x3D400000) and + * kicking RXCOM, so they are the IIS2 PCM pads. The mux is right, the owner + * was wrong -- s5l8740-i2s claims them with the rest of the capture setup, + * which also means the capture PCM works without the tuner being on. + * gpio_poke=1 restores the old local poking for bring-up comparisons. + * * FM vendor opcode 0xFC15 is sent via __hci_cmd_sync on hci0 when HCI_UP. * Phase 3: thin V4L2 radio (/dev/radio0). Sysfs fm_* remains debug. * Audio is IIS2 ALSA capture → userspace → IIS0 play. No FM→A2DP path. @@ -38,6 +48,90 @@ #define HCI_OP_FC15 0xFC15 +/* + * FM_RDS_Command register map, from the BlueTool hcidef (COMMAND + * "FM_RDS_Command" 0x015). The opcode carries an I2C-style register + * transaction: address, read/write, then either the write data or, for a + * read, the byte count. Register widths below are the read lengths that + * same definition encodes. + */ +#define FM_REG_RDS_SYSTEM 0x00 /* 1: FM_ON | RDS_ON */ +#define FM_REG_FM_CTRL 0x01 /* 1: band, stereo, injection */ +#define FM_REG_RDS_CTRL 0x02 /* 1 */ +#define FM_REG_AUDIO_PAUSE 0x04 /* 1 */ +#define FM_REG_AUDIO_CTRL 0x05 /* 2: mute/route/de-emphasis */ +#define FM_REG_SEARCH_CTRL 0x07 /* 1: direction + RSSI threshold */ +#define FM_REG_SEARCH_CTRL1 0x08 /* 1 */ +#define FM_REG_SEARCH_TUNE 0x09 /* 1: 1 = tune, 2 = search */ +#define FM_REG_FREQ 0x0a /* 2 */ +#define FM_REG_AF_FREQ 0x0c /* 2 */ +#define FM_REG_CARRIER 0x0e /* 1 */ +#define FM_REG_RSSI 0x0f /* 1 */ +#define FM_REG_RDS_MASK 0x10 /* 2 */ +#define FM_REG_RDS_FLAG 0x12 /* 2 */ +#define FM_REG_RDS_WLINE 0x14 /* 1: RDS FIFO watermark */ +#define FM_REG_RDS_BLKB_MATCH 0x16 /* 2 */ +#define FM_REG_RDS_BLKB_MASK 0x18 /* 2 */ +#define FM_REG_RDS_PI_MATCH 0x1a /* 2 */ +#define FM_REG_RDS_PI_MASK 0x1c /* 2 */ +#define FM_REG_RDS_BOOT 0x1e /* 1 */ +#define FM_REG_RDS_TEST 0x1f /* 1 */ +#define FM_REG_SLAVE_CONFIG 0x29 +#define FM_REG_ROUTE_PCM 0x4d /* 1: tuner audio onto the PCM port */ +#define FM_REG_RDS_DATA 0x80 /* n: RDS FIFO, caller-sized */ +#define FM_REG_BEST_TUNE 0x90 /* 1 */ +#define FM_REG_SMUTE_V3 0xda /* 5 */ +#define FM_REG_FEATURES 0xdb /* 4 */ +#define FM_REG_PRESCAN_QUALITY 0xde /* 1 */ +#define FM_REG_SNR 0xdf /* 1 */ +#define FM_REG_EXTRA_AUDIO 0xf5 /* 1: FMRX_2_AFIFO_ENABLE */ +#define FM_REG_ANT_MATCHING 0xf6 /* 1 */ +#define FM_REG_VOLUME_CTRL 0xf8 /* 2 */ +#define FM_REG_BLEND_SMUTE 0xf9 /* 8: stereo blend + soft mute */ +#define FM_REG_ANT_SELECT 0xfa /* 1 */ +#define FM_REG_SEARCH_BOUND 0xfb /* 4: band edges */ +#define FM_REG_SEARCH_METHOD 0xfc /* 1 */ +#define FM_REG_SEARCH_STEP 0xfd /* 2 */ +#define FM_REG_PRESET_MAX 0xfe /* 1 */ +#define FM_REG_PRESET_CHAN 0xff /* n */ + +#define FM_MODE_WRITE 0 +#define FM_MODE_READ 1 + +/* SEARCH_TUNE_MODE values. */ +#define FM_TUNE_MODE_IDLE 0 +#define FM_TUNE_MODE_PRESET 1 +#define FM_TUNE_MODE_SEARCH 2 + +/* RDS_SYSTEM bits. */ +#define FM_SYSTEM_FM_ON 0x01 +#define FM_SYSTEM_RDS_ON 0x02 + +/* FM_CTRL bits. Band select 0 = 87.5-108 MHz, 1 = 76-90 MHz. */ +#define FM_CTRL_BAND_JAPAN 0x01 +#define FM_CTRL_STEREO_AUTO 0x02 +#define FM_CTRL_STEREO_MANUAL 0x04 +#define FM_CTRL_STEREO_BLEND 0x08 +#define FM_CTRL_INJECTION 0x10 + +/* AUDIO_CTRL bits 6:0; 15:7 is the audio bandwidth select. */ +#define FM_AUDIO_RF_MUTE 0x0001 +#define FM_AUDIO_MANUAL_MUTE 0x0002 +#define FM_AUDIO_Z_MUTE_LEFT 0x0004 +#define FM_AUDIO_Z_MUTE_RIGHT 0x0008 +#define FM_AUDIO_ROUTE_DAC 0x0010 +#define FM_AUDIO_ROUTE_I2S 0x0020 +#define FM_AUDIO_DEEMPH_75US 0x0040 + +/* Largest RDS read we will ask for in one go. */ +#define FM_RDS_READ_MAX 60 +#define FM_RDS_TEXT_MAX 64 +#define FM_RDS_PS_MAX 8 + +/* Bounds for the raw HCI passthrough. */ +#define BCM_HCI_PARAM_MAX 255 +#define BCM_HCI_RSP_MAX 64 + /* V4L2_TUNER_CAP_LOW: 62.5 Hz units → kHz * 16 */ #define BCM_FM_FREQ_TO_V4L(khz) ((khz) * 16u) #define BCM_FM_V4L_TO_FREQ(f) ((f) / 16u) @@ -46,17 +140,30 @@ #define BCM_FM_KHZ_DEFAULT 94700u /* Canada test station */ /* - * FC15 reg 0x05 audio ctrl (BCM4325/2048 family, via 0xFC15 on 2078): - * 0x0001 = RetailOS audio route (DD334) - * 0x0040 = 75 µs de-emphasis (Canada/US; 50 µs = clear bit) - * 0x0020 = I2S PCM route (IIS2 on N31) + * RF_MUTE squelches the output as C/N falls, de-emphasis is 75 us for + * North America (clear the bit for the 50 us regions), and ROUTE_I2S is + * what puts tuner audio on the PCM port that IIS2 captures. + */ +#define BCM_FM_AUDIO_CTRL0_DEFAULT (FM_AUDIO_RF_MUTE | \ + FM_AUDIO_DEEMPH_75US | \ + FM_AUDIO_ROUTE_I2S) + +/* + * Off by default: these pins belong to hci_bcm. See the file header. */ -#define BCM_FM_AUDIO_ROUTE_ORACLE 0x0001u -#define BCM_FM_AUDIO_DEMPH_75US 0x0040u -#define BCM_FM_AUDIO_ROUTE_I2S 0x0020u -#define BCM_FM_AUDIO_CTRL0_DEFAULT (BCM_FM_AUDIO_ROUTE_ORACLE | \ - BCM_FM_AUDIO_DEMPH_75US | \ - BCM_FM_AUDIO_ROUTE_I2S) +static bool gpio_poke; +module_param(gpio_poke, bool, 0644); +MODULE_PARM_DESC(gpio_poke, + "Drive the BCM control pins directly (default N; hci_bcm owns them)"); + +static u8 rds_wline = 12; +module_param(rds_wline, byte, 0644); +MODULE_PARM_DESC(rds_wline, "RDS FIFO watermark in blocks (default 12)"); + +static bool fm_route_pcm = true; +module_param(fm_route_pcm, bool, 0644); +MODULE_PARM_DESC(fm_route_pcm, + "Write ROUTE_PCM on FM power-on so IIS2 receives audio"); static u16 fm_audio_ctrl0 = BCM_FM_AUDIO_CTRL0_DEFAULT; module_param(fm_audio_ctrl0, ushort, 0644); @@ -69,7 +176,24 @@ struct bcm2078_bt { void __iomem *gpiocmd; bool powered; bool fm_on; + bool rds_on; unsigned int fm_khz; + /* Last decoded RDS. Guarded by lock along with everything else. */ + char rds_ps[FM_RDS_PS_MAX + 1]; + char rds_rt[FM_RDS_TEXT_MAX + 1]; + char rds_ps_build[FM_RDS_PS_MAX]; + char rds_rt_build[FM_RDS_TEXT_MAX]; + u16 rds_pi; + u8 rds_pty; + u8 rds_rt_ab; + unsigned int rds_groups; + u8 reg_addr; + u8 reg_len; + u8 reg_data[FM_RDS_READ_MAX]; + u16 hci_opcode; + bool hci_valid; + u8 hci_rsp_len; + u8 hci_rsp[BCM_HCI_RSP_MAX]; struct mutex lock; struct v4l2_device v4l2_dev; struct video_device vdev; @@ -163,46 +287,287 @@ static int bcm_fm_w16(struct bcm2078_bt *bt, u8 reg, u16 val) return bcm_fc15(bt, p, 4); } +/* + * A read transaction is address + mode + byte count; the count is fixed + * per register except for the RDS FIFO, where the caller chooses it. + * + * The command-complete parameters begin with the status byte. Some + * firmware revisions echo the address and mode ahead of the payload and + * some do not, so accept either rather than assuming: the payload is + * whatever trails a 1- or 3-byte header of the expected total length. + */ +static int bcm_fm_read(struct bcm2078_bt *bt, u8 reg, u8 *out, u8 len) +{ + u8 req[3] = { reg, FM_MODE_READ, len }; + struct hci_dev *hdev; + struct sk_buff *skb; + unsigned int hdr; + int ret = 0; + + if (!len) + return -EINVAL; + + hdev = hci_dev_get(0); + if (!hdev) + return -ENODEV; + if (!test_bit(HCI_UP, &hdev->flags)) { + hci_dev_put(hdev); + return -ENETDOWN; + } + + skb = __hci_cmd_sync(hdev, HCI_OP_FC15, sizeof(req), req, + HCI_CMD_TIMEOUT); + hci_dev_put(hdev); + if (IS_ERR(skb)) + return PTR_ERR(skb); + + if (skb->len == 1u + len) + hdr = 1; + else if (skb->len == 3u + len) + hdr = 3; + else { + dev_warn_ratelimited(bt->dev, + "FC15 read reg 0x%02x: %u bytes for len %u: %*ph\n", + reg, skb->len, len, + min_t(int, skb->len, 16), skb->data); + ret = -EPROTO; + goto out; + } + if (skb->data[0]) { + dev_dbg(bt->dev, "FC15 read reg 0x%02x status 0x%02x\n", + reg, skb->data[0]); + ret = -EIO; + goto out; + } + memcpy(out, skb->data + hdr, len); +out: + kfree_skb(skb); + return ret; +} + +static int bcm_fm_r8(struct bcm2078_bt *bt, u8 reg, u8 *val) +{ + return bcm_fm_read(bt, reg, val, 1); +} + +static int bcm_fm_r16(struct bcm2078_bt *bt, u8 reg, u16 *val) +{ + u8 b[2]; + int ret = bcm_fm_read(bt, reg, b, 2); + + if (!ret) + *val = (u16)b[0] | ((u16)b[1] << 8); + return ret; +} + +/* ---------- RDS ---------- */ + +/* + * The FIFO hands back RDS blocks as 3-byte records: two data bytes and a + * status byte whose low bits carry the block offset (A, B, C, C', D) and + * whose upper bits flag correction/error. Four blocks make a group, and + * only the well-formed ones are worth decoding. + */ +#define FM_RDS_REC_LEN 3 +#define FM_RDS_BLK_MASK 0x07 +#define FM_RDS_BLK_A 0 +#define FM_RDS_BLK_B 1 +#define FM_RDS_BLK_C 2 +#define FM_RDS_BLK_CP 3 +#define FM_RDS_BLK_D 4 +#define FM_RDS_ERR_MASK 0x80 + +/* Printable-ASCII guard: RDS pads with 0x20 and terminates RT with 0x0D. */ +static char bcm_rds_char(u8 c) +{ + return (c >= 0x20 && c < 0x7f) ? (char)c : ' '; +} + +static void bcm_rds_group(struct bcm2078_bt *bt, const u16 blk[4]) +{ + unsigned int type = blk[1] >> 12; + unsigned int ver = (blk[1] >> 11) & 1; + unsigned int i; + + bt->rds_pi = blk[0]; + bt->rds_pty = (blk[1] >> 5) & 0x1f; + bt->rds_groups++; + + if (type == 0) { + /* 0A/0B: two Program Service characters at offset 2*seg. */ + unsigned int seg = blk[1] & 0x03; + + bt->rds_ps_build[seg * 2] = bcm_rds_char(blk[3] >> 8); + bt->rds_ps_build[seg * 2 + 1] = bcm_rds_char(blk[3] & 0xff); + if (seg == 3) { + memcpy(bt->rds_ps, bt->rds_ps_build, FM_RDS_PS_MAX); + bt->rds_ps[FM_RDS_PS_MAX] = 0; + } + } else if (type == 2) { + /* + * 2A carries four RadioText characters per group, 2B two. The + * A/B flag toggles when the station starts a new message, so + * clear the buffer rather than blending two texts together. + */ + unsigned int seg = blk[1] & 0x0f; + unsigned int ab = (blk[1] >> 4) & 1; + unsigned int n = ver ? 2 : 4; + unsigned int base = seg * n; + + if (ab != bt->rds_rt_ab) { + bt->rds_rt_ab = ab; + memset(bt->rds_rt_build, ' ', FM_RDS_TEXT_MAX); + } + if (base + n <= FM_RDS_TEXT_MAX) { + if (ver) { + bt->rds_rt_build[base] = bcm_rds_char(blk[3] >> 8); + bt->rds_rt_build[base + 1] = + bcm_rds_char(blk[3] & 0xff); + } else { + bt->rds_rt_build[base] = bcm_rds_char(blk[2] >> 8); + bt->rds_rt_build[base + 1] = + bcm_rds_char(blk[2] & 0xff); + bt->rds_rt_build[base + 2] = + bcm_rds_char(blk[3] >> 8); + bt->rds_rt_build[base + 3] = + bcm_rds_char(blk[3] & 0xff); + } + memcpy(bt->rds_rt, bt->rds_rt_build, FM_RDS_TEXT_MAX); + bt->rds_rt[FM_RDS_TEXT_MAX] = 0; + for (i = FM_RDS_TEXT_MAX; i > 0; i--) { + if (bt->rds_rt[i - 1] != ' ') + break; + bt->rds_rt[i - 1] = 0; + } + } + } +} + +/* + * Drain the FIFO once and feed whole groups to the decoder. Blocks are + * accumulated by their offset code so a partial group at either end of + * the read is discarded rather than shifting everything that follows. + */ +static int bcm_rds_poll(struct bcm2078_bt *bt) +{ + u8 buf[FM_RDS_READ_MAX]; + u16 blk[4]; + bool have[4] = { false, false, false, false }; + unsigned int i; + int ret; + + ret = bcm_fm_read(bt, FM_REG_RDS_DATA, buf, sizeof(buf)); + if (ret) + return ret; + + for (i = 0; i + FM_RDS_REC_LEN <= sizeof(buf); i += FM_RDS_REC_LEN) { + u8 st = buf[i + 2]; + unsigned int off = st & FM_RDS_BLK_MASK; + u16 val = ((u16)buf[i] << 8) | buf[i + 1]; + + if (st & FM_RDS_ERR_MASK) + continue; + if (off == FM_RDS_BLK_CP) + off = FM_RDS_BLK_C; + if (off > FM_RDS_BLK_D) + continue; + if (off == FM_RDS_BLK_A) { + memset(have, 0, sizeof(have)); + blk[0] = val; + have[0] = true; + continue; + } + if (!have[0]) + continue; + blk[off > FM_RDS_BLK_C ? 3 : off] = val; + have[off > FM_RDS_BLK_C ? 3 : off] = true; + if (have[0] && have[1] && have[2] && have[3]) { + bcm_rds_group(bt, blk); + memset(have, 0, sizeof(have)); + } + } + return 0; +} + +static int bcm_rds_enable(struct bcm2078_bt *bt, bool on) +{ + int ret; + + ret = bcm_fm_w8(bt, FM_REG_RDS_SYSTEM, + FM_SYSTEM_FM_ON | (on ? FM_SYSTEM_RDS_ON : 0)); + if (ret) + return ret; + if (on) { + /* Interrupt once the FIFO holds this many blocks. */ + ret = bcm_fm_w8(bt, FM_REG_RDS_WLINE, rds_wline); + if (ret) + return ret; + } + bt->rds_on = on; + dev_info(bt->dev, "RDS %s\n", on ? "on" : "off"); + return 0; +} + static int bcm_fm_power_on(struct bcm2078_bt *bt) { int ret; - ret = bcm_fm_w8(bt, 0x00, 0x03); + /* Tuner and RDS decoder on together; RDS costs nothing when idle. */ + ret = bcm_fm_w8(bt, FM_REG_RDS_SYSTEM, + FM_SYSTEM_FM_ON | FM_SYSTEM_RDS_ON); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x14, 0x0c); + ret = bcm_fm_w8(bt, FM_REG_RDS_WLINE, rds_wline); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x02, 0x02); + ret = bcm_fm_w8(bt, FM_REG_RDS_CTRL, 0x02); if (ret) return ret; - ret = bcm_fm_w16(bt, 0x05, fm_audio_ctrl0); + ret = bcm_fm_w16(bt, FM_REG_AUDIO_CTRL, fm_audio_ctrl0); if (ret) return ret; - { - u8 rd[3] = { 0x4d, 0x01, 0x01 }; - bcm_fc15(bt, rd, 3); + /* + * ROUTE_PCM is what actually puts tuner audio on the port IIS2 + * captures. The old code only read this register back and never + * wrote it, leaving the route wherever the last owner left it. + */ + if (fm_route_pcm) { + ret = bcm_fm_w8(bt, FM_REG_ROUTE_PCM, 0x01); + if (ret) + return ret; } + + /* Stereo blend and soft-mute curve; 8 data bytes per the register map. */ { u8 p[11] = { - 0xf9, 0x00, 0x21, 0x00, 0x00, 0x00, + FM_REG_BLEND_SMUTE, FM_MODE_WRITE, + 0x21, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00 }; - ret = bcm_fc15(bt, p, 11); + ret = bcm_fc15(bt, p, sizeof(p)); } bt->fm_on = !ret; - dev_info(bt->dev, "FM power ON (0xFC15 via hci0) audio_ctrl=0x%04x%s\n", - fm_audio_ctrl0, ret ? " FAIL" : ""); + bt->rds_on = !ret; + memset(bt->rds_ps, 0, sizeof(bt->rds_ps)); + memset(bt->rds_rt, 0, sizeof(bt->rds_rt)); + memset(bt->rds_ps_build, ' ', sizeof(bt->rds_ps_build)); + memset(bt->rds_rt_build, ' ', sizeof(bt->rds_rt_build)); + bt->rds_groups = 0; + dev_info(bt->dev, + "FM power ON audio_ctrl=0x%04x route_pcm=%d wline=%u%s\n", + fm_audio_ctrl0, fm_route_pcm, rds_wline, + ret ? " FAIL" : ""); return ret; } static int bcm_fm_power_off(struct bcm2078_bt *bt) { - int ret = bcm_fm_w8(bt, 0x00, 0x00); + int ret = bcm_fm_w8(bt, FM_REG_RDS_SYSTEM, 0x00); bt->fm_on = false; + bt->rds_on = false; dev_info(bt->dev, "FM power OFF%s\n", ret ? " FAIL" : ""); return ret; } @@ -217,17 +582,21 @@ static int bcm_fm_tune_khz(struct bcm2078_bt *bt, unsigned int khz) if (ret) return ret; } - ret = bcm_fm_w8(bt, 0x01, khz >= 87500 ? 2 : 3); + /* Band select is inverted: the Japan band is the one with the bit. */ + ret = bcm_fm_w8(bt, FM_REG_FM_CTRL, + FM_CTRL_STEREO_AUTO | + (khz >= 87500 ? 0 : FM_CTRL_BAND_JAPAN)); if (ret) return ret; - ret = bcm_fm_w16(bt, 0x10, 0x1203); + /* Arm the tune/search-complete and RDS flags we care about. */ + ret = bcm_fm_w16(bt, FM_REG_RDS_MASK, 0x1203); if (ret) return ret; enc = (u16)((khz + 1536) & 0xffff); - ret = bcm_fm_w16(bt, 0x0a, enc); + ret = bcm_fm_w16(bt, FM_REG_FREQ, enc); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x09, 0x01); + ret = bcm_fm_w8(bt, FM_REG_SEARCH_TUNE, FM_TUNE_MODE_PRESET); if (!ret) bt->fm_khz = khz; dev_info(bt->dev, "FM tune %u kHz enc=%04x%s\n", @@ -240,19 +609,19 @@ static int bcm_fm_seek(struct bcm2078_bt *bt, int up, u8 rssi) u8 flags = 0x70 | (up ? 0x80 : 0); int ret; - ret = bcm_fm_w8(bt, 0x07, flags); + ret = bcm_fm_w8(bt, FM_REG_SEARCH_CTRL, flags); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x08, rssi ? rssi : 33); + ret = bcm_fm_w8(bt, FM_REG_SEARCH_CTRL1, rssi ? rssi : 33); if (ret) return ret; - ret = bcm_fm_w8(bt, 0xde, 0x01); + ret = bcm_fm_w8(bt, FM_REG_PRESCAN_QUALITY, 0x01); if (ret) return ret; - ret = bcm_fm_w8(bt, 0xfc, 0x00); + ret = bcm_fm_w8(bt, FM_REG_SEARCH_METHOD, 0x00); if (ret) return ret; - ret = bcm_fm_w8(bt, 0x09, 0x02); + ret = bcm_fm_w8(bt, FM_REG_SEARCH_TUNE, FM_TUNE_MODE_SEARCH); dev_info(bt->dev, "FM seek %s rssi=%u%s\n", up ? "up" : "down", rssi ? rssi : 33, ret ? " FAIL" : ""); return ret; @@ -260,18 +629,25 @@ static int bcm_fm_seek(struct bcm2078_bt *bt, int up, u8 rssi) static int bcm_power_on(struct bcm2078_bt *bt) { + bt->powered = true; + if (!gpio_poke) { + dev_dbg(bt->dev, + "control pins left to hci_bcm (gpio_poke=0)\n"); + return 0; + } bcm_power_pins_on(bt); msleep(150); - bt->powered = true; dev_info(bt->dev, - "RetailOS mode-2 GPIOs on — hci_bcm owns UART1/hci0\n"); + "RetailOS mode-2 GPIOs forced on (gpio_poke=1)\n"); return 0; } static void bcm_power_off(struct bcm2078_bt *bt) { - bcm_power_pins_off(bt); bt->powered = false; + if (!gpio_poke) + return; + bcm_power_pins_off(bt); } /* ---------- V4L2 radio (tuner control only; PCM is ALSA IIS2) ---------- */ @@ -291,6 +667,7 @@ static int bcm_radio_querycap(struct file *file, void *fh, static int bcm_radio_g_tuner(struct file *file, void *fh, struct v4l2_tuner *t) { struct bcm2078_bt *bt = video_drvdata(file); + u8 rssi = 0; if (t->index > 0) return -EINVAL; @@ -302,8 +679,20 @@ static int bcm_radio_g_tuner(struct file *file, void *fh, struct v4l2_tuner *t) t->rangehigh = BCM_FM_FREQ_TO_V4L(BCM_FM_KHZ_MAX); t->rxsubchans = V4L2_TUNER_SUB_STEREO; t->audmode = V4L2_TUNER_MODE_STEREO; - t->signal = bt->fm_on ? 0xffff : 0; t->afc = 0; + + /* + * Report what the tuner actually sees. RSSI is a single byte on the + * chip's own scale, spread over the 16-bit field V4L2 expects; a + * failed read is reported as no signal rather than as an error, so + * that polling a powered-down tuner stays harmless. + */ + mutex_lock(&bt->lock); + if (bt->fm_on && !bcm_fm_r8(bt, FM_REG_RSSI, &rssi)) + t->signal = (u16)rssi * 257; + else + t->signal = 0; + mutex_unlock(&bt->lock); return 0; } @@ -571,14 +960,208 @@ static ssize_t patchram_info_show(struct device *dev, struct device_attribute *a "hci=hci_bcm/serdev on uart1 (not this companion)\n" "hcd=/lib/firmware/brcm/BCM2076B1.hcd\n" "bringup=/bin/n31-bt-up → hci0 + HCIDEVUP\n" - "gpio=RetailOS mode-2 on 0x61/0x62/0x77 via power_on\n" - "radio=/dev/radio0 V4L2 (prefer); sysfs fm_* = debug\n" - "audio=IIS2 capture → arecord|aplay IIS0 (headphones required)\n" + "gpio=97/98/119 owned by hci_bcm (gpio_poke=1 to override)\n" + "radio=/dev/radio0 V4L2 (tune/seek/signal); sysfs fm_* = debug\n" + "metrics=fm_rssi, fm_snr\n" + "rds=fm_rds (read drains the FIFO; PS/RT/PI/PTY decoded)\n" + "raw=fm_reg \"r|w \" per BlueTool FM_RDS_Command map\n" + "hci=hci_cmd \" [bytes]\" for any command incl. vendor\n" + "audio_bcm=FC15 reg0x05 bit0x20 routes tuner audio to the PCM port\n" + "audio_soc=IIS2 is only clocked while the capture PCM is open:\n" + " arecord -D hw:0,1 -f S16_LE -r 44100 -c 2 | aplay -D hw:0,0 -\n" "fm_default=94700 kHz deemph=75us (Canada)\n" "no_fm_a2dp=1 (local speakers/HP only)\n"); } static DEVICE_ATTR_RO(patchram_info); +/* Signal quality. Both are one byte; RSSI is the tuner's own scale. */ +static ssize_t fm_rssi_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + u8 v = 0; + int ret; + + mutex_lock(&bt->lock); + ret = bcm_fm_r8(bt, FM_REG_RSSI, &v); + mutex_unlock(&bt->lock); + return ret ? ret : sysfs_emit(buf, "%u\n", v); +} +static DEVICE_ATTR_RO(fm_rssi); + +static ssize_t fm_snr_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + u8 v = 0; + int ret; + + mutex_lock(&bt->lock); + ret = bcm_fm_r8(bt, FM_REG_SNR, &v); + mutex_unlock(&bt->lock); + return ret ? ret : sysfs_emit(buf, "%u\n", v); +} +static DEVICE_ATTR_RO(fm_snr); + +/* Drain the FIFO, then report whatever has been decoded so far. */ +static ssize_t fm_rds_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + u16 flag = 0; + int ret; + + mutex_lock(&bt->lock); + ret = bt->fm_on ? bcm_rds_poll(bt) : -ENODEV; + if (bt->fm_on && bcm_fm_r16(bt, FM_REG_RDS_FLAG, &flag)) + flag = 0; + ret = sysfs_emit(buf, + "on=%d poll=%d groups=%u flag=0x%04x\n" + "pi=0x%04x pty=%u\n" + "ps=%s\n" + "rt=%s\n", + bt->rds_on, ret, bt->rds_groups, flag, + bt->rds_pi, bt->rds_pty, + bt->rds_ps, bt->rds_rt); + mutex_unlock(&bt->lock); + return ret; +} + +/* "1" / "0" toggles the decoder without disturbing the tuner. */ +static ssize_t fm_rds_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + unsigned int on; + int ret; + + if (kstrtouint(buf, 0, &on)) + return -EINVAL; + mutex_lock(&bt->lock); + ret = bcm_rds_enable(bt, on); + mutex_unlock(&bt->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(fm_rds); + +/* + * Raw register access, for walking the map in the BlueTool hcidef without + * a driver change: + * echo "w 05 0061" > fm_reg write (1 or 2 bytes by value width) + * echo "r 0f 1" > fm_reg read, result appears on the next read + */ +static ssize_t fm_reg_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + + if (!bt->reg_len) + return sysfs_emit(buf, "no read pending\n"); + return sysfs_emit(buf, "0x%02x: %*ph\n", + bt->reg_addr, bt->reg_len, bt->reg_data); +} + +static ssize_t fm_reg_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + unsigned int reg, val; + char op; + int ret, n; + + n = sscanf(buf, " %c %x %x", &op, ®, &val); + if (n < 3 || reg > 0xff) + return -EINVAL; + + mutex_lock(&bt->lock); + if (op == 'r' || op == 'R') { + if (!val || val > sizeof(bt->reg_data)) + ret = -EINVAL; + else + ret = bcm_fm_read(bt, reg, bt->reg_data, val); + bt->reg_addr = reg; + bt->reg_len = ret ? 0 : val; + } else if (op == 'w' || op == 'W') { + ret = (val > 0xff) ? bcm_fm_w16(bt, reg, val) : + bcm_fm_w8(bt, reg, val); + } else { + ret = -EINVAL; + } + mutex_unlock(&bt->lock); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(fm_reg); + +/* + * Arbitrary HCI command, so the whole vendor surface is reachable from a + * shell without BlueZ tools present: + * echo "fc15 0f 01 01" > hci_cmd opcode then parameter bytes + * cat hci_cmd status and command-complete payload + * The kernel owns hci0; this only borrows it for one synchronous command. + */ +static ssize_t hci_cmd_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + + if (!bt->hci_valid) + return sysfs_emit(buf, "no command run\n"); + if (!bt->hci_rsp_len) + return sysfs_emit(buf, "opcode 0x%04x: no payload\n", + bt->hci_opcode); + return sysfs_emit(buf, "opcode 0x%04x: %*ph\n", + bt->hci_opcode, bt->hci_rsp_len, bt->hci_rsp); +} + +static ssize_t hci_cmd_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + struct bcm2078_bt *bt = dev_get_drvdata(dev); + u8 params[BCM_HCI_PARAM_MAX]; + unsigned int opcode, v, plen = 0; + struct hci_dev *hdev; + struct sk_buff *skb; + const char *p = buf; + int used, ret = 0; + + if (sscanf(p, " %x%n", &opcode, &used) != 1 || opcode > 0xffff) + return -EINVAL; + p += used; + while (plen < sizeof(params) && sscanf(p, " %x%n", &v, &used) == 1) { + if (v > 0xff) + return -EINVAL; + params[plen++] = (u8)v; + p += used; + } + + hdev = hci_dev_get(0); + if (!hdev) + return -ENODEV; + if (!test_bit(HCI_UP, &hdev->flags)) { + hci_dev_put(hdev); + return -ENETDOWN; + } + + mutex_lock(&bt->lock); + skb = __hci_cmd_sync(hdev, opcode, plen, plen ? params : NULL, + HCI_CMD_TIMEOUT); + bt->hci_opcode = opcode; + bt->hci_valid = true; + if (IS_ERR(skb)) { + bt->hci_rsp_len = 0; + ret = PTR_ERR(skb); + } else { + bt->hci_rsp_len = min_t(unsigned int, skb->len, + sizeof(bt->hci_rsp)); + memcpy(bt->hci_rsp, skb->data, bt->hci_rsp_len); + kfree_skb(skb); + } + mutex_unlock(&bt->lock); + hci_dev_put(hdev); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(hci_cmd); + static struct attribute *bcm_attrs[] = { &dev_attr_power_on.attr, &dev_attr_patchram.attr, @@ -586,6 +1169,11 @@ static struct attribute *bcm_attrs[] = { &dev_attr_fm_power.attr, &dev_attr_fm_tune.attr, &dev_attr_fm_seek.attr, + &dev_attr_fm_rssi.attr, + &dev_attr_fm_snr.attr, + &dev_attr_fm_rds.attr, + &dev_attr_fm_reg.attr, + &dev_attr_hci_cmd.attr, NULL, }; ATTRIBUTE_GROUPS(bcm); @@ -632,7 +1220,15 @@ static void bcm2078_remove(struct platform_device *pdev) bcm_radio_unregister(bt); sysfs_remove_groups(&pdev->dev.kobj, bcm_groups); - bcm_power_off(bt); + /* + * Turn the tuner off, but leave the control pins alone unless this + * driver was the one driving them -- otherwise unloading it drops + * REG_ON and takes hci0 down with it. + */ + if (bt->fm_on) + bcm_fm_power_off(bt); + if (gpio_poke) + bcm_power_off(bt); } static const struct of_device_id bcm2078_of_match[] = { diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index ceb6d9f0251fc9..2fc27e67056f4f 100755 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -21,6 +21,7 @@ * * Copyright (C) 2026 Vencislav Atanasov */ +#include #include #include #include @@ -32,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -407,6 +409,11 @@ static irqreturn_t d1830_irq_thread(int irq, void *data) return IRQ_HANDLED; } +/* Screen-sleep and power-button policy; defined with the PMU rail code. */ +static void n31_power_button(bool pressed); +static void n31_power_button_poll(void); +static void n31_home_button(bool pressed); + static void d1830_key_active_low(struct d1830_gpio *gpio_dev, unsigned int code, u8 now_bit, u8 *last, const char *name) { @@ -423,7 +430,8 @@ static void d1830_key_active_low(struct d1830_gpio *gpio_dev, unsigned int code, dev_dbg(&gpio_dev->client->dev, "n31-btn %s %s (bit=%u, 0=pressed OSOS)\n", name, pressed ? "PRESS" : "release", now_bit); - s5l8740_n31_report_key(code, pressed); + if (code == KEY_HOMEPAGE) + n31_home_button(pressed); if (gpio_dev->input) { input_report_key(gpio_dev->input, code, pressed); input_sync(gpio_dev->input); @@ -503,7 +511,7 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) d1830_vinfo(&client->dev, "n31-btn SLEEP PRESS r7=0x%02x (bit5 1->0)\n", r7); - s5l8740_n31_report_key(KEY_POWER, 1); + n31_power_button(true); if (gpio_dev->input) { input_report_key(gpio_dev->input, KEY_POWER, 1); input_sync(gpio_dev->input); @@ -515,6 +523,7 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) * userspace is not watching. One noisy I2C byte must not * hibernate. */ + n31_power_button_poll(); if (gpio_dev->sleep_hold < 5) gpio_dev->sleep_hold++; if (gpio_dev->sleep_hold == 5) { @@ -535,7 +544,7 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) if (!gpio_dev->last_sleep) { dev_dbg(&client->dev, "n31-btn SLEEP release r7=0x%02x\n", r7); - s5l8740_n31_report_key(KEY_POWER, 0); + n31_power_button(false); if (gpio_dev->input) { input_report_key(gpio_dev->input, KEY_POWER, 0); input_sync(gpio_dev->input); @@ -874,6 +883,862 @@ static int d1830_rmw(struct i2c_client *client, u8 reg, u8 clear, u8 set) return i2c_smbus_write_byte_data(client, reg, newv); } +/* ------------------------------------------------------------------ */ +/* N31 PMU register / rail decode */ +/* */ +/* Read-only by default. The point is to make the boot rail model */ +/* visible so audio and display power can be debugged from evidence */ +/* rather than by poking registers and watching what breaks. Names are */ +/* driver-local: they describe what the boot sequence does with each */ +/* register, and carry no claim about which peripheral owns a rail */ +/* until a snapshot diff proves it. */ +/* ------------------------------------------------------------------ */ + +#define N31_PMU_VARIANT "n31_d1830_pmu_v1" +#define N31_PMU_REG_MAX 0x88 /* dump 0x00..0x87 */ +#define N31_PMU_VSEL_MASK 0x1f +#define N31_PMU_NO_VSEL 0xff + +static char *pmu_variant = N31_PMU_VARIANT; +module_param(pmu_variant, charp, 0444); +MODULE_PARM_DESC(pmu_variant, "PMU register-map variant this driver decodes"); + +static bool allow_pmu_writes; +module_param(allow_pmu_writes, bool, 0644); +MODULE_PARM_DESC(allow_pmu_writes, + "Permit PMU rail writes at all (default N — decode only)"); + +static bool apply_boot_rails; +module_param(apply_boot_rails, bool, 0644); +MODULE_PARM_DESC(apply_boot_rails, + "Replay the boot rail sequence exactly (needs allow_pmu_writes)"); + +static bool audio_rail_test; +module_param(audio_rail_test, bool, 0644); +MODULE_PARM_DESC(audio_rail_test, + "Arm the analog rail experiment (needs allow_pmu_writes)"); + +static bool restore_after_test = true; +module_param(restore_after_test, bool, 0644); +MODULE_PARM_DESC(restore_after_test, + "Restore the pre-test register values afterwards (default Y)"); + +struct n31_pmu_regname { + u8 reg; + const char *name; +}; + +static const struct n31_pmu_regname n31_pmu_names[] = { + { 0x00, "PMU_CHIP_ID" }, + { 0x01, "PMU_EVENT_A" }, + { 0x02, "PMU_EVENT_B" }, + { 0x03, "PMU_EVENT_C" }, + { 0x04, "PMU_EVENT_D" }, + { 0x05, "PMU_STATUS_A" }, + { 0x06, "PMU_STATUS_B" }, + { 0x07, "PMU_STATUS_C" }, + { 0x08, "PMU_STATUS_D" }, + { 0x09, "PMU_IRQ_MASK_A" }, + { 0x0a, "PMU_IRQ_MASK_B" }, + { 0x0b, "PMU_IRQ_MASK_C" }, + { 0x0c, "PMU_IRQ_MASK_D" }, + { 0x0d, "PMU_SYS_CONTROL" }, + { 0x0e, "PMU_FAULT_LOG" }, + { 0x10, "PMU_ACTIVE_1" }, + { 0x11, "PMU_ACTIVE_2" }, + { 0x12, "PMU_STANDBY_1" }, + { 0x13, "PMU_HIBERNATE_1" }, + { 0x14, "PMU_BUCK_1_CFG" }, + { 0x15, "PMU_BUCK_2_CFG" }, + { 0x16, "PMU_SPECIAL_CFG" }, + { 0x17, "PMU_LDO_1_CFG" }, + { 0x18, "PMU_LDO_2_CFG" }, + { 0x19, "PMU_LDO_3_CFG" }, + { 0x1a, "PMU_LDO_4_CFG" }, + { 0x1b, "PMU_LDO_5_CFG" }, + { 0x1c, "PMU_LDO_6_CFG" }, + { 0x1d, "PMU_LDO_7_CFG" }, + { 0x1e, "PMU_LDO_8_CFG" }, + { 0x1f, "PMU_LDO_9_CFG" }, + { 0x20, "PMU_LDO_10_CFG" }, + { 0x21, "PMU_LDO_11_CFG" }, + { 0x22, "PMU_LDO_CONTROL" }, + { 0x23, "PMU_BUCK_CONTROL" }, + { 0x24, "PMU_BUCK_CONTROL_2" }, + { 0x25, "PMU_WLED_ISET" }, + { 0x26, "PMU_WLED_CONTROL" }, + { 0x27, "PMU_CHARGE_BUCK_CONTROL" }, + { 0x28, "PMU_CHARGE_CONTROL_A" }, + { 0x29, "PMU_CHARGE_CONTROL_B" }, + { 0x2a, "PMU_CHARGE_TIME" }, + { 0x2b, "PMU_CHARGE_MISC" }, + { 0x30, "PMU_ADC_CONTROL" }, + { 0x31, "PMU_ADC_LSB" }, + { 0x32, "PMU_ADC_MSB" }, + { 0x35, "PMU_ICHG_AVG" }, + { 0x3c, "PMU_MISC_ENABLE" }, + { 0x5f, "PMU_SYS_CONFIG" }, + { 0x6e, "PMU_N31_STATE" }, + { 0x6f, "PMU_N31_CAL_INPUT" }, +}; + +/* Named ranges; 0x6E/0x6F are looked up before the MEMBYTE range wins. */ +static const char *n31_pmu_reg_name(u8 reg, char *buf, size_t len) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(n31_pmu_names); i++) + if (n31_pmu_names[i].reg == reg) + return n31_pmu_names[i].name; + + if (reg >= 0x40 && reg <= 0x4f) + scnprintf(buf, len, "PMU_RTC_OR_UPCOUNT_%u", reg - 0x40); + else if (reg >= 0x50 && reg <= 0x57) + scnprintf(buf, len, "PMU_GPIO_%u", reg - 0x50 + 1); + else if (reg >= 0x59 && reg <= 0x5b) + scnprintf(buf, len, "PMU_GPIO_DEBOUNCE_%u", reg - 0x59 + 1); + else if (reg >= 0x5c && reg <= 0x5e) + scnprintf(buf, len, "PMU_BUTTON_%u", reg - 0x5c + 1); + else if (reg >= 0x60 && reg <= 0x87) + scnprintf(buf, len, "PMU_MEMBYTE_%u", reg - 0x60); + else + scnprintf(buf, len, "PMU_RESERVED_%02X", reg); + return buf; +} + +/* + * Rail decode. `active` names the register whose `mask` bit gates the rail; + * `vsel` holds the 5-bit voltage code. Ownership is deliberately unassigned: + * which peripheral each rail feeds should come out of a snapshot diff. + */ +struct n31_pmu_rail { + const char *name; + u8 vsel; + u8 active_reg; + u8 active_mask; + u16 base_mv; + u16 step_mv; +}; + +static const struct n31_pmu_rail n31_pmu_rails[] = { + { "PMU_LDO_1", 0x17, 0x10, 0x08, 2500, 50 }, + { "PMU_LDO_2", 0x18, 0x10, 0x10, 1500, 50 }, + { "PMU_LDO_3", 0x19, 0x10, 0x20, 2500, 50 }, + { "PMU_LDO_4", 0x1a, 0x10, 0x40, 1800, 50 }, + { "PMU_LDO_5", 0x1b, 0x10, 0x80, 2500, 50 }, + { "PMU_LDO_6", 0x1c, 0x11, 0x01, 2500, 50 }, + { "PMU_LDO_7", 0x1d, 0x11, 0x02, 1500, 100 }, + { "PMU_LDO_8", 0x1e, 0x11, 0x04, 2000, 50 }, + { "PMU_LDO_9", 0x1f, 0x11, 0x80, 1200, 25 }, + { "PMU_LDO_10", 0x20, 0x11, 0x10, 1700, 50 }, + { "PMU_LDO_11", 0x21, 0x11, 0x20, 1700, 50 }, + { "PMU_WDIG", N31_PMU_NO_VSEL, 0x11, 0x40, 0, 0 }, +}; + +/* Registers worth watching across an audio or display state change. */ +static const u8 n31_pmu_watch[] = { + 0x0d, 0x0e, 0x10, 0x11, 0x13, 0x14, 0x15, 0x16, 0x17, 0x1a, + 0x23, 0x24, 0x25, 0x26, 0x29, 0x2a, 0x2b, 0x30, 0x3c, +}; + +/* + * Registers the PMU updates on its own: measurement results, counters and + * latched status. They differ between any two reads, so a diff marks them + * instead of letting them bury a real rail change. + */ +static bool n31_pmu_reg_is_live(u8 reg) +{ + if (reg >= 0x01 && reg <= 0x08) /* events and status */ + return true; + if (reg >= 0x31 && reg <= 0x35) /* ADC result, charge current */ + return true; + if (reg >= 0x3d && reg <= 0x3f) /* observed ticking with the ADC */ + return true; + if (reg >= 0x40 && reg <= 0x4f) /* RTC / up-counter */ + return true; + return false; +} + +static u8 n31_pmu_snap[N31_PMU_REG_MAX]; +static bool n31_pmu_snap_valid; +static struct dentry *n31_pmu_debugfs; + +static int n31_pmu_read(u8 reg) +{ + struct i2c_client *client = d1830_poweroff_client; + + if (!client) + return -ENODEV; + return i2c_smbus_read_byte_data(client, reg); +} + +static void n31_pmu_read_all(u8 *out, bool *ok) +{ + unsigned int r; + + for (r = 0; r < N31_PMU_REG_MAX; r++) { + int v = n31_pmu_read((u8)r); + + ok[r] = v >= 0; + out[r] = ok[r] ? (u8)v : 0; + } +} + +static int n31_pmu_variant_show(struct seq_file *s, void *unused) +{ + seq_printf(s, "%s\n", pmu_variant ? pmu_variant : N31_PMU_VARIANT); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_variant); + +static int n31_pmu_regs_raw_show(struct seq_file *s, void *unused) +{ + unsigned int r; + + for (r = 0; r < N31_PMU_REG_MAX; r += 16) { + unsigned int i; + + seq_printf(s, "%02x:", r); + for (i = 0; i < 16 && r + i < N31_PMU_REG_MAX; i++) { + int v = n31_pmu_read((u8)(r + i)); + + if (v < 0) + seq_puts(s, " --"); + else + seq_printf(s, " %02x", v); + } + seq_puts(s, "\n"); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_regs_raw); + +static int n31_pmu_regs_named_show(struct seq_file *s, void *unused) +{ + char buf[32]; + unsigned int r; + + for (r = 0; r < N31_PMU_REG_MAX; r++) { + int v = n31_pmu_read((u8)r); + + if (v < 0) + continue; + seq_printf(s, "0x%02x %-24s 0x%02x\n", r, + n31_pmu_reg_name((u8)r, buf, sizeof(buf)), v); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_regs_named); + +static int n31_pmu_rails_show(struct seq_file *s, void *unused) +{ + unsigned int i; + + seq_puts(s, "rail vsel active raw on mV\n"); + for (i = 0; i < ARRAY_SIZE(n31_pmu_rails); i++) { + const struct n31_pmu_rail *ra = &n31_pmu_rails[i]; + int act = n31_pmu_read(ra->active_reg); + int cfg = ra->vsel == N31_PMU_NO_VSEL ? -1 : + n31_pmu_read(ra->vsel); + bool on = act >= 0 && (act & ra->active_mask); + + seq_printf(s, "%-11s ", ra->name); + if (ra->vsel == N31_PMU_NO_VSEL) + seq_puts(s, "---- "); + else + seq_printf(s, "0x%02x ", ra->vsel); + seq_printf(s, "0x%02x/0x%02x ", ra->active_reg, ra->active_mask); + if (cfg < 0) + seq_puts(s, " -- "); + else + seq_printf(s, "0x%02x ", cfg); + seq_printf(s, "%-3s ", act < 0 ? "?" : (on ? "yes" : "no")); + if (cfg >= 0 && ra->step_mv) + seq_printf(s, "%u", + ra->base_mv + + (cfg & N31_PMU_VSEL_MASK) * ra->step_mv); + else + seq_puts(s, "-"); + seq_puts(s, "\n"); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_rails); + +/* + * The boot rail sequence in this driver's register names. Blind writes and + * read-modify-writes are shown as they appear, because reproducing one as the + * other would change what the hardware ends up with. + */ +static int n31_pmu_bootseq_decode_show(struct seq_file *s, void *unused) +{ + seq_puts(s, "sub_23EC — rail configuration and active state\n"); + seq_puts(s, " PMU_BUCK_CONTROL [0x23] &= 0xFC\n"); + seq_puts(s, " PMU_BUCK_1_CFG [0x14] = computed 5-bit A\n"); + seq_puts(s, " PMU_BUCK_2_CFG [0x15] = computed 5-bit B\n"); + seq_puts(s, " PMU_SPECIAL_CFG [0x16] = computed 5-bit B\n"); + seq_puts(s, " PMU_LDO_1_CFG [0x17] = computed 5-bit B\n"); + seq_puts(s, " PMU_LDO_4_CFG [0x1A] = 0xB2 (written twice)\n"); + seq_puts(s, " PMU_ACTIVE_1 [0x10] = (old & 0x2F) | 0x10\n"); + seq_puts(s, " cold boot also | 0x20\n"); + seq_puts(s, " PMU_ACTIVE_2 [0x11] |= 0x07\n"); + seq_puts(s, " PMU_HIBERNATE_1 [0x13] |= 0x02\n"); + seq_puts(s, "\n"); + seq_puts(s, "sub_27F4 — board power initialisation tail\n"); + seq_puts(s, " PMU_SYS_CONTROL [0x0D] &= 0x8F\n"); + seq_puts(s, " (apply sub_23EC)\n"); + seq_puts(s, " PMU_ADC_CONTROL [0x30] |= 0x40\n"); + seq_puts(s, " PMU_GPIO_DEBOUNCE_1[0x59] &= 0xE3\n"); + seq_puts(s, " PMU_MISC_ENABLE [0x3C] = 0x01\n"); + seq_puts(s, " PMU_CHARGE_CONTROL_B[0x29] = (old & 0xEC) | 0x10\n"); + seq_puts(s, " PMU_CHARGE_TIME [0x2A] = (old & 0xC0) | 0x14\n"); + seq_puts(s, " PMU_CHARGE_MISC [0x2B] = (old & 0xF0) | 0x01\n"); + seq_puts(s, " PMU_FAULT_LOG [0x0E] = 0x20\n"); + seq_puts(s, " PMU_BUCK_CONTROL_2 [0x24] = f(cal) >> 1\n"); + seq_puts(s, " PMU_WLED_ISET [0x25] = 4 * (f(cal) & 1)\n"); + seq_puts(s, " PMU_WLED_CONTROL [0x26] &= ~0x01\n"); + seq_puts(s, " PMU_SYS_CONTROL [0x0D] &= 0xF3\n"); + seq_puts(s, "\n"); + seq_puts(s, "sub_1E7C — low-power boot finish\n"); + seq_puts(s, " PMU_CHARGE_CONTROL_B[0x29] = (old & 0xF8) | 0x06\n"); + seq_puts(s, " PMU_BUCK_CONTROL_2 [0x24] = f(0x28) >> 1\n"); + seq_puts(s, " PMU_WLED_ISET [0x25] = 4 * (f(0x28) & 1)\n"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_bootseq_decode); + +/* Reading this captures the current register file; diff_last compares to it. */ +static int n31_pmu_snapshot_show(struct seq_file *s, void *unused) +{ + bool ok[N31_PMU_REG_MAX]; + char buf[32]; + unsigned int r; + + n31_pmu_read_all(n31_pmu_snap, ok); + n31_pmu_snap_valid = true; + + seq_puts(s, "captured; compare with diff_last\n"); + for (r = 0; r < N31_PMU_REG_MAX; r++) { + if (!ok[r]) + continue; + seq_printf(s, "0x%02x %-24s 0x%02x\n", r, + n31_pmu_reg_name((u8)r, buf, sizeof(buf)), + n31_pmu_snap[r]); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_snapshot); + +static int n31_pmu_diff_last_show(struct seq_file *s, void *unused) +{ + char buf[32]; + unsigned int r, n = 0; + + if (!n31_pmu_snap_valid) { + seq_puts(s, "no snapshot yet; read snapshot first\n"); + return 0; + } + for (r = 0; r < N31_PMU_REG_MAX; r++) { + int v = n31_pmu_read((u8)r); + u8 old, xor; + + if (v < 0) + continue; + old = n31_pmu_snap[r]; + if (old == (u8)v) + continue; + xor = old ^ (u8)v; + seq_printf(s, "0x%02x %-24s 0x%02x -> 0x%02x xor=0x%02x set=0x%02x cleared=0x%02x%s\n", + r, n31_pmu_reg_name((u8)r, buf, sizeof(buf)), + old, v, xor, (u8)(xor & v), (u8)(xor & old), + n31_pmu_reg_is_live((u8)r) ? " [live]" : ""); + if (!n31_pmu_reg_is_live((u8)r)) + n++; + } + if (!n) + seq_puts(s, + "no change since snapshot (ignoring [live] registers)\n"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_diff_last); + +static void n31_pmu_show_watch(struct seq_file *s, const char *tag) +{ + char buf[32]; + unsigned int i; + + seq_printf(s, "=== %s ===\n", tag); + for (i = 0; i < ARRAY_SIZE(n31_pmu_watch); i++) { + u8 reg = n31_pmu_watch[i]; + int v = n31_pmu_read(reg); + + seq_printf(s, "0x%02x %-24s ", reg, + n31_pmu_reg_name(reg, buf, sizeof(buf))); + if (v < 0) + seq_puts(s, "--\n"); + else + seq_printf(s, "0x%02x\n", v); + } +} + +static int n31_pmu_audio_snapshot_show(struct seq_file *s, void *unused) +{ + n31_pmu_show_watch(s, "audio rail watch"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_audio_snapshot); + +static int n31_pmu_display_snapshot_show(struct seq_file *s, void *unused) +{ + n31_pmu_show_watch(s, "display rail watch"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_display_snapshot); + +/* + * Show what the boot sequence would write given the registers as they stand, + * without writing any of it. The read-modify-write steps are evaluated + * against live values so the result is what would actually land. + */ +static int n31_pmu_apply_bootseq_dryrun_show(struct seq_file *s, void *unused) +{ + int r10, r11, r13, r23, r0d, r29, r2a, r2b, r26, r30, r59; + + seq_printf(s, "allow_pmu_writes=%d apply_boot_rails=%d (dry run only)\n\n", + allow_pmu_writes, apply_boot_rails); + + r23 = n31_pmu_read(0x23); + r10 = n31_pmu_read(0x10); + r11 = n31_pmu_read(0x11); + r13 = n31_pmu_read(0x13); + r0d = n31_pmu_read(0x0d); + r29 = n31_pmu_read(0x29); + r2a = n31_pmu_read(0x2a); + r2b = n31_pmu_read(0x2b); + r26 = n31_pmu_read(0x26); + r30 = n31_pmu_read(0x30); + r59 = n31_pmu_read(0x59); + if (r23 < 0 || r10 < 0 || r11 < 0 || r13 < 0) { + seq_puts(s, "PMU read failed\n"); + return 0; + } + + seq_puts(s, "sub_23EC:\n"); + seq_printf(s, " PMU_BUCK_CONTROL 0x%02x -> 0x%02x\n", + r23, r23 & 0xfc); + seq_printf(s, " PMU_LDO_4_CFG ---- -> 0xb2 (blind, twice)\n"); + seq_printf(s, " PMU_ACTIVE_1 0x%02x -> 0x%02x (cold: 0x%02x)\n", + r10, (r10 & 0x2f) | 0x10, (r10 & 0x2f) | 0x30); + seq_printf(s, " PMU_ACTIVE_2 0x%02x -> 0x%02x\n", + r11, r11 | 0x07); + seq_printf(s, " PMU_HIBERNATE_1 0x%02x -> 0x%02x\n", + r13, r13 | 0x02); + + seq_puts(s, "sub_27F4 tail:\n"); + if (r0d >= 0) + seq_printf(s, " PMU_SYS_CONTROL 0x%02x -> 0x%02x then 0x%02x\n", + r0d, r0d & 0x8f, (r0d & 0x8f) & 0xf3); + if (r30 >= 0) + seq_printf(s, " PMU_ADC_CONTROL 0x%02x -> 0x%02x\n", + r30, r30 | 0x40); + if (r59 >= 0) + seq_printf(s, " PMU_GPIO_DEBOUNCE_1 0x%02x -> 0x%02x\n", + r59, r59 & 0xe3); + seq_puts(s, " PMU_MISC_ENABLE ---- -> 0x01 (blind)\n"); + if (r29 >= 0) + seq_printf(s, " PMU_CHARGE_CONTROL_B 0x%02x -> 0x%02x\n", + r29, (r29 & 0xec) | 0x10); + if (r2a >= 0) + seq_printf(s, " PMU_CHARGE_TIME 0x%02x -> 0x%02x\n", + r2a, (r2a & 0xc0) | 0x14); + if (r2b >= 0) + seq_printf(s, " PMU_CHARGE_MISC 0x%02x -> 0x%02x\n", + r2b, (r2b & 0xf0) | 0x01); + seq_puts(s, " PMU_FAULT_LOG ---- -> 0x20 (blind)\n"); + if (r26 >= 0) + seq_printf(s, " PMU_WLED_CONTROL 0x%02x -> 0x%02x\n", + r26, r26 & ~0x01); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(n31_pmu_apply_bootseq_dryrun); + + +/* ------------------------------------------------------------------ */ +/* Consumer rail control */ +/* */ +/* A rail is enabled while at least one consumer holds it and is turned */ +/* off again once the last reference has been gone for rail_off_delay_ms.*/ +/* The delay matters for the analog rail: track changes drop and retake */ +/* it within a second, and cycling it each time both wastes power and */ +/* risks an audible pop. */ +/* */ +/* Only the rail's own enable bit in PMU_ACTIVE_* is touched, never the */ +/* whole register. */ +/* ------------------------------------------------------------------ */ + +static bool rail_control = true; +module_param(rail_control, bool, 0644); +MODULE_PARM_DESC(rail_control, + "Let drivers enable/disable their rail via n31_pmu_rail_get/put"); + +static unsigned int rail_off_delay_ms = 5000; +module_param(rail_off_delay_ms, uint, 0644); +MODULE_PARM_DESC(rail_off_delay_ms, + "Idle time before a released rail is powered down (ms)"); + +struct n31_pmu_rail_state { + int users; + bool on; + struct delayed_work off_work; +}; + +static struct n31_pmu_rail_state n31_pmu_rail_state[ARRAY_SIZE(n31_pmu_rails)]; +static DEFINE_MUTEX(n31_pmu_rail_lock); + +/* Enable bits in `active_reg` belonging to rails a consumer still holds. */ +static u8 n31_pmu_rail_held_mask(u8 active_reg) +{ + unsigned int i; + u8 mask = 0; + + mutex_lock(&n31_pmu_rail_lock); + for (i = 0; i < ARRAY_SIZE(n31_pmu_rails); i++) + if (n31_pmu_rails[i].active_reg == active_reg && + n31_pmu_rail_state[i].users) + mask |= n31_pmu_rails[i].active_mask; + mutex_unlock(&n31_pmu_rail_lock); + return mask; +} + +static int n31_pmu_rail_apply(unsigned int id, bool on) +{ + const struct n31_pmu_rail *ra = &n31_pmu_rails[id]; + struct i2c_client *client = d1830_poweroff_client; + int ret; + + if (!client) + return -ENODEV; + if (!rail_control) + return -EPERM; + + ret = d1830_rmw(client, ra->active_reg, + on ? 0 : ra->active_mask, + on ? ra->active_mask : 0); + if (ret) { + dev_warn(&client->dev, "%s %s failed: %d\n", + ra->name, on ? "enable" : "disable", ret); + return ret; + } + d1830_vinfo(&client->dev, "%s %s\n", ra->name, on ? "on" : "off"); + return 0; +} + +static void n31_pmu_rail_off_work(struct work_struct *work) +{ + struct n31_pmu_rail_state *st = container_of(to_delayed_work(work), + struct n31_pmu_rail_state, + off_work); + unsigned int id = st - n31_pmu_rail_state; + + mutex_lock(&n31_pmu_rail_lock); + if (!st->users && st->on && !n31_pmu_rail_apply(id, false)) + st->on = false; + mutex_unlock(&n31_pmu_rail_lock); +} + +/* Enable a rail and hold it. Balanced by n31_pmu_rail_put(). */ +int n31_pmu_rail_get(unsigned int id) +{ + struct n31_pmu_rail_state *st; + int ret = 0; + + if (id >= ARRAY_SIZE(n31_pmu_rails)) + return -EINVAL; + st = &n31_pmu_rail_state[id]; + + mutex_lock(&n31_pmu_rail_lock); + cancel_delayed_work(&st->off_work); + if (!st->on) { + ret = n31_pmu_rail_apply(id, true); + if (!ret) + st->on = true; + } + if (!ret) + st->users++; + mutex_unlock(&n31_pmu_rail_lock); + return ret; +} +EXPORT_SYMBOL_GPL(n31_pmu_rail_get); + +/* Drop a reference; the rail powers down once idle for rail_off_delay_ms. */ +void n31_pmu_rail_put(unsigned int id) +{ + struct n31_pmu_rail_state *st; + + if (id >= ARRAY_SIZE(n31_pmu_rails)) + return; + st = &n31_pmu_rail_state[id]; + + mutex_lock(&n31_pmu_rail_lock); + if (st->users) + st->users--; + if (!st->users && st->on) + schedule_delayed_work(&st->off_work, + msecs_to_jiffies(rail_off_delay_ms)); + mutex_unlock(&n31_pmu_rail_lock); +} +EXPORT_SYMBOL_GPL(n31_pmu_rail_put); + +static void n31_pmu_rail_init(void) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(n31_pmu_rail_state); i++) + INIT_DELAYED_WORK(&n31_pmu_rail_state[i].off_work, + n31_pmu_rail_off_work); +} + +static void n31_pmu_rail_exit(void) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(n31_pmu_rail_state); i++) { + cancel_delayed_work_sync(&n31_pmu_rail_state[i].off_work); + /* + * Leave rail state as-is on unbind: the boot configuration is + * what the rest of the system expects to find. + */ + } +} + + +/* ------------------------------------------------------------------ */ +/* Screen sleep and power-button policy */ +/* */ +/* Short press toggles screen sleep: fade the backlight out and quiesce */ +/* touch. Holding the button past power_hold_ms asks the kernel to shut */ +/* down, which reaches the PMU through the machine pm_power_off this */ +/* driver already installs. */ +/* */ +/* The panel itself is deliberately left running. Nothing in this stack */ +/* can re-initialise it, so powering it down would be a one-way trip */ +/* until the panel init sequence is recovered. Backlight and touch are */ +/* where the current draw is anyway. */ +/* */ +/* Two tiers are selected from whether the analog audio rail is held: */ +/* with playback running a press only sleeps the screen, and the deeper */ +/* path is left for when nothing is playing. */ +/* ------------------------------------------------------------------ */ + +static bool screen_sleep_enable = true; +module_param(screen_sleep_enable, bool, 0644); +MODULE_PARM_DESC(screen_sleep_enable, + "Short press toggles screen sleep (default Y)"); + +static unsigned int power_hold_ms = 4000; +module_param(power_hold_ms, uint, 0644); +MODULE_PARM_DESC(power_hold_ms, + "Hold the power button this long to power off (0 = never)"); + +static unsigned int backlight_fade_ms = 400; +module_param(backlight_fade_ms, uint, 0644); +MODULE_PARM_DESC(backlight_fade_ms, "Backlight ramp duration either way (ms)"); + +static bool n31_screen_asleep; +static int n31_screen_saved_level = -1; +static unsigned long n31_power_press_jiffies; +static bool n31_power_off_pending; +static DEFINE_MUTEX(n31_screen_lock); + +/* + * True while audio is playing. The codec has no PMU rail of its own, so + * this asks the codec directly rather than inferring it from rail state. + */ +static bool n31_audio_active(void) +{ + bool (*active)(void); + bool r = false; + + active = (bool (*)(void))__symbol_get("n31_audio_playback_active"); + if (active) { + r = active(); + __symbol_put("n31_audio_playback_active"); + } + return r; +} + +static void n31_screen_set(bool asleep) +{ + int (*fade)(int, unsigned int); + int (*level)(void); + int (*touch)(void); + int (*lcd)(bool); + + mutex_lock(&n31_screen_lock); + if (asleep == n31_screen_asleep) + goto out; + + fade = (int (*)(int, unsigned int))__symbol_get("n31_backlight_fade"); + level = (int (*)(void))__symbol_get("n31_backlight_level"); + + if (asleep) { + if (level) { + n31_screen_saved_level = level(); + __symbol_put("n31_backlight_level"); + } + if (fade) { + fade(0, backlight_fade_ms); + __symbol_put("n31_backlight_fade"); + } + touch = (int (*)(void))__symbol_get("n31_touch_suspend"); + if (touch) { + touch(); + __symbol_put("n31_touch_suspend"); + } + /* Panel last, once nothing is drawing to it. */ + lcd = (int (*)(bool))__symbol_get("n31_lcd_power"); + if (lcd) { + lcd(false); + __symbol_put("n31_lcd_power"); + } + } else { + /* Panel first: it has to be scanning before the light comes up. */ + lcd = (int (*)(bool))__symbol_get("n31_lcd_power"); + if (lcd) { + lcd(true); + __symbol_put("n31_lcd_power"); + } + touch = (int (*)(void))__symbol_get("n31_touch_resume"); + if (touch) { + touch(); + __symbol_put("n31_touch_resume"); + } + if (level) + __symbol_put("n31_backlight_level"); + if (fade) { + fade(n31_screen_saved_level > 0 ? + n31_screen_saved_level : 1, + backlight_fade_ms); + __symbol_put("n31_backlight_fade"); + } + } + + n31_screen_asleep = asleep; + pr_info("n31: screen %s (audio %s)\n", + asleep ? "asleep" : "awake", + n31_audio_active() ? "active" : "idle"); +out: + mutex_unlock(&n31_screen_lock); +} + +bool n31_screen_is_asleep(void) +{ + return n31_screen_asleep; +} +EXPORT_SYMBOL_GPL(n31_screen_is_asleep); + +static void n31_power_off_work(struct work_struct *work) +{ + pr_warn("n31: power button held — shutting down\n"); + orderly_poweroff(true); +} +static DECLARE_WORK(n31_power_off_worker, n31_power_off_work); + +/* + * Called on every observed Sleep-button edge. Press only records when it + * started; the decision happens on release, or as soon as the hold passes + * power_hold_ms while still down. + */ +static void n31_power_button(bool pressed) +{ + unsigned long held_ms; + + if (pressed) { + n31_power_press_jiffies = jiffies; + n31_power_off_pending = false; + return; + } + + if (!n31_power_press_jiffies) + return; + held_ms = jiffies_to_msecs(jiffies - n31_power_press_jiffies); + n31_power_press_jiffies = 0; + + if (power_hold_ms && held_ms >= power_hold_ms) { + if (!n31_power_off_pending) { + n31_power_off_pending = true; + schedule_work(&n31_power_off_worker); + } + return; + } + + if (!screen_sleep_enable) + return; + + /* + * A press while asleep only wakes; it should not immediately put the + * screen back down. + */ + n31_screen_set(!n31_screen_asleep); +} + +/* Long holds must act while the button is still down, not on release. */ +static void n31_power_button_poll(void) +{ + unsigned long held_ms; + + if (!n31_power_press_jiffies || !power_hold_ms) + return; + if (n31_power_off_pending) + return; + held_ms = jiffies_to_msecs(jiffies - n31_power_press_jiffies); + if (held_ms < power_hold_ms) + return; + n31_power_off_pending = true; + schedule_work(&n31_power_off_worker); +} + +/* Home wakes the screen but is otherwise left to userspace. */ +static void n31_home_button(bool pressed) +{ + if (pressed && n31_screen_asleep) + n31_screen_set(false); +} + +static void n31_pmu_debugfs_init(void) +{ + struct dentry *d; + + d = debugfs_create_dir("n31_pmu", NULL); + if (IS_ERR(d)) + return; + n31_pmu_debugfs = d; + + debugfs_create_file("variant", 0444, d, NULL, &n31_pmu_variant_fops); + debugfs_create_file("regs_raw", 0444, d, NULL, &n31_pmu_regs_raw_fops); + debugfs_create_file("regs_named", 0444, d, NULL, + &n31_pmu_regs_named_fops); + debugfs_create_file("rails", 0444, d, NULL, &n31_pmu_rails_fops); + debugfs_create_file("bootseq_decode", 0444, d, NULL, + &n31_pmu_bootseq_decode_fops); + debugfs_create_file("snapshot", 0444, d, NULL, &n31_pmu_snapshot_fops); + debugfs_create_file("diff_last", 0444, d, NULL, + &n31_pmu_diff_last_fops); + debugfs_create_file("audio_snapshot", 0444, d, NULL, + &n31_pmu_audio_snapshot_fops); + debugfs_create_file("display_snapshot", 0444, d, NULL, + &n31_pmu_display_snapshot_fops); + debugfs_create_file("apply_bootseq_dryrun", 0444, d, NULL, + &n31_pmu_apply_bootseq_dryrun_fops); +} + +static void n31_pmu_debugfs_exit(void) +{ + debugfs_remove_recursive(n31_pmu_debugfs); + n31_pmu_debugfs = NULL; +} + + static void d1830_log_audio_regs(struct i2c_client *client, const char *tag) { static const u8 regs[] = { @@ -959,6 +1824,12 @@ static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) r16 = (u8)((v21 & 0x2f) | 0x10); if (!boot_mode) r16 |= 0x20; + /* + * The boot form of this write clears bits 6 and 7, which is correct + * at boot but would drop a rail a driver is currently holding. Put + * those back before writing. + */ + r16 |= n31_pmu_rail_held_mask(0x10); d1830_write8(client, 16, r16); d1830_rmw(client, 17, 0, 0x07); @@ -1210,6 +2081,11 @@ static int d1830_gpio_probe(struct i2c_client *client) gpio_dev->input = devm_input_allocate_device(dev); if (gpio_dev->input) { + /* + * Home, Sleep and Play live on the PMIC status registers, so + * they are reported here and nowhere else. n31-buttons keeps + * Vol+/Vol-, which are SoC GPIOs. + */ gpio_dev->input->name = "n31-pmic-buttons"; gpio_dev->input->phys = "d1830/gpio"; gpio_dev->input->dev.parent = dev; @@ -1231,6 +2107,8 @@ static int d1830_gpio_probe(struct i2c_client *client) INIT_DELAYED_WORK(&gpio_dev->confirm, d1830_confirm_work); schedule_delayed_work(&gpio_dev->trace, msecs_to_jiffies(btn_poll_ms ? btn_poll_ms : 1000)); + n31_pmu_rail_init(); + n31_pmu_debugfs_init(); d1830_n31_din_nirq_hook = d1830_n31_din_nirq; return 0; } @@ -1249,6 +2127,8 @@ static void d1830_gpio_remove(struct i2c_client *client) if (pm_power_off == d1830_pm_power_off) pm_power_off = NULL; d1830_n31_din_nirq_hook = NULL; + n31_pmu_debugfs_exit(); + n31_pmu_rail_exit(); d1830_poweroff_client = NULL; } diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c index 60ff482c7c649e..89d7178b766be5 100644 --- a/drivers/gpio/gpio-s5l8740.c +++ b/drivers/gpio/gpio-s5l8740.c @@ -127,6 +127,11 @@ static struct s5l8740_gpio *s5l8740_n31; void (*d1830_n31_din_nirq_hook)(void); EXPORT_SYMBOL_GPL(d1830_n31_din_nirq_hook); +/* + * Report a key on n31-buttons. Only for keys this driver actually sources; + * PMIC-sourced keys go out on n31-pmic-buttons instead, so that a single + * physical press produces a single event. + */ void s5l8740_n31_report_key(unsigned int code, int pressed) { if (!s5l8740_n31 || !s5l8740_n31->input) @@ -538,11 +543,13 @@ static int s5l8740_gpio_probe(struct platform_device *pdev) sg->input->phys = "s5l8740/gpio"; sg->input->dev.parent = dev; sg->input->id.bustype = BUS_HOST; + /* + * Vol+/Vol- only. Home, Sleep and Play are PMIC-sourced and + * are reported on n31-pmic-buttons; declaring them here too + * made one press look like two events to userspace. + */ input_set_capability(sg->input, EV_KEY, KEY_VOLUMEUP); input_set_capability(sg->input, EV_KEY, KEY_VOLUMEDOWN); - input_set_capability(sg->input, EV_KEY, KEY_POWER); - input_set_capability(sg->input, EV_KEY, KEY_HOMEPAGE); - input_set_capability(sg->input, EV_KEY, KEY_PLAYPAUSE); if (input_register_device(sg->input)) sg->input = NULL; } diff --git a/drivers/gpu/drm/tiny/s5l8740.c b/drivers/gpu/drm/tiny/s5l8740.c index e0e470b987f246..2bbf09e5403010 100644 --- a/drivers/gpu/drm/tiny/s5l8740.c +++ b/drivers/gpu/drm/tiny/s5l8740.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-only +#include #include +#include #include #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #define S5L8740_LCD_CON 0x00 /* Control register. */ #define S5L8740_LCD_WCMD 0x04 /* Write command register. */ @@ -58,6 +61,11 @@ /* memory management */ void __iomem *lcdif; + void __iomem *clkcon; /* gates cycled across an LCDIF reset */ + + /* display power */ + struct mutex power_lock; + bool powered; /* modesetting */ uint32_t formats[8]; @@ -107,6 +115,17 @@ static void s5l8740_primary_plane_helper_atomic_update(struct drm_plane *plane, if (!fb || drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE)) return; + /* + * Never push pixels at a stopped interface. Each write waits on the + * status register, so a full frame against a powered-down LCDIF would + * stall for the timeout on every one of them. The panel is repainted + * by n31_lcd_power() once it is running again. + */ + if (!READ_ONCE(sdev->powered)) { + drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE); + return; + } + if (!drm_dev_enter(dev, &idx)) goto out_drm_gem_fb_end_cpu_access; @@ -173,6 +192,300 @@ static const struct drm_connector_funcs s5l8740_connector_funcs = { .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, }; + +/* ------------------------------------------------------------------ */ +/* Display power sequence */ +/* */ +/* Reconstructed from the stock firmware, which does the whole thing in */ +/* three steps and never sends the panel a command: */ +/* */ +/* on: enable the display rail, reset the LCDIF, reprogram it, run */ +/* off: stop the LCDIF, drop the rail */ +/* */ +/* The panel needs no init sequence of its own — the LCDIF is the whole */ +/* story — which is what makes a real off/on cycle possible here. */ +/* ------------------------------------------------------------------ */ + +#define S5L8740_LCD_RESET 0x30 /* reset command / ack */ +#define S5L8740_LCD_UNK2C 0x2c +#define S5L8740_LCD_UNK68 0x68 +#define S5L8740_LCD_UNK70 0x70 +#define S5L8740_LCD_SIZE 0x74 /* height | (width << 16) */ +#define S5L8740_LCD_UNK78 0x78 +#define S5L8740_LCD_UNK7C 0x7c +#define S5L8740_LCD_UNK84 0x84 +#define S5L8740_LCD_UNKA4 0xa4 + +#define S5L8740_CON_HOLD BIT(10) /* 0x400: interface held in reset */ +#define S5L8740_CON_RUN BIT(11) /* 0x800: interface running */ +#define S5L8740_CON_BASE 0x00100ab0 +#define S5L8740_CON_MODE_MASK 0xc0000000 +#define S5L8740_CON_FMT_MASK 0x00000007 +#define S5L8740_STATUS_RESETTING 0x1000 + +/* CLKCON gates that have to be dropped across an LCDIF reset. */ +#define S5L8740_CLKCON_08 0x08 +#define S5L8740_CLKCON_18 0x18 +#define S5L8740_CLKCON_08_MASK 0x7fff7fff +#define S5L8740_CLKCON_18_MASK 0xffff3fff + +#define S5L8740_LCD_RESET_TRIES 5 + +static unsigned int lcd_power_tries = S5L8740_LCD_RESET_TRIES; +module_param(lcd_power_tries, uint, 0644); +MODULE_PARM_DESC(lcd_power_tries, "Attempts to bring the display up (default 5)"); + +/* + * The stock sequence waits between enabling the rail and touching the + * LCDIF. That wait is a thunk into ROM, so its length is not recoverable; + * this is a conservative stand-in, tunable if the panel proves fussy. + */ +static unsigned int lcd_rail_settle_us = 3000; +module_param(lcd_rail_settle_us, uint, 0644); +MODULE_PARM_DESC(lcd_rail_settle_us, + "Settle time after enabling the display rail (us)"); + +static bool lcd_manage_rail = true; +module_param(lcd_manage_rail, bool, 0644); +MODULE_PARM_DESC(lcd_manage_rail, + "Hold the PMU display rail while the panel is on (default Y)"); + +static struct s5l8740_device *s5l8740_lcd_dev; + +static void s5l8740_lcd_rail(struct s5l8740_device *sdev, bool on) +{ + int (*get)(unsigned int); + void (*put)(unsigned int); + + if (!lcd_manage_rail) + return; + if (on) { + get = (int (*)(unsigned int))__symbol_get("n31_pmu_rail_get"); + if (!get) + return; + if (get(N31_PMU_RAIL_DISPLAY)) + drm_warn(&sdev->dev, "display rail enable failed\n"); + __symbol_put("n31_pmu_rail_get"); + } else { + put = (void (*)(unsigned int))__symbol_get("n31_pmu_rail_put"); + if (!put) + return; + put(N31_PMU_RAIL_DISPLAY); + __symbol_put("n31_pmu_rail_put"); + } +} + +/* + * LCDIF reset. `light` skips the parts that wait on hardware, which the + * stock code uses when it only needs the clocks cycled. + */ +static int s5l8740_lcdif_reset(struct s5l8740_device *sdev, bool light) +{ + void __iomem *b = sdev->lcdif; + u32 clk08 = 0, clk18 = 0, val; + int ret = 0; + + if (sdev->clkcon) { + clk08 = readl(sdev->clkcon + S5L8740_CLKCON_08); + clk18 = readl(sdev->clkcon + S5L8740_CLKCON_18); + writel(clk08 & S5L8740_CLKCON_08_MASK, + sdev->clkcon + S5L8740_CLKCON_08); + writel(clk18 & S5L8740_CLKCON_18_MASK, + sdev->clkcon + S5L8740_CLKCON_18); + } + + writel(readl(b + S5L8740_LCD_CON) & ~S5L8740_CON_HOLD, + b + S5L8740_LCD_CON); + + if (!light && + readl_poll_timeout(b + S5L8740_LCD_STATUS, val, + !(val & S5L8740_STATUS_RESETTING), 100, + 500 * USEC_PER_MSEC)) + drm_warn(&sdev->dev, "LCDIF busy before reset (status %08x)\n", + readl(b + S5L8740_LCD_STATUS)); + + writel(1, b + S5L8740_LCD_RESET); + if (readl_poll_timeout(b + S5L8740_LCD_RESET, val, !val, 100, + 500 * USEC_PER_MSEC)) { + drm_warn(&sdev->dev, "LCDIF reset did not ack\n"); + ret = -ETIMEDOUT; + } + + if (!light) { + /* + * Poke the hold bit until the interface admits it is held; + * the stock code loops on this for up to half a second. + */ + ktime_t end = ktime_add_ms(ktime_get(), 500); + + while (!(readl(b + S5L8740_LCD_CON) & S5L8740_CON_HOLD)) { + writel(readl(b + S5L8740_LCD_CON) | S5L8740_CON_HOLD, + b + S5L8740_LCD_CON); + if (ktime_after(ktime_get(), end)) { + drm_warn(&sdev->dev, "LCDIF hold ack timeout\n"); + ret = -ETIMEDOUT; + break; + } + usleep_range(100, 200); + } + } + + writel(readl(b + S5L8740_LCD_CON) & ~S5L8740_CON_HOLD, + b + S5L8740_LCD_CON); + + if (sdev->clkcon) { + writel(clk08, sdev->clkcon + S5L8740_CLKCON_08); + writel(clk18, sdev->clkcon + S5L8740_CLKCON_18); + } + return ret; +} + +/* Reprogram the LCDIF for the current mode. Leaves it stopped. */ +static void s5l8740_lcdif_program(struct s5l8740_device *sdev) +{ + void __iomem *b = sdev->lcdif; + u32 con = readl(b + S5L8740_LCD_CON); + u32 keep = con & (S5L8740_CON_MODE_MASK | S5L8740_CON_FMT_MASK); + + writel(0x000a000a, b + S5L8740_LCD_UNK78); + writel(keep | S5L8740_CON_BASE, b + S5L8740_LCD_CON); + writel(1, b + S5L8740_LCD_UNK2C); + writel(0, b + S5L8740_LCD_UNK68); + writel(0, b + S5L8740_LCD_UNK70); + writel(sdev->mode.vdisplay | (sdev->mode.hdisplay << 16), + b + S5L8740_LCD_SIZE); + writel(0, b + S5L8740_LCD_PHTIME); + writel(770, b + S5L8740_LCD_UNK7C); + writel(100, b + S5L8740_LCD_UNK84); + writel(1, b + S5L8740_LCD_UNKA4); +} + +/* Start the interface, matching the stock enable path. */ +static int s5l8740_lcdif_run(struct s5l8740_device *sdev) +{ + void __iomem *b = sdev->lcdif; + u32 con = readl(b + S5L8740_LCD_CON); + u32 val; + + if ((con & (S5L8740_CON_MODE_MASK | S5L8740_CON_FMT_MASK)) != + (S5L8740_CON_BASE & (S5L8740_CON_MODE_MASK | S5L8740_CON_FMT_MASK))) { + if (readl_poll_timeout(b + S5L8740_LCD_STATUS, val, + !(val & S5L8740_STATUS_RESETTING), 100, + 500 * USEC_PER_MSEC)) + drm_warn(&sdev->dev, "LCDIF busy before run\n"); + writel((con & 0x3ffffff8) | S5L8740_CON_BASE, + b + S5L8740_LCD_CON); + } + writel(readl(b + S5L8740_LCD_CON) | S5L8740_CON_RUN, + b + S5L8740_LCD_CON); + return 0; +} + +static int s5l8740_lcd_power_on_locked(struct s5l8740_device *sdev) +{ + unsigned int try; + int ret = -ETIMEDOUT; + + if (sdev->powered) + return 0; + + s5l8740_lcd_rail(sdev, true); + if (lcd_manage_rail && lcd_rail_settle_us) + usleep_range(lcd_rail_settle_us, lcd_rail_settle_us + 500); + + for (try = 0; try < (lcd_power_tries ? lcd_power_tries : 1); try++) { + ret = s5l8740_lcdif_reset(sdev, try == 0); + if (ret && try == 0) + ret = s5l8740_lcdif_reset(sdev, false); + + s5l8740_lcdif_program(sdev); + s5l8740_lcdif_run(sdev); + + if (readl(sdev->lcdif + S5L8740_LCD_CON) & S5L8740_CON_RUN) { + ret = 0; + break; + } + drm_warn(&sdev->dev, "display did not start (attempt %u)\n", + try + 1); + ret = -EIO; + msleep(2); + } + + if (ret) { + drm_err(&sdev->dev, "failed to turn on display after %u tries\n", + lcd_power_tries); + s5l8740_lcd_rail(sdev, false); + return ret; + } + + sdev->powered = true; + drm_info(&sdev->dev, "display on (CON=%08x STATUS=%08x)\n", + readl(sdev->lcdif + S5L8740_LCD_CON), + readl(sdev->lcdif + S5L8740_LCD_STATUS)); + return 0; +} + +static void s5l8740_lcd_power_off_locked(struct s5l8740_device *sdev) +{ + if (!sdev->powered) + return; + + writel(readl(sdev->lcdif + S5L8740_LCD_CON) & ~S5L8740_CON_RUN, + sdev->lcdif + S5L8740_LCD_CON); + s5l8740_lcd_rail(sdev, false); + sdev->powered = false; + drm_info(&sdev->dev, "display off (CON=%08x)\n", + readl(sdev->lcdif + S5L8740_LCD_CON)); +} + +/* + * Entry points for the screen-sleep policy. Powering the panel down is the + * point of the exercise: with only the backlight off the panel keeps drawing. + */ +int n31_lcd_power(bool on) +{ + struct s5l8740_device *sdev = s5l8740_lcd_dev; + int ret; + + if (!sdev) + return -ENODEV; + + /* + * Suspend the in-kernel clients before the panel goes down so fbcon + * stops drawing into a stopped interface; its damage would otherwise + * be dropped and the cursor would keep queueing work for nothing. + */ + if (!on) + drm_client_dev_suspend(&sdev->dev, false); + + mutex_lock(&sdev->power_lock); + if (on) { + ret = s5l8740_lcd_power_on_locked(sdev); + } else { + s5l8740_lcd_power_off_locked(sdev); + ret = 0; + } + mutex_unlock(&sdev->power_lock); + + if (on) { + if (ret) { + /* Left suspended on failure; nothing to draw to. */ + return ret; + } + /* Buffer survived, interface state did not: resume and repaint. */ + drm_client_dev_resume(&sdev->dev, false); + drm_client_dev_restore(&sdev->dev); + } + return ret; +} +EXPORT_SYMBOL_GPL(n31_lcd_power); + +bool n31_lcd_is_on(void) +{ + return s5l8740_lcd_dev && s5l8740_lcd_dev->powered; +} +EXPORT_SYMBOL_GPL(n31_lcd_is_on); + static const struct drm_crtc_helper_funcs s5l8740_crtc_helper_funcs = { .atomic_check = drm_crtc_helper_atomic_check, }; @@ -254,6 +567,27 @@ static int s5l8740_probe(struct platform_device *pdev) if (IS_ERR(sdev->lcdif)) return PTR_ERR(sdev->lcdif); + mutex_init(&sdev->power_lock); + + /* + * Second window is the clock controller. Only two gates are touched, + * and only to cycle them across an LCDIF reset. Optional: without it + * the reset still works, it just does not gate the clocks first. + */ + res = platform_get_resource(pdev, IORESOURCE_MEM, 1); + if (res) { + sdev->clkcon = devm_ioremap_resource(&pdev->dev, res); + if (IS_ERR(sdev->clkcon)) + sdev->clkcon = NULL; + } + if (!sdev->clkcon) + drm_info(dev, + "no clkcon window; LCDIF reset will not gate clocks\n"); + + /* The panel is already running from the boot loader handoff. */ + sdev->powered = true; + s5l8740_lcd_dev = sdev; + /* GATE0: log WTF handoff, never rewrite CON/PHTIME */ drm_info(dev, "LCDIF handoff CON=%08x PHTIME=%08x (untouched)\n", readl(sdev->lcdif + S5L8740_LCD_CON), diff --git a/drivers/i2c/busses/i2c-s5l8702.c b/drivers/i2c/busses/i2c-s5l8702.c index a413adece55797..0f3e04a01a2944 100644 --- a/drivers/i2c/busses/i2c-s5l8702.c +++ b/drivers/i2c/busses/i2c-s5l8702.c @@ -111,6 +111,10 @@ struct s5l8702_i2c_dev { struct i2c_adapter adapter; struct clk_bulk_data *clks; int num_clks; + void __iomem *gpio; + void __iomem *gpiocmd; + u32 pads[2]; + unsigned int npads; }; static inline u32 s5l8702_i2c_readl(struct s5l8702_i2c_dev *i2c_dev, u32 reg) @@ -658,6 +662,129 @@ static int s5l8702_i2c_init(struct s5l8702_i2c_dev *i2c_dev) return 0; } +/* + * Pad mux. + * + * Nothing else in this port muxes the I2C pads, which is fine for the bus + * the bootloader leaves configured but not for one it does not. Stock does + * it per bus in sub_5714EE: + * + * bus 0 (0x3C600000) GPIO 4, 5 bus 2 GPIO 66, 67 + * bus 1 (0x3C900000) GPIO 78, 79 bus 3 GPIO 83, 84 + * + * Each pad goes to function 2 via the GPIO command port, and the first of + * the pair additionally gets its bit set in the bank's +0x0C register -- + * stock applies that to one pad only, so this does the same rather than + * guessing that both want it. + * + * The pads come from DT rather than a bus-index table here: the index a + * controller has in the stock enumeration is not something the driver can + * see, and getting it wrong would mux somebody else's pins. + */ +#define S5L8702_I2C_GPIO_PHYS 0x3cf00000ul +#define S5L8702_I2C_GPIO_LEN 0x200 +#define S5L8702_I2C_GPIOCMD_PHYS 0x3cf001e0ul +#define S5L8702_I2C_PAD_FUNC 2 +#define S5L8702_I2C_PAD_RELEASE 0 +#define S5L8702_I2C_BANK_STRIDE 32 +#define S5L8702_I2C_BANK_DIR 0x14 +#define S5L8702_I2C_BANK_ENABLE 0x0c +#define S5L8702_I2C_PADS 2 + +static void s5l8702_i2c_pad_func(struct s5l8702_i2c_dev *i2c_dev, + unsigned int gpio, u8 func, bool claim) +{ + void __iomem *bank; + unsigned int pin; + u32 dir; + + if (!i2c_dev->gpio || !i2c_dev->gpiocmd) + return; + bank = i2c_dev->gpio + S5L8702_I2C_BANK_STRIDE * (gpio >> 3); + pin = gpio & 7; + + dir = readl(bank + S5L8702_I2C_BANK_DIR); + if (claim) + dir |= BIT(pin); + else + dir &= ~BIT(pin); + writel(dir, bank + S5L8702_I2C_BANK_DIR); + + writel(((gpio >> 3) << 16) | (pin << 8) | func, i2c_dev->gpiocmd); +} + +static void s5l8702_i2c_pad_enable(struct s5l8702_i2c_dev *i2c_dev, + unsigned int gpio, bool on) +{ + void __iomem *bank; + unsigned int pin; + u32 v; + + if (!i2c_dev->gpio) + return; + bank = i2c_dev->gpio + S5L8702_I2C_BANK_STRIDE * (gpio >> 3); + pin = gpio & 7; + v = readl(bank + S5L8702_I2C_BANK_ENABLE); + if (on) + v |= BIT(pin); + else + v &= ~BIT(pin); + writel(v, bank + S5L8702_I2C_BANK_ENABLE); +} + +static void s5l8702_i2c_pads(struct s5l8702_i2c_dev *i2c_dev, bool claim) +{ + unsigned int i; + + if (!i2c_dev->npads) + return; + + if (claim) { + for (i = 0; i < i2c_dev->npads; i++) + s5l8702_i2c_pad_func(i2c_dev, i2c_dev->pads[i], + S5L8702_I2C_PAD_FUNC, true); + s5l8702_i2c_pad_enable(i2c_dev, i2c_dev->pads[0], true); + dev_info(i2c_dev->dev, "pads %u/%u -> function %u\n", + i2c_dev->pads[0], i2c_dev->pads[1], + S5L8702_I2C_PAD_FUNC); + } else { + s5l8702_i2c_pad_enable(i2c_dev, i2c_dev->pads[0], false); + for (i = 0; i < i2c_dev->npads; i++) + s5l8702_i2c_pad_func(i2c_dev, i2c_dev->pads[i], + S5L8702_I2C_PAD_RELEASE, false); + } +} + +static int s5l8702_i2c_pads_init(struct s5l8702_i2c_dev *i2c_dev, + struct device *dev) +{ + u32 pads[S5L8702_I2C_PADS]; + int n; + + n = of_property_count_u32_elems(dev->of_node, "apple,pads"); + if (n <= 0) + return 0; + if (n != S5L8702_I2C_PADS) { + dev_warn(dev, "apple,pads needs %d entries, got %d\n", + S5L8702_I2C_PADS, n); + return 0; + } + if (of_property_read_u32_array(dev->of_node, "apple,pads", pads, n)) + return 0; + + i2c_dev->gpio = devm_ioremap(dev, S5L8702_I2C_GPIO_PHYS, + S5L8702_I2C_GPIO_LEN); + i2c_dev->gpiocmd = devm_ioremap(dev, S5L8702_I2C_GPIOCMD_PHYS, 4); + if (!i2c_dev->gpio || !i2c_dev->gpiocmd) { + dev_warn(dev, "pad mux unavailable (ioremap)\n"); + return 0; + } + i2c_dev->pads[0] = pads[0]; + i2c_dev->pads[1] = pads[1]; + i2c_dev->npads = S5L8702_I2C_PADS; + return 0; +} + static int s5l8702_i2c_probe(struct platform_device *pdev) { struct s5l8702_i2c_dev *i2c_dev; @@ -688,6 +815,9 @@ static int s5l8702_i2c_probe(struct platform_device *pdev) return ret; } + s5l8702_i2c_pads_init(i2c_dev, &pdev->dev); + s5l8702_i2c_pads(i2c_dev, true); + s5l8702_i2c_init(i2c_dev); i2c_dev->irq = platform_get_irq(pdev, 0); diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c index 75da807390cce8..d229d5db02b7a4 100755 --- a/drivers/input/touchscreen/apple-nimbus.c +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -112,6 +112,7 @@ #define NIMBUS_ACK_CHUNK 0x4BC1 /* 19393 */ #define NIMBUS_ACK_34AD0 0x4AD1 /* 19153 */ #define NIMBUS_POST_POKE 0x011F /* 287 */ +#define NIMBUS_EXEC_SETTLE_MS 40 /* 273A0 tail: 410522(40) */ #define NIMBUS_GPIO_EN 0x0E #define NIMBUS_GPIO_RST 0x27 @@ -142,9 +143,30 @@ #define NIMBUS_CS_BEGIN BIT(0) #define NIMBUS_CS_END BIT(1) -static int spi_clkdiv = 16; +/* + * 0 lets the SPI controller own the engine setup, which is what the stock + * firmware does: one sub_11B70 configuration, divider 2 for a 12 MHz bus. + * This driver used to reprogram the same registers afterwards with a much + * larger divider, quietly running the bus eight times too slow. + */ +static int spi_clkdiv; module_param(spi_clkdiv, int, 0644); -MODULE_PARM_DESC(spi_clkdiv, "SPI2 CLKDIV (higher=slower; try 8-32 for FW download)"); +MODULE_PARM_DESC(spi_clkdiv, + "Override SPI2 CLKDIV (0 = leave it to the SPI controller)"); +/* + * The stock code retries the transfer itself three times before giving + * up; the caller's retry re-runs the whole bring-up. + */ +static unsigned int download_tries = 3; +module_param(download_tries, uint, 0644); +MODULE_PARM_DESC(download_tries, + "Firmware download attempts before re-running bring-up"); + +/* + * The stock path does nothing after a good download but wait 2 ms and + * enable the interrupt. These register pokes keep speaking the boot + * protocol at a part that should already be running its application. + */ static int reset_hold_ms = 5; module_param(reset_hold_ms, int, 0644); MODULE_PARM_DESC(reset_hold_ms, "RST low ms in optional extra_por_pulse (1A5AC uses 5)"); @@ -384,22 +406,32 @@ static void nimbus_power_down(struct nimbus *n) */ static void nimbus_spi2_11b70(struct nimbus *n) { + void (*reinit)(void); u32 setup; if (!n->spi2) return; - writel(0xf, n->spi2 + SPI2_STATUS); - writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, - n->spi2 + SPI2_CTRL); - writel(10, n->spi2 + 0x44); - writel(24, n->spi2 + 0x38); /* 24 * a4=1 */ - writel(255, n->spi2 + 0x40); - writel(144, n->spi2 + 0x3c); /* 3 * 24 * (1+1) */ - writel(clamp(spi_clkdiv, 1, 255), n->spi2 + SPI2_CLKDIV); - writel(SPI2_SETUP_11B70, n->spi2 + SPI2_SETUP); - writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, - n->spi2 + SPI2_CTRL); - writel(SPI2_CTRL_ENABLE, n->spi2 + SPI2_CTRL); + reinit = (void (*)(void))__symbol_get("s5l8702_spi2_reinit"); + if (reinit) { + reinit(); + __symbol_put("s5l8702_spi2_reinit"); + } else { + /* Controller absent: same sequence, same numbers. */ + writel(0xf, n->spi2 + SPI2_STATUS); + writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, + n->spi2 + SPI2_CTRL); + writel(10, n->spi2 + 0x44); + writel(24, n->spi2 + 0x38); /* 24 * a4=1 */ + writel(255, n->spi2 + 0x40); + writel(144, n->spi2 + 0x3c); /* 3 * 24 * (1+1) */ + writel(2, n->spi2 + SPI2_CLKDIV); /* 24000/12000 */ + writel(SPI2_SETUP_11B70, n->spi2 + SPI2_SETUP); + writel(readl(n->spi2 + SPI2_CTRL) | SPI2_CTRL_FIFO_RST, + n->spi2 + SPI2_CTRL); + writel(SPI2_CTRL_ENABLE, n->spi2 + SPI2_CTRL); + } + if (spi_clkdiv) + writel(clamp(spi_clkdiv, 1, 255), n->spi2 + SPI2_CLKDIV); setup = readl(n->spi2 + SPI2_SETUP); nimbus_vinfo(n, "11B70 SPI2 SETUP=0x%x CLKDIV=%u\n", setup, readl(n->spi2 + SPI2_CLKDIV)); @@ -606,8 +638,18 @@ static int nimbus_probe_26494(struct nimbus *n, const char *tag) "26494 %s ret=%d words 0x%04x 0x%04x known=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", tag, ret, w0, w1, nimbus_opcode_known(w0) && nimbus_opcode_known(w1), rx[0], rx[1], rx[2], rx[3], rx[4], rx[5], rx[6], rx[7]); - if (ret) - return ret; + /* + * The stock probe only rejects the part when the transfer worked and + * came back with words it does not recognise. A failed transfer is not + * a verdict, and treating it as one meant a single soft SPI error + * skipped the firmware download entirely. + */ + if (ret) { + dev_warn(&n->spi->dev, + "26494 %s transfer %d; continuing to download\n", + tag, ret); + return 0; + } if (!nimbus_opcode_known(w0) || !nimbus_opcode_known(w1)) return -EIO; return 0; @@ -886,6 +928,13 @@ static void nimbus_fw_readback(struct nimbus *n, const char *tag) u8 *buf; unsigned int i; + /* + * Six 4 KB reads in the middle of the download sequence. Harmless to + * look at, but not something to put on the bus by default. + */ + if (!verbose) + return; + buf = kmalloc(0x1000, GFP_KERNEL); if (!buf) return; @@ -913,6 +962,9 @@ static void nimbus_cal_readback(struct nimbus *n, const u8 *upload) u8 *buf; u32 crc_chip, crc_host; + if (!verbose) + return; + buf = kmalloc(NIMBUS_FW_HDR_LEN, GFP_KERNEL); if (!buf) return; @@ -1626,8 +1678,13 @@ static int nimbus_post_download(struct nimbus *n) nimbus_vinfo(n, "34AD0[%d] %d\n", i, ret); if (ret) return ret; - /* 4AD1 = write ACK only; verify with RDREG while still in HBPP. */ - if (nimbus_rdreg(n, pokes[i].a1, &rb) == 0) + /* + * 4AD1 acknowledges the write without echoing it, so the only + * way to see the value is a separate RDREG. Stock sub_2D5B0 + * does not do this, and it inserts a transfer between pokes + * that the part was not told to expect. Verbose runs only. + */ + if (verbose && nimbus_rdreg(n, pokes[i].a1, &rb) == 0) nimbus_vinfo(n, "34AD0[%d] RDREG 0x%08x -> 0x%08x (wrote %u)\n", i, pokes[i].a1, rb, pokes[i].a2); @@ -1703,6 +1760,9 @@ static void nimbus_pre_exec_verify(struct nimbus *n) }; unsigned int i; + if (!verbose) + return; + for (i = 0; i < ARRAY_SIZE(addrs); i++) { u32 v = 0; @@ -1723,8 +1783,11 @@ static int nimbus_cmd_2d54c(struct nimbus *n) nimbus_pre_exec_verify(n); ret = nimbus_cmd_2d54c_raw(n, exec_addr, exec_word1); - if (!ret) + if (!ret) { n->exec_sent = true; + /* Stock waits 40 ms here before touching the part again. */ + msleep(NIMBUS_EXEC_SETTLE_MS); + } return ret; } @@ -2541,6 +2604,7 @@ static void nimbus_irq_enable(struct nimbus *n) static int nimbus_1a5ac_and_download(struct nimbus *n, const u8 *data, size_t size, const char *tag) { + unsigned int attempt; int err; /* Dump path has no pre-1A5AC POR; gate for glass A/B only. */ @@ -2561,7 +2625,18 @@ static int nimbus_1a5ac_and_download(struct nimbus *n, const u8 *data, dev_warn(&n->spi->dev, "26494 %s failed\n", tag); return -EIO; } - err = nimbus_download_fw(n, data, size, false); + /* + * Retry the transfer before escalating. The caller's retry re-runs the + * whole rail/reset/HBPP bring-up, which is a lot of disruption for what + * is usually a transient bus error; the stock code retries just this. + */ + for (attempt = 0; attempt < download_tries; attempt++) { + err = nimbus_download_fw(n, data, size, false); + if (!err) + break; + dev_warn(&n->spi->dev, "download %s attempt %u: %d\n", + tag, attempt + 1, err); + } n->fw_tried = true; return err; } @@ -2584,6 +2659,115 @@ static void nimbus_park(struct nimbus *n, const char *why) nimbus_power_down(n); } +/* ------------------------------------------------------------------ */ +/* Screen-sleep suspend / resume */ +/* */ +/* Two levels, because they trade power against wake latency: */ +/* */ +/* touch_power_down=0 IRQ masked and SPI2 released, controller still */ +/* powered. Resume is immediate. */ +/* touch_power_down=1 full 1A878 power cut including the PMU rail. */ +/* Resume has to re-run bring-up and download the */ +/* firmware again, so it costs a few hundred ms. */ +/* ------------------------------------------------------------------ */ + +static struct nimbus *nimbus_pm_dev; + +static bool touch_power_down = true; +module_param(touch_power_down, bool, 0644); +MODULE_PARM_DESC(touch_power_down, + "Cut the touch rail on screen sleep (default Y; N keeps it powered)"); + +static bool nimbus_pm_suspended; + +int n31_touch_suspend(void) +{ + struct nimbus *n = nimbus_pm_dev; + + if (!n) + return -ENODEV; + if (nimbus_pm_suspended) + return 0; + + if (n->irq > 0) + disable_irq(n->irq); + + if (touch_power_down) { + mutex_lock(&n->lock); + nimbus_power_down(n); + n->runtime_ready = false; + n->spi_ok = false; + mutex_unlock(&n->lock); + } else { + /* Stop driving the bus, leave the controller alive. */ + mutex_lock(&n->lock); + nimbus_spi2_pinmux(n, false); + mutex_unlock(&n->lock); + } + + nimbus_pm_suspended = true; + nimbus_vinfo(n, "touch suspended (power_down=%d)\n", touch_power_down); + return 0; +} +EXPORT_SYMBOL_GPL(n31_touch_suspend); + +int n31_touch_resume(void) +{ + struct nimbus *n = nimbus_pm_dev; + const struct firmware *fw = NULL; + const u8 *data = NULL; + u8 *kbuf = NULL; + size_t size = 0; + int ret = 0; + + if (!n) + return -ENODEV; + if (!nimbus_pm_suspended) + return 0; + + if (!touch_power_down) { + mutex_lock(&n->lock); + nimbus_spi2_pinmux(n, true); + mutex_unlock(&n->lock); + goto done; + } + + /* + * The controller lost its firmware with the rail, so this is the same + * bring-up the probe runs. Storage is back by the time a wake reaches + * here, so fetching the blob again is safe. + */ + ret = nimbus_acquire_fw(&n->spi->dev, &data, &size, &fw, &kbuf); + if (ret) { + dev_warn(&n->spi->dev, "touch resume: no firmware (%d)\n", ret); + goto done; + } + + mutex_lock(&n->lock); + n->fw_uploaded = false; + n->cal_uploaded = false; + n->requestcal_done = false; + n->exec_sent = false; + n->runtime_ready = false; + n->fw_loaded = false; + n->spi_ok = false; + ret = nimbus_1a5ac_and_download(n, data, size, "resume"); + mutex_unlock(&n->lock); + + nimbus_release_fw(fw, kbuf); + if (ret) + dev_warn(&n->spi->dev, "touch resume download: %d\n", ret); + +done: + if (n->irq > 0) + enable_irq(n->irq); + nimbus_pm_suspended = false; + nimbus_vinfo(n, "touch resumed (%d)\n", ret); + return ret; +} +EXPORT_SYMBOL_GPL(n31_touch_resume); + + static void nimbus_recycle(struct nimbus *n) { const struct firmware *fw = NULL; @@ -2795,6 +2979,7 @@ static int nimbus_probe(struct spi_device *spi) nimbus_verbose = verbose || !quiet; mutex_init(&n->lock); spi_set_drvdata(spi, n); + nimbus_pm_dev = n; n->gpio_base = devm_ioremap(&spi->dev, S5L8740_GPIO_PHYS, 0x400); n->gpiocmd = devm_ioremap(&spi->dev, S5L8740_GPIOCMD_PHYS, 4); @@ -2972,6 +3157,7 @@ static int nimbus_probe(struct spi_device *spi) static void nimbus_remove(struct spi_device *spi) { + nimbus_pm_dev = NULL; struct nimbus *n = spi_get_drvdata(spi); n->stopped = true; diff --git a/drivers/spi/spi-s5l8702.c b/drivers/spi/spi-s5l8702.c index e08d34857dc19d..f5c9cfd739132d 100755 --- a/drivers/spi/spi-s5l8702.c +++ b/drivers/spi/spi-s5l8702.c @@ -64,6 +64,20 @@ #define SPI_WAIT_GUARD 500000 +/* Verbose 11B70/CS setup spam off by default. */ +static bool verbose; +module_param(verbose, bool, 0644); +MODULE_PARM_DESC(verbose, "Verbose S5L SPI controller logs (default N)"); + +#define spi_vinfo(dev, fmt, ...) \ + do { \ + if (verbose) \ + dev_info((dev), fmt, ##__VA_ARGS__); \ + else \ + dev_dbg((dev), fmt, ##__VA_ARGS__); \ + } while (0) + + struct s5l8702_spi { void __iomem *base; struct device *dev; @@ -151,7 +165,7 @@ static void s5l8702_spi0_11b70(struct s5l8702_spi *sspi) sspi->base + SPICTRL); writel(SPICTRL_ENABLE, sspi->base + SPICTRL); sspi->prepared = true; - dev_info(sspi->dev, + spi_vinfo(sspi->dev, "SPI0 11B70 SETUP=0x%x CLKDIV=%u dd=%u u3c=%u u40=255 u44=10\n", SPISETUP_SPI0_11B70, clkdiv, dd, u3c); } @@ -161,6 +175,8 @@ static void s5l8702_spi0_11b70(struct s5l8702_spi *sspi) * CLKDIV stays 2 (440A58(24000, 0x2EE0)). Do not use the generic * CLKDIV=4 path; that left +0x38/+0x3c at reset and ping RX was junk. */ +static struct s5l8702_spi *s5l8702_spi2_dev; + static void s5l8702_spi2_11b70(struct s5l8702_spi *sspi) { const unsigned int a4 = 1; @@ -182,11 +198,23 @@ static void s5l8702_spi2_11b70(struct s5l8702_spi *sspi) writel(SPISETUP_SPI0_11B70, sspi->base + SPISETUP); writel(SPICTRL_ENABLE, sspi->base + SPICTRL); sspi->prepared = true; - dev_info(sspi->dev, + spi_vinfo(sspi->dev, "SPI2 11B70 SETUP=0x%x CLKDIV=%u dd=%u u3c=%u (mode 0x1A)\n", SPISETUP_SPI0_11B70, clkdiv, dd, u3c); } +/* + * Re-run the SPI2 engine setup. The stock firmware reapplies sub_11B70 + * after every pinmux enable, and the touch driver does the same on + * bring-up; routing it here keeps one owner for the divider and timing. + */ +void s5l8702_spi2_reinit(void) +{ + if (s5l8702_spi2_dev) + s5l8702_spi2_11b70(s5l8702_spi2_dev); +} +EXPORT_SYMBOL_GPL(s5l8702_spi2_reinit); + static void s5l8702_spi_cs(struct s5l8702_spi *sspi, bool assert) { u32 pin = readl(sspi->base + SPIPIN); @@ -454,7 +482,7 @@ static int s5l8702_spi_probe(struct platform_device *pdev) if (ret) dev_warn(&pdev->dev, "clk_bulk_prepare_enable failed: %d\n", ret); else - dev_info(&pdev->dev, "enabled %d SPI clockgate(s)\n", sspi->num_clks); + spi_vinfo(&pdev->dev, "enabled %d SPI clockgate(s)\n", sspi->num_clks); } else { sspi->num_clks = 0; } @@ -476,7 +504,7 @@ static int s5l8702_spi_probe(struct platform_device *pdev) } s5l8702_spi0_pinmux(sspi); s5l8702_spi0_11b70(sspi); - dev_info(&pdev->dev, "SPI0 CS42 4043D0 CS=SPIPIN.1 PWRCON1=%08x\n", + spi_vinfo(&pdev->dev, "SPI0 CS42 4043D0 CS=SPIPIN.1 PWRCON1=%08x\n", readl(sspi->pwrcon1)); } else if (sspi->spi2_nimbus) { void __iomem *pwrcon4; @@ -494,11 +522,12 @@ static int s5l8702_spi_probe(struct platform_device *pdev) writel(readl(pwrcon4) & ~BIT(15), pwrcon4); /* SPI2_2 */ iounmap(pwrcon4); } - dev_info(&pdev->dev, "SPI2 PWRCON1=%08x (after ungate)\n", + spi_vinfo(&pdev->dev, "SPI2 PWRCON1=%08x (after ungate)\n", readl(sspi->pwrcon1)); s5l8702_spi2_pinmux(sspi); s5l8702_spi2_11b70(sspi); - dev_info(&pdev->dev, + s5l8702_spi2_dev = sspi; + spi_vinfo(&pdev->dev, "SPI2 Nimbus 4043D0 PIO (SETUP=0x%x CLKDIV=2 CS=SPIPIN.1 11B70)\n", SPISETUP_SPI0_11B70); } diff --git a/drivers/video/backlight/backlight-s5l8740.c b/drivers/video/backlight/backlight-s5l8740.c index 9d8017b26be77d..e6fac69ce89f89 100644 --- a/drivers/video/backlight/backlight-s5l8740.c +++ b/drivers/video/backlight/backlight-s5l8740.c @@ -18,25 +18,44 @@ * LCD backlight at 0x3E000000 for iPod nano 7G (N31). */ #include +#include #include #include #include #include +#include #include +#include #define S5L8740_BL_ENABLE_OFF 0x04 #define S5L8740_BL_LEVEL_OFF 0x08 #define S5L8740_BL_MAX 62 +/* + * The LED boost is the expensive part of the display, so screen sleep + * ramps it rather than cutting it. Stepping happens in a work item so a + * caller in a button handler does not block for the length of the fade. + */ struct s5l8740_bl { void __iomem *base; struct backlight_device *bd; + struct delayed_work fade; + struct mutex lock; + int level; /* what the hardware currently has */ + int target; + unsigned int step_ms; }; +static struct s5l8740_bl *s5l8740_bl_dev; + static void s5l8740_bl_hw_set(struct s5l8740_bl *bl, int level) { u32 en, lvl; + if (level > S5L8740_BL_MAX) + level = S5L8740_BL_MAX; + bl->level = level > 0 ? level : 0; + if (level <= 0) { en = readl(bl->base + S5L8740_BL_ENABLE_OFF); writel(en & ~BIT(0), bl->base + S5L8740_BL_ENABLE_OFF); @@ -45,9 +64,6 @@ static void s5l8740_bl_hw_set(struct s5l8740_bl *bl, int level) return; } - if (level > S5L8740_BL_MAX) - level = S5L8740_BL_MAX; - writel((u32)level, bl->base + S5L8740_BL_LEVEL_OFF); en = readl(bl->base + S5L8740_BL_ENABLE_OFF); writel(en | BIT(0), bl->base + S5L8740_BL_ENABLE_OFF); @@ -55,12 +71,79 @@ static void s5l8740_bl_hw_set(struct s5l8740_bl *bl, int level) writel(lvl | BIT(0), bl->base + S5L8740_BL_LEVEL_OFF); } +static void s5l8740_bl_fade_work(struct work_struct *work) +{ + struct s5l8740_bl *bl = container_of(to_delayed_work(work), + struct s5l8740_bl, fade); + bool more; + + mutex_lock(&bl->lock); + if (bl->level < bl->target) + s5l8740_bl_hw_set(bl, bl->level + 1); + else if (bl->level > bl->target) + s5l8740_bl_hw_set(bl, bl->level - 1); + more = bl->level != bl->target; + mutex_unlock(&bl->lock); + + if (more) + schedule_delayed_work(&bl->fade, + msecs_to_jiffies(bl->step_ms)); +} + +/* + * Ramp to `level` over roughly `ms`. ms = 0 jumps straight there, which is + * what the backlight class writes should do. Returns the level that was + * programmed before the fade started, so a caller can restore it on wake. + */ +int n31_backlight_fade(int level, unsigned int ms) +{ + struct s5l8740_bl *bl = s5l8740_bl_dev; + int previous, delta; + + if (!bl) + return -ENODEV; + if (level < 0) + level = 0; + if (level > S5L8740_BL_MAX) + level = S5L8740_BL_MAX; + + cancel_delayed_work_sync(&bl->fade); + mutex_lock(&bl->lock); + previous = bl->level; + bl->target = level; + delta = abs(level - bl->level); + if (!ms || !delta) { + s5l8740_bl_hw_set(bl, level); + mutex_unlock(&bl->lock); + return previous; + } + bl->step_ms = max_t(unsigned int, ms / delta, 1); + mutex_unlock(&bl->lock); + + schedule_delayed_work(&bl->fade, msecs_to_jiffies(bl->step_ms)); + return previous; +} +EXPORT_SYMBOL_GPL(n31_backlight_fade); + +/* Level the hardware currently has, ignoring any fade in progress. */ +int n31_backlight_level(void) +{ + struct s5l8740_bl *bl = s5l8740_bl_dev; + + return bl ? bl->level : -ENODEV; +} +EXPORT_SYMBOL_GPL(n31_backlight_level); + static int s5l8740_bl_update_status(struct backlight_device *bd) { struct s5l8740_bl *bl = bl_get_data(bd); int brightness = backlight_get_brightness(bd); + cancel_delayed_work_sync(&bl->fade); + mutex_lock(&bl->lock); + bl->target = brightness; s5l8740_bl_hw_set(bl, brightness); + mutex_unlock(&bl->lock); return 0; } @@ -83,6 +166,10 @@ static int s5l8740_bl_probe(struct platform_device *pdev) if (IS_ERR(bl->base)) return PTR_ERR(bl->base); + mutex_init(&bl->lock); + INIT_DELAYED_WORK(&bl->fade, s5l8740_bl_fade_work); + bl->target = S5L8740_BL_MAX; + /* Full brightness + enables (U-Boot path) */ s5l8740_bl_hw_set(bl, S5L8740_BL_MAX); @@ -96,6 +183,7 @@ static int s5l8740_bl_probe(struct platform_device *pdev) return PTR_ERR(bd); bl->bd = bd; + s5l8740_bl_dev = bl; platform_set_drvdata(pdev, bl); dev_info(dev, "S5L8740 backlight @%pR max=%u\n", platform_get_resource(pdev, IORESOURCE_MEM, 0), S5L8740_BL_MAX); @@ -109,8 +197,17 @@ static const struct of_device_id s5l8740_bl_of_match[] = { }; MODULE_DEVICE_TABLE(of, s5l8740_bl_of_match); +static void s5l8740_bl_remove(struct platform_device *pdev) +{ + struct s5l8740_bl *bl = platform_get_drvdata(pdev); + + s5l8740_bl_dev = NULL; + cancel_delayed_work_sync(&bl->fade); +} + static struct platform_driver s5l8740_bl_driver = { .probe = s5l8740_bl_probe, + .remove = s5l8740_bl_remove, .driver = { .name = "backlight-s5l8740", .of_match_table = s5l8740_bl_of_match, diff --git a/include/linux/apple-n31.h b/include/linux/apple-n31.h index 7525a6b94a8393..008bd1800739b1 100755 --- a/include/linux/apple-n31.h +++ b/include/linux/apple-n31.h @@ -42,6 +42,60 @@ int d1830_audio_rails(void); /* gpio-d1830.c — power the touch controller rail, for the Nimbus driver. */ int d1830_nimbus_rail(bool on); +/* + * gpio-d1830.c — refcounted rail control. Rail ids index the PMU rail + * table; a rail stays up while any consumer holds it and powers down a + * few seconds after the last release. + * + * Ownership below comes from the stock firmware's own rail dispatcher, + * which maps a consumer id onto a bit in PMU_ACTIVE_1/2; the call sites + * name the consumer. + * + * LDO_3 ACTIVE_1 bit5 touch controller + * LDO_4 ACTIVE_1 bit6 display, enabled immediately before LCDIF init + * LDO_5 ACTIVE_1 bit7 accessory port, voltage negotiated 2.5-3.3 V + * + * No stock path enables a rail for the audio codec: its analog supply is + * always on. Do not add one. + */ +#define N31_PMU_RAIL_TOUCH 2 /* PMU_LDO_3 */ +#define N31_PMU_RAIL_DISPLAY 3 /* PMU_LDO_4 */ +#define N31_PMU_RAIL_ACCESSORY 4 /* PMU_LDO_5 */ + +int n31_pmu_rail_get(unsigned int id); +void n31_pmu_rail_put(unsigned int id); + +/* + * backlight-s5l8740.c — ramp the LED boost. n31_backlight_fade() returns + * the level that was set before the ramp began, so a screen-sleep caller + * can restore exactly what the user had. ms = 0 applies immediately. + */ +int n31_backlight_fade(int level, unsigned int ms); +int n31_backlight_level(void); + +/* + * apple-nimbus.c — screen-sleep hooks for the touch controller. Whether + * the rail is cut is the driver's own touch_power_down parameter; the + * caller only says sleep or wake. + */ +int n31_touch_suspend(void); +int n31_touch_resume(void); + +/* + * s5l8740.c (DRM) — display power. Turning the panel off means stopping + * the LCDIF and dropping its rail; turning it back on resets and + * reprograms the interface and repaints. The panel itself needs no + * command sequence. + */ +int n31_lcd_power(bool on); +bool n31_lcd_is_on(void); + +/* spi-s5l8702.c — reapply the SPI2 engine setup after a pinmux change. */ +void s5l8702_spi2_reinit(void); + +/* cs42l81-spi.c — true while the analog play graph is latched. */ +bool n31_audio_playback_active(void); + /* nand-s5l8740.c — true once the FTL has a usable logical-to-virtual map. */ bool nand_ftl_present(void); diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index a6fd71134174e5..256b282f027352 100755 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -11,28 +11,20 @@ config SND_SOC_APPLE_NANO7 tristate "iPod nano 7G audio machine" depends on SND_SOC select SND_SOC_APPLE_S5L8740_I2S - select SND_SOC_APPLE_S5L8740_IIS2 select SND_SOC_APPLE_CS42L81_SPI help - Registers ASoC card: S5L8740 IIS0+CS42 playback and optional - IIS2 FM capture (local speakers/HP only; no FM→BT). + Registers the ASoC card: CS42L81 headphone playback on IIS0 and + BCM2078 FM capture on IIS2, as the two PCMs of one card. config SND_SOC_APPLE_S5L8740_I2S - tristate "S5L8740 IIS0 I2S CPU DAI" + tristate "S5L8740 I2S CPU DAIs (IIS0 playback, IIS2 capture)" depends on SND_SOC && HAS_IOMEM select SND_SOC_GENERIC_DMAENGINE_PCM select SND_DMAENGINE_PCM help - IIS0 @0x3CA00000 CPU DAI with optional PL080 dmaengine PCM. - -config SND_SOC_APPLE_S5L8740_IIS2 - tristate "S5L8740 IIS2 FM capture CPU DAI" - depends on SND_SOC && HAS_IOMEM - select SND_SOC_GENERIC_DMAENGINE_PCM - select SND_DMAENGINE_PCM - help - IIS2 @0x3D400000 FM digital RX (peri 13 ← FIFO +0x38). Oracle - register program from RetailOS fm-playing MMIO. Local PCM only. + IIS0 @0x3CA00000 playback (PL080 peri 10) and IIS2 @0x3D400000 + BCM2078 digital PCM capture (peri 13). Both ports are in one + driver because they share the SoC audio clock gate. config SND_SOC_APPLE_CS42L81_SPI tristate "CS42L81 / 338S1146 SPI codec (N31)" diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index d4dc2d1bc173ce..712e5844679602 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -4,4 +4,3 @@ obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o obj-$(CONFIG_SND_SOC_APPLE_NANO7) += nano7-audio.o obj-$(CONFIG_SND_SOC_APPLE_CS42L81_SPI) += cs42l81-spi.o obj-$(CONFIG_SND_SOC_APPLE_S5L8740_I2S) += s5l8740-i2s.o -obj-$(CONFIG_SND_SOC_APPLE_S5L8740_IIS2) += s5l8740-iis2.o diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c index c4059abb3dd7f9..d40bb7721dd6e3 100755 --- a/sound/soc/apple/cs42l81-spi.c +++ b/sound/soc/apple/cs42l81-spi.c @@ -68,14 +68,33 @@ #include #include #include +#include #include #include #include +#include + #include "n31-audio-rates.h" -#define CS42L81_USER_VOL_MAX 256 -#define CS42L81_VOL_STEP 16 /* ~16 presses full 0..256 range */ +/* + * Output gain, register 0x0227. sub_400330 takes a signed value and + * writes the low seven bits of it; sub_D2C98 turns a dB figure into that + * value: + * + * -50 dB .. +12 dB code = dB (1 dB per step) + * below -50 dB code = -50 + (dB+50)/2 (2 dB per step) + * clamped to -76 dB .. +12 dB, i.e. code -63 .. +12 + * + * The mixer control is in whole dB so it stays linear for the user; the + * kink lives in the register encoding, not in the control. + */ +#define CS42L81_VOL_DB_MIN (-76) +#define CS42L81_VOL_DB_MAX 12 +#define CS42L81_VOL_DB_KNEE (-50) +#define CS42L81_USER_VOL_MAX (CS42L81_VOL_DB_MAX - CS42L81_VOL_DB_MIN) +#define CS42L81_VOL_DEFAULT (CS42L81_USER_VOL_MAX - 32) /* -20 dB */ +#define CS42L81_VOL_STEP 4 /* dB per Vol+/- press */ /* * Play rate for D34C0/183138. 0 = follow PCM hw_params (OSOS 44100 if none). @@ -301,7 +320,6 @@ void cs42l81_cancel_post_iis(void); static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val); static int cs42l81_apply_user_vol(struct cs42l81 *c); static void cs42l81_log_start_state(struct cs42l81 *c, const char *tag); -static void cs42l81_push_pcm_q8(unsigned int vol); static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val) { @@ -1182,11 +1200,38 @@ static void cs42_jack_poll_start(struct cs42l81 *c) * OSOS sub_400330 → sub_3FA0E0(551=0x227, value&0x7f). * D3280(4) calls this with 64 then 65 before final 0x229=0x41. */ -static int cs42l81_set_output_gain(struct cs42l81 *c, u8 val) +/* Mixer position (0..CS42L81_USER_VOL_MAX) to dB. */ +static int cs42l81_vol_to_db(unsigned int vol) { - if (val & 0x40) - val |= 0x80; - return cs42l81_write(c, 0x0227, val); + return (int)vol + CS42L81_VOL_DB_MIN; +} + +/* sub_D2C98: dB to the signed code that register 0x0227 carries. */ +static int cs42l81_db_to_code(int db) +{ + if (db > CS42L81_VOL_DB_MAX) + db = CS42L81_VOL_DB_MAX; + if (db < CS42L81_VOL_DB_MIN) + db = CS42L81_VOL_DB_MIN; + if (db > CS42L81_VOL_DB_KNEE) + return db; + /* + * Below the knee the register carries 2 dB per code, and sub_D2C98 + * steps an odd figure down before halving it rather than rounding. + */ + if (db & 1) + db--; + return CS42L81_VOL_DB_KNEE + (db - CS42L81_VOL_DB_KNEE) / 2; +} + +/* + * sub_400330 sign-extends bit 6 only so it can compare the value, then + * writes `v4 & 0x7F`. Writing the sign-extended byte instead sets a bit 7 + * that is not part of the gain field. + */ +static int cs42l81_set_output_gain(struct cs42l81 *c, int code) +{ + return cs42l81_write(c, 0x0227, (u8)(code & 0x7f)); } /* @@ -1596,7 +1641,7 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) cs42l81_read(c, 0x0227, &st); cs42l81_read(c, 0x0219, &r219); - cs42l81_push_pcm_q8(c->user_vol); + cs42l81_apply_user_vol(c); cs42_log_graph_snapshot(c, "pre_play"); cs42l81_log_start_state(c, "codec_prepare"); dev_info(&c->spi->dev, @@ -1660,24 +1705,22 @@ static int __maybe_unused cs42l81_set_mute(struct cs42l81 *c, int mute) return 0; } -static void cs42l81_push_pcm_q8(unsigned int vol) -{ - void (*set)(unsigned int); - - set = (void (*)(unsigned int))__symbol_get("s5l8740_set_user_vol_q8"); - if (set) { - set(vol); - __symbol_put("s5l8740_set_user_vol_q8"); - } -} - -/* User vol mute during play: F141C(0) only — not full 42D364 teardown. */ +/* + * Push the mixer state at the hardware: the gain register always carries + * the selected level, and mute is the codec's own analog mute (F141C / + * 0x0527) rather than a digital scale. Nothing here scales PCM in + * software -- that only ever worked on the PIO path, and the codec gain + * applies to both paths anyway. + */ static int cs42l81_apply_user_vol(struct cs42l81 *c) { - unsigned int q8 = c->dai_mute ? 0 : c->user_vol; + int ret; - cs42l81_push_pcm_q8(q8); - if (q8 == 0) + ret = cs42l81_set_output_gain(c, + cs42l81_db_to_code(cs42l81_vol_to_db(c->user_vol))); + if (ret) + return ret; + if (c->dai_mute) return cs42_f141c_play_unmute(c, false); if (c->play_started) return cs42_play_unmute(c); @@ -1695,8 +1738,9 @@ static void cs42l81_log_start_state(struct cs42l81 *c, const char *tag) cs42l81_read(c, 0x0219, &r219); cs42l81_read(c, 0xc96f, &rc96f); dev_info(&c->spi->dev, - "CS42 %s: vol=%u/%u dai_mute=%d 2F=%02x 527=%02x 401=%02x 219=%02x C96F=%02x\n", - tag, c->user_vol, CS42L81_USER_VOL_MAX, c->dai_mute, + "CS42 %s: vol=%u/%u (%d dB) dai_mute=%d 2F=%02x 527=%02x 401=%02x 219=%02x C96F=%02x\n", + tag, c->user_vol, CS42L81_USER_VOL_MAX, + cs42l81_vol_to_db(c->user_vol), c->dai_mute, r2f, r527, r401, r219, rc96f); } @@ -1877,11 +1921,13 @@ static ssize_t volume_show(struct device *dev, struct device_attribute *attr, if (ra || rb || rc) return sysfs_emit(buf, "read err %d/%d/%d\n", ra, rb, rc); return sysfs_emit(buf, - "user=%u/%u dai_mute=%d\n" + "user=%u/%u (%d dB, 0x227 code %d) dai_mute=%d\n" "0x403=0x%02x 0x404=0x%02x (taps, play 2/1)\n" "0x527=0x%02x analog_mute=%d\n", - c->user_vol, CS42L81_USER_VOL_MAX, c->dai_mute, - tap_l, tap_r, analog, analog == 0xff); + c->user_vol, CS42L81_USER_VOL_MAX, + cs42l81_vol_to_db(c->user_vol), + cs42l81_db_to_code(cs42l81_vol_to_db(c->user_vol)), + c->dai_mute, tap_l, tap_r, analog, analog == 0xff); } static DEVICE_ATTR_RW(volume); @@ -2333,6 +2379,19 @@ static int cs42l81_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream return 0; } +/* + * True while the analog play graph is latched. Used by the power-button + * policy to decide how deeply it may sleep; the codec has no dedicated + * PMU rail to infer this from. + */ +bool n31_audio_playback_active(void) +{ + struct cs42l81 *c = cs42l81_dev; + + return c && c->play_started; +} +EXPORT_SYMBOL_GPL(n31_audio_playback_active); + static const struct snd_soc_dai_ops cs42l81_dai_ops = { .hw_params = cs42l81_dai_hw_params, .trigger = cs42l81_dai_trigger, @@ -2366,7 +2425,7 @@ static void cs42l81_notify_master_vol(struct cs42l81 *c) if (!comp || !comp->card || !comp->card->snd_card) return; - kctl = snd_soc_component_get_kcontrol(comp, "Master Playback Volume"); + kctl = snd_soc_component_get_kcontrol(comp, "Headphones Playback Volume"); if (!kctl) return; snd_ctl_notify(comp->card->snd_card, SNDRV_CTL_EVENT_MASK_VALUE, @@ -2374,7 +2433,7 @@ static void cs42l81_notify_master_vol(struct cs42l81 *c) } /* - * Vol± from gpio-s5l8740 (KEY_VOLUMEUP/DOWN) → Master Playback Volume. + * Vol± from gpio-s5l8740 (KEY_VOLUMEUP/DOWN) → Headphones Playback Volume. * Input softirq must not SPI; defer apply + ALSA notify to process context. */ static void cs42l81_vol_workfn(struct work_struct *work) @@ -2413,8 +2472,9 @@ static void cs42l81_vol_workfn(struct work_struct *work) if (vol != prev || unmute) { cs42l81_notify_master_vol(c); dev_info(&c->spi->dev, - "Vol%c → Master %u/%u%s\n", + "Vol%c → Headphones %u/%u (%d dB)%s\n", delta > 0 ? '+' : '-', vol, CS42L81_USER_VOL_MAX, + cs42l81_vol_to_db(vol), unmute ? " (unmuted)" : ""); } } @@ -2460,7 +2520,7 @@ static int cs42l81_input_connect(struct input_handler *handler, if (err) goto err_unregister; - dev_info(&c->spi->dev, "Vol± keys → Master Playback Volume (%s)\n", + dev_info(&c->spi->dev, "Vol± keys → Headphones Playback Volume (%s)\n", dev->name ? dev->name : "input"); return 0; @@ -2546,17 +2606,24 @@ static int cs42l81_sw_put(struct snd_kcontrol *kcontrol, return changed; } +/* Whole-dB steps, so a plain dB scale describes the control exactly. */ +static const DECLARE_TLV_DB_SCALE(cs42l81_hp_tlv, + CS42L81_VOL_DB_MIN * 100, 100, 0); + static const struct snd_kcontrol_new cs42l81_controls[] = { { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, - .name = "Master Playback Volume", + .name = "Headphones Playback Volume", + .access = SNDRV_CTL_ELEM_ACCESS_READWRITE | + SNDRV_CTL_ELEM_ACCESS_TLV_READ, .info = cs42l81_vol_info, .get = cs42l81_vol_get, .put = cs42l81_vol_put, + .tlv.p = cs42l81_hp_tlv, }, { .iface = SNDRV_CTL_ELEM_IFACE_MIXER, - .name = "Master Playback Switch", + .name = "Headphones Playback Switch", .info = cs42l81_sw_info, .get = cs42l81_sw_get, .put = cs42l81_sw_put, @@ -2600,7 +2667,7 @@ static int cs42l81_probe(struct spi_device *spi) if (!c) return -ENOMEM; c->spi = spi; - c->user_vol = CS42L81_USER_VOL_MAX; + c->user_vol = CS42L81_VOL_DEFAULT; c->dai_mute = false; mutex_init(&c->lock); atomic_set(&c->vol_steps, 0); diff --git a/sound/soc/apple/nano7-audio.c b/sound/soc/apple/nano7-audio.c index 0c571f370eff0a..f0a229bec01dc7 100755 --- a/sound/soc/apple/nano7-audio.c +++ b/sound/soc/apple/nano7-audio.c @@ -1,14 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * N31 ASoC machine — IIS0+CS42 playback + IIS2 FM capture (local only). + * iPod nano 7G ASoC machine. * - * Playback: IIS0 CPU DAI + CS42L81 SPI codec (peri 10 TX). - * Capture: IIS2 CPU DAI + snd-soc-dummy (peri 13 RX). Userspace loops - * arecord/tinycap → aplay/tinyplay. No FM→BT / A2DP path. + * One card, two PCMs: + * playback IIS0 -> CS42L81 -> 3.5 mm headphones (PL080 peri 10) + * capture IIS2 <- BCM2078 digital PCM (FM) (PL080 peri 13) * - * CS42 path has no snd_soc_dapm_route table — analog routing uses explicit - * register writes: cs42_codec_prepare() + cs42_retailos_play_start/stop(). - * (SoC master, codec slave, 16-bit S16_LE). + * The two ports face different chips, which is why the capture side has + * no codec of its own and uses the dummy DAI. Bluetooth audio is not + * here and cannot be: A2DP is host-encoded and leaves over UART1 HCI. + * + * The CS42 path has no DAPM route table. Analog routing is done with + * explicit register writes in cs42_codec_prepare() and + * cs42_retailos_play_start/stop(). SoC is master, codec is slave, S16_LE. */ #include #include @@ -27,15 +31,21 @@ SND_SOC_DAILINK_DEFS(fm_capture, static struct snd_soc_dai_link nano7_dais[] = { { - .name = "CS42L81", - .stream_name = "Playback", + .name = "CS42L81 Headphones", + .stream_name = "Headphones", SND_SOC_DAILINK_REG(playback), .playback_only = 1, + /* + * The IIS0 trigger latches the codec play graph over SPI, and + * spi_sync() sleeps. ASoC runs trigger atomically unless the + * link opts out, so this is required, not a preference. + */ + .nonatomic = 1, .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, }, { - .name = "BCM2078-PCM", + .name = "BCM2078 FM", .stream_name = "BCM2078 PCM Capture", SND_SOC_DAILINK_REG(fm_capture), .capture_only = 1, @@ -106,8 +116,8 @@ static int nano7_audio_probe(struct platform_device *pdev) return ret; } - dev_info(dev, "nano7g-audio: IIS0+CS42 play%s (no FM→BT)\n", - nano7_card.num_links > 1 ? " + BCM2078 PCM capture (IIS2 RX)" : ""); + dev_info(dev, "nano7g-audio: headphone playback%s\n", + nano7_card.num_links > 1 ? " + BCM2078 FM capture" : ""); return 0; } @@ -128,5 +138,5 @@ static struct platform_driver nano7_audio_driver = { module_platform_driver(nano7_audio_driver); MODULE_LICENSE("GPL"); -MODULE_DESCRIPTION("iPod nano 7G ASoC machine (play + FM capture)"); -MODULE_SOFTDEP("pre: cs42l81_spi s5l8740_i2s s5l8740_iis2"); +MODULE_DESCRIPTION("iPod nano 7G ASoC machine (headphones + FM capture)"); +MODULE_SOFTDEP("pre: cs42l81_spi s5l8740_i2s"); diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c index 39dc31a70b5b06..b8598b8f5bd22e 100755 --- a/sound/soc/apple/s5l8740-i2s.c +++ b/sound/soc/apple/s5l8740-i2s.c @@ -1,7 +1,23 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * S5L8740 IIS0 (I2S) platform DAI — N31 - * IIS0 @ 0x3CA00000, TX FIFO @ +0x10. Optional PL080 dmaengine PCM. + * S5L8740 I2S platform DAIs — N31 + * + * Both of the board's audio ports live here because they share the + * SoC audio clock gate at CLKCON+0x30, and arbitrating that across two + * modules is not worth the symbol traffic: + * + * IIS0 @ 0x3CA00000 TX FIFO +0x10, PL080 peri 10 -> CS42L81 headphones + * IIS2 @ 0x3D400000 RX FIFO +0x38, PL080 peri 13 <- BCM2078 digital PCM + * + * They face different chips, but to userspace they are simply the + * playback and capture PCMs of one card. + * + * IIS2 register program is from the RetailOS fm-playing MMIO capture: + * CLKCON +0x00 = 0x1 TXCON +0x04 = 0x0b000099 + * RXCON +0x30 = 0x1000 RXCOM +0x34 = 0x6 running, 0x2 idle + * CLKDIV +0x40 = 0x96 REG44 +0x44 = 0x00010007 + * IIS1 @ 0x3CD00000 is XSP and always reads zero -- it is not a BCM port. + * A2DP does not appear here at all: it is host-encoded over UART1 HCI. */ #include #include @@ -16,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +54,7 @@ #define I2STXFIFO 0x10 #define I2SRXCON 0x30 #define I2SRXCOM 0x34 +#define I2SRXFIFO 0x38 #define I2SSTATUS 0x3c #define I2SCLKDIV 0x40 /* OSOS 4F716: *(base+64). Not Rockbox +0x24. */ /* RetailOS music IIS0+0x44 readback 0x00010007 (oracle 2026-08-25). */ @@ -93,7 +111,8 @@ MODULE_PARM_DESC(sustain_ms, "ignore ALSA STOP for this many ms after START (def static uint fifo_prefill = 16; module_param(fifo_prefill, uint, 0644); -MODULE_PARM_DESC(fifo_prefill, "stereo words to push into TX FIFO before TXCOM kick"); +MODULE_PARM_DESC(fifo_prefill, + "silent stereo words to push into TX FIFO before TXCOM kick"); /* * OSOS B6620(port,0) does TXCOM |= 6 after PL080 is armed (peri 10). * RE body: sub_B6620 only ORs 0x6 — not 0xC. Hybrid 0xE was Linux invention. @@ -106,6 +125,42 @@ MODULE_PARM_DESC(fifo_prefill, "stereo words to push into TX FIFO before TXCOM k #define CLKCON_AUDIO_OFF 0x30 #define CLKCON_AUDIO_PLAY 0x32190u #define CLKCON_AUDIO_IDLE 0x1c20u +/* FM additionally regates CLKCON+0x10; music/idle leaves it at 0x8004. */ +#define CLKCON_FM_GATE 0x10 +#define CLKCON_FM_GATE_ON 0x4u +#define CLKCON_FM_GATE_IDLE 0x8004u + +/* fm-playing oracle values for the IIS2 side. */ +#define IIS2_CLKCON_ON 0x1u +#define IIS2_TXCON_FM 0x0b000099u +#define IIS2_RXCON_FM 0x1000u +#define IIS2_RXCOM_DMA 0x6u +#define IIS2_RXCOM_IDLE 0x2u +#define IIS2_CLKDIV_FM_ORACLE 0x96u +#define IIS2_REG44_ORACLE 0x00010007u +#define IIS2_REGS_LEN 0x48 + +/* + * IIS2 PCM pads. sub_15DD5C claims these three at function 2 when FM + * powers on and releases them to input when it powers off, in the same + * breath as programming device 2 (0x3D400000) and kicking RXCOM -- so + * they belong to this port, not to the Bluetooth controller. They were + * previously described as BCM shutdown / device-wakeup / host-wakeup and + * handed to hci_bcm, which drove the capture bus as GPIOs. + * + * Claimed alongside the register program rather than from FM power-on, + * so opening the capture PCM works regardless of who owns the tuner. + */ +#define IIS2_PAD_BCLK 97 /* 0x61 */ +#define IIS2_PAD_SYNC 98 /* 0x62 */ +#define IIS2_PAD_DATA 119 /* 0x77 */ +#define IIS2_PAD_FUNC 2 +#define IIS2_PAD_RELEASE 0 + +/* Ports sharing the CLKCON+0x30 gate. */ +#define S5L8740_AUDIO_PORT_IIS0 0 +#define S5L8740_AUDIO_PORT_IIS2 1 +#define S5L8740_AUDIO_PORTS 2 #define GPIO_PHYS 0x3cf00000ul #define GPIOCMD_PHYS 0x3cf001e0ul @@ -286,11 +341,39 @@ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) } /* RetailOS absolute dword — better than sticky play when idle. */ +/* + * CLKCON+0x30 gates the audio clock for IIS0 and IIS2 together. Each port + * used to write it directly, so stopping FM capture also idled the clock + * out from under music that was still playing. Track which ports want it + * running and only idle the gate once nobody does. + */ +static DEFINE_SPINLOCK(s5l8740_audio_clk_lock); +static bool s5l8740_audio_clk_wanted[S5L8740_AUDIO_PORTS]; + +static void s5l8740_audio_clk_set(void __iomem *clkcon, unsigned int port, + bool on) +{ + unsigned long flags; + unsigned int i; + bool any = false; + + if (!clkcon) + return; + spin_lock_irqsave(&s5l8740_audio_clk_lock, flags); + s5l8740_audio_clk_wanted[port] = on; + for (i = 0; i < S5L8740_AUDIO_PORTS; i++) + any |= s5l8740_audio_clk_wanted[i]; + writel(any ? CLKCON_AUDIO_PLAY : CLKCON_AUDIO_IDLE, + clkcon + CLKCON_AUDIO_OFF); + spin_unlock_irqrestore(&s5l8740_audio_clk_lock, flags); +} + static void s5l8740_i2s_clkcon_audio(struct s5l8740_i2s *i2s, u32 val) { - if (!i2s || !i2s->clkcon) + if (!i2s) return; - writel(val, i2s->clkcon + CLKCON_AUDIO_OFF); + s5l8740_audio_clk_set(i2s->clkcon, S5L8740_AUDIO_PORT_IIS0, + val != CLKCON_AUDIO_IDLE); } /* @@ -368,7 +451,23 @@ static void s5l8740_i2s_log_iis_gpio(struct s5l8740_i2s *i2s, const char *tag) } } -/* sub_43D38C(7,3) and (20,3) — IIS0 pads. Optional (6,3) in pad_mode 1/4. */ +/* + * GPIO 4/5 are deliberately left alone. Two stock paths claim them and it + * is not settled which applies here: sub_71B8 drives them as a two-bit + * output mux, while the per-bus I2C pinmux helper puts them at function 2 + * as a SCL/SDA pair. i2c0 is the Tristar bus and it times out on glass, + * so the I2C reading is the more likely one and forcing them to outputs + * would make that permanent. Nothing in the audio path needs them. + */ + +/* + * sub_BCB60 muxes both IIS0 pads together on every TX enable: + * sub_43D38C(0x14, 3) and sub_43D38C(7, 3), and puts them back to + * function 2 on disable. GPIO 7 is an I2S pad, not a display pad -- the + * panel is driven entirely from the LCDIF and no display code here + * touches GPIO at all. Claiming only GPIO 20 leaves the bus incomplete + * and the jack silent. Optional (6,3) in pad_mode 1/4. + */ static void s5l8740_i2s_pads(struct s5l8740_i2s *i2s) { static const u8 sec_words[] = { 6, 7, 20 }; @@ -418,22 +517,6 @@ static void s5l8740_i2s_pads(struct s5l8740_i2s *i2s) s5l8740_i2s_gpiocmd(i2s, g, 3); } - /* - * RetailOS music/idle gpio.bin bank0 PCON = 0x32112224 - * (pins4/5 = func1). Linux often left 0x32222224 — force stock. - */ - if (i2s->gpio) { - u32 p0 = readl(i2s->gpio); - - if (p0 != 0x32112224u) { - writel(0x32112224u, i2s->gpio); - if (i2s->dev) - dev_info(i2s->dev, - "PCON0 %08x -> 32112224 (RetailOS music)\n", - p0); - } - } - s5l8740_i2s_log_iis_gpio(i2s, "pads-applied"); } @@ -658,18 +741,19 @@ static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma) if (!i2s || !i2s->base) return; s5l8740_i2s_pre_codec(); + /* + * Prime the FIFO with silence so the serialiser has something to + * clock out between the kick and the first DMA burst. This used to + * push a generated tone, which mixed a chirp into the front of every + * stream the card played. + */ { unsigned int n = fifo_prefill, i; - unsigned int rate = i2s->rate ? i2s->rate : N31_RATE_DEFAULT; - s16 s; if (n > 64) n = 64; - for (i = 0; i < n; i++) { - s = s5l8740_scale_s16(n31_tone_s16(i, rate)); - writel(((u32)(u16)s << 16) | (u16)s, - i2s->base + I2STXFIFO); - } + for (i = 0; i < n; i++) + writel(0, i2s->base + I2STXFIFO); } before = readl(i2s->base + I2STXCOM); if (txcom_exact >= 0) { @@ -1376,7 +1460,338 @@ static struct platform_driver s5l8740_i2s_driver = { .of_match_table = s5l8740_i2s_of_match, }, }; -module_platform_driver(s5l8740_i2s_driver); -MODULE_DESCRIPTION("S5L8740 IIS0 DAI + optional PL080 PCM (N31)"); +/* ------------------------------------------------------------------ */ +/* IIS2 — BCM2078 digital PCM capture */ +/* ------------------------------------------------------------------ */ + +static uint iis2_clkdiv; +module_param(iis2_clkdiv, uint, 0644); +MODULE_PARM_DESC(iis2_clkdiv, "IIS2 CLKDIV override; 0 = FM oracle 0x96"); + +struct s5l8740_iis2 { + void __iomem *base; + void __iomem *clkcon; + void __iomem *gpio; + void __iomem *gpiocmd; + struct device *dev; + struct clk_bulk_data *clks; + int num_clks; + bool has_dma; + struct snd_dmaengine_dai_dma_data cap_dma; + u32 fm_gate_saved; + bool fm_gate_held; + unsigned int rate; +}; + +static u32 iis2_pick_clkdiv(unsigned int rate) +{ + const struct n31_rate_cfg *r; + + if (iis2_clkdiv) + return iis2_clkdiv; + /* + * The FM capture uses 0x96 where IIS0 runs 0x177 in the same session, + * so this divider is not derived from the IIS0 rate table. + */ + if (rate == 44100 || rate == 48000) + return IIS2_CLKDIV_FM_ORACLE; + r = n31_find_rate(rate); + if (r) + return r->clkdiv; + if (!rate) + rate = 44100; + return MCLK_ASSUME_HZ / rate; +} + +static void iis2_pads(struct s5l8740_iis2 *iis2, bool claim) +{ + static const u8 pads[] = { + IIS2_PAD_BCLK, IIS2_PAD_SYNC, IIS2_PAD_DATA, + }; + void __iomem *bank; + unsigned int i, pin; + u32 dir; + + if (!iis2->gpio || !iis2->gpiocmd) + return; + for (i = 0; i < ARRAY_SIZE(pads); i++) { + bank = iis2->gpio + 32u * (pads[i] >> 3); + pin = pads[i] & 7; + dir = readl(bank + 0x14); + if (claim) + dir |= BIT(pin); + else + dir &= ~BIT(pin); + writel(dir, bank + 0x14); + writel(((u32)(pads[i] >> 3) << 16) | (pin << 8) | + (claim ? IIS2_PAD_FUNC : IIS2_PAD_RELEASE), + iis2->gpiocmd); + } + if (iis2->dev) + dev_dbg(iis2->dev, "IIS2 pads 97/98/119 %s\n", + claim ? "claimed" : "released"); +} + +static void iis2_fm_gate(struct s5l8740_iis2 *iis2, bool on) +{ + u32 cur; + + if (!iis2 || !iis2->clkcon) + return; + cur = readl(iis2->clkcon + CLKCON_FM_GATE); + if (on) { + if (!iis2->fm_gate_held) { + iis2->fm_gate_saved = cur; + iis2->fm_gate_held = true; + } + writel(CLKCON_FM_GATE_ON, iis2->clkcon + CLKCON_FM_GATE); + } else if (iis2->fm_gate_held) { + writel(iis2->fm_gate_saved ? iis2->fm_gate_saved : + CLKCON_FM_GATE_IDLE, + iis2->clkcon + CLKCON_FM_GATE); + iis2->fm_gate_held = false; + } +} + +/* Peri 13 must be armed by dmaengine before RXCOM is kicked. */ +static void iis2_program_rx(struct s5l8740_iis2 *iis2) +{ + iis2_pads(iis2, true); + iis2_fm_gate(iis2, true); + s5l8740_audio_clk_set(iis2->clkcon, S5L8740_AUDIO_PORT_IIS2, true); + writel(IIS2_CLKCON_ON, iis2->base + I2SCLKCON); + writel(IIS2_TXCON_FM, iis2->base + I2STXCON); + writel(IIS2_RXCON_FM, iis2->base + I2SRXCON); + writel(iis2_pick_clkdiv(iis2->rate), iis2->base + I2SCLKDIV); + writel(IIS2_REG44_ORACLE, iis2->base + I2SREG44); +} + +static void iis2_hw_stop(struct s5l8740_iis2 *iis2) +{ + if (!iis2 || !iis2->base) + return; + writel(IIS2_RXCOM_IDLE, iis2->base + I2SRXCOM); + s5l8740_audio_clk_set(iis2->clkcon, S5L8740_AUDIO_PORT_IIS2, false); + iis2_fm_gate(iis2, false); + iis2_pads(iis2, false); +} + +static int s5l8740_iis2_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + iis2->rate = params_rate(params); + iis2_program_rx(iis2); + dev_info(dai->dev, + "IIS2 hw_params rate=%u ch=%u clkdiv=0x%x reg44=0x%x status=0x%x\n", + iis2->rate, params_channels(params), + readl(iis2->base + I2SCLKDIV), readl(iis2->base + I2SREG44), + readl(iis2->base + I2SSTATUS)); + return 0; +} + +static int s5l8740_iis2_trigger(struct snd_pcm_substream *substream, int cmd, + struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (!iis2 || !iis2->base) + return -ENODEV; + if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) + return -EINVAL; + + switch (cmd) { + case SNDRV_PCM_TRIGGER_START: + case SNDRV_PCM_TRIGGER_RESUME: + case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + iis2_program_rx(iis2); + writel(IIS2_RXCOM_DMA, iis2->base + I2SRXCOM); + dev_info(dai->dev, + "IIS2 capture start rxcom=0x%x status=0x%x\n", + readl(iis2->base + I2SRXCOM), + readl(iis2->base + I2SSTATUS)); + return 0; + case SNDRV_PCM_TRIGGER_STOP: + case SNDRV_PCM_TRIGGER_SUSPEND: + case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + iis2_hw_stop(iis2); + return 0; + default: + return -EINVAL; + } +} + +static int s5l8740_iis2_dai_probe(struct snd_soc_dai *dai) +{ + struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); + + if (iis2->has_dma) + snd_soc_dai_init_dma_data(dai, NULL, &iis2->cap_dma); + return 0; +} + +static const struct snd_soc_dai_ops s5l8740_iis2_dai_ops = { + .probe = s5l8740_iis2_dai_probe, + .hw_params = s5l8740_iis2_hw_params, + .trigger = s5l8740_iis2_trigger, +}; + +static struct snd_soc_dai_driver s5l8740_iis2_dai = { + .name = "bcm2078-pcm", + .capture = { + .stream_name = "BCM2078 PCM Capture", + .channels_min = 1, + .channels_max = 2, + .rates = S5L8740_I2S_RATES, + .formats = S5L8740_I2S_FORMATS, + }, + .ops = &s5l8740_iis2_dai_ops, +}; + +static const struct snd_soc_component_driver s5l8740_iis2_component = { + .name = "bcm2078-pcm", + .legacy_dai_naming = 1, +}; + +static ssize_t iis2_regs_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct s5l8740_iis2 *iis2 = dev_get_drvdata(dev); + unsigned int i; + ssize_t n = 0; + + if (!iis2 || !iis2->base) + return sysfs_emit(buf, "not mapped\n"); + + for (i = 0; i < IIS2_REGS_LEN; i += 4) { + n += sysfs_emit_at(buf, n, "%02x: %08x\n", i, + readl(iis2->base + i)); + if (n >= PAGE_SIZE - 32) + break; + } + if (iis2->clkcon) { + n += sysfs_emit_at(buf, n, "clk+10: %08x\n", + readl(iis2->clkcon + CLKCON_FM_GATE)); + n += sysfs_emit_at(buf, n, "clk+30: %08x\n", + readl(iis2->clkcon + CLKCON_AUDIO_OFF)); + } + return n; +} +/* Same sysfs name as the IIS0 dump; different device, different symbol. */ +static struct device_attribute dev_attr_iis2_regs = + __ATTR(regs, 0444, iis2_regs_show, NULL); + +static int s5l8740_iis2_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct s5l8740_iis2 *iis2; + struct resource *res; + int ret; + + iis2 = devm_kzalloc(dev, sizeof(*iis2), GFP_KERNEL); + if (!iis2) + return -ENOMEM; + iis2->dev = dev; + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + iis2->base = devm_ioremap_resource(dev, res); + if (IS_ERR(iis2->base)) + return PTR_ERR(iis2->base); + + iis2->clkcon = devm_ioremap(dev, CLKCON_PHYS, 0x80); + iis2->gpio = devm_ioremap(dev, GPIO_PHYS, 0x200); + iis2->gpiocmd = devm_ioremap(dev, GPIOCMD_PHYS, 4); + + ret = devm_clk_bulk_get_all(dev, &iis2->clks); + if (ret > 0) { + iis2->num_clks = ret; + ret = clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); + if (ret) + dev_warn(dev, "clk_bulk: %d\n", ret); + } + + if (res) { + iis2->cap_dma.addr = res->start + I2SRXFIFO; + iis2->cap_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; + iis2->cap_dma.maxburst = 1; + } + + platform_set_drvdata(pdev, iis2); + dev_set_drvdata(dev, iis2); + + if (!of_property_present(dev->of_node, "dmas")) { + dev_err(dev, "missing dmas (need peri 13 rx)\n"); + return -EINVAL; + } + ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); + if (ret) + return dev_err_probe(dev, ret, "dmaengine_pcm\n"); + iis2->has_dma = true; + + ret = devm_snd_soc_register_component(dev, &s5l8740_iis2_component, + &s5l8740_iis2_dai, 1); + if (ret) + return ret; + + ret = device_create_file(dev, &dev_attr_iis2_regs); + if (ret) + dev_warn(dev, "regs sysfs: %d\n", ret); + + dev_info(dev, "BCM2078 PCM RX @%pR peri13 FIFO@+0x38\n", res); + return 0; +} + +static void s5l8740_iis2_remove(struct platform_device *pdev) +{ + struct s5l8740_iis2 *iis2 = platform_get_drvdata(pdev); + + device_remove_file(&pdev->dev, &dev_attr_iis2_regs); + iis2_hw_stop(iis2); + if (iis2 && iis2->num_clks) + clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); +} + +static const struct of_device_id s5l8740_iis2_of_match[] = { + { .compatible = "apple,s5l8740-bcm2078-pcm" }, + { .compatible = "apple,s5l8740-iis2" }, + { } +}; +MODULE_DEVICE_TABLE(of, s5l8740_iis2_of_match); + +static struct platform_driver s5l8740_iis2_driver = { + .probe = s5l8740_iis2_probe, + .remove = s5l8740_iis2_remove, + .driver = { + .name = "s5l8740-iis2", + .of_match_table = s5l8740_iis2_of_match, + }, +}; + +static struct platform_driver * const s5l8740_audio_drivers[] = { + &s5l8740_i2s_driver, + &s5l8740_iis2_driver, +}; + +static int __init s5l8740_audio_init(void) +{ + return platform_register_drivers(s5l8740_audio_drivers, + ARRAY_SIZE(s5l8740_audio_drivers)); +} +module_init(s5l8740_audio_init); + +static void __exit s5l8740_audio_exit(void) +{ + platform_unregister_drivers(s5l8740_audio_drivers, + ARRAY_SIZE(s5l8740_audio_drivers)); +} +module_exit(s5l8740_audio_exit); + +MODULE_DESCRIPTION("S5L8740 I2S DAIs: IIS0 playback + IIS2 capture (N31)"); MODULE_LICENSE("GPL"); diff --git a/sound/soc/apple/s5l8740-iis2.c b/sound/soc/apple/s5l8740-iis2.c deleted file mode 100755 index 12792264d863fb..00000000000000 --- a/sound/soc/apple/s5l8740-iis2.c +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-only -/* - * S5L8740 IIS2 @ 0x3D400000 — BCM2078 digital PCM port (N31). - * - * RetailOS oracles (fm/, bt-*-scsi-live/): - * IIS1 @ 0x3CD00000 = XSP, always zero — NOT BCM TX. - * IIS2 = shared BCM2078 I²S: RX FIFO @ +0x38 (PL080 peri 13, FM + module PCM in), - * TXCON @ +0x04 = 0x0b000099 programmed for music/FM/BT (TXCOM often 0 on BT). - * BT A2DP over-the-air = UART1 @ 0x3DB HCI → BCM2078 (no IIS0/CS42). - * CLKCON +0x00 = 0x1 - * TXCON +0x04 = 0x0b000099 (RetailOS programs this on IIS2 too) - * RXCON +0x30 = 0x1000 - * RXCOM +0x34 = 0x6 (DMA kick; idle/stopped often 0x2) - * RXFIFO +0x38 ← PL080 peri 13 P2M - * STATUS +0x3c = 0x10804 live - * CLKDIV +0x40 = 0x96 (FM oracle; IIS0 play uses 0x177/375) - * REG44 +0x44 = 0x00010007 (same as IIS0 music oracle) - * - * SoC clocks: CLKCON+0x30 = 0x32190 play; FM also +0x10 = 0x4 - * (vs music/idle 0x8004). No FM→BT / A2DP path here — local PCM only. - */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "n31-audio-rates.h" - -#define S5L8740_IIS2_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) -#define S5L8740_IIS2_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) - -#define I2SCLKCON 0x00 -#define I2STXCON 0x04 -#define I2SRXCON 0x30 -#define I2SRXCOM 0x34 -#define I2SRXFIFO 0x38 -#define I2SSTATUS 0x3c -#define I2SCLKDIV 0x40 -#define I2SREG44 0x44 - -#define MCLK_ASSUME_HZ 12000000u - -/* fm/20260826T203834Z oracle */ -#define IIS2_CLKCON_ON 0x1u -#define IIS2_TXCON_FM 0x0b000099u -#define IIS2_RXCON_FM 0x1000u -#define IIS2_RXCOM_DMA 0x6u -#define IIS2_RXCOM_IDLE 0x2u -#define IIS2_CLKDIV_FM_ORACLE 0x96u -#define IIS2_REG44_ORACLE 0x00010007u - -static uint iis2_clkdiv; -module_param(iis2_clkdiv, uint, 0644); -MODULE_PARM_DESC(iis2_clkdiv, "IIS2 CLKDIV override; 0 = FM oracle 0x96"); - -#define CLKCON_PHYS 0x3c500000ul -#define CLKCON_AUDIO_OFF 0x30 -#define CLKCON_FM_GATE_OFF 0x10 -#define CLKCON_AUDIO_PLAY 0x32190u -#define CLKCON_AUDIO_IDLE 0x1c20u -#define CLKCON_FM_GATE_ON 0x4u -#define CLKCON_FM_GATE_OFF_VAL 0x8004u - -#define IIS2_REGS_LEN 0x48 - -struct s5l8740_iis2 { - void __iomem *base; - void __iomem *clkcon; - struct device *dev; - struct clk_bulk_data *clks; - int num_clks; - bool has_dma; - struct snd_dmaengine_dai_dma_data cap_dma; - u32 clkcon10_saved; - bool clkcon10_held; - unsigned int rate; -}; - -static u32 iis2_pick_clkdiv(unsigned int rate) -{ - const struct n31_rate_cfg *r; - - if (iis2_clkdiv) - return iis2_clkdiv; - /* - * FM IIS2 oracle differs from IIS0: 0x96 while IIS0 HP path uses - * 0x177 (32 kHz table entry) during the same FM session. - */ - if (rate == 44100 || rate == 48000) - return IIS2_CLKDIV_FM_ORACLE; - r = n31_find_rate(rate); - if (r) - return r->clkdiv; - if (!rate) - rate = 44100; - return MCLK_ASSUME_HZ / rate; -} - -static void iis2_clkcon_audio(struct s5l8740_iis2 *iis2, u32 val) -{ - if (!iis2 || !iis2->clkcon) - return; - writel(val, iis2->clkcon + CLKCON_AUDIO_OFF); -} - -static void iis2_clkcon_fm_gate(struct s5l8740_iis2 *iis2, bool on) -{ - u32 cur; - - if (!iis2 || !iis2->clkcon) - return; - cur = readl(iis2->clkcon + CLKCON_FM_GATE_OFF); - if (on) { - if (!iis2->clkcon10_held) { - iis2->clkcon10_saved = cur; - iis2->clkcon10_held = true; - } - writel(CLKCON_FM_GATE_ON, iis2->clkcon + CLKCON_FM_GATE_OFF); - } else if (iis2->clkcon10_held) { - writel(iis2->clkcon10_saved ? - iis2->clkcon10_saved : CLKCON_FM_GATE_OFF_VAL, - iis2->clkcon + CLKCON_FM_GATE_OFF); - iis2->clkcon10_held = false; - } -} - -/* - * Program IIS2 RX from fm-playing dump. Peri 13 DMA must be armed by - * dmaengine before RXCOM |= 0x6 (same kick model as IIS0 TXCOM). - */ -static void iis2_program_rx(struct s5l8740_iis2 *iis2) -{ - u32 div; - - iis2_clkcon_fm_gate(iis2, true); - iis2_clkcon_audio(iis2, CLKCON_AUDIO_PLAY); - writel(IIS2_CLKCON_ON, iis2->base + I2SCLKCON); - writel(IIS2_TXCON_FM, iis2->base + I2STXCON); - writel(IIS2_RXCON_FM, iis2->base + I2SRXCON); - div = iis2_pick_clkdiv(iis2->rate); - writel(div, iis2->base + I2SCLKDIV); - writel(IIS2_REG44_ORACLE, iis2->base + I2SREG44); -} - -static void iis2_rx_kick(struct s5l8740_iis2 *iis2) -{ - writel(IIS2_RXCOM_DMA, iis2->base + I2SRXCOM); -} - -static void iis2_hw_stop(struct s5l8740_iis2 *iis2) -{ - if (!iis2 || !iis2->base) - return; - writel(IIS2_RXCOM_IDLE, iis2->base + I2SRXCOM); - iis2_clkcon_audio(iis2, CLKCON_AUDIO_IDLE); - iis2_clkcon_fm_gate(iis2, false); -} - -static int s5l8740_iis2_hw_params(struct snd_pcm_substream *substream, - struct snd_pcm_hw_params *params, - struct snd_soc_dai *dai) -{ - struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); - - if (!iis2 || !iis2->base) - return -ENODEV; - if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) - return -EINVAL; - iis2->rate = params_rate(params); - iis2_program_rx(iis2); - dev_info(dai->dev, - "IIS2 hw_params rate=%u ch=%u clkdiv=0x%x reg44=0x%x status=0x%x\n", - iis2->rate, params_channels(params), - readl(iis2->base + I2SCLKDIV), readl(iis2->base + I2SREG44), - readl(iis2->base + I2SSTATUS)); - return 0; -} - -static int s5l8740_iis2_trigger(struct snd_pcm_substream *substream, int cmd, - struct snd_soc_dai *dai) -{ - struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); - - if (!iis2 || !iis2->base) - return -ENODEV; - if (substream->stream != SNDRV_PCM_STREAM_CAPTURE) - return -EINVAL; - - switch (cmd) { - case SNDRV_PCM_TRIGGER_START: - case SNDRV_PCM_TRIGGER_RESUME: - case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: - iis2_program_rx(iis2); - iis2_rx_kick(iis2); - dev_info(dai->dev, "IIS2 capture start rxcom=0x%x status=0x%x\n", - readl(iis2->base + I2SRXCOM), - readl(iis2->base + I2SSTATUS)); - return 0; - case SNDRV_PCM_TRIGGER_STOP: - case SNDRV_PCM_TRIGGER_SUSPEND: - case SNDRV_PCM_TRIGGER_PAUSE_PUSH: - iis2_hw_stop(iis2); - return 0; - default: - return -EINVAL; - } -} - -static int s5l8740_iis2_dai_probe(struct snd_soc_dai *dai) -{ - struct s5l8740_iis2 *iis2 = snd_soc_dai_get_drvdata(dai); - - if (iis2->has_dma) - snd_soc_dai_init_dma_data(dai, NULL, &iis2->cap_dma); - return 0; -} - -static const struct snd_soc_dai_ops s5l8740_iis2_dai_ops = { - .probe = s5l8740_iis2_dai_probe, - .hw_params = s5l8740_iis2_hw_params, - .trigger = s5l8740_iis2_trigger, -}; - -static struct snd_soc_dai_driver s5l8740_iis2_dai = { - .name = "bcm2078-pcm", - .capture = { - .stream_name = "BCM2078 PCM Capture", - .channels_min = 1, - .channels_max = 2, - .rates = S5L8740_IIS2_RATES, - .formats = S5L8740_IIS2_FORMATS, - }, - .ops = &s5l8740_iis2_dai_ops, -}; - -static const struct snd_soc_component_driver s5l8740_iis2_component = { - .name = "bcm2078-pcm", - .legacy_dai_naming = 1, -}; - -static ssize_t regs_show(struct device *dev, struct device_attribute *a, - char *buf) -{ - struct s5l8740_iis2 *iis2 = dev_get_drvdata(dev); - unsigned int i; - ssize_t n = 0; - - if (!iis2 || !iis2->base) - return sysfs_emit(buf, "not mapped\n"); - - for (i = 0; i < IIS2_REGS_LEN; i += 4) { - n += sysfs_emit_at(buf, n, "%02x: %08x\n", i, - readl(iis2->base + i)); - if (n >= PAGE_SIZE - 32) - break; - } - if (iis2->clkcon) { - n += sysfs_emit_at(buf, n, "clk+10: %08x\n", - readl(iis2->clkcon + CLKCON_FM_GATE_OFF)); - n += sysfs_emit_at(buf, n, "clk+30: %08x\n", - readl(iis2->clkcon + CLKCON_AUDIO_OFF)); - } - return n; -} -static DEVICE_ATTR_RO(regs); - -static int s5l8740_iis2_probe(struct platform_device *pdev) -{ - struct device *dev = &pdev->dev; - struct s5l8740_iis2 *iis2; - struct resource *res; - int ret; - - iis2 = devm_kzalloc(dev, sizeof(*iis2), GFP_KERNEL); - if (!iis2) - return -ENOMEM; - iis2->dev = dev; - - res = platform_get_resource(pdev, IORESOURCE_MEM, 0); - iis2->base = devm_ioremap_resource(dev, res); - if (IS_ERR(iis2->base)) - return PTR_ERR(iis2->base); - - iis2->clkcon = devm_ioremap(dev, CLKCON_PHYS, 0x80); - - ret = devm_clk_bulk_get_all(dev, &iis2->clks); - if (ret > 0) { - iis2->num_clks = ret; - ret = clk_bulk_prepare_enable(iis2->num_clks, iis2->clks); - if (ret) - dev_warn(dev, "clk_bulk: %d\n", ret); - } - - if (res) { - iis2->cap_dma.addr = res->start + I2SRXFIFO; - iis2->cap_dma.addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES; - iis2->cap_dma.maxburst = 1; - } - - platform_set_drvdata(pdev, iis2); - dev_set_drvdata(dev, iis2); - - if (of_property_present(dev->of_node, "dmas")) { - ret = devm_snd_dmaengine_pcm_register(dev, NULL, 0); - if (ret) { - dev_err(dev, "dmaengine_pcm: %d\n", ret); - return ret; - } - iis2->has_dma = true; - } else { - dev_err(dev, "missing dmas (need peri 13 rx)\n"); - return -EINVAL; - } - - ret = devm_snd_soc_register_component(dev, &s5l8740_iis2_component, - &s5l8740_iis2_dai, 1); - if (ret) - return ret; - - ret = device_create_file(dev, &dev_attr_regs); - if (ret) - dev_warn(dev, "regs sysfs: %d\n", ret); - - dev_info(dev, - "BCM2078 PCM RX @%pR peri13 FIFO@+0x38 (IIS2; FM/A2DP PCM in)\n", - res); - return 0; -} - -static void s5l8740_iis2_remove(struct platform_device *pdev) -{ - struct s5l8740_iis2 *iis2 = platform_get_drvdata(pdev); - - device_remove_file(&pdev->dev, &dev_attr_regs); - iis2_hw_stop(iis2); - if (iis2 && iis2->num_clks) - clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); -} - -static const struct of_device_id s5l8740_iis2_of_match[] = { - { .compatible = "apple,s5l8740-bcm2078-pcm" }, - { .compatible = "apple,s5l8740-iis2" }, - { } -}; -MODULE_DEVICE_TABLE(of, s5l8740_iis2_of_match); - -static struct platform_driver s5l8740_iis2_driver = { - .probe = s5l8740_iis2_probe, - .remove = s5l8740_iis2_remove, - .driver = { - .name = "s5l8740-iis2", - .of_match_table = s5l8740_iis2_of_match, - }, -}; -module_platform_driver(s5l8740_iis2_driver); - -MODULE_DESCRIPTION("S5L8740 BCM2078 PCM capture DAI (IIS2 @0x3D400000, peri 13 RX)"); -MODULE_LICENSE("GPL"); -MODULE_SOFTDEP("pre: dma_s5l8740_pl080"); From cc1f7ee4027482ad3af26cb9f3cbda03bd5870d3 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:49:41 -0230 Subject: [PATCH 21/31] N31: FTL recovery correctness -- VBA space, short reads, BTOC formats A volume that mounted with files missing, then would not mount, then mounted but could not open files. Six defects, each found by measuring rather than by reasoning about the previous one. The VBA space was bank-major -- every (ce, cau, vblock) triple got its own superblock index -- while the FTL native space puts the plane between the page and the slot. CXT VBAs had to be translated on the way in, and a run of consecutive ones was contiguous only within a 4-slot group. The seed produced 236675 ranges for 938395 LBAs, under four LBAs per range. Matching the native layout removes the translation and lets extents stay whole: 3137 ranges. The CXT diff replay skipped every superblock on the volume. The skip tested page 0's weave -- the oldest page in the block, written when it was opened -- so a block appended to right up to power loss still tested older than the checkpoint. It now tests the newest weave, taken from page 127, and only when page 127 actually supplied one. The VBA range check bounded by user_blocks, which is blocks_per_cau less the VFL tail. That is the right bound for what may be allocated and the wrong one for what a stored VBA may name; it discarded 81 CXT records for vblocks 1987..1991, around 2000 LBAs. The VFL is an identity map over blocks_per_cau, so that is the bound. struct whimory_bte is {weave_seq_add, aux, lba, span} and 185 BTOC pages carry an eight-byte header before the array. The parser read the header as a record, got span=1266 against a 508-VBA superblock, broke on record zero and declared the page unrecognised -- 127 pages and ~2032 LBAs per superblock, 255 superblocks. That is what left directory sectors unreadable on a volume whose FAT mounted fine. btoc_pages_valid 308 -> 493. A BTOC with no BTE array, and a block no rule classified at all, are now rebuilt from per-page meta instead of dropped. WHIMORY_SB_UNKNOWN had been defined and never assigned: 78 blocks hit `continue` before nsb++, so they never entered sbs[] and were never replayed. Classify reads one 4112-byte record instead of a 16448-byte page. Slot 0 carries the data and meta it inspects, so a block settles there or escalates; only an erased, zero or unrecognised slot-0 meta needs the full page, which is ~80 blocks against ~2270. The pass went from ~57s to ~31s. The FPart scan had never read the NAND. It called page_read with a meta buffer, which is refused unless meta_dma_read is set -- and that is deliberately off because a permanent live CS kick reboots the device. Every read returned -EOPNOTSUPP: 512 reads in 60ms, and "sig=0, not a native open" was a verdict about a region nobody had looked at. It uses cs_phys inside a dma_session now, which needed an slc-capable entry point since the FPart region is SLC and is found by trying plane 1 first. Measured after: every file on the volume reads with no kernel error. --- drivers/misc/ftl-s5l8740-core.c | 1141 ++++++++++++++++++++++++++++--- drivers/misc/nand-s5l8740.c | 69 +- drivers/misc/nand-s5l8740.h | 6 + drivers/misc/whimory-s5l8740.h | 36 +- 4 files changed, 1166 insertions(+), 86 deletions(-) diff --git a/drivers/misc/ftl-s5l8740-core.c b/drivers/misc/ftl-s5l8740-core.c index 0e61b7282bbf5e..dfae972b89132f 100755 --- a/drivers/misc/ftl-s5l8740-core.c +++ b/drivers/misc/ftl-s5l8740-core.c @@ -46,15 +46,47 @@ module_param(import_l2v_oracle, bool, 0644); MODULE_PARM_DESC(import_l2v_oracle, "Load L2V root/nodes/globals from /lib/firmware/apple/"); -static unsigned int max_open_sbs = 16; +/* + * Rebuild every open superblock we find, up to a backstop. + * + * This was 16, which was a bring-up number and never lifted. It is not a + * tuning knob: an open superblock holds writes that happened after the + * checkpoint, so a cap on how many get rebuilt is a cap on how much recent + * data ends up in the map. With 1619 open superblocks on this volume it was + * rebuilding one percent of them and silently dropping the rest, which is a + * second and entirely separate cause of the missing-recent-files symptom + * that the page-0 weave bug caused. + * + * 4096 is above the number of blocks a CAU has, so in practice this is "all + * of them" -- it stays a finite number only so a corrupt classify cannot turn + * into an unbounded loop. The cost is real and is reported: each rebuild + * reads pages from page 0 until it finds a blank one, so a genuinely open + * block stops early and a block that is actually full reads all 127. + */ +static unsigned int max_open_sbs = 4096; module_param(max_open_sbs, uint, 0644); MODULE_PARM_DESC(max_open_sbs, - "Max open superblocks to META-rebuild (0 = all; default 16)"); + "Max open superblocks to META-rebuild (0 = all; default 4096)"); -static unsigned int scan_blocks = 256; +/* + * Scan every block. 256 was a bring-up limit that was never lifted, and + * it quietly disabled the CXT fast path: the snapshot blocks sit above + * that mark, so classify never saw one and every boot fell back to + * replaying the whole device. Measured on this unit, 256 against 0: + * + * replayed superblocks 624 -> 0 + * skipped_by_cxt 0 -> 2274 + * cxt_seeded 0 -> 239525 + * classified_cxt 0 -> 4 + * mapped_ranges 1933 -> 200000 + * + * so it was not only slow, it was building a far less complete map. + * Classifying more blocks costs less than replaying 624 superblocks. + */ +static unsigned int scan_blocks; module_param(scan_blocks, uint, 0644); MODULE_PARM_DESC(scan_blocks, - "User blocks per CE/CAU to classify (0 = all; default 256)"); + "User blocks per CE/CAU to classify (0 = all, the default)"); /* * BTOC meta-confirm re-reads every candidate data page over CS. Full-SB @@ -67,10 +99,71 @@ module_param(btoc_meta_confirm, bool, 0644); MODULE_PARM_DESC(btoc_meta_confirm, "CS-read data pages to take meta_lba as L2V key (default Y)"); -static unsigned int btoc_confirm_max = 512; +/* + * BTOC confirmation budget. + * + * This was 512, and on a full volume that is not a budget, it is a + * truncation. Measured on an N31 with the same flash contents: + * + * 512 65536 + * pages_valid 6 312 + * l2v_updates 2048 97820 + * confirm_capped 23943 0 + * mapped_lbas 7468 77553 + * mapped_ranges 1933 20596 + * + * Every one of the extra confirmations was good -- meta_mismatch stayed + * at 0 -- so the old default was discarding about nine tenths of the + * mapping and reporting success while doing it. That is the same shape + * of bug as the old max_range_nodes ceiling: the statistic sits exactly + * on the limit, which means the limit chose the answer. + * + * This was reverted to 512 once, after a single run where the richer map + * flipped BPB selection to the other candidate (49285) and the volume + * would not mount. That revert was wrong. It let one observation override + * a measurement, and the cost is not subtle: at 512 the volume mounts but + * only about a tenth of it is mapped, so most files simply are not there. + * On the boot that settled it, 65536 bound to 49279 and mounted with Apps, + * iPod_Control and n31os all present. + * + * If BPB selection ever does pick an unmountable candidate again, fix the + * selection -- it should prefer a candidate whose FAT actually reads -- + * rather than starving the map to steer it. + * + * 24455 confirm pages were needed here, so 65536 leaves headroom for a + * fuller volume without being unbounded. Raise it (or set 0) if + * btoc_confirm_capped is ever non-zero -- that value being non-zero is + * the signal that recovery is being cut short. + */ +/* + * Read page 127 only when page 0 leaves the question open. + * + * This is a large IO saving and it is why classify dropped from ~57s to + * ~26s. It is also the kind of optimisation that can quietly change what + * gets classified, so it is switchable: set btoc_page_lazy=0 to go back + * to reading page 127 for every block and compare the classify totals. + * If the two disagree, the laziness is wrong, not the flash. + */ +/* + * Probe blocks for "empty" with a one-record read before doing the full + * four-record page read. On by default: it is a strict reduction in NAND + * traffic for the majority case and changes no classification, because a + * negative probe falls through to exactly the old path. + */ +static bool fast_empty_probe = true; +module_param(fast_empty_probe, bool, 0644); +MODULE_PARM_DESC(fast_empty_probe, + "1=one-record empty probe before the full page read (default)"); + +static bool btoc_page_lazy = true; +module_param(btoc_page_lazy, bool, 0644); +MODULE_PARM_DESC(btoc_page_lazy, + "1=read page 127 only when page 0 is inconclusive (default); 0=always read it"); + +static unsigned int btoc_confirm_max = 65536; module_param(btoc_confirm_max, uint, 0644); MODULE_PARM_DESC(btoc_confirm_max, - "Max BTOC CS page confirms per recover (default 512; 0=unlimited)"); + "Max BTOC CS page confirms per recover (default 65536; 0=unlimited). Non-zero btoc_confirm_capped means this is too low"); /* Alias name from bring-up notes. */ module_param_named(btoc_confirm_pages_cap, btoc_confirm_max, uint, 0644); @@ -92,7 +185,32 @@ MODULE_PARM_DESC(recover_yield_us, * OOM panic (panic=-1 → reboot to RetailOS), which costs a DFU cycle and * loses the log. Stop adding mappings at the budget and report instead. */ -static unsigned int max_range_nodes = 200000; +/* + * 200000 was below what this device actually needs, and the failure is + * silent in the worst way: the map fills to exactly the ceiling and the + * rest of the volume is simply absent. + * + * Measured here, with the CXT fast path working: + * + * CXT_SEED extents=239525 + * mapped_ranges=200000 range_budget_stop=39512 + * + * so 39512 ranges were dropped on the floor. Reads past them return + * UNMAPPED, FAT cannot fetch its directory blocks, and the mount comes + * up with "invalid cluster chain" and missing folders -- which looks + * like a corrupt disk rather than a driver that stopped writing down + * where things are. L2V packing then fails with -12 and it falls back + * to the truncated interval map. + * + * A whimory_range is about 32 bytes, so this ceiling is roughly 16 MB + * if it were ever reached. It is a ceiling rather than an allocation: + * nodes are only created for extents that exist, so a device with + * fewer extents pays nothing for the headroom. Set well above this + * unit's 239525 deliberately, so a fuller or more fragmented volume + * on another device does not hit the same silent truncation. + * Set 0 for unlimited. + */ +static unsigned int max_range_nodes = 500000; module_param(max_range_nodes, uint, 0644); MODULE_PARM_DESC(max_range_nodes, "Interval-map node ceiling; stop mapping past it (0=unlimited)"); @@ -144,9 +262,15 @@ static bool ftl_progress = true; module_param_named(progress, ftl_progress, bool, 0644); MODULE_PARM_DESC(progress, "Periodic recover progress lines (default Y)"); -static unsigned int progress_ms = 5000; +/* + * 5 s suited a console log but is far too coarse to drive a progress + * bar, which needs to move several times a second or it reads as hung. + * This gates the sysfs counters as well as the log lines, so it is now + * a UI refresh rate rather than a logging interval. + */ +static unsigned int progress_ms = 500; module_param(progress_ms, uint, 0644); -MODULE_PARM_DESC(progress_ms, "Minimum ms between progress lines"); +MODULE_PARM_DESC(progress_ms, "Minimum ms between progress updates"); /* Rate-limit: emit at most this many of a repeating diagnostic. */ static unsigned int diag_max_lines = 3; @@ -167,6 +291,14 @@ static bool ftl_progress_due(struct whimory *w) return true; } +static void ftl_progress_set(struct whimory *w, const char *phase, + unsigned int cur, unsigned int total) +{ + w->prog_phase = phase; + w->prog_cur = cur; + w->prog_total = total; +} + static bool range_coalesce = true; module_param(range_coalesce, bool, 0644); MODULE_PARM_DESC(range_coalesce, @@ -394,6 +526,90 @@ static bool whimory_meta_erased(const u8 *m, unsigned int n) return true; } +/* + * Cheap "is this block empty" probe. + * + * classify visits every block and, for seventy percent of them, only needs + * to answer "empty?" -- 64 bytes of slot-0 data and slot-0's 16-byte meta. + * The full page read moves 16448 bytes to obtain those 80. This reads one + * record, 4112 bytes. + * + * Deliberately narrow: it answers empty/not-empty and nothing else. Every + * other question classify asks, CXT detection above all, reads all four + * slots' meta, so a "no" here must be followed by the full read. Returning + * a bool rather than filling meta0 makes that impossible to get wrong by + * accident. + */ +/* + * Read slot 0 of a page: 4112 bytes instead of 16448. + * + * The controller takes a column offset and length -- col_len packs the + * length in the low half and the start column in the high half -- so a + * one-record read is a genuine short transfer, not a full page quietly + * discarded. Slot 0 carries the first 4096 data bytes and its own 16-byte + * meta, which between them settle almost every question classify asks. + * + * The exception is whimory_meta_slot0_or_any_cxt(), which scans all four + * slots, so a meta this read cannot classify must escalate to the full + * page. That is rare: on this volume the meta type histogram is 5492 + * erased, 2268 data, 2 data2, 2 at 0x4b and 76 zero, so slot 0 answers for + * all but the last hundred or so. + * + * Returns 0 with data and meta filled, or negative on a read error. + */ +static int whimory_cs_read_slot0(struct whimory *w, unsigned int ce, + unsigned int cau, unsigned int block, + unsigned int page, const u8 **data, + u8 *meta0) +{ + struct s5l8740_cs_page *csp = w->sftl.cs_page; + unsigned int i; + int ret; + + if (!csp) + return -ENOMEM; + ret = s5l8740_nand_cs_phys_read_slot0((u8)ce, (u8)cau, (u16)block, + (u8)page, csp); + if (ret) + return ret; + for (i = 0; i < WHIMORY_META_SIZE; i++) + meta0[i] = csp->meta_raw[0][i]; + *data = csp->data[0]; + return 0; +} + +/* + * Can slot 0 alone classify this block? + * + * The only thing classify needs the other three slots for is + * whimory_meta_slot0_or_any_cxt(), which exists to catch a partially + * written CXT page whose slot 0 never got its meta. So the question is + * narrower than it looks: does this slot-0 type rule out a CXT hiding in + * slots 1..3? + * + * Plain user data and a slot-0 CXT do. A page is written slot 0 first with + * one kind of content, so DATA in slot 0 means the page is user data, and + * SFTL_CXT in slot 0 answers the CXT question outright. + * + * Everything else escalates, deliberately -- an erased or zero or + * unrecognised slot-0 meta is exactly the partial-write shape that + * _or_any_cxt() was written for, and misreading one of those loses a + * checkpoint. On this volume that leaves about eighty blocks paying for a + * full read against roughly 2270 settling from slot 0. + */ +static bool whimory_meta0_is_conclusive(const u8 *meta0) +{ + switch (meta0[0]) { + case WHIMORY_META_TYPE_DATA: + case WHIMORY_META_TYPE_DATA2: + case WHIMORY_META_TYPE_SFTL_CXT: + return true; + default: + return false; + } +} + + static bool whimory_meta_is_user_data(const struct whimory_meta *m) { return m->type == WHIMORY_META_TYPE_DATA || @@ -514,10 +730,11 @@ static u32 whimory_vfl_virt(struct whimory *w, u32 cau, u32 phys) return phys; } -static u32 s_g_addr_to_vba(const struct whimory *w, u32 sb, u32 ofs) -{ - return sb * w->sftl.vbas_per_sb + ofs; -} +/* + * Removed with the move to the native VBA space: nothing builds a VBA from + * a bank-major superblock index any more. whimory_sb_ofs_to_vba() is the + * replacement and converts at the boundary instead. + */ static u32 s_g_vba_to_sb(const struct whimory *w, u32 vba) { @@ -539,37 +756,124 @@ static u32 whimory_sb_index(const struct whimory *w, u32 ce, u32 cau, return (ce * w->geom.num_cau + cau) * w->sftl.user_blocks + vblock; } +/* + * The VBA space is the FTL's native one. + * + * Apple treats a superblock as the same virtual block across every + * (ce, cau) plane, so the plane index sits between the page and the slot: + * + * vba = vblock * (pages_per_sb * planes * vbas_per_page) + * + page * (planes * vbas_per_page) + * + plane * vbas_per_page + * + slot + * + * This used to be bank-major -- every (ce, cau, vblock) triple got its own + * superblock index -- which meant CXT VBAs had to be translated on the way + * in, and a run of consecutive CXT VBAs was only contiguous here within one + * 4-slot group, because the next group belonged to a different plane. The + * cost of that was not subtle: the CXT seed produced 236675 ranges for + * 938395 LBAs, just under four LBAs per range, when the same data in the + * native space is a few thousand contiguous runs. + * + * Matching the native layout removes the translation entirely and lets + * extents stay whole, which is what the range budget was fighting. + */ +/* + * How many virtual blocks a VBA may name. + * + * Not user_blocks. user_blocks is blocks_per_cau minus the VFL tail -- 1960 + * of 2088 here -- and it is the right number for "how much space may be + * allocated to the user". It is the wrong number for "which blocks may a + * stored VBA refer to", and using it as the range check silently discarded + * 81 CXT records: + * + * CXT_XLATE_FAIL vba=0x003e3883 (vblk=1991 pg=8 plane=0 slot=3) + * lba=841408 span=256 user_blocks=1960 + * + * Those are not corrupt entries. They cluster in vblocks 1987..1991, their + * spans are large and ordinary (93, 116, 125, 128, 256, 384), and their VBAs + * run contiguously through the plane interleave exactly as the arithmetic + * predicts -- 0x3e3883 + 256 lands on 0x3e3983 and the next record begins at + * 0x3e3984. That is the FTL telling us, correctly, where it put roughly two + * thousand LBAs around 839k-842k. We were throwing them away. + * + * The VFL is an identity map over blocks_per_cau, so any block below that is + * addressable and a VBA naming one is legitimate. + */ +static u32 whimory_vba_blocks(const struct whimory *w) +{ + if (w->geom.blocks_per_cau) + return w->geom.blocks_per_cau; + return w->sftl.user_blocks; +} + static u32 whimory_pack_vba(const struct whimory *w, u32 ce, u32 cau, u32 vblock, u32 page, u32 slot) { - u32 sb = whimory_sb_index(w, ce, cau, vblock); - u32 ofs = page * w->sftl.vbas_per_page + slot; + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 plane = ce * w->geom.num_cau + cau; + u32 per_page = planes * w->sftl.vbas_per_page; + u32 per_sb = w->sftl.pages_per_sb * per_page; + + return vblock * per_sb + page * per_page + + plane * w->sftl.vbas_per_page + slot; +} + +/* + * Build a VBA from a bank-major superblock index and an in-superblock + * offset. The replay paths still enumerate one (ce, cau, vblock) at a + * time, which is a bank-major idea; this converts at the boundary so the + * stored VBA is native. + */ +static u32 whimory_sb_ofs_to_vba(const struct whimory *w, u32 sb_idx, u32 ofs) +{ + u32 per_ce = w->geom.num_cau * w->sftl.user_blocks; + u32 ce, cau, vblock, rem; + + if (!per_ce || !w->sftl.user_blocks || !w->sftl.vbas_per_page) + return 0; + ce = sb_idx / per_ce; + rem = sb_idx % per_ce; + cau = rem / w->sftl.user_blocks; + vblock = rem % w->sftl.user_blocks; - return s_g_addr_to_vba(w, sb, ofs); + return whimory_pack_vba(w, ce, cau, vblock, + ofs / w->sftl.vbas_per_page, + ofs % w->sftl.vbas_per_page); } static int whimory_unpack_vba(const struct whimory *w, u32 vba, u32 *ce, u32 *cau, u32 *vblock, u32 *page, u32 *slot) { - u32 sb, ofs, per_ce; - if (!w->sftl.vbas_per_sb || !w->sftl.vbas_per_page || !w->sftl.user_blocks) return -EINVAL; - sb = s_g_vba_to_sb(w, vba); - ofs = s_g_vba_to_ofs(w, vba); - *page = ofs / w->sftl.vbas_per_page; - *slot = ofs % w->sftl.vbas_per_page; - per_ce = w->geom.num_cau * w->sftl.user_blocks; - if (!per_ce) - return -EINVAL; - *ce = sb / per_ce; - sb %= per_ce; - *cau = sb / w->sftl.user_blocks; - *vblock = sb % w->sftl.user_blocks; + /* Exact inverse of whimory_pack_vba(); see the layout there. */ + { + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 per_page, per_sb, rem, plane; + + if (!planes) + return -EINVAL; + per_page = planes * w->sftl.vbas_per_page; + per_sb = w->sftl.pages_per_sb * per_page; + if (!per_sb) + return -EINVAL; + + *vblock = vba / per_sb; + rem = vba % per_sb; + *page = rem / per_page; + plane = (rem % per_page) / w->sftl.vbas_per_page; + *slot = rem % w->sftl.vbas_per_page; + *ce = plane / w->geom.num_cau; + *cau = plane % w->geom.num_cau; + } + if (*ce >= w->geom.num_ce || *cau >= w->geom.num_cau) return -ERANGE; + if (*vblock >= whimory_vba_blocks(w)) + return -ERANGE; if (*page >= w->sftl.pages_per_sb) return -ERANGE; return 0; @@ -1851,12 +2155,29 @@ static bool fpart_has_xrmw(const u8 *page); *op=1 analogue. Special objects often live on SLC; try SLC * then MLC. Full 16 KiB data + 64B META; special uses first 16 META bytes. */ +/* + * Read one FPart page, trying SLC plane 1 then 0. + * + * This went through s5l8740_nand_page_read() until now, and never once + * reached the NAND. That function refuses any request carrying a meta buffer + * unless meta_dma_read is set, and meta_dma_read is deliberately off -- a + * permanent live CS kick reboots the device, so the sanctioned path is a + * temporary dma_session around cs_phys instead. Every FPart read therefore + * returned -EOPNOTSUPP: the log said reads=512 fail=512 in sixty + * milliseconds, which is far too fast to have been a NAND access at all, and + * "sig=0, not a native open" was a verdict about a region nobody had looked + * at. + * + * cs_phys is the same path classify uses, and it needs the caller to hold a + * DMA session -- fpart_scan_region() opens one. + */ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, - u32 page, void *data, u8 *meta) + u32 page, struct s5l8740_cs_page *csp, + void *data, u8 *meta) { - unsigned int ce, cau, i; + unsigned int ce, cau, i, sl; int last = -EIO; - const unsigned int slc_order[2] = { 1, 0 }; + const u8 slc_order[2] = { 1, 0 }; fpart_bank_to_ce_cau(w, bank, &ce, &cau); if (ce >= w->geom.num_ce || cau >= w->geom.num_cau || @@ -1867,11 +2188,25 @@ static int fpart_fil_read_page(struct whimory *w, u16 bank, u32 block, for (i = 0; i < 2; i++) { int ret; - ret = s5l8740_nand_page_read(ce, cau, block, page, slc_order[i], - 16, data, w->geom.page_size, - meta, S5L8740_NAND_META_SIZE); + ret = s5l8740_nand_cs_phys_read_slc((u8)ce, (u8)cau, (u16)block, + (u8)page, slc_order[i], + csp, 4); if (ret) continue; + + /* Flatten the four records back into the flat page and the + * 64-byte meta the FPart parsers expect. + */ + for (sl = 0; sl < N31_DATA_SLOTS; sl++) { + size_t doff = (size_t)sl * N31_DATA_SLOT_SIZE; + + if (doff + N31_DATA_SLOT_SIZE <= w->geom.page_size) + memcpy((u8 *)data + doff, csp->data[sl], + N31_DATA_SLOT_SIZE); + memcpy(meta + sl * WHIMORY_META_SIZE, + csp->meta_raw[sl], WHIMORY_META_SIZE); + } + last = 0; if (fpart_meta_special(meta, 0, NULL) || fpart_meta_is_assign(meta, NULL)) @@ -2093,21 +2428,42 @@ static int fpart_scan_region(struct whimory *w, u16 type, { u8 *page; u8 meta[S5L8740_NAND_META_SIZE]; + struct s5l8740_cs_page *csp; u16 bank, nbanks = fpart_num_banks(w); u32 b, p; int ret, reads = 0, tag30 = 0, xrmw = 0, wrmx = 0, fail = 0; unsigned int sample = 0; u32 hist[256]; + int sess; page = kvmalloc(w->geom.page_size, GFP_KERNEL); if (!page) return -ENOMEM; + csp = kvmalloc(sizeof(*csp), GFP_KERNEL); + if (!csp) { + kvfree(page); + return -ENOMEM; + } + memset(hist, 0, sizeof(hist)); if (page_hi >= w->geom.pages_per_block) page_hi = w->geom.pages_per_block - 1; + /* + * Arm live CS for the sweep. -EBUSY means an outer session is already + * open, which is fine -- it just means this one does not own the + * teardown. + */ + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + dev_warn(w->dev, "FPART_SCAN no DMA session (%d)\n", sess); + kvfree(csp); + kvfree(page); + return sess; + } + s5l8740_nand_reset(); for (bank = 0; bank < nbanks; bank++) { @@ -2122,7 +2478,7 @@ static int fpart_scan_region(struct whimory *w, u16 type, cond_resched(); ret = fpart_fil_read_page(w, bank, blk, p, - page, meta); + csp, page, meta); reads++; if (ret) { fail++; @@ -2226,6 +2582,9 @@ static int fpart_scan_region(struct whimory *w, u16 type, wrmx, w->fpart_ctx.count, *matched, 0xff, hist[0xff], 0x00, hist[0], 0x30, hist[0x30], 0x20, hist[0x20]); + if (!sess) + s5l8740_nand_dma_session_end(); + kvfree(csp); kvfree(page); return 0; } @@ -2286,11 +2645,13 @@ static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, u16 entry_i, u32 *gen_out) { struct fpart_special_entry *e; + struct s5l8740_cs_page *csp; u8 *page; u8 meta[S5L8740_NAND_META_SIZE]; u32 page_size, chunk_count = 1, copy_slots, chunk, slot; u32 object_len = 0, copy_len = 0, generation = 0; int ret = -ENOENT; + int sess; if (entry_i >= w->fpart_ctx.count) return -EINVAL; @@ -2304,6 +2665,23 @@ static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, if (!page) return -ENOMEM; + csp = kvmalloc(sizeof(*csp), GFP_KERNEL); + if (!csp) { + kvfree(page); + return -ENOMEM; + } + + /* Same as the scan: cs_phys needs live CS armed, and -EBUSY only + * means someone outside already armed it. + */ + sess = s5l8740_nand_dma_session_begin(); + if (sess && sess != -EBUSY) { + dev_warn(w->dev, "FPART_COPY no DMA session (%d)\n", sess); + kvfree(csp); + kvfree(page); + return sess; + } + for (chunk = 0; chunk < chunk_count; chunk++) { bool got = false; @@ -2315,7 +2693,7 @@ static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, break; cond_resched(); ret = fpart_fil_read_page(w, e->bank, e->block, pg, - page, meta); + csp, page, meta); if (ret) continue; if (!fpart_meta_special(meta, chunk, &meta_type)) @@ -2382,6 +2760,9 @@ static int fpart_read_special_copy(struct whimory *w, u8 *dst, u32 dst_len, entry_i, dst); ret = 0; out: + if (!sess) + s5l8740_nand_dma_session_end(); + kvfree(csp); kvfree(page); return ret; } @@ -3564,24 +3945,87 @@ static int whimory_ingest_btoc_page(struct whimory *w, unsigned int ce, const char *verdict = "NONE"; int hit = 0; - if (whimory_page_blank(page, 64)) + if (whimory_page_blank(page, 64)) { + w->sftl.btoc_blank++; return 0; + } if (whimory_btoc_looks_be_bte(page)) { if (whimory_btoc_parse_be_bte(w, page, len, ce, cau, vblock)) { verdict = "BE_BTE"; + w->sftl.btoc_be_bte++; hit = 1; } } if (!hit && whimory_btoc_looks_be_lpn(page)) { if (whimory_btoc_parse_be_lpn(w, page, len, ce, cau, vblock)) { verdict = "BE_LPN_ARRAY"; + w->sftl.btoc_be_lpn++; hit = 1; } } if (!hit && whimory_btoc_parse_bte(w, page, len, ce, cau, vblock)) { verdict = "LE_BTE"; + w->sftl.btoc_le_bte++; hit = 1; } + + /* + * Some BTOC pages carry an eight-byte header before the BTE array, + * and 255 of the 563 closed superblocks on this device use it. Every + * one of them was dropped -- 127 pages and about 2032 LBAs each -- + * which is what left directory sectors unreadable on a volume whose + * FAT mounted fine. + * + * struct whimory_bte is {weave_seq_add, aux, lba, span}, so the + * parser was reading the header's third and fourth words as lba and + * span. The dumps make the misread obvious: + * + * 00000000 00000002 0000c30d 000004f2 | 000004f2 00000002 ... + * ^header........^ ^read as lba/span^ | ^the real first record^ + * + * span came out as 1266 against a 508-VBA superblock, so the loop + * broke on record zero and the page was declared unrecognised. Shift + * eight bytes and the same parser reads lba=0x4f2 span=2, then 0x4f3, + * then 0x4f4 -- ascending LBAs with uniform spans, exactly what a + * BTOC is. + * + * Tried only after the unshifted parse yields nothing, and only when + * the first word is zero as it is in every sample, so a page that + * already parses cannot be re-read at the wrong offset. + */ + if (!hit && len > 8 && get_unaligned_le32(page) == 0 && + whimory_btoc_parse_bte(w, page + 8, len - 8, ce, cau, vblock)) { + verdict = "LE_BTE_HDR8"; + w->sftl.btoc_le_bte_hdr8++; + hit = 1; + } + + /* + * A BTOC nobody claimed is a whole closed superblock missing from the + * map -- 127 pages, about 2032 LBAs -- and 255 of 563 were going + * unclaimed with no record of it beyond a count that did not + * distinguish "erased" from "unrecognised". + * + * The read misses that follow look like this, and they are what stops + * files opening on a volume whose FAT reads fine: + * + * read miss fmss_lba=3723686 ret=-2 + * neighbor 3723682..85 MAPPED blk=1714 pg=43 slot=0..3 + * neighbor 3723686 UNMAPPED + * FAT-fs: Directory bread(block 3674407) failed + * + * So the unclaimed ones are dumped. Three parsers is three guesses at + * a format, and the bytes say whether there is a fourth shape here or + * whether one of the three is rejecting pages it should accept. + */ + if (!hit) { + w->sftl.btoc_unclaimed++; + if (w->sftl.btoc_unclaimed <= 8) + dev_info(w->dev, + "BTOC_UNCLAIMED n=%u ce=%u cau=%u vblock=%u first64=%32ph %32ph\n", + w->sftl.btoc_unclaimed, ce, cau, vblock, + page, page + 32); + } if (ftl_diag && w->sftl.btoc_pages_read <= 8) dev_info(w->dev, "BTOC_VERDICT ce=%u cau=%u vblock=%u %s first32=%32ph\n", @@ -3602,6 +4046,7 @@ static int whimory_rebuild_open_sb(struct whimory *w, struct whimory_sb *sb) ret = whimory_cs_read_page(w, sb->ce, sb->cau, sb->block, pg, data, S5L8740_NAND_PAGE_SIZE, spare, sizeof(spare)); + w->sftl.open_pages_read++; if (ret) break; if (whimory_page_blank(data, 64) && @@ -3841,7 +4286,7 @@ static int whimory_cxt_load_sb(struct whimory *w, u32 sb_idx) for (ofs = 0; ofs < s->vbas_per_sb && !done; ofs += zone) { n = min(zone, s->vbas_per_sb - ofs); for (i = 0; i < n; i++) { - vba = s_g_addr_to_vba(w, sb_idx, ofs + i); + vba = whimory_sb_ofs_to_vba(w, sb_idx, ofs + i); ret = whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot); if (ret) @@ -3999,7 +4444,7 @@ static int whimory_cxt_read_vba(struct whimory *w, u32 sb_idx, u32 ofs, { struct whimory_sftl *s = &w->sftl; u32 ce, cau, vblock, page, slot, pblock, key; - u32 vba = s_g_addr_to_vba(w, sb_idx, ofs); + u32 vba = whimory_sb_ofs_to_vba(w, sb_idx, ofs); int ret; ret = whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot); @@ -4202,24 +4647,23 @@ MODULE_PARM_DESC(cxt_max_extents, * only contiguous in our space within one 4-slot group, because the next * group belongs to a different plane. */ +/* + * Now that the VBA space is the native one, a CXT VBA is already a VBA. + * All that is left is the range check the old translation did on the way + * through -- kept because an out-of-range CXT entry is real and must not + * be turned into a mapping onto some unrelated block. + */ static int whimory_cxt_vba_translate(struct whimory *w, u32 cxt_vba, u32 *out) { u32 planes = w->geom.num_ce * w->geom.num_cau; - u32 per_page, per_sb, vblock, rem, page, plane, slot; + u32 per_sb; if (!planes || !w->sftl.vbas_per_page || !w->sftl.pages_per_sb) return -EINVAL; - per_page = planes * w->sftl.vbas_per_page; - per_sb = w->sftl.pages_per_sb * per_page; - vblock = cxt_vba / per_sb; - rem = cxt_vba % per_sb; - page = rem / per_page; - plane = (rem % per_page) / w->sftl.vbas_per_page; - slot = rem % w->sftl.vbas_per_page; - if (vblock >= w->sftl.user_blocks || page >= w->sftl.pages_per_sb) + per_sb = w->sftl.pages_per_sb * planes * w->sftl.vbas_per_page; + if (cxt_vba / per_sb >= whimory_vba_blocks(w)) return -ERANGE; - *out = whimory_pack_vba(w, plane / w->geom.num_cau, - plane % w->geom.num_cau, vblock, page, slot); + *out = cxt_vba; return 0; } @@ -4243,6 +4687,34 @@ static int whimory_cxt_ext_add(struct whimory *w, u32 lba, u32 span, u32 vba) return 0; } +/* + * Spell a raw CXT VBA out in the terms it is built from. + * + * Every question about the CXT map so far -- are the holes real, is the + * translation ceiling right -- has been answered by arguing about + * arithmetic. A VBA printed as vblock/page/plane/slot settles them by + * inspection: a genuine hole sentinel is far outside the geometry, while an + * arithmetic fault lands just past a boundary. + */ +static void whimory_vba_describe(const struct whimory *w, u32 vba, + char *buf, size_t len) +{ + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 per_page, per_sb, rem; + + if (!planes || !w->sftl.vbas_per_page || !w->sftl.pages_per_sb) { + scnprintf(buf, len, "?"); + return; + } + per_page = planes * w->sftl.vbas_per_page; + per_sb = w->sftl.pages_per_sb * per_page; + rem = vba % per_sb; + scnprintf(buf, len, "vblk=%u pg=%u plane=%u slot=%u", + vba / per_sb, rem / per_page, + (rem % per_page) / w->sftl.vbas_per_page, + rem % w->sftl.vbas_per_page); +} + /* * A TREE record is {start_lba, CONTIG_SPAN} followed by (vba, span) pairs, * each pair advancing the logical cursor by span. Same shape as @@ -4278,25 +4750,67 @@ static int whimory_cxt_parse_tree(struct whimory *w, const u8 *data, break; w->sftl.cxt_records_seen++; if (vba >= WHIMORY_CXT_VBA_HOLE || vba >= w->l2v.invalid_vba) { - /* Hole: consumes logical space, maps nothing. */ + /* + * Hole: consumes logical space, maps nothing. + * + * Sampled, because "609 holes" on its own does not say + * whether the checkpoint is describing unmapped space + * or whether the ceiling is wrong. A sentinel sits far + * outside the geometry; a ceiling fault sits just past + * it. The covered LBA count tells them apart too -- a + * volume a quarter full should have most of its + * logical space in holes. + */ w->sftl.cxt_hole_entries++; + w->sftl.cxt_hole_lbas += span; + if (w->sftl.cxt_hole_entries <= 12) { + char d[64]; + + whimory_vba_describe(w, vba, d, sizeof(d)); + dev_info(w->dev, + "CXT_HOLE n=%u vba=0x%08x (%s) lba=%u span=%u limit=0x%x\n", + w->sftl.cxt_hole_entries, vba, d, lba, + span, w->l2v.invalid_vba); + } lba += span; continue; } while (span) { - u32 chunk = w->sftl.vbas_per_page - - (vba % w->sftl.vbas_per_page); + /* + * Whole runs now. This used to break every extent at + * the next 4-slot boundary because consecutive CXT + * VBAs were not contiguous in the old bank-major + * space. They are in the native one, so a run stays a + * run and the range count drops by about four times. + */ + u32 chunk = span; u32 tvba; int ret; - - if (chunk > span) - chunk = span; if (!whimory_cxt_vba_translate(w, vba, &tvba)) { ret = whimory_cxt_ext_add(w, lba, chunk, tvba); - if (ret) + if (ret) { + /* Loud: a full table is a short map, + * and a short map is lost files. + */ + w->sftl.cxt_ext_nospc++; + dev_warn(w->dev, + "CXT extent table full at %u -- map will be short\n", + w->n_cxt_ext); return ret; + } } else { w->sftl.cxt_xlate_fail++; + if (w->sftl.cxt_xlate_fail <= 12) { + char d[64]; + + whimory_vba_describe(w, vba, d, + sizeof(d)); + dev_info(w->dev, + "CXT_XLATE_FAIL n=%u vba=0x%08x (%s) lba=%u span=%u vba_blocks=%u\n", + w->sftl.cxt_xlate_fail, vba, d, + lba, chunk, + whimory_vba_blocks(w)); + } } lba += chunk; vba += chunk; @@ -4315,7 +4829,7 @@ static int whimory_cxt_build_from_sb(struct whimory *w, u32 sb_idx) u8 *data = s->gc_data; u8 meta[WHIMORY_META_SIZE]; u8 spare[S5L8740_NAND_META_SIZE]; - u32 ofs, last_key = ~0u, next_lba = 0; + u32 ofs, last_key = ~0u, next_lba = 0, n_l2v = 0; bool lba_valid = false; int ret; @@ -4325,19 +4839,46 @@ static int whimory_cxt_build_from_sb(struct whimory *w, u32 sb_idx) for (ofs = 0; ofs < s->vbas_per_sb; ofs++) { ret = whimory_cxt_read_vba(w, sb_idx, ofs, data, meta, spare, &last_key); - if (ret) + if (ret) { + dev_warn(w->dev, + "CXT sb=%u read failed at ofs=%u/%u (%d) -- rest of this checkpoint dropped\n", + sb_idx, ofs, s->vbas_per_sb, ret); return ret; + } if (meta[0] != WHIMORY_META_TYPE_SFTL_CXT) continue; if (meta[1] == WHIMORY_CXT_TAG_CLEAN) break; if (meta[1] != WHIMORY_CXT_TAG_L2V) continue; + n_l2v++; ret = whimory_cxt_parse_tree(w, data, WHIMORY_LBA_SIZE, &next_lba, &lba_valid); - if (ret) + if (ret) { + /* + * Stop this checkpoint, but say what stopping cost. + * + * Carrying on past a bad record is not an option: the + * logical cursor is what places every record after it, + * so a record parsed against a broken cursor lands at + * the wrong LBA, and a mapping in the wrong place is + * worse than a mapping missing. But the records after + * it were silently lost before, and a checkpoint that + * quietly stops halfway looks exactly like one that + * finished. + */ + s->cxt_records_lost += s->vbas_per_sb - ofs; + dev_warn(w->dev, + "CXT sb=%u parse stopped at record %u of %u (%d, %u L2V records read) -- up to %u records dropped\n", + sb_idx, ofs, s->vbas_per_sb, ret, n_l2v, + s->vbas_per_sb - ofs); return ret; + } } + if (!n_l2v) + dev_info(w->dev, + "CXT sb=%u holds no L2V records (clean or superseded)\n", + sb_idx); return 0; } @@ -4391,6 +4932,10 @@ static int whimory_cxt_build_candidate(struct whimory *w) w->sftl.cxt_records_seen = 0; w->sftl.cxt_hole_entries = 0; w->sftl.cxt_xlate_fail = 0; + w->sftl.cxt_hole_lbas = 0; + w->sftl.cxt_ext_nospc = 0; + w->sftl.cxt_records_lost = 0; + w->sftl.cxt_sb_empty = 0; sess = s5l8740_nand_dma_session_begin(); for (i = 0; i < n_all; i++) { @@ -4403,8 +4948,20 @@ static int whimory_cxt_build_candidate(struct whimory *w) all[i].sb, ret); continue; } - if (w->n_cxt_ext == before) + if (w->n_cxt_ext == before) { + /* + * Counted. sbs_used=2/4 read as "two checkpoints were + * stale", and it may well be, but nothing here had + * ever checked -- a superblock that parsed fine and + * produced nothing left exactly the same trace as one + * that was never looked at. + */ + w->sftl.cxt_sb_empty++; + dev_info(w->dev, + "CXT_CAND_MAP sb=%u weave=%llu parsed but added no extents\n", + all[i].sb, (unsigned long long)all[i].weave); continue; + } ok++; if (all[i].weave > w->cxt_ext_weave) { w->cxt_ext_weave = all[i].weave; @@ -4434,6 +4991,18 @@ static int whimory_cxt_build_candidate(struct whimory *w) ok, n_all, w->n_cxt_ext, w->sftl.cxt_records_seen, w->sftl.cxt_hole_entries, w->sftl.cxt_xlate_fail, overlaps, (unsigned long long)w->cxt_ext_weave); + /* + * The accounting the previous line was missing. hole_lbas is the one + * that settles whether the holes are a fault: this volume maps about + * 938k of 3.86M sectors, so if the holes cover roughly the other + * three quarters they are describing unmapped space and are correct. + * If they cover a little and there are hundreds of them, they are + * not. + */ + dev_info(w->dev, + "CXT_MAP hole_lbas=%u empty_sbs=%u records_lost=%u nospc=%u\n", + w->sftl.cxt_hole_lbas, w->sftl.cxt_sb_empty, + w->sftl.cxt_records_lost, w->sftl.cxt_ext_nospc); return 0; } @@ -4816,6 +5385,9 @@ static void whimory_print_recovery_stats(struct whimory *w) " cxt_blocks_seen=%u cxt_records_seen=%u cxt_l2v_updates=%u\n" " btoc_pages_read=%u btoc_pages_valid=%u btoc_entries_seen=%u btoc_l2v_updates=%u\n" " btoc_meta_confirmed=%u btoc_meta_mismatch=%u btoc_skipped_zero=%u\n" + " btoc_blank=%u be_bte=%u be_lpn=%u le_bte=%u hdr8=%u unclaimed=%u\n" + " btoc_fallback sbs=%u pages=%u hits=%u (%u pages/sb)\n" + " unknown_fallback sbs=%u pages=%u hits=%u (%u pages/sb)\n" " btoc_confirm_pages=%u btoc_confirm_capped=%u btoc_confirm_budget_stop=%u\n" " btoc_unmap_entries=%u btoc_hole_entries=%u btoc_unknown_entries=%u\n" " btoc_token_ffff0000=%u btoc_token_ffffff00=%u btoc_token_ffffffff=%u btoc_holelist_ffff0001=%u\n" @@ -4836,6 +5408,12 @@ static void whimory_print_recovery_stats(struct whimory *w) s->btoc_l2v_updates, s->btoc_meta_confirmed, s->btoc_meta_mismatch, s->btoc_skipped_zero, + s->btoc_blank, s->btoc_be_bte, s->btoc_be_lpn, + s->btoc_le_bte, s->btoc_le_bte_hdr8, s->btoc_unclaimed, + s->btoc_fb_sbs, s->btoc_fb_pages, s->btoc_fb_hits, + s->btoc_fb_sbs ? s->btoc_fb_pages / s->btoc_fb_sbs : 0, + s->unk_fb_sbs, s->unk_fb_pages, s->unk_fb_hits, + s->unk_fb_sbs ? s->unk_fb_pages / s->unk_fb_sbs : 0, s->btoc_confirm_pages, s->btoc_confirm_capped, s->btoc_confirm_budget_stop, s->btoc_unmap_entries, s->btoc_hole_entries, @@ -5034,6 +5612,8 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) u8 meta0[S5L8740_NAND_META_SIZE]; u8 meta127[S5L8740_NAND_META_SIZE]; u8 *p127; + u32 *meta0_hist; + u32 *meta127_hist; int ret; nscan = scan_blocks ? scan_blocks : s->user_blocks; @@ -5043,6 +5623,18 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) p127 = s->btoc_page; if (!p127) return -ENOMEM; + + /* + * Histogram of the page 0 meta type byte over every block scanned. + * When a class comes out at zero -- cxt=0, say -- the useful question + * is whether the flash has no such block or whether the test for it + * stopped matching, and the classify counters cannot tell those apart. + * This can: WHIMORY_META_TYPE_SFTL_CXT is 0x1f, so a non-zero count at + * 0x1f with cxt=0 means the recogniser is wrong, and a zero count means + * the blocks really are not there. + */ + meta0_hist = kcalloc(256, sizeof(*meta0_hist), GFP_KERNEL); + meta127_hist = kcalloc(256, sizeof(*meta127_hist), GFP_KERNEL); s->btoc_dumps_left = ftl_diag ? 5 : 0; w->l2v_defer_pack = true; s->btoc_verified = 0; @@ -5071,6 +5663,7 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (nsb >= s->num_sb) goto classify_done; if ((b & 0x1f) == 0 && ftl_progress_due(w)) + ftl_progress_set(w, "classify", b, nscan), dev_info(w->dev, "SFTL classify ce=%u cau=%u blk=%u/%u nsb=%u\n", ce, cau, b, nscan, nsb); @@ -5079,37 +5672,174 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) usleep_range(recover_yield_us, recover_yield_us + 500); } - r0 = whimory_cs_read_page(w, ce, cau, b, 0, + /* + * Page 127 is read lazily. It used to be fetched for + * every block alongside page 0, but meta127 is only + * ever consulted for the closed/BTOC test below -- so + * on this device 5484 empty blocks and the 4 CXT + * blocks paid for a full page read whose result was + * discarded. That is roughly 70 percent of the page + * 127 traffic and a third of all classify IO, against + * a pass that takes about 57 seconds at 7.4 ms per + * block. + * + * A blank page 0 with an erased meta is taken as an + * empty block without confirming page 127. Blocks are + * written from page 0 upwards, so blank-at-0 with data + * at 127 does not occur in normal operation. If page 0 + * cannot be read at all the old both-failed rule still + * applies and page 127 is consulted. + */ + /* + * One short read answers both questions. + * + * Empty is the common answer -- about seven + * blocks in ten on this volume -- and slot 0 + * settles it. But the same 4112 bytes also + * carry the meta that classifies a non-empty + * block, so there is no reason to throw them + * away and read the page again: the block + * either finishes here or escalates to the + * full read, and nothing reads page 0 twice. + */ + r0 = -EAGAIN; + if (fast_empty_probe) { + const u8 *d0 = NULL; + u8 m0[WHIMORY_META_SIZE]; + + if (!whimory_cs_read_slot0(w, ce, cau, + b, 0, &d0, + m0)) { + if (whimory_page_blank(d0, 64) && + whimory_meta_erased(m0, + WHIMORY_META_SIZE)) { + s->empty_sbs++; + s->fast_empty_hits++; + continue; + } + if (whimory_meta0_is_conclusive(m0)) { + /* + * Slots 1..3 stay 0xff + * so an erased-meta + * test on them tells + * the truth rather + * than repeating the + * last page's bytes. + */ + memset(meta0, 0xff, + sizeof(meta0)); + memcpy(meta0, m0, + WHIMORY_META_SIZE); + memcpy(w->sftl.data_page, + d0, + S5L8740_NAND_SLOT_DATA); + memset(w->sftl.data_page + + S5L8740_NAND_SLOT_DATA, + 0xff, + S5L8740_NAND_PAGE_SIZE - + S5L8740_NAND_SLOT_DATA); + s->fast_slot0_hits++; + r0 = 0; + } + } + } + + r127 = -EAGAIN; /* not read yet */ + + if (r0) + r0 = whimory_cs_read_page(w, ce, cau, b, 0, w->sftl.data_page, S5L8740_NAND_PAGE_SIZE, meta0, sizeof(meta0)); - r127 = whimory_cs_read_page(w, ce, cau, b, - WHIMORY_BTOC_PAGE, - p127, - S5L8740_NAND_PAGE_SIZE, - meta127, - sizeof(meta127)); - if (!r0) + if (!r0) { whimory_note_meta0(w, ce, cau, b, 0, w->sftl.data_page, meta0); - if (r0 && r127) - continue; - if ((!r0 && whimory_page_blank(w->sftl.data_page, 64) && - whimory_meta_erased(meta0, 16)) && - (r127 || (whimory_page_blank(p127, 64) && - whimory_meta_erased(meta127, 16)))) { + if (meta0_hist) + meta0_hist[meta0[0]]++; + } + + if (!btoc_page_lazy) + r127 = whimory_cs_read_page(w, ce, cau, b, + WHIMORY_BTOC_PAGE, + p127, + S5L8740_NAND_PAGE_SIZE, + meta127, + sizeof(meta127)); + + /* + * Only call a block empty when page 127 agrees, or + * when we deliberately did not look. Checking page 0 + * alone is what the lazy path relies on. + */ + if (!r0 && whimory_page_blank(w->sftl.data_page, 64) && + whimory_meta_erased(meta0, 16) && + (btoc_page_lazy || + (!r127 && whimory_page_blank(p127, 64) && + whimory_meta_erased(meta127, 16)))) { s->empty_sbs++; continue; } + + /* CXT is decided from meta0 alone. */ + if (r127 == -EAGAIN && + (r0 || !whimory_meta_slot0_or_any_cxt(meta0))) { + r127 = whimory_cs_read_page(w, ce, cau, b, + WHIMORY_BTOC_PAGE, + p127, + S5L8740_NAND_PAGE_SIZE, + meta127, + sizeof(meta127)); + if (r0 && r127) + continue; + if (r0 && + whimory_page_blank(p127, 64) && + whimory_meta_erased(meta127, 16)) { + s->empty_sbs++; + continue; + } + } sb = &s->sbs[nsb]; sb->ce = ce; sb->cau = cau; sb->block = b; + /* + * Two weaves, because they answer different + * questions and only one of them is any use + * for the CXT diff. + * + * sb->weave is page 0: the OLDEST content in + * the superblock. A block is filled from page + * 0 upwards, so page 0 was written when the + * block was opened -- which may have been long + * before the checkpoint even if the block was + * still being appended to long after it. + * + * sb->weave_max is the newest weave we have + * actually seen. For a closed superblock that + * is page 127, written when the block was + * sealed, so it is the real answer. For an + * open one there is no page whose weave bounds + * the block, which is why the replay below + * refuses to skip open superblocks at all. + */ sb->weave = 0; + sb->weave_max = 0; + sb->weave_max_p127 = 0; if (!r0 && (whimory_meta_is_data_raw(meta0) || meta0[0] == WHIMORY_META_TYPE_SFTL_CXT)) sb->weave = whimory_weave48(meta0); + if (!r127 && (whimory_meta_is_data_raw(meta127) || + whimory_meta_any_btoc(meta127))) { + sb->weave_max = whimory_weave48(meta127); + sb->weave_max_p127 = 1; + } + if (sb->weave_max < sb->weave) { + sb->weave_max = sb->weave; + sb->weave_max_p127 = 0; + } + if (meta127_hist && !r127) + meta127_hist[meta127[0]]++; if (!r0 && whimory_meta_is_cxt_base(meta0, 0)) { u32 vblock = whimory_vfl_virt(w, cau, b); u32 sb_idx = whimory_sb_index(w, ce, cau, @@ -5129,8 +5859,30 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) sb->kind = WHIMORY_SB_OPEN; s->open_sbs++; } else { + /* + * Nothing recognised this block, which + * is not the same as it being empty -- + * the empty test ran earlier and said + * no. 78 blocks land here on this + * volume, meta type 00 on 76 of them + * and 0x4b on two, and until now they + * were counted and then dropped before + * ever entering sbs[]. A block that + * never enters sbs[] is never + * replayed, so up to 2032 LBAs each + * were unreachable with nothing in the + * log to say so. + * + * They are kept now and rebuilt from + * per-page meta in the replay, which + * is the one method that needs no + * recognised structure at all. Cost is + * bounded the same way as the BTOC + * fallback: a blank page 0 stops it + * after one read. + */ + sb->kind = WHIMORY_SB_UNKNOWN; s->unknown_sbs++; - continue; } nsb++; } @@ -5142,6 +5894,56 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) "SFTL classified nsb=%u closed=%u open=%u cxt=%u empty=%u unknown=%u\n", nsb, s->btoc_sbs, s->open_sbs, s->cxt_sbs, s->empty_sbs, s->unknown_sbs); + dev_info(w->dev, "SFTL fast-empty probe settled %u of %u blocks\n", + s->fast_empty_hits, s->empty_sbs); + dev_info(w->dev, "SFTL slot0 read settled %u non-empty blocks\n", + s->fast_slot0_hits); + + if (meta0_hist) { + char hb[192]; + unsigned int t, hn = 0; + + for (t = 0; t < 256; t++) { + if (!meta0_hist[t] || hn + 16 >= sizeof(hb)) + continue; + hn += scnprintf(hb + hn, sizeof(hb) - hn, "%02x:%u ", + t, meta0_hist[t]); + } + dev_info(w->dev, + "SFTL meta0 types %s(cxt=0x%02x btoc=0x%02x data=0x%02x)\n", + hb, WHIMORY_META_TYPE_SFTL_CXT, WHIMORY_META_TYPE_BTOC, + WHIMORY_META_TYPE_DATA); + kfree(meta0_hist); + meta0_hist = NULL; + } + + /* + * What page 127 actually holds, and how the superblocks sit either + * side of the checkpoint. + * + * Both were guesses until now. open=1619 against closed=563 is a + * strange shape for a mostly-static volume and says either that a + * great many blocks really are mid-write, or that page 127 is not + * where this geometry keeps its BTOC -- the type histogram tells + * those apart. The weave split says how much of the volume the + * checkpoint genuinely covers, which is the number the diff replay + * is supposed to act on. + */ + if (meta127_hist) { + char hb[192]; + unsigned int t, hn = 0; + + for (t = 0; t < 256; t++) { + if (!meta127_hist[t] || hn + 16 >= sizeof(hb)) + continue; + hn += scnprintf(hb + hn, sizeof(hb) - hn, "%02x:%u ", + t, meta127_hist[t]); + } + dev_info(w->dev, "SFTL meta127 types %s(page %u)\n", + hb, WHIMORY_BTOC_PAGE); + kfree(meta127_hist); + meta127_hist = NULL; + } whimory_cxt_index_build(w, nsb); /* @@ -5177,11 +5979,62 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) if (sb->kind == WHIMORY_SB_CXT) continue; - if (use_cxt && s->cxt_loaded && sb->weave && - sb->weave < w->cxt_base_weave) { + + /* + * Skip what the checkpoint already covers -- but only when we + * can prove it does. + * + * This used to test sb->weave, which is page 0: the oldest + * page in the superblock. That is the wrong end. A block being + * appended to right up to the moment of the crash still has an + * old page 0, so every superblock on the volume tested older + * than the checkpoint and the diff replay skipped all 2182 of + * them -- sbs=0 replayed. The map then reflected the volume + * exactly as of the last checkpoint and nothing written after + * it, which is precisely the missing-recent-files symptom. + * + * The fix for that was to stop skipping open superblocks at + * all, on the reasoning that an open block is still being + * written and no page in it bounds the rest. On this hardware + * that reasoning does not hold, and the measurement is + * unambiguous: + * + * SFTL meta127 types 00:76 01:1615 1c:563 ff:6 + * + * 1615 of the 1619 "open" superblocks carry user data at page + * 127. They are full blocks that were never sealed with a + * BTOC, not blocks mid-write -- the rebuild proved it by + * reading 126 pages per superblock before finding a blank one. + * A full block has a real newest page, so page 127 bounds it + * exactly as it bounds a closed one. + * + * Leaving them unskipped was not merely slow. It read 205194 + * pages in 411 seconds, had 98.6 percent of the slots rejected + * as stale, and overrode 5726 CXT mappings with older data -- + * on a volume where not one superblock is newer than the + * checkpoint (weave newer=0 older=2182). A map that had been + * mounting stopped mounting. + * + * So the test is the bound, not the kind: skip only when the + * weave came from page 127. A superblock whose page 127 gave + * no usable weave has nothing but page 0 to offer, which is + * the wrong end again, and is replayed. + */ + if (!sb->weave_max) + s->weave_none++; + else if (sb->weave_max >= w->cxt_base_weave) + s->weave_newer++; + else + s->weave_older++; + + if (use_cxt && s->cxt_loaded && sb->weave_max_p127 && + sb->weave_max && sb->weave_max < w->cxt_base_weave) { s->diff_skipped_sbs++; continue; } + if (use_cxt && s->cxt_loaded && !sb->weave_max_p127 && + sb->weave && sb->weave < w->cxt_base_weave) + s->diff_open_kept++; s->diff_replayed_sbs++; if (sb->kind == WHIMORY_SB_CLOSED) { int ingested; @@ -5213,9 +6066,55 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) S5L8740_NAND_PAGE_SIZE); s->claim_weave = 0; s->claim_source = 0; - if (ingested) + if (ingested) { s->btoc_pages_valid++; + } else { + /* + * No BTE array here -- rebuild the superblock + * from per-page meta instead of writing it off. + * + * 70 of these remain after the header-8 parse + * and they share one shape: header, aux=0x7fc, + * then zeros where the first record would be. + * The decomp of the writer (s_btoc sub_567E3C) + * says that cannot be a record. It writes + * v11[2]=lba and v11[3]=span with the caller + * asserting span != 0, and it keeps + * nextVbaOfs += span consistent with + * s_g_addr_to_vba(sb, nextVbaOfs) afterwards -- + * a zero span would break that invariant. aux + * is not a length either; at the call site it + * is a per-write tag lifted from the stream + * context, so 0x7fc there is not the array + * declaring its own size. And 0x7fc is 2044, + * which is a superblock's 2048 VBAs less one + * 4-slot group. + * + * So these read as sealed-block trailers: a + * fill count written when the block closed, + * terminator after it, no BTE array at all. + * That is not the same as an empty superblock, + * and the difference is about 2032 LBAs each. + * + * Rather than decide which, measure. The + * rebuild costs one read against an empty + * block -- page 0 comes back blank and it + * stops -- and about 127 against a full one. + * The counters below say which happened. + */ + int fb; + + s->btoc_fb_sbs++; + fb = s->open_pages_read; + ret = whimory_rebuild_open_sb(w, sb); + s->btoc_fb_pages += s->open_pages_read - fb; + if (ret > 0) + s->btoc_fb_hits += ret; + else if (ret < 0) + return ret; + } if (ftl_progress_due(w)) + ftl_progress_set(w, "replay", i, nsb), dev_info(w->dev, "SFTL replay progress i=%u/%u closed_valid=%u " "open_updates=%u unmap_calls=%u stale_rej=%u " @@ -5225,15 +6124,37 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) s->stale_mapping_rejected, s->mapped_lbas, s->range_nodes, s->btoc_confirm_pages, s->range_budget_stop); + } else if (sb->kind == WHIMORY_SB_UNKNOWN) { + /* Same rebuild, counted apart so the two unknowns -- + * BTOCs with no array, and blocks with no recognised + * meta at all -- stay distinguishable. + */ + int fb = s->open_pages_read; + + s->unk_fb_sbs++; + ret = whimory_rebuild_open_sb(w, sb); + s->unk_fb_pages += s->open_pages_read - fb; + if (ret > 0) + s->unk_fb_hits += ret; + else if (ret < 0) + return ret; } else if (sb->kind == WHIMORY_SB_OPEN) { - if (max_open_sbs && open_done >= max_open_sbs) + if (max_open_sbs && open_done >= max_open_sbs) { + /* Counted, not silent: a cap that drops open + * superblocks drops recent writes, and a map + * that is quietly short is worse than a slow + * one. + */ + s->open_truncated++; continue; + } ret = whimory_rebuild_open_sb(w, sb); if (ret > 0) open_done++; else if (ret < 0) return ret; if (ftl_progress_due(w)) + ftl_progress_set(w, "open", open_done, s->open_sbs), dev_info(w->dev, "SFTL open progress done=%u/%u i=%u/%u " "open_updates=%u ranges=%u mapped=%u\n", @@ -5244,8 +6165,31 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) } dev_info(w->dev, - "SFTL diff replay sbs=%u skipped_by_cxt=%u cxt_seeded=%u\n", - s->diff_replayed_sbs, s->diff_skipped_sbs, s->cxt_l2v_updates); + "SFTL diff replay sbs=%u skipped_by_cxt=%u cxt_seeded=%u " + "open_kept=%u open_truncated=%u\n", + s->diff_replayed_sbs, s->diff_skipped_sbs, s->cxt_l2v_updates, + s->diff_open_kept, s->open_truncated); + dev_info(w->dev, + "SFTL weave vs cxt base=%llu: newer=%u older=%u none=%u\n", + w->cxt_base_weave, s->weave_newer, s->weave_older, + s->weave_none); + /* + * How deep the open rebuilds went. A genuinely open superblock stops + * at its first blank page, so pages/sb well under 127 says these + * really are partly written; pages/sb at 127 says they are full + * blocks that classify called open because page 127 held data rather + * than a BTOC, and the fix belongs in the classification instead. + */ + if (open_done) + dev_info(w->dev, + "SFTL open rebuild sbs=%u pages=%u (%u pages/sb)\n", + open_done, s->open_pages_read, + s->open_pages_read / open_done); + if (s->open_truncated) + dev_warn(w->dev, + "SFTL %u open superblocks dropped by max_open_sbs=%u -- " + "recent writes in them are NOT in the map\n", + s->open_truncated, max_open_sbs); w->l2v_defer_pack = false; ret = whimory_l2v_build_from_ranges(w); if (ret) { @@ -5928,8 +6872,39 @@ static ssize_t whimory_status_show(struct device *dev, } static DEVICE_ATTR_RO(whimory_status); +/* + * One field per line, no prose, so a shell loop or a UI can read this + * without parsing the human log. percent is -1 rather than 0 while the + * total is unknown, so a bar can show indeterminate instead of snapping + * back to the left every time a phase begins. + */ +static ssize_t recover_progress_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct whimory *w = whimory_dev; + int pct = -1; + + if (!w) + return sysfs_emit(buf, "state=absent\n"); + if (w->prog_total) + /* 32-bit: cur is a block/SB index, so *100 cannot overflow, and + * a u64 divide would need div_u64 on arm anyway. */ + pct = min_t(unsigned int, 100, + w->prog_cur / (w->prog_total / 100 + 1)); + return sysfs_emit(buf, + "state=%s\nphase=%s\ncur=%u\ntotal=%u\n" + "percent=%d\nmapped_lbas=%u\ndisk=%s\n", + whimory_recovery_state_name(), + w->prog_phase ? w->prog_phase : "idle", + w->prog_cur, w->prog_total, pct, + w->sftl.mapped_lbas, + w->disk ? "registered" : "none"); +} +static DEVICE_ATTR_RO(recover_progress); + static struct attribute *ftl_attrs[] = { &dev_attr_whimory_status.attr, + &dev_attr_recover_progress.attr, NULL, }; static const struct attribute_group ftl_attr_group = { diff --git a/drivers/misc/nand-s5l8740.c b/drivers/misc/nand-s5l8740.c index fdfb8a479bf63d..cf49a8d47b5988 100755 --- a/drivers/misc/nand-s5l8740.c +++ b/drivers/misc/nand-s5l8740.c @@ -6292,6 +6292,20 @@ static int fmss_probe(struct platform_device *pdev) return 0; } +/* + * The controller has its own DMA. Handing the machine over with a + * transfer in flight lets it complete into memory the next kernel owns, + * and on the write side it can leave a page half-programmed -- so this + * is the one shutdown here that protects the flash rather than just RAM. + */ +static void fmss_shutdown(struct platform_device *pdev) +{ + struct nand_s5l8740 *f = platform_get_drvdata(pdev); + + if (f) + fmss_dma_teardown(f); +} + static void fmss_remove(struct platform_device *pdev) { struct nand_s5l8740 *f = platform_get_drvdata(pdev); @@ -6308,6 +6322,7 @@ static void fmss_remove(struct platform_device *pdev) static struct platform_driver nand_driver = { .probe = fmss_probe, .remove = fmss_remove, + .shutdown = fmss_shutdown, .driver = { .name = "s5l8740-nand", .dev_groups = nand_groups, @@ -6737,8 +6752,57 @@ EXPORT_SYMBOL_GPL(s5l8740_nand_meta_pick_lba); * FTL map CS physical read: always slot0/span4/rec4112. * Fills struct s5l8740_cs_page. No lba_map ingest. */ +/* + * Read only the first record of a page. + * + * A page is four 4096+16 records and the full read moves 16448 bytes. + * The FTL classify sweep looks at 64 bytes of slot-0 data and slot-0's + * 16-byte meta to decide "is this block empty", and seventy percent of + * the volume answers yes -- so most of that sweep was transferring 16 KB + * to read 80 bytes. Span 1 moves 4112 bytes instead. + * + * Only the empty test is safe on one record. Anything else classify asks + * -- CXT in particular -- inspects all four slots' meta, so the caller + * must fall back to the full read whenever the answer is not "empty". + * Slots 1..3 are left as 0xff here so a caller that ignores that rule + * sees erased meta rather than stale data from the previous page. + */ +int s5l8740_nand_cs_phys_read_slot0(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out) +{ + return s5l8740_nand_cs_phys_read_span(ce, cau, block, page, out, 1); +} +EXPORT_SYMBOL_GPL(s5l8740_nand_cs_phys_read_slot0); + int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, struct s5l8740_cs_page *out) +{ + return s5l8740_nand_cs_phys_read_span(ce, cau, block, page, out, 4); +} + +int s5l8740_nand_cs_phys_read_span(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out, unsigned int span) +{ + return s5l8740_nand_cs_phys_read_slc(ce, cau, block, page, 0, out, + span); +} + +/* + * The same read, on a nominated SLC plane. + * + * Everything above reads slc=0 because that is where the user area lives. + * The FPart region at the tail of each CAU does not: it is SLC, and locating + * it means trying slc=1 before slc=0. + * + * That scan used to go through s5l8740_nand_page_read(), which refuses any + * request for meta unless meta_dma_read is set -- and it is deliberately not + * set, because a permanent live CS kick reboots the device. So every FPart + * read returned -EOPNOTSUPP before touching the NAND: 512 reads, 512 + * failures, sixty milliseconds, and a "signature not found" verdict about a + * region nobody had actually looked at. + */ +int s5l8740_nand_cs_phys_read_slc(u8 ce, u8 cau, u16 block, u8 page, u8 slc, + struct s5l8740_cs_page *out, unsigned int span) { struct nand_s5l8740 *f = nand_dev; u32 addr; @@ -6760,7 +6824,7 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, return -EPERM; memset(out, 0, sizeof(*out)); - addr = fmss_ppn_addr(cau, block, page, 0); + addr = fmss_ppn_addr(cau, block, page, slc); mutex_lock(&f->lock); if (cs_reset_every && f->pages_since_reset >= cs_reset_every) @@ -6770,7 +6834,7 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, dma_armed = true; dma_skip_ingest = true; t0 = ktime_get_ns(); - ret = fmss_dma_page_read_records(f, ce, addr, 0, 4); + ret = fmss_dma_page_read_records(f, ce, addr, 0, span); t1 = ktime_get_ns(); dma_skip_ingest = false; if (!dma_one_shot) @@ -6827,6 +6891,7 @@ int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, mutex_unlock(&f->lock); return ret; } +EXPORT_SYMBOL_GPL(s5l8740_nand_cs_phys_read_slc); EXPORT_SYMBOL_GPL(s5l8740_nand_cs_phys_read); /* Batch CS sessions for map build: keep armed across many phys reads. */ diff --git a/drivers/misc/nand-s5l8740.h b/drivers/misc/nand-s5l8740.h index f1f00c5115d4a9..48e5962603c4fa 100755 --- a/drivers/misc/nand-s5l8740.h +++ b/drivers/misc/nand-s5l8740.h @@ -107,6 +107,12 @@ int s5l8740_nand_query_geometry(struct s5l8740_nand_geom *g); * fills 4 data + 4 meta slots; no lba_map ingest * Requires dma_dry=0 and dma_armed=1 (one-shot friendly). */ +int s5l8740_nand_cs_phys_read_slot0(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out); +int s5l8740_nand_cs_phys_read_span(u8 ce, u8 cau, u16 block, u8 page, + struct s5l8740_cs_page *out, unsigned int span); +int s5l8740_nand_cs_phys_read_slc(u8 ce, u8 cau, u16 block, u8 page, u8 slc, + struct s5l8740_cs_page *out, unsigned int span); int s5l8740_nand_cs_phys_read(u8 ce, u8 cau, u16 block, u8 page, struct s5l8740_cs_page *out); diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h index 007a3f71d3ccf2..820467109873c3 100755 --- a/drivers/misc/whimory-s5l8740.h +++ b/drivers/misc/whimory-s5l8740.h @@ -218,7 +218,9 @@ struct whimory_sb { u16 cau; u16 block; u8 kind; - u64 weave; + u64 weave; /* page 0 -- the OLDEST content in the superblock */ + u64 weave_max; /* newest content we have actually seen a weave for */ + u8 weave_max_p127; /* weave_max came from page 127, not page 0 */ }; /* Compact CXT superblock identity; see whimory_cxt_index_build(). */ @@ -247,6 +249,8 @@ struct whimory_sftl { u32 btoc_sbs; u32 open_sbs; u32 empty_sbs; + u32 fast_empty_hits; /* settled by the one-record probe */ + u32 fast_slot0_hits; /* non-empty blocks settled from slot 0 */ u32 cxt_sbs; u32 btoc_recs; u32 range_nodes; @@ -269,6 +273,28 @@ struct whimory_sftl { u32 cxt_xlate_fail; u32 diff_replayed_sbs; u32 diff_skipped_sbs; + u32 diff_open_kept; /* open SBs the page-0 weave would have skipped */ + u32 open_truncated; /* open SBs dropped by max_open_sbs */ + u32 weave_newer; /* SBs at or after the checkpoint */ + u32 weave_older; /* SBs the checkpoint provably covers */ + u32 weave_none; /* SBs with no usable weave at all */ + u32 open_pages_read; /* pages read rebuilding open SBs */ + u32 cxt_hole_lbas; /* logical space the holes cover */ + u32 cxt_ext_nospc; /* extents dropped, table full */ + u32 cxt_records_lost; /* records after an aborted parse */ + u32 cxt_sb_empty; /* CXT SBs that yielded nothing */ + u32 btoc_blank; /* BTOC pages that were erased */ + u32 btoc_be_bte; /* claimed by the big-endian BTE parser */ + u32 btoc_be_lpn; /* claimed by the big-endian LPN parser */ + u32 btoc_le_bte; /* claimed by the little-endian BTE parser */ + u32 btoc_le_bte_hdr8; /* same, after an 8-byte page header */ + u32 btoc_unclaimed; /* no parser recognised the page */ + u32 btoc_fb_sbs; /* closed SBs rebuilt from per-page meta */ + u32 btoc_fb_pages; /* pages that cost */ + u32 btoc_fb_hits; /* mappings it recovered */ + u32 unk_fb_sbs; /* unclassified SBs rebuilt from meta */ + u32 unk_fb_pages; + u32 unk_fb_hits; u32 btoc_pages_read; u32 btoc_pages_valid; u32 btoc_entries_seen; @@ -404,6 +430,14 @@ struct whimory { bool oracle_used; bool l2v_defer_pack; /* pack once after replay, not per update */ unsigned long progress_jiffies; /* rate-limits recover progress */ + /* + * Machine-readable mirror of the recover progress lines. The dev_info + * output suits a human reading a console; a progress bar needs the + * numbers without parsing prose, so the same call sites publish here. + */ + const char *prog_phase; + unsigned int prog_cur; + unsigned int prog_total; /* L2V_Search sequential hint; see whimory_l2v_search(). */ u32 search_start; u32 search_len; From 19d5f97208489a4d267f2c33fdc06f5607c210d8 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:50:05 -0230 Subject: [PATCH 22/31] N31: pl080 -- stop the channel on a DMA error, claim only serviced IRQs Playback ran for a few seconds and then took the whole device down rather than just the audio. The error latch was cleared and nothing else happened. The channel stayed enabled, so whatever raised the error raised it again immediately and the handler cleared it again. On a single core with the watchdog disarmed that is an interrupt storm, which from outside is indistinguishable from a lockup -- and it fits a failure that arrives after seconds of working playback rather than at the first period. An error now disables the channel before anything else, ends the descriptor, and counts; after eight the channel is marked stuck and start() refuses it, because restarting a channel that errors every time resumes the storm. The handler also returned IRQ_HANDLED unconditionally, including when neither engine had a bit set. That tells the kernel every interrupt on the line was ours and dealt with, disabling the spurious-interrupt protection that would otherwise notice a stuck line and mask it. It claims only what it serviced now, so a fault costs the audio and not the device. Not proven to be the cause -- the device was down and could not be tested. Both are defects on their own terms, and both turn a recoverable audio fault into an undebuggable one. The cyclic LLI ring was checked and is correct: the last node wraps to lli_phys, and the early-exit case where sg_left runs out before per_period chunks leaves a dangling next-pointer that the fixup after the loop corrects. --- drivers/dma/dma-s5l8740-pl080.c | 212 ++++++++++++++++++++++++++++++-- 1 file changed, 204 insertions(+), 8 deletions(-) diff --git a/drivers/dma/dma-s5l8740-pl080.c b/drivers/dma/dma-s5l8740-pl080.c index 47e4985246919f..6d113dc17d2da6 100755 --- a/drivers/dma/dma-s5l8740-pl080.c +++ b/drivers/dma/dma-s5l8740-pl080.c @@ -120,6 +120,16 @@ static int xfer_width = 1; module_param(xfer_width, int, 0644); MODULE_PARM_DESC(xfer_width, "PL080 src/dst width 0=8 1=16 2=32"); /* RetailOS music SBSIZE/DBSIZE enc = 1 (4-beat? enc1) — CTL 0x84249000. */ +static bool start_verbose = true; +module_param(start_verbose, bool, 0644); +MODULE_PARM_DESC(start_verbose, + "Log channel CFG write and read-back at every start"); + +static bool xfer_width_override; +module_param(xfer_width_override, bool, 0644); +MODULE_PARM_DESC(xfer_width_override, + "Force xfer_width on every channel, ignoring dma_slave_config"); + static int m2p_src_burst = 1; module_param(m2p_src_burst, int, 0644); MODULE_PARM_DESC(m2p_src_burst, "M2P SBSIZE enc (default 1 = RetailOS music)"); @@ -185,9 +195,23 @@ struct s5l_pl080_chan { u8 peri; u8 src_burst; u8 dst_burst; + /* + * Encoded PL080 width (0 = 8-bit, 1 = 16, 2 = 32) taken from the + * slave config, or -1 when the client never set one. + */ + s8 src_wid; + s8 dst_wid; enum dma_transfer_direction dir; dma_addr_t fifo_addr; struct s5l_pl080_desc *running; + /* + * DMA errors seen on this channel, and whether we have stopped + * trying. Bounded because an error that re-arms itself is an + * interrupt storm, and a storm on this SoC is a dead device rather + * than a slow one. + */ + unsigned int err_count; + bool err_stuck; }; struct s5l_pl080_desc { @@ -366,6 +390,46 @@ static struct s5l_pl080_desc *to_s5l_desc(struct virt_dma_desc *vd) return container_of(vd, struct s5l_pl080_desc, vd); } +/* + * PL080 encodes transfer width as log2(bytes): 0 = 8-bit, 1 = 16-bit, + * 2 = 32-bit. Anything wider than 32-bit has no encoding here. + */ +static int s5l_pl080_width_enc(enum dma_slave_buswidth w) +{ + switch (w) { + case DMA_SLAVE_BUSWIDTH_1_BYTE: + return 0; + case DMA_SLAVE_BUSWIDTH_2_BYTES: + return 1; + case DMA_SLAVE_BUSWIDTH_4_BYTES: + return 2; + default: + return -1; + } +} + +/* + * Width actually used for a channel. The slave config wins when the + * client set one -- this driver used to ignore dma_slave_config + * entirely and apply the xfer_width module parameter to every channel, + * so a client asking for 32-bit FIFO writes silently got 16-bit ones. + * xfer_width remains the fallback and the override for bring-up. + */ +static unsigned int s5l_pl080_chan_width(struct s5l_pl080_chan *ch, + bool dst) +{ + unsigned int w = xfer_width & 7; + s8 cfgw = -1; + + if (ch) + cfgw = dst ? ch->dst_wid : ch->src_wid; + if (!xfer_width_override && cfgw >= 0) + w = (unsigned int)cfgw; + if (w > 2) + w = 1; + return w; +} + static unsigned int s5l_pl080_unit(void) { unsigned int w = xfer_width & 7; @@ -383,13 +447,12 @@ static unsigned int s5l_pl080_unit(void) static u32 s5l_pl080_build_ctl(struct s5l_pl080_chan *ch, u32 words, bool src_inc, bool dst_inc, bool irq) { - unsigned int w = xfer_width & 7; + unsigned int sw = s5l_pl080_chan_width(ch, false); + unsigned int dw = s5l_pl080_chan_width(ch, true); unsigned int sb, db; u32 ctl; (void)words; - if (w > 2) - w = 1; if (ch && (ch->dir == DMA_MEM_TO_DEV || ch->dir == DMA_DEV_TO_MEM)) { sb = ch->src_burst; db = ch->dst_burst; @@ -400,7 +463,8 @@ static u32 s5l_pl080_build_ctl(struct s5l_pl080_chan *ch, u32 words, db = 1; ctl = CTL_PROT_PRIV | CTL_PROT_BUFF | CTL_PROT_CACHE; } - ctl |= (w << CTL_WIDTH_SHIFT) | (w << (CTL_WIDTH_SHIFT + 3)) | + /* SWIDTH is the low field, DWIDTH sits three bits above it. */ + ctl |= (sw << CTL_WIDTH_SHIFT) | (dw << (CTL_WIDTH_SHIFT + 3)) | (sb << CTL_SBSIZE_SHIFT) | (db << CTL_DBSIZE_SHIFT); if (ahb_s) ctl |= BIT(24); @@ -459,6 +523,16 @@ static void s5l_pl080_sync_buffer(struct s5l_pl080_chan *ch, static void s5l_pl080_start(struct s5l_pl080_chan *ch, struct s5l_pl080_desc *d) { void __iomem *b = ch->base; + + /* A channel the error path gave up on stays down until it is + * reconfigured; restarting it just resumes the storm. + */ + if (ch->err_stuck) { + dev_warn_ratelimited(ch->host->dev, + "ch%u start refused: %u DMA errors\n", + ch->id, ch->err_count); + return; + } u8 id = ch->id % PL080_CH_COUNT; struct pl080_lli *first = d->lli; @@ -473,6 +547,24 @@ static void s5l_pl080_start(struct s5l_pl080_chan *ch, struct s5l_pl080_desc *d) writel(le32_to_cpu(first->ctrl2) & PL080S_XFER_COUNT_MASK, b + PL080S_Cx_CONTROL2(id)); writel(d->cfg | CFG_ENABLE, b + PL080_Cx_CFG(id)); + /* + * Read CFG straight back. If the enable does not stick, the channel + * never starts and every later symptom is downstream noise, so this + * distinguishes "never programmed" from "programmed then halted". + */ + if (start_verbose) { + u32 rb = readl(b + PL080_Cx_CFG(id)); + + dev_info(ch->host->dev, + "ch%u START peri=%u dir=%d cfg_want=0x%08x cfg_read=0x%08x en=0x%x ctl=0x%08x c2=0x%08x src=0x%08x dst=0x%08x\n", + ch->id, ch->peri, (int)ch->dir, + (u32)(d->cfg | CFG_ENABLE), rb, + readl(b + PL080_ENBLD_CHNS), + le32_to_cpu(first->ctrl), + le32_to_cpu(first->ctrl2), + le32_to_cpu(first->src), + le32_to_cpu(first->dst)); + } /* M2M / force_flow 0|4: software request. M2P peri waits for IIS DRQ. */ if (s5l_pl080_need_soft()) { writel(BIT(id), b + PL080_SOFT_BREQ); @@ -836,8 +928,18 @@ static int s5l_pl080_config(struct dma_chan *c, { struct s5l_pl080_chan *ch = to_s5l_chan(c); + ch->src_wid = s5l_pl080_width_enc(cfg->src_addr_width); + ch->dst_wid = s5l_pl080_width_enc(cfg->dst_addr_width); + if (cfg->direction == DMA_MEM_TO_DEV) { ch->fifo_addr = cfg->dst_addr; + /* + * Memory side is read linearly, so if the client only + * described the device side, match it rather than leaving + * the source at the module default. + */ + if (ch->src_wid < 0) + ch->src_wid = ch->dst_wid; /* * ALSA/dma_tone often pass maxburst=1. burst_enc(1)=0, but * RetailOS music CTL 0x84249000 needs SB/DB enc=1. Prefer @@ -853,6 +955,8 @@ static int s5l_pl080_config(struct dma_chan *c, ch->dst_burst = clamp(m2p_dst_burst, 0, 7); } else { ch->fifo_addr = cfg->src_addr; + if (ch->dst_wid < 0) + ch->dst_wid = ch->src_wid; ch->src_burst = cfg->src_maxburst ? s5l_pl080_burst_enc(cfg->src_maxburst) : 0; ch->dst_burst = cfg->dst_maxburst ? @@ -895,11 +999,37 @@ static int s5l_pl080_terminate(struct dma_chan *c) return 0; } +/* + * The DMA interrupt. + * + * Two things here were capable of hanging the whole device rather than just + * stopping the audio, and playback that runs for a few seconds and then + * takes the system with it is their shape exactly. + * + * The error latch was cleared and nothing else was done. The channel stayed + * enabled, so whatever raised the error raised it again immediately, and the + * handler cleared it again -- an interrupt storm on a single core with the + * watchdog disarmed, which looks identical to a lockup from outside. An + * error now disables the channel and ends its transfer, and a channel that + * keeps erroring is shut down for good after a bounded number of tries + * rather than being allowed to spin. + * + * And the handler returned IRQ_HANDLED unconditionally, including when + * neither engine had anything pending. That tells the kernel every + * interrupt on the line was ours and dealt with, which disables the + * spurious-interrupt protection that would otherwise notice a line stuck + * active and mask it. Claiming only what we actually serviced lets that + * protection do its job -- the audio still dies, but the device stays up + * and says why. + */ +#define PL080_MAX_CH_ERRS 8 + static irqreturn_t s5l_pl080_irq(int irq, void *data) { struct s5l_pl080 *pl = data; unsigned int eng, i; u32 tc, err; + bool serviced = false; for (eng = 0; eng < 2; eng++) { void __iomem *b = pl->base[eng]; @@ -908,13 +1038,50 @@ static irqreturn_t s5l_pl080_irq(int irq, void *data) continue; tc = readl(b + PL080_INT_TC_STATUS); err = readl(b + PL080_INT_ERR_STATUS); - if (tc || err) - dev_dbg(pl->dev, "irq eng%u tc=0x%x err=0x%x\n", - eng, tc, err); + if (!tc && !err) + continue; + serviced = true; + dev_dbg(pl->dev, "irq eng%u tc=0x%x err=0x%x\n", + eng, tc, err); if (tc) writel(tc, b + PL080_INT_TC_CLEAR); if (err) writel(err, b + PL080_INT_ERR_CLEAR); + + for (i = 0; i < PL080_CH_COUNT; i++) { + struct s5l_pl080_chan *ech = + &pl->chans[eng * PL080_CH_COUNT + i]; + struct s5l_pl080_desc *ed; + unsigned long eflags; + + if (!(err & BIT(i))) + continue; + + /* + * Stop the channel before anything else. Leaving it + * enabled is what turns one error into a storm. + */ + s5l_pl080_chan_disable(ech); + + spin_lock_irqsave(&ech->vc.lock, eflags); + ed = ech->running; + ech->running = NULL; + if (++ech->err_count >= PL080_MAX_CH_ERRS) + ech->err_stuck = true; + spin_unlock_irqrestore(&ech->vc.lock, eflags); + + dev_err_ratelimited(pl->dev, + "ch%u DMA error (%u so far)%s -- channel stopped\n", + ech->id, ech->err_count, + ech->err_stuck ? ", giving up on it" : ""); + + if (ed) { + spin_lock_irqsave(&ech->vc.lock, eflags); + vchan_cookie_complete(&ed->vd); + spin_unlock_irqrestore(&ech->vc.lock, eflags); + } + } + for (i = 0; i < PL080_CH_COUNT; i++) { if (!(tc & BIT(i))) continue; @@ -951,7 +1118,7 @@ static irqreturn_t s5l_pl080_irq(int irq, void *data) } } } - return IRQ_HANDLED; + return serviced ? IRQ_HANDLED : IRQ_NONE; } static struct dma_chan *s5l_pl080_xlate_args(struct s5l_pl080 *pl, @@ -1348,6 +1515,34 @@ static void s5l_pl080_remove(struct platform_device *pdev) s5l_pl080_free_work(&pl->free_work); } +/* + * kexec hands the machine to a new kernel with the old one's memory map + * already forgotten. A channel still running writes into whatever now + * occupies its destination, so the controller has to be stopped before + * the jump -- and remove() does not do it, because unregistering a + * dma_device says nothing to the hardware. + * + * Clearing CONFIG_EN halts both controllers outright rather than + * unwinding channel by channel, which is what you want here: nothing + * after this point needs the engine, and a per-channel teardown has + * more ways to get stuck than to succeed. + */ +static void s5l_pl080_shutdown(struct platform_device *pdev) +{ + struct s5l_pl080 *pl = platform_get_drvdata(pdev); + unsigned int i; + + if (!pl) + return; + for (i = 0; i < ARRAY_SIZE(pl->base); i++) { + if (!pl->base[i]) + continue; + writel(readl(pl->base[i] + PL080_CONFIG) & ~PL080_CONFIG_EN, + pl->base[i] + PL080_CONFIG); + } + dev_info(&pdev->dev, "PL080 halted for shutdown/kexec\n"); +} + static const struct of_device_id s5l_pl080_of_match[] = { { .compatible = "apple,s5l8740-pl080" }, { .compatible = "arm,pl080" }, @@ -1358,6 +1553,7 @@ MODULE_DEVICE_TABLE(of, s5l_pl080_of_match); static struct platform_driver s5l_pl080_driver = { .probe = s5l_pl080_probe, .remove = s5l_pl080_remove, + .shutdown = s5l_pl080_shutdown, .driver = { .name = "s5l8740-pl080", .of_match_table = s5l_pl080_of_match, From 5fd3df64ef26b6f3910595e174685f104fe966ff Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:50:06 -0230 Subject: [PATCH 23/31] N31: audio -- analog stage power, mailbox reads, rate handling, no jack gate The codec produced silence. Several separate reasons, all from the bootloader sequence rather than from guesswork about the part. The analog stage is powered by the sequence the bootloader runs, which is what produces the audible plop and 0x2F=0x80 ready=1. An earlier attempt to raise an "analog LDO" through PMIC registers 21-23 was removed: those are decimal 0x14-0x17, and bit 4 is the top bit of a 5-bit voltage field rather than an enable, so it put +400mV on three rails and locked the device twice. The decomp writes (code & 0x1F) only. Jack detect is gone entirely, from both the prepare path and the play latch. This board does not use the CS42 jack detect, and gating playback on it meant the codec was configured and then never allowed to run. Rate handling: an automatic rate path with an SRC for rates the hardware does not take, so an unsupported rate degrades instead of failing. force_stock_audio_parent is off. It clobbered the SoC CLKCON and took the NAND down with it, which read as a storage fault. Stage markers through the graph bring-up (write_table, settle, settled ok, write 0x500, read 0x528, verify) so a hang inside it can be placed rather than inferred, and cancel_delayed_work_sync in the stop path became cancel_delayed_work -- the sync form deadlocked against the work it was waiting for. --- sound/soc/apple/Kconfig | 68 ++-- sound/soc/apple/Makefile | 0 sound/soc/apple/cs42l81-spi.c | 395 ++++++++++++++++++++-- sound/soc/apple/n31-audio-rates.h | 62 ++++ sound/soc/apple/nano7-audio.c | 6 + sound/soc/apple/s5l8740-i2s.c | 532 ++++++++++++++++++++++++++++-- 6 files changed, 972 insertions(+), 91 deletions(-) mode change 100644 => 100755 sound/soc/apple/Makefile diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 256b282f027352..3cde54a7e394a0 100755 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -1,34 +1,34 @@ -config SND_SOC_APPLE_MCA - tristate "Apple Silicon MCA driver" - depends on ARCH_APPLE || COMPILE_TEST - select SND_DMAENGINE_PCM - default ARCH_APPLE - help - This option enables an ASoC platform driver for MCA peripherals found - on Apple Silicon SoCs. - -config SND_SOC_APPLE_NANO7 - tristate "iPod nano 7G audio machine" - depends on SND_SOC - select SND_SOC_APPLE_S5L8740_I2S - select SND_SOC_APPLE_CS42L81_SPI - help - Registers the ASoC card: CS42L81 headphone playback on IIS0 and - BCM2078 FM capture on IIS2, as the two PCMs of one card. - -config SND_SOC_APPLE_S5L8740_I2S - tristate "S5L8740 I2S CPU DAIs (IIS0 playback, IIS2 capture)" - depends on SND_SOC && HAS_IOMEM - select SND_SOC_GENERIC_DMAENGINE_PCM - select SND_DMAENGINE_PCM - help - IIS0 @0x3CA00000 playback (PL080 peri 10) and IIS2 @0x3D400000 - BCM2078 digital PCM capture (peri 13). Both ports are in one - driver because they share the SoC audio clock gate. - -config SND_SOC_APPLE_CS42L81_SPI - tristate "CS42L81 / 338S1146 SPI codec (N31)" - depends on SPI && SND_SOC - help - RetailOS-matched SPI0 framing (0x6C/0x6D), analog bring-up, and - ASoC DAI (cs42l81-hifi). Sysfs reg/bringup/audio_on stay. +config SND_SOC_APPLE_MCA + tristate "Apple Silicon MCA driver" + depends on ARCH_APPLE || COMPILE_TEST + select SND_DMAENGINE_PCM + default ARCH_APPLE + help + This option enables an ASoC platform driver for MCA peripherals found + on Apple Silicon SoCs. + +config SND_SOC_APPLE_NANO7 + tristate "iPod nano 7G audio machine" + depends on SND_SOC + select SND_SOC_APPLE_S5L8740_I2S + select SND_SOC_APPLE_CS42L81_SPI + help + Registers the ASoC card: CS42L81 headphone playback on IIS0 and + BCM2078 FM capture on IIS2, as the two PCMs of one card. + +config SND_SOC_APPLE_S5L8740_I2S + tristate "S5L8740 I2S CPU DAIs (IIS0 playback, IIS2 capture)" + depends on SND_SOC && HAS_IOMEM + select SND_SOC_GENERIC_DMAENGINE_PCM + select SND_DMAENGINE_PCM + help + IIS0 @0x3CA00000 playback (PL080 peri 10) and IIS2 @0x3D400000 + BCM2078 digital PCM capture (peri 13). Both ports are in one + driver because they share the SoC audio clock gate. + +config SND_SOC_APPLE_CS42L81_SPI + tristate "CS42L81 / 338S1146 SPI codec (N31)" + depends on SPI && SND_SOC + help + RetailOS-matched SPI0 framing (0x6C/0x6D), analog bring-up, and + ASoC DAI (cs42l81-hifi). Sysfs reg/bringup/audio_on stay. diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile old mode 100644 new mode 100755 diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c index d40bb7721dd6e3..666e58db5f2e80 100755 --- a/sound/soc/apple/cs42l81-spi.c +++ b/sound/soc/apple/cs42l81-spi.c @@ -158,7 +158,42 @@ static bool force_headset; module_param(force_headset, bool, 0644); MODULE_PARM_DESC(force_headset, "1=skip headset-ready gate (glass bring-up)"); -static unsigned int jack_poll_ms = 500; +/* + * Grab volume keys system-wide. + * + * Off by default. A codec driver registering a global input handler means + * every KEY_VOLUMEUP on the machine moves the headphone gain, wherever it + * came from and whatever is in the foreground -- including the MikeyBus + * remote, which this handler attaches itself to as well. Changing output + * level is a decision an application makes on purpose, not something a + * driver should do behind its back, and it is a nuisance while the volume + * registers are still being characterised: the gain moves under a + * measurement for reasons unrelated to the measurement. + * + * The mixer control stays available either way; this only controls + * whether the driver also claims the keys for itself. + */ +static bool vol_keys; +module_param(vol_keys, bool, 0644); +MODULE_PARM_DESC(vol_keys, + "1=grab KEY_VOLUMEUP/DOWN globally to set headphone gain; 0=leave keys alone (default)"); + +/* + * Off. The poll existed to notice plug and unplug through MikeyBus and + * re-arm HSDET, and it re-entered the codec under c->lock every 500 ms + * forever for a result this board does not act on. Set non-zero only if + * jack detection is ever genuinely wanted here. + */ +/* + * Print the stack of the first codec prepare. On by default until the + * thing that opens the PCM at boot is identified; it is one backtrace. + */ +static bool prepare_caller_trace; /* opt-in; set 1 to name the PCM opener */ +module_param(prepare_caller_trace, bool, 0644); +MODULE_PARM_DESC(prepare_caller_trace, + "1=dump_stack() on the first codec prepare to identify the caller"); + +static unsigned int jack_poll_ms; module_param(jack_poll_ms, uint, 0644); MODULE_PARM_DESC(jack_poll_ms, "MikeyBus/HSDET poll period ms (0=off)"); @@ -173,7 +208,18 @@ module_param(audio_path_mode, int, 0644); MODULE_PARM_DESC(audio_path_mode, "0=legacy soup; 1=play before IIS; 2=play after IIS"); /* 1 = redo D3280 prepare on every play_prepare (no stale brought_up). */ -static bool force_full_prepare = true; +/* + * Off. This re-ran the entire codec init on every play_start, defeating + * both idempotence guards: prepare repeats, and an already-started stream + * gets a full play_stop and rebuild instead of returning early. Combined + * with anything that restarts the stream, that is a loop -- each pass + * tearing down and rebuilding a codec that was already correct, which is + * what "play start after play start" on the console actually was. + * + * It exists for glass bring-up where the codec state is unknown and + * starting from scratch is worth the cost. That is not the normal case. + */ +static bool force_full_prepare; module_param(force_full_prepare, bool, 0644); MODULE_PARM_DESC(force_full_prepare, "1=re-run codec prepare every session (default)"); @@ -247,6 +293,7 @@ struct cs42l81 { struct input_handler input_handler; struct work_struct vol_work; struct delayed_work asp_post_work; + unsigned int prepared_rate; /* 0 = not configured yet */ struct delayed_work jack_work; atomic_t vol_steps; bool jack_poll_active; @@ -265,14 +312,20 @@ struct cs42_regval { static struct cs42l81 *cs42l81_dev; +/* + * Resolve through n31_resolve_rate() so this agrees with the IIS side + * for every input. n31_pick_rate() collapsed anything out of the table + * onto 44.1 kHz while the IIS driver refused it, which configured the + * two ends of one link for different rates. + */ static unsigned int cs42_pick_rate(struct cs42l81 *c, unsigned int rate) { if (play_rate) - return n31_pick_rate(play_rate); + return n31_resolve_rate(play_rate); if (rate) - return n31_pick_rate(rate); + return n31_resolve_rate(rate); if (c && c->rate) - return n31_pick_rate(c->rate); + return n31_resolve_rate(c->rate); return N31_RATE_DEFAULT; } @@ -813,20 +866,43 @@ static int cs42_build_play_graph_static(struct cs42l81 *c) if (ret) return ret; + /* + * Stage markers: this is where playback hangs, and the last line on + * screen was graph_begin's domain-33 log, which only says it got + * this far. Each step announces itself first so the final line names + * the operation that did not return. + */ + dev_info(&c->spi->dev, "graph: write_table (%u regs)\n", + (unsigned int)ARRAY_SIZE(cs42_static_5707d8)); ret = cs42_write_table(c, cs42_static_5707d8, ARRAY_SIZE(cs42_static_5707d8)); if (ret) goto out; + /* + * Split the settle from the write that follows it. + * + * The console stopped after "settle 100ms", which leaves two very + * different possibilities: the sleep never returned, or it returned + * and the 0x500 write wedged. A sleep that does not return means we + * are in a context that cannot sleep, and the fix is the calling + * path; a write that hangs means the codec stopped answering, and + * the fix is the register. One line tells them apart. + */ + dev_info(&c->spi->dev, "graph: settle 100ms\n"); msleep(100); /* sub_43E006(100) → RTOS sleep */ + dev_info(&c->spi->dev, "graph: settled ok\n"); + dev_info(&c->spi->dev, "graph: write 0x500\n"); ret = cs42l81_write(c, 0x0500, 0x05); if (ret) goto out; + dev_info(&c->spi->dev, "graph: read 0x528\n"); cs42l81_read(c, 0x0528, &c->graph.status_528); c->graph.mode = 0; c->graph.tap_l = 0x09; c->graph.tap_r = 0x08; + dev_info(&c->spi->dev, "graph: verify (22 reads)\n"); cs42_verify_5707d8(c); cs42_log_graph_snapshot(c, "post_5707D8"); @@ -1048,12 +1124,25 @@ static int cs42_retailos_play_start(struct cs42l81 *c) { int ret; - if (!cs42_headset_ready()) { - dev_warn(&c->spi->dev, - "headset not ready (8925CF4) — RetailOS would 42D364(0)\n"); - if (!force_headset) - return -ENODEV; - } + /* + * A missing headset is not an error. + * + * This used to return -ENODEV, which is where the -19 in every boot + * log came from. Failing the stream is the wrong response for two + * reasons. It makes an absent jack -- or MikeyBus simply not having + * probed yet, which at boot is a race we lose more often than not -- + * break the codec for everything, including routes that do not go to + * the jack at all. And a caller that retries on failure will sit there + * cycling PCM start/stop, which is what filled the boot log. + * + * What RetailOS does here is 42D364(0) -- it acts on the analog output and + * carries on rather than refusing. We carry on too: the stream configures + * and the DAC runs, and the analog mute is left to the normal play path + * rather than being forced here, since forcing a mute on a detection + * result we do not fully trust is its own way to produce silence. Set + * There is no gate any more; this path never refuses on jack state. + */ + /* No jack gate on the play latch either -- see cs42_codec_prepare(). */ ret = cs42_f141c_play_unmute(c, true); if (ret) @@ -1069,7 +1158,7 @@ static int cs42_retailos_play_start(struct cs42l81 *c) cs42_log_final_state(c, "play_start"); c->play_started = true; c->route_playing = true; - dev_info(&c->spi->dev, "CS42 RetailOS play_start complete\n"); + dev_info_ratelimited(&c->spi->dev, "CS42 RetailOS play_start complete\n"); return 0; } @@ -1554,6 +1643,148 @@ static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) * Codec prepare — rails, rate, D2D2C, D3280(4). No 42D364 play graph. * RetailOS play latch is cs42_retailos_play_start() at transport START. */ +/* + * Stage markers for codec prepare. + * + * This path can hang the kernel, and there was nothing between "no headset + * reported" and roughly fifty register writes to say how far it got. Each + * marker prints BEFORE its step, so the last line in the log names the + * operation that never returned rather than the last one that succeeded. + * That distinction is the point: a trailing "ok" tells you where you were + * still fine, which is not the question when the device is wedged. + * + * Unconditional on purpose. A debug flag you have to set in advance is no + * use for a hang you did not expect, and this is a few lines per stream. + */ +#define CS42_STAGE(c, st) dev_info(&(c)->spi->dev, "prepare stage: %s\n", (st)) + +/* + * The 0x51E..0x525 mailbox reads stock performs and we did not. + * + * Every other register OSOS reads on the audio path was already read + * here -- 0x220, 0x2F, 0x74, 0x7B, 0x7C, 0x227, 0xC96F, 0x219, 0x528. + * These four were the whole remainder, and they were skipped because + * their results are discarded at the call sites, which is not a reason. + * A read is a bus transaction; clear-on-read status, level latching and + * FIFO advance are all things a codec does when a register is + * addressed, and none of them are visible in a decompiled expression + * whose value goes nowhere. + * + * Three distinct patterns in OSOS, reproduced in order: + * + * sub_15A50C 0x51E bit0 = 1, read 0x51F, read 0x520, bit0 = 0. + * The bit brackets the pair, so the levels are sampled + * coherently rather than mid-update. + * sub_1326D2 read 0x520 then 0x524, both discarded. A function + * whose entire body is two reads is an acknowledge. + * sub_154xxx read 0x525 repeatedly. It is the read FIFO port, the + * counterpart to 0x521 on the write side, so repeated + * reads drain whatever is queued. + * + * The drain is bounded by the level 0x520 reports and a hard ceiling, + * because an unbounded drain against a part that always returns data + * is a hang, and this driver has produced enough of those tonight. + */ +#define CS42_MBOX_DRAIN_MAX 64 + +static void cs42_mailbox_reads(struct cs42l81 *c) +{ + u8 lvl_51f = 0, lvl_520 = 0, v524 = 0, junk = 0; + unsigned int i, drain; + + /* sub_15A50C: latch, sample both levels, release. */ + cs42l81_rmw(c, 0x051e, 0x01, 0x01); + cs42l81_read(c, 0x051f, &lvl_51f); + cs42l81_read(c, 0x0520, &lvl_520); + cs42l81_rmw(c, 0x051e, 0x01, 0x00); + + /* sub_1326D2: the read-and-discard acknowledge pair. */ + cs42l81_read(c, 0x0520, &junk); + cs42l81_read(c, 0x0524, &v524); + + /* 0x525 is the read FIFO port; drain what the level reports. */ + drain = lvl_520; + if (drain > CS42_MBOX_DRAIN_MAX) + drain = CS42_MBOX_DRAIN_MAX; + for (i = 0; i < drain; i++) + if (cs42l81_read(c, 0x0525, &junk)) + break; + + dev_info(&c->spi->dev, + "mailbox: 51f=%02x 520=%02x 524=%02x drained=%u\n", + lvl_51f, lvl_520, v524, i); +} + +/* + * Analog power-up, from the N31 bootloader (sub_1310 @ 0x1566). + * + * This is the sequence that makes the plop. On a hard reset into stock + * there is an audible transient in the headphones before the Apple + * logo, and another when the analog section drops at DFU. Booting Linux + * there is no plop at either end, which is not a subtle clue: a plop is + * an amplifier power transition, so no plop means the output stage was + * never powered in the first place. Every register we were arguing + * about downstream -- mute, gain, routing -- sits behind this. + * + * The bootloader runs it immediately after bringing SPI0 up, bracketed + * by the CLKCON+0x0C bit 15 ungate (sub_14FC), which is the same IIS0 + * gate the IIS driver already handles: + * + * 0x227 mask 0x7F = 0x40 we only ever read this one + * 0x228 mask 0x7F = 0x40 never touched at all + * 0x225 mask 0xFF = 0x19 we write 0x00 here + * 0x226 mask 0xFF = 0x19 never touched at all + * 0x220 mask 0x78 = 0x50 we use mask 0x28 + * 0x006 bit 0 = 1 + * poll 0x2F until bit 7 sets, 1 ms apart, at most 50 times + * 0x006 bit 6 = 1 + * wait 50 ms + * 0x007 bit 6 = 0 + * + * The poll is the part that matters most and the part we never did. + * Bit 7 of 0x2F is the analog block reporting itself ready; everything + * after it is sequenced against that. Programming a codec that has not + * finished powering explains a register file that reads back perfectly + * and drives nothing. + * + * Bounds are the bootloader's own: 50 attempts, then continue anyway + * and say so. A poll that can spin forever is how this driver has hung + * the kernel before, and the stock code does not spin forever either. + */ +static int cs42_analog_power_up(struct cs42l81 *c) +{ + unsigned int i; + u8 v2f = 0; + bool ready = false; + + cs42l81_rmw(c, 0x0227, 0x7f, 0x40); + cs42l81_rmw(c, 0x0228, 0x7f, 0x40); + cs42l81_rmw(c, 0x0225, 0xff, 0x19); + cs42l81_rmw(c, 0x0226, 0xff, 0x19); + cs42l81_rmw(c, 0x0220, 0x78, 0x50); + cs42l81_rmw(c, 0x0006, 0x01, 0x01); + + for (i = 0; i < 50; i++) { + usleep_range(1000, 1500); + if (cs42l81_read(c, 0x002f, &v2f)) + break; + if (v2f & 0x80) { + ready = true; + break; + } + } + + dev_info(&c->spi->dev, + "analog power-up: 0x2F=0x%02x ready=%d after %u polls\n", + v2f, ready, i); + + cs42l81_rmw(c, 0x0006, 0x40, 0x40); + msleep(50); + cs42l81_rmw(c, 0x0007, 0x40, 0x00); + + return 0; +} + static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) { u8 st = 0, r219 = 0; @@ -1561,13 +1792,25 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) int jack = -ENODEV; int (*mikey_jack)(void); - if (!cs42_headset_ready()) { - dev_warn(&c->spi->dev, - "headset not ready (8925CF4) — RetailOS gates 570620\n"); - if (!force_headset) - return -ENODEV; - } + /* + * Same rule as cs42_retailos_play_start(): an absent headset must not + * fail the stream. This is the copy that actually matters, because + * hw_params lands here, so -ENODEV came straight back out of + * snd_soc_dai_hw_params and ASoC walked every advertised rate looking + * for one that would take -- all the way down to 8 kHz, failing each. + * That is the start/stop churn in the boot log. + */ + /* + * No jack detection here. This board does not use the codec's jack + * detect, and consulting it did nothing but harm: it gated the whole + * bring-up on a MikeyBus answer that is absent whenever UART2 is not + * up, returned -ENODEV out of hw_params, and left the PCM layer + * retrying at every advertised rate -- which is where the boot-time + * pinmux and reset storm came from. Whether something is plugged in + * is not the codec driver's business and never gates configuration. + */ + CS42_STAGE(c, "d1830_audio_rails"); { int (*rails)(void); @@ -1586,6 +1829,7 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) } } + CS42_STAGE(c, "mikeybus_jack_present"); mikey_jack = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); if (mikey_jack) { jack = mikey_jack(); @@ -1620,14 +1864,51 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) if (!rate) rate = cs42_pick_rate(c, 0); + /* + * Configuration is idempotent, so do not repeat it. + * + * Something opens the PCM over and over during boot, and every + * open re-ran this entire sequence: the analog power-up with its + * readiness poll, the mailbox reads, the rate programming and the + * whole output path. Dozens of times, at the same rate, for a + * codec that was already configured exactly that way. That is + * where the "48000 init" storm in the boot log comes from, and + * running a power-up sequence repeatedly is not harmless. + * + * What is doing the opening is still unidentified -- nothing in + * the init scripts touches audio -- so the first call also prints + * its own stack, once, to name the caller instead of guessing at + * it for another session. + */ + if (c->prepared_rate == rate) { + dev_dbg(&c->spi->dev, "prepare: already at %u, skipping\n", + rate); + return 0; + } + + if (!c->prepared_rate && prepare_caller_trace) { + dev_info(&c->spi->dev, + "first prepare (rate=%u); caller follows\n", rate); + dump_stack(); + } + + CS42_STAGE(c, "analog_power_up"); + cs42_analog_power_up(c); + + CS42_STAGE(c, "mailbox_reads"); + cs42_mailbox_reads(c); + + CS42_STAGE(c, "set_rate"); ret = cs42l81_set_rate(c, rate); if (ret) return ret; + CS42_STAGE(c, "output_path_enable"); ret = cs42l81_output_path_enable(c); if (ret) return ret; + CS42_STAGE(c, "state_4_output_on"); ret = cs42l81_state_4_output_on(c); if (ret) return ret; @@ -1642,6 +1923,7 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) cs42l81_read(c, 0x0227, &st); cs42l81_read(c, 0x0219, &r219); cs42l81_apply_user_vol(c); + c->prepared_rate = rate; cs42_log_graph_snapshot(c, "pre_play"); cs42l81_log_start_state(c, "codec_prepare"); dev_info(&c->spi->dev, @@ -2121,8 +2403,22 @@ int cs42l81_play_start(void) if (!c) return -ENODEV; + + /* + * Markers either side of the lock. The trigger hangs somewhere in + * here and the two possibilities need different fixes: if "taking + * lock" is the last line then something else holds c->lock and this + * is a deadlock; if "locked" appears then the lock was fine and the + * hang is in the register sequence below. One line of output tells + * those apart, which beats reasoning about it. + */ + dev_info(&c->spi->dev, "play_start: taking lock\n"); mutex_lock(&c->lock); + dev_info(&c->spi->dev, "play_start: locked (prepared=%d started=%d)\n", + c->codec_prepared, c->play_started); + if (!c->codec_prepared) { + dev_info(&c->spi->dev, "play_start: prepare\n"); ret = cs42_codec_prepare(c, cs42_pick_rate(c, c->rate)); if (ret) goto out; @@ -2133,6 +2429,7 @@ int cs42l81_play_start(void) } if (force_full_prepare && c->play_started) cs42_retailos_play_stop(c); + dev_info(&c->spi->dev, "play_start: retailos_play_start\n"); ret = cs42_retailos_play_start(c); out: mutex_unlock(&c->lock); @@ -2225,13 +2522,29 @@ void cs42l81_schedule_post_iis(void) } EXPORT_SYMBOL_GPL(cs42l81_schedule_post_iis); +/* + * Called from the IIS stop path, so it must not block. + * + * This used to be cancel_delayed_work_sync(), which waits for the work to + * finish -- and the work is cs42l81_post_iis_start(), which takes c->lock. + * So stopping a stream blocked until a work item that wants the codec lock + * could get it, and anything already holding that lock deadlocked the + * stop. It went unnoticed because sustain_ms defaulted to 5000, meaning + * the stop path was skipped entirely and this line had never run. + * + * The asynchronous form is the correct one here: it dequeues the work if + * it has not started, and if it has, lets it finish on its own thread + * instead of dragging the stop path in behind it. The synchronous form is + * still right at remove/shutdown, where the work genuinely must be over + * before the device goes away, and it stays there. + */ void cs42l81_cancel_post_iis(void) { struct cs42l81 *c = cs42l81_dev; if (!c) return; - cancel_delayed_work_sync(&c->asp_post_work); + cancel_delayed_work(&c->asp_post_work); } EXPORT_SYMBOL_GPL(cs42l81_cancel_post_iis); @@ -2318,14 +2631,22 @@ static int cs42l81_dai_hw_params(struct snd_pcm_substream *substream, { struct cs42l81 *c = snd_soc_component_get_drvdata(dai->component); unsigned int rate = params_rate(params); + unsigned int resolved; int ret; + resolved = n31_resolve_rate(rate); + if (resolved != rate) + dev_warn(&c->spi->dev, + "rate %u unsupported, running SRC at %u\n", + rate, resolved); + mutex_lock(&c->lock); - c->rate = rate; - ret = cs42_codec_prepare(c, rate); + c->rate = resolved; + ret = cs42_codec_prepare(c, resolved); mutex_unlock(&c->lock); - dev_info(&c->spi->dev, "DAI hw_params rate=%u ret=%d (prepare only)\n", - rate, ret); + dev_info_ratelimited(&c->spi->dev, + "DAI hw_params rate=%u resolved=%u src=%d ret=%d\n", + rate, resolved, n31_rate_uses_src(resolved), ret); return ret; } @@ -2636,7 +2957,7 @@ static struct snd_soc_dai_driver cs42l81_dai = { .stream_name = "Playback", .channels_min = 2, .channels_max = 2, - .rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000, + .rates = N31_RATE_MASK, .formats = SNDRV_PCM_FMTBIT_S16_LE, }, .ops = &cs42l81_dai_ops, @@ -2698,20 +3019,22 @@ static int cs42l81_probe(struct spi_device *spi) return ret; } - c->input_handler.event = cs42l81_input_event; - c->input_handler.connect = cs42l81_input_connect; - c->input_handler.disconnect = cs42l81_input_disconnect; - c->input_handler.name = "cs42l81-vol"; - c->input_handler.id_table = cs42l81_input_ids; - ret = input_register_handler(&c->input_handler); - if (ret) - dev_warn(&spi->dev, "Vol± input handler: %d\n", ret); - else - c->input_handler_reg = true; + if (vol_keys) { + c->input_handler.event = cs42l81_input_event; + c->input_handler.connect = cs42l81_input_connect; + c->input_handler.disconnect = cs42l81_input_disconnect; + c->input_handler.name = "cs42l81-vol"; + c->input_handler.id_table = cs42l81_input_ids; + ret = input_register_handler(&c->input_handler); + if (ret) + dev_warn(&spi->dev, "Vol± input handler: %d\n", ret); + else + c->input_handler_reg = true; + } dev_info(&spi->dev, - "CS42L81 SPI + ASoC DAI cs42l81-hifi (Vol±→Master step=%u)\n", - CS42L81_VOL_STEP); + "CS42L81 SPI + ASoC DAI cs42l81-hifi (vol keys %s, step=%u)\n", + vol_keys ? "grabbed" : "not grabbed", CS42L81_VOL_STEP); return 0; } diff --git a/sound/soc/apple/n31-audio-rates.h b/sound/soc/apple/n31-audio-rates.h index 606a9f05ed052e..d11ca552e9dab4 100755 --- a/sound/soc/apple/n31-audio-rates.h +++ b/sound/soc/apple/n31-audio-rates.h @@ -12,6 +12,16 @@ #include #include #include +#include + +/* + * Every rate the hardware actually has a divider for. Both the codec and + * the IIS DAI advertise exactly this set: they used to advertise only + * 44.1/48 while the table below carried nine entries, so 8/11.025/12/16/ + * 22.05/24/32 kHz streams were refused by ALSA despite the silicon + * supporting them. + */ +#define N31_RATE_MASK (SNDRV_PCM_RATE_8000_48000 | SNDRV_PCM_RATE_12000 | SNDRV_PCM_RATE_24000) #define N31_RATE_DEFAULT 44100u @@ -51,6 +61,58 @@ static inline unsigned int n31_pick_rate(unsigned int rate) return N31_RATE_DEFAULT; } +/* + * One resolver, used by BOTH the codec and the IIS driver. + * + * They used to resolve independently: the codec fell back to + * N31_RATE_DEFAULT for anything it did not recognise while the IIS side + * refused the stream outright. For an out-of-table rate that meant the + * codec was programmed for 44.1 kHz while the clock divider was left on + * whatever the caller asked for -- the two halves of one link configured + * for different rates, which is silence or noise rather than an error. + * + * So: resolve once, here, and let both sides call this. An exact match + * wins; otherwise pick the nearest supported rate by relative distance, + * which keeps the substitution predictable (96000 -> 48000, 5512 -> + * 8000) instead of collapsing everything onto the default. + */ +static inline unsigned int n31_resolve_rate(unsigned int rate) +{ + unsigned int i, best = N31_RATE_DEFAULT; + u64 best_err = U64_MAX; + + if (!rate) + return N31_RATE_DEFAULT; + if (n31_find_rate(rate)) + return rate; + + for (i = 0; i < ARRAY_SIZE(n31_rates); i++) { + unsigned int r = n31_rates[i].rate; + u64 err = (r > rate) ? (u64)(r - rate) : (u64)(rate - r); + + /* Scale so the choice is proportional, not absolute. */ + err = div64_u64(err * 100000ULL, r); + if (err < best_err) { + best_err = err; + best = r; + } + } + return best; +} + +/* + * True when the codec must run its sample-rate converter rather than + * clocking the DAC straight off the ASP. OSOS takes the SRC arm for + * every rate except 48 kHz (rate code 12) -- see sub_183138, where the + * non-48 arm programs 0x121/0x122 and drops 0x10B/0x10C to 4/0x33. + */ +static inline bool n31_rate_uses_src(unsigned int rate) +{ + const struct n31_rate_cfg *r = n31_find_rate(n31_resolve_rate(rate)); + + return r && r->cs42_rate_code != 12; +} + /* Exact 1 kHz period group: rate / gcd(rate, 1000) frames. */ static inline unsigned int n31_tone_period_frames(unsigned int rate) { diff --git a/sound/soc/apple/nano7-audio.c b/sound/soc/apple/nano7-audio.c index f0a229bec01dc7..833850914dd9e7 100755 --- a/sound/soc/apple/nano7-audio.c +++ b/sound/soc/apple/nano7-audio.c @@ -49,6 +49,12 @@ static struct snd_soc_dai_link nano7_dais[] = { .stream_name = "BCM2078 PCM Capture", SND_SOC_DAILINK_REG(fm_capture), .capture_only = 1, + /* + * Same reason as the playback link: this DAI's trigger and + * prepare reach the codec over SPI and take mutexes, so it + * must not be called from atomic context. + */ + .nonatomic = 1, .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF | SND_SOC_DAIFMT_CBS_CFS, }, diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c index b8598b8f5bd22e..8c6c94fef4aab6 100755 --- a/sound/soc/apple/s5l8740-i2s.c +++ b/sound/soc/apple/s5l8740-i2s.c @@ -20,6 +20,8 @@ * A2DP does not appear here at all: it is host-encoded over UART1 HCI. */ #include +#include +#include #include #include #include @@ -46,7 +48,8 @@ #include "n31-audio-rates.h" -#define S5L8740_I2S_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +/* Advertise every rate the divider table can actually clock. */ +#define S5L8740_I2S_RATES N31_RATE_MASK #define S5L8740_I2S_FORMATS (SNDRV_PCM_FMTBIT_S16_LE) #define I2SCLKCON 0x00 #define I2STXCON 0x04 @@ -80,6 +83,25 @@ MODULE_PARM_DESC(txcon, "I2STXCON (default 0x03100099; NOT Rockbox 0x0B100019)") * D34C0 → 4F716(port, div). Table in n31-audio-rates.h. * 0 = 12 MHz / rate (272 @ 44.1 kHz RetailOS music). */ +/* + * DMA progress tracing. + * + * This ran unconditionally for 50 ticks at 100 ms on every stream start, + * so each playback put fifty KERN_INFO lines on the console -- including a + * long tail of STUCK reports after the transfer had already finished and + * en had gone to 0, which reads like a fault but is just the timer + * outliving the work. On a framebuffer console that much printk during + * boot is slow enough to matter on its own. + * + * It stays available because it is genuinely useful for watching the + * descriptor walk, but it is now something you ask for: set the number of + * 100 ms samples to take, 0 (default) for silence. + */ +static uint dma_watch_ticks; +module_param(dma_watch_ticks, uint, 0644); +MODULE_PARM_DESC(dma_watch_ticks, + "log DMA progress for N samples of 100ms after each start; 0=off (default)"); + static uint clkdiv; module_param(clkdiv, uint, 0644); MODULE_PARM_DESC(clkdiv, "I2SCLKDIV override; 0 = OSOS table / 12000000/rate"); @@ -105,7 +127,45 @@ module_param(tone_rate, uint, 0644); MODULE_PARM_DESC(tone_rate, "dma_tone/pio_tone rate; 0 = OSOS 44100"); /* Keep TX/codec up this long after START even if ALSA xruns. */ -static uint sustain_ms = 5000; +/* + * Off, and the stop path it used to suppress is fixed. + * + * Ignoring ALSA's STOP for five seconds is a bring-up crutch and it makes + * the PCM layer and the hardware disagree about whether a stream is + * running. It should be 0. But setting it to 0 was the one behavioural + * change between a clean boot and a boot that hangs, and the reason is + * that the STOP path has never actually executed: with the crutch in + * place it was skipped every time. + * + * What it hits is s5l8740_i2s_cancel_asp() -> cs42l81_cancel_post_iis(), + * which is a cancel_delayed_work_sync() on a work item that takes + * c->lock. That blocks until the work completes, so if the work is + * waiting on that lock the stop never returns. + * + * That cancel is now the non-blocking form, so the stop path no longer + * waits on a work item that wants the codec lock. Dead code that has never + * run is not the same as code that works, which is the whole reason this + * surfaced the moment the crutch came off. + */ +/* + * Backtrace the first few hw_params calls, on by default until the thing + * that drives the boot-time open/start loop is identified. + */ +static bool hw_params_trace = true; +module_param(hw_params_trace, bool, 0644); +MODULE_PARM_DESC(hw_params_trace, "1=dump_stack() on the first few hw_params calls"); +static unsigned int hw_params_seen; + +/* + * Halt after this many hw_params calls, naming the caller. 0 disables. + * A normal boot opens the PCM a handful of times at most. + */ +static unsigned int hw_params_loop_panic; /* off: the loop was tone-loop from net-up */ +module_param(hw_params_loop_panic, uint, 0644); +MODULE_PARM_DESC(hw_params_loop_panic, + "panic after N PCM opens to freeze the caller name on screen (0=off)"); + +static uint sustain_ms; module_param(sustain_ms, uint, 0644); MODULE_PARM_DESC(sustain_ms, "ignore ALSA STOP for this many ms after START (default 5000)"); @@ -209,6 +269,76 @@ static u32 s5l8740_scale_lr(u32 sample) return ((u32)(u16)r << 16) | (u16)l; } +/* ------------------------------------------------------------------ */ +/* PCM inspection and sample-format probes */ +/* */ +/* The jack carries a 1 kHz fundamental with 2k/3k/4k/5k harmonics at */ +/* comparable amplitude, and a peak around 255 against a source that */ +/* peaks near 23000. A gain error would keep the sine clean and only */ +/* lower it; a mangled periodic waveform means the sample word is being */ +/* interpreted wrongly somewhere between the ring buffer and the codec. */ +/* */ +/* pcm_dump prints what the driver is actually about to hand the DMA, so */ +/* the question "is the buffer already byte-scale?" is answered from the */ +/* kernel rather than inferred from the analog end. */ +/* ------------------------------------------------------------------ */ + +/* + * Off. pio_tone, dma_tone and walk_bit exist to poke tones and sweep TXCON + * bits by hand during bring-up. They drive the codec and the DMA engine + * directly, and walk_one re-enters codec prepare, so a stray write to any + * of them from a script or a stale test harness moves real hardware. They + * are not needed for normal operation; set debug_tone=1 when deliberately + * using them. + */ +static bool debug_tone; +module_param(debug_tone, bool, 0644); +MODULE_PARM_DESC(debug_tone, + "1=enable the pio_tone/dma_tone/walk_bit debug pokes (default off)"); + +static unsigned int walk_ms = 1000; +module_param(walk_ms, uint, 0644); +MODULE_PARM_DESC(walk_ms, "Milliseconds per walking-bit step"); + +static unsigned int walk_gap_ms = 400; +module_param(walk_gap_ms, uint, 0644); +MODULE_PARM_DESC(walk_gap_ms, "Silence between walking-bit steps"); + +static bool pcm_dump; +module_param(pcm_dump, bool, 0644); +MODULE_PARM_DESC(pcm_dump, + "Log the first frames and their range at each stream start"); + +/* + * Sample rewrites applied on the way to the FIFO. All default to off, so + * the transport is unchanged unless something is being tested. + */ +static int sample_shift; +module_param(sample_shift, int, 0644); +MODULE_PARM_DESC(sample_shift, + "Arithmetic shift applied per sample, -16..16 (0 = none)"); + +static bool sample_byteswap; +module_param(sample_byteswap, bool, 0644); +MODULE_PARM_DESC(sample_byteswap, "Swap the two bytes of each sample"); + +static bool sample_swap_lr; +module_param(sample_swap_lr, bool, 0644); +MODULE_PARM_DESC(sample_swap_lr, "Swap left and right within each frame"); + +static s16 s5l8740_sample_fix(s16 v) +{ + int x = v; + + if (sample_shift > 0) + x <<= min(sample_shift, 16); + else if (sample_shift < 0) + x >>= min(-sample_shift, 16); + if (sample_byteswap) + x = (s16)__swab16((u16)x); + return (s16)x; +} + static int use_pio; module_param(use_pio, int, 0644); MODULE_PARM_DESC(use_pio, "1 = CPU FIFO PCM; 0 = PL080 M2P peri 10 from DT (default)"); @@ -279,11 +409,43 @@ struct s5l8740_i2s { unsigned int pio_hw_ptr; unsigned int rate; struct delayed_work dma_watch; + bool programmed; unsigned long play_jiffies; u32 last_dma_src; u8 watch_ticks; }; +/* + * Report the buffer as the hardware will see it: signed values, the range + * over a period, and the raw halfwords. A source that peaks in the low + * hundreds here is an application or format-negotiation fault and nothing + * downstream needs changing. + */ +static void s5l8740_pcm_dump(struct s5l8740_i2s *i2s, const s16 *buf, + unsigned int frames, const char *tag) +{ + int lo = 32767, hi = -32768; + unsigned int i, n = min(frames, 512u); + long long acc = 0; + + if (!pcm_dump || !buf || !n || !i2s->dev) + return; + for (i = 0; i < n * 2; i++) { + int v = buf[i]; + + lo = min(lo, v); + hi = max(hi, v); + acc += (long long)v * v; + } + dev_info(i2s->dev, + "pcm %s: %u frames min=%d max=%d rms=%u\n", + tag, n, lo, hi, + (unsigned int)int_sqrt((unsigned long)div64_u64(acc, n * 2))); + dev_info(i2s->dev, "pcm %s: first 8 (L,R) %*ph\n", + tag, 32, buf); +} + + /* * SEC sub_2034 leftovers. OSOS 983430 never programs clock 9; * it does program clocks 6/20 into +0x1C after SEC. If U-Boot @@ -304,7 +466,41 @@ struct s5l8740_i2s { #define STOCK_CLKCON_18 0x20012001u #define STOCK_CLKCON_1C 0xD0052003u -static bool force_stock_audio_parent = true; +/* + * CLKCON+0x10 is the FM clock, and both paths write it. + * + * The decomp names it: sub_15DD5C powers FM through sub_41CBD8(v2, on) + * with v2 = sub_4E7B0() = 11, and case 11 of sub_41CBD8 clears bit 15 + * of 0x3C500010 to enable and sets it to disable. That is exactly the + * CLKCON_FM_GATE_ON / _IDLE pair below, which had been derived from the + * oracle. + * + * s5l8740_i2s_ungate writes the whole music-playing CLKCON snapshot, + * and in that snapshot FM is off -- STOCK_CLKCON_10 is 0x8000, bit 15 + * set, divider nibble zeroed. Playback starting while FM capture ran + * therefore gated off the capture's own clock and destroyed its + * divider. That is the same failure the +0x30 arbitration above already + * fixes in the other direction, so it gets the same treatment: while + * IIS2 holds the FM gate, playback leaves +0x10 alone. + */ +static bool s5l8740_fm_gate_held; + +/* + * OFF by default. This pushes a RetailOS music-playing snapshot into + * CLKCON +0x08..0x1C as whole-register writes, and those are SoC-wide + * clock gates, not audio-private ones. Blind full-register writes + * therefore discard whatever every other block had set. Observed: the + * device boots, FIL_Init reports the NAND fine, then the first audio + * start overwrites the clock tree and the FMSS controller loses its + * clock -- FMCTRL1 bit 30 never sets again, FMCTRL0 and NANDSTAT both + * read back the same stale word, and storage is gone until reboot. + * + * The conservative branch below is also the attested one: it only + * ungates what sub_41CBD8(9,1) actually specifies, read-modify-write, + * and leaves every other block's bits alone. Set this to 1 only to + * reproduce the snapshot experiment, and expect to lose storage. + */ +static bool force_stock_audio_parent; module_param(force_stock_audio_parent, bool, 0644); MODULE_PARM_DESC(force_stock_audio_parent, "1=force CLKCON+0x08..0x1C to RetailOS music-playing snapshot"); @@ -325,7 +521,8 @@ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) */ writel(STOCK_CLKCON_08, i2s->clkcon + 0x08); writel(STOCK_CLKCON_0C, i2s->clkcon + 0x0c); - writel(STOCK_CLKCON_10, i2s->clkcon + 0x10); + if (!READ_ONCE(s5l8740_fm_gate_held)) + writel(STOCK_CLKCON_10, i2s->clkcon + 0x10); writel(STOCK_CLKCON_14, i2s->clkcon + 0x14); writel(STOCK_CLKCON_18, i2s->clkcon + 0x18); writel(STOCK_CLKCON_1C, i2s->clkcon + 0x1c); @@ -699,16 +896,42 @@ static void s5l8740_i2s_status_w1c_tx(struct s5l8740_i2s *i2s) /* 26DDDE: 41CBD8(9,1), 5705DC RX, 414FAE (C09AC + BCB60), D34C0 CLKDIV. */ static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate) { - const struct n31_rate_cfg *r = n31_find_rate(rate); + const struct n31_rate_cfg *r; u32 div; u32 rxcom; + /* Same resolver the codec uses, so the divider always matches. */ + rate = n31_resolve_rate(rate); + r = n31_find_rate(rate); + + /* + * Do not re-do all of this for a rate we are already programmed for. + * + * Something opens the PCM during boot and prepare can fail, and when + * it does the PCM layer retries at the next advertised rate. Every one + * of those retries landed here and re-ran the pad mux, the TXCON and + * RXCON writes and the clock programming from scratch. With two rates + * advertised that was a couple of passes; advertising all nine the + * hardware supports turned it into a visible storm of pinmux and reset + * lines at boot, which is alarming to read and does real work for no + * reason. + * + * Programming is idempotent, so the cheapest correct answer is not to + * repeat it. This does not fix whatever opens the PCM or whatever makes + * prepare fail -- both are still open -- it stops those from thrashing + * the pads and the clock while they are unresolved. + */ + if (i2s->programmed && i2s->rate == rate && !clkdiv) { + dev_dbg(i2s->dev, "program: already at %u, skipping\n", rate); + return; + } + if (clkdiv) div = clkdiv; else if (r) div = r->clkdiv; else - div = MCLK_ASSUME_HZ / n31_pick_rate(rate); + div = MCLK_ASSUME_HZ / N31_RATE_DEFAULT; if (div < 1) div = 1; s5l8740_i2s_ungate(i2s); @@ -727,7 +950,8 @@ static void s5l8740_i2s_program(struct s5l8740_i2s *i2s, unsigned int rate) writel(0x00010007u, i2s->base + I2SREG44); /* Setup only — TXCOM stays 0 until .trigger START (OSOS B6620). */ writel(I2STXCOM_STOP, i2s->base + I2STXCOM); - i2s->rate = n31_pick_rate(rate); + i2s->rate = rate; + i2s->programmed = true; } /* @@ -797,7 +1021,7 @@ static void s5l8740_i2s_dma_watch(struct work_struct *work) u32 src = 0, dst = 0, en = 0, st, txcom; int ret; - if (!i2s || !i2s->base) + if (!i2s || !i2s->base || !dma_watch_ticks) return; ret = s5l_pl080_peri_snapshot(10, &src, &dst, &en); st = readl(i2s->base + I2SSTATUS); @@ -819,7 +1043,7 @@ static void s5l8740_i2s_dma_watch(struct work_struct *work) } i2s->last_dma_src = src; i2s->watch_ticks++; - if (i2s->watch_ticks < 50) + if (i2s->watch_ticks < dma_watch_ticks) schedule_delayed_work(&i2s->dma_watch, msecs_to_jiffies(100)); } @@ -829,24 +1053,81 @@ static int s5l8740_i2s_hw_params(struct snd_pcm_substream *substream, { struct s5l8740_i2s *i2s = dev_get_drvdata(dai->dev); unsigned int rate = params_rate(params); - const struct n31_rate_cfg *r = n31_find_rate(rate); + unsigned int resolved = n31_resolve_rate(rate); + const struct n31_rate_cfg *r = n31_find_rate(resolved); u32 div; int ret; if (!i2s || !i2s->base) return -ENODEV; - if (!r && !clkdiv) - return -EINVAL; + /* + * Do not refuse an out-of-table rate: resolve it to the nearest + * supported one and let the codec SRC carry it. Refusing here while + * the codec silently fell back to 44.1 was how the two ends ended up + * disagreeing. + */ + if (resolved != rate) + dev_warn(dai->dev, "rate %u unsupported, using %u (SRC)\n", + rate, resolved); + /* + * Name whatever is driving the open/start cycle. + * + * The whole sequence -- hw_params, program, pinmux, DMA start, mute -- + * repeats endlessly at boot, and every fix so far has been to a step + * inside it, which cannot stop something that keeps calling the cycle + * again. Individual steps are not the problem; the caller is. + * + * hw_params is the top of that cycle, so print a backtrace for the + * first few passes. Three is enough to see whether it arrives from a + * syscall (a process opening the PCM), from a kernel worker, or from + * the same place every time. + */ + /* + * One line, every time, naming who asked. + * + * This was three dump_stack() calls, which is the wrong shape for a + * device with no console: by the time anyone reads the screen the + * loop has run hundreds of times and those three multi-line traces + * are long gone off the top. current->comm and pid fit on one line + * and answer the only question that matters -- whether this arrives + * from a userspace process, and which, or from a kernel thread. + */ + if (hw_params_trace) { + hw_params_seen++; + dev_info(dai->dev, "hw_params #%u by %s[%d] rate=%u\n", + hw_params_seen, current->comm, current->pid, + params_rate(params)); + + /* + * Stop the scroll and leave the answer on screen. + * + * This device has no console; the log is read off the display + * by eye. A loop that reopens the PCM hundreds of times makes + * every added line unreadable -- it scrolls past faster than + * anyone can parse, so more logging cannot help. Halting can: + * with panic=0 the machine stops with this as the last thing + * printed, and it names exactly who kept asking. + * + * Only fires when the loop is real. A handful of opens during + * a normal boot stays well under the limit. + */ + if (hw_params_loop_panic && + hw_params_seen >= hw_params_loop_panic) + panic("n31: PCM reopened %u times, last by %s[%d] rate=%u", + hw_params_seen, current->comm, current->pid, + params_rate(params)); + } + ret = s5l8740_i2s_codec_prepare(); if (ret && i2s->dev) dev_warn(i2s->dev, "codec prepare in hw_params: %d\n", ret); - s5l8740_i2s_program(i2s, rate); + s5l8740_i2s_program(i2s, resolved); div = clkdiv ? clkdiv : (r ? r->clkdiv : 0); s5l8740_i2s_log_clocks(i2s, "hw_params"); dev_info(dai->dev, - "IIS hw_params rate=%u code=%u clkdiv=%u dma=%d pio=%d txcom=%08x\n", - rate, r ? r->cs42_rate_code : 0, div, i2s->has_dma, use_pio, - readl(i2s->base + I2STXCOM)); + "IIS hw_params rate=%u resolved=%u code=%u clkdiv=%u dma=%d pio=%d txcom=%08x\n", + rate, resolved, r ? r->cs42_rate_code : 0, div, i2s->has_dma, + use_pio, readl(i2s->base + I2STXCOM)); return 0; } @@ -862,20 +1143,39 @@ static int s5l8740_i2s_trigger(struct snd_pcm_substream *substream, int cmd, case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + /* + * Stage markers, printed before each step. The trigger has + * hung here, and the DMA start log was the last thing on + * screen -- which tells us only that it got that far, not + * which of the following steps failed to return. Naming the + * step before running it answers that directly, on a device + * whose only diagnostic is what is left on the display. + */ path_mode = s5l8740_i2s_audio_path_mode(); - if (path_mode == 1) + dev_info(dai->dev, "trig: path_mode=%d\n", path_mode); + if (path_mode == 1) { + dev_info(dai->dev, "trig: codec_play_start(1)\n"); s5l8740_i2s_codec_play_start(); + } + dev_info(dai->dev, "trig: tx_kick\n"); s5l8740_i2s_tx_kick(i2s, !use_pio); - if (path_mode == 2) + if (path_mode == 2) { + dev_info(dai->dev, "trig: codec_play_start(2)\n"); s5l8740_i2s_codec_play_start(); + } + dev_info(dai->dev, "trig: log_clocks\n"); s5l8740_i2s_log_clocks(i2s, "trigger_start"); + dev_info(dai->dev, "trig: schedule_asp\n"); s5l8740_i2s_schedule_asp(); + dev_info(dai->dev, "trig: done\n"); i2s->pio_run = use_pio; i2s->play_jiffies = jiffies; i2s->watch_ticks = 0; i2s->last_dma_src = 0; - mod_delayed_work(system_wq, &i2s->dma_watch, msecs_to_jiffies(100)); - dev_info(dai->dev, + if (dma_watch_ticks) + mod_delayed_work(system_wq, &i2s->dma_watch, + msecs_to_jiffies(100)); + dev_info_ratelimited(dai->dev, "DAI trigger START path_mode=%d txcom=%08x sustain=%ums\n", path_mode, readl(i2s->base + I2STXCOM), sustain_ms); return 0; @@ -1197,6 +1497,11 @@ static ssize_t pio_tone_store(struct device *dev, struct device_attribute *attr, unsigned int rate, frames, i; s16 s; + if (!debug_tone) { + dev_info(dev, "debug tone disabled (set debug_tone=1)\n"); + return -EPERM; + } + if (!i2s || !i2s->base) return -ENODEV; if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') @@ -1222,6 +1527,151 @@ static ssize_t pio_tone_store(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_WO(pio_tone); +/* + * Walking-bit oracle. + * + * A sine cannot tell you which bit lanes survive the trip to the codec, + * because every wrong answer still looks like "quiet and dirty". This + * plays a square wave built from one bit at a time -- +BIT(n), -BIT(n), + * alternating -- so the analog result is a direct readout of that bit's + * significance. + * + * A correct 16-bit path doubles the output for each step of n. Reading + * the result: + * + * only low bits audible the codec is latching the wrong byte lane + * only high bits audible samples are landing too far down the slot + * bit 15 not the loudest sign or justification is wrong + * flat across all n the link is not carrying sample data at all + * + * Square edges are deliberate: they survive whatever the analog stage + * does far better than a low-amplitude sine, which matters when the + * quantity being measured may be 40 dB down. + * + * echo 9 > walk_bit play ~1 s of +/-BIT(9) + * echo -1 > walk_bit sweep 0..15, one second each, logging as it goes + */ +#define S5L8740_WALK_HZ 1000u + +static int s5l8740_walk_one(struct s5l8740_i2s *i2s, int bit, + unsigned int ms) +{ + struct dma_async_tx_descriptor *desc; + struct dma_slave_config cfg = { }; + struct dma_chan *chan; + dma_addr_t dma; + unsigned int rate, frames, half, i; + size_t bytes; + s16 *buf, hi, lo; + int ret; + + if (bit < 0 || bit > 15) + return -EINVAL; + + rate = i2s->rate ? i2s->rate : N31_RATE_DEFAULT; + half = max(1u, rate / (2 * S5L8740_WALK_HZ)); + frames = half * 2; + bytes = frames * 2 * sizeof(s16); + + /* + * Bit 15 is the sign bit, so the pair is 0 and -32768 rather than + * a symmetric swing; every other bit alternates about zero. + */ + hi = (bit == 15) ? 0 : (s16)(1 << bit); + lo = (bit == 15) ? (s16)-32768 : (s16)-(1 << bit); + + mutex_lock(&i2s->dma_lock); + chan = s5l8740_i2s_tx_get(i2s); + if (IS_ERR_OR_NULL(chan)) { + mutex_unlock(&i2s->dma_lock); + return chan ? PTR_ERR(chan) : -ENODEV; + } + buf = dma_alloc_coherent(i2s->dev, bytes, &dma, GFP_KERNEL); + if (!buf) { + ret = -ENOMEM; + goto out_unlock; + } + for (i = 0; i < frames; i++) { + s16 v = (i < half) ? hi : lo; + + buf[i * 2] = v; + buf[i * 2 + 1] = v; + } + s5l8740_pcm_dump(i2s, buf, frames, "walk"); + + cfg.direction = DMA_MEM_TO_DEV; + cfg.dst_addr = i2s->play_dma.addr; + cfg.dst_addr_width = (tone_width == 2) ? + DMA_SLAVE_BUSWIDTH_2_BYTES : DMA_SLAVE_BUSWIDTH_4_BYTES; + cfg.dst_maxburst = 1; + ret = dmaengine_slave_config(chan, &cfg); + if (ret) + goto out_buf; + + s5l8740_i2s_codec_prepare(); + s5l8740_i2s_program(i2s, rate); + desc = dmaengine_prep_dma_cyclic(chan, dma, bytes, bytes, + DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); + if (!desc) { + ret = -ENOMEM; + goto out_buf; + } + if (dma_submit_error(dmaengine_submit(desc))) { + ret = -EIO; + goto out_buf; + } + dma_async_issue_pending(chan); + s5l8740_i2s_codec_play_start(); + s5l8740_i2s_tx_kick(i2s, !use_pio); + s5l8740_i2s_schedule_asp(); + dev_info(i2s->dev, "walk bit %d: +%d/%d for %u ms\n", + bit, hi, lo, ms); + msleep(ms); + s5l8740_i2s_cancel_asp(); + s5l8740_i2s_codec_play_stop(); + dmaengine_terminate_sync(chan); + s5l8740_i2s_hw_stop(i2s, NULL); + ret = 0; +out_buf: + dma_free_coherent(i2s->dev, bytes, buf, dma); +out_unlock: + mutex_unlock(&i2s->dma_lock); + return ret; +} + +static ssize_t walk_bit_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct s5l8740_i2s *i2s = dev_get_drvdata(dev); + int bit, ret; + + if (!debug_tone) { + dev_info(dev, "debug tone disabled (set debug_tone=1)\n"); + return -EPERM; + } + + if (!i2s || !i2s->base) + return -ENODEV; + if (kstrtoint(buf, 0, &bit)) + return -EINVAL; + + if (bit >= 0) { + ret = s5l8740_walk_one(i2s, bit, walk_ms); + return ret ? ret : count; + } + for (bit = 0; bit < 16; bit++) { + ret = s5l8740_walk_one(i2s, bit, walk_ms); + if (ret) { + dev_err(dev, "walk bit %d: %d\n", bit, ret); + return ret; + } + msleep(walk_gap_ms); + } + return count; +} +static DEVICE_ATTR_WO(walk_bit); + static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { @@ -1237,6 +1687,11 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, s16 s; int ret; + if (!debug_tone) { + dev_info(dev, "debug tone disabled (set debug_tone=1)\n"); + return -EPERM; + } + if (!i2s || !i2s->base) return -ENODEV; if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') @@ -1261,10 +1716,11 @@ static ssize_t dma_tone_store(struct device *dev, struct device_attribute *attr, goto out_unlock; } for (i = 0; i < frames; i++) { - s = s5l8740_scale_s16(n31_tone_s16(i, rate)); + s = s5l8740_sample_fix(s5l8740_scale_s16(n31_tone_s16(i, rate))); tone[i * 2] = s; tone[i * 2 + 1] = s; } + s5l8740_pcm_dump(i2s, tone, frames, "dma_tone"); dma_sync_single_for_device(dev, dma, bytes, DMA_TO_DEVICE); cfg.direction = DMA_MEM_TO_DEV; @@ -1409,6 +1865,9 @@ static int s5l8740_i2s_probe(struct platform_device *pdev) ret = device_create_file(dev, &dev_attr_pio_tone); if (ret) dev_warn(dev, "pio_tone sysfs: %d\n", ret); + ret = device_create_file(dev, &dev_attr_walk_bit); + if (ret) + dev_warn(dev, "walk_bit sysfs: %d\n", ret); ret = device_create_file(dev, &dev_attr_dma_tone); if (ret) dev_warn(dev, "dma_tone sysfs: %d\n", ret); @@ -1445,6 +1904,23 @@ static void s5l8740_i2s_remove(struct platform_device *pdev) clk_bulk_disable_unprepare(i2s->num_clks, i2s->clks); } +/* + * IIS0 drives the codec through PL080. Left running across a kexec it + * keeps fetching from a buffer the next kernel has reused, so the + * handover is audible as well as unsafe. hw_stop is the same teardown + * the STOP path uses -- TXCOM stop, DMA terminated, pads released. + */ +static void s5l8740_i2s_shutdown_pdev(struct platform_device *pdev) +{ + struct s5l8740_i2s *i2s = platform_get_drvdata(pdev); + + if (!i2s) + return; + i2s->pio_run = false; + s5l8740_i2s_hw_stop(i2s, NULL); + s5l8740_i2s_tx_put(i2s); +} + static const struct of_device_id s5l8740_i2s_of_match[] = { { .compatible = "apple,s5l8740-i2s" }, { .compatible = "samsung,s5l8740-i2s" }, @@ -1455,6 +1931,7 @@ MODULE_DEVICE_TABLE(of, s5l8740_i2s_of_match); static struct platform_driver s5l8740_i2s_driver = { .probe = s5l8740_i2s_probe, .remove = s5l8740_i2s_remove, + .shutdown = s5l8740_i2s_shutdown_pdev, .driver = { .name = "s5l8740-i2s", .of_match_table = s5l8740_i2s_of_match, @@ -1545,12 +2022,14 @@ static void iis2_fm_gate(struct s5l8740_iis2 *iis2, bool on) iis2->fm_gate_saved = cur; iis2->fm_gate_held = true; } + WRITE_ONCE(s5l8740_fm_gate_held, true); writel(CLKCON_FM_GATE_ON, iis2->clkcon + CLKCON_FM_GATE); } else if (iis2->fm_gate_held) { writel(iis2->fm_gate_saved ? iis2->fm_gate_saved : CLKCON_FM_GATE_IDLE, iis2->clkcon + CLKCON_FM_GATE); iis2->fm_gate_held = false; + WRITE_ONCE(s5l8740_fm_gate_held, false); } } @@ -1758,6 +2237,16 @@ static void s5l8740_iis2_remove(struct platform_device *pdev) clk_bulk_disable_unprepare(iis2->num_clks, iis2->clks); } +/* + * IIS2 captures FM over PL080 and holds the CLKCON+0x10 gate while it + * does. Stopping it here also puts that gate back, so the next kernel + * does not inherit a clock enabled by a driver that no longer exists. + */ +static void s5l8740_iis2_shutdown(struct platform_device *pdev) +{ + iis2_hw_stop(platform_get_drvdata(pdev)); +} + static const struct of_device_id s5l8740_iis2_of_match[] = { { .compatible = "apple,s5l8740-bcm2078-pcm" }, { .compatible = "apple,s5l8740-iis2" }, @@ -1767,6 +2256,7 @@ MODULE_DEVICE_TABLE(of, s5l8740_iis2_of_match); static struct platform_driver s5l8740_iis2_driver = { .probe = s5l8740_iis2_probe, + .shutdown = s5l8740_iis2_shutdown, .remove = s5l8740_iis2_remove, .driver = { .name = "s5l8740-iis2", From 6ea5bc15b97e1503a3b952756f3cf757c8a4bfa5 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:50:48 -0230 Subject: [PATCH 24/31] N31: Bluetooth rail as a regulator, and PMIC button/rail work bcm2078-bt is built in and probes around t=2.4s; the PMIC driver is a module userspace loads at about t=7.1s. A driver asking for power through a bespoke hook in that window gets -ENODEV and has no way to wait, so the controller simply never came up and hci0 timed out on 0xFC18. The rail is exposed as a regulator instead, with of_match and regulators_node so a device tree node can name it. That makes the kernel do the waiting: devm_regulator_get returns -EPROBE_DEFER until the PMIC registers, and the consumer is re-probed. It does not fit n31_pmu_rails[], which describes single-register LDOs at 0x17..0x21 -- this one is 0x57 bits 7:6 and 0x58 bit 0 plus 6:4 -- so it carries its own descriptor whose enable and disable defer to d1830_bt_rails(). The UART-to-HCI bridge stays dumb: powering the chip belongs to the chip driver, and the bridge only carries bytes. REG_ON on GPIO 97 was never being asserted; gpio_poke covers that. Also here: the PMIC button poll is load-bearing and stays at 100ms. /proc/interrupts has no PMIC nIRQ -- of_irq never maps GPIO 86 -- so the poll is the mechanism, not a redundant product-behaviour poll. Disabling it on that reading broke Home, Sleep and Play. --- drivers/bluetooth/bcm2078-bt.c | 232 +++- drivers/gpio/gpio-d1830.c | 1911 ++++++++++++++++++++++++++++++-- 2 files changed, 2023 insertions(+), 120 deletions(-) diff --git a/drivers/bluetooth/bcm2078-bt.c b/drivers/bluetooth/bcm2078-bt.c index 3a0aa989cadd9b..edf94694ff2e67 100755 --- a/drivers/bluetooth/bcm2078-bt.c +++ b/drivers/bluetooth/bcm2078-bt.c @@ -22,6 +22,7 @@ */ #include #include +#include #include #include #include @@ -35,6 +36,84 @@ #include #include +/* + * The PMIC driver is a module and this file is built in, so the link + * can only go the other way: expose a hook and let gpio-d1830 register + * its rail control when it probes. Everything here still works with + * nothing registered -- the rails simply stay as the bootloader left + * them, which is the behaviour we had before. + */ +static int (*bcm_bt_rails_fn)(bool on); + +/* + * What we asked for while nobody was listening. + * + * This driver is built in and probes at about t=2.4s. gpio-d1830, which + * owns the PMIC rails, is a module that userspace loads later -- it + * registered here at t=7.1s on the boot that exposed this. In between, + * every bcm_bt_rails() call returned -ENODEV and the rails were simply + * never switched on, so the controller had no supply. hci_bcm sent + * 0xFC18 into a dead part at t=4.8s, timed out, and gave up two seconds + * before the rails became available. + * + * The hook was always the right shape for a built-in talking to a module; + * it just had no memory. Record the last request and apply it when the + * provider finally shows up. + */ +static bool bcm_bt_rails_want; +static bool bcm_bt_rails_pending; + +void bcm2078_register_bt_rails(int (*fn)(bool on)) +{ + bcm_bt_rails_fn = fn; + + if (fn && bcm_bt_rails_pending) { + int ret = fn(bcm_bt_rails_want); + + bcm_bt_rails_pending = false; + pr_info("bcm2078-bt: rails provider arrived late; applied %s: %d\n", + bcm_bt_rails_want ? "on" : "off", ret); + } +} +EXPORT_SYMBOL_GPL(bcm2078_register_bt_rails); + +/* + * The rail, taken as a regulator. + * + * This is the ordering fix. The hook below can only report that no provider + * exists yet; a regulator makes the kernel wait for one. devm_regulator_get() + * returns -EPROBE_DEFER while gpio-d1830 is still unloaded, probe is retried + * once it registers, and by the time this driver runs the rail is reachable. + * + * Power belongs here, in the chip driver, and not in the UART-to-HCI bridge: + * the bridge should move bytes and nothing else. hci_bcm carries supplies and + * shutdown-gpios upstream because on most boards it is also the chip driver, + * which is not true on this one. + */ +static struct regulator *bcm_bt_vreg; + +static int bcm_bt_rails(bool on) +{ + if (bcm_bt_vreg) { + int ret = on ? regulator_enable(bcm_bt_vreg) + : regulator_disable(bcm_bt_vreg); + + if (ret) + pr_warn("bcm2078-bt: bt rail %s failed: %d\n", + on ? "enable" : "disable", ret); + return ret; + } + + if (!bcm_bt_rails_fn) { + /* Remember it; the provider may still be loading. */ + bcm_bt_rails_want = on; + bcm_bt_rails_pending = true; + pr_info("bcm2078-bt: rails %s deferred, no provider yet\n", + on ? "on" : "off"); + return -ENODEV; + } + return bcm_bt_rails_fn(on); +} #define BCM_GPIO_PHYS 0x3cf00000UL #define BCM_GPIOCMD_OFF 0x1e0 @@ -42,7 +121,21 @@ #define BCM_GPIO_A 0x61 /* 97 — shutdown / REG_ON */ #define BCM_GPIO_B 0x62 /* 98 — device-wakeup */ #define BCM_GPIO_C 0x77 /* 119 — host-wakeup */ +/* + * 0xC8 = 200 is not a no-op. sub_17D4DC selects the Bluetooth power + * control by board variant: + * + * variant 1 or 2: sub_428F70(0xC8, 1); sub_43D38C(0xC8, 1, 1); + * variant 5: sub_43D38C(0x46, 1, 1); sub_428F70(0x46, 1); + * + * and sphwBluetooth_Init then drives 0x46 = 70 high unconditionally. + * Both orders pair the pad write with sub_428F70, which sets the pad's + * +0x0C bit -- something a gpiod output cannot express, so hci_bcm + * driving shutdown-gpios does not cover it. + */ #define BCM_GPIO_NOP 0xC8 +#define BCM_GPIO_PWR 0x46 /* 70 — power control, all variants */ +#define BCM_GPIO_PWR_ALT 0xC8 /* 200 — variants 1 and 2 */ #define BCM_MODE_POWER 2 #define BCM_MODE_CLEAR 0xFFFE @@ -151,7 +244,33 @@ /* * Off by default: these pins belong to hci_bcm. See the file header. */ -static bool gpio_poke; +/* + * On. Without it the BCM part is never actually powered. + * + * hci_bcm drives shutdown-gpios, which the device tree points at GPIO 70 -- + * the power control pin. REG_ON is GPIO 97, and device-wake and host-wake + * are 98 and 119. Stock sets all three to mode 2 as part of its power + * sequence: + * + * sub_43D38C(0x61u, 2, 0) 97 REG_ON + * sub_43D38C(0x62u, 2, 0) 98 device-wake + * sub_43D38C(0x77u, 2, 0) 119 host-wake + * sub_43D38C(0x46u, 1, 1) 70 power control, paired with 428F70 + * + * With this off we did the rails and the +0x0C pad gate and then stopped, + * so REG_ON was never asserted and the controller stayed in reset. The + * symptom is unambiguous: vendor command 0xFC18 times out, hci_bcm reports + * "failed to write update baudrate (-110)", and /proc/interrupts shows + * zero interrupts on 3db00000.serial -- the chip has never sent a byte. + * + * It defaulted off because driving these pads early once correlated with a + * reset back to RetailOS. That is a single observation against a sequence + * the stock firmware performs on every power-on, and the cost of honouring + * it is that Bluetooth cannot work at all. If the reset returns, the thing + * to investigate is ordering against the rails, not whether to power the + * part. + */ +static bool gpio_poke = true; module_param(gpio_poke, bool, 0644); MODULE_PARM_DESC(gpio_poke, "Drive the BCM control pins directly (default N; hci_bcm owns them)"); @@ -226,6 +345,39 @@ static void bcm_43D38C(struct bcm2078_bt *bt, unsigned int gpio, u16 mode, int v writel(((gpio >> 3) << 16) | (pin << 8) | cmd, bt->gpiocmd); } +/* sub_428F70(gpio, on): the pad's +0x0C bit, paired with every + * sub_43D38C power write in sub_17D4DC. */ +static void bcm_428F70(struct bcm2078_bt *bt, unsigned int gpio, int on) +{ + void __iomem *bank; + u32 pin, v; + + if (!bt->gpio) + return; + bank = bt->gpio + 32 * (gpio >> 3); + pin = gpio & 7; + v = readl(bank + 0x0c); + if (on) + v |= BIT(pin); + else + v &= ~BIT(pin); + writel(v, bank + 0x0c); +} + +/* + * The +0x0C half of the power sequence, kept separate from the mode-2 + * level pokes below. Those drive pads 97/98/119, which are the IIS2 PCM + * bus, and doing that early once correlated with resets back to + * RetailOS -- hence gpio_poke defaulting off. This is only an input + * enable on the power pad, so it is safe to run whenever the caller + * asks for power, and hci_bcm cannot do it through gpiod. + */ +static void bcm_power_pad_gate(struct bcm2078_bt *bt, int on) +{ + bcm_428F70(bt, BCM_GPIO_PWR, on); + dev_dbg(bt->dev, "pad %#x +0x0C -> %d\n", BCM_GPIO_PWR, on); +} + static void bcm_power_pins_on(struct bcm2078_bt *bt) { bcm_43D38C(bt, BCM_GPIO_NOP, 0, 0); @@ -629,7 +781,18 @@ static int bcm_fm_seek(struct bcm2078_bt *bt, int up, u8 rssi) static int bcm_power_on(struct bcm2078_bt *bt) { + int ret; + bt->powered = true; + /* + * Rails first, then the pad gate, then the level: powering a pin + * before its supply is the wrong order and is what the de-init + * sequence unwinds. + */ + ret = bcm_bt_rails(true); + if (ret && ret != -ENODEV) + dev_warn(bt->dev, "bt rails on: %d\n", ret); + bcm_power_pad_gate(bt, 1); if (!gpio_poke) { dev_dbg(bt->dev, "control pins left to hci_bcm (gpio_poke=0)\n"); @@ -642,12 +805,26 @@ static int bcm_power_on(struct bcm2078_bt *bt) return 0; } +/* + * sub_51688C: two delays, then sub_158C82 zeroing entries 3 and 5. + * Unwound in the reverse order of power-on -- levels, then the pad + * gate, then the rails -- so the part is not left driving a pin whose + * supply has already gone. Doing the rails is also what makes this a + * real off rather than an idle: without it the companion keeps drawing + * even with the control pin low. + */ static void bcm_power_off(struct bcm2078_bt *bt) { + int ret; + bt->powered = false; - if (!gpio_poke) - return; - bcm_power_pins_off(bt); + if (gpio_poke) + bcm_power_pins_off(bt); + msleep(2); + bcm_power_pad_gate(bt, 0); + ret = bcm_bt_rails(false); + if (ret && ret != -ENODEV) + dev_warn(bt->dev, "bt rails off: %d\n", ret); } /* ---------- V4L2 radio (tuner control only; PCM is ALSA IIS2) ---------- */ @@ -1182,8 +1359,37 @@ static int bcm2078_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct bcm2078_bt *bt; + struct regulator *bt_vreg; int ret; + /* + * Take the rail first, and let the kernel handle the ordering. + * + * This driver is built in and probes at about t=2.4s; gpio-d1830, + * which owns the PMIC, is a module userspace loads at about t=7.1s. + * Everything in between used to fail with -ENODEV and there was no + * way to wait, so the controller was never powered and hci_bcm timed + * out talking to a dead part. Deferring is the whole point: the + * kernel re-probes this driver once the provider registers. + * + * Optional on purpose. A device tree without bt-supply keeps the old + * hook path rather than refusing to probe at all. + */ + bt_vreg = devm_regulator_get_optional(dev, "bt"); + if (IS_ERR(bt_vreg)) { + ret = PTR_ERR(bt_vreg); + bt_vreg = NULL; + if (ret == -EPROBE_DEFER) { + dev_info(dev, "waiting for the bt rail provider\n"); + return ret; + } + dev_info(dev, "no bt-supply (%d); using the legacy rails hook\n", + ret); + } else { + bcm_bt_vreg = bt_vreg; + dev_info(dev, "bt rail acquired as a regulator\n"); + } + bt = devm_kzalloc(dev, sizeof(*bt), GFP_KERNEL); if (!bt) return -ENOMEM; @@ -1214,6 +1420,23 @@ static int bcm2078_probe(struct platform_device *pdev) return 0; } +/* + * sub_51688C is the de-init OSOS runs when Bluetooth goes away, and + * bcm_power_off now implements it. Reaching it only from remove() meant + * the companion kept its rails across a reboot, so the part came up in + * whatever state the previous kernel left it rather than from reset. + */ +static void bcm2078_shutdown(struct platform_device *pdev) +{ + struct bcm2078_bt *bt = platform_get_drvdata(pdev); + + if (!bt) + return; + mutex_lock(&bt->lock); + bcm_power_off(bt); + mutex_unlock(&bt->lock); +} + static void bcm2078_remove(struct platform_device *pdev) { struct bcm2078_bt *bt = platform_get_drvdata(pdev); @@ -1240,6 +1463,7 @@ MODULE_DEVICE_TABLE(of, bcm2078_of_match); static struct platform_driver bcm2078_driver = { .probe = bcm2078_probe, .remove = bcm2078_remove, + .shutdown = bcm2078_shutdown, .driver = { .name = "bcm2078-bt", .of_match_table = bcm2078_of_match, diff --git a/drivers/gpio/gpio-d1830.c b/drivers/gpio/gpio-d1830.c index 2fc27e67056f4f..0cf53faa4a477f 100755 --- a/drivers/gpio/gpio-d1830.c +++ b/drivers/gpio/gpio-d1830.c @@ -22,6 +22,8 @@ * Copyright (C) 2026 Vencislav Atanasov */ #include +#include +#include #include #include #include @@ -40,16 +42,161 @@ #include #include +#include -#define D1830_REG_POWEROFF 13 +/* + * Power-off and restart are the same PMIC write. The bootloader's + * __noreturn sub_128C does + * + * sub_3F60(110, 1, 0) reg 110 = 0 + * sub_3F40(64, 4, v5) read reg 64, add 2, write reg 69 + * sub_3F60(73, 1, 1) reg 73 = 1 restart-after-cut + * sub_3F40(13, ...) reg 13 |= 1 cut power + * sub_1130() halt + * + * while its low-battery path does the reg 13 write with no reg 73 at + * all and stays off. So bit 0 of 13 always cuts power, and 73 decides + * whether the PMIC comes back. Writing 13 without clearing 73 is a + * reboot dressed as a shutdown, which is what this driver was doing. + */ +/* + * Registers are written in hex here, deliberately. + * + * Hex-Rays prints them in decimal, so sub_7484's "case 16: v7 = 40" + * means register 40 decimal, which is 0x28 -- not 0x40. Reading those + * listings as hex is what produced a whole fictional rail map with a + * codec enable at 0x40, and 0x40 turned out to be a ticking counter. + * One radix mistake is a bad afternoon; leaving the door open for a + * second is a design choice. + * + * The decimal literals this driver used were correct -- reg 16 really + * is 0x10, the rail bitfield -- but they made checking against a + * decompiler listing an exercise in mental arithmetic, which is + * exactly when radix mistakes happen. + */ +/* + * sub_1E7C, the boot low-battery decision, tests this after finding + * VBAT below 3550 mV. If the bit is set RetailOS permits Low Power Boot + * even below 3400 mV; if it is clear below 3400 mV it powers the device + * off instead. That is precisely the shape of an external-source + * indication. + * + * Confirmed on glass. Unplugging cleared the bit and replugging set + * it, observed live: + * + * 3094 external power OFF -> gadget disconnect + * 3169 external power ON -> gadget connect + * + * so this is the VBUS presence indication rather than merely some + * external source. VBUS_PRESENT is what it means; the EXT_POWER + * spelling is kept as an alias so the firmware notes that introduced + * it still read straight. + * + * Bit 2 is separate and also moved. 0x05 went 0x68 to 0x6c and 0x06 + * went 0x40 to 0x44 across the same cable cycle, so bit 2 latched in + * both. 0x05 to 0x08 are the event latches this driver already reads + * for buttons, masked at 0x09 to 0x0c, which makes bit 2 an + * attach/detach event rather than a level. + * + * Deliberately not wired to anything. One observation cannot separate + * attach from detach, and acknowledging a latch we have not + * characterised risks consuming an event something else depends on. + */ +#define D1830_REG_STATUS0 0x05 /* 5 */ +#define D1830_STATUS_VBUS_PRESENT BIT(6) +#define D1830_STATUS_EXT_POWER D1830_STATUS_VBUS_PRESENT +#define D1830_REG_STATUS1 0x06 /* 6 */ +/* Latched in both 0x05 and 0x06 by a cable event; polarity unproven. */ +#define D1830_STATUS_CABLE_EVENT BIT(2) + +#define D1830_REG_POWEROFF 0x0d /* 13 */ #define D1830_POWEROFF_BIT BIT(0) -#define D1830_REG_ADC_CFG 48 -#define D1830_REG_ADC_LOW 49 -#define D1830_REG_ADC_HIGH 50 + +/* + * Read at entry to sub_1E7C, before the battery is even measured: if + * set, RetailOS writes 1 back and goes straight to Low Power Boot. A + * persistent latch, and specifically NOT the external-power test -- + * that decision happens later from 0x05 bit 6. + */ +#define D1830_REG_LOWPOWER_LATCH 0x0e /* 14 */ +#define D1830_LOWPOWER_LATCH BIT(0) + +/* Programmed by the stock Low Power Boot path. Meanings unresolved. */ +#define D1830_REG_LOWPOWER_CFG0 0x24 /* 36 */ +#define D1830_REG_LOWPOWER_CFG1 0x29 /* 41 */ +#define D1830_LOWPOWER_CFG1_MASK 0x07 +#define D1830_LOWPOWER_CFG1_RETAIL 0x06 + +/* + * sub_1E7C thresholds, in millivolts against d1830_adc_to_mv(): + * >= 3550 boot normally + * 3400..3549 Low Power Boot + * < 3400 Low Power Boot if external power, else power off + */ +#define N31_VBAT_BOOT_NORMAL_MV 3550 +#define N31_VBAT_BOOT_MIN_MV 3400 +#define D1830_REG_RESTART 0x49 /* 73; 1 = come back after the cut */ +#define D1830_REG_CLR_ON_CUT 0x6e /* 110 */ + +/* + * SoC fallback from the tail of sub_128C, reached only if the PMIC + * write fails. 0xA5 is a magic value rather than a bit pattern. + */ +#define S5L8740_RESET_A_PHYS 0x3c800000UL +#define S5L8740_RESET_B_PHYS 0x3c500050UL +#define S5L8740_RESET_MAGIC 0xa5 +#define D1830_REG_ADC_CFG 0x30 /* 48 */ +#define D1830_REG_ADC_LOW 0x31 /* 49 */ +#define D1830_REG_ADC_HIGH 0x32 /* 50 */ #define D1830_ADC_CH_VBAT 3 /* OSOS 439A98 case 1 */ #define D1830_ADC_START 0x10 #define D1830_ADC_SAMPLES 5 -#define D1830_ADC_FS_MV 6000 /* 10-bit, 6 V FS (emcore/Apple) */ +/* + * VBAT conversion, from the RetailOS formula rather than a full-scale + * guess: + * + * mv = (62 * raw + 207000) / 100 + * + * which reproduces its own reference points exactly -- raw 1823 gives + * 3200 mV, 2877 gives 3853, 3435 gives 4199. + * + * Two things follow. The transfer function has a 2070 mV offset, so + * treating the ADC as linear from zero was wrong; and its slope is + * 0.62 mV per count against the 1.47 mV per count that 6000/1023 + * implied, so the old conversion multiplied every LSB of ADC noise by + * more than twice as much as it should have. That is a large part of + * why vbat looked so unstable. + * + * The formula is written for a 12-bit raw. This ADC path assembles ten + * bits -- (r50 << 2) | (r49 & 3) -- so it is shifted up by two rather + * than the constants being rescaled, which keeps the published numbers + * checkable against the source they came from. + */ +/* + * sub_4234, the boot battery helper, and this is the conversion to use. + * Its raw assembly is (reg32 << 2) | reg31, which is exactly what this + * driver already reads, and its constants are explicit in the ARM: + * + * 0x3FF = 1023, 0x7D0 = 2000, 0x9C4 = 2500 + * mv = 2500 + raw * 2000 / 1023 + * + * so 0 is 2500 mV and 1023 is 4500 mV. + * + * This replaces (62 * raw12 + 207000) / 100, which came from the later + * twelve-bit averaging API. Both are real, but they are different + * paths, and ours assembles ten bits the way the boot helper does. The + * deciding argument is that the 3550 and 3400 thresholds below are + * compared against this function's output, so any other scale makes + * them mean something the firmware never intended. + */ +#define D1830_VBAT_BASE_MV 2500 +#define D1830_VBAT_SPAN_MV 2000 +#define D1830_VBAT_FULL_SCALE 1023 + +/* RetailOS sub_8005C314 warns and acts at 3.2 V. Reported, not acted on + * here: a driver that powers the machine off the instant a noisy read + * dips below a threshold is worse than one that reports it. */ +#define D1830_VBAT_LOW_MV 3200 #define D1830_DESIGN_UAH 200000 /* nano 7 pack, 200 mAh */ #define D1830_DESIGN_MIN_UV 3300000 #define D1830_DESIGN_MAX_UV 4200000 @@ -79,10 +226,28 @@ MODULE_PARM_DESC(verbose, "Verbose n31-pmic I2C/reg logging (default N; also gpi dev_dbg((dev), fmt, ##__VA_ARGS__); \ } while (0) -static bool allow_audio_rails = true; +/* + * Off by default, and it should stay that way: this replays the + * board-wide rail trim, whose ACTIVE_1 form is (old & 0x2F) | 0x10. + * That is correct at boot, but it clears bits 6 and 7, so running it + * again once the panel is up switches the display rail off. Measured on + * glass: ACTIVE_1 goes 0x7f -> 0x3f on the first codec prepare after + * boot, and the screen turns white. + * + * This comment used to end "the codec's analog supply is always on, so + * audio does not need this at all". That claim is unverified and should + * not be relied on -- but the correction attempted here, powering the + * codec from registers 20-23 bit 4, was also wrong and locked the + * kernel. Those registers hold a bare 5-bit voltage code with no enable + * bit; see the note above d1830_audio_rails(). Where the CS42L81 analog + * supply actually comes from is still an open question, and neither + * claim in this paragraph's history should be treated as settled. + */ +static bool allow_audio_rails; module_param(allow_audio_rails, bool, 0644); MODULE_PARM_DESC(allow_audio_rails, - "Apply sub_23EC LDO trim from d1830_audio_rails() (default on)"); + "Replay the whole board rail trim on codec prepare (default N; touches the display rail)"); + /* Off by default: false Sleep during NAND CS storms was cutting power. */ static bool sleep_poweroff; @@ -97,7 +262,40 @@ MODULE_PARM_DESC(sleep_poweroff, * produced the phantom "SLEEP PRESS r7=0x0e" that power-watch turned * into a poweroff mid-recover. Interrupt-driven by default. */ -static unsigned int btn_poll_ms; +/* + * On by default: without it the nIRQ latch is never released. Left as a + * knob only so the old behaviour can be reproduced when comparing. + */ +static bool pmic_irq; +module_param(pmic_irq, bool, 0444); +MODULE_PARM_DESC(pmic_irq, + "Request the PMIC nIRQ line (default N; polling is used)"); + +static bool ack_events = true; +module_param(ack_events, bool, 0644); +MODULE_PARM_DESC(ack_events, + "Write back the PMIC event latches to release nIRQ"); + +/* + * Polled by default. The PMIC nIRQ reaches the EIC, but the EIC's level + * behaviour is not yet pinned down, so this is what makes Home, Play and + * Sleep work today. 100 ms is well inside a keypress. + */ +/* + * On, and load-bearing. Do not turn this off. + * + * It was switched off once on the strength of the comment above + * d1830_trace_work() calling it "not a product poll", and Home, Play and + * Sleep stopped working immediately. The reason is in /proc/interrupts: + * there is no PMIC nIRQ line there at all. The EIC path this is nominally + * a fallback for does not deliver, so the poll is not backup -- it is the + * only thing turning a key press into an input event. + * + * The cost is real: ten I2C transactions a second for the life of the + * system. The fix is to make the PMIC interrupt work and then retire this, + * not to delete the mechanism that currently carries the buttons. + */ +static unsigned int btn_poll_ms = 100; module_param(btn_poll_ms, uint, 0644); MODULE_PARM_DESC(btn_poll_ms, "Fallback button poll period in ms (0=off, interrupt-only)"); @@ -129,7 +327,10 @@ struct d1830_gpio { u8 sleep_hold; int last_r5, last_r6, last_r7, last_r8; bool keys_inited; + /* Event-register reads serviced, for the input diagnostics. */ + unsigned int irq_events; bool lsb_logged; + int mv_filtered; int last_mv; u16 last_adc; u8 last_r48, last_r49, last_r50; @@ -222,7 +423,184 @@ static int d1830_gpio_parse_dt(struct d1830_gpio *gpio_dev) return 0; } -static void d1830_cut_power(struct i2c_client *client) +/* ------------------------------------------------------------------ */ +/* RTC */ +/* */ +/* The counter lives in the PMIC rather than in SoC MMIO. OSOS reads it */ +/* a byte at a time, least significant first, in sub_16517E: */ +/* */ +/* sub_41286E(a1, 124, v8); byte 0 */ +/* sub_41286E(a1, 125, (char *)v8 + 1); byte 1 */ +/* sub_41286E(a1, 126, (char *)v8 + 2); byte 2 */ +/* sub_41286E(a1, 127, (char *)v8 + 3); byte 3 */ +/* */ +/* and the bootloader's sub_FDE zeroes the same four, which is what you */ +/* would expect of a counter being reset rather than of scratch space. */ +/* */ +/* Honest limitation: OSOS passes the four bytes through sub_16CA5E */ +/* before using them, and that routine is a long bit-manipulation which */ +/* has not been decoded. This driver therefore treats the registers as */ +/* a plain little-endian seconds counter. Reads and writes are */ +/* self-consistent, so timekeeping across a reboot works; what is not */ +/* guaranteed is that the epoch agrees with RetailOS. Set the clock */ +/* once with hwclock and it will keep. */ +/* ------------------------------------------------------------------ */ + +/* + * Calendar registers, not the seconds counter this driver first used. + * + * 124 to 127 looked like a 32-bit counter because OSOS sub_16517E reads + * exactly those four LSB-first, and writing them round-tripped perfectly. + * They are MEMBYTE, 0x60 to 0x87, which is general purpose non-volatile + * scratch -- so that was our own value being read back, and OSOS keeps a + * timestamp there by convention rather than because it is the clock. + * + * The real block, confirmed against the part: R64 counts seconds and was + * observed ticking 0x23 to 0x26 across three seconds. + * + * 0x40 64 COUNT_SEC bits 5:0, bit 6 MONITOR + * 0x41 65 COUNT_MIN bits 5:0 + * 0x42 66 COUNT_HOUR bits 4:0 + * 0x43 67 COUNT_DAY bits 4:0, 1 based + * 0x44 68 COUNT_MONTH bits 3:0, 1 based + * 0x45 69 COUNT_YEAR bits 5:0, 0 is 2000 + * + * Reading COUNT_SEC latches the rest, and writing COUNT_YEAR commits + * them, so the order of access is part of the interface rather than a + * convenience. + * + * MONITOR is the part telling us whether it kept time: 0 means power was + * lost. On this unit it reads 0 with day and month at zero, which are not + * legal values -- the clock has been free-running since the factory and + * was never set. Reporting that as a date would be worse than refusing, + * so an unset clock returns -EINVAL and userspace can decide. + */ +#define D1830_RTC_SEC 0x40 +#define D1830_RTC_MIN 0x41 +#define D1830_RTC_HOUR 0x42 +#define D1830_RTC_DAY 0x43 +#define D1830_RTC_MONTH 0x44 +#define D1830_RTC_YEAR 0x45 +#define D1830_RTC_MONITOR BIT(6) +#define D1830_RTC_YEAR_BASE 100 /* tm_year for 2000 */ + +/* + * This uses MEMBYTE 124 to 127 rather than the hardware calendar, and the + * reason is worth writing down because the obvious reading of the + * register map does not survive contact with the part. + * + * The map has calendar at 0x40 to 0x45 and a 32-bit upcount at 0x4c to + * 0x4f, with MEMBYTE, general purpose non-volatile scratch, at 0x60 to + * 0x87. So 124 to 127 is scratch, and OSOS sub_16517E reading exactly + * those four LSB-first is OSOS keeping its own timestamp there by + * convention rather than reading a clock. + * + * Measured on this unit: + * + * R64 ticks -- observed 0x23 to 0x26 across three seconds -- and + * accepts writes to its seconds field and to bit 6. + * R69 accepts a year write: 0xf7 became 0x1a for 2026. + * R65 to R68 accept nothing. They kept a2 26 00 00 through single + * writes and through a six-register block write, and they do not + * advance on their own -- R65 held a2 across several minutes, so it is + * not running minutes either. + * The upcount at 0x4c did not advance across three seconds. + * + * Day and month therefore read zero, which is not a legal value, and + * rtc_valid_tm rightly refuses it. A calendar whose middle four + * registers cannot be set is not a clock this driver can offer, and + * guessing at another address for them would be inventing hardware. + * + * MEMBYTE does work, end to end and verified: writing 1787941200 read + * back as 2026-08-28 18:20:00, and the raw registers held 50 d1 91 6a, + * which is that value little-endian. It is battery-backed so it survives + * a reboot, and it is what OSOS itself uses. + * + * The limitation is real and not hidden: scratch does not tick, so time + * does not advance while the system is off. Userspace should write the + * clock on shutdown, which is what CONFIG_RTC_SYSTOHC does. Fixing this + * properly needs the addresses for minutes through month, which the + * evidence here does not supply. + */ +#define D1830_RTC_MEM_BASE 0x7c /* 124, inside MEMBYTE 0x60-0x87 */ +#define D1830_RTC_MEM_COUNT 4 + +static int d1830_rtc_read_time(struct device *dev, struct rtc_time *tm) +{ + struct i2c_client *client = to_i2c_client(dev); + u32 secs = 0; + int i, v; + + for (i = 0; i < D1830_RTC_MEM_COUNT; i++) { + v = i2c_smbus_read_byte_data(client, D1830_RTC_MEM_BASE + i); + if (v < 0) + return v; + secs |= (u32)(v & 0xff) << (8 * i); + } + rtc_time64_to_tm((time64_t)secs, tm); + return 0; +} + +static int d1830_rtc_set_time(struct device *dev, struct rtc_time *tm) +{ + struct i2c_client *client = to_i2c_client(dev); + time64_t secs = rtc_tm_to_time64(tm); + int i, ret; + + if (secs < 0 || secs > U32_MAX) + return -EINVAL; + + for (i = 0; i < D1830_RTC_MEM_COUNT; i++) { + ret = i2c_smbus_write_byte_data(client, D1830_RTC_MEM_BASE + i, + (u8)(((u32)secs >> (8 * i)) & 0xff)); + if (ret) + return ret; + } + return 0; +} + +static const struct rtc_class_ops d1830_rtc_ops = { + .read_time = d1830_rtc_read_time, + .set_time = d1830_rtc_set_time, +}; + +static int d1830_rtc_register(struct i2c_client *client) +{ + struct rtc_device *rtc; + + rtc = devm_rtc_allocate_device(&client->dev); + if (IS_ERR(rtc)) + return PTR_ERR(rtc); + + rtc->ops = &d1830_rtc_ops; + rtc->range_min = 0; + rtc->range_max = U32_MAX; + /* + * Four PMIC registers and nothing else: there is no alarm here. Say + * so, or registration goes looking for one -- __rtc_read_alarm walks + * forward from the current time hunting a valid match, which with a + * counter that reads zero means a long search over i2c inside probe, + * and the driver sits in Loading while it happens. + */ + clear_bit(RTC_FEATURE_ALARM, rtc->features); + + return devm_rtc_register_device(rtc); +} + +static void d1830_soc_reset_fallback(void) +{ + void __iomem *a, *b; + + a = ioremap(S5L8740_RESET_A_PHYS, 4); + b = ioremap(S5L8740_RESET_B_PHYS, 4); + if (a) + writel(S5L8740_RESET_MAGIC, a); + if (b) + writel(S5L8740_RESET_MAGIC, b); + /* No iounmap: this does not return if it works. */ +} + +static void d1830_cut_power(struct i2c_client *client, bool restart) { int v, ret; u8 out; @@ -230,9 +608,18 @@ static void d1830_cut_power(struct i2c_client *client) if (!client) return; - dev_emerg(&client->dev, "PMIC poweroff: reg %u |= 0x%02lx\n", + dev_emerg(&client->dev, "PMIC %s: reg %u |= 0x%02lx\n", + restart ? "restart" : "poweroff", D1830_REG_POWEROFF, D1830_POWEROFF_BIT); + /* + * Order matters and this half is the whole difference between the + * two: 73 must be settled before 13 is written, because 13 is what + * actually removes power and nothing runs afterwards. + */ + i2c_smbus_write_byte_data(client, D1830_REG_CLR_ON_CUT, 0); + i2c_smbus_write_byte_data(client, D1830_REG_RESTART, restart ? 1 : 0); + v = i2c_smbus_read_byte_data(client, D1830_REG_POWEROFF); out = (v < 0) ? (u8)D1830_POWEROFF_BIT : (u8)(v | D1830_POWEROFF_BIT); @@ -245,14 +632,105 @@ static void d1830_cut_power(struct i2c_client *client) } mdelay(100); + + /* + * Still here, so the PMIC did not take it. For a restart the SoC can + * still do the job; for a power-off there is no equivalent, and + * resetting instead would be worse than stopping -- with panic=-1 on + * the command line, returning from here reboots, which is the + * "shutdown rebooted the device" symptom rather than a fix for it. + */ + if (restart) { + dev_emerg(&client->dev, + "PMIC restart did not take; SoC reset\n"); + d1830_soc_reset_fallback(); + mdelay(100); + } else { + dev_emerg(&client->dev, + "PMIC poweroff did not take; halting\n"); + } while (1) cpu_relax(); } static void d1830_pm_power_off(void) { - d1830_cut_power(d1830_poweroff_client); + d1830_cut_power(d1830_poweroff_client, false); +} + +/* + * pm_power_off is the legacy global and mainline is migrating off it. + * The sys-off handler is the current interface, is properly scoped to + * this device, and unregisters itself, so the global is only kept as a + * fallback for the in-driver callers that still reference it. + */ +static int d1830_sys_off_handler(struct sys_off_data *data) +{ + d1830_cut_power(d1830_poweroff_client, false); + return NOTIFY_DONE; +} + +/* + * Without this, reboot fell through to whatever the architecture could + * manage on its own, which on this SoC is nothing reliable. Priority is + * high because the PMIC is the only thing here that genuinely restarts + * the machine. + */ +static int d1830_restart_handler(struct notifier_block *nb, + unsigned long mode, void *cmd) +{ + d1830_cut_power(d1830_poweroff_client, true); + return NOTIFY_DONE; +} + +static struct notifier_block d1830_restart_nb = { + .notifier_call = d1830_restart_handler, + .priority = 192, +}; + +/* + * Register window, for identifying this part against the DA9053 + * datasheet -- the closest public Dialog device. The layouts are + * similar but not known to be identical, and several things worth + * having (the RTC monitor bit, the alarm block, the charger) are only + * usable once the offset between the two is established rather than + * assumed. + * + * echo 108 24 > regs dump 24 registers from 108 + * cat regs + */ +static unsigned int d1830_regs_first = 108, d1830_regs_count = 24; + +static ssize_t regs_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + unsigned int i; + int len = 0, v; + + for (i = 0; i < d1830_regs_count && len < PAGE_SIZE - 24; i++) { + v = i2c_smbus_read_byte_data(client, d1830_regs_first + i); + len += scnprintf(buf + len, PAGE_SIZE - len, + "R%-3u = %02x\n", + d1830_regs_first + i, v < 0 ? 0 : v); + } + return len; +} + +static ssize_t regs_store(struct device *dev, struct device_attribute *a, + const char *buf, size_t count) +{ + unsigned int first, n; + + if (sscanf(buf, "%u %u", &first, &n) != 2) + return -EINVAL; + if (first > 255 || n == 0 || n > 64 || first + n > 256) + return -EINVAL; + d1830_regs_first = first; + d1830_regs_count = n; + return count; } +static DEVICE_ATTR_RW(regs); static ssize_t do_poweroff_store(struct device *dev, struct device_attribute *attr, @@ -262,7 +740,7 @@ static ssize_t do_poweroff_store(struct device *dev, if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') return -EINVAL; - d1830_cut_power(client); + d1830_cut_power(client, false); return count; } static DEVICE_ATTR_WO(do_poweroff); @@ -438,6 +916,78 @@ static void d1830_key_active_low(struct d1830_gpio *gpio_dev, unsigned int code, } } +/* + * Registers 5-8 are the event latches and 9-12 their masks, the usual + * layout for this PMIC family -- d1830_osos_nirq_mask() programs the + * masks. Event latches are write-1-to-clear, and nothing here was + * clearing them. + * + * That is fatal rather than untidy. nIRQ is requested level-low and + * one-shot, so an uncleared latch holds the line asserted: the handler + * returns, the line is still low, the interrupt fires again, and genirq + * eventually disables it as spurious. Buttons work once and then stop + * for the rest of the boot, which is exactly the reported symptom. + * + * Only bits that actually read as set are written back, so this cannot + * disturb a latch that armed between the read and the acknowledgement. + */ +/* + * Everything needed to tell a dead nIRQ from a dead button, without + * having to reflash to add a printk: the live event and mask registers, + * how many interrupts genirq has actually delivered, and whether it has + * given up on the line. + */ +static ssize_t buttons_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + struct d1830_gpio *g = i2c_get_clientdata(client); + int r5, r6, r7, r8, r9, r10, r11, r12; + + if (!g) + return -ENODEV; + r5 = i2c_smbus_read_byte_data(client, 5); + r6 = i2c_smbus_read_byte_data(client, 6); + r7 = i2c_smbus_read_byte_data(client, 7); + r8 = i2c_smbus_read_byte_data(client, 8); + r9 = i2c_smbus_read_byte_data(client, 9); + r10 = i2c_smbus_read_byte_data(client, 10); + r11 = i2c_smbus_read_byte_data(client, 11); + r12 = i2c_smbus_read_byte_data(client, 12); + + return sysfs_emit(buf, + "events r5=%02x r6=%02x r7=%02x r8=%02x\n" + "masks r9=%02x r10=%02x r11=%02x r12=%02x\n" + "home=%d sleep=%d play=%d (0 = pressed)\n" + "irq=%d serviced=%u ack_events=%d btn_poll_ms=%u\n" + "input=%s\n", + r5 & 0xff, r6 & 0xff, r7 & 0xff, r8 & 0xff, + r9 & 0xff, r10 & 0xff, r11 & 0xff, r12 & 0xff, + r7 < 0 ? -1 : !!(r7 & BIT(4)), + r7 < 0 ? -1 : !!(r7 & BIT(5)), + r8 < 0 ? -1 : !!(r8 & BIT(1)), + client->irq, g->irq_events, ack_events, btn_poll_ms, + g->input ? "registered" : "absent"); +} +static DEVICE_ATTR_RO(buttons); + +static void d1830_ack_events(struct i2c_client *client, + int r5, int r6, int r7, int r8) +{ + static const u8 regs[] = { 5, 6, 7, 8 }; + int vals[4] = { r5, r6, r7, r8 }; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(regs); i++) { + if (vals[i] <= 0) + continue; + if (i2c_smbus_write_byte_data(client, regs[i], (u8)vals[i])) + dev_warn_ratelimited(&client->dev, + "event ack r%u=0x%02x failed\n", + regs[i], vals[i]); + } +} + static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) { struct i2c_client *client = gpio_dev->client; @@ -451,6 +1001,10 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) if (r7 < 0) return; + if (ack_events) + d1830_ack_events(client, r5, r6, r7, r8); + gpio_dev->irq_events++; + /* OSOS sub_26520: Home=r7b4, Sleep=r7b5, Play=r8b1. */ home = !!(r7 & BIT(4)); sleep = !!(r7 & BIT(5)); @@ -537,7 +1091,7 @@ static void d1830_btn_poll_once(struct d1830_gpio *gpio_dev) if (pm_power_off) pm_power_off(); else - d1830_cut_power(client); + d1830_cut_power(client, false); } } } else { @@ -599,8 +1153,8 @@ static void d1830_confirm_work(struct work_struct *work) * 439A98 then >>2 — logged only. RetailOS UI cache at 0x891DB18 * is still unmapped. No writes to 87/88. Never write reg 13 here. */ -static int d1830_adc_once(struct d1830_gpio *gpio_dev, int *adc, - u8 *r48, u8 *r49, u8 *r50) +static int d1830_adc_once_ch(struct d1830_gpio *gpio_dev, u8 channel, + int *adc, u8 *r48, u8 *r49, u8 *r50) { struct i2c_client *client = gpio_dev->client; int cfg, hi, lo, i; @@ -617,19 +1171,31 @@ static int d1830_adc_once(struct d1830_gpio *gpio_dev, int *adc, return -EBUSY; } cfg = i2c_smbus_write_byte_data(client, D1830_REG_ADC_CFG, - (cfg & 0xF0) | D1830_ADC_CH_VBAT | + (cfg & 0xF0) | (channel & 0x0f) | D1830_ADC_START); if (cfg) return cfg; - for (i = 0; i < 5; i++) { + /* + * Wait for START to clear. It is the conversion-in-progress bit -- + * the busy check at the top of this function already treats it that + * way -- but this loop used to break while it was still set, so the + * data registers were read mid-conversion every time. Averaging five + * torn samples still gives a torn answer, which is why vbat swung + * 3325 to 4058 mV inside twenty seconds and capacity wandered + * between 2 and 24 percent. Nothing downstream of this could be + * trusted, including the charging state. + */ + for (i = 0; i < 10; i++) { usleep_range(1000, 1500); cfg = i2c_smbus_read_byte_data(client, D1830_REG_ADC_CFG); if (cfg < 0) return cfg; - if (cfg & D1830_ADC_START) + if (!(cfg & D1830_ADC_START)) break; } + if (cfg & D1830_ADC_START) + return -ETIMEDOUT; hi = i2c_smbus_read_byte_data(client, D1830_REG_ADC_HIGH); lo = i2c_smbus_read_byte_data(client, D1830_REG_ADC_LOW); @@ -640,14 +1206,22 @@ static int d1830_adc_once(struct d1830_gpio *gpio_dev, int *adc, *r48 = (u8)cfg; *r49 = (u8)lo; *r50 = (u8)hi; - /* 347E4: (reg50 << 2) | reg49. Mask 10-bit; low nibble is 2 LSBs. */ - *adc = ((hi << 2) | lo) & 0x3ff; + /* + * 347E4: (reg50 << 2) | reg49, where only the bottom two bits of + * reg49 are sample data. They were being OR'd in unmasked, so + * anything above bit 1 landed on top of bits belonging to reg50 -- + * reg49 was observed at 0x0e, which is about 60 LSBs of corruption, + * or roughly 300 mV across a 10-bit range. That is most of the swing + * that made vbat and capacity unusable. + */ + *adc = (((hi & 0xff) << 2) | (lo & 0x3)) & 0x3ff; return 0; } static int d1830_adc_to_mv(int adc) { - return (adc * D1830_ADC_FS_MV) / 1023; + return D1830_VBAT_BASE_MV + + (adc * D1830_VBAT_SPAN_MV) / D1830_VBAT_FULL_SCALE; } static int d1830_read_vbat(struct d1830_gpio *gpio_dev, int *mv) @@ -663,7 +1237,8 @@ static int d1830_read_vbat(struct d1830_gpio *gpio_dev, int *mv) } for (i = 0; i < D1830_ADC_SAMPLES; i++) { - ret = d1830_adc_once(gpio_dev, &adc, &r48, &r49, &r50); + ret = d1830_adc_once_ch(gpio_dev, D1830_ADC_CH_VBAT, &adc, + &r48, &r49, &r50); if (ret) { if (n) break; @@ -676,6 +1251,31 @@ static int d1830_read_vbat(struct d1830_gpio *gpio_dev, int *mv) return -EIO; adc = sum / n; *mv = d1830_adc_to_mv(adc); + + /* + * Smooth across calls, not just within one. + * + * The conversions are honest -- r48 shows START clear before each + * read and r49 never exceeds 2 -- but the raw high byte still ranges + * over a7 to d3 between samples, which is real movement on VBAT + * under a switching load rather than a decoding fault. The five + * samples above are taken back to back in a few milliseconds, so + * they all land inside the same transient and averaging them + * smooths nothing. That is what made vbat swing 700 mV and capacity + * wander between 2 and 100 percent, which in turn made the charging + * state meaningless. + * + * An exponential average over successive reads spans many + * transients instead of one. Weight is 1/4 new, giving a time + * constant of a few seconds at the half-second cache interval -- + * slow enough to be steady, quick enough to follow a real charge. + * The first reading seeds it rather than ramping up from zero. + */ + if (gpio_dev->mv_filtered) + gpio_dev->mv_filtered = (gpio_dev->mv_filtered * 3 + *mv) / 4; + else + gpio_dev->mv_filtered = *mv; + *mv = gpio_dev->mv_filtered; gpio_dev->last_adc = (u16)adc; gpio_dev->last_mv = *mv; gpio_dev->last_r48 = r48; @@ -703,6 +1303,14 @@ static int d1830_get_vbat_uV(struct d1830_gpio *gpio_dev, int *val) return 0; } +/* + * A voltage-curve estimate, not a fuel gauge. The hardware measures + * VBAT and nothing else -- no coulomb counter, no charge current, no + * temperature -- so this is an interpolation between two constants and + * should be read as one. It is exposed because a UI that shows nothing + * is worse than one showing an approximation, not because the number + * is measured. + */ static int d1830_get_capacity(struct d1830_gpio *gpio_dev, int *val) { int mv, pct, ret; @@ -735,105 +1343,719 @@ static ssize_t vbat_raw_show(struct device *dev, struct device_attribute *attr, gpio_dev->last_r48, gpio_dev->last_r49, gpio_dev->last_r50, gpio_dev->last_adc, mv); } -static DEVICE_ATTR_RO(vbat_raw); +static DEVICE_ATTR_RO(vbat_raw); + +static int d1830_psy_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct d1830_gpio *gpio_dev = power_supply_get_drvdata(psy); + int mv, pct, ret; + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return d1830_get_vbat_uV(gpio_dev, &val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + val->intval = D1830_DESIGN_MIN_UV; + return 0; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + val->intval = D1830_DESIGN_MAX_UV; + return 0; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + val->intval = D1830_DESIGN_UAH; + return 0; + case POWER_SUPPLY_PROP_CAPACITY: + return d1830_get_capacity(gpio_dev, &val->intval); + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + ret = d1830_get_capacity(gpio_dev, &pct); + if (ret) + return ret; + if (pct <= 5) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + else if (pct <= 15) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + else if (pct >= 95) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else if (pct >= 80) + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + else + val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + return 0; + case POWER_SUPPLY_PROP_STATUS: + /* + * UNKNOWN is the honest answer. Charge state needs a VBUS or + * charger status bit and neither is identified: TriStar's own + * VBUS task exists in OSOS but its register is not mapped, and + * apple_tristar_vbus() is still a stub. + * + * Guessing from voltage alone was actively misleading. It + * reported Full above 4150 mV, so a cell sitting high after a + * charge read as Full while unplugged, and ADC noise flipped it + * between Full and Discharging inside a few seconds. A field + * that changes meaning with noise is worse than one that admits + * it does not know. + */ + val->intval = POWER_SUPPLY_STATUS_UNKNOWN; + return 0; + case POWER_SUPPLY_PROP_HEALTH: + ret = d1830_read_vbat(gpio_dev, &mv); + if (ret) { + val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; + return 0; + } + if (mv < 3000) + val->intval = POWER_SUPPLY_HEALTH_DEAD; + else if (mv > 4300) + val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else + val->intval = POWER_SUPPLY_HEALTH_GOOD; + return 0; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + return 0; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + return 0; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + return 0; + default: + return -EINVAL; + } +} + +static enum power_supply_property d1830_psy_props[] = { + POWER_SUPPLY_PROP_STATUS, + POWER_SUPPLY_PROP_HEALTH, + POWER_SUPPLY_PROP_PRESENT, + POWER_SUPPLY_PROP_TECHNOLOGY, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_CAPACITY_LEVEL, + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, + POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, + POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_SCOPE, +}; + +/* ------------------------------------------------------------------ */ +/* Charger scaffold */ +/* */ +/* Deliberately inert. Every property here answers -ENODATA, and no */ +/* register is written, because not one charger register on this part is */ +/* proven: not VBUS presence, not enable, not input current limit, not */ +/* charge current or voltage, not termination, not fault, not battery */ +/* presence. */ +/* */ +/* What exists is the shape. When the registers are found, filling in */ +/* d1830_charger_regs below and the switch bodies is the whole job -- */ +/* the class device, the property list and the plumbing are already */ +/* here and already exercised. */ +/* */ +/* The reason this stops at the shape is asymmetry of consequence. A */ +/* wrong GPIO bit gives silence and you try the next one. A wrong */ +/* charger register pushes current into a soldered lithium cell with no */ +/* replaceable path, and the register map for this part has already been */ +/* revised three times -- a rail block at 0x40 that turned out to be a */ +/* counter, a calendar whose middle registers refuse writes, and a */ +/* seconds store that turned out to be scratch. A default-off flag */ +/* guards against accidents, not against the values being wrong. */ +/* */ +/* Enabling charger_enable does not make it write anything. It only */ +/* registers the class device so a consumer can be developed against it. */ +/* ------------------------------------------------------------------ */ + +/* + * Read-only telemetry for charger reverse engineering. + * + * Nothing here writes a charger register, because none is proven. What + * it does is make the part observable, which is the step that has to come + * first: the way to find the VBUS bit is to watch which bit moves when + * the cable state changes, not to guess an address. + * + * adc_channels sweep every ADC channel, not just VBAT + * charger_regs labelled dump of the candidate window + * charger_watch write 1 to snapshot, read to see what has changed + * + * Only channel 3 has a trustworthy interpretation today -- it is VBAT, + * via sub_8005C2C4 to sub_8005C618(3, ...). The others are read and + * reported as raw counts with no label, because plausible occupants + * include VBUS voltage, charge current and a thermistor, and writing any + * of those names into the output before correlating it would make a + * guess look like a measurement. + * + * The register window is bounded rather than a blind 0x00..0xff sweep. + * Some PMIC registers are clear-on-read or otherwise side-effecting -- + * the event latches in this same address space are write-one-to-clear -- + * so reading everything to see what happens is not free. + */ +#define D1830_ADC_CH_MAX 8 + +static ssize_t adc_channels_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct d1830_gpio *gpio_dev = dev_get_drvdata(dev); + unsigned int ch; + int len = 0; + + if (!gpio_dev) + return -ENODEV; + + for (ch = 0; ch < D1830_ADC_CH_MAX; ch++) { + u8 r48 = 0, r49 = 0, r50 = 0; + int adc = 0, ret; + + ret = d1830_adc_once_ch(gpio_dev, (u8)ch, &adc, + &r48, &r49, &r50); + if (ret) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "ch%u err=%d\n", ch, ret); + continue; + } + /* mV is only meaningful for channel 3; the rest are counts. */ + if (ch == D1830_ADC_CH_VBAT) + len += scnprintf(buf + len, PAGE_SIZE - len, + "ch%u raw=%4d r48=%02x r49=%02x r50=%02x mv=%d (VBAT)\n", + ch, adc, r48, r49, r50, + d1830_adc_to_mv(adc)); + else + len += scnprintf(buf + len, PAGE_SIZE - len, + "ch%u raw=%4d r48=%02x r49=%02x r50=%02x (unidentified)\n", + ch, adc, r48, r49, r50); + } + return len; +} +static DEVICE_ATTR_RO(adc_channels); + +/* + * The window worth watching. Deliberately excludes the event latches at + * 0x05..0x08, which are write-one-to-clear and whose read semantics are + * not established, and the hibernate block, which RetailOS only touches + * during power-state transitions. + */ +static const struct { + u8 first, count; + const char *what; +} d1830_charger_window[] = { + /* Non-overlapping: a register appearing twice would be counted + * twice in the snapshot and reported twice in a diff. */ + /* + * The event latches. Previously left out because their read + * semantics were unestablished, but the buttons path reads them + * continuously without harm, and the cable event turned up here -- + * bit 2 latched in both 0x05 and 0x06 -- so excluding them meant + * excluding the interesting part. Read only; nothing acknowledges. + */ + { 0x05, 4, "event latches (0x05 bit6 = VBUS present, bit2 = cable event)" }, + { 0x09, 4, "event masks" }, + { 0x0d, 5, "0x0d poweroff, 0x10-0x11 rail enables (0x10 bit5 = Nimbus, proven)" }, + { 0x14, 8, "bootloader LDO trims; 0x1a takes 0xb2, charge-adjacent" }, + { 0x23, 2, "touched by the bootloader trim sequence" }, + { 0x30, 4, "ADC config and result" }, + { 0x57, 2, "BT companion rails (proven via sub_51688C)" }, + /* + * The regions RetailOS only touches during power-state changes. + * Live and stable -- two consecutive reads returned identical + * values, so nothing here is clear-on-read -- and unmapped. + * + * Worth watching rather than naming. 0xc9..0xcc reads ff 0d ff 0d, + * which is 0x0dff twice as little-endian 16-bit, or 3583. That + * sits beside RetailOS's own 3550 and 3400 millivolt thresholds, + * and a duplicated voltage constant in a power block is the shape + * of a charge or recharge threshold. One reading is not a decode, + * so it goes in the capture window and gets a name only if it + * moves when the charging state does. + */ + { 0xa4, 1, "hibernate seq, RetailOS masks 0x3f" }, + { 0xaf, 9, "hibernate config 0xaf-0xb7, unmapped" }, + { 0xc0, 19, "hibernate config 0xc0-0xd2; 0xc9-0xcc looks like 3583 twice" }, +}; + +static ssize_t charger_regs_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + unsigned int i, j; + int len = 0, v; + + for (i = 0; i < ARRAY_SIZE(d1830_charger_window) && + len < PAGE_SIZE - 96; i++) { + len += scnprintf(buf + len, PAGE_SIZE - len, "# %s\n", + d1830_charger_window[i].what); + for (j = 0; j < d1830_charger_window[i].count && + len < PAGE_SIZE - 32; j++) { + u8 reg = d1830_charger_window[i].first + j; + + v = i2c_smbus_read_byte_data(client, reg); + len += scnprintf(buf + len, PAGE_SIZE - len, + "0x%02x = %02x\n", + reg, v < 0 ? 0 : v); + } + } + return len; +} +static DEVICE_ATTR_RO(charger_regs); + +/* + * Snapshot and diff. Write 1 to record the window, read to see only what + * moved since. This is the tool that actually finds a status bit: take a + * snapshot, change something in the world, read back, and the bits that + * changed are the short list. + */ +static u8 d1830_snap[64]; +static bool d1830_snap_valid; + +static unsigned int d1830_snap_fill(struct i2c_client *client, u8 *out) +{ + unsigned int i, j, n = 0; + + for (i = 0; i < ARRAY_SIZE(d1830_charger_window); i++) + for (j = 0; j < d1830_charger_window[i].count && + n < sizeof(d1830_snap); j++) { + int v = i2c_smbus_read_byte_data(client, + d1830_charger_window[i].first + j); + + out[n++] = (v < 0) ? 0 : (u8)v; + } + return n; +} + +static ssize_t charger_watch_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + u8 now[sizeof(d1830_snap)]; + unsigned int i, j, n = 0, changed = 0; + int len = 0; + + if (!d1830_snap_valid) + return sysfs_emit(buf, + "no snapshot; echo 1 > charger_watch first\n"); + + d1830_snap_fill(client, now); + for (i = 0; i < ARRAY_SIZE(d1830_charger_window); i++) + for (j = 0; j < d1830_charger_window[i].count && + n < sizeof(d1830_snap); j++, n++) { + u8 reg = d1830_charger_window[i].first + j; + + if (now[n] == d1830_snap[n]) + continue; + changed++; + if (len < PAGE_SIZE - 64) + len += scnprintf(buf + len, PAGE_SIZE - len, + "0x%02x %02x -> %02x (xor %02x) %s\n", + reg, d1830_snap[n], now[n], + d1830_snap[n] ^ now[n], + d1830_charger_window[i].what); + } + if (!changed) + len += scnprintf(buf + len, PAGE_SIZE - len, + "no change\n"); + return len; +} + +static ssize_t charger_watch_store(struct device *dev, + struct device_attribute *a, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev); + + if (buf[0] != '1') + return -EINVAL; + d1830_snap_fill(client, d1830_snap); + d1830_snap_valid = true; + return count; +} +static DEVICE_ATTR_RW(charger_watch); + +/* ------------------------------------------------------------------ */ +/* Charger state and RetailOS power init */ +/* ------------------------------------------------------------------ */ + +enum d1830_input_type { + D1830_INPUT_NONE, + D1830_INPUT_USB, + D1830_INPUT_ACCESSORY, + D1830_INPUT_UNKNOWN_VBUS, +}; + +struct d1830_charger_state { + bool vbus_known; /* false until a VBUS source is identified */ + bool vbus_present; + bool charging_enabled; + bool charging; + bool full; + int vbat_mv; + enum d1830_input_type input; +}; + +static struct d1830_charger_state d1830_chg; + +/* + * VBUS presence. + * + * Returns -ENODATA rather than a value, and that is the honest answer + * today: no D1830 status bit and no TriStar register has been identified + * as the VBUS indication. The existing USB supply reported ONLINE=1 + * unconditionally, which was a bring-up shortcut, and the note is right + * that it must not become the charging policy -- a charger that believes + * the cable is always attached will happily conclude it is charging while + * running the battery flat. + * + * When the bit is found, this is the only function that needs to change. + * Everything downstream already handles the unknown case explicitly + * rather than defaulting to a convenient answer. + */ +/* + * External power present, from sub_1E7C. Reads 0x05 bit 6. + * + * This is the one function the whole charger design was waiting on, and + * everything downstream was already written to consume it. What it + * reports is "a usable external source is available" -- the firmware + * proves that much and no more, so consumers must not upgrade it to + * "charging" on their own. + */ +static int d1830_vbus_present(struct i2c_client *client, bool *present) +{ + int v; + + if (!client) + return -ENODEV; + v = i2c_smbus_read_byte_data(client, D1830_REG_STATUS0); + if (v < 0) + return v; + *present = !!(v & D1830_STATUS_EXT_POWER); + return 0; +} + +/* + * sub_23EC, the RetailOS PMIC power initialisation, in its original order: + * + * program the board trims at 0x14..0x17 + * write 0x1a = 0xb2, twice + * 0x10: clear 0xd0, set 0x10, and set 0x20 on a cold boot + * 0x11: set 0x07 + * 0x13: set 0x02 + * + * Off by default, and the reason is specific rather than caution in + * general. On this device 0x10 currently reads 0x7f, and the RetailOS + * masking takes that to 0x3f -- which clears bit 6. Bit 6 is a live rail + * in our own table. RetailOS runs this at cold boot, before anything + * depends on those rails; running it from a module probe, with the + * display already up, would drop a rail out from under a running system. + * + * The 0x14..0x17 values are computed by RetailOS from board tables that + * have not been recovered, so this reproduces the write ordering and the + * masking, and deliberately leaves those four registers alone rather than + * inventing trim values. That is why it is called an init reproduction + * and not an equivalent. + * + * 0x1a already reads 0xb2 on this unit -- the bootloader wrote it -- so + * that part is a confirmation rather than a change. + */ +/* + * Off by default, and this one is a correction rather than caution. + * + * This write was enabled at probe while 0x1a was believed to be + * charger configuration. It is not: the OSOS rail setter writes it as + * (code & 0x1f) | 0xa0 with code = (mv - 1200) / 100, so 0xb2 is a + * 3.0 V rail. Writing a live rail register during probe on the + * strength of a label that turned out to be wrong is not something + * to leave on by default, and two boots failed to come up with it + * enabled. + * + * The value is what the bootloader already leaves there, so this is + * expected to be a no-op -- but expected is not the same as + * observed, and the cost of being wrong is a device that needs + * recovering by hand. + */ +static bool ldo_1a_write; +module_param(ldo_1a_write, bool, 0444); +MODULE_PARM_DESC(ldo_1a_write, + "Re-write the 0x1a rail voltage at probe (default N)"); + +static bool charger_hw_init; +module_param(charger_hw_init, bool, 0444); +MODULE_PARM_DESC(charger_hw_init, + "Replay the sub_23EC PMIC init (clears 0x10 bit 6; boot only)"); + +/* + * 0x1a is a rail voltage register, not charger configuration. + * + * The OSOS rail setter writes it as + * + * code = (millivolts - 1200) / 100 + * reg = (code & 0x1f) | 0xa0 + * + * and 0xb2 decodes exactly: 0xb2 & 0xe0 is 0xa0, matching the mask, + * and 0xb2 & 0x1f is 18, giving 1200 + 1800 = 3000 mV. The same + * function covers 0x14 through 0x22 with per-register bases and steps, + * which is also what the bootloader is doing when it programs + * 0x14..0x17 from board tables -- those are voltages. + * + * So writing 0xb2 here sets a 3.0 V rail. It is harmless because that + * is the value already present, but calling it CHG_CFG was wrong and + * would have sent the next person looking for a charger in the rail + * block. + */ +#define D1830_REG_LDO_1A 0x1a /* 26 */ +#define D1830_LDO_1A_STOCK 0xb2 /* 3000 mV: 0xa0 | ((3000-1200)/100) */ +#define D1830_REG_ACTIVE_1 0x10 /* 16 */ +#define D1830_REG_ACTIVE_2 0x11 /* 17 */ +#define D1830_REG_CTRL_13 0x13 /* 19 */ + +/* + * The charger-configuration half of sub_23EC, on its own: 0x1a takes + * 0xb2, written twice, exactly as both the bootloader and RetailOS do. + * The second write is not a different value -- the firmware reuses the + * same stack byte -- so this reproduces it rather than tidying it away. + * + * Safe to run, and enabled by default: this device already reads 0xb2 + * at 0x1a because the bootloader put it there, so this confirms the + * state rather than changing it, and it makes the driver correct on a + * path where the bootloader did not. + * + * What the individual bits of 0xb2 mean is not decoded. There is no + * bitwise read-modify-write of this register anywhere in the extracted + * firmware to give any bit an independent meaning, so the whole byte is + * written as a unit and no D1830_CHARGE_ENABLE is invented from it. + */ +static int d1830_charger_config(struct i2c_client *client) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, D1830_REG_LDO_1A, + D1830_LDO_1A_STOCK); + if (ret) + return ret; + return i2c_smbus_write_byte_data(client, D1830_REG_LDO_1A, + D1830_LDO_1A_STOCK); +} + +static int d1830_charger_hw_init(struct i2c_client *client, bool cold_boot) +{ + int v, ret; + + /* 0x14..0x17 are deliberately not written: the trim values come from + * board tables we have not recovered, and a wrong trim is worse than + * whatever the bootloader already left there. */ + + ret = d1830_charger_config(client); + if (ret) + return ret; + + v = i2c_smbus_read_byte_data(client, D1830_REG_ACTIVE_1); + if (v < 0) + return v; + v &= ~0xd0; + v |= 0x10; + if (cold_boot) + v |= 0x20; + ret = i2c_smbus_write_byte_data(client, D1830_REG_ACTIVE_1, (u8)v); + if (ret) + return ret; + + v = i2c_smbus_read_byte_data(client, D1830_REG_ACTIVE_2); + if (v < 0) + return v; + ret = i2c_smbus_write_byte_data(client, D1830_REG_ACTIVE_2, + (u8)(v | 0x07)); + if (ret) + return ret; + + v = i2c_smbus_read_byte_data(client, D1830_REG_CTRL_13); + if (v < 0) + return v; + return i2c_smbus_write_byte_data(client, D1830_REG_CTRL_13, + (u8)(v | 0x02)); +} + +/* + * Deliberately absent: d1830_charger_set_enabled(). + * + * 0x1a = 0xb2 is proven as configuration written during init, twice, by + * both the bootloader and RetailOS. It is not proven to be the bit that + * starts and stops charging at runtime, and nothing observed so far shows + * RetailOS toggling it dynamically. Writing a guessed enable would be the + * one mistake in this driver that damages hardware rather than annoying + * the user, so the dynamic enable stays unimplemented until the bit is + * identified. + */ + +/* + * The policy, written now so that identifying VBUS is the only remaining + * step rather than the start of a design. Note what it does not do: it + * never keys off USB enumeration. VBUS present, host detected and gadget + * configured are three different things -- a wall charger gives VBUS with + * no enumeration at all -- so gating charge on enumeration would refuse to + * charge from exactly the source most likely to be attached. + */ +static void d1830_charger_update(struct i2c_client *client) +{ + struct d1830_gpio *gpio_dev = i2c_get_clientdata(client); + bool present = false; + int mv = 0; + + if (d1830_vbus_present(client, &present)) { + d1830_chg.vbus_known = false; + d1830_chg.input = D1830_INPUT_NONE; + d1830_chg.charging = false; + d1830_chg.full = false; + } else { + d1830_chg.vbus_known = true; + d1830_chg.vbus_present = present; + /* + * UNKNOWN_VBUS, not USB. 0x05 bit 6 says an external source + * is available; it does not say what kind. Classifying the + * attachment is TriStar's job and is not wired up yet. + */ + d1830_chg.input = present ? D1830_INPUT_UNKNOWN_VBUS : + D1830_INPUT_NONE; + /* + * Charging stays false even with external power. Present and + * delivering current are different claims, and no bit + * separating input-present from charger-active, CV phase or + * complete has been isolated. Reporting CHARGING here would be + * the same category of error as the old voltage heuristic. + */ + d1830_chg.charging = false; + d1830_chg.full = false; + } + + if (gpio_dev && !d1830_read_vbat(gpio_dev, &mv)) + d1830_chg.vbat_mv = mv; +} -static int d1830_psy_get_property(struct power_supply *psy, - enum power_supply_property psp, - union power_supply_propval *val) -{ - struct d1830_gpio *gpio_dev = power_supply_get_drvdata(psy); - int mv, pct, ret; +static bool charger_enable; +module_param(charger_enable, bool, 0444); +MODULE_PARM_DESC(charger_enable, + "Register the charger class device (properties still unproven)"); + +/* +* Fill these in as they are established, one line per proven register, +* with the evidence in the comment. An entry that is still zero means +* nobody has proven it, and the property stays -ENODATA. +*/ +struct d1830_charger_reg { + const char *what; + u8 reg; /* 0 = not identified */ + u8 mask; +}; + +static const struct d1830_charger_reg d1830_charger_regs[] = { + { "vbus_present", 0, 0 }, + { "charge_enable", 0, 0 }, + { "input_current_limit", 0, 0 }, + { "charge_current", 0, 0 }, + { "charge_voltage", 0, 0 }, + { "term_current", 0, 0 }, + { "charge_status", 0, 0 }, + { "fault", 0, 0 }, +}; +static int d1830_charger_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ switch (psp) { - case POWER_SUPPLY_PROP_VOLTAGE_NOW: - return d1830_get_vbat_uV(gpio_dev, &val->intval); - case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: - val->intval = D1830_DESIGN_MIN_UV; - return 0; - case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: - val->intval = D1830_DESIGN_MAX_UV; - return 0; - case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: - val->intval = D1830_DESIGN_UAH; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; return 0; - case POWER_SUPPLY_PROP_CAPACITY: - return d1830_get_capacity(gpio_dev, &val->intval); - case POWER_SUPPLY_PROP_CAPACITY_LEVEL: - ret = d1830_get_capacity(gpio_dev, &pct); - if (ret) - return ret; - if (pct <= 5) - val->intval = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; - else if (pct <= 15) - val->intval = POWER_SUPPLY_CAPACITY_LEVEL_LOW; - else if (pct >= 95) - val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; - else if (pct >= 80) - val->intval = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; - else - val->intval = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + case POWER_SUPPLY_PROP_ONLINE: + if (!d1830_chg.vbus_known) + return -ENODATA; + val->intval = d1830_chg.vbus_present ? 1 : 0; return 0; case POWER_SUPPLY_PROP_STATUS: - ret = d1830_read_vbat(gpio_dev, &mv); - if (ret) { - val->intval = POWER_SUPPLY_STATUS_UNKNOWN; - return 0; - } - /* USB gadget is the only supply we have; no charge-bit RE. */ - if (mv >= 4150) - val->intval = POWER_SUPPLY_STATUS_FULL; - else + if (!d1830_chg.vbus_known) + return -ENODATA; + /* + * NOT_CHARGING rather than CHARGING when external power is + * present. charging and full are only ever set from a proven + * status bit, and none is identified, so this reports the one + * thing that is known: a source is or is not attached. + */ + if (!d1830_chg.vbus_present) val->intval = POWER_SUPPLY_STATUS_DISCHARGING; - return 0; - case POWER_SUPPLY_PROP_HEALTH: - ret = d1830_read_vbat(gpio_dev, &mv); - if (ret) { - val->intval = POWER_SUPPLY_HEALTH_UNKNOWN; - return 0; - } - if (mv < 3000) - val->intval = POWER_SUPPLY_HEALTH_DEAD; - else if (mv > 4300) - val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; + else if (d1830_chg.full) + val->intval = POWER_SUPPLY_STATUS_FULL; + else if (d1830_chg.charging) + val->intval = POWER_SUPPLY_STATUS_CHARGING; else - val->intval = POWER_SUPPLY_HEALTH_GOOD; - return 0; - case POWER_SUPPLY_PROP_PRESENT: - val->intval = 1; - return 0; - case POWER_SUPPLY_PROP_TECHNOLOGY: - val->intval = POWER_SUPPLY_TECHNOLOGY_LION; - return 0; - case POWER_SUPPLY_PROP_SCOPE: - val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING; return 0; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + /* Register not identified. Saying so beats a number. */ + return -ENODATA; default: return -EINVAL; } } -static enum power_supply_property d1830_psy_props[] = { +static enum power_supply_property d1830_charger_props[] = { + POWER_SUPPLY_PROP_ONLINE, POWER_SUPPLY_PROP_STATUS, - POWER_SUPPLY_PROP_HEALTH, - POWER_SUPPLY_PROP_PRESENT, - POWER_SUPPLY_PROP_TECHNOLOGY, - POWER_SUPPLY_PROP_CAPACITY, - POWER_SUPPLY_PROP_CAPACITY_LEVEL, - POWER_SUPPLY_PROP_VOLTAGE_NOW, - POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN, - POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN, - POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN, + POWER_SUPPLY_PROP_CHARGE_TYPE, + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, + POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE, + POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT, POWER_SUPPLY_PROP_SCOPE, }; +static const struct power_supply_desc d1830_charger_desc = { + .name = "d1830-charger", + .type = POWER_SUPPLY_TYPE_USB, + .properties = d1830_charger_props, + .num_properties = ARRAY_SIZE(d1830_charger_props), + .get_property = d1830_charger_get_property, +}; + +static void d1830_charger_register(struct device *dev) +{ + struct power_supply_config cfg = { }; + struct power_supply *psy; + unsigned int i, known = 0; + + if (!charger_enable) + return; + + for (i = 0; i < ARRAY_SIZE(d1830_charger_regs); i++) + if (d1830_charger_regs[i].reg) + known++; + + psy = devm_power_supply_register(dev, &d1830_charger_desc, &cfg); + if (IS_ERR(psy)) { + dev_warn(dev, "charger scaffold not registered: %ld\n", + PTR_ERR(psy)); + return; + } + dev_info(dev, + "charger scaffold registered: %u/%zu registers proven, all properties -ENODATA\n", + known, ARRAY_SIZE(d1830_charger_regs)); +} + static int d1830_usb_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) { switch (psp) { case POWER_SUPPLY_PROP_ONLINE: - /* Gadget host is the only path this image runs. */ - val->intval = 1; + /* + * Was hardcoded to 1. That was a bring-up shortcut and it + * cannot be the charging policy: a supply that claims to be + * online whether or not a cable is attached will report + * charging while the battery drains. No VBUS source is + * identified, so -ENODATA is the true answer until + * d1830_vbus_present() can give one. + */ + if (!d1830_chg.vbus_known) + return -ENODATA; + val->intval = d1830_chg.vbus_present ? 1 : 0; return 0; case POWER_SUPPLY_PROP_USB_TYPE: val->intval = POWER_SUPPLY_USB_TYPE_SDP; @@ -1530,7 +2752,18 @@ static void n31_pmu_rail_exit(void) /* path is left for when nothing is playing. */ /* ------------------------------------------------------------------ */ -static bool screen_sleep_enable = true; +/* + * Off by default. Twice now a power press has left the panel stuck -- + * image still on screen, no fade, and no wake from a second press -- + * needing DFU to recover. The transition has never been driven + * deliberately, only caught by accident, so arming it on every boot + * risks the display on a device whose only recovery is a reflash. + * + * The mechanism is still there and still wired: set screen_sleep_enable=1 + * to test it on purpose, with the console reachable, rather than + * discovering it by pressing a button. + */ +static bool screen_sleep_enable; module_param(screen_sleep_enable, bool, 0644); MODULE_PARM_DESC(screen_sleep_enable, "Short press toggles screen sleep (default Y)"); @@ -1567,6 +2800,15 @@ static bool n31_audio_active(void) return r; } +/* + * Sleeping the panel is only safe if we can also bring it back. These + * are resolved through __symbol_get, so a provider that is not loaded + * silently does half the job: the panel goes off with no fade, which + * looks like a lockup, or the backlight comes up over a panel that is + * still off, which looks like a device that will not wake. Neither is + * distinguishable from a crash at the time, so refuse to start a + * transition we cannot finish and say why. + */ static void n31_screen_set(bool asleep) { int (*fade)(int, unsigned int); @@ -1580,6 +2822,19 @@ static void n31_screen_set(bool asleep) fade = (int (*)(int, unsigned int))__symbol_get("n31_backlight_fade"); level = (int (*)(void))__symbol_get("n31_backlight_level"); + lcd = (int (*)(bool))__symbol_get("n31_lcd_power"); + + if (asleep && !lcd && !fade) { + /* + * Nothing to dim and nothing to blank. Going "asleep" here + * would only set a flag that stops the next press waking + * anything, which is worse than staying awake. + */ + if (level) + __symbol_put("n31_backlight_level"); + pr_warn("n31: screen sleep skipped -- no backlight or LCD provider\n"); + goto out; + } if (asleep) { if (level) { @@ -1596,18 +2851,12 @@ static void n31_screen_set(bool asleep) __symbol_put("n31_touch_suspend"); } /* Panel last, once nothing is drawing to it. */ - lcd = (int (*)(bool))__symbol_get("n31_lcd_power"); - if (lcd) { + if (lcd) lcd(false); - __symbol_put("n31_lcd_power"); - } } else { /* Panel first: it has to be scanning before the light comes up. */ - lcd = (int (*)(bool))__symbol_get("n31_lcd_power"); - if (lcd) { + if (lcd) lcd(true); - __symbol_put("n31_lcd_power"); - } touch = (int (*)(void))__symbol_get("n31_touch_resume"); if (touch) { touch(); @@ -1623,14 +2872,40 @@ static void n31_screen_set(bool asleep) } } + if (lcd) + __symbol_put("n31_lcd_power"); + n31_screen_asleep = asleep; - pr_info("n31: screen %s (audio %s)\n", + pr_info("n31: screen %s (audio %s, bl=%s lcd=%s)\n", asleep ? "asleep" : "awake", - n31_audio_active() ? "active" : "idle"); + n31_audio_active() ? "active" : "idle", + fade ? "yes" : "no", lcd ? "yes" : "no"); out: mutex_unlock(&n31_screen_lock); } +/* + * The transition takes backlight_fade_ms either way and holds + * n31_screen_lock while it does. Called straight from the button path + * that runs it, that stalls every other button for the length of the + * fade -- so a press during it appears to do nothing, which reads as a + * hang rather than a fade. Push it to a workqueue and let the poller + * carry on. + */ +static bool n31_screen_want_asleep; + +static void n31_screen_work_fn(struct work_struct *work) +{ + n31_screen_set(READ_ONCE(n31_screen_want_asleep)); +} +static DECLARE_WORK(n31_screen_worker, n31_screen_work_fn); + +static void n31_screen_request(bool asleep) +{ + WRITE_ONCE(n31_screen_want_asleep, asleep); + schedule_work(&n31_screen_worker); +} + bool n31_screen_is_asleep(void) { return n31_screen_asleep; @@ -1679,7 +2954,7 @@ static void n31_power_button(bool pressed) * A press while asleep only wakes; it should not immediately put the * screen back down. */ - n31_screen_set(!n31_screen_asleep); + n31_screen_request(!n31_screen_asleep); } /* Long holds must act while the button is still down, not on release. */ @@ -1702,7 +2977,207 @@ static void n31_power_button_poll(void) static void n31_home_button(bool pressed) { if (pressed && n31_screen_asleep) - n31_screen_set(false); + n31_screen_request(false); +} + +/* ------------------------------------------------------------------ */ +/* Regulator provider */ +/* */ +/* The rails already have a refcounted in-kernel API, but nothing gave */ +/* userspace or other drivers the conventional view of them. Exposing */ +/* them as regulators means /sys/class/regulator carries the real names */ +/* and voltages, and a consumer can be wired up in DT like any other */ +/* board. */ +/* */ +/* Enable and disable route through n31_pmu_rail_get/put rather than */ +/* touching the ACTIVE register directly, so the regulator count and */ +/* the driver-internal holders cannot disagree about who wants a rail */ +/* on -- which is exactly the confusion that let the display rail be */ +/* dropped underneath a live panel. */ +/* ------------------------------------------------------------------ */ + +struct n31_pmu_reg { + struct regulator_desc desc; + char name[16]; + unsigned int id; +}; + +static struct n31_pmu_reg n31_pmu_regs[ARRAY_SIZE(n31_pmu_rails)]; + +static int n31_pmu_reg_enable(struct regulator_dev *rdev) +{ + unsigned int id = rdev_get_id(rdev); + + if (!allow_pmu_writes) + return -EPERM; + return n31_pmu_rail_get(id); +} + +static int n31_pmu_reg_disable(struct regulator_dev *rdev) +{ + unsigned int id = rdev_get_id(rdev); + + if (!allow_pmu_writes) + return -EPERM; + n31_pmu_rail_put(id); + return 0; +} + +/* + * Report what the hardware says rather than what this driver last asked + * for: the rail may have been brought up by the bootloader, and a rail + * that is already on is the normal case at probe. + */ +static int n31_pmu_reg_is_enabled(struct regulator_dev *rdev) +{ + unsigned int id = rdev_get_id(rdev); + const struct n31_pmu_rail *r; + struct i2c_client *client = d1830_poweroff_client; + int v; + + if (id >= ARRAY_SIZE(n31_pmu_rails) || !client) + return -ENODEV; + r = &n31_pmu_rails[id]; + v = i2c_smbus_read_byte_data(client, r->active_reg); + if (v < 0) + return v; + return !!(v & r->active_mask); +} + +static int n31_pmu_reg_get_voltage(struct regulator_dev *rdev) +{ + unsigned int id = rdev_get_id(rdev); + const struct n31_pmu_rail *r; + struct i2c_client *client = d1830_poweroff_client; + int v; + + if (id >= ARRAY_SIZE(n31_pmu_rails) || !client) + return -ENODEV; + r = &n31_pmu_rails[id]; + if (r->vsel == N31_PMU_NO_VSEL || !r->step_mv) + return -EINVAL; + v = i2c_smbus_read_byte_data(client, r->vsel); + if (v < 0) + return v; + /* Low bits select the step; the upper bits are trim/control. */ + return (r->base_mv + (v & 0x1f) * r->step_mv) * 1000; +} + +static const struct regulator_ops n31_pmu_reg_ops = { + .enable = n31_pmu_reg_enable, + .disable = n31_pmu_reg_disable, + .is_enabled = n31_pmu_reg_is_enabled, + .get_voltage = n31_pmu_reg_get_voltage, +}; + +#define D1830_BT_REG_A 0x57 /* 87 */ +#define D1830_BT_REG_B 0x58 /* 88 */ +#define D1830_BT_A_MASK 0xc0 /* bits 7:6 */ +#define D1830_BT_B_MASK 0x71 /* bit 0 and bits 6:4 */ + +/* Defined below; the regulator ops need it before its definition. */ +int d1830_bt_rails(bool on); + +/* + * The Bluetooth rail, as a regulator. + * + * It does not fit n31_pmu_rails[]: that table describes single-register + * LDOs at 0x17..0x21, and this one is two registers -- 0x57 bits 7:6 and + * 0x58 bit 0 plus bits 6:4. So it gets its own descriptor whose enable and + * disable defer to d1830_bt_rails(), which already knows the sequence. + * + * The point of exposing it at all is ordering. bcm2078-bt is built into + * the kernel and probes around t=2.4s; this driver is a module userspace + * loads at about t=7.1s. A driver asking for power through a bespoke hook + * in that window gets -ENODEV and has no way to wait, so the controller + * simply never came up. Asking for it as a regulator makes the kernel do + * the waiting: devm_regulator_get() returns -EPROBE_DEFER until this + * driver registers, and the consumer is re-probed afterwards. + * + * of_match plus regulators_node is what lets a device tree node name it, + * which is the whole mechanism -- without those it is registered but + * unreachable from DT. + */ +static int n31_bt_reg_enable(struct regulator_dev *rdev) +{ + return d1830_bt_rails(true); +} + +static int n31_bt_reg_disable(struct regulator_dev *rdev) +{ + return d1830_bt_rails(false); +} + +static int n31_bt_reg_is_enabled(struct regulator_dev *rdev) +{ + struct i2c_client *client = d1830_poweroff_client; + int a; + + if (!client) + return 0; + a = i2c_smbus_read_byte_data(client, D1830_BT_REG_A); + if (a < 0) + return a; + return (a & D1830_BT_A_MASK) ? 1 : 0; +} + +static const struct regulator_ops n31_bt_reg_ops = { + .enable = n31_bt_reg_enable, + .disable = n31_bt_reg_disable, + .is_enabled = n31_bt_reg_is_enabled, +}; + +static const struct regulator_desc n31_bt_reg_desc = { + .name = "bt", + .of_match = "bt", + .regulators_node = "regulators", + .id = 0x100, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .ops = &n31_bt_reg_ops, + .n_voltages = 1, +}; + +static void n31_pmu_regulators_register(struct device *dev) +{ + struct regulator_config cfg = { }; + struct regulator_dev *rdev; + unsigned int i; + + cfg.dev = dev; + for (i = 0; i < ARRAY_SIZE(n31_pmu_rails); i++) { + struct n31_pmu_reg *pr = &n31_pmu_regs[i]; + + strscpy(pr->name, n31_pmu_rails[i].name, sizeof(pr->name)); + pr->id = i; + pr->desc.name = pr->name; + pr->desc.id = i; + pr->desc.type = REGULATOR_VOLTAGE; + pr->desc.owner = THIS_MODULE; + pr->desc.ops = &n31_pmu_reg_ops; + pr->desc.n_voltages = 1; + + cfg.driver_data = pr; + rdev = devm_regulator_register(dev, &pr->desc, &cfg); + if (IS_ERR(rdev)) + dev_warn(dev, "regulator %s: %ld\n", + pr->name, PTR_ERR(rdev)); + } + { + struct regulator_config btcfg = { }; + struct regulator_dev *btrdev; + + btcfg.dev = dev; + btrdev = devm_regulator_register(dev, &n31_bt_reg_desc, &btcfg); + if (IS_ERR(btrdev)) + dev_warn(dev, "bt regulator: %ld\n", PTR_ERR(btrdev)); + else + dev_info(dev, "bt rail exposed as a regulator\n"); + } + + dev_info(dev, "%u PMU rails exposed as regulators (writes %s)\n", + (unsigned int)ARRAY_SIZE(n31_pmu_rails), + allow_pmu_writes ? "allowed" : "blocked"); } static void n31_pmu_debugfs_init(void) @@ -1763,6 +3238,111 @@ static void d1830_log_audio_regs(struct i2c_client *client, const char *tag) * OSOS sub_20766(1) → 439B00(1) → 6644(4) → 7484(pmic, 9, on): * RMW D1830 register 16 bit 5. Targeted Nimbus rail — not the SEC seq. */ +/* + * Bluetooth companion rails. + * + * sub_51688C, the de-init half of the Bluetooth bring-up, zeroes two + * entries through sub_158C82 -- a different accessor from the sub_7484 + * rail toggler, writing multi-bit fields rather than a single enable: + * + * index 3: reg 87 bits 7:6, and reg 88 bit 0 + * index 5: reg 88 bits 6:4 + * + * Nothing in the init path sets them, so their running values come from + * the bootloader. Clearing them without keeping the originals would mean + * Bluetooth could be turned off exactly once per boot and never restored, + * so the first power-off saves what it found and power-on puts it back. + */ + +static u8 d1830_bt_saved_a, d1830_bt_saved_b; +static bool d1830_bt_saved; + +int d1830_bt_rails(bool on) +{ + struct i2c_client *client = d1830_poweroff_client; + int a, b, ret; + + if (!client) + return -ENODEV; + + if (on) { + if (!d1830_bt_saved) + return 0; /* never turned off; leave boot state alone */ + ret = d1830_rmw(client, D1830_BT_REG_A, D1830_BT_A_MASK, + d1830_bt_saved_a & D1830_BT_A_MASK); + if (!ret) + ret = d1830_rmw(client, D1830_BT_REG_B, D1830_BT_B_MASK, + d1830_bt_saved_b & D1830_BT_B_MASK); + d1830_vinfo(&client->dev, + "bt rails restored r87=%02x r88=%02x ret=%d\n", + d1830_bt_saved_a, d1830_bt_saved_b, ret); + return ret; + } + + a = i2c_smbus_read_byte_data(client, D1830_BT_REG_A); + if (a < 0) + return a; + b = i2c_smbus_read_byte_data(client, D1830_BT_REG_B); + if (b < 0) + return b; + if (!d1830_bt_saved) { + d1830_bt_saved_a = (u8)a; + d1830_bt_saved_b = (u8)b; + d1830_bt_saved = true; + } + ret = d1830_rmw(client, D1830_BT_REG_A, D1830_BT_A_MASK, 0); + if (!ret) + ret = d1830_rmw(client, D1830_BT_REG_B, D1830_BT_B_MASK, 0); + d1830_vinfo(&client->dev, + "bt rails off (was r87=%02x r88=%02x) ret=%d\n", + a, b, ret); + return ret; +} +EXPORT_SYMBOL_GPL(d1830_bt_rails); + +/* bcm2078-bt is built in and cannot link against this module, so it + * publishes a hook and we fill it in. */ +void bcm2078_register_bt_rails(int (*fn)(bool on)); + +/* + * Read PMIC register 0x51 during touch bring-up. + * + * OSOS does this in sub_20E94, via sub_26144(...,8) -> sub_41286E(81), + * between the 26494 probe and the download loop. The value it reads is + * not used afterwards, which is exactly why it was skipped here for so + * long -- and that reasoning was wrong. A register read is a bus + * transaction, and plenty of PMIC registers do something when they are + * addressed: clear a latched status, sample an input, arm a state + * machine. The decompiler can show the returned value going nowhere and + * still tell us nothing about what the read did to the part. + * + * 0x51 is inside the PMIC GPIO block (0x50..0x57). Whether this one + * latches anything is unknown; what is known is that stock performs it + * on every touch bring-up and we did not. Reproduce the sequence, + * including the parts whose purpose is not obvious yet. + */ +int d1830_touch_bringup_read(void) +{ + struct i2c_client *client = d1830_poweroff_client; + int v; + + if (!client) + return -ENODEV; + + v = i2c_smbus_read_byte_data(client, 0x51); + if (v < 0) { + dev_warn(&client->dev, + "n31-pmic: touch bring-up read of 0x51 failed: %d\n", + v); + return v; + } + + d1830_vinfo(&client->dev, + "n31-pmic: touch bring-up read 0x51 = 0x%02x\n", v); + return v; +} +EXPORT_SYMBOL_GPL(d1830_touch_bringup_read); + int d1830_nimbus_rail(bool on) { struct i2c_client *client = d1830_poweroff_client; @@ -1825,11 +3405,23 @@ static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) if (!boot_mode) r16 |= 0x20; /* - * The boot form of this write clears bits 6 and 7, which is correct - * at boot but would drop a rail a driver is currently holding. Put - * those back before writing. + * The boot form of this write clears bits 6 and 7 -- display and + * accessory. That is correct exactly once, on a cold boot, when + * neither is up yet. Replaying it later switches off a panel that + * is already lit, which is what the white screen was. + * + * Two guards, because one is not enough. Restoring rails a driver + * holds only works if a driver actually claimed one, and the + * display is the case where none did: the panel arrives already + * running from the bootloader handoff, and the DRM driver is + * built in, so it probes long before this module exists and its + * claim cannot reach us. So also refuse to clear any of those two + * bits that the hardware currently has set. Turning a live rail + * off is never what this sequence is for. */ r16 |= n31_pmu_rail_held_mask(0x10); + if (!apply_boot_rails) + r16 |= (u8)(v21 & 0xc0); d1830_write8(client, 16, r16); d1830_rmw(client, 17, 0, 0x07); @@ -1839,6 +3431,29 @@ static int d1830_sec_trim_seq(struct i2c_client *client, u8 boot_mode) return 0; } +/* + * There is no "analog LDO enable" at registers 20-23 (0x14-0x17). + * + * A note in our Rockbox placeholder claimed the codec analog side was + * powered by "regs 21-23 bit 4", and acting on it locked the kernel on + * boot. The OSOS rail setter settles it: registers 20-23 take LABEL_25, + * which computes the code at a 25 mV step and writes (code & 0x1F) -- + * the low five bits and nothing else. Contrast case 17/18, registers 46 + * and 47, which deliberately preserve (old & 0xE0); 20-23 preserve + * nothing, because there is nothing up there to preserve. Register 0x1a + * is different again: it alone uses (code & 0x1F) | 0xA0 at a 100 mV + * step, which is where the idea of an enable pattern came from. + * + * So bit 4 in these registers is the top bit of a voltage field, not a + * switch. Setting it on 0x15-0x17, which read 0x09 on this unit, moved + * three rails by +16 steps -- 400 mV each, simultaneously -- and the + * device did not survive it. + * + * If the codec analog supply is gated somewhere, it is not here. Whoever + * picks this up next: establish which rail actually feeds the CS42L81 + * analog stage before writing anything in this range. + */ + /* * sub_23EC analog-adjacent trim only. Called from CS42 prepare. * Does not run hibernate cookie / POWEROFF / charger 0xB2. @@ -1850,9 +3465,10 @@ int d1830_audio_rails(void) if (!client) return -ENODEV; + if (!allow_audio_rails) { - d1830_vinfo(&client->dev, "n31-pmic: audio rails skipped (allow_audio_rails=0)\n"); - d1830_log_audio_regs(client, "audio-skip"); + d1830_vinfo(&client->dev, + "n31-pmic: board rail trim skipped (allow_audio_rails=0)\n"); return 0; } @@ -1946,6 +3562,9 @@ static int d1830_gpio_probe(struct i2c_client *client) return ret; d1830_poweroff_client = client; + /* bcm2078-bt is built in; hand it our rail control now that we have + * a client to talk to. */ + bcm2078_register_bt_rails(d1830_bt_rails); /* Opt-in only. Default probe is GPIO + VBAT reads — no rail writes. * The old default seq wrote reg 13 = 0x01 (POWEROFF bit) at boot. @@ -1975,6 +3594,17 @@ static int d1830_gpio_probe(struct i2c_client *client) return ret; } + /* Every create here has a matching remove below. A leaked attribute + * outlives the module and reading it jumps into freed text, which is + * how buttons oopsed. */ + if (device_create_file(dev, &dev_attr_adc_channels)) + dev_warn(dev, "adc_channels sysfs failed\n"); + if (device_create_file(dev, &dev_attr_charger_regs)) + dev_warn(dev, "charger_regs sysfs failed\n"); + if (device_create_file(dev, &dev_attr_charger_watch)) + dev_warn(dev, "charger_watch sysfs failed\n"); + if (device_create_file(dev, &dev_attr_regs)) + dev_warn(dev, "regs sysfs failed\n"); ret = device_create_file(dev, &dev_attr_do_poweroff); if (ret) dev_warn(dev, "sysfs do_poweroff unavailable: %d\n", ret); @@ -1989,6 +3619,36 @@ static int d1830_gpio_probe(struct i2c_client *client) if (!pm_power_off) { pm_power_off = d1830_pm_power_off; + if (devm_register_sys_off_handler(dev, SYS_OFF_MODE_POWER_OFF, + SYS_OFF_PRIO_DEFAULT, + d1830_sys_off_handler, NULL)) + dev_warn(dev, "sys-off handler not registered\n"); + /* + * The charger configuration write runs by default: 0x1a already + * reads 0xb2 here because the bootloader wrote it, so this confirms + * the stock state and makes us correct on a path where it did not. + * + * The full sub_23EC replay stays behind charger_hw_init, because its + * 0x10 masking clears bit 6 -- a live rail -- and RetailOS only runs + * that at cold boot, before anything depends on those rails. + */ + if (charger_hw_init) { + int cret = d1830_charger_hw_init(client, true); + + dev_warn(dev, "sub_23EC PMIC init replayed: %d\n", cret); + } else if (ldo_1a_write) { + int cret = d1830_charger_config(client); + + if (cret) + dev_warn(dev, "0x1a rail write: %d\n", cret); + } + d1830_charger_update(client); + d1830_charger_register(dev); + if (d1830_rtc_register(client)) + dev_warn(&client->dev, "RTC not registered\n"); + if (register_restart_handler(&d1830_restart_nb)) + dev_warn(&client->dev, + "restart handler not registered\n"); d1830_vinfo(dev, "registered pm_power_off (SEC reg %u bit0)\n", D1830_REG_POWEROFF); } else { @@ -2000,7 +3660,14 @@ static int d1830_gpio_probe(struct i2c_client *client) d1830_osos_nirq_mask(client); d1830_dump_irq_chain(client, "osos-mask"); - if (client->irq > 0) { + /* + * Requesting this arms a real EIC line. The level/type encoding is + * now taken from the stock configuration path, but it has not been + * confirmed on hardware, and getting it wrong wedges the system + * rather than merely failing. Polling covers the buttons meanwhile, + * so this stays opt-in until INTSTAT is observed tracking a press. + */ + if (client->irq > 0 && pmic_irq) { struct irq_data *d = irq_get_irq_data(client->irq); if (d) @@ -2090,6 +3757,8 @@ static int d1830_gpio_probe(struct i2c_client *client) gpio_dev->input->phys = "d1830/gpio"; gpio_dev->input->dev.parent = dev; gpio_dev->input->id.bustype = BUS_I2C; + gpio_dev->input->phys = "d1830/input0"; + gpio_dev->input->id.bustype = BUS_I2C; input_set_capability(gpio_dev->input, EV_KEY, KEY_HOMEPAGE); input_set_capability(gpio_dev->input, EV_KEY, KEY_POWER); input_set_capability(gpio_dev->input, EV_KEY, KEY_PLAYPAUSE); @@ -2109,6 +3778,9 @@ static int d1830_gpio_probe(struct i2c_client *client) msecs_to_jiffies(btn_poll_ms ? btn_poll_ms : 1000)); n31_pmu_rail_init(); n31_pmu_debugfs_init(); + n31_pmu_regulators_register(&client->dev); + if (device_create_file(dev, &dev_attr_buttons)) + dev_warn(dev, "buttons sysfs\n"); d1830_n31_din_nirq_hook = d1830_n31_din_nirq; return 0; } @@ -2123,12 +3795,19 @@ static void d1830_gpio_remove(struct i2c_client *client) } device_remove_file(&client->dev, &dev_attr_vbat_raw); device_remove_file(&client->dev, &dev_attr_audio_rails); + device_remove_file(&client->dev, &dev_attr_adc_channels); + device_remove_file(&client->dev, &dev_attr_charger_regs); + device_remove_file(&client->dev, &dev_attr_charger_watch); + device_remove_file(&client->dev, &dev_attr_buttons); + device_remove_file(&client->dev, &dev_attr_regs); device_remove_file(&client->dev, &dev_attr_do_poweroff); if (pm_power_off == d1830_pm_power_off) pm_power_off = NULL; + unregister_restart_handler(&d1830_restart_nb); d1830_n31_din_nirq_hook = NULL; n31_pmu_debugfs_exit(); n31_pmu_rail_exit(); + bcm2078_register_bt_rails(NULL); d1830_poweroff_client = NULL; } From 513e09a60fda2361e5e0dce852c9a61b467825b7 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:50:48 -0230 Subject: [PATCH 25/31] N31: touch and remote -- SPI status encoding, Nimbus reports, MikeyBus demux The SPI engine has been seen with two different status encodings. The driver waited on the ROS masks and then, on timeout, accepted the other family -- which turns a genuinely stuck transfer into a slow success and hides it. The encoding is a per-instance property now: AUTO latches on the first transfer and only the latched family is consulted afterwards, so a timeout is a timeout. The spin budget came down from 500000, which is less a timeout than a way to make a stuck engine look like a hang; more than one "lockup" chased here was exactly that. Nimbus reads a report whenever ATTN is asserted rather than requiring the ping to have succeeded first, honours the length the firmware declares instead of a fixed size, and burst-transfers rather than byte-banging. Bootloader status words are recognised as such, 0x4f81 among them, so "the application never started" is distinguishable from "the part is not answering at all". MikeyBus was discarding pkt[2], which is the channel -- the remote stream is channel 4 and was being parsed as though it were something else. It is demuxed by channel now and registers an evdev node. The button map is a placeholder and is marked as one in the source. The decomp gives the vocabulary from the handler names -- HandleMikeyCenter, HandleMikeyVolumeUp, HandleMikeyVolumeDown, HandleMikeyAllUp -- and that last one is the useful inference, since an "all up" event means the byte is a bitmask of held buttons rather than a button code. What the decomp does not give is which bit is which, so the ordering is a guess and says so. --- drivers/input/touchscreen/apple-nimbus.c | 790 ++++++++++++++++++++++- drivers/misc/apple-mikeybus.c | 183 +++++- drivers/spi/spi-s5l8702.c | 313 +++++++-- 3 files changed, 1212 insertions(+), 74 deletions(-) diff --git a/drivers/input/touchscreen/apple-nimbus.c b/drivers/input/touchscreen/apple-nimbus.c index d229d5db02b7a4..055f08aba0e1f6 100755 --- a/drivers/input/touchscreen/apple-nimbus.c +++ b/drivers/input/touchscreen/apple-nimbus.c @@ -181,6 +181,25 @@ static int go_spi_setup; module_param(go_spi_setup, int, 0644); MODULE_PARM_DESC(go_spi_setup, "SPI2 SETUP override for 2D54C GO (0=11B70)"); /* 0=8-bit PIO (RetailOS HBPP default), 1=u16 TXDATA pairs, 2=spi_sync */ +/* + * Read a report whenever ATTN is asserted, without requiring the ping + * to checksum first. Off restores the ping-gated behaviour. + */ +static bool attn_read = true; +module_param(attn_read, bool, 0644); +MODULE_PARM_DESC(attn_read, + "Read a frame when ATTN asserts even if the ping fails"); + +static bool post_poke_strict; +module_param(post_poke_strict, bool, 0644); +MODULE_PARM_DESC(post_poke_strict, + "Require a known 2D5B0 status before EXEC"); + +static bool park_power_down = true; +module_param(park_power_down, bool, 0644); +MODULE_PARM_DESC(park_power_down, + "Cut the rail when parking (0 keeps it up for raw_xfer)"); + static int go_xfer; module_param(go_xfer, int, 0644); MODULE_PARM_DESC(go_xfer, @@ -292,6 +311,9 @@ struct nimbus { bool cal_uploaded; bool requestcal_done; /* 2D5B0 / 1F01 path done */ bool exec_sent; /* 2D54C SPI xfer completed — not runtime */ + u8 *raw_rx; /* last raw_xfer response */ + unsigned int raw_n; + unsigned int attn_fails; bool runtime_ready; /* valid 182590 ping checksum */ bool fw_loaded; /* alias of runtime_ready for older call sites */ bool fw_tried; @@ -307,6 +329,9 @@ struct nimbus { int irq; unsigned int ping_fails; unsigned int recycle_count; + int spi_fam; /* 0 auto, 1 ROS, 2 Classic */ + unsigned int tx_timeouts; + unsigned int rx_timeouts; }; /* From gpio-d1830.c — OSOS 20766 / 6644(4) / reg16 bit5 */ @@ -398,6 +423,98 @@ static void nimbus_power_down(struct nimbus *n) nimbus_vinfo(n, "1A878 power-cut (RST hold, rail off, EN mode 1)\n"); } +/* + * CLKCON oracle replay. + * + * Deliberately not named after any peripheral. Nothing in the extracted + * Nimbus boot path -- sub_13A20, sub_1A5AC and the 2075A/20766/20690/ + * 11B70/20848/20E94/20490 chain -- writes CLKCON or calls sub_41CBD8 at + * all, so there is no evidence naming any of these bits as a touch + * clock. What is established is narrower and still worth replaying: + * Linux is missing global CLKCON state that both the Apple bootloader + * and the running stock system have. + * + * The N31 bootloader sets, at 0x3C500000: + * + * +0x08 = 0x2009200A we inherit this unchanged + * +0x0C = 0x80008000 we have 0x00000000 + * +0x10 = 0x00008000 we have 0x00000000 + * +0x14 = 0x80008000 we have 0x00002200 + * +0x18 = 0x20012001 we inherit this unchanged + * + * so something between DFU, u-boot and Linux clears two of them and + * rewrites a third, while leaving their neighbours alone. + * + * Polarity is inverted -- sub_41CBD8 clears a bit to enable and sets it + * to disable -- so our zeroes mean more clocks running than stock, not + * fewer. And the low nibble of +0x10 is a divider, not a flag: the rate + * decoder reads (MEMORY[0x3C500010] & 0xF) + 1. The stock live-touch + * 0x8000 -> 0x8004 delta is therefore a divider change, not an enable. + * + * clkcon_oracle=1 replay stock's live-touch values + * clkcon_oracle=2 replay the bootloader's post-init values + * + * Values are restored on unload so a failed experiment does not leave + * the clock tree in a state the rest of the kernel did not ask for. + */ +#define N31_CLKCON_PHYS 0x3c500000UL + +static int clkcon_oracle; +module_param(clkcon_oracle, int, 0644); +MODULE_PARM_DESC(clkcon_oracle, + "Replay observed CLKCON state: 1=stock touch, 2=bootloader"); + +static const unsigned int n31_clkcon_off[] = { 0x08, 0x0c, 0x10, 0x14 }; +static const u32 n31_clkcon_touch[] = { + 0xa009200a, 0x80000001, 0x00008004, 0x80002200, +}; +static const u32 n31_clkcon_boot[] = { + 0x2009200a, 0x80008000, 0x00008000, 0x80008000, +}; +static u32 n31_clkcon_saved[ARRAY_SIZE(n31_clkcon_off)]; +static bool n31_clkcon_applied; + +static void nimbus_clkcon_replay(struct nimbus *n) +{ + const u32 *want; + void __iomem *ck; + unsigned int i; + + if (!clkcon_oracle || n31_clkcon_applied) + return; + want = (clkcon_oracle == 2) ? n31_clkcon_boot : n31_clkcon_touch; + + ck = ioremap(N31_CLKCON_PHYS, 0x80); + if (!ck) + return; + for (i = 0; i < ARRAY_SIZE(n31_clkcon_off); i++) { + n31_clkcon_saved[i] = readl(ck + n31_clkcon_off[i]); + writel(want[i], ck + n31_clkcon_off[i]); + dev_info(&n->spi->dev, + "ORACLE_REPLAY clkcon+0x%02x %08x -> %08x\n", + n31_clkcon_off[i], n31_clkcon_saved[i], + readl(ck + n31_clkcon_off[i])); + } + n31_clkcon_applied = true; + iounmap(ck); +} + +static void nimbus_clkcon_restore(void) +{ + void __iomem *ck; + unsigned int i; + + if (!n31_clkcon_applied) + return; + ck = ioremap(N31_CLKCON_PHYS, 0x80); + if (!ck) + return; + for (i = 0; i < ARRAY_SIZE(n31_clkcon_off); i++) + writel(n31_clkcon_saved[i], ck + n31_clkcon_off[i]); + n31_clkcon_applied = false; + iounmap(ck); +} + /* * sub_11B70(2, 0x1A, 0x2EE0, 1) after every 20690(1). * 1A5AC always re-inits SPI2 here. Skipping it after 1A878 remux @@ -465,11 +582,192 @@ static void nimbus_spi2_fifo_flush(struct nimbus *n) writel(0xf, n->spi2 + SPI2_STATUS); } +/* + * SPI2 status polling. + * + * Both transfer loops below used to do this: + * + * guard = 100000; + * do { st = readl(STATUS); } while (!(st & 0xf800) && --guard); + * rx[i] = readl(RXDATA); + * + * The guard running out was not treated as a failure. RXDATA was read + * either way and the function returned 0, so when the ready bit never set, + * the FIFO's previous contents were handed back as if they were a reply. + * That does not look like noise because it is not noise: it is the same + * stale word every time, which is where the repeating 4f81 came from, and + * it is why the part appeared to answer while telling us nothing. + * + * Two changes. A timeout is now an error that propagates, so a dead bus + * reports as dead instead of inventing a reply. And because this engine has + * been seen reporting readiness in either of two encodings depending on the + * init path that ran, both are polled in one loop until one genuinely + * satisfies; that result is latched, logged once, and used exclusively + * afterwards. Latching matters: alternating between interpretations is how + * a transfer gets declared complete early. + */ +#define NIMBUS_SPI_GUARD 100000u + +#define NIMBUS_TXBUSY_ROS 0x7c0u +#define NIMBUS_TXBUSY_RESIDUE 0x40u /* stays set after 11B70 setup */ +#define NIMBUS_TXLVL_CLASSIC 0x1f0u +#define NIMBUS_RXRDY_ROS 0xf800u +#define NIMBUS_RXLVL_CLASSIC 0x3e00u + +static int nimbus_spi_family; +module_param(nimbus_spi_family, int, 0644); +MODULE_PARM_DESC(nimbus_spi_family, + "SPI2 status encoding: 0=auto-latch, 1=ROS (0x7C0/0xF800), 2=Classic (0x1F0/0x3E00)"); + +static bool nimbus_strict_rx; /* opt-in until proven on glass */ +module_param(nimbus_strict_rx, bool, 0644); +MODULE_PARM_DESC(nimbus_strict_rx, + "1=receive timeout fails the transfer (default); 0=legacy, read the FIFO anyway"); + +static void nimbus_latch_fam(struct nimbus *n, int fam, u32 st) +{ + if (n->spi_fam == fam) + return; + n->spi_fam = fam; + dev_info(&n->spi->dev, + "SPI2 status encoding latched: %s (STATUS=0x%08x)\n", + fam == 1 ? "ROS(0x7C0/0xF800)" : "Classic(0x1F0/0x3E00)", st); +} + +/* Wait for the transmit side to accept another word. */ +static int nimbus_wait_tx(struct nimbus *n) +{ + unsigned int guard = NIMBUS_SPI_GUARD; + u32 st = 0; + + while (guard--) { + st = readl(n->spi2 + SPI2_STATUS); + + if (n->spi_fam != 2) { + u32 b = st & NIMBUS_TXBUSY_ROS; + + if (b == 0 || b == NIMBUS_TXBUSY_RESIDUE) { + nimbus_latch_fam(n, 1, st); + return 0; + } + } + if (n->spi_fam != 1 && (st & NIMBUS_TXLVL_CLASSIC) == 0) { + nimbus_latch_fam(n, 2, st); + return 0; + } + cpu_relax(); + } + + if (!n->tx_timeouts++) + dev_warn(&n->spi->dev, + "SPI2 transmit never went idle (STATUS=0x%08x)\n", st); + return -ETIMEDOUT; +} + +/* Wait for a received word to actually be available. */ +static int nimbus_wait_rx(struct nimbus *n) +{ + unsigned int guard = NIMBUS_SPI_GUARD; + u32 st = 0; + + while (guard--) { + st = readl(n->spi2 + SPI2_STATUS); + + if (n->spi_fam != 2 && (st & NIMBUS_RXRDY_ROS)) { + nimbus_latch_fam(n, 1, st); + return 0; + } + if (n->spi_fam != 1 && (st & NIMBUS_RXLVL_CLASSIC)) { + nimbus_latch_fam(n, 2, st); + return 0; + } + cpu_relax(); + } + + if (!n->rx_timeouts++) + dev_warn(&n->spi->dev, + "SPI2 receive never became ready (STATUS=0x%08x), refusing to report stale FIFO\n", + st); + return -ETIMEDOUT; +} + +/* + * Transfers go through the SPI core. + * + * This driver is bound as an spi_device yet drove the SPI2 registers itself: + * its own CS, its own FIFO reset, its own TXDATA/RXDATA polling. That meant + * none of the usual guarantees applied -- no bus locking against another + * client, no controller-side clock or mode setup, no error propagation -- + * and it duplicated the controller driver's completion logic well enough to + * drift from it. Two copies of a tricky wait loop is one copy too many. + * + * The only thing the register path really offered was holding CS down across + * several calls, which the core expresses with cs_change on the final + * transfer of a message. The controller now honours that, so the whole thing + * is reachable through spi_sync(). + * + * The legacy path is kept behind nimbus_use_spi=0 purely so the two can be + * compared on hardware; it is not the supported route. + */ +static bool nimbus_use_spi; /* opt-in until proven on glass */ +module_param(nimbus_use_spi, bool, 0644); +MODULE_PARM_DESC(nimbus_use_spi, + "1=transfer via the SPI core (default); 0=legacy direct SPI2 register PIO"); + +/* + * One HBPP burst as a single spi_message. + * + * @hold_cs: leave the part selected when the message ends, for a frame that + * spans more than one call. Expressed as cs_change on the last + * transfer, which is exactly what that flag means there. + * + * The controller skips reading RXDATA when rx is NULL, but the part clocks a + * reply out regardless; without draining it an 8 KiB chunk overruns the RX + * FIFO and quietly loses everything after the header. So a TX-only caller + * still gets a throwaway receive buffer. + */ +static int nimbus_spi_burst(struct nimbus *n, const u8 *tx, u8 *rx, + unsigned int len, bool hold_cs) +{ + struct spi_transfer t = { + .tx_buf = tx, + .rx_buf = rx, + .len = len, + .cs_change = hold_cs, + }; + struct spi_message m; + u8 *drain = NULL; + int ret; + + if (!n->spi) + return -ENODEV; + + if (!rx) { + drain = kzalloc(len, GFP_KERNEL); + if (!drain) + return -ENOMEM; + t.rx_buf = drain; + } + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + ret = spi_sync(n->spi, &m); + kfree(drain); + + if (ret && !n->tx_timeouts++) + dev_warn(&n->spi->dev, "spi_sync failed: %d\n", ret); + return ret; +} + static int nimbus_burst_ex(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len, unsigned int cs_flags) { - unsigned int i, guard; - u32 st; + unsigned int i; + int ret = 0; + + if (nimbus_use_spi) + return nimbus_spi_burst(n, tx, rx, len, + !(cs_flags & NIMBUS_CS_END)); if (!n->spi2) return -ENODEV; @@ -482,26 +780,31 @@ static int nimbus_burst_ex(struct nimbus *n, const u8 *tx, u8 *rx, } for (i = 0; i < len; i++) { writel(1, n->spi2 + SPI2_RXLIMIT); - guard = 100000; - do { - st = readl(n->spi2 + SPI2_STATUS); - } while ((st & 0x7c0) != 0 && (st & 0x7c0) != 0x40 && --guard); + + ret = nimbus_wait_tx(n); + if (ret) + goto out; + writel(tx[i], n->spi2 + SPI2_TXDATA); writel(1, n->spi2 + SPI2_UNK4C); - guard = 100000; - do { - st = readl(n->spi2 + SPI2_STATUS); - } while (!(st & 0xf800) && --guard); + + ret = nimbus_wait_rx(n); + if (ret && nimbus_strict_rx) + goto out; + ret = 0; + if (rx) rx[i] = (u8)readl(n->spi2 + SPI2_RXDATA); else readl(n->spi2 + SPI2_RXDATA); } + +out: if (cs_flags & NIMBUS_CS_END) { writel(readl(n->spi2 + SPI2_SETUP) & ~0x400001u, n->spi2 + SPI2_SETUP); nimbus_spi2_cs(n, false); } - return 0; + return ret; } static int nimbus_burst(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len) @@ -519,13 +822,24 @@ static int nimbus_burst(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len static int nimbus_burst_u16_ex(struct nimbus *n, const u8 *tx, u8 *rx, unsigned int len, unsigned int cs_flags) { - unsigned int i, guard; - u32 st; + unsigned int i; + int ret = 0; if (!n->spi2) return -ENODEV; if (len & 1) return nimbus_burst_ex(n, tx, rx, len, cs_flags); + + /* + * The controller advertises SPI_BPW_MASK(8) and TXDATA is byte wide, + * so a 16-bit write only ever put the low byte on the wire -- which is + * why this path never got its 4BC1 ack and fell back. Through the core + * the same buffer goes out as two bytes in the same order, which is + * what the part was always receiving from the working path anyway. + */ + if (nimbus_use_spi) + return nimbus_spi_burst(n, tx, rx, len, + !(cs_flags & NIMBUS_CS_END)); if (cs_flags & NIMBUS_CS_BEGIN) { nimbus_spi2_cs(n, true); ndelay(2000); @@ -538,27 +852,32 @@ static int nimbus_burst_u16_ex(struct nimbus *n, const u8 *tx, u8 *rx, u16 r; writel(1, n->spi2 + SPI2_RXLIMIT); - guard = 100000; - do { - st = readl(n->spi2 + SPI2_STATUS); - } while ((st & 0x7c0) != 0 && (st & 0x7c0) != 0x40 && --guard); + + ret = nimbus_wait_tx(n); + if (ret) + goto out; + writel(w, n->spi2 + SPI2_TXDATA); writel(1, n->spi2 + SPI2_UNK4C); - guard = 100000; - do { - st = readl(n->spi2 + SPI2_STATUS); - } while (!(st & 0xf800) && --guard); + + ret = nimbus_wait_rx(n); + if (ret && nimbus_strict_rx) + goto out; + ret = 0; + r = (u16)readl(n->spi2 + SPI2_RXDATA); if (rx) { rx[i] = (u8)(r >> 8); rx[i + 1] = (u8)r; } } + +out: if (cs_flags & NIMBUS_CS_END) { writel(readl(n->spi2 + SPI2_SETUP) & ~0x400001u, n->spi2 + SPI2_SETUP); nimbus_spi2_cs(n, false); } - return 0; + return ret; } static int nimbus_burst_u16(struct nimbus *n, const u8 *tx, u8 *rx, @@ -613,7 +932,25 @@ static u32 nimbus_sum32(const u8 *p, unsigned int len); static bool nimbus_opcode_known(u16 w) { return w == 0x18e1 || w == 0x1aa1 || w == 0x1f01 || w == 0x19c1 || - w == 0x4879 || w == 0x4bc1 || w == 0x4969 || w == 0x4ad1; + w == 0x4879 || w == 0x4bc1 || w == 0x4969 || w == 0x4ad1 || + w == 0x4f81; +} + +/* + * Words the bootloader answers with. + * + * This distinction is the useful part, and it is worth stating plainly + * because the log did not: seeing 0x4f81 come back is not a failed read + * and not a bus fault. The part is alive, it is answering, and it is + * answering as the bootloader -- which means the application it was + * asked to start is not running. A checksum failure on the runtime ping + * says only that the reply was not a runtime reply; this says what it + * was instead. + */ +static bool nimbus_status_is_bootloader(u16 w) +{ + return w == 0x4f81 || w == 0x4879 || w == 0x4bc1 || + w == 0x4ad1 || w == 0x4969; } /* sub_26494 — 16↔16 1A A1 + 18 E1 pad; two rev16 words must be known */ @@ -1356,6 +1693,44 @@ static int nimbus_acquire_fw(struct device *dev, const u8 **data, return -ENOENT; *data = (*fw_out)->data; *size = (*fw_out)->size; + + /* + * The image declares its own length, and ours does not match the file. + * + * Layout starts with an ASCII revision string -- "87402.0" followed by + * a revision byte -- and carries a little-endian u32 at +12. On the + * Rockbox port's blob that u32 is 0xE960 = 59744, exactly the file + * size, which is what identifies the field. The blob extracted here is + * 61680 bytes while its own header says 59760, so 1920 bytes of + * whatever follows the image were being uploaded as if they were part + * of it. + * + * That is worth being strict about: the part checksums what it + * receives, so trailing bytes do not produce a diagnostic, they produce + * an image that fails to start while every transfer reports success -- + * which is exactly the state this driver has been stuck in, with the + * bootloader still answering pings after EXEC. + * + * Trust the header when it is sane and smaller than the file. A header + * larger than the file means the file is truncated and the declared + * length is unusable, so keep the file size and say so. + */ + if (*size >= NIMBUS_FW_HDR_LEN) { + u32 declared = get_unaligned_le32(*data + 12); + + if (declared && declared < *size) { + dev_info(dev, + "fw '%c%c%c%c%c%c%c' rev %02x: file %zu, header says %u; uploading %u\n", + (*data)[0], (*data)[1], (*data)[2], (*data)[3], + (*data)[4], (*data)[5], (*data)[6], (*data)[7], + *size, declared, declared); + *size = declared; + } else if (declared > *size) { + dev_warn(dev, + "fw header declares %u but file is only %zu; using the file\n", + declared, *size); + } + } return 0; } @@ -1690,21 +2065,77 @@ static int nimbus_post_download(struct nimbus *n) i, pokes[i].a1, rb, pokes[i].a2); } + /* + * Stock gates everything after this point on the reply: + * + * if (!sub_40F770(&v8, 2, &v10, 2)) { sub_410522(65); + * if (!sub_3D5706(&v9, ...)) return 1; } + * return 0; + * + * We were throwing both answers away. nimbus_xfer returning 0 + * only says the SPI transfer completed, and nimbus_status_poll + * below returns 0 for any status at all, so post_download always + * reported success and we went on to EXEC no matter what the part + * said. This is the last handshake before the application is + * supposed to start, and its reply has never been looked at. + */ put_unaligned_le16(NIMBUS_POST_POKE, tx); ret = nimbus_xfer(n, tx, rx, 2); if (ret) return ret; + st = (u16)((rx[0] << 8) | rx[1]); + dev_info(&n->spi->dev, + "011F reply %02x %02x (0x%04x)%s\n", + rx[0], rx[1], st, + nimbus_opcode_known(st) ? " known" : " UNKNOWN"); + st = 0; msleep(65); /* 2D5B0: 3D5706 success only — does not require 0x4BC1 */ - if (nimbus_status_poll(n, &st) == 0) { - nimbus_vinfo(n, "post-poke status 0x%04x\n", st); - n->requestcal_done = true; - return 0; + if (nimbus_status_poll(n, &st) != 0) + return -EIO; + + dev_info(&n->spi->dev, "post-poke 1AA1 status 0x%04x%s\n", + st, nimbus_opcode_known(st) ? " known" : " UNKNOWN"); + + /* + * Refuse to EXEC on an unrecognised status when post_poke_strict + * is set. Off by default so a run still reaches EXEC and we can + * see both halves before deciding which status stock treats as a + * pass. + */ + if (post_poke_strict && !nimbus_opcode_known(st)) { + dev_warn(&n->spi->dev, + "post-poke status 0x%04x rejected; not running EXEC\n", + st); + return -EIO; } - return -EIO; + n->requestcal_done = true; + return 0; } /* sub_2D54C — 12↔12: 1D 53 + two LE u32 + sum16 */ +/* + * Neither implementation reconfigures SPI2 for EXEC -- go_spi_setup + * defaults to 0 here, and stock's sub_2D54C goes through sub_40F770 + * like every other command, same channel-2 bracket and all. But the + * part stops driving MISO from EXEC onwards, so the question is + * whether the controller's own state moves underneath us. Measure it + * either side of the transfer rather than reasoning about it. + */ +static void nimbus_spi2_dump(struct nimbus *n, const char *when) +{ + if (!n->spi2) + return; + dev_info(&n->spi->dev, + "SPI2 %-6s ctrl=%08x setup=%08x status=%08x pin=%08x clkdiv=%08x\n", + when, + readl(n->spi2 + SPI2_CTRL), + readl(n->spi2 + SPI2_SETUP), + readl(n->spi2 + SPI2_STATUS), + readl(n->spi2 + SPI2_PIN), + readl(n->spi2 + SPI2_CLKDIV)); +} + static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) { u8 tx[12] = { 0x1d, 0x53 }; @@ -1727,14 +2158,18 @@ static int nimbus_cmd_2d54c_raw(struct nimbus *n, u32 word0, u32 word1) go_spi_setup, saved_setup); } } + nimbus_spi2_dump(n, "pre"); if (go_xfer == 2) ret = nimbus_xfer(n, tx, rx, 12); else if (go_xfer == 1) ret = nimbus_burst_u16(n, tx, rx, 12); else ret = nimbus_burst(n, tx, rx, 12); + nimbus_spi2_dump(n, "post"); if (saved_setup) writel(saved_setup, n->spi2 + SPI2_SETUP); + msleep(NIMBUS_EXEC_SETTLE_MS); + nimbus_spi2_dump(n, "settle"); nimbus_vinfo(n, "2D54C %08x %08x ret=%d xfer=%d rx %02x %02x %02x %02x %02x %02x\n", word0, word1, ret, go_xfer, rx[0], rx[1], rx[2], rx[3], @@ -2377,6 +2812,14 @@ static int nimbus_ping(struct nimbus *n, u16 *status_out) rx[5], rx[6], rx[7], rx[8], rx[9], rx[10], rx[11], rx[12], rx[13], rx[14], rx[15]); + { + u16 w0 = get_unaligned_be16(rx); + + if (nimbus_status_is_bootloader(w0)) + dev_warn(&n->spi->dev, + "bootloader status 0x%04x: the part is answering, its application is not running\n", + w0); + } if (tries == 5) return -EIO; msleep(1); @@ -2551,6 +2994,8 @@ static void nimbus_gpio_bringup(struct nimbus *n) { int rail; + nimbus_clkcon_replay(n); + /* GPIOCMD only — gpiod set_value fights polarity on RST. */ nimbus_gpiocmd_mode(n, NIMBUS_GPIO_RST, 1, 0); msleep(5); @@ -2656,7 +3101,17 @@ static void nimbus_park(struct nimbus *n, const char *why) nimbus_verbose = false; dev_warn(&n->spi->dev, "nimbus parked (%s) — rmmod/insmod to retry\n", why); - nimbus_power_down(n); + /* + * Parking cuts the rail, which makes every post-mortem probe read + * an undriven bus and look exactly like the failure being + * investigated. touch_power_down only covers suspend, so there was + * no way to examine a part that had reached EXEC and stayed up. + */ + if (park_power_down) + nimbus_power_down(n); + else + dev_warn(&n->spi->dev, + "park_power_down=0: rail left on for probing\n"); } /* ------------------------------------------------------------------ */ @@ -2822,6 +3277,31 @@ static void nimbus_service(struct nimbus *n) nimbus_read_reports(n, st); return; } + /* + * The ping is a bootloader-era transaction. Once the application + * is running it answers with a repeating word instead, so a failed + * ping stopped us from ever reading a report -- even though ATTN, + * which the application drives low when a frame is waiting, was + * asserted the whole time. + * + * Measured on a fresh boot: the pad idles high at 0x40 in bank 4 + * DIN, goes low during bring-up and stays low. That is the part + * asking to be serviced. Honour it: if ATTN says there is data, + * read regardless of what the ping thought. + */ + if (attn_read && n->attn && !gpiod_get_value_cansleep(n->attn)) { + int ret = nimbus_read_reports(n, 0); + + if (!ret) { + n->spi_ok = true; + n->ping_fails = 0; + return; + } + if (n->attn_fails++ < 3) + dev_info(&n->spi->dev, + "attn asserted but read failed (%d)\n", + ret); + } n->ping_fails++; if (nimbus_verbose && (n->ping_fails <= 3 || n->ping_fails == 10)) nimbus_vinfo(n, @@ -2960,6 +3440,209 @@ static void nimbus_isys_sysfs_remove(struct nimbus *n) n->isys_sysfs = false; } +/* + * Bring-up state. This driver carries two dozen module parameters and had + * no way to read back what any of them achieved, so a failed download and + * a controller that never booted looked identical from userspace. + * + * The flags are the download milestones in order, so the first one that + * reads 0 is where the sequence stopped. + */ +static ssize_t state_show(struct device *dev, struct device_attribute *a, + char *buf) +{ + struct nimbus *n = spi_get_drvdata(to_spi_device(dev)); + + if (!n) + return -ENODEV; + return sysfs_emit(buf, + "spi_ok=%d fw_tried=%d fw_uploaded=%d cal_uploaded=%d\n" + "requestcal_done=%d exec_sent=%d runtime_ready=%d\n" + "irq=%d suspended=%d\n", + n->spi_ok, n->fw_tried, n->fw_uploaded, + n->cal_uploaded, n->requestcal_done, n->exec_sent, + n->runtime_ready, n->irq, nimbus_pm_suspended); +} +/* + * Raw transfer window. + * + * The application firmware answers the bootloader's ping with a + * repeating 16-bit word rather than a checksummed frame. A repeating + * word is not an idle bus -- the part is clocking something out -- so + * the remaining unknown is framing, not liveness. Guessing at that one + * hypothesis per rebuild is far too slow, so expose the transfer itself: + * + * echo 'ea 01 01 00 ...' > raw_xfer send these bytes + * cat raw_xfer what came back + * echo N > raw_len pad/clock out to N bytes + * raw_mode=0 8-bit PIO (bootloader width) + * raw_mode=1 16-bit PIO (application width) + * raw_mode=2 SPI core + * + * Read-only against a part that is already running: it clocks bytes and + * reports what returns. It cannot wedge the SoC the way arming an + * unclaimed level interrupt can, which is the whole point of preferring + * it to another EIC experiment. + */ +static int raw_mode = 1; +module_param(raw_mode, int, 0644); +MODULE_PARM_DESC(raw_mode, + "raw_xfer transport: 0=8-bit PIO 1=16-bit PIO 2=SPI core"); + +static unsigned int raw_len; +module_param(raw_len, uint, 0644); +MODULE_PARM_DESC(raw_len, + "Clock raw_xfer out to this many bytes (0 = as written)"); + +static int nimbus_raw_do(struct nimbus *n, const u8 *tx, u8 *rx, + unsigned int len) +{ + switch (raw_mode) { + case 0: + return nimbus_burst(n, tx, rx, len); + case 2: + return nimbus_xfer(n, tx, rx, len); + default: + return nimbus_burst_u16(n, tx, rx, len); + } +} + +static ssize_t raw_xfer_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nimbus *n = dev_get_drvdata(dev); + unsigned int i; + int len = 0; + + if (!n || !n->raw_rx || !n->raw_n) + return sysfs_emit(buf, "(no transfer yet)\n"); + + mutex_lock(&n->lock); + for (i = 0; i < n->raw_n && len < PAGE_SIZE - 4; i++) + len += scnprintf(buf + len, PAGE_SIZE - len, "%02x%c", + n->raw_rx[i], + ((i & 15) == 15 || i + 1 == n->raw_n) ? + '\n' : ' '); + mutex_unlock(&n->lock); + return len; +} + +static ssize_t raw_xfer_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nimbus *n = dev_get_drvdata(dev); + unsigned int nb = 0, want; + const char *p = buf; + u8 *tx, *rx; + int ret; + + if (!n) + return -ENODEV; + + tx = kzalloc(NIMBUS_READ_MAX, GFP_KERNEL); + rx = kzalloc(NIMBUS_READ_MAX, GFP_KERNEL); + if (!tx || !rx) { + ret = -ENOMEM; + goto out; + } + + while (*p && nb < NIMBUS_READ_MAX) { + unsigned int v; + + while (*p == ' ' || *p == ',' || *p == '\n' || + *p == '\t') + p++; + if (!*p) + break; + if (sscanf(p, "%2x", &v) != 1) + break; + tx[nb++] = (u8)v; + while (*p && *p != ' ' && *p != ',' && *p != '\n') + p++; + } + + /* Clocking past the written bytes is how you see a reply that + * arrives after the command, so honour raw_len when it is longer. */ + want = raw_len ? raw_len : nb; + if (!want || want > NIMBUS_READ_MAX) { + ret = -EINVAL; + goto out; + } + if ((raw_mode == 1) && (want & 1)) + want++; + + mutex_lock(&n->lock); + ret = nimbus_raw_do(n, tx, rx, want); + if (!ret) { + if (!n->raw_rx) + n->raw_rx = devm_kzalloc(&n->spi->dev, + NIMBUS_READ_MAX, GFP_KERNEL); + if (n->raw_rx) { + memcpy(n->raw_rx, rx, want); + n->raw_n = want; + } + } + mutex_unlock(&n->lock); + + dev_info(&n->spi->dev, + "raw_xfer mode=%d len=%u ret=%d rx %02x %02x %02x %02x %02x %02x %02x %02x\n", + raw_mode, want, ret, rx[0], rx[1], rx[2], rx[3], + rx[4], rx[5], rx[6], rx[7]); +out: + kfree(tx); + kfree(rx); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(raw_xfer); + +/* + * attn sysfs: the line the application drives when it has a report. + * Readable straight from DIN, which is why touch does not have to wait + * for the EIC to be understood. + */ +static ssize_t attn_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nimbus *n = dev_get_drvdata(dev); + + if (!n || !n->attn) + return sysfs_emit(buf, "-1\n"); + return sysfs_emit(buf, "%d\n", + gpiod_get_value_cansleep(n->attn)); +} +static DEVICE_ATTR_RO(attn); + +static DEVICE_ATTR_RO(state); + +/* Force a re-run of the bring-up without unbinding the driver. */ +static ssize_t redownload_store(struct device *dev, + struct device_attribute *a, + const char *buf, size_t count) +{ + struct nimbus *n = spi_get_drvdata(to_spi_device(dev)); + int ret; + + if (!n) + return -ENODEV; + if (buf[0] != '1') + return -EINVAL; + ret = n31_touch_suspend(); + if (!ret) + ret = n31_touch_resume(); + return ret ? ret : count; +} +static DEVICE_ATTR_WO(redownload); + +static struct attribute *nimbus_attrs[] = { + &dev_attr_raw_xfer.attr, + &dev_attr_attn.attr, + &dev_attr_state.attr, + &dev_attr_redownload.attr, + NULL, +}; +ATTRIBUTE_GROUPS(nimbus); + static int nimbus_probe(struct spi_device *spi) { struct nimbus *n; @@ -2980,6 +3663,8 @@ static int nimbus_probe(struct spi_device *spi) mutex_init(&n->lock); spi_set_drvdata(spi, n); nimbus_pm_dev = n; + if (sysfs_create_groups(&spi->dev.kobj, nimbus_groups)) + dev_warn(&spi->dev, "sysfs groups failed\n"); n->gpio_base = devm_ioremap(&spi->dev, S5L8740_GPIO_PHYS, 0x400); n->gpiocmd = devm_ioremap(&spi->dev, S5L8740_GPIOCMD_PHYS, 4); @@ -3023,6 +3708,22 @@ static int nimbus_probe(struct spi_device *spi) * only after a failed 1A5AC, max 3. remove() already * 1A878s on reload. */ + /* + * sub_20E94 reads PMIC 0x51 here, between the 26494 probe and + * the download loop, and discards the value. Do the same: the + * read itself may be the point. + */ + { + int (*pmic_read)(void); + + pmic_read = (int (*)(void)) + __symbol_get("d1830_touch_bringup_read"); + if (pmic_read) { + pmic_read(); + __symbol_put("d1830_touch_bringup_read"); + } + } + for (attempt = 0; attempt < 3; attempt++) { if (attempt) { nimbus_power_down(n); @@ -3155,6 +3856,26 @@ static int nimbus_probe(struct spi_device *spi) return 0; } +/* + * OSOS sub_1A878 is the disable path -- 20490(0), RST asserted, 20690(0) + * to release the SPI2 pads, rail off, EN mode 1 -- and nimbus_power_down + * already implements it. It was only ever reached through remove(), + * which a shutdown or kexec does not call, so the part was left powered + * and holding the bus across the handover. + */ +static void nimbus_shutdown(struct spi_device *spi) +{ + struct nimbus *n = spi_get_drvdata(spi); + + if (!n) + return; + n->stopped = true; + if (n->thread) + kthread_stop(n->thread); + n->thread = NULL; + nimbus_power_down(n); +} + static void nimbus_remove(struct spi_device *spi) { nimbus_pm_dev = NULL; @@ -3164,6 +3885,16 @@ static void nimbus_remove(struct spi_device *spi) if (n->thread) kthread_stop(n->thread); nimbus_isys_sysfs_remove(n); + /* + * probe creates these but nothing removed them, so the group + * outlived the module. The next insmod then hit a duplicate + * filename, and internal_create_group rolls the whole group back + * on failure -- so a second load silently lost every attribute, + * including state and redownload. It looked like the attributes + * were never registered rather than registered twice. + */ + sysfs_remove_groups(&spi->dev.kobj, nimbus_groups); + nimbus_clkcon_restore(); nimbus_power_down(n); } @@ -3180,6 +3911,7 @@ static struct spi_driver nimbus_driver = { }, .probe = nimbus_probe, .remove = nimbus_remove, + .shutdown = nimbus_shutdown, }; module_spi_driver(nimbus_driver); diff --git a/drivers/misc/apple-mikeybus.c b/drivers/misc/apple-mikeybus.c index 4e045bb0201586..9aa483774beefe 100755 --- a/drivers/misc/apple-mikeybus.c +++ b/drivers/misc/apple-mikeybus.c @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,40 @@ static int poll_ms = 500; module_param(poll_ms, int, 0644); MODULE_PARM_DESC(poll_ms, "Model poll interval in milliseconds"); +/* + * Remote buttons. + * + * What the decomp proves: the event vocabulary, from the handler names -- + * HandleMikeyCenter, HandleMikeyVolumeUp, HandleMikeyVolumeDown and + * HandleMikeyAllUp. That last one is the useful inference. An "all up" + * event only makes sense if the wire carries a bitmap of the buttons + * currently held rather than discrete press and release codes: with + * discrete codes each button would report its own release and a combined + * all-up event would be redundant. So this decodes a held-button bitmap and + * derives press/release by comparing against the previous value, with an + * all-zero byte meaning everything is released. + * + * What the decomp does NOT give is which bit is which button. The names + * prove the vocabulary, not the wire encoding, and nothing in the + * disassembly pins the bit order down. Guessing it in the source would bury + * an assumption somewhere it cannot be seen, so it lives here instead: one + * keycode per bit, changeable at runtime. Read remote_raw while pressing a + * known button, see which bit moves, and set the map accordingly -- that is + * one session with the hardware rather than an argument about byte order. + * + * The default below is a placeholder ordering, NOT an attested one. + */ +#define MIKEY_BUTTON_BITS 8 + +static int button_map[MIKEY_BUTTON_BITS] = { + KEY_PLAYPAUSE, KEY_VOLUMEUP, KEY_VOLUMEDOWN, KEY_NEXTSONG, + KEY_PREVIOUSSONG, 0, 0, 0, +}; +static int button_map_count = MIKEY_BUTTON_BITS; +module_param_array(button_map, int, &button_map_count, 0644); +MODULE_PARM_DESC(button_map, + "keycode per remote bitmap bit 0..7, 0=unused; bit order is NOT attested, confirm with remote_raw"); + static int baud = 115200; module_param(baud, int, 0644); MODULE_PARM_DESC(baud, "MikeyBus UART baud rate"); @@ -149,6 +184,13 @@ struct apple_mikeybus { struct apple_mikey_ring rx_raw; struct apple_mikey_ring rx_task_stream; + struct apple_mikey_ring remote_raw; /* channel 4 only */ + + struct input_dev *input; + u8 last_buttons; + u32 remote_bytes; + u32 button_events; + u32 all_up_events; u32 rx_bytes; u32 lower_packets; @@ -333,6 +375,45 @@ static void mikey_rx_byte_locked(struct apple_mikeybus *m, u8 b) m->rx_bytes++; } +/* + * One byte of the channel 4 remote stream. + * + * Treated as a held-button bitmap: bits set now that were not set before are + * presses, bits that cleared are releases, and 0x00 releases everything -- + * the AllUp case. Reporting is edge-driven, so a repeated identical byte + * costs nothing and a dropped byte self-corrects on the next one. + */ +static void mikey_remote_byte_locked(struct apple_mikeybus *m, u8 b) +{ + u8 changed; + unsigned int bit; + + mikey_ring_put(&m->remote_raw, b); + m->remote_bytes++; + + if (!m->input) + return; + + changed = b ^ m->last_buttons; + if (!changed) + return; + + for (bit = 0; bit < MIKEY_BUTTON_BITS; bit++) { + int code = button_map[bit]; + + if (!code || !(changed & BIT(bit))) + continue; + input_report_key(m->input, code, !!(b & BIT(bit))); + m->button_events++; + } + input_sync(m->input); + + if (!b && m->last_buttons) + m->all_up_events++; + + m->last_buttons = b; +} + static void mikey_report_state_locked(struct apple_mikeybus *m, const char *reason) { @@ -422,8 +503,22 @@ static void mikey_handle_lower_packet_locked(struct apple_mikeybus *m, return; count = pkt[0] - 3; - for (i = 0; i < count; i++) - mikey_rx_byte_locked(m, pkt[3 + i]); + + /* + * pkt[2] is the channel and was being thrown away, so the + * headset-model stream on channel 3 and the remote stream on + * channel 4 were interleaved into one buffer. They are + * different protocols; anything reading the mixed result is + * parsing two things at once. Keep feeding both to the raw + * ring for tracing, but route channel 4 to the remote decoder. + */ + for (i = 0; i < count; i++) { + u8 payload = pkt[3 + i]; + + mikey_rx_byte_locked(m, payload); + if (pkt[2] == MIKEY_CH_READ) + mikey_remote_byte_locked(m, payload); + } m->lower_rx70_packets++; break; @@ -1021,6 +1116,47 @@ static ssize_t rx_status_shadow_show(struct device *dev, } static DEVICE_ATTR_RO(rx_status_shadow); +static ssize_t remote_raw_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + size_t n; + + mutex_lock(&m->lock); + n = mikey_ring_dump_hex(&m->remote_raw, buf, PAGE_SIZE); + mutex_unlock(&m->lock); + return n; +} +static DEVICE_ATTR_RO(remote_raw); + +/* + * Everything needed to pin down the bit order: the live bitmap, the map in + * force, and whether any of it is moving. Hold a button, read this. + */ +static ssize_t buttons_show(struct device *dev, struct device_attribute *attr, + char *buf) +{ + struct apple_mikeybus *m = dev_get_drvdata(dev); + unsigned int i; + size_t n = 0; + + mutex_lock(&m->lock); + n += scnprintf(buf + n, PAGE_SIZE - n, + "held=0x%02x remote_bytes=%u events=%u all_up=%u\n", + m->last_buttons, m->remote_bytes, m->button_events, + m->all_up_events); + for (i = 0; i < MIKEY_BUTTON_BITS; i++) + n += scnprintf(buf + n, PAGE_SIZE - n, + "bit%u keycode=%d held=%d\n", + i, button_map[i], + !!(m->last_buttons & BIT(i))); + n += scnprintf(buf + n, PAGE_SIZE - n, + "note: bit order is not attested by the decomp; confirm here\n"); + mutex_unlock(&m->lock); + return n; +} +static DEVICE_ATTR_RO(buttons); + static ssize_t rx_raw_show(struct device *dev, struct device_attribute *attr, char *buf) { @@ -1133,6 +1269,8 @@ static struct attribute *mikey_attrs[] = { &dev_attr_active_probe.attr, &dev_attr_resistor_backend_ready.attr, &dev_attr_decomp_channel_mask_shadow.attr, + &dev_attr_remote_raw.attr, + &dev_attr_buttons.attr, &dev_attr_rx_status_shadow.attr, &dev_attr_rx_raw.attr, &dev_attr_rx_task_stream.attr, @@ -1151,6 +1289,39 @@ static const struct attribute_group *mikey_groups[] = { NULL, }; +/* + * The remote is an input device, so publish it as one. + * + * Without this the driver decoded a button stream into nothing: no evdev + * node, so no key ever reached userspace no matter how well the packets + * parsed. devm-managed, so teardown follows the device. + */ +static int mikey_register_input(struct apple_mikeybus *m) +{ + struct input_dev *in; + unsigned int i; + int ret; + + in = devm_input_allocate_device(m->dev); + if (!in) + return -ENOMEM; + + in->name = "Apple MikeyBus Remote"; + in->phys = "mikeybus/input0"; + in->id.bustype = BUS_HOST; + + for (i = 0; i < MIKEY_BUTTON_BITS; i++) + if (button_map[i]) + input_set_capability(in, EV_KEY, button_map[i]); + + ret = input_register_device(in); + if (ret) + return ret; + + m->input = in; + return 0; +} + static int mikey_create_sysfs(struct apple_mikeybus *m) { return sysfs_create_groups(&m->dev->kobj, mikey_groups); @@ -1210,6 +1381,14 @@ static int mikey_bind(struct device *dev, struct serdev_device *serdev) mikey_pinmux_uart(m, true); + /* + * Not fatal: the bus is still useful for headset detection and + * tracing even if the input node cannot be created. + */ + ret = mikey_register_input(m); + if (ret) + dev_warn(m->dev, "no input device: %d\n", ret); + ret = mikey_create_sysfs(m); if (ret) { if (serdev && m->uart_opened) { diff --git a/drivers/spi/spi-s5l8702.c b/drivers/spi/spi-s5l8702.c index f5c9cfd739132d..b0dc6d005557b5 100755 --- a/drivers/spi/spi-s5l8702.c +++ b/drivers/spi/spi-s5l8702.c @@ -40,6 +40,38 @@ #define SPISTATUS_TXBUSY_ROS 0x7c0 #define SPISTATUS_RXRDY_ROS 0xf800 +/* + * Two different status encodings have been seen on this engine. + * + * ROS TX busy 0x7C0 -> 0, RX ready 0xF800 nonzero + * CLASSIC TX level 0x1F0 -> 0, RX level 0x3E00 nonzero + * + * This driver used to wait on the ROS masks and then, on timeout, accept + * the CLASSIC masks as a fallback -- and vice versa for RX. That is a + * correctness hazard, not just a slow path: the two families' masks + * overlap (0x7C0 vs 0x1F0 share bits 6-8, 0xF800 vs 0x3E00 share bits + * 11-13), so the wrong family's test can read as satisfied while the + * transfer is still in flight. RXDATA is then sampled early and returns + * the previous FIFO contents. That does not look like noise: it repeats, + * which is exactly the shape of the 4f814f81 pattern seen on SPI2. + * + * So the encoding is now a per-instance property. In AUTO the first + * transfer polls both families in one loop and latches whichever + * genuinely satisfies first -- a measurement, made once, and logged. + * After that only the latched family is consulted and a timeout is a + * real timeout rather than a cue to try the other interpretation. + */ +enum s5l8702_spi_fam { + SPI_FAM_AUTO = 0, + SPI_FAM_ROS, + SPI_FAM_CLASSIC, +}; + +static int spi_family; +module_param(spi_family, int, 0644); +MODULE_PARM_DESC(spi_family, + "status encoding: 0=auto-latch per instance, 1=ROS (0x7C0/0xF800), 2=Classic (0x1F0/0x3E00)"); + #define SPISETUP_RXMODE BIT(0) #define SPISETUP_RETAILOS 0x403c /* 0x402C | 0x10 — SPI2 / 11B70(2,0x1A,…) */ #define SPISETUP_SPI0_11B70 0x403e /* 11B70(0,0x1A,0x2EE0,8) → 0x402E|0x10 */ @@ -62,7 +94,29 @@ #define SPI0_BASE_PHYS 0x3c300000UL #define SPI2_BASE_PHYS 0x3d200000UL -#define SPI_WAIT_GUARD 500000 +/* + * Spin budget per status wait. + * + * This was 500000, which is not a timeout so much as a way to make a stuck + * bus look like a dead device: every wait burns the full budget with + * preemption held off, and with dozens of register accesses in one codec + * bring-up the machine stops scheduling userspace entirely. The symptom is + * distinctive and misleading -- ping still answers because that is + * interrupt context, while ssh times out and the device reads as locked. + * More than one "lockup" chased tonight looked exactly like that. + * + * A transfer that has not progressed in a few milliseconds is not going to. + * Fail fast, report it, and let the caller decide. + * + * Do NOT cond_resched() in these loops. transfer_one can run in atomic + * context, and sleeping there is "BUG: scheduling while atomic" -- a panic, + * which with panic=-1 on the cmdline reboots before the message can be + * read. That was added here as a fix for CPU starvation and was itself the + * crash. Lowering the budget is the safe half of that idea; yielding is + * not, and the correct place to solve starvation is a smaller budget, not + * a sleep in a path that may not sleep. + */ +#define SPI_WAIT_GUARD 20000 /* Verbose 11B70/CS setup spam off by default. */ static bool verbose; @@ -91,6 +145,11 @@ struct s5l8702_spi { bool prepared; int last_err; u32 last_status; + enum s5l8702_spi_fam fam; + bool cs_held; /* CS left asserted by a cs_change transfer */ + unsigned int fam_latch_status; + unsigned int tx_timeouts; + unsigned int rx_timeouts; }; static void s5l8702_gpiocmd_func(struct s5l8702_spi *sspi, unsigned int gpio, u8 func) @@ -175,6 +234,20 @@ static void s5l8702_spi0_11b70(struct s5l8702_spi *sspi) * CLKDIV stays 2 (440A58(24000, 0x2EE0)). Do not use the generic * CLKDIV=4 path; that left +0x38/+0x3c at reset and ping RX was junk. */ +/* + * The RetailOS SPI2 oracle, captured with touch working, has +0x3c at + * 0x18c. The formula below yields 0x90 for a4=1, and it is not a matter + * of the wrong a4: solving 3*24*(a4+1) = 396 gives 4.5, so the + * derivation itself does not reproduce the hardware. +0x38 does match + * at 0x18, and this driver already records that leaving the pair at + * reset made ping RX junk, so the register affects receive behaviour. + * Overridable while that is being tested. + */ +static int spi2_u3c = 0x18c; +module_param(spi2_u3c, int, 0644); +MODULE_PARM_DESC(spi2_u3c, + "SPI2 +0x3c value (0 = computed 0x90, default 0x18c = stock)"); + static struct s5l8702_spi *s5l8702_spi2_dev; static void s5l8702_spi2_11b70(struct s5l8702_spi *sspi) @@ -182,7 +255,7 @@ static void s5l8702_spi2_11b70(struct s5l8702_spi *sspi) const unsigned int a4 = 1; const unsigned int clk_kunit = 24; u32 dd = clk_kunit * a4; - u32 u3c = 3 * clk_kunit * (a4 + 1); + u32 u3c = spi2_u3c ? (u32)spi2_u3c : 3 * clk_kunit * (a4 + 1); u32 clkdiv = 2; /* @@ -226,47 +299,122 @@ static void s5l8702_spi_cs(struct s5l8702_spi *sspi, bool assert) writel(pin, sspi->base + SPIPIN); } -static int s5l8702_wait_clear(struct s5l8702_spi *sspi, u32 mask) +static const char *s5l8702_fam_name(enum s5l8702_spi_fam f) +{ + switch (f) { + case SPI_FAM_ROS: + return "ROS(0x7C0/0xF800)"; + case SPI_FAM_CLASSIC: + return "Classic(0x1F0/0x3E00)"; + default: + return "auto"; + } +} + +/* Latch the observed encoding once, and say so. */ +static void s5l8702_latch_fam(struct s5l8702_spi *sspi, + enum s5l8702_spi_fam f, u32 status) +{ + if (sspi->fam == f) + return; + sspi->fam = f; + sspi->fam_latch_status = status; + dev_info(sspi->dev, "status encoding latched: %s (SPISTATUS=0x%08x)\n", + s5l8702_fam_name(f), status); +} + +/* + * Wait for the transmit side to go idle. + * + * Only the latched family is consulted. While still AUTO both are polled in + * the same loop so the first genuine completion decides, rather than one + * family being given a full guard interval of head start. + */ +static int s5l8702_wait_tx_idle(struct s5l8702_spi *sspi) { unsigned int guard = SPI_WAIT_GUARD; - u32 val; + u32 val = 0; while (guard--) { val = readl(sspi->base + SPISTATUS); - if ((val & mask) == 0) + + if (sspi->fam != SPI_FAM_CLASSIC && + (val & SPISTATUS_TXBUSY_ROS) == 0) { + s5l8702_latch_fam(sspi, SPI_FAM_ROS, val); return 0; - cpu_relax(); - } - /* Rockbox Classic uses 0x1f0 TX-empty — accept either family */ - if (mask == SPISTATUS_TXBUSY_ROS) { - guard = SPI_WAIT_GUARD / 4; - while (guard--) { - val = readl(sspi->base + SPISTATUS); - if ((val & 0x1f0) == 0) - return 0; - cpu_relax(); } + if (sspi->fam != SPI_FAM_ROS && + (val & SPISTATUS_TXLVL_MASK) == 0) { + s5l8702_latch_fam(sspi, SPI_FAM_CLASSIC, val); + return 0; + } + cpu_relax(); } + + sspi->tx_timeouts++; + sspi->last_status = val; return -ETIMEDOUT; } -static int s5l8702_wait_set(struct s5l8702_spi *sspi, u32 mask) +/* Wait for received data to be available. Same latching rule as TX. */ +static int s5l8702_wait_rx_ready(struct s5l8702_spi *sspi) { unsigned int guard = SPI_WAIT_GUARD; - u32 val; + u32 val = 0; while (guard--) { val = readl(sspi->base + SPISTATUS); - if (val & mask) + + if (sspi->fam != SPI_FAM_CLASSIC && + (val & SPISTATUS_RXRDY_ROS)) { + s5l8702_latch_fam(sspi, SPI_FAM_ROS, val); return 0; + } + if (sspi->fam != SPI_FAM_ROS && + (val & SPISTATUS_RXLVL_MASK)) { + s5l8702_latch_fam(sspi, SPI_FAM_CLASSIC, val); + return 0; + } cpu_relax(); } - /* Rockbox Classic RX ready 0x3e00 */ - if (mask == SPISTATUS_RXRDY_ROS) { - guard = SPI_WAIT_GUARD / 4; + + sspi->rx_timeouts++; + sspi->last_status = val; + return -ETIMEDOUT; +} + +/* + * Thin shims so existing call sites keep reading naturally. The mask + * argument now only selects which side is meant, not which encoding. + */ +static int s5l8702_wait_clear(struct s5l8702_spi *sspi, u32 mask) +{ + if (mask == SPISTATUS_TXBUSY_ROS || mask == SPISTATUS_TXLVL_MASK) + return s5l8702_wait_tx_idle(sspi); + + /* Anything else is a literal wait on the caller's own mask. */ + { + unsigned int guard = SPI_WAIT_GUARD; + + while (guard--) { + if ((readl(sspi->base + SPISTATUS) & mask) == 0) + return 0; + cpu_relax(); + } + } + return -ETIMEDOUT; +} + +static int s5l8702_wait_set(struct s5l8702_spi *sspi, u32 mask) +{ + if (mask == SPISTATUS_RXRDY_ROS || mask == SPISTATUS_RXLVL_MASK) + return s5l8702_wait_rx_ready(sspi); + + { + unsigned int guard = SPI_WAIT_GUARD; + while (guard--) { - val = readl(sspi->base + SPISTATUS); - if (val & 0x3e00) + if (readl(sspi->base + SPISTATUS) & mask) return 0; cpu_relax(); } @@ -307,26 +455,40 @@ static int s5l8702_spi2_pio_one(struct s5l8702_spi *sspi, for (i = 0; i < len; i++) { unsigned int guard = SPI_WAIT_GUARD; u32 st; + int ret; writel(1, sspi->base + SPIRXLIMIT); - while (guard--) { - st = readl(sspi->base + SPISTATUS); - if ((st & 0x1f0) != 0x100) - break; - cpu_relax(); + + /* + * TX-full is the one test that is genuinely encoding-specific + * here (0x100 is the full flag within the Classic level field), + * so keep it, but only while this instance is actually running + * the Classic encoding. Otherwise defer to the latched waits -- + * this loop previously asserted Classic unconditionally, which + * is how a ROS-encoded SPI2 ended up sampling RXDATA early. + */ + if (sspi->fam == SPI_FAM_CLASSIC) { + while (guard--) { + st = readl(sspi->base + SPISTATUS); + if ((st & SPISTATUS_TXFULL) == 0) + break; + cpu_relax(); + } + if (readl(sspi->base + SPISTATUS) & SPISTATUS_TXFULL) { + sspi->tx_timeouts++; + return -ETIMEDOUT; + } + } else { + ret = s5l8702_wait_tx_idle(sspi); + if (ret) + return ret; } - if ((readl(sspi->base + SPISTATUS) & 0x1f0) == 0x100) - return -ETIMEDOUT; + writel(tx ? tx[i] : 0xff, sspi->base + SPITXDATA); - guard = SPI_WAIT_GUARD; - while (guard--) { - st = readl(sspi->base + SPISTATUS); - if (st & 0x3e00) - break; - cpu_relax(); - } - if (!(readl(sspi->base + SPISTATUS) & 0x3e00)) - return -ETIMEDOUT; + + ret = s5l8702_wait_rx_ready(sspi); + if (ret) + return ret; { u8 b = (u8)readl(sspi->base + SPIRXDATA); @@ -354,13 +516,31 @@ static int s5l8702_spi_prepare_message(struct spi_controller *ctlr, return 0; } +/* + * One transfer's worth of PIO. + * + * @assert_cs: pull CS down before the preamble. False when a previous + * transfer already left it down. + * @deassert_cs: release CS when finished. False when the next transfer in + * this message must see the same selection. + * + * This used to assert and release unconditionally, which meant CS dropped + * between the transfers of a multi-transfer message. For a protocol that + * frames on chip select -- HBPP being the one that matters here -- that + * silently splits one frame into several, so a caller could not express a + * held-CS sequence through the SPI core at all and had to drive the + * registers itself. Honouring cs_change is what makes spi_sync() usable. + */ static int s5l8702_spi_pio_one(struct s5l8702_spi *sspi, - const u8 *tx, u8 *rx, unsigned int len) + const u8 *tx, u8 *rx, unsigned int len, + bool assert_cs, bool deassert_cs) { unsigned int i; int ret; - s5l8702_spi_cs(sspi, true); + if (assert_cs) + s5l8702_spi_cs(sspi, true); + sspi->cs_held = true; /* sub_4043D0 preamble */ writel(readl(sspi->base + SPICTRL) | SPICTRL_RESET_FIFO, @@ -423,7 +603,14 @@ static int s5l8702_spi_pio_one(struct s5l8702_spi *sspi, sspi->base + SPISETUP); out_cs: - s5l8702_spi_cs(sspi, false); + /* + * Always drop CS on error: leaving it asserted after a timeout would + * strand the bus for every later message. + */ + if (deassert_cs || ret) { + s5l8702_spi_cs(sspi, false); + sspi->cs_held = false; + } if (ret) { sspi->last_err = ret; sspi->last_status = readl(sspi->base + SPISTATUS); @@ -443,14 +630,36 @@ static int s5l8702_spi_pio_one(struct s5l8702_spi *sspi, return ret; } +/* + * Map the SPI core's cs_change rules onto the CS line. + * + * Within a message CS stays down between transfers; a transfer with + * cs_change set toggles it afterwards. On the final transfer the meaning + * inverts -- cs_change there means keep the device selected past the end + * of this message, which is how a caller holds one frame across several + * spi_sync() calls. + */ static int s5l8702_spi_transfer_one(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer) { struct s5l8702_spi *sspi = spi_controller_get_devdata(ctlr); + struct spi_message *msg = ctlr->cur_msg; + bool last = true; + bool deassert; (void)spi; - return s5l8702_spi_pio_one(sspi, xfer->tx_buf, xfer->rx_buf, xfer->len); + + if (msg) + last = list_is_last(&xfer->transfer_list, &msg->transfers); + + if (last) + deassert = !xfer->cs_change; + else + deassert = xfer->cs_change; + + return s5l8702_spi_pio_one(sspi, xfer->tx_buf, xfer->rx_buf, + xfer->len, !sspi->cs_held, deassert); } static int s5l8702_spi_probe(struct platform_device *pdev) @@ -474,6 +683,24 @@ static int s5l8702_spi_probe(struct platform_device *pdev) sspi->spi0 = res && res->start == SPI0_BASE_PHYS; sspi->spi2_nimbus = res && res->start == SPI2_BASE_PHYS; + /* + * Start in AUTO unless told otherwise, so each instance reports the + * encoding it actually uses instead of inheriting an assumption. The + * two instances have been seen to differ depending on which init path + * ran, which is precisely why this is per-instance and not global. + */ + switch (spi_family) { + case 1: + sspi->fam = SPI_FAM_ROS; + break; + case 2: + sspi->fam = SPI_FAM_CLASSIC; + break; + default: + sspi->fam = SPI_FAM_AUTO; + break; + } + /* Optional DT clocks (CLK_SPI* / secondary); ignore -ENOENT */ ret = devm_clk_bulk_get_all(&pdev->dev, &sspi->clks); if (ret > 0) { From 1c06a56c474aa984daf0cbb18a6f91961fc401b7 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:51:40 -0230 Subject: [PATCH 26/31] N31: platform glue -- GPIO keys, LCDIF clock, Tristar, shared header gpio-s5l8740 gains the button wiring and a release timer, and keeps the Bluetooth power control that some board variants route through it behind a switch that is off: the VIC routing is known but the EIC level semantics are not, and asserting a rail on a guess about edge polarity is not something to leave enabled by default. The LCD driver takes CLKCON with devm_ioremap rather than devm_ioremap_resource. The region is shared with the clock controller, so requesting it exclusively made whichever driver probed second fail -- which presented as the display working or the clocks working depending on probe order. Tristar carries the OSOS sub_11C8C ID/VBUS/CONDET reading, read-only with the status poll off. The 3.5mm path on this board is CS42 plus MikeyBus and not a Dx write, and the v36 bits that are still unmapped say so rather than being given a plausible meaning. include/linux/apple-n31.h is the interface the PMIC, GPIO, DMA, NAND and touch drivers share; it grows the rail and bring-up entry points the Bluetooth and touch work needed. A note on the canonical copy of these files: the tree under n7-upstream is a build artifact. rebuild-linux-gate0.sh syncs it from tools/linux-n31, so an edit made there is overwritten on the next build and the change appears to have had no effect. This tree and tools/linux-n31 are the sources. --- drivers/gpio/gpio-s5l8740.c | 313 +++++++++++++++++++- drivers/gpu/drm/tiny/s5l8740.c | 109 ++++++- drivers/i2c/busses/i2c-s5l8702.c | 0 drivers/misc/apple-tristar-cbtl1609.c | 128 ++++++++ drivers/video/backlight/backlight-s5l8740.c | 0 include/linux/apple-n31.h | 30 +- 6 files changed, 571 insertions(+), 9 deletions(-) mode change 100644 => 100755 drivers/gpio/gpio-s5l8740.c mode change 100644 => 100755 drivers/gpu/drm/tiny/s5l8740.c mode change 100644 => 100755 drivers/i2c/busses/i2c-s5l8702.c mode change 100644 => 100755 drivers/video/backlight/backlight-s5l8740.c diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c old mode 100644 new mode 100755 index 89d7178b766be5..04bd74573a25c8 --- a/drivers/gpio/gpio-s5l8740.c +++ b/drivers/gpio/gpio-s5l8740.c @@ -23,9 +23,13 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include #include #include @@ -38,10 +42,17 @@ #define S5L8740_GPIO_BANK_STRIDE 32 #define S5L8740_GPIO_DIN_OFF 0x04 +/* sub_428F70 target: input/pull enable, one bit per pad. */ +#define S5L8740_GPIO_INEN_OFF 0x0c #define S5L8740_GPIO_DOUT_OFF 0x08 #define S5L8740_GPIO_DIR_OFF 0x14 #define S5L8740_GPIOCMD_OFF 0x1e0 -#define S5L8740_GPIO_DEFAULT_NGPIO 128 /* BT host-wake is GPIO 119 */ +/* + * 32 banks of 8 across the 0x400 the block occupies. The old 128 cleared + * BT host-wake at 119 but not pad 200, which sub_17D4DC uses as the + * Bluetooth power control on some board variants. + */ +#define S5L8740_GPIO_DEFAULT_NGPIO 256 #define S5L8740_CMD_OUT_LOW 14 #define S5L8740_CMD_OUT_HIGH 15 @@ -110,6 +121,22 @@ static void s5l8740_pinmux_223C(struct device *dev, void __iomem *gpio_base) (unsigned int)ARRAY_SIZE(k_pinmux_table)); } +#define S5L8740_NKEYS 2 +/* Re-entries before a key is judged to be misconfigured, not pressed. */ +#define S5L8740_KEY_STORM_MAX 64 + +struct s5l8740_key { + struct s5l8740_gpio *sg; + struct delayed_work release; + const char *name; + unsigned int gpio; + unsigned int code; + int irq; + bool down; + bool masked; + unsigned int storm; +}; + struct s5l8740_gpio { void __iomem *base; void __iomem *gpiocmd; @@ -120,10 +147,115 @@ struct s5l8740_gpio { struct input_dev *input; u8 last40, last41, last86; bool din_inited; + struct s5l8740_key keys[S5L8740_NKEYS]; + bool keys_on_irq; }; static struct s5l8740_gpio *s5l8740_n31; +/* ------------------------------------------------------------------ */ +/* Pad-function debugfs */ +/* */ +/* gpiolib covers direction and value, but not the pad function nibble, */ +/* which is what most N31 bring-up questions are actually about: which */ +/* peripheral owns a pin right now. Reading it previously meant mapping */ +/* /dev/mem from userspace, which is an easy way to mistake a tool bug */ +/* for a hardware finding. */ +/* */ +/* pads one line per bank: PCON word then the eight nibbles */ +/* pad_set " " -- 14/15 drive an output low/high, */ +/* 0-7 select a peripheral function, 0xFFFE releases to in */ +/* ------------------------------------------------------------------ */ + +#define S5L8740_GPIO_BANKS 16 +#define S5L8740_GPIO_BANK_STRIDE 32 +#define S5L8740_GPIO_DIR 0x14 +#define S5L8740_GPIO_RELEASE 0xfffe + +static struct dentry *s5l8740_gpio_debugfs; + +static int s5l8740_pads_show(struct seq_file *s, void *unused) +{ + struct s5l8740_gpio *sg = s->private; + unsigned int bank, pin; + + if (!sg || !sg->base) + return -ENODEV; + seq_puts(s, "bank PCON dir pads 0..7 (function nibble)\n"); + for (bank = 0; bank < S5L8740_GPIO_BANKS; bank++) { + void __iomem *b = sg->base + S5L8740_GPIO_BANK_STRIDE * bank; + u32 pcon = readl(b); + u32 dir = readl(b + S5L8740_GPIO_DIR); + + seq_printf(s, "%-4u %08x %08x ", bank, pcon, dir); + for (pin = 0; pin < 8; pin++) + seq_printf(s, "%u%s", (pcon >> (4 * pin)) & 0xf, + pin == 7 ? "" : " "); + seq_printf(s, " (gpio %u-%u)\n", bank * 8, bank * 8 + 7); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(s5l8740_pads); + +static ssize_t s5l8740_pad_set_write(struct file *file, + const char __user *ubuf, + size_t len, loff_t *ppos) +{ + struct s5l8740_gpio *sg = file_inode(file)->i_private; + char buf[32]; + unsigned int gpio, func, bank, pin; + u32 dir; + + if (!sg || !sg->base || !sg->gpiocmd) + return -ENODEV; + if (len >= sizeof(buf)) + return -EINVAL; + if (copy_from_user(buf, ubuf, len)) + return -EFAULT; + buf[len] = 0; + if (sscanf(buf, "%u %i", &gpio, &func) != 2) + return -EINVAL; + if (gpio >= S5L8740_GPIO_BANKS * 8) + return -EINVAL; + + bank = gpio >> 3; + pin = gpio & 7; + dir = readl(sg->base + S5L8740_GPIO_BANK_STRIDE * bank + + S5L8740_GPIO_DIR); + if (func == S5L8740_GPIO_RELEASE) + dir &= ~BIT(pin); + else + dir |= BIT(pin); + writel(dir, sg->base + S5L8740_GPIO_BANK_STRIDE * bank + + S5L8740_GPIO_DIR); + writel((bank << 16) | (pin << 8) | + (func == S5L8740_GPIO_RELEASE ? 0 : (func & 0xff)), + sg->gpiocmd); + + dev_info(sg->gc.parent, "pad gpio %u (bank %u pin %u) -> func %u\n", + gpio, bank, pin, func); + return len; +} + +static const struct file_operations s5l8740_pad_set_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = s5l8740_pad_set_write, + .llseek = noop_llseek, +}; + +static void s5l8740_gpio_debugfs_init(struct s5l8740_gpio *sg) +{ + struct dentry *d = debugfs_create_dir("s5l8740_gpio", NULL); + + if (IS_ERR(d)) + return; + s5l8740_gpio_debugfs = d; + debugfs_create_file("pads", 0444, d, sg, &s5l8740_pads_fops); + debugfs_create_file("pad_set", 0200, d, sg, &s5l8740_pad_set_fops); +} + + void (*d1830_n31_din_nirq_hook)(void); EXPORT_SYMBOL_GPL(d1830_n31_din_nirq_hook); @@ -245,6 +377,20 @@ static int s5l8740_gpio_to_irq(struct gpio_chip *gc, unsigned int offset) if (!sg->eic_domain) return -ENXIO; + /* + * Stock arms an interrupt-capable pad with sub_43D38C(gpio, 0, 1) + * followed by sub_428F70(gpio, 1), which sets the bank's +0x0C bit. + * Without that second step the pad is muxed but its input stage is + * not enabled, so the EIC has nothing to level-detect. + */ + { + void __iomem *b = s5l8740_bank(sg, offset); + unsigned int pin = offset & 7; + + writel(readl(b + S5L8740_GPIO_INEN_OFF) | BIT(pin), + b + S5L8740_GPIO_INEN_OFF); + } + ret = s5l8740_eic_enable_gpio(offset, IRQ_TYPE_LEVEL_LOW); if (ret) return ret; @@ -437,8 +583,12 @@ static void s5l8740_din_timer(struct timer_list *t) /* OSOS GPIOButtonManager: only GPIO 40/41. Home/Play/Sleep * are PMIC bits; GPIO 86 is the nIRQ doorbell into d1830. */ - s5l8740_key_edge(sg, KEY_VOLUMEUP, v40, &sg->last40, "VOL+"); - s5l8740_key_edge(sg, KEY_VOLUMEDOWN, v41, &sg->last41, "VOL-"); + if (!sg->keys_on_irq) { + s5l8740_key_edge(sg, KEY_VOLUMEUP, v40, + &sg->last40, "VOL+"); + s5l8740_key_edge(sg, KEY_VOLUMEDOWN, v41, + &sg->last41, "VOL-"); + } if (v86 != sg->last86) { dev_dbg(sg->gc.parent, "n31-btn NIRQ86 %u->%u\n", sg->last86, v86); @@ -451,6 +601,154 @@ static void s5l8740_din_timer(struct timer_list *t) mod_timer(&sg->din_timer, jiffies + msecs_to_jiffies(50)); } +/* ------------------------------------------------------------------ */ +/* Volume keys on real interrupts */ +/* */ +/* The EIC only offers level-low, so a held key would re-assert forever */ +/* and genirq would retire the line as spurious -- the same failure the */ +/* PMIC event latches caused. The shape that works on a level-only */ +/* irqchip is: take the interrupt for the press, mask the line, then */ +/* poll only while the key is down, and unmask on release. */ +/* */ +/* So the press is interrupt-driven, which is the part latency is */ +/* visible in, and polling exists only for the tens of milliseconds a */ +/* finger is actually on the button instead of forever at 50 ms. */ +/* ------------------------------------------------------------------ */ + +/* + * Off until the EIC level semantics are understood. The VIC routing is + * now correct, so enabling this actually delivers interrupts -- and an + * idle active-low pad reads as permanently asserted, which wedges the + * system. The sweep is slower but it works. + */ +static bool btn_irq; +module_param(btn_irq, bool, 0444); +MODULE_PARM_DESC(btn_irq, + "Drive the volume keys from EIC interrupts (default Y)"); + +static unsigned int btn_release_ms = 30; +module_param(btn_release_ms, uint, 0644); +MODULE_PARM_DESC(btn_release_ms, + "Poll interval while a key is held, waiting for release"); + +static irqreturn_t s5l8740_key_isr(int irq, void *data) +{ + struct s5l8740_key *k = data; + struct s5l8740_gpio *sg = k->sg; + + /* + * Mask before reporting. The pad stays low for as long as the key is + * held, so leaving it unmasked here is an instant interrupt storm. + */ + if (!k->masked) { + disable_irq_nosync(irq); + k->masked = true; + } + /* + * If the line keeps re-asserting with nobody touching the key, the + * polarity or routing is wrong and re-enabling would spin here + * forever, taking userspace down with it. Give up instead and say + * so; the sweep still reports the key. + */ + if (++k->storm > S5L8740_KEY_STORM_MAX) { + pr_warn_once("n31-btn %s: runaway interrupt, left masked\n", + k->name); + k->sg->keys_on_irq = false; + return IRQ_HANDLED; + } + if (!k->down) { + k->down = true; + if (sg->input) { + input_report_key(sg->input, k->code, 1); + input_sync(sg->input); + } + dev_dbg(sg->gc.parent, "n31-btn %s PRESS (irq)\n", k->name); + } + schedule_delayed_work(&k->release, + msecs_to_jiffies(btn_release_ms)); + return IRQ_HANDLED; +} + +static void s5l8740_key_release_work(struct work_struct *work) +{ + struct s5l8740_key *k = container_of(to_delayed_work(work), + struct s5l8740_key, release); + struct s5l8740_gpio *sg = k->sg; + + /* Pad is active low, so a 1 here means the key came back up. */ + if (!s5l8740_din_bit(sg, k->gpio)) { + schedule_delayed_work(&k->release, + msecs_to_jiffies(btn_release_ms)); + return; + } + k->storm = 0; + if (k->down) { + k->down = false; + if (sg->input) { + input_report_key(sg->input, k->code, 0); + input_sync(sg->input); + } + dev_dbg(sg->gc.parent, "n31-btn %s release\n", k->name); + } + if (k->masked) { + k->masked = false; + enable_irq(k->irq); + } +} + +/* + * Returns the number of keys successfully wired. The caller keeps the + * legacy sweep running if this is not the full set, so a partial or + * failed setup degrades to the old behaviour instead of losing input. + */ +static unsigned int s5l8740_keys_irq_init(struct s5l8740_gpio *sg) +{ + static const struct { + unsigned int gpio, code; + const char *name; + } want[] = { + { 40, KEY_VOLUMEUP, "VOL+" }, + { 41, KEY_VOLUMEDOWN, "VOL-" }, + }; + unsigned int i, ok = 0; + + if (!btn_irq || !sg->eic_domain) + return 0; + + for (i = 0; i < ARRAY_SIZE(want) && i < S5L8740_NKEYS; i++) { + struct s5l8740_key *k = &sg->keys[i]; + int virq; + + k->sg = sg; + k->gpio = want[i].gpio; + k->code = want[i].code; + k->name = want[i].name; + INIT_DELAYED_WORK(&k->release, s5l8740_key_release_work); + + virq = s5l8740_gpio_to_irq(&sg->gc, k->gpio); + if (virq <= 0) { + dev_info(sg->gc.parent, + "key %s: no EIC irq (%d), staying on the sweep\n", + k->name, virq); + continue; + } + k->irq = virq; + if (devm_request_irq(sg->gc.parent, virq, s5l8740_key_isr, + IRQF_TRIGGER_LOW | IRQF_SHARED, + k->name, k)) { + dev_info(sg->gc.parent, + "key %s: irq %d busy, staying on the sweep\n", + k->name, virq); + k->irq = 0; + continue; + } + ok++; + dev_info(sg->gc.parent, "key %s on irq %d (EIC level-low)\n", + k->name, virq); + } + return ok; +} + static struct irq_domain *s5l8740_gpio_find_eic_domain(struct device *dev) { struct device_node *np = dev->of_node; @@ -555,10 +853,19 @@ static int s5l8740_gpio_probe(struct platform_device *pdev) } INIT_WORK(&sg->poweroff_work, s5l8740_poweroff_work); + /* + * GPIO 86 is the PMIC doorbell and still needs watching, so the + * sweep runs either way; it just stops carrying the volume keys + * once they are on interrupts. + */ + sg->keys_on_irq = s5l8740_keys_irq_init(sg) == S5L8740_NKEYS; + dev_info(dev, "volume keys: %s\n", + sg->keys_on_irq ? "EIC interrupts" : "50 ms sweep"); timer_setup(&sg->din_timer, s5l8740_din_timer, 0); mod_timer(&sg->din_timer, jiffies + msecs_to_jiffies(50)); s5l8740_n31 = sg; platform_set_drvdata(pdev, sg); + s5l8740_gpio_debugfs_init(sg); dev_info(dev, "S5L8740 GPIO @%pR ngpios=%u (GPIOCMD @+0x1E0) eic=%s\n", res, ngpios, sg->eic_domain ? "yes" : "no"); diff --git a/drivers/gpu/drm/tiny/s5l8740.c b/drivers/gpu/drm/tiny/s5l8740.c old mode 100644 new mode 100755 index 2bbf09e5403010..126c8c8aa90b2f --- a/drivers/gpu/drm/tiny/s5l8740.c +++ b/drivers/gpu/drm/tiny/s5l8740.c @@ -66,6 +66,7 @@ /* display power */ struct mutex power_lock; bool powered; + bool rail_held; /* modesetting */ uint32_t formats[8]; @@ -534,6 +535,58 @@ static struct drm_driver s5l8740_driver = { * Platform driver */ +/* + * Power control from userspace. The panel has no command interface, so + * an off/on cycle here is the whole recovery path after anything glitches + * the display rail: rail on, settle, LCDIF reset, reprogram, run, repaint. + * + * cat lcd_power 1 while the interface is running + * echo 0 > ... stop the interface and drop the rail + * echo 1 > ... full bring-up + */ +static ssize_t lcd_power_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct s5l8740_device *sdev = s5l8740_lcd_dev; + + if (!sdev) + return -ENODEV; + return sysfs_emit(buf, "%d\n", sdev->powered ? 1 : 0); +} + +static ssize_t lcd_power_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + unsigned int on; + int ret; + + if (kstrtouint(buf, 0, &on)) + return -EINVAL; + ret = n31_lcd_power(on != 0); + return ret ? ret : count; +} +static DEVICE_ATTR_RW(lcd_power); + +static ssize_t lcd_state_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct s5l8740_device *sdev = s5l8740_lcd_dev; + + if (!sdev || !sdev->lcdif) + return -ENODEV; + return sysfs_emit(buf, + "powered=%d rail_held=%d\n" + "CON=%08x STATUS=%08x SIZE=%08x\n" + "clkcon_window=%s\n", + sdev->powered, sdev->rail_held, + readl(sdev->lcdif + S5L8740_LCD_CON), + readl(sdev->lcdif + S5L8740_LCD_STATUS), + readl(sdev->lcdif + S5L8740_LCD_SIZE), + sdev->clkcon ? "mapped" : "absent"); +} +static DEVICE_ATTR_RO(lcd_state); + static int s5l8740_probe(struct platform_device *pdev) { struct s5l8740_device *sdev; @@ -576,9 +629,18 @@ static int s5l8740_probe(struct platform_device *pdev) */ res = platform_get_resource(pdev, IORESOURCE_MEM, 1); if (res) { - sdev->clkcon = devm_ioremap_resource(&pdev->dev, res); - if (IS_ERR(sdev->clkcon)) - sdev->clkcon = NULL; + /* + * Map without claiming. This window is the clock controller, which + * the clock-controller node already owns and the IIS driver also + * maps, so an exclusive devm_ioremap_resource() always lost the + * race and returned -EBUSY. The driver then carried on with + * clkcon = NULL and quietly stopped gating the clocks across an + * LCDIF reset -- a real behaviour change reported only as an error + * line nobody acted on. CLKCON is a shared block; sharing it is + * correct, claiming it is not. + */ + sdev->clkcon = devm_ioremap(&pdev->dev, res->start, + resource_size(res)); } if (!sdev->clkcon) drm_info(dev, @@ -588,6 +650,34 @@ static int s5l8740_probe(struct platform_device *pdev) sdev->powered = true; s5l8740_lcd_dev = sdev; + /* + * Take the display rail for the panel we inherited. Without this + * nobody holds LDO_4, so the PMU's global rail repair -- which + * clears bits 6 and 7 by design -- has nothing to preserve and + * switches the panel off underneath us. That is the white screen: + * the first audio bring-up after boot replays that sequence. + */ + if (lcd_manage_rail) { + int (*get)(unsigned int) = + (int (*)(unsigned int))__symbol_get("n31_pmu_rail_get"); + + if (get) { + if (get(N31_PMU_RAIL_DISPLAY)) + drm_warn(dev, "display rail claim failed\n"); + else + sdev->rail_held = true; + __symbol_put("n31_pmu_rail_get"); + } else { + drm_info(dev, + "PMIC absent; display rail unprotected\n"); + } + } + + if (device_create_file(&pdev->dev, &dev_attr_lcd_power)) + drm_warn(dev, "lcd_power sysfs\n"); + if (device_create_file(&pdev->dev, &dev_attr_lcd_state)) + drm_warn(dev, "lcd_state sysfs\n"); + /* GATE0: log WTF handoff, never rewrite CON/PHTIME */ drm_info(dev, "LCDIF handoff CON=%08x PHTIME=%08x (untouched)\n", readl(sdev->lcdif + S5L8740_LCD_CON), @@ -680,6 +770,18 @@ static int s5l8740_probe(struct platform_device *pdev) return 0; } +/* + * The LCDIF scans out of the framebuffer by DMA. Across a kexec that + * memory belongs to the next kernel, so the controller would keep + * fetching whatever landed there and the panel would show it. Powering + * down stops the fetch; n31_lcd_power suspends the DRM clients on the + * way, so nothing is left drawing into a stopped interface. + */ +static void s5l8740_shutdown(struct platform_device *pdev) +{ + n31_lcd_power(false); +} + static void s5l8740_remove(struct platform_device *pdev) { struct s5l8740_device *sdev = platform_get_drvdata(pdev); @@ -701,6 +803,7 @@ static struct platform_driver s5l8740_platform_driver = { }, .probe = s5l8740_probe, .remove = s5l8740_remove, + .shutdown = s5l8740_shutdown, }; module_platform_driver(s5l8740_platform_driver); diff --git a/drivers/i2c/busses/i2c-s5l8702.c b/drivers/i2c/busses/i2c-s5l8702.c old mode 100644 new mode 100755 diff --git a/drivers/misc/apple-tristar-cbtl1609.c b/drivers/misc/apple-tristar-cbtl1609.c index b42bd14302032a..d67a13cb3948ca 100755 --- a/drivers/misc/apple-tristar-cbtl1609.c +++ b/drivers/misc/apple-tristar-cbtl1609.c @@ -846,6 +846,131 @@ static ssize_t poll_show(struct device *dev, struct device_attribute *attr, } static DEVICE_ATTR_RW(poll); +/* + * Read-only telemetry. + * + * TriStar is not the charger -- it decides what is attached to Lightning + * and where the signal paths go, while the D1830 owns the battery and the + * power path. What it does hold is the attach/detach state, which is the + * missing input to any charging policy, so making its state observable is + * the useful contribution here rather than trying to drive anything. + * + * tristar_stats counters and decoded state in one place + * tristar_regs the dump with labels on the registers we can justify + * tristar_watch echo 1 to snapshot, read to see only what moved + * + * The watch is the one that matters for attach detection: snapshot, change + * the cable, read back, and whatever moved is the short list. + */ +static ssize_t tristar_stats_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct apple_tristar *ts = dev_get_drvdata(dev); + + if (!ts) + return -ENODEV; + + return sysfs_emit(buf, + "polls=%u deltas=%u writes=%u i2c_fail_streak=%u poll_disabled=%d\n" + "dump_ok=%d dump_flat=%d i2c_echo=%d seen_mask=%08x\n" + "id=%s id_valid=%d id_off=%u accx=%02x dx=%02x\n" + "reg11=%02x reg11_ret=%d osos_event=%02x prev=%02x cf9_latch=%d cfa=%02x\n", + ts->polls, ts->deltas, ts->writes, ts->i2c_fail_streak, + ts->poll_disabled, + ts->dump_ok, ts->dump_flat, ts->i2c_echo, ts->seen_mask, + ts->id_name ? ts->id_name : "unknown", ts->id_valid, + ts->id_off, ts->accx, ts->dx, + ts->reg11, ts->reg11_ret, ts->osos_event, ts->prev_osos_event, + ts->cf9_latch, ts->cfa_state); +} +static DEVICE_ATTR_RO(tristar_stats); + +/* + * Only 0x11 has a name we can defend -- CBTL1610 configuration status, + * and it may NAK or echo on a 1609. The rest are listed as offsets with + * their observed values so a diff has somewhere to point; naming them + * before correlation would turn guesses into apparent fact, which is + * exactly how a rail block at 0x40 got invented for the PMIC. + */ +static ssize_t tristar_regs_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct apple_tristar *ts = dev_get_drvdata(dev); + unsigned int i; + int len = 0; + + if (!ts) + return -ENODEV; + if (!ts->dump_ok) + return sysfs_emit(buf, "no valid dump (dump_ok=0)\n"); + + mutex_lock(&ts->lock); + for (i = 0; i < TRISTAR_DUMP_LEN && len < PAGE_SIZE - 48; i++) { + if (!ts->last_dump[i]) + continue; /* zeros are the overwhelming majority */ + len += scnprintf(buf + len, PAGE_SIZE - len, + "0x%02x = %02x%s\n", i, ts->last_dump[i], + i == 0x11 ? " (CBTL1610 config status)" : ""); + } + mutex_unlock(&ts->lock); + len += scnprintf(buf + len, PAGE_SIZE - len, + "# non-zero offsets only; flat=%d echo=%d\n", + ts->dump_flat, ts->i2c_echo); + return len; +} +static DEVICE_ATTR_RO(tristar_regs); + +static u8 tristar_snap[TRISTAR_DUMP_LEN]; +static bool tristar_snap_valid; + +static ssize_t tristar_watch_show(struct device *dev, + struct device_attribute *a, char *buf) +{ + struct apple_tristar *ts = dev_get_drvdata(dev); + unsigned int i, changed = 0; + int len = 0; + + if (!ts) + return -ENODEV; + if (!tristar_snap_valid) + return sysfs_emit(buf, + "no snapshot; echo 1 > tristar_watch first\n"); + + mutex_lock(&ts->lock); + for (i = 0; i < TRISTAR_DUMP_LEN && len < PAGE_SIZE - 64; i++) { + if (ts->last_dump[i] == tristar_snap[i]) + continue; + changed++; + len += scnprintf(buf + len, PAGE_SIZE - len, + "0x%02x %02x -> %02x (xor %02x)\n", + i, tristar_snap[i], ts->last_dump[i], + tristar_snap[i] ^ ts->last_dump[i]); + } + mutex_unlock(&ts->lock); + if (!changed) + len += scnprintf(buf + len, PAGE_SIZE - len, + "no change\n"); + return len; +} + +static ssize_t tristar_watch_store(struct device *dev, + struct device_attribute *a, + const char *buf, size_t count) +{ + struct apple_tristar *ts = dev_get_drvdata(dev); + + if (!ts) + return -ENODEV; + if (buf[0] != '1') + return -EINVAL; + mutex_lock(&ts->lock); + memcpy(tristar_snap, ts->last_dump, TRISTAR_DUMP_LEN); + tristar_snap_valid = true; + mutex_unlock(&ts->lock); + return count; +} +static DEVICE_ATTR_RW(tristar_watch); + static struct attribute *tristar_attrs[] = { &dev_attr_dump.attr, &dev_attr_poke.attr, @@ -857,6 +982,9 @@ static struct attribute *tristar_attrs[] = { &dev_attr_value.attr, &dev_attr_verify.attr, &dev_attr_poll.attr, + &dev_attr_tristar_stats.attr, + &dev_attr_tristar_regs.attr, + &dev_attr_tristar_watch.attr, NULL, }; ATTRIBUTE_GROUPS(tristar); diff --git a/drivers/video/backlight/backlight-s5l8740.c b/drivers/video/backlight/backlight-s5l8740.c old mode 100644 new mode 100755 diff --git a/include/linux/apple-n31.h b/include/linux/apple-n31.h index 008bd1800739b1..ce7efd67e40000 100755 --- a/include/linux/apple-n31.h +++ b/include/linux/apple-n31.h @@ -41,6 +41,10 @@ int d1830_audio_rails(void); /* gpio-d1830.c — power the touch controller rail, for the Nimbus driver. */ int d1830_nimbus_rail(bool on); +/* Bluetooth companion rails: reg 87 bits 7:6, reg 88 bit 0 and bits 6:4, + * the fields sub_51688C zeroes on de-init. Saves the boot values on the + * first power-off so power-on can restore them. */ +int d1830_bt_rails(bool on); /* * gpio-d1830.c — refcounted rail control. Rail ids index the PMU rail @@ -58,9 +62,29 @@ int d1830_nimbus_rail(bool on); * No stock path enables a rail for the audio codec: its analog supply is * always on. Do not add one. */ -#define N31_PMU_RAIL_TOUCH 2 /* PMU_LDO_3 */ -#define N31_PMU_RAIL_DISPLAY 3 /* PMU_LDO_4 */ -#define N31_PMU_RAIL_ACCESSORY 4 /* PMU_LDO_5 */ +/* + * Indexes into n31_pmu_rails[], NOT RetailOS logical rail IDs. The two + * numbering systems overlap and disagree, which is a trap worth naming: + * + * this table index 2 -> 0x10 bit 5 -> Nimbus + * RetailOS logical 4 -> 0x10 bit 5 -> Nimbus + * + * Same physical bit, different number, and sub_6644 converts logical + * IDs 1..10 into selectors 6..15 before sub_7484 turns those into + * register and bit. So a bare 4 in a decompiler listing and a bare 4 + * here mean different rails. Always carry the {register, mask} pair. + * + * The physical assignments, from sub_7484: + * 0x10 bits 2..7 are selectors 6..11 + * 0x11 bits 0..3 are selectors 12..15 + * Only 0x10 bit 5 has a proven consumer -- Nimbus, via the call chain + * sub_20766(1) to sub_439B00(1) to sub_6644(4) to sub_7484(9). The + * display and accessory names below are this project's mapping and are + * not re-proven from the firmware. + */ +#define N31_PMU_RAIL_TOUCH 2 /* PMU_LDO_3, 0x10 bit 5, Nimbus */ +#define N31_PMU_RAIL_DISPLAY 3 /* PMU_LDO_4, 0x10 bit 6, unproven */ +#define N31_PMU_RAIL_ACCESSORY 4 /* PMU_LDO_5, 0x10 bit 7, unproven */ int n31_pmu_rail_get(unsigned int id); void n31_pmu_rail_put(unsigned int id); From 86e10e63696cbde0cf8ee7e02ebd0dc59cb9bb26 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 13:51:40 -0230 Subject: [PATCH 27/31] N31: device tree -- BT rail supply, panic=0, nodrm variant resync The PMIC node gains a regulators subnode holding the bt rail, and the Bluetooth nodes gain the supply properties that make the ordering work: bt-supply on the companion and vbat-supply on the controller. Without a DT node naming it, the regulator is registered but unreachable, and the consumer has nothing to defer on. panic=-1 became panic=0. Rebooting instantly on panic meant every oops scrolled off with the reboot and the only symptom left was a device that came back up, which cost real time on the scheduling-while-atomic fault in the SPI wait. Halting leaves the trace on the glass. The nodrm variant is resynced against the main DTS; it had drifted far enough that the two described different hardware in places. --- .../boot/dts/samsung/s5l8740-n31-nodrm.dts | 940 +++++++++--------- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 103 +- 2 files changed, 560 insertions(+), 483 deletions(-) mode change 100644 => 100755 arch/arm/boot/dts/samsung/s5l8740-n31.dts diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts index 7e3cb6a2efd745..20659039a7a8a5 100755 --- a/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31-nodrm.dts @@ -1,470 +1,470 @@ -// SPDX-License-Identifier: GPL-2.0+ -/* - * Apple S5L8740 SoC - * Board name: N31 - * Product name: iPod nano (7th generation) - */ -/dts-v1/; - -#include -#include -#include -#include - -/ { - #address-cells = <1>; - #size-cells = <1>; - model = "Apple iPod nano (7th generation)"; - compatible = "apple,n31", "samsung,s5l8740"; - - aliases { - serial0 = &uart3; - console = &uart3; - }; - - chosen { - /* quiet: suppress non-critical boot spam; nimbus uses explicit prints */ - bootargs = "panic=-1 loglevel=8 nohlt"; - stdout-path = "serial0"; - }; - - nclk: external_clock { - compatible = "fixed-clock"; - #clock-cells = <0>; - clock-frequency = <24000000>; - clock-output-names = "nclk"; - }; - - // pclk: clock-ref { - // compatible = "fixed-clock"; - // #clock-cells = <0>; - // clock-frequency = <6000000>; - // clock-output-names = "pclk"; - // }; - - cpus { - #address-cells = <1>; - #size-cells = <0>; - - cpu@0 { - device_type = "cpu"; - reg = <0>; - compatible = "arm,cortex-a5"; - }; - }; - - memory@8000000 { - device_type = "memory"; - reg = <0x08000000 0x04000000>; - }; - - soc { - compatible = "simple-bus"; - #address-cells = <1>; - #size-cells = <1>; - ranges; - - vic0: interrupt-controller@38e00000 { - compatible = "arm,pl192-vic"; - interrupt-controller; - reg = <0x38e00000 0x1000>; - #interrupt-cells = <1>; - }; - - vic1: interrupt-controller@38e01000 { - compatible = "arm,pl192-vic"; - interrupt-controller; - reg = <0x38e01000 0x1000>; - #interrupt-cells = <1>; - }; - - clkctrl: clock-controller@3c500000 { - compatible = "samsung,s5l8740-clock", "samsung,s5l8702-clock"; - reg = <0x3c500000 0x100>; - #clock-cells = <1>; - /* boot-minimal: WTF already ungated clocks — skip SEC mass reprogram */ - apple,skip-clkcon-ensure; - apple,skip-clkcon-bringup; - status = "okay"; - }; - eic: interrupt-controller@39700000 { - compatible = "apple,s5l8740-eic", "samsung,s5l8740-eic"; - reg = <0x39700000 0x1000>; - interrupt-controller; - #interrupt-cells = <2>; - /* - * Chain ONLY VIC EXT1 (group1 / GPIOs 32-63): Vol 40/41, Nimbus 38. - * Chaining all EXT0-6 hung boot previously. - */ - interrupt-parent = <&vic0>; - interrupts = <1>; /* EXT1 */ - /* boot-minimal v2: no EIC chain (IRQ storm risk without Nimbus) */ - status = "disabled"; - }; - - timer: timer@3c700000 { - compatible = "samsung,s5l8720-timer"; - reg = <0x3c700000 0x20000>; - interrupt-parent = <&vic0>; - interrupts = <7>; - }; - - /* Probe lcdif early — before SPI/Nimbus (boot-minimal bisect) */ - lcdif: lcdif@38300000 { - compatible = "samsung,s5l8740-lcdif"; - reg = <0x38300000 0x10000>; - status = "disabled"; - }; - - backlight: backlight@3e000000 { - compatible = "apple,s5l8740-backlight", "samsung,s5l8740-backlight"; - reg = <0x3e000000 0x100>; - default-brightness = <62>; - status = "disabled"; - }; - - /* if this one is not first, pointers get overwritten - in the driver and it fails to initialize */ - uart3: serial@3dd00000 { - compatible = "apple,s5l-uart"; - reg = <0x3dd00000 0x3c>; - clocks = <&nclk>, <&nclk>; - clock-names = "uart", "clk_uart_baud0"; - reg-io-width = <4>; - interrupt-parent = <&vic0>; - interrupts = <27>; - status = "okay"; - }; - - uart0: serial@3cc00000 { - compatible = "apple,s5l-uart"; - reg = <0x3cc00000 0x3c>; - clocks = <&nclk>, <&nclk>; - clock-names = "uart", "clk_uart_baud0"; - reg-io-width = <4>; - interrupt-parent = <&vic0>; - interrupts = <24>; - status = "okay"; - }; - - uart1: serial@3db00000 { - compatible = "apple,s5l-uart"; - reg = <0x3db00000 0x3c>; - clocks = <&nclk>, <&nclk>; - clock-names = "uart", "clk_uart_baud0"; - reg-io-width = <4>; - interrupt-parent = <&vic0>; - interrupts = <25>; - status = "okay"; - - /* BCM2078KUBG on UART1 @115200 — Vincent CodePatches → BCM2076B1.hcd */ - bluetooth { - compatible = "brcm,bcm2078", "brcm,bcm4329-bt"; - max-speed = <115200>; - shutdown-gpios = <&gpio 97 GPIO_ACTIVE_LOW>; - device-wakeup-gpios = <&gpio 98 GPIO_ACTIVE_HIGH>; - host-wakeup-gpios = <&gpio 119 GPIO_ACTIVE_HIGH>; - firmware-name = "brcm/BCM2076B1.hcd"; - /* Boot-safe: defer BCM until init up — see bcm2078-bt.c */ - status = "disabled"; - }; - }; - - uart2: serial@3dc00000 { - compatible = "apple,s5l-uart"; - reg = <0x3dc00000 0x3c>; - clocks = <&nclk>, <&nclk>; - clock-names = "uart", "clk_uart_baud0"; - reg-io-width = <4>; - interrupt-parent = <&vic0>; - interrupts = <26>; - status = "okay"; - - mikeybus { - compatible = "apple,mikeybus", "apple,n31-mikeybus"; - current-speed = <115200>; - }; - }; - - usbphy: usbphy@3c400000 { - /* N31 uses s5l87xx PHY sequence (NOT Nano3 8702 ramp) */ - compatible = "apple,s5l8740-otgphy", "apple,s5l87xx-otgphy"; - reg = <0x3c400000 0x100>; - status = "disabled"; - #phy-cells = <0>; - }; - - usbotg_hs: usb@38400000 { - compatible = "apple,s5l8740-usb", "apple,s5l87xx-usb"; - reg = <0x38400000 0x40000>; - interrupt-parent = <&vic0>; - interrupts = <19>; - phys = <&usbphy>; - phy-names = "usb2-phy"; - clocks = <&clkctrl CLK_USBOTG>, <&clkctrl CLK_USBPHY>, <&nclk>; - clock-names = "otg", "phy", "ref"; - /* rx=256 + np-tx=256 leaves room for periodic TX FIFOs (536+256 overflowed) */ - g-rx-fifo-size = <256>; - g-np-tx-fifo-size = <256>; - status = "disabled"; - dr_mode = "peripheral"; - }; - - - spi2: spi@3d200000 { - /* N31 Nimbus on SPI2 — CLK_SPI2 (+ alt/secondary) + pinmux in driver */ - compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; - reg = <0x3d200000 0x100>; - clocks = <&clkctrl CLK_SPI2>, <&clkctrl CLK_SPI2_2>, <&clkctrl CLK_SPI2_ALT>; - clock-names = "spi", "spi-2", "spi-alt"; - /* SPI DMA peri IDs OPEN in N31 RE — Nimbus uses proven PIO path */ - #address-cells = <1>; - #size-cells = <0>; - status = "disabled"; - nimbus: touchscreen@0 { - compatible = "apple,nimbus"; - reg = <0>; - spi-max-frequency = <1000000>; - enable-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; - reset-gpios = <&gpio 39 GPIO_ACTIVE_LOW>; - attn-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; - /* GPIO38 → EIC group1 → VIC EXT1 */ - interrupts-extended = <&eic 38 IRQ_TYPE_LEVEL_LOW>; - status = "disabled"; - }; - }; - - /* - * SPI0 @0x3C300000: panel in IpodSec, CS42 in RetailOS. - * Keep disabled so LCD SPI ownership is undisturbed; enable only - * after panel/CS42 mux is sequenced. - */ - /* SPI0: CS42 control. Panel pixels are LCDIF@383 — not this bus. */ - spi0: spi@3c300000 { - compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; - reg = <0x3c300000 0x100>; - clocks = <&clkctrl CLK_SPI0>, <&clkctrl CLK_SPI0_2>; - clock-names = "spi", "spi-2"; - #address-cells = <1>; - #size-cells = <0>; - status = "disabled"; - cs42l81: codec@0 { - compatible = "cirrus,cs42l81", "apple,338s1146"; - reg = <0>; - spi-max-frequency = <1000000>; - status = "okay"; - }; - }; - - i2s0: i2s@3ca00000 { - compatible = "apple,s5l8740-i2s", "samsung,s5l8740-i2s"; - reg = <0x3ca00000 0x1000>; - clocks = <&clkctrl CLK_I2S0>, <&clkctrl CLK_CG16_9>; - clock-names = "i2s", "cg16"; - dmas = <&dmac 12 0>, <&dmac 13 0>; - dma-names = "tx", "rx"; - #sound-dai-cells = <0>; - status = "disabled"; - }; - - i2s2: i2s@3d400000 { - compatible = "apple,s5l8740-iis2"; - reg = <0x3d400000 0x1000>; - clocks = <&clkctrl CLK_I2S2>, <&clkctrl CLK_CG16_11>; - clock-names = "i2s", "cg16"; - dmas = <&dmac 16 0>, <&dmac 17 0>; - dma-names = "tx", "rx"; - status = "disabled"; - }; - - nano7_audio: audio { - compatible = "apple,n31-audio"; - apple,cpu = <&i2s0>; - status = "disabled"; - }; - - wdt: watchdog@3c800000 { - compatible = "apple,s5l8740-syscon", "syscon", "simple-mfd"; - reg = <0x3c800000 0x8>; - - reboot: syscon-reboot@3c800000 { - compatible = "syscon-reboot"; - offset = <0x0>; - value = <0x100000>; - }; - }; - - /* - * Full S5L8740 banked GPIO (was: 2-line bcm6345 hack at 0x3cf000a4). - * Keep label gpio5 disabled so old phandles fail closed if any remain. - */ - gpio5: gpio-hack@3cf000a4 { - compatible = "brcm,bcm6345-gpio"; - reg-names = "dat"; - reg = <0x3cf000a4 0x4>; - #gpio-cells = <2>; - gpio-controller; - ngpios = <2>; - status = "disabled"; - }; - - gpio: gpio@3cf00000 { - compatible = "apple,s5l8740-gpio", "samsung,s5l8740-gpio"; - reg = <0x3cf00000 0x400>; - clocks = <&clkctrl CLK_GPIO>; - clock-names = "gpio"; - apple,eic = <&eic>; - /* boot-minimal: skip SEC sub_223C mass pinmux (WTF handoff) */ - apple,skip-sec-pinmux; - #gpio-cells = <2>; - gpio-controller; - ngpios = <128>; /* BT host-wake GPIO 119 */ - status = "disabled"; - }; - - i2c0: i2c@3c600000 { - compatible = "samsung,s5l8702-i2c"; - samsung,write-busy-poll; - #address-cells = <1>; - #size-cells = <0>; - reg = <0x3c600000 0x100>; - clocks = <&clkctrl CLK_I2C0>, <&clkctrl CLK_I2C0_2>; - clock-names = "i2c", "i2c-2"; - clock-frequency = <100000>; - interrupt-parent = <&vic0>; - interrupts = <21>; - status = "disabled"; - - /* - * Tristar CBTL1609A1 — public "0x34 write / 0x35 read" is 8-bit; - * Linux DT 7-bit address is 0x1a. - * RetailOS RE: zero Dx/mux register writes observed — dump stays - * flat until accessory; only apple,init-sequence may write. - */ - tristar: lightning-mux@1a { - compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", - "apple,n31-tristar"; - reg = <0x1a>; - /* Parent i2c0 is disabled in this DTB; child cannot probe. */ - status = "okay"; - }; - }; - - i2c1: i2c@3c900000 { - compatible = "samsung,s5l8702-i2c"; - samsung,write-busy-poll; - #address-cells = <1>; - #size-cells = <0>; - reg = <0x3c900000 0x100>; - clocks = <&clkctrl CLK_I2C1>, <&clkctrl CLK_I2C1_2>; - clock-names = "i2c", "i2c-2"; - clock-frequency = <100000>; /* UPDATE ME */ - interrupt-parent = <&vic0>; - interrupts = <22>; - status = "disabled"; - - /* ST lis3lv02d binding; mount-matrix optional (skip until board orient known) */ - lis3dc: lis331dlh@18 { - compatible = "st,lis3lv02d"; - reg = <0x18>; - /* Identity until board orientation proven on HW */ - mount-matrix = "1", "0", "0", - "0", "1", "0", - "0", "0", "1"; - status = "okay"; - }; - - /* Duplicate of i2c0 tristar@1a — keep disabled; primary is i2c0 only */ - tristar_i2c1: lightning-mux@1a { - compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; - reg = <0x1a>; - status = "disabled"; - }; - /* Also try literal 7-bit 0x34 in case public notes meant that */ - /* 0x34 is 8-bit write addr form — NOT a Linux 7-bit address */ - tristar_i2c1_34: lightning-mux@34 { - compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; - reg = <0x34>; - status = "disabled"; - }; - - d1830: pmic@73 { - compatible = "dlg,d1830-gpio"; - reg = <0x73>; - gpio-controller; - #gpio-cells = <2>; - /* N31: no Home/Play. Sleep/Wake is PMIC — map TBD. */ - dlg,gpio-map = <0x07 5>; /* SEC sub_27F4: Sleep/Wake status bit5 */ - }; - }; - - sha1: sha1@38000000 { - compatible = "samsung,s5l8702-sha1"; - reg = <0x38000000 0x100>; - clocks = <&clkctrl CLK_SHA1>; - clock-names = "sha1"; - status = "disabled"; - }; - - /* - * PL080 DMAC0/1 — OSOS pairs 0x38200000 + 0x38700000. - * NOT 0x384 (DWC OTG). Peri IDs: N31 RE IIS0 12/13, IIS2 16/17. - */ - dmac: dma-controller@38200000 { - compatible = "apple,s5l8740-pl080", "arm,pl080"; - reg = <0x38200000 0x1000>, <0x38700000 0x1000>; - clocks = <&clkctrl CLK_DMAC0>, <&clkctrl CLK_DMAC1>; - clock-names = "dmac0", "dmac1"; - interrupt-parent = <&vic0>; - interrupts = <16>, <17>; - #dma-cells = <2>; - status = "disabled"; - }; - - aes: aes@38c00000 { - compatible = "samsung,s5l8702-aes"; - reg = <0x38c00000 0x100>; - clocks = <&clkctrl CLK_AES>; - clock-names = "aes"; - status = "disabled"; - }; - - prng: prng@3c100000 { - compatible = "samsung,s5l8702-prng"; - reg = <0x3c100000 0x100>; - clocks = <&clkctrl CLK_PRNG>; - clock-names = "prng"; - status = "disabled"; - }; - }; - gpio-keys { - compatible = "gpio-keys"; - status = "disabled"; - - button-power { - label = "Sleep/Wake"; - gpios = <&d1830 0 GPIO_ACTIVE_LOW>; - linux,code = ; - debounce-interval = <50>; - wakeup-source; - status = "okay"; - }; - - /* Active-low CONFIRMED; Vol+ = GPIO40 provisional (swap with 41 if inverted) */ - button-volup { - label = "Volume Up"; - gpios = <&gpio 40 GPIO_ACTIVE_LOW>; - linux,code = ; - debounce-interval = <30>; - status = "okay"; - }; - - button-voldown { - label = "Volume Down"; - gpios = <&gpio 41 GPIO_ACTIVE_LOW>; - linux,code = ; - debounce-interval = <30>; - status = "okay"; - }; - }; -}; +// SPDX-License-Identifier: GPL-2.0+ +/* + * Apple S5L8740 SoC + * Board name: N31 + * Product name: iPod nano (7th generation) + */ +/dts-v1/; + +#include +#include +#include +#include + +/ { + #address-cells = <1>; + #size-cells = <1>; + model = "Apple iPod nano (7th generation)"; + compatible = "apple,n31", "samsung,s5l8740"; + + aliases { + serial0 = &uart3; + console = &uart3; + }; + + chosen { + /* quiet: suppress non-critical boot spam; nimbus uses explicit prints */ + bootargs = "panic=-1 loglevel=8 nohlt"; + stdout-path = "serial0"; + }; + + nclk: external_clock { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <24000000>; + clock-output-names = "nclk"; + }; + + // pclk: clock-ref { + // compatible = "fixed-clock"; + // #clock-cells = <0>; + // clock-frequency = <6000000>; + // clock-output-names = "pclk"; + // }; + + cpus { + #address-cells = <1>; + #size-cells = <0>; + + cpu@0 { + device_type = "cpu"; + reg = <0>; + compatible = "arm,cortex-a5"; + }; + }; + + memory@8000000 { + device_type = "memory"; + reg = <0x08000000 0x04000000>; + }; + + soc { + compatible = "simple-bus"; + #address-cells = <1>; + #size-cells = <1>; + ranges; + + vic0: interrupt-controller@38e00000 { + compatible = "arm,pl192-vic"; + interrupt-controller; + reg = <0x38e00000 0x1000>; + #interrupt-cells = <1>; + }; + + vic1: interrupt-controller@38e01000 { + compatible = "arm,pl192-vic"; + interrupt-controller; + reg = <0x38e01000 0x1000>; + #interrupt-cells = <1>; + }; + + clkctrl: clock-controller@3c500000 { + compatible = "samsung,s5l8740-clock", "samsung,s5l8702-clock"; + reg = <0x3c500000 0x100>; + #clock-cells = <1>; + /* boot-minimal: WTF already ungated clocks — skip SEC mass reprogram */ + apple,skip-clkcon-ensure; + apple,skip-clkcon-bringup; + status = "okay"; + }; + eic: interrupt-controller@39700000 { + compatible = "apple,s5l8740-eic", "samsung,s5l8740-eic"; + reg = <0x39700000 0x1000>; + interrupt-controller; + #interrupt-cells = <2>; + /* + * Chain ONLY VIC EXT1 (group1 / GPIOs 32-63): Vol 40/41, Nimbus 38. + * Chaining all EXT0-6 hung boot previously. + */ + interrupt-parent = <&vic0>; + interrupts = <1>; /* EXT1 */ + /* boot-minimal v2: no EIC chain (IRQ storm risk without Nimbus) */ + status = "disabled"; + }; + + timer: timer@3c700000 { + compatible = "samsung,s5l8720-timer"; + reg = <0x3c700000 0x20000>; + interrupt-parent = <&vic0>; + interrupts = <7>; + }; + + /* Probe lcdif early — before SPI/Nimbus (boot-minimal bisect) */ + lcdif: lcdif@38300000 { + compatible = "samsung,s5l8740-lcdif"; + reg = <0x38300000 0x10000>; + status = "disabled"; + }; + + backlight: backlight@3e000000 { + compatible = "apple,s5l8740-backlight", "samsung,s5l8740-backlight"; + reg = <0x3e000000 0x100>; + default-brightness = <62>; + status = "disabled"; + }; + + /* if this one is not first, pointers get overwritten + in the driver and it fails to initialize */ + uart3: serial@3dd00000 { + compatible = "apple,s5l-uart"; + reg = <0x3dd00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <27>; + status = "okay"; + }; + + uart0: serial@3cc00000 { + compatible = "apple,s5l-uart"; + reg = <0x3cc00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <24>; + status = "okay"; + }; + + uart1: serial@3db00000 { + compatible = "apple,s5l-uart"; + reg = <0x3db00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <25>; + status = "okay"; + + /* BCM2078KUBG on UART1 @115200 — Vincent CodePatches → BCM2076B1.hcd */ + bluetooth { + compatible = "brcm,bcm2078", "brcm,bcm4329-bt"; + max-speed = <115200>; + shutdown-gpios = <&gpio 97 GPIO_ACTIVE_LOW>; + device-wakeup-gpios = <&gpio 98 GPIO_ACTIVE_HIGH>; + host-wakeup-gpios = <&gpio 119 GPIO_ACTIVE_HIGH>; + firmware-name = "brcm/BCM2076B1.hcd"; + /* Boot-safe: defer BCM until init up — see bcm2078-bt.c */ + status = "disabled"; + }; + }; + + uart2: serial@3dc00000 { + compatible = "apple,s5l-uart"; + reg = <0x3dc00000 0x3c>; + clocks = <&nclk>, <&nclk>; + clock-names = "uart", "clk_uart_baud0"; + reg-io-width = <4>; + interrupt-parent = <&vic0>; + interrupts = <26>; + status = "okay"; + + mikeybus { + compatible = "apple,mikeybus", "apple,n31-mikeybus"; + current-speed = <115200>; + }; + }; + + usbphy: usbphy@3c400000 { + /* N31 uses s5l87xx PHY sequence (NOT Nano3 8702 ramp) */ + compatible = "apple,s5l8740-otgphy", "apple,s5l87xx-otgphy"; + reg = <0x3c400000 0x100>; + status = "disabled"; + #phy-cells = <0>; + }; + + usbotg_hs: usb@38400000 { + compatible = "apple,s5l8740-usb", "apple,s5l87xx-usb"; + reg = <0x38400000 0x40000>; + interrupt-parent = <&vic0>; + interrupts = <19>; + phys = <&usbphy>; + phy-names = "usb2-phy"; + clocks = <&clkctrl CLK_USBOTG>, <&clkctrl CLK_USBPHY>, <&nclk>; + clock-names = "otg", "phy", "ref"; + /* rx=256 + np-tx=256 leaves room for periodic TX FIFOs (536+256 overflowed) */ + g-rx-fifo-size = <256>; + g-np-tx-fifo-size = <256>; + status = "disabled"; + dr_mode = "peripheral"; + }; + + + spi2: spi@3d200000 { + /* N31 Nimbus on SPI2 — CLK_SPI2 (+ alt/secondary) + pinmux in driver */ + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; + reg = <0x3d200000 0x100>; + clocks = <&clkctrl CLK_SPI2>, <&clkctrl CLK_SPI2_2>, <&clkctrl CLK_SPI2_ALT>; + clock-names = "spi", "spi-2", "spi-alt"; + /* SPI DMA peri IDs OPEN in N31 RE — Nimbus uses proven PIO path */ + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + nimbus: touchscreen@0 { + compatible = "apple,nimbus"; + reg = <0>; + spi-max-frequency = <1000000>; + enable-gpios = <&gpio 14 GPIO_ACTIVE_HIGH>; + reset-gpios = <&gpio 39 GPIO_ACTIVE_LOW>; + attn-gpios = <&gpio 38 GPIO_ACTIVE_LOW>; + /* GPIO38 → EIC group1 → VIC EXT1 */ + interrupts-extended = <&eic 38 IRQ_TYPE_LEVEL_LOW>; + status = "disabled"; + }; + }; + + /* + * SPI0 @0x3C300000: panel in IpodSec, CS42 in RetailOS. + * Keep disabled so LCD SPI ownership is undisturbed; enable only + * after panel/CS42 mux is sequenced. + */ + /* SPI0: CS42 control. Panel pixels are LCDIF@383 — not this bus. */ + spi0: spi@3c300000 { + compatible = "apple,s5l8702-spi", "samsung,s5l8740-spi", "samsung,s5l8702-spi"; + reg = <0x3c300000 0x100>; + clocks = <&clkctrl CLK_SPI0>, <&clkctrl CLK_SPI0_2>; + clock-names = "spi", "spi-2"; + #address-cells = <1>; + #size-cells = <0>; + status = "disabled"; + cs42l81: codec@0 { + compatible = "cirrus,cs42l81", "apple,338s1146"; + reg = <0>; + spi-max-frequency = <1000000>; + status = "okay"; + }; + }; + + i2s0: i2s@3ca00000 { + compatible = "apple,s5l8740-i2s", "samsung,s5l8740-i2s"; + reg = <0x3ca00000 0x1000>; + clocks = <&clkctrl CLK_I2S0>, <&clkctrl CLK_CG16_9>; + clock-names = "i2s", "cg16"; + dmas = <&dmac 12 0>, <&dmac 13 0>; + dma-names = "tx", "rx"; + #sound-dai-cells = <0>; + status = "disabled"; + }; + + i2s2: i2s@3d400000 { + compatible = "apple,s5l8740-iis2"; + reg = <0x3d400000 0x1000>; + clocks = <&clkctrl CLK_I2S2>, <&clkctrl CLK_CG16_11>; + clock-names = "i2s", "cg16"; + dmas = <&dmac 16 0>, <&dmac 17 0>; + dma-names = "tx", "rx"; + status = "disabled"; + }; + + nano7_audio: audio { + compatible = "apple,n31-audio"; + apple,cpu = <&i2s0>; + status = "disabled"; + }; + + wdt: watchdog@3c800000 { + compatible = "apple,s5l8740-syscon", "syscon", "simple-mfd"; + reg = <0x3c800000 0x8>; + + reboot: syscon-reboot@3c800000 { + compatible = "syscon-reboot"; + offset = <0x0>; + value = <0x100000>; + }; + }; + + /* + * Full S5L8740 banked GPIO (was: 2-line bcm6345 hack at 0x3cf000a4). + * Keep label gpio5 disabled so old phandles fail closed if any remain. + */ + gpio5: gpio-hack@3cf000a4 { + compatible = "brcm,bcm6345-gpio"; + reg-names = "dat"; + reg = <0x3cf000a4 0x4>; + #gpio-cells = <2>; + gpio-controller; + ngpios = <2>; + status = "disabled"; + }; + + gpio: gpio@3cf00000 { + compatible = "apple,s5l8740-gpio", "samsung,s5l8740-gpio"; + reg = <0x3cf00000 0x400>; + clocks = <&clkctrl CLK_GPIO>; + clock-names = "gpio"; + apple,eic = <&eic>; + /* boot-minimal: skip SEC sub_223C mass pinmux (WTF handoff) */ + apple,skip-sec-pinmux; + #gpio-cells = <2>; + gpio-controller; + ngpios = <128>; /* BT host-wake GPIO 119 */ + status = "disabled"; + }; + + i2c0: i2c@3c600000 { + compatible = "samsung,s5l8702-i2c"; + samsung,write-busy-poll; + #address-cells = <1>; + #size-cells = <0>; + reg = <0x3c600000 0x100>; + clocks = <&clkctrl CLK_I2C0>, <&clkctrl CLK_I2C0_2>; + clock-names = "i2c", "i2c-2"; + clock-frequency = <100000>; + interrupt-parent = <&vic0>; + interrupts = <21>; + status = "disabled"; + + /* + * Tristar CBTL1609A1 — public "0x34 write / 0x35 read" is 8-bit; + * Linux DT 7-bit address is 0x1a. + * RetailOS RE: zero Dx/mux register writes observed — dump stays + * flat until accessory; only apple,init-sequence may write. + */ + tristar: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1", + "apple,n31-tristar"; + reg = <0x1a>; + /* Parent i2c0 is disabled in this DTB; child cannot probe. */ + status = "okay"; + }; + }; + + i2c1: i2c@3c900000 { + compatible = "samsung,s5l8702-i2c"; + samsung,write-busy-poll; + #address-cells = <1>; + #size-cells = <0>; + reg = <0x3c900000 0x100>; + clocks = <&clkctrl CLK_I2C1>, <&clkctrl CLK_I2C1_2>; + clock-names = "i2c", "i2c-2"; + clock-frequency = <100000>; /* UPDATE ME */ + interrupt-parent = <&vic0>; + interrupts = <22>; + status = "disabled"; + + /* ST lis3lv02d binding; mount-matrix optional (skip until board orient known) */ + lis3dc: lis331dlh@18 { + compatible = "st,lis3lv02d"; + reg = <0x18>; + /* Identity until board orientation proven on HW */ + mount-matrix = "1", "0", "0", + "0", "1", "0", + "0", "0", "1"; + status = "okay"; + }; + + /* Duplicate of i2c0 tristar@1a — keep disabled; primary is i2c0 only */ + tristar_i2c1: lightning-mux@1a { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + reg = <0x1a>; + status = "disabled"; + }; + /* Also try literal 7-bit 0x34 in case public notes meant that */ + /* 0x34 is 8-bit write addr form — NOT a Linux 7-bit address */ + tristar_i2c1_34: lightning-mux@34 { + compatible = "apple,tristar-cbtl1609", "nxp,cbtl1609a1"; + reg = <0x34>; + status = "disabled"; + }; + + d1830: pmic@73 { + compatible = "dlg,d1830-gpio"; + reg = <0x73>; + gpio-controller; + #gpio-cells = <2>; + /* N31: no Home/Play. Sleep/Wake is PMIC — map TBD. */ + dlg,gpio-map = <0x07 5>; /* SEC sub_27F4: Sleep/Wake status bit5 */ + }; + }; + + sha1: sha1@38000000 { + compatible = "samsung,s5l8702-sha1"; + reg = <0x38000000 0x100>; + clocks = <&clkctrl CLK_SHA1>; + clock-names = "sha1"; + status = "disabled"; + }; + + /* + * PL080 DMAC0/1 — OSOS pairs 0x38200000 + 0x38700000. + * NOT 0x384 (DWC OTG). Peri IDs: N31 RE IIS0 12/13, IIS2 16/17. + */ + dmac: dma-controller@38200000 { + compatible = "apple,s5l8740-pl080", "arm,pl080"; + reg = <0x38200000 0x1000>, <0x38700000 0x1000>; + clocks = <&clkctrl CLK_DMAC0>, <&clkctrl CLK_DMAC1>; + clock-names = "dmac0", "dmac1"; + interrupt-parent = <&vic0>; + interrupts = <16>, <17>; + #dma-cells = <2>; + status = "disabled"; + }; + + aes: aes@38c00000 { + compatible = "samsung,s5l8702-aes"; + reg = <0x38c00000 0x100>; + clocks = <&clkctrl CLK_AES>; + clock-names = "aes"; + status = "disabled"; + }; + + prng: prng@3c100000 { + compatible = "samsung,s5l8702-prng"; + reg = <0x3c100000 0x100>; + clocks = <&clkctrl CLK_PRNG>; + clock-names = "prng"; + status = "disabled"; + }; + }; + gpio-keys { + compatible = "gpio-keys"; + status = "disabled"; + + button-power { + label = "Sleep/Wake"; + gpios = <&d1830 0 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <50>; + wakeup-source; + status = "okay"; + }; + + /* Active-low CONFIRMED; Vol+ = GPIO40 provisional (swap with 41 if inverted) */ + button-volup { + label = "Volume Up"; + gpios = <&gpio 40 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <30>; + status = "okay"; + }; + + button-voldown { + label = "Volume Down"; + gpios = <&gpio 41 GPIO_ACTIVE_LOW>; + linux,code = ; + debounce-interval = <30>; + status = "okay"; + }; + }; +}; diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts old mode 100644 new mode 100755 index 625458b19b2b29..187325dab137ae --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -24,7 +24,7 @@ chosen { /* U-Boot CONFIG_BOOTARGS overrides this. g_ether is built-in (0525:a4a2). */ - bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=-1 clk_ignore_unused init=/init"; + bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=0 clk_ignore_unused init=/init"; stdout-path = "serial0"; /* * U-Boot ft_board_setup adds apple,n31-isys-addr / apple,n31-isys-size @@ -100,14 +100,25 @@ #interrupt-cells = <1>; }; - /* GPIO → EIC → VIC EXTn. EXT1 = Vol 40/41; EXT3 = PMIC GPIO86. */ + /* + * GPIO -> EIC -> VIC. The two groups do not share a controller, + * which is why this needs interrupts-extended: + * + * group 1 (GPIO 32-63, volume keys 40/41) -> VIC1 line 0 + * group 2 (GPIO 64-95, PMIC nIRQ 86) -> VIC0 line 31 + * + * Measured on the device by toggling each group's INTEN and + * watching which VIC RAWINTR bit followed. The previous entry + * put both on vic0 lines 1 and 3; neither line exists for the + * EIC, so no GPIO interrupt was ever delivered -- buttons and + * the touch nIRQ alike latched pending in the EIC forever. + */ eic: interrupt-controller@39700000 { compatible = "apple,s5l8740-eic", "samsung,s5l8740-eic"; reg = <0x39700000 0x1000>; interrupt-controller; #interrupt-cells = <2>; - interrupt-parent = <&vic0>; - interrupts = <1>, <3>; + interrupts-extended = <&vic1 0>, <&vic0 31>; apple,eic-groups = <1>, <2>; status = "okay"; }; @@ -157,16 +168,53 @@ * Companion GPIO/FM lives under apple,n31-bcm2078-companion. */ bluetooth { compatible = "brcm,bcm4329-bt", "brcm,bcm2078"; - max-speed = <115200>; /* - * No control GPIOs here on purpose. 97/98/119 were - * listed as shutdown/device-wakeup/host-wakeup, but - * sub_15DD5C claims those three at function 2 when FM - * powers on and releases them when it powers off -- - * they are the IIS2 PCM pads. Handing them to hci_bcm - * made it drive the capture bus as GPIOs. The real BT - * control pins are not identified yet. + * sphwBluetooth_Init opens the port and immediately + * sends 01 18 FC 06 00 00 00 9F 24 00 -- vendor command + * 0xFC18, baud rate 0x00249F00 = 2400000. hci_bcm sends + * the same command from this property, so leaving it at + * 115200 would have run the link eight times slower than + * stock once the controller started answering. + */ + /* + * A dependency, not a power role. + * + * The chip driver owns the sequence -- rails, REG_ON, the + * wake pins -- but it can only run after gpio-d1830 loads, + * at about t=7.4s. hci_bcm probes the serdev at t=4.8s and + * sent 0xFC18 into a part that had no supply yet, timing + * out before the chip driver ever got its turn. + * + * Naming the same rail here makes the bridge defer until + * the provider exists, which is the only ordering hook a + * serdev node has. Enabling an already-enabled regulator is + * a refcount, so this does not take power management away + * from the chip driver. + */ + vbat-supply = <&bt_rail>; + max-speed = <2400000>; + /* + * 97/98/119 were once listed as shutdown/device-wakeup/ + * host-wakeup and must not go here: sub_15DD5C claims + * those three at function 2 when FM powers on and + * releases them when it powers off -- they are the IIS2 + * PCM pads, and handing them to hci_bcm made it drive + * the capture bus as GPIOs. + * + * GPIO 70 is a different pin and is decomp-backed. + * sphwBluetooth_Init (sub_570054, reached from + * BluetoothOSBridgeInit) opens with + * + * sub_43D38C(0x46u, 1, 1); + * + * pad 0x46 = 70, mode 1 = output, value 1 = high, before + * any HCI traffic. That is the power-on control the + * controller needs; without it hci0 binds and then + * HCI_Reset (0x0c03) times out, which is exactly what we + * were seeing. hci_bcm drives shutdown-gpios high on + * power-on, which matches. */ + shutdown-gpios = <&gpio 70 GPIO_ACTIVE_HIGH>; firmware-name = "brcm/BCM2076B1.hcd"; status = "okay"; }; @@ -270,7 +318,13 @@ apple,skip-sec-pinmux; #gpio-cells = <2>; gpio-controller; - ngpios = <128>; + /* + * The block is mapped 0x400, which is 32 banks of 8, so 256 + * pads exist. 128 was enough for BT host-wake at 119 but put + * pad 0xC8 = 200 out of reach, and sub_17D4DC drives that one + * as the Bluetooth power control on board variants 1 and 2. + */ + ngpios = <256>; status = "okay"; }; @@ -278,6 +332,13 @@ * Does not own UART1 — that is bluetooth { } under uart1 → hci_bcm. */ bcm2078_companion: bcm2078-companion { compatible = "apple,n31-bcm2078-companion"; + /* + * The chip driver owns power. The UART-to-HCI bridge is a dumb + * transport and takes no supply: hci_bcm carries power handling + * upstream only because it doubles as the chip driver on most + * boards, which is not the case here. + */ + bt-supply = <&bt_rail>; status = "okay"; }; @@ -353,6 +414,22 @@ interrupt-parent = <&eic>; interrupts = <86 IRQ_TYPE_LEVEL_LOW>; monitored-battery = <&battery>; + + /* + * The Bluetooth rail, named so a consumer can reference it. + * + * bcm2078-bt is built into the kernel and probes long before + * this driver, which is a module. Asking for power through a + * direct hook in that window just fails. Asking for it as a + * regulator makes the kernel defer the consumer's probe until + * this node exists, which is the ordering guarantee that was + * missing. + */ + regulators { + bt_rail: bt { + regulator-name = "bt"; + }; + }; }; }; From 876747fdb05842c6a0c90fcceac229c7c4a3b9d7 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 23:43:33 -0230 Subject: [PATCH 28/31] ftl: pick the BPB whose FAT is actually a FAT The volume mounted its boot sector and then failed every directory walk: FAT-fs (s5l8740-ftl): error, fat_get_cluster: invalid cluster chain The wrong BPB candidate was selected and the whole volume sat six sectors out. n31_validate_fat_critical() checks that its nine critical sectors *read*; it never looks at what came back, so both candidates scored 9/9: BPB_CAND #1 fmss_lba=49285 weave=..ad crit=9/9 <- selected, newest BPB_CAND #2 fmss_lba=49279 weave=..ac crit=9/9 49285 - 49279 = 6, and the FAT belongs to the older one. Sector 0 still read as a valid BPB and sector 1 as valid FSInfo because both are found by content and are static. Dumping the FAT region showed the tell: a well-formed FAT32 chain stepping 0x400 entries per 4096-byte sector, starting six sectors early, so vfat began its walk mid-table. Every FAT32 opens entry 0 with the media descriptor in the low byte and the EOC nibbles above it, and entry 1 all ones. n31_fat_first_sector_ok() reads L->fat_start and checks exactly that, masked to the 28 bits FAT32 entries carry, against the media byte in the BPB at offset 0x15. It gates both the "perfect critical set wins immediately" early-out and the scoring below. bpb_try fmss=49285 crit=9/9 fatsig=0 bpb_try fmss=49279 crit=9/9 fatsig=1 fat_base_lba=49279 valid=1 selected=2 MOUNT_OK -> Apps iPod_Control n31os System Volume Information 496 files under iPod_Control/Music, full-file reads, 0 errors n31_fat_semantic_validate() is declared and never called, which is why the bpb log has always printed itunesdb=0 music_dirs=0 -- stale zeros, not findings. It would have caught this too. Co-Authored-By: Claude Opus 5 --- drivers/misc/ftl-s5l8740-core.c | 587 ++++++++++++++++++++++++++++++- drivers/misc/ftl-s5l8740-csmap.c | 69 +++- drivers/misc/nand-s5l8740.c | 23 +- drivers/misc/whimory-s5l8740.h | 14 + 4 files changed, 673 insertions(+), 20 deletions(-) diff --git a/drivers/misc/ftl-s5l8740-core.c b/drivers/misc/ftl-s5l8740-core.c index dfae972b89132f..f3090273edc242 100755 --- a/drivers/misc/ftl-s5l8740-core.c +++ b/drivers/misc/ftl-s5l8740-core.c @@ -63,6 +63,112 @@ MODULE_PARM_DESC(import_l2v_oracle, * reads pages from page 0 until it finds a blank one, so a genuinely open * block stops early and a block that is actually full reads all 127. */ +/* + * Rebuild a closed superblock whose BTOC has no BTE array. + * + * Off, and the measurement that turned it off is worth keeping. Enabling it + * answered the question it was built for -- the 70 such superblocks report + * 127 pages/sb, so they are full sealed blocks and not empty ones, exactly + * as the decomp of sub_567E3C predicted. But the second number says the + * work is unnecessary and the third says it is harmful: + * + * btoc_fallback sbs=70 pages=8890 hits=35560 (127 pages/sb) + * mapped_lbas 949733 -> 949733 + * + * 35560 mappings applied and not one new LBA. They did not add anything; + * they replaced existing mappings one for one with different VBAs. Reads + * then came back holding the wrong logical block: + * + * sftl lba mismatch want=0x8042b meta=0x7fed0 type=01 + * sftl lba mismatch want=0x7483f meta=0x7482c type=01 + * + * with scattered deltas, which is what overwriting a correct map with a + * differently-derived one looks like. The build immediately before this + * read every file on the volume with no error. + * + * The reason is the same one that made the uncapped open rebuild + * destructive: on this volume no superblock is newer than the checkpoint + * (weave newer=0 older=2182), so per-page meta has nothing to add over the + * CXT and every override is a regression. The CXT already maps everything + * those blocks hold, which is precisely why mapped_lbas did not move. + * + * Kept as a switch rather than deleted because the reasoning is + * volume-specific: a device whose checkpoint is genuinely behind its data + * would need this, and would show it by mapped_lbas rising when it runs. + */ +/* + * Dump the raw CXT tree records covering a window of logical space. + * + * The mismatch narrowed to the logical cursor in whimory_cxt_parse_tree: + * the runs land in the right VBAs and carry the wrong LBA labels. The + * cursor is checked against each record's declared start LBA and never + * disagreed, so whatever goes wrong is internal to a record and cancels out + * by the end of it -- which cannot be reasoned about from summary counters. + * + * cxt_dump_lba names the window, cxt_dump_len its size. Every pair whose + * logical extent touches it is printed with the cursor value it was given, + * so the pair whose span does not match the run it describes is visible + * directly. + */ +/* + * Trace the first records of every CXT superblock walk. + * + * Two readings are left for the two newest checkpoints, whose records are + * eight bytes of 0xff followed by file content, and they need opposite + * fixes: + * + * the walk is landing on the wrong page -> an addressing bug here + * a record can be a header-only terminator followed by pre-erase + * contents -> accept it and keep walking + * + * The discriminator is whether the meta and the data come from the same + * place. Printing each slot's own meta beside its own data head, with the + * page's four metas alongside, settles it: a page whose slots all carry + * SFTL_CXT meta while their data is XML is a page we should not be reading, + * whereas a CXT page whose later slots hold stale content is a terminator. + */ +/* + * Replay this virtual block even if the checkpoint says it is covered. + * + * The last hypothesis for the handful of files that will not read. Their + * mapping comes from the checkpoint, is structurally sound, and points at a + * page holding much older data -- so either the checkpoint entry is stale + * and the correction lives in per-page meta that the skip threw away, or it + * is not stale and the fault is elsewhere. Forcing one block through the + * replay separates those without changing the rule for anything else. + * + * 0 disables. This is a test instrument, not a fix: if it works the fix is + * to make the skip rule stop being wrong about blocks like it, not to keep + * a hardcoded exception. + */ +static unsigned int force_replay_vblock; +module_param(force_replay_vblock, uint, 0644); +MODULE_PARM_DESC(force_replay_vblock, + "replay this vblock even when the CXT covers it (0 = off)"); + +static unsigned int cxt_trace_sb; +module_param(cxt_trace_sb, uint, 0644); +MODULE_PARM_DESC(cxt_trace_sb, "trace this many leading records of each CXT superblock"); + +static unsigned int cxt_dump_lba; +module_param(cxt_dump_lba, uint, 0644); +MODULE_PARM_DESC(cxt_dump_lba, "dump CXT tree pairs covering this LBA (0 = off)"); + +static unsigned int cxt_dump_len = 16384; +module_param(cxt_dump_len, uint, 0644); +MODULE_PARM_DESC(cxt_dump_len, "size of the cxt_dump_lba window"); + +static unsigned int cxt_dump_max = 96; +module_param(cxt_dump_max, uint, 0644); +MODULE_PARM_DESC(cxt_dump_max, "cap on dumped pairs"); + +static bool btoc_meta_fallback; +module_param(btoc_meta_fallback, bool, 0644); +MODULE_PARM_DESC(btoc_meta_fallback, + "Rebuild BTOC-less closed superblocks from per-page meta " + "(default N; overrides the CXT and corrupts the map when " + "the CXT is newer, which it is on N31)"); + static unsigned int max_open_sbs = 4096; module_param(max_open_sbs, uint, 0644); MODULE_PARM_DESC(max_open_sbs, @@ -842,6 +948,17 @@ static u32 whimory_sb_ofs_to_vba(const struct whimory *w, u32 sb_idx, u32 ofs) ofs % w->sftl.vbas_per_page); } +/* The virtual block a bank-major superblock index names. */ +static u32 whimory_cxt_sb_vblock(const struct whimory *w, u32 sb_idx) +{ + u32 per_ce = w->geom.num_cau * w->sftl.user_blocks; + + if (!per_ce || !w->sftl.user_blocks) + return 0; + return (sb_idx % per_ce) % w->sftl.user_blocks; +} + + static int whimory_unpack_vba(const struct whimory *w, u32 vba, u32 *ce, u32 *cau, u32 *vblock, u32 *page, u32 *slot) @@ -971,6 +1088,7 @@ static int whimory_range_split(struct whimory *w, struct whimory_range *r, right->len = r->len - left_len; right->vba = r->vba + left_len; right->weave = r->weave; + right->src = r->src; r->len = left_len; whimory_range_link(&w->ranges, right); w->sftl.range_nodes++; @@ -1005,6 +1123,7 @@ static int whimory_range_insert_new(struct whimory *w, u32 start, u32 len, n->len = len; n->vba = vba; n->weave = w->sftl.claim_weave; + n->src = w->sftl.claim_source; whimory_range_link(&w->ranges, n); w->sftl.range_nodes++; w->sftl.map_gen++; @@ -4389,11 +4508,21 @@ static unsigned int whimory_cxt_collect_sbs(struct whimory *w, continue; vblock = whimory_vfl_virt(w, sb->cau, sb->block); idx = whimory_sb_index(w, sb->ce, sb->cau, vblock); + /* + * One candidate per virtual block, not per plane. A CXT is a + * single superblock striped across all four (ce, cau) planes; + * counting each plane separately turned one checkpoint into + * four candidates with four weaves, of which the two highest + * appeared to contribute nothing. + */ for (j = 0; j < n; j++) - if (out[j].sb == idx) + if (whimory_cxt_sb_vblock(w, out[j].sb) == vblock) break; - if (j < n) + if (j < n) { + if (sb->weave > out[j].weave) + out[j].weave = sb->weave; continue; + } out[n].sb = idx; out[n].weave = sb->weave; n++; @@ -4439,6 +4568,61 @@ static const char *whimory_cxt_tag_name(u8 tag) * the 16-byte record metadata in `meta`. Page reads are cached across the * four slots of a physical page by the caller. */ +/* + * Read one record of a checkpoint, by offset within the whole superblock. + * + * The offset is a native VBA offset, which means it walks all four planes + * in the order the FTL wrote them. That matters more than it looks. + * + * A CXT is one superblock striped across the four (ce, cau) planes, and its + * records run in VBA order: plane 0 slots 0..3 of page 0, then plane 1, + * plane 2, plane 3, then page 1 of plane 0. The weave in each record's meta + * says so directly -- 580d..5810 on plane 0, 5811..5814 on plane 1, then + * 5815, 5819, and 581d back on plane 0. + * + * This used to take a bank-major superblock index and walk one plane at a + * time, which visited the same records in the wrong order: every record of + * plane 0, then every record of plane 1, and so on. The L2V tree cannot + * survive that. Each record declares the LBA it continues from and the + * parse rejects a record whose declared start does not match the running + * cursor, so out-of-order records either abort the walk or, worse, attach a + * run to the wrong logical position. + * + * It also made one checkpoint look like four candidates with four different + * weaves, of which the two "newest" contributed nothing -- which read as a + * stale map when it was a misread one. + */ +static int whimory_cxt_read_ofs(struct whimory *w, u32 vblock, u32 ofs, + u8 *data, u8 *meta, u8 *spare, u32 *last_key) +{ + struct whimory_sftl *s = &w->sftl; + u32 ce, cau, vb, page, slot, pblock, key; + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 per_sb = s->pages_per_sb * planes * s->vbas_per_page; + u32 vba = vblock * per_sb + ofs; + int ret; + + ret = whimory_unpack_vba(w, vba, &ce, &cau, &vb, &page, &slot); + if (ret) + return ret; + cau = whimory_vfl_bank(w, cau, vb); + pblock = whimory_vfl_phys(w, cau, vb); + key = ((ce & 0xf) << 28) | ((cau & 0xf) << 24) | + ((pblock & 0xffff) << 8) | (page & 0xff); + if (key != *last_key) { + ret = whimory_cs_read_page(w, ce, cau, pblock, page, + s->data_page, + S5L8740_NAND_PAGE_SIZE, + spare, S5L8740_NAND_META_SIZE); + if (ret) + return ret; + *last_key = key; + } + memcpy(data, s->data_page + slot * WHIMORY_LBA_SIZE, WHIMORY_LBA_SIZE); + memcpy(meta, spare + slot * WHIMORY_META_SIZE, WHIMORY_META_SIZE); + return 0; +} + static int whimory_cxt_read_vba(struct whimory *w, u32 sb_idx, u32 ofs, u8 *data, u8 *meta, u8 *spare, u32 *last_key) { @@ -4731,10 +4915,33 @@ static int whimory_cxt_parse_tree(struct whimory *w, const u8 *data, return 0; lba = get_unaligned_le32(data); span = get_unaligned_le32(data + 4); - if (span == 0xffffffffu) + if (span == 0xffffffffu) { + /* + * Header says nothing follows, and until now that was + * indistinguishable from a record that parsed and contained + * nothing. They are not the same: the two newest checkpoints + * on this device contribute no extents while the map is built + * from the two older ones, leaving it a generation behind the + * volume -- which is what the bad reads are. If those + * checkpoints are being turned away here, this says so and + * shows the header that did it. + */ + w->sftl.cxt_hdr_skipped++; + if (w->sftl.cxt_hdr_skipped <= 6) + dev_info(w->dev, + "CXT_HDR_SKIP n=%u lba=%u span=0x%08x first16=%16ph\n", + w->sftl.cxt_hdr_skipped, lba, span, data); return 0; - if (span != WHIMORY_CXT_CONTIG_SPAN) + } + if (span != WHIMORY_CXT_CONTIG_SPAN) { + w->sftl.cxt_hdr_bad++; + if (w->sftl.cxt_hdr_bad <= 6) + dev_info(w->dev, + "CXT_HDR_BAD n=%u lba=%u span=0x%08x want=0x%08x first16=%16ph\n", + w->sftl.cxt_hdr_bad, lba, span, + (u32)WHIMORY_CXT_CONTIG_SPAN, data); return -EINVAL; + } if (*lba_valid && lba != *next_lba) { dev_warn(w->dev, "CXT_TREE lba discontinuity want=%u got=%u\n", @@ -4743,12 +4950,33 @@ static int whimory_cxt_parse_tree(struct whimory *w, const u8 *data, } *lba_valid = true; + if (cxt_dump_lba && lba < cxt_dump_lba + cxt_dump_len) + dev_info(w->dev, + "CXT_REC header lba=%u contig=0x%x pairs<=%u\n", + lba, span, n - 1); + for (i = 1; i < n; i++) { vba = get_unaligned_le32(data + 8 * i); span = get_unaligned_le32(data + 8 * i + 4); if (vba == 0xffffffffu || !span) break; w->sftl.cxt_records_seen++; + + if (cxt_dump_lba && w->sftl.cxt_dumped < cxt_dump_max && + lba + span > cxt_dump_lba && + lba < cxt_dump_lba + cxt_dump_len) { + bool hole = vba >= WHIMORY_CXT_VBA_HOLE || + vba >= w->l2v.invalid_vba; + char d[64]; + + w->sftl.cxt_dumped++; + whimory_vba_describe(w, vba, d, sizeof(d)); + dev_info(w->dev, + "CXT_PAIR[%u] lba=%u..%u vba=0x%08x span=%u %s%s\n", + i, lba, lba + span - 1, vba, span, + hole ? "HOLE" : d, + hole ? "" : ""); + } if (vba >= WHIMORY_CXT_VBA_HOLE || vba >= w->l2v.invalid_vba) { /* * Hole: consumes logical space, maps nothing. @@ -4830,25 +5058,56 @@ static int whimory_cxt_build_from_sb(struct whimory *w, u32 sb_idx) u8 meta[WHIMORY_META_SIZE]; u8 spare[S5L8740_NAND_META_SIZE]; u32 ofs, last_key = ~0u, next_lba = 0, n_l2v = 0; + u32 tag_hist[256] = { 0 }; + u32 n_cxt_meta = 0, n_clean = 0; + u32 planes = w->geom.num_ce * w->geom.num_cau; + u32 per_sb = s->pages_per_sb * planes * s->vbas_per_page; + u32 vblock = whimory_cxt_sb_vblock(w, sb_idx); bool lba_valid = false; int ret; - if (!data || !s->data_page) + if (!data || !s->data_page || !per_sb) return -ENOMEM; - for (ofs = 0; ofs < s->vbas_per_sb; ofs++) { - ret = whimory_cxt_read_vba(w, sb_idx, ofs, data, meta, spare, + /* + * The whole superblock, all four planes, in the order the FTL wrote + * it -- not one plane at a time. See whimory_cxt_read_ofs(). + */ + for (ofs = 0; ofs < per_sb; ofs++) { + ret = whimory_cxt_read_ofs(w, vblock, ofs, data, meta, spare, &last_key); + if (!ret && cxt_trace_sb && ofs < cxt_trace_sb) { + u32 tv = vblock * per_sb + ofs; + unsigned int tce, tcau, tvb, tpg, tsl; + char d[64] = "?"; + + if (!whimory_unpack_vba(w, tv, &tce, &tcau, &tvb, &tpg, + &tsl)) + scnprintf(d, sizeof(d), + "ce%u/cau%u/vblk%u/pg%u/slot%u", + tce, tcau, tvb, tpg, tsl); + dev_info(w->dev, + "CXT_TRACE sb=%u ofs=%u vba=%u %s meta=%16ph data8=%8ph\n", + sb_idx, ofs, tv, d, meta, data); + if (tsl == 0) + dev_info(w->dev, + "CXT_TRACE page metas s0=%16ph s1=%16ph\n", + spare, spare + WHIMORY_META_SIZE); + } if (ret) { dev_warn(w->dev, "CXT sb=%u read failed at ofs=%u/%u (%d) -- rest of this checkpoint dropped\n", - sb_idx, ofs, s->vbas_per_sb, ret); + sb_idx, ofs, per_sb, ret); return ret; } if (meta[0] != WHIMORY_META_TYPE_SFTL_CXT) continue; - if (meta[1] == WHIMORY_CXT_TAG_CLEAN) + n_cxt_meta++; + tag_hist[meta[1]]++; + if (meta[1] == WHIMORY_CXT_TAG_CLEAN) { + n_clean++; break; + } if (meta[1] != WHIMORY_CXT_TAG_L2V) continue; n_l2v++; @@ -4867,18 +5126,46 @@ static int whimory_cxt_build_from_sb(struct whimory *w, u32 sb_idx) * quietly stops halfway looks exactly like one that * finished. */ - s->cxt_records_lost += s->vbas_per_sb - ofs; + s->cxt_records_lost += per_sb - ofs; dev_warn(w->dev, "CXT sb=%u parse stopped at record %u of %u (%d, %u L2V records read) -- up to %u records dropped\n", - sb_idx, ofs, s->vbas_per_sb, ret, n_l2v, - s->vbas_per_sb - ofs); + sb_idx, ofs, per_sb, ret, n_l2v, + per_sb - ofs); return ret; } } - if (!n_l2v) + if (!n_l2v) { + /* + * A checkpoint that contributes nothing is not necessarily + * stale, and on this device it is not: the two newest CXT + * superblocks -- weave 2049391 and 2049387 -- both land here, + * while the map is built from the two older ones at 2049383 + * and 2049379. That is a map one generation behind the + * volume, which is exactly what the bad reads look like: the + * checkpoint names a page whose contents were superseded, and + * the page still holds what it held at weave 687252. + * + * So the tags actually present are worth having. The walk + * only parses WHIMORY_CXT_TAG_L2V and stops at + * WHIMORY_CXT_TAG_CLEAN; if the newest checkpoints carry the + * tree under some other tag, or lead with a CLEAN record that + * stops the walk before the tree, this says so instead of + * leaving it as "superseded". + */ + char hb[160]; + unsigned int t, hn = 0; + + for (t = 0; t < 256; t++) { + if (!tag_hist[t] || hn + 14 >= sizeof(hb)) + continue; + hn += scnprintf(hb + hn, sizeof(hb) - hn, "%02x:%u ", + t, tag_hist[t]); + } dev_info(w->dev, - "CXT sb=%u holds no L2V records (clean or superseded)\n", - sb_idx); + "CXT sb=%u no L2V records: cxt_meta=%u clean=%u tags=%s(l2v=0x%02x clean=0x%02x)\n", + sb_idx, n_cxt_meta, n_clean, hb, + WHIMORY_CXT_TAG_L2V, WHIMORY_CXT_TAG_CLEAN); + } return 0; } @@ -4895,6 +5182,73 @@ static int whimory_cxt_ext_cmp(const void *a, const void *b) return 0; } +static int whimory_cxt_ext_vba_cmp(const void *a, const void *b) +{ + const struct whimory_cxt_extent *x = a, *y = b; + + if (x->vba != y->vba) + return x->vba < y->vba ? -1 : 1; + return 0; +} + +/* + * Do two extents claim the same physical VBA? + * + * The overlap check next to this one sorts by LBA and asks whether two + * extents claim the same logical block. That is the wrong axis for the + * failure that is left: a checkpoint mapping lba 555968 to a VBA whose page + * holds lba 564360, with both extents structurally sound. Two extents can + * name disjoint LBA ranges and still point at the same place, and nothing + * looked for it -- overlaps=0 was measuring the other axis and reading as + * "no collisions". + * + * If a VBA is claimed twice, the map keeps whichever extent the LBA sort + * happened to place last, not whichever the FTL wrote last. There is no + * weave arbitration in the seed, so the wrong one can win. + */ +static void whimory_cxt_check_vba_overlaps(struct whimory *w) +{ + struct whimory_cxt_extent *by_vba; + unsigned int i, n = w->n_cxt_ext, hits = 0, shown = 0; + size_t bytes; + + if (n < 2) + return; + bytes = (size_t)n * sizeof(*by_vba); + by_vba = kvmalloc(bytes, GFP_KERNEL); + if (!by_vba) { + dev_info(w->dev, "CXT_VBA_OVERLAP skipped (no memory)\n"); + return; + } + memcpy(by_vba, w->cxt_ext, bytes); + sort(by_vba, n, sizeof(*by_vba), whimory_cxt_ext_vba_cmp, NULL); + + for (i = 1; i < n; i++) { + const struct whimory_cxt_extent *p = &by_vba[i - 1]; + const struct whimory_cxt_extent *c = &by_vba[i]; + + if (c->vba >= p->vba + p->span) + continue; + hits++; + if (shown < 8) { + shown++; + dev_err(w->dev, + "CXT_VBA_OVERLAP lba=%u span=%u vba=%u overlaps lba=%u span=%u vba=%u by %u\n", + c->lba, c->span, c->vba, + p->lba, p->span, p->vba, + p->vba + p->span - c->vba); + } + } + if (hits) + dev_err(w->dev, + "CXT_VBA_OVERLAP %u extents claim a VBA another already holds\n", + hits); + else + dev_info(w->dev, + "CXT_VBA_OVERLAP none -- every extent has its own VBAs\n"); + kvfree(by_vba); +} + /* * Build the candidate map from every CXT superblock. * @@ -5003,6 +5357,7 @@ static int whimory_cxt_build_candidate(struct whimory *w) "CXT_MAP hole_lbas=%u empty_sbs=%u records_lost=%u nospc=%u\n", w->sftl.cxt_hole_lbas, w->sftl.cxt_sb_empty, w->sftl.cxt_records_lost, w->sftl.cxt_ext_nospc); + whimory_cxt_check_vba_overlaps(w); return 0; } @@ -5499,6 +5854,22 @@ module_param(payload_string_scan, bool, 0644); MODULE_PARM_DESC(payload_string_scan, "Scan confirmed pages for iTunesDB/F00/mp3 strings (default N)"); +/* + * Seven strnstr() sweeps over 16 KiB, on the hottest path in the recover. + * + * This runs from whimory_btoc_confirm_page(), which on this volume is + * called 26653 times -- so it is 26653 passes over a 16 KiB buffer looking + * for "iTunesDB", "F00", "iPod_Control", "Music" and friends, inside the + * phase that already dominates the boot. It is pure diagnostics: nothing + * reads the counters except the RECOVERY_STATS line, and on this volume + * every one of them prints 0. + * + * payload_string_scan gates it and already defaults off, so this costs + * nothing today -- noted here only so nobody enables it during a boot-time + * measurement and wonders where the seconds went. It stays because it was + * useful once, for confirming that a rebuilt map really did point at a + * filesystem. + */ static void whimory_note_payload_strings(struct whimory *w, const u8 *data, unsigned int len) { @@ -6027,8 +6398,16 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) else s->weave_older++; - if (use_cxt && s->cxt_loaded && sb->weave_max_p127 && - sb->weave_max && sb->weave_max < w->cxt_base_weave) { + if (force_replay_vblock && vblock == force_replay_vblock) { + dev_info(w->dev, + "SFTL forcing vblk=%u through replay (kind=%u weave=%llu max=%llu p127=%u base=%llu)\n", + vblock, sb->kind, + (unsigned long long)sb->weave, + (unsigned long long)sb->weave_max, + sb->weave_max_p127, + (unsigned long long)w->cxt_base_weave); + } else if (use_cxt && s->cxt_loaded && sb->weave_max_p127 && + sb->weave_max && sb->weave_max < w->cxt_base_weave) { s->diff_skipped_sbs++; continue; } @@ -6105,6 +6484,8 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) int fb; s->btoc_fb_sbs++; + if (!btoc_meta_fallback) + goto btoc_done; fb = s->open_pages_read; ret = whimory_rebuild_open_sb(w, sb); s->btoc_fb_pages += s->open_pages_read - fb; @@ -6112,6 +6493,8 @@ static int whimory_sftl_recover_l2v_from_media(struct whimory *w) s->btoc_fb_hits += ret; else if (ret < 0) return ret; +btoc_done: + ; } if (ftl_progress_due(w)) ftl_progress_set(w, "replay", i, nsb), @@ -6534,6 +6917,169 @@ static int whimory_ftl_open(struct whimory *w) /* Read path */ /* ------------------------------------------------------------------ */ +/* + * Explain a bad read from the live map. + * + * The first attempt at this read the CXT extent table, which is freed after + * seeding -- it is about 12 MiB on a 55 MiB device -- so it printed nothing + * at all. The interval map holds the same information and is still there. + * + * The question is what kind of wrong the mapping is, and the deltas already + * hint at it. One of them is exactly -16: + * + * want=0x89af4 meta=0x89ae4 delta -16 + * + * 16 is one page of VBAs -- planes * vbas_per_page -- which is the quantum + * you get wrong if the two VBA spaces disagree about a superblock stride. + * Replay builds VBAs with whimory_pack_vba over pages_per_sb = 128, while + * CXT VBAs come from the FTL. If the FTL strides over the 127 data pages + * instead, the two differ by one page per superblock and pack/unpack cancel + * it everywhere except where a CXT mapping meets a replayed one. + * + * So: look the returned LBA up as well. delta_vba tells them apart. + * + * delta_vba == delta_lba -> the run is intact and sitting at the wrong + * place; a placement error + * delta_vba == 0 -> two LBAs claim one VBA; the map is + * double-mapped and one writer overwrote the + * other + * otherwise -> the run itself is malformed + */ +static void whimory_explain_bad_map(struct whimory *w, u32 lba, u32 meta_lba, + u32 vba, u32 span, const struct whimory_meta *m) +{ + unsigned int ce, cau, vblock, page, slot; + char dw[64]; + u32 vba2 = 0, span2 = 0; + int r2; + + if (w->sftl.bad_map_logged >= 8) + return; + w->sftl.bad_map_logged++; + + whimory_vba_describe(w, vba, dw, sizeof(dw)); + r2 = whimory_l2v_search(w, meta_lba, &vba2, &span2); + + if (r2) { + dev_err(w->dev, + " lba=%u -> vba=%u span=%u (%s); meta lba=%u is NOT mapped (%d), delta_lba=%d\n", + lba, vba, span, dw, meta_lba, r2, + (int)meta_lba - (int)lba); + return; + } + + { + struct whimory_range *r = whimory_range_find(&w->ranges, lba); + + dev_err(w->dev, + " lba=%u -> vba=%u span=%u (%s) src=%s weave=%llu\n", + lba, vba, span, dw, + !r ? "?" : + r->src == 1 ? "BTOC" : + r->src == 2 ? "open" : + r->src == 3 ? "CXT" : + r->src == 4 ? "LIST" : "seed", + r ? (unsigned long long)r->weave : 0ULL); + } + whimory_vba_describe(w, vba2, dw, sizeof(dw)); + dev_err(w->dev, + " meta lba=%u -> vba=%u span=%u (%s) | delta_lba=%d delta_vba=%d%s\n", + meta_lba, vba2, span2, dw, + (int)meta_lba - (int)lba, (int)vba - (int)vba2, + ((int)vba - (int)vba2) == 0 ? " DOUBLE-MAPPED" : + (((int)meta_lba - (int)lba) == ((int)vba - (int)vba2) ? + " run intact, misplaced" : " run malformed")); + + if (!whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, &slot)) + dev_err(w->dev, + " read from ce%u/cau%u/vblk%u/pg%u/slot%u\n", + ce, cau, vblock, page, slot); + + /* + * The one number that decides whether the skip rule is at fault. + * + * The mapping came from the CXT and the CXT was parsed correctly, so + * the checkpoint is describing a page that now holds something else: + * the FTL moved data there after the checkpoint was taken. That is + * only possible if the block was not covered by the skip, and the + * skip uses page 127 as the block's upper bound. + * + * If this page's weave is newer than cxt_base_weave, page 127 does + * not bound this block -- the block was appended to after its BTOC + * was written -- and the diff replay skipped a superblock it should + * have replayed. If it is older, the checkpoint and the NAND + * genuinely disagree and the fault is elsewhere. + */ + if (m) { + u64 pw = whimory_weave48((const u8 *)m); + + dev_err(w->dev, + " page weave=%llu vs cxt base=%llu -- %s\n", + (unsigned long long)pw, + (unsigned long long)w->cxt_base_weave, + pw > w->cxt_base_weave ? + "NEWER: page 127 does not bound this block, the skip was wrong" : + "older: written before the checkpoint"); + } + + /* + * And what is actually at the VBA the map gave the returned LBA. + * + * The two extents in the failing case are exactly adjacent in VBA + * space -- 737713 + 440 = 738153 -- and both sit in vblock 360, so + * the runs are placed contiguously and only their LBA labels are in + * question. One read settles which way round they belong: if the page + * at vba2 holds the LBA we originally wanted, the two runs simply + * have each other's labels, and the fault is in how parse_tree pairs + * a run with its starting LBA rather than in the VBA arithmetic. + * + * It costs a NAND read on a path that has already failed. + */ + { + struct whimory_meta m2; + u8 *tmp = w->sftl.data_page; + + if (tmp && w->vfl_ops && w->vfl_ops->read_vba && + !w->vfl_ops->read_vba(w, vba2, 1, tmp, &m2)) + dev_err(w->dev, + " vba=%u actually holds lba=%u type=%02x%s\n", + vba2, le32_to_cpu(m2.lba), m2.type, + le32_to_cpu(m2.lba) == lba ? + " <-- the LBA we wanted: the two runs have swapped labels" : ""); + + /* + * The same page on the other plane convention. + * + * A plane index packs a (ce, cau) pair, and with two of each + * there are two ways round: ce * num_cau + cau, which is what + * this driver uses, or cau * num_ce + ce. They agree on planes + * 0 and 3 and swap 1 and 2 -- and both of the failing reads + * land on plane 2 and plane 1 respectively, which is exactly + * the half a wrong convention would break. + * + * Reading the same vblock/page/slot with ce and cau exchanged + * settles it in one access: if that page holds the LBA we + * asked for, the convention is backwards. + */ + if (tmp && w->vfl_ops && w->vfl_ops->read_vba && + !whimory_unpack_vba(w, vba, &ce, &cau, &vblock, &page, + &slot) && + w->geom.num_ce == w->geom.num_cau) { + u32 swapped = whimory_pack_vba(w, cau, ce, vblock, + page, slot); + + if (swapped != vba && + !w->vfl_ops->read_vba(w, swapped, 1, tmp, &m2)) + dev_err(w->dev, + " plane-swapped vba=%u (ce%u/cau%u) holds lba=%u type=%02x%s\n", + swapped, cau, ce, + le32_to_cpu(m2.lba), m2.type, + le32_to_cpu(m2.lba) == lba ? + " <-- MATCH: the ce/cau plane order is reversed" : ""); + } + } +} + static int whimory_validate_meta(struct whimory *w, const struct whimory_meta *m, u32 expected_lba) @@ -6544,6 +7090,8 @@ static int whimory_validate_meta(struct whimory *w, dev_err(w->dev, "sftl non-data meta want=0x%x type=%02x flags=%02x lba=0x%x\n", expected_lba, m->type, m->flags, meta_lba); + whimory_explain_bad_map(w, expected_lba, meta_lba, + w->bad_vba, w->bad_span, m); return -EIO; } @@ -6551,6 +7099,8 @@ static int whimory_validate_meta(struct whimory *w, dev_err(w->dev, "sftl lba mismatch want=0x%x meta=0x%x type=%02x flags=%02x\n", expected_lba, meta_lba, m->type, m->flags); + whimory_explain_bad_map(w, expected_lba, meta_lba, + w->bad_vba, w->bad_span, m); return -EIO; } @@ -6609,6 +7159,9 @@ static int n31_sftl_read_lba(struct whimory *w, u32 lba, void *buf, ret = w->vfl_ops->read_vba(w, vba, 1, buf, &meta); if (ret) return ret; + /* What the map actually answered, for the explainer below. */ + w->bad_vba = vba; + w->bad_span = span; ret = whimory_validate_meta(w, &meta, lba); if (!ret) dev_dbg(w->dev, diff --git a/drivers/misc/ftl-s5l8740-csmap.c b/drivers/misc/ftl-s5l8740-csmap.c index fd9cb10228f2c5..b32ffa30e12d48 100755 --- a/drivers/misc/ftl-s5l8740-csmap.c +++ b/drivers/misc/ftl-s5l8740-csmap.c @@ -199,6 +199,7 @@ static int n31_ftl_find_bpb(struct n31_ftl_cs *ftl); static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl); static int n31_validate_fat_critical(struct n31_ftl_cs *ftl); static void n31_fat_semantic_validate(struct n31_ftl_cs *ftl); +static bool n31_fat_first_sector_ok(struct n31_ftl_cs *ftl); static int n31_ftl_register_disk(struct n31_ftl_cs *ftl); static void n31_ftl_unregister_disk(struct n31_ftl_cs *ftl); static int n31_ftl_apply_bpb(struct n31_ftl_cs *ftl, u32 fmss_lba, @@ -1183,7 +1184,7 @@ static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl) } for (i = 0; i < ftl->bpb_ncand; i++) { - bool apple; + bool apple, fatsig; int vret; n31_ftl_apply_bpb(ftl, ftl->bpb_candidates[i], @@ -1191,6 +1192,7 @@ static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl) ftl->bpb_cand_sector[i]); vret = n31_validate_fat_critical(ftl); apple = !memcmp(ftl->bpb_cand_oem[i], "*UOKJIHC", 8); + fatsig = n31_fat_first_sector_ok(ftl); dev_info(ftl->dev, "bpb_try fmss=%u weave=%012llx oem='%.8s' " @@ -1200,6 +1202,18 @@ static int n31_ftl_select_bpb(struct n31_ftl_cs *ftl) ftl->bpb_cand_oem[i], ftl->fat_crit_ok_n, ftl->fat_crit_need_n, vret); + dev_info(ftl->dev, "bpb_try fmss=%u fatsig=%d\n", + ftl->bpb_candidates[i], fatsig); + + /* + * A candidate whose first FAT sector is not a FAT is the wrong + * volume base, however well its critical sectors read. That is + * the entire failure this check exists for, so it gates both + * the early-out below and the scoring after it. + */ + if (!fatsig) + continue; + /* Perfect critical set: newest weave wins immediately. */ if (!vret && ftl->fat_crit_ok_n == ftl->fat_crit_need_n && ftl->fat_crit_need_n > 0) { @@ -1275,6 +1289,59 @@ static int n31_read_disk_checked(struct n31_ftl_cs *ftl, u32 disk_lba, * Walk root cluster chain, count dir entries, search for known iPod names. * FAT1 starts at reserved + fat_size32 (e.g. 32+942=974), not disk_lba=33. */ +/* + * Does the first FAT sector actually look like the start of a FAT? + * + * n31_validate_fat_critical() only checks that its nine critical sectors + * *read*. It never looks at what came back, so a BPB whose volume base is + * wrong still scores a perfect 9/9 as long as the shifted sectors happen to + * be mapped -- which they were: + * + * BPB_CAND #1 fmss_lba=49285 weave=..ad crit=9/9 selected + * BPB_CAND #2 fmss_lba=49279 weave=..ac + * + * Six sectors apart, and the FAT belongs to the older one. Selecting #1 put + * the whole volume six sectors out: sector 0 still read as a valid BPB and + * sector 1 as a valid FSInfo, because those are static and were found by + * content, but the FAT region was offset. Reading it back showed a perfectly + * well-formed FAT32 chain stepping 0x400 per 4096-byte sector and starting + * six sectors early -- so vfat walked into the middle of the table and got + * "invalid cluster chain". + * + * Every FAT32 begins entry 0 with the media descriptor in the low byte and + * the FAT32 12-bit-wide EOC nibbles above it: F8 FF FF 0F. That single test + * separates the two candidates, and nothing that is genuinely the first FAT + * sector can fail it. + */ +static bool n31_fat_first_sector_ok(struct n31_ftl_cs *ftl) +{ + struct n31_fat_layout *L = &ftl->layout; + u8 *buf; + bool ok = false; + + if (!ftl->fat_base_valid || !L->fat_start) + return false; + buf = kmalloc(N31_DATA_SLOT_SIZE, GFP_KERNEL); + if (!buf) + return false; + if (!n31_ftl_read_disk_lba(ftl, L->fat_start, buf)) { + u32 e0 = get_unaligned_le32(buf); + u32 e1 = get_unaligned_le32(buf + 4); + + /* + * Entry 0 low byte is the media descriptor and matches the + * one in the BPB at offset 0x15; the rest of entry 0 and all + * of entry 1 are set. Mask + * to 28 bits -- FAT32 entries carry only the low 28. + */ + ok = (e0 & 0xff) == ftl->bpb_sector[0x15] && + (e0 & 0x0fffff00) == 0x0fffff00 && + (e1 & 0x0fffffff) == 0x0fffffff; + } + kfree(buf); + return ok; +} + static void n31_fat_semantic_validate(struct n31_ftl_cs *ftl) { struct n31_fat_layout *L = &ftl->layout; diff --git a/drivers/misc/nand-s5l8740.c b/drivers/misc/nand-s5l8740.c index cf49a8d47b5988..053913ae658902 100755 --- a/drivers/misc/nand-s5l8740.c +++ b/drivers/misc/nand-s5l8740.c @@ -400,7 +400,18 @@ module_param(cs_reads_total, uint, 0444); /* Where CS read wall time actually goes; reported by the heartbeat. */ static u64 cs_ns_kick, cs_ns_copy; -static unsigned int cs_reset_every; +/* + * On. The comment above explains exactly why this is needed and it then + * defaulted to 0, so the counter was incremented in five places and acted + * on in none. A recovery walks eight thousand pages of back-to-back live + * C00 kicks with nothing reasserting the sequencer across the whole run, + * which is the condition the comment describes. + * + * 4096 is two register writes and two 10 us delays roughly twice per + * recovery -- unmeasurable against the reads between them, and it bounds + * how far the sequencer can drift before something puts it back. + */ +static unsigned int cs_reset_every = 4096; module_param(cs_reset_every, uint, 0644); MODULE_PARM_DESC(cs_reset_every, "fmss_nand_reset after this many CS phys reads (0=off)"); @@ -6827,8 +6838,16 @@ int s5l8740_nand_cs_phys_read_slc(u8 ce, u8 cau, u16 block, u8 page, u8 slc, addr = fmss_ppn_addr(cau, block, page, slc); mutex_lock(&f->lock); - if (cs_reset_every && f->pages_since_reset >= cs_reset_every) + if (cs_reset_every && f->pages_since_reset >= cs_reset_every) { + /* + * Clearing the counter is the half that was missing. Without + * it the threshold latches and every read after the first + * reset resets again, which turns a periodic safeguard into a + * per-read cost. + */ fmss_nand_reset(f); + f->pages_since_reset = 0; + } saved_armed = dma_armed; /* One-shot friendly: re-arm for this kick; disarm after if one_shot. */ dma_armed = true; diff --git a/drivers/misc/whimory-s5l8740.h b/drivers/misc/whimory-s5l8740.h index 820467109873c3..14790ae27c3009 100755 --- a/drivers/misc/whimory-s5l8740.h +++ b/drivers/misc/whimory-s5l8740.h @@ -193,6 +193,14 @@ struct whimory_range { u32 len; u32 vba; u64 weave; + /* + * Which producer put this range here: 1 BTOC, 2 open rebuild, + * 3 CXT seed, 4 list token. Diagnostic only, and the reason it + * exists is that a wrong mapping looks identical whoever wrote + * it -- the CXT record for the failing LBAs parses correctly, + * so the question is who overwrote it afterwards. + */ + u8 src; }; struct whimory_vfl { @@ -295,6 +303,10 @@ struct whimory_sftl { u32 unk_fb_sbs; /* unclassified SBs rebuilt from meta */ u32 unk_fb_pages; u32 unk_fb_hits; + u32 bad_map_logged; /* bounded explanations for bad reads */ + u32 cxt_dumped; /* pairs printed by the record dump */ + u32 cxt_hdr_skipped; /* records whose header said nothing follows */ + u32 cxt_hdr_bad; /* records whose header was not a CONTIG marker */ u32 btoc_pages_read; u32 btoc_pages_valid; u32 btoc_entries_seen; @@ -411,6 +423,8 @@ struct whimory { struct gendisk *ipod_disk; struct platform_device *pdev; u32 lba0_vba; + u32 bad_vba; /* what the map answered for a failing read */ + u32 bad_span; u64 cxt_base_weave; struct whimory_cxt_extent *cxt_ext; /* candidate map (Phase 3) */ u32 n_cxt_ext; From e1acaa44be8194e65de91e0d11ac7f4e27979e6d Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 23:43:52 -0230 Subject: [PATCH 29/31] ASoC: s5l8740-i2s: stop stamping a whole-SoC clock snapshot over CLKCON+0x30 s5l8740_audio_clk_set() did writel(any ? CLKCON_AUDIO_PLAY : CLKCON_AUDIO_IDLE, clkcon + 0x30); on every play and every stop. Those constants (0x32190 and 0x1c20) are whole-register snapshots captured from RetailOS at two moments, so each write imposes the entire captured clock state of the SoC, including whatever every other peripheral was doing when the snapshot was taken. CLKCON+0x30 carries gates that have nothing to do with audio. The NAND controller is one of them. After enough play/stop transitions the FMSS reads back FMCTRL0=0 NANDSTAT=0, sub_10453C times out on FMCTRL1 bit 30, and Whimory open fails with -110: s5l8740-nand: 10453C FMCTRL1 bit30 timeout v=00000000 s5l8740-ftl: Whimory open failed (-110) - NOT registering /dev/s5l8740-ftl (fil=0 sig=0 vfl=0 ftl=0 l2v=0 lba0=0) The raw CS path still worked, so the NAND itself was fine -- it was the controller clock, and storage stayed dead until the next boot. Stock never writes this register wholesale. sub_41CBD8 sets or clears one bit and preserves the rest. So restrict the write to the bits that differ between the two snapshots and preserve everything else from the live register. Confirmed on hardware: FMCTRL0=0x1 NANDSTAT=0x62, FIL_Init OK. Also in this change: - sub_B6620(port, 0) is *(base + 8) |= 6, an OR. The DMA arm wrote a bare 0x6 and cleared every other bit in TXCOM. Equivalent while TXCOM reads 0 first, which it does today, but it silently drops any bit something else sets; the same function ORs 0x6 into RXCOM on the capture side. - s5l8740_codec_clk_gate(): sub_4F82F8() returns 9 and case 9 of sub_41CBD8 is CLKCON+0x0C bit 15, active low. D3280(1) drops that gate and D3280(3) restores it, which is what the codec 0x0006/0x0007 bit-6 freeze latch brackets. Exported for the codec driver, which owns the state machine but not the CLKCON mapping. - The txcon module parameter now writes IIS0+0x04 when set. Sweeping it used to require defeating the "already programmed" check with clkdiv, which re-ran the pad mux and the CLKCON writes on every play -- which is how the NAND got gated and how the device wedged mid-stream. Co-Authored-By: Claude Opus 5 --- sound/soc/apple/s5l8740-i2s.c | 219 ++++++++++++++++++++++++++++++++-- 1 file changed, 212 insertions(+), 7 deletions(-) diff --git a/sound/soc/apple/s5l8740-i2s.c b/sound/soc/apple/s5l8740-i2s.c index 8c6c94fef4aab6..5e7e2d05493305 100755 --- a/sound/soc/apple/s5l8740-i2s.c +++ b/sound/soc/apple/s5l8740-i2s.c @@ -77,8 +77,7 @@ * 0x24 (no external clock). Clearing it moves STATUS to 0x8020. * Override via txcon= for bring-up; default stays OSOS. */ static uint txcon = I2STXCON_N31_16; -module_param(txcon, uint, 0644); -MODULE_PARM_DESC(txcon, "I2STXCON (default 0x03100099; NOT Rockbox 0x0B100019)"); +/* module_param moved below s5l8740_i2s_iis0; see txcon_set(). */ /* * D34C0 → 4F716(port, div). Table in n31-audio-rates.h. * 0 = 12 MHz / rate (272 @ 44.1 kHz RetailOS music). @@ -500,6 +499,29 @@ static bool s5l8740_fm_gate_held; * and leaves every other block's bits alone. Set this to 1 only to * reproduce the snapshot experiment, and expect to lose storage. */ +/* + * Write only CLKCON+0x1C, and nothing else. + * + * force_stock_audio_parent below is off because it pushes the whole + * music-playing snapshot into +0x08..0x1C, and those are SoC-wide gates: + * the first audio start took the FMSS clock with it and storage was gone + * until reboot. That is a good reason to distrust the snapshot, and a bad + * reason to leave the one register the oracle actually calls out. + * + * Checkpoint-010's truth table gives +0x1C = 0xD0052003 for RetailOS + * playing music. We run 0x10122003, the SEC bring-up value. The low half + * is identical -- 0x2003 either way -- so the difference is entirely in + * the upper 16 bits, which is where the audio parent selection lives. + * + * +0x08, +0x0C, +0x10 and +0x14 are the registers that killed the NAND and + * they are not touched. This writes one register that the oracle attests + * to, and the storage path is checked after every test. + */ +static bool stock_clkcon_1c = true; +module_param(stock_clkcon_1c, bool, 0644); +MODULE_PARM_DESC(stock_clkcon_1c, + "write the RetailOS audio parent to CLKCON+0x1C only (default Y)"); + static bool force_stock_audio_parent; module_param(force_stock_audio_parent, bool, 0644); MODULE_PARM_DESC(force_stock_audio_parent, @@ -529,8 +551,16 @@ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) } else { if (!r18) writel(SEC_CLKCON_18, i2s->clkcon + 0x18); - if (!r1c) + if (stock_clkcon_1c) { + if (r1c != STOCK_CLKCON_1C) { + writel(STOCK_CLKCON_1C, i2s->clkcon + 0x1c); + dev_info(i2s->dev, + "CLKCON+1C %08x -> %08x (stock audio parent)\n", + r1c, STOCK_CLKCON_1C); + } + } else if (!r1c) { writel(SEC_CLKCON_1C, i2s->clkcon + 0x1c); + } v = readl(i2s->clkcon + 0x0c); if (v & 0x8000u) writel(v & ~0x8000u, i2s->clkcon + 0x0c); @@ -547,12 +577,73 @@ static void s5l8740_i2s_ungate(struct s5l8740_i2s *i2s) static DEFINE_SPINLOCK(s5l8740_audio_clk_lock); static bool s5l8740_audio_clk_wanted[S5L8740_AUDIO_PORTS]; +/* + * Only touch the bits that are actually about audio. + * + * CLKCON_AUDIO_PLAY (0x32190) and CLKCON_AUDIO_IDLE (0x1c20) are whole- + * register snapshots taken from RetailOS at two moments, and this used to + * writel() one of them over CLKCON+0x30 in its entirety on every play and + * every stop. That imposes the entire captured clock state of the SoC, + * including whatever every other peripheral happened to be doing when the + * snapshot was taken -- and CLKCON+0x30 carries gates that are nothing to + * do with audio. The FMSS is one of them: after a few play/stop cycles the + * NAND controller reads back FMCTRL0=0 NANDSTAT=0, sub_10453C times out on + * FMCTRL1 bit 30, and Whimory open fails with -110. Storage is gone until + * the next boot. + * + * Stock never does this. sub_41CBD8 sets or clears exactly one bit: + * + * v = MEMORY[0x3C500008] & 0x7FFFFFFF; // or | 0x80000000 + * + * and leaves the register otherwise untouched. + * + * The two snapshots differ in a fixed set of bits, and those are the ones + * that plausibly belong to audio. Everything outside that set is somebody + * else's and is now preserved from the live register. + */ +#define CLKCON_AUDIO_MASK (CLKCON_AUDIO_PLAY ^ CLKCON_AUDIO_IDLE) + + +/* + * Apply TXCON to the live register as soon as it is written. + * + * Sweeping TXCON used to mean setting clkdiv to defeat the "already + * programmed" check in s5l8740_i2s_program(), which re-ran the pad mux and + * the CLKCON writes on every play. That is how the NAND got clock-gated and + * how the device wedged mid-stream. This writes IIS0+0x04 and nothing else, + * so a sweep costs one register write and cannot disturb any clock. + */ +static struct s5l8740_i2s *s5l8740_i2s_iis0; + +static int txcon_set(const char *val, const struct kernel_param *kp) +{ + struct s5l8740_i2s *i2s = READ_ONCE(s5l8740_i2s_iis0); + int ret = param_set_uint(val, kp); + + if (ret) + return ret; + if (i2s && i2s->base) { + writel(txcon, i2s->base + I2STXCON); + dev_info(i2s->dev, "txcon live -> 0x%08x (status=0x%08x)\n", + txcon, readl(i2s->base + I2SSTATUS)); + } + return 0; +} + +static const struct kernel_param_ops txcon_ops = { + .set = txcon_set, + .get = param_get_uint, +}; +module_param_cb(txcon, &txcon_ops, &txcon, 0644); +MODULE_PARM_DESC(txcon, "I2STXCON; written live on set (default 0x03100099)"); + static void s5l8740_audio_clk_set(void __iomem *clkcon, unsigned int port, bool on) { unsigned long flags; unsigned int i; bool any = false; + u32 want, cur, new; if (!clkcon) return; @@ -560,11 +651,55 @@ static void s5l8740_audio_clk_set(void __iomem *clkcon, unsigned int port, s5l8740_audio_clk_wanted[port] = on; for (i = 0; i < S5L8740_AUDIO_PORTS; i++) any |= s5l8740_audio_clk_wanted[i]; - writel(any ? CLKCON_AUDIO_PLAY : CLKCON_AUDIO_IDLE, - clkcon + CLKCON_AUDIO_OFF); + want = any ? CLKCON_AUDIO_PLAY : CLKCON_AUDIO_IDLE; + cur = readl(clkcon + CLKCON_AUDIO_OFF); + new = (cur & ~CLKCON_AUDIO_MASK) | (want & CLKCON_AUDIO_MASK); + if (new != cur) + writel(new, clkcon + CLKCON_AUDIO_OFF); spin_unlock_irqrestore(&s5l8740_audio_clk_lock, flags); } + +/* + * The codec clock gate, RetailOS sub_41CBD8 with the id sub_4F82F8 returns. + * + * sub_4F82F8() returns 9, and case 9 of sub_41CBD8 is CLKCON+0x0C bit 15, + * active low: + * + * if (on) MEMORY[0x3C50000C] &= 0xFFFF7FFF; + * else MEMORY[0x3C50000C] |= 0x8000; + * + * D3280(1) ends by turning it OFF and D3280(3) begins by turning it back + * ON, which is what the 0x0006 / 0x0007 bit-6 freeze latch is bracketing: + * the codec is parked, its clock is stopped, and then the clock returns and + * the latch is released. This driver left the clock running the whole time, + * so that transition never happened. + * + * Exported because the CLKCON mapping lives here and the codec driver needs + * it. One bit, read-modify-write, nothing else touched -- the opposite of + * what s5l8740_audio_clk_set() used to do to CLKCON+0x30. + */ +void s5l8740_codec_clk_gate(bool on) +{ + struct s5l8740_i2s *i2s = READ_ONCE(s5l8740_i2s_iis0); + unsigned long flags; + u32 v; + + if (!i2s || !i2s->clkcon) + return; + spin_lock_irqsave(&s5l8740_audio_clk_lock, flags); + v = readl(i2s->clkcon + 0x0c); + if (on) + v &= ~0x8000u; + else + v |= 0x8000u; + writel(v, i2s->clkcon + 0x0c); + spin_unlock_irqrestore(&s5l8740_audio_clk_lock, flags); + dev_info(i2s->dev, "codec clk gate %s (CLKCON+0x0C=0x%08x)\n", + on ? "ON" : "OFF", v); +} +EXPORT_SYMBOL_GPL(s5l8740_codec_clk_gate); + static void s5l8740_i2s_clkcon_audio(struct s5l8740_i2s *i2s, u32 val) { if (!i2s) @@ -665,11 +800,70 @@ static void s5l8740_i2s_log_iis_gpio(struct s5l8740_i2s *i2s, const char *tag) * touches GPIO at all. Claiming only GPIO 20 leaves the bus incomplete * and the jack silent. Optional (6,3) in pad_mode 1/4. */ +/* + * The stock pad set while RetailOS plays, from audio checkpoint-010: + * bank0 PCON 0x32112224 / DIR 0xFF, bank2 PCON 0x02230000 / DIR 0x70. + * + * We set 7 and 20 and have never touched 6, 21 or 22. That gap is worth + * closing now because everything else matches the oracle -- TXCON, TXCOM, + * CLKDIV 272, CLKCON +0x18 and +0x1C, IIS STATUS 0x424, PL080 channel 2 on + * peri 10 -- and the jack is still silent. Clocks and status can all read + * correct while the serialiser's data pin is not muxed out of the SoC, + * which is exactly the case the checkpoint's "wire/data" branch describes. + */ +static const struct { u8 gpio, func; } stock_audio_pads[] = { + { 6, 2 }, { 7, 3 }, { 20, 3 }, { 21, 2 }, { 22, 2 }, +}; + +/* + * Off: measured, and there is nothing to restore. + * + * devmem on the running device reads bank0 PCON 0x32222224 / DIR 0xFF and + * bank2 PCON 0x02230000 / DIR 0x70 against the oracle's 0x32112224 / 0xFF + * and 0x02230000 / 0x70. Every audio pad already matches -- GPIO6 f2, + * GPIO7 f3, GPIO20 f3, GPIO21 f2, GPIO22 f2, all outputs. The only bank0 + * difference is pins 4 and 5, which belong to I2C0 and which the + * checkpoint explicitly says to leave alone. + * + * Kept because it costs nothing and pins the invariant, but it is not the + * silence and turning it on requires a kernel rebuild -- gpio-s5l8740 is + * built in, so the export it needs is not reachable from a module reload. + */ +static bool force_stock_audio_pads; +module_param(force_stock_audio_pads, bool, 0644); +MODULE_PARM_DESC(force_stock_audio_pads, + "restore the stock GPIO6/7/20/21/22 audio pad nibbles (default Y)"); + +static void s5l8740_i2s_stock_pads(struct s5l8740_i2s *i2s) +{ + int (*setpad)(unsigned int, unsigned int, bool); + unsigned int i; + + setpad = (int (*)(unsigned int, unsigned int, bool)) + __symbol_get("s5l8740_gpio_set_pad"); + if (!setpad) { + dev_warn(i2s->dev, "stock pads: gpio export missing\n"); + return; + } + for (i = 0; i < ARRAY_SIZE(stock_audio_pads); i++) + setpad(stock_audio_pads[i].gpio, stock_audio_pads[i].func, + true); + __symbol_put("s5l8740_gpio_set_pad"); + dev_info(i2s->dev, + "stock audio pads applied: 6=f2 7=f3 20=f3 21=f2 22=f2, all out\n"); +} + static void s5l8740_i2s_pads(struct s5l8740_i2s *i2s) { static const u8 sec_words[] = { 6, 7, 20 }; unsigned int i; + if (force_stock_audio_pads) { + s5l8740_i2s_stock_pads(i2s); + s5l8740_i2s_log_iis_gpio(i2s, "pads-stock"); + return; + } + if (pad_mode == 2) { void (*en)(unsigned int); @@ -984,8 +1178,18 @@ static void s5l8740_i2s_tx_kick(struct s5l8740_i2s *i2s, bool dma) writel((u32)txcom_exact, i2s->base + I2STXCOM); } else if (dma) { switch (txcom_mode) { - case 0: /* retail: OSOS B6620 TXCOM = 0x6 after DMA armed */ - writel(I2STXCOM_DMA, i2s->base + I2STXCOM); + case 0: + /* + * OSOS sub_B6620(port, 0) is + * *(base + 8) |= 6; + * an OR, not an assignment. This wrote a bare 0x6 and + * so cleared every other bit in TXCOM. It happens to + * be equivalent while TXCOM reads 0 beforehand, which + * is the case today, but the moment anything else + * sets a bit here the plain write silently drops it. + */ + writel(readl(i2s->base + I2STXCOM) | I2STXCOM_DMA, + i2s->base + I2STXCOM); break; case 1: /* pio-only kick (debug) */ writel(txcom_pio, i2s->base + I2STXCOM); @@ -1832,6 +2036,7 @@ static int s5l8740_i2s_probe(struct platform_device *pdev) platform_set_drvdata(pdev, i2s); dev_set_drvdata(dev, i2s); + WRITE_ONCE(s5l8740_i2s_iis0, i2s); mutex_init(&i2s->dma_lock); INIT_DELAYED_WORK(&i2s->dma_watch, s5l8740_i2s_dma_watch); From 20b00ecf578c58fb3bb20593bff641c2035586fa Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 23:44:24 -0230 Subject: [PATCH 30/31] ASoC: cs42l81: make the codec state machine match the decomp Nine defects on the prepare/play path, found by decoding sub_D3280, sub_D2F64, sub_D34C0 and sub_183138 properly rather than transcribing the bootloader, and by checking every register this driver writes against every write in the RetailOS image. The state machine was inside out: - The analog power-up was the bootloader's sub_1310. OSOS has its own, sub_D3280(1), differing in five registers -- 0x0225 = 0x33 not 0x19, 0x0220 mask 0x78 = 0x78 not 0x50, plus 0x0229 and 0x0075 writes the bootloader never makes -- and it leaves 0x0007 bit 6 set where the bootloader leaves it clear. - States 1 and 3 ran in the wrong order. They are a matched pair around the codec clock gate: state 1 takes the freeze latch and stops the clock, state 3 restarts it and releases the latch. Calling state 3 first released a latch nothing had taken, and state 1 then took it with no one left to release it. 0x0075 bit 7 inverted the same way. - state_4_output_on() is a power-DOWN and ran as the last step of every prepare. sub_3C6244 settles it: a dB-code converter, fed codes 0x40 and 0x41, i.e. -90 dB and -76 dB. It also cleared the analog enable state 1 had just polled 0x002F for, dropped the 2v5 rail, wrote 0x0225 = 0x00 over state 1's 0x33, and clobbered every 44.1 kHz stream's SRC pair with the 48 kHz native values. Renamed and moved to the stop path. Hardcoded results of computed functions: - sub_D2F64 is computed. Mode 271 ("on") applied mode 6's ("off") value to 0x000D, and mode 6 drove 0x0006 bit 2 the same way mode 271 does when stock drives them oppositely. - sub_D34C0 is a three-way branch on MEMORY[0x892A038], not a sequence. This ran the sub_183138 body and another arm's tail every time; they program different blocks for the same job (0x010B/0x010C vs 0x0223/0x0224), and the tail released a hold 183138 had just raised. 183138 ends muted with the 0x0220 bit-5 hold set by design -- sub_D2F64 is what releases it -- so prepare must run set_rate before output_path_enable, and does. Work that did not belong on the play path: - post_iis_start() wrote 0x0229 = 0x41 and 0xC96F = 0x0E, both standby values from D3280(4). 0x0229 is written by sub_D3280 and nothing else in the whole image, and 0xC96F = 0x0E is the 2v5 rail *down*. - asp_lock() re-ran the entire rate programming on every start, and asp_clock_pulse() ran its 0x0220 bracket backwards so it finished in idle. Both left the part muted and held at play time. - Setting the volume forced the mute state and set the graph commit bit. Stock's volume path is sub_D2C98 then sub_400330: gain and nothing else. Split into apply_user_vol() and apply_mute(). - The graph is 80 SPI writes and a 100 ms settle -- 118 ms measured -- and ran inside the transport START callback, between the application asking for playback and the DMA being kicked. The stream underran and ALSA restarted it, giving three STARTs in 260 ms. Built in hw_params now; START went from 118 ms to 2 ms. Invented code, removed: - The ASP lock. 0x002F is read exactly once in the whole image (the readiness poll in sub_D3280(1), testing bit 7) and bit 6 is never examined anywhere. This was five attempts of eighty polls on bit 6, with a module parameter to flip a polarity that had never been established -- up to two seconds per playback start on a bit stock does not look at. - cs42_hsdet_pulse(). Credited to "RE D3280(3)/audio_on" and from neither; nothing in the image writes 0x0073, 0x0079 or 0x0009. It left 0x0009 -- MCLK control -- rewritten rather than restored. - All codec headset detection, by request: force_headset, jack_poll_ms, the poll work and its mid-stream 42D364(0) teardown. Settled and left alone: the IIS0 TX port object sets a3 = 1, so sub_BCB60 takes its a3 != 0 branch and TXCON = 0x03100099, RXCON = 0x1000 and pads at function 3 are stock-exact -- matching the live pad readback. The static graph table matches the image position-for-position across all 80 writes. sub_174E7C's tap formula matches including the +2/+1 and the 160 divisor. Not yet producing sustained sound: the IIS0 transmitter still takes one 32-byte burst and stalls with STATUS=0x424. Every register value now matches stock, so what remains is sequence, not content. Co-Authored-By: Claude Opus 5 --- sound/soc/apple/cs42l81-spi.c | 1318 ++++++++++++++++++++--------- sound/soc/apple/n31-audio-rates.h | 29 +- 2 files changed, 962 insertions(+), 385 deletions(-) diff --git a/sound/soc/apple/cs42l81-spi.c b/sound/soc/apple/cs42l81-spi.c index 666e58db5f2e80..79ed7ee43ecd1b 100755 --- a/sound/soc/apple/cs42l81-spi.c +++ b/sound/soc/apple/cs42l81-spi.c @@ -36,10 +36,9 @@ * * Cirrus bring-up notes: * Reset mutes outputs (0x527=0xFF); unmute 0x60 on play. - * 0x2F bit6: glass shows 0x40 idle (no IIS), 0x00 while BCLK/LRCLK run. - * Treat bit6 as LOS / no-sync when asp_bit6_is_los=1 (default): asp_lock - * succeeds when bit6 is CLEAR. Legacy asp_bit6_is_los=0 waits for bit6 set. - * ASP lock after IIS clocks (414FAE), not before. + * 0x2F bit 7 is the analog-block readiness D3280(1) polls. Bit 6 moves + * with the IIS clocks on glass but stock never reads it -- 0x2F is read + * exactly once in the whole image. There is no ASP lock handshake. * I2S slave NB_NF 16-bit; no DAPM graph — path is register audio_on(). * * ARTP routes (CoreAudio debug: "BT-%s, HP-%s, USB-%s, MB-%s"): @@ -153,11 +152,6 @@ static int audio_route; module_param(audio_route, int, 0644); MODULE_PARM_DESC(audio_route, "0=HP jack (default); USB/BT/MB unimplemented"); -/* 570620 gates on 0x8925CF4==1 (headset ready). */ -static bool force_headset; -module_param(force_headset, bool, 0644); -MODULE_PARM_DESC(force_headset, "1=skip headset-ready gate (glass bring-up)"); - /* * Grab volume keys system-wide. * @@ -178,12 +172,6 @@ module_param(vol_keys, bool, 0644); MODULE_PARM_DESC(vol_keys, "1=grab KEY_VOLUMEUP/DOWN globally to set headphone gain; 0=leave keys alone (default)"); -/* - * Off. The poll existed to notice plug and unplug through MikeyBus and - * re-arm HSDET, and it re-entered the codec under c->lock every 500 ms - * forever for a result this board does not act on. Set non-zero only if - * jack detection is ever genuinely wanted here. - */ /* * Print the stack of the first codec prepare. On by default until the * thing that opens the PCM at boot is identified; it is one backtrace. @@ -193,10 +181,6 @@ module_param(prepare_caller_trace, bool, 0644); MODULE_PARM_DESC(prepare_caller_trace, "1=dump_stack() on the first codec prepare to identify the caller"); -static unsigned int jack_poll_ms; -module_param(jack_poll_ms, uint, 0644); -MODULE_PARM_DESC(jack_poll_ms, "MikeyBus/HSDET poll period ms (0=off)"); - /* * audio_path_mode (i2s trigger also reads via cs42l81_get_audio_path_mode): * 0 = legacy: play graph folded into codec prepare (debug only) @@ -246,9 +230,6 @@ module_param(post_iis_401_rmw, bool, 0644); MODULE_PARM_DESC(post_iis_401_rmw, "1=post_iis 401&3=2 (default); 0=keep graph 0x12"); /* D3280(4) RE writes C96F=0x0E. Glass sometimes needed 0x1E — A/B. */ -static int c96f_final = 0x0e; -module_param(c96f_final, int, 0644); -MODULE_PARM_DESC(c96f_final, "D3280(4) final 0xC96F (default 0x0E RE; try 0x1E)"); /* Dynamic 570620: force 41F944 gate pass (route_present=1, busy=0). */ static bool graph_force_gate = true; @@ -294,12 +275,16 @@ struct cs42l81 { struct work_struct vol_work; struct delayed_work asp_post_work; unsigned int prepared_rate; /* 0 = not configured yet */ - struct delayed_work jack_work; atomic_t vol_steps; - bool jack_poll_active; - bool jack_last_present; bool route_playing; /* 892A058 mirror */ bool codec_prepared; + bool unlock_done; + bool graph_latched; + /* + * RetailOS MEMORY[0x892A038]. Selects which of sub_D34C0's three + * rate paths is live; written by D3280(3) and by sub_D2F64. + */ + u8 mode38; bool play_started; int graph_domain; /* RetailOS SVC domain 33 held (not SPI page) */ struct cs42_graph_state graph; @@ -329,23 +314,6 @@ static unsigned int cs42_pick_rate(struct cs42l81 *c, unsigned int rate) return N31_RATE_DEFAULT; } -/* - * 0x2F bit6 polarity: glass idle (no IIS) reads 0x40; running IIS reads 0x00. - * Default asp_bit6_is_los=1 → bit6 set = loss/no-sync, clear = ASP locked. - */ -static bool asp_bit6_is_los = true; -module_param(asp_bit6_is_los, bool, 0644); -MODULE_PARM_DESC(asp_bit6_is_los, - "1=bit6 is LOS flag (clear=synced); 0=legacy bit6-set=synced"); - -/* - * asp_gate_unmute=0 (default): post_iis always forces HP unmute; 0x2F is telemetry. - * asp_gate_unmute=1: legacy — unmute only when asp probe reports synced. - */ -static bool asp_gate_unmute; -module_param(asp_gate_unmute, bool, 0644); -MODULE_PARM_DESC(asp_gate_unmute, "1=gate unmute on 0x2F probe; 0=force unmute (default)"); - /* * of_asp_slave=1: clear 0x0F bit7 before IIS (CS42L73-family ASP slave test). * Default 0 keeps RetailOS D3280(3) pad-drive write. @@ -354,13 +322,6 @@ static bool of_asp_slave; module_param(of_asp_slave, bool, 0644); MODULE_PARM_DESC(of_asp_slave, "1=force CS42 0x0F bit7=0 (ASP slave) before IIS"); -static bool cs42l81_asp_synced(u8 r2f) -{ - if (asp_bit6_is_los) - return !(r2f & 0x40); - return !!(r2f & 0x40); -} - int cs42l81_post_iis_start(void); int cs42l81_play_stop(void); int cs42l81_play_start(void); @@ -371,9 +332,97 @@ void cs42l81_schedule_post_iis(void); void cs42l81_cancel_post_iis(void); static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val); +static void cs42_codec_clk(struct cs42l81 *c, bool on); static int cs42l81_apply_user_vol(struct cs42l81 *c); +static int cs42l81_apply_mute(struct cs42l81 *c); static void cs42l81_log_start_state(struct cs42l81 *c, const char *tag); +/* + * There are two write frames, and until now this driver only sent one. + * + * RetailOS has two register-write helpers and they put different things on + * the wire: + * + * sub_43CDB4 6C hi lo 00 data 5 bytes + * sub_3FA0E0 6C hi (lo|0x80) 01 data data 6 bytes + * + * The five-byte form is what this driver has always sent, and it is correct + * -- it is what stock uses for the overwhelming majority of writes, + * including all 80 entries of the graph table. + * + * The six-byte form is used for exactly three registers, and stock never + * writes them any other way: + * + * 0x0225 0x0227 0x0229 + * + * All three sit in the analog output block, all three are in the address + * space CS42L42 and CS43L36 both mark Reserved, and 0x0227 is the output + * gain. This driver has been writing all three with the short frame. + * + * That is consistent with everything observed: the register file accepts + * the short write and reads the value back perfectly -- which is how + * writes to 0x0227 have always verified -- while the analog side never + * takes it. Registers correct, clocks correct, ASP locked, jack silent. + * + * What the extra bytes mean is not documented anywhere available. Bit 7 of + * the low address byte and the 0x01 in the count position both look like + * flags, and the data byte appearing twice looks like a wider transfer than + * the register is. Rather than guess at semantics, the frame is reproduced + * exactly as stock emits it and the dispatch is by register number, which + * is the one thing the decomp states unambiguously. + * + * Dispatch happens inside cs42l81_write() so no call site can get it wrong, + * including the ones that route through cs42l81_rmw(). + */ +/* + * On by default, but switchable, because it is new on the wire. + * + * The device wedged during the first playback test after this went in, and + * the six-byte frame is the newest variable in that path -- the codec + * shares SPI0 and a transfer the engine does not complete looks like a hang + * from the outside. That is a suspicion, not a finding: the same test also + * walked the codec through an 8 kHz open, which is its own hazard and is + * fixed separately. This exists so the two can be told apart from userspace + * on the next boot instead of by reflashing twice. + */ +static bool wide_write = true; +module_param(wide_write, bool, 0644); +MODULE_PARM_DESC(wide_write, + "use the six-byte RetailOS frame for 0x225/0x227/0x229 (default Y)"); + +static bool cs42l81_reg_wants_wide_write(u16 reg) +{ + if (!wide_write) + return false; + + switch (reg) { + case 0x0225: + case 0x0227: + case 0x0229: + return true; + default: + return false; + } +} + +static int cs42l81_write_wide(struct cs42l81 *c, u16 reg, u8 val) +{ + u8 tx[6] = { + 0x6c, + (reg >> 8) & 0xff, + (reg & 0xff) | 0x80, + 0x01, + val, + val, + }; + struct spi_transfer t = { .tx_buf = tx, .len = sizeof(tx) }; + struct spi_message m; + + spi_message_init(&m); + spi_message_add_tail(&t, &m); + return spi_sync(c->spi, &m); +} + static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val) { u8 tx[5] = { @@ -383,9 +432,12 @@ static int cs42l81_write(struct cs42l81 *c, u16 reg, u8 val) 0x00, val, }; - struct spi_transfer t = { .tx_buf = tx, .len = 5 }; + struct spi_transfer t = { .tx_buf = tx, .len = sizeof(tx) }; struct spi_message m; + if (cs42l81_reg_wants_wide_write(reg)) + return cs42l81_write_wide(c, reg, val); + spi_message_init(&m); spi_message_add_tail(&t, &m); return spi_sync(c->spi, &m); @@ -437,10 +489,12 @@ static int cs42l81_bringup(struct cs42l81 *c) if (ret) return ret; - /* unlock-like */ - cs42l81_write(c, 0x9901, 0xa5); - cs42l81_write(c, 0x9901, 0x00); - + /* + * The 0x9901 0xa5/0x00 unlock used to be here. Stock (sub_D2EFC) runs + * it from inside state 3, once per boot, after the codec clock has + * been re-enabled -- not at probe, where there is no clock at all yet. + * It moved to cs42_d3280_state3_unfreeze(). + */ cs42l81_write(c, 0xc81f, 0xff); cs42l81_write(c, 0xc85f, 0x0f); @@ -543,6 +597,31 @@ static int cs42_graph_end(struct cs42l81 *c, int page) return 0; } +/* + * D3280(4) on the stop path. It is a genuine power-down -- it mutes to + * -76 dB, clears the analog enable and drops the 2v5 rail -- and for a + * long time this driver ran it as the last step of *prepare* under the + * name "state_4_output_on". Switchable while the play path is still being + * characterised. + */ +static bool state4_on_stop; +module_param(state4_on_stop, bool, 0644); +MODULE_PARM_DESC(state4_on_stop, "run D3280(4) standby on stop (default N)"); + +static int cs42_d3280_state4_standby(struct cs42l81 *c); + +/* + * Which analog power-up to run at prepare: OSOS sub_D3280(1) or the + * bootloader's sub_1310. They differ in five registers and in the final + * state of 0x0007 bit 6; see cs42_d3280_state1_analog_on(). OSOS is the + * one that goes on to play music, so it is the default, and the bootloader + * path stays reachable for comparison. + */ +static bool osos_analog_on = true; +module_param(osos_analog_on, bool, 0644); +MODULE_PARM_DESC(osos_analog_on, + "use OSOS D3280(1) rather than bootloader sub_1310 (default Y)"); + static int cs42_write_table(struct cs42l81 *c, const struct cs42_regval *t, unsigned int n) { @@ -557,11 +636,53 @@ static int cs42_write_table(struct cs42l81 *c, const struct cs42_regval *t, return 0; } +/* + * Put 0x0006 bit 0 back after the graph table, and check that it took. + * + * This was argued both ways and the device settled it. The call graph says + * sub_5707D8 is reached only from sub_570620, which is reached only from + * sub_42D364(1) -- the play trigger -- so the graph is built after the + * power-up, and since nothing in the image re-sets bit 0 it looked as + * though stock simply plays with it clear. + * + * It does not. Measured on the device, across the 80 writes of the table: + * + * prepare complete 0x002F = 0x80 ready=1 + * after the table 0x002F = 0x00 ready=0 + * + * 0x002F bit 7 is the readiness D3280(1) polls for, and the table's + * {0x0006, 0x24} drops it. So bit 0 is a level after all, the analog block + * really does go down when the graph is programmed, and the inference from + * the call graph was wrong -- most likely because the vtable dispatch hides + * a D3280 transition that stock runs after the graph and we cannot see. + * + * Restore the bit and re-poll, exactly as the power-up does, so the log + * says whether the block came back rather than leaving it to be assumed. + */ +static void cs42_restore_analog_power(struct cs42l81 *c) +{ + unsigned int i; + u8 r06 = 0, r2f = 0; + + cs42l81_rmw(c, 0x0006, 0x01, 0x01); + for (i = 0; i < 50; i++) { + if (cs42l81_read(c, 0x002f, &r2f)) + break; + if (r2f & 0x80) + break; + usleep_range(1000, 1500); + } + cs42l81_read(c, 0x0006, &r06); + dev_info(&c->spi->dev, + "graph: 0x006=0x%02x 0x02F=0x%02x ready=%d after %u polls\n", + r06, r2f, !!(r2f & 0x80), i); +} + /* Read back last-write-wins expected values for 0x400..0x448 + key regs. */ static void cs42_verify_5707d8(struct cs42l81 *c) { static const struct cs42_regval expect[] = { - { 0x0006, 0x24 }, + { 0x0006, 0x25 }, { 0x0529, 0x2c }, { 0x052a, 0x2c }, { 0x0533, 0x2c }, { 0x0534, 0x2c }, { 0x0400, 0x04 }, { 0x0401, 0x12 }, @@ -878,6 +999,7 @@ static int cs42_build_play_graph_static(struct cs42l81 *c) ARRAY_SIZE(cs42_static_5707d8)); if (ret) goto out; + cs42_restore_analog_power(c); /* * Split the settle from the write that follows it. @@ -1082,7 +1204,13 @@ static int cs42_play_unmute(struct cs42l81 *c) if (ret) return ret; if (post_iis_401_rmw) { - ret = cs42l81_rmw(c, 0x0401, 0x03, 0x02); + /* + * Bit 1 only. sub_570620 and sub_42D364 always drive bits 0 + * and 1 of 0x0401 in separate masked writes and never as a + * pair; mask 0x03 cleared bit 0 as a side effect of setting + * bit 1. + */ + ret = cs42l81_rmw(c, 0x0401, 0x02, 0x02); if (ret) return ret; } @@ -1115,8 +1243,6 @@ static int cs42_570620_play_graph(struct cs42l81 *c, int mode) return 0; } -static bool cs42_headset_ready(void); - /* * Do not fold into codec prepare — this is the play lifecycle latch. */ @@ -1124,33 +1250,26 @@ static int cs42_retailos_play_start(struct cs42l81 *c) { int ret; - /* - * A missing headset is not an error. - * - * This used to return -ENODEV, which is where the -19 in every boot - * log came from. Failing the stream is the wrong response for two - * reasons. It makes an absent jack -- or MikeyBus simply not having - * probed yet, which at boot is a race we lose more often than not -- - * break the codec for everything, including routes that do not go to - * the jack at all. And a caller that retries on failure will sit there - * cycling PCM start/stop, which is what filled the boot log. - * - * What RetailOS does here is 42D364(0) -- it acts on the analog output and - * carries on rather than refusing. We carry on too: the stream configures - * and the DAC runs, and the analog mute is left to the normal play path - * rather than being forced here, since forcing a mute on a detection - * result we do not fully trust is its own way to produce silence. Set - * There is no gate any more; this path never refuses on jack state. - */ - /* No jack gate on the play latch either -- see cs42_codec_prepare(). */ - ret = cs42_f141c_play_unmute(c, true); if (ret) return ret; - ret = cs42_570620_play_graph(c, 1); - if (ret) - return ret; + /* + * The graph is built in cs42_codec_prepare(); see the note there. + * Starting playback only re-arms it. The fallback covers a start + * that somehow arrives without a prepare. + */ + if (!c->graph_latched) { + dev_warn(&c->spi->dev, "play_start: graph not built, building now\n"); + ret = cs42_570620_play_graph(c, 1); + if (ret) + return ret; + c->graph_latched = true; + } else { + ret = cs42l81_rmw(c, 0x0401, 0x02, 0x02); + if (ret) + return ret; + } cs42_log_graph_snapshot(c, "post_play_start"); cs42l81_log_start_state(c, "play_start"); @@ -1167,122 +1286,30 @@ static int cs42_retailos_play_stop(struct cs42l81 *c) int ret; ret = cs42_42d364_stop(c); - if (!ret) - cs42l81_log_start_state(c, "play_stop"); - return ret; -} - -static bool cs42_headset_ready(void) -{ - int (*ready)(void); - int r; - - if (force_headset) - return true; - ready = (int (*)(void))__symbol_get("apple_mikeybus_headset_ready"); - if (!ready) { - ready = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); - if (!ready) - return true; /* no mikey module — analog HP path */ - r = ready(); - __symbol_put("apple_mikeybus_jack_present"); - if (r < 0) - return true; /* loaded but unbound (uart2 disabled) */ - return r > 0; - } - r = ready(); - __symbol_put("apple_mikeybus_headset_ready"); + if (ret) + return ret; /* - * -ENODEV: module loaded, serdev never probed (uart2 status=disabled). - * That is not "open circuit". Blocking DAI here is the -19 bug. - * 0: resistor task measured open circuit. - * 1: identified accessory or force_plugged / unmeasured-ready. + * D3280(4): mute to -76 dB, analog enable off, 2v5 rail down. This is + * where stock runs it and where the register writes make sense. */ - if (r < 0) - return true; - return r > 0; -} - -/* RE D3280(3)/audio_on HSDET pulse — tip/ring sense + 0x0B type read. */ -static void cs42_hsdet_pulse(struct cs42l81 *c) -{ - u8 r220 = 0, r2f = 0, r0b = 0, r08 = 0, r09 = 0; - unsigned int j; - - cs42l81_rmw(c, 0x0073, 0xc3, 0x00); - cs42l81_rmw(c, 0x0073, 0xc0, 0xc0); - cs42l81_rmw(c, 0x0079, 0x60, 0x00); - cs42l81_read(c, 0x0220, &r220); - cs42l81_rmw(c, 0x0220, 0x40, 0x40); - msleep(1); - cs42l81_rmw(c, 0x0009, 0xc0, 0xc0); - for (j = 0; j < 3; j++) { - msleep(1); - cs42l81_read(c, 0x002f, &r2f); - if (r2f & 0x40) - break; - } - cs42l81_read(c, 0x000b, &r0b); - cs42l81_rmw(c, 0x0009, 0xc0, 0x80); - cs42l81_rmw(c, 0x0220, 0x40, r220 & 0x40); - cs42l81_read(c, 0x0008, &r08); - cs42l81_read(c, 0x0009, &r09); - dev_info(&c->spi->dev, - "HSDET 0x0B=0x%02x type=%u 0x2F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", - r0b, r0b & 3, r2f, r08, r09); -} - -static void cs42_jack_poll_stop(struct cs42l81 *c) -{ - c->jack_poll_active = false; - cancel_delayed_work_sync(&c->jack_work); -} - -static void cs42_jack_workfn(struct work_struct *work) -{ - struct cs42l81 *c = container_of(work, struct cs42l81, jack_work.work); - bool present; - int jack; - - mutex_lock(&c->lock); - if (!c->jack_poll_active) - goto out_unlock; - - jack = -ENODEV; - { - int (*jp)(void) = (int (*)(void)) - __symbol_get("apple_mikeybus_jack_present"); - - if (jp) { - jack = jp(); - __symbol_put("apple_mikeybus_jack_present"); - } - } - present = force_headset || jack != 0; - if (jack == 0 && c->route_playing) { - dev_info(&c->spi->dev, "jack unplug -> 42D364(0)\n"); - cs42_42d364_stop(c); - cs42_hsdet_pulse(c); - } else if (jack > 0 && !c->jack_last_present && !c->route_playing) { - dev_info(&c->spi->dev, "jack plug -> re-arm HSDET\n"); - cs42_hsdet_pulse(c); + /* + * Off by default. A transport stop is sub_42D364(0) and nothing more. + * D3280(4) is a full analog power-down -- 0x0006 bit 0 clear, rail to + * 0x0E, 0x0225 to 0, gain to -76 dB -- and running it on every PCM + * stop was unrecoverable, because cs42_codec_prepare() early-returns + * when the rate has not changed and so never re-ran the power-up. + * The first tone played and every one after it was silent. + * + * When it is enabled, drop prepared_rate so the next prepare really + * does reconfigure. + */ + if (state4_on_stop) { + cs42_d3280_state4_standby(c); + c->prepared_rate = 0; + c->graph_latched = false; } - c->jack_last_present = present; - if (c->jack_poll_active && jack_poll_ms) - schedule_delayed_work(&c->jack_work, - msecs_to_jiffies(jack_poll_ms)); -out_unlock: - mutex_unlock(&c->lock); -} - -static void cs42_jack_poll_start(struct cs42l81 *c) -{ - if (!jack_poll_ms) - return; - c->jack_poll_active = true; - c->jack_last_present = cs42_headset_ready(); - cancel_delayed_work(&c->jack_work); - schedule_delayed_work(&c->jack_work, msecs_to_jiffies(jack_poll_ms)); + cs42l81_log_start_state(c, "play_stop"); + return 0; } /* @@ -1318,9 +1345,124 @@ static int cs42l81_db_to_code(int db) * writes `v4 & 0x7F`. Writing the sign-extended byte instead sets a bit 7 * that is not part of the gain field. */ +/* + * 0x0227 is a signed gain in dB, not a magnitude. + * + * RetailOS sub_400330 opens with + * + * if ( (a1 & 0x40) != 0 ) a1 |= 0x80u; + * + * which is a six-bit-to-eight-bit sign extension: 0x6c reads as -20 dB, + * which is exactly what this driver already prints for user volume 56. The + * mask-to-0x7f on the way out keeps the field width. + */ +static int cs42_gain_to_db(u8 raw) +{ + if (raw & 0x40) + raw |= 0x80; + return (int)(s8)raw; +} + +/* + * Bring the 2.5 V analog backpower rail up. + * + * This is the piece that was missing. sub_400330 is not a volume setter -- + * it is a volume setter wrapped around rail management, and the symbols in + * it say so outright: + * + * "!gCS42L81_2v5Backpower_LastTimestamp" "sphwDACCS42L81.c" + * + * The rail is gated on the gain crossing -8 dB. Coming up out of deep + * attenuation it runs the sequence below; going back down it starts a + * 601 ms timer and drops the rail afterwards. Everything else in the codec + * can be configured correctly -- ASP locked, clocks present, DAC clocked, + * mixer powered -- and the jack stays silent if this rail was never raised, + * which is the state this driver has been leaving it in: it wrote 0x0227 + * and nothing else. + * + * The guard is stock's own: skip the work when 0xC96F already reads 30 and + * 0x0219 low three bits already read 1. + * + * sub_345D58 between the two 0xC96F writes is a thunk out to 0x2200104A, + * outside the extracted range. A settle delay is the only thing that fits + * between raising a rail and latching it, so it is one here, with the + * length exposed rather than guessed silently. + */ +static unsigned int backpower_settle_ms = 50; +module_param(backpower_settle_ms, uint, 0644); +MODULE_PARM_DESC(backpower_settle_ms, + "settle inside the 2v5 backpower sequence (sub_345D58)"); + +static int cs42_2v5_backpower_up(struct cs42l81 *c) +{ + u8 r_c96f = 0, r219 = 0; + int ret; + + ret = cs42l81_read(c, 0xc96f, &r_c96f); + if (ret) + return ret; + ret = cs42l81_read(c, 0x0219, &r219); + if (ret) + return ret; + + if (r_c96f == 0x1e && (r219 & 0x07) == 0x01) { + dev_dbg(&c->spi->dev, "2v5 backpower already up\n"); + return 0; + } + + dev_info(&c->spi->dev, + "2v5 backpower up: C96F=%02x 219=%02x -> C96F=0e, 219 lo3=1, %ums, C96F=1e\n", + r_c96f, r219, backpower_settle_ms); + + ret = cs42l81_write(c, 0xc96f, 0x0e); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0219, 0x07, 0x01); + if (ret) + return ret; + if (backpower_settle_ms) + msleep(backpower_settle_ms); + return cs42l81_write(c, 0xc96f, 0x1e); +} + +/* + * Set the output gain, and manage the rail around it -- sub_400330. + * + * Deliberately the only place 0x0227 is written, so the threshold crossing + * cannot be bypassed by a caller that just wants a volume change. + */ static int cs42l81_set_output_gain(struct cs42l81 *c, int code) { - return cs42l81_write(c, 0x0227, (u8)(code & 0x7f)); + u8 raw = 0; + int old_db, new_db, ret; + + new_db = cs42_gain_to_db((u8)(code & 0x7f)); + + ret = cs42l81_read(c, 0x0227, &raw); + old_db = ret ? -128 : cs42_gain_to_db(raw); + + /* Coming up out of deep attenuation: the rail has to be there first. */ + if (old_db < -8 && new_db >= -8) { + ret = cs42_2v5_backpower_up(c); + if (ret) + dev_warn(&c->spi->dev, + "2v5 backpower failed (%d); gain applied anyway\n", + ret); + } + + ret = cs42l81_write(c, 0x0227, (u8)(code & 0x7f)); + + /* + * Going the other way stock starts a 601 ms timer and drops the rail + * when it expires. Not implemented: dropping a rail we have only just + * learned to raise buys nothing here, and leaving it up costs idle + * current on a device that is being debugged. + */ + if (!ret && old_db >= -8 && new_db < -8) + dev_dbg(&c->spi->dev, + "gain %d -> %d dB: stock would drop 2v5 after 601ms\n", + old_db, new_db); + return ret; } /* @@ -1337,9 +1479,14 @@ static int cs42l81_apply_mode_271(struct cs42l81 *c) ret = cs42l81_rmw(c, 0x0220, 0x28, 0x00); if (ret) return ret; - ret = cs42l81_rmw(c, 0x000d, 0x03, 0x00); - if (ret) - return ret; + c->mode38 = 0; /* MEMORY[0x892A038] = v3 & 0x28, v3 == 0 here */ + /* + * 0x000D is deliberately absent. sub_D2F64 only writes it when its + * v25 is non-zero, and v25 is set solely in the (a1 & 0xC08) == 0 + * branch -- which is mode 6, not mode 271. This driver wrote + * 0x000D[1:0] = 0 in both, so the "output on" path was applying the + * "output off" path's value and holding it there for all of playback. + */ ret = cs42l81_rmw(c, 0x0206, 0x3f, 0x3d); if (ret) return ret; @@ -1349,9 +1496,10 @@ static int cs42l81_apply_mode_271(struct cs42l81 *c) ret = cs42l81_rmw(c, 0x0205, 0xff, 0x5a); if (ret) return ret; - ret = cs42l81_rmw(c, 0x0204, 0x03, 0x00); - if (ret) - return ret; + /* + * 0x0204 is also absent: sub_42A5D6(516, v6, v32) with v6 == 0 is a + * read-modify-write under an empty mask, i.e. nothing. + */ ret = cs42l81_rmw(c, 0x0206, 0xc0, 0x00); if (ret) return ret; @@ -1385,12 +1533,19 @@ static int cs42l81_apply_mode_6(struct cs42l81 *c) { int ret; - ret = cs42l81_rmw(c, 0x0006, 0x04, 0x00); + /* + * Set, not cleared. sub_D2F64 computes this value as v31, and v31 + * becomes 4 exactly when (a1 & 0x180) == 0 -- true for 6, false for + * 271. The two modes drive this bit in opposite directions and this + * driver had them driving it the same way. + */ + ret = cs42l81_rmw(c, 0x0006, 0x04, 0x04); if (ret) return ret; ret = cs42l81_rmw(c, 0x0220, 0x28, 0x00); if (ret) return ret; + c->mode38 = 0; /* MEMORY[0x892A038] = v3 & 0x28, v3 == 0 here */ ret = cs42l81_rmw(c, 0x000d, 0x03, 0x00); if (ret) return ret; @@ -1435,6 +1590,7 @@ static int cs42l81_output_path_enable(struct cs42l81 *c) } /* OSOS sub_D2D2C(0). Kept for teardown; HP bring-up uses enable only. */ + static int __maybe_unused cs42l81_output_path_disable(struct cs42l81 *c) { int ret; @@ -1457,7 +1613,7 @@ static int __maybe_unused cs42l81_output_path_disable(struct cs42l81 *c) * Do not treat D3280(3) alone as playback-active. * Sequence includes sub_400330(64)/(65) before final 0x229=0x41. */ -static int cs42l81_state_4_output_on(struct cs42l81 *c) +static int cs42_d3280_state4_standby(struct cs42l81 *c) { int ret; @@ -1470,6 +1626,7 @@ static int cs42l81_state_4_output_on(struct cs42l81 *c) ret = cs42l81_write(c, 0x0229, 0x40); if (ret) return ret; + /* Analog enable off -- this is the standby transition. */ ret = cs42l81_rmw(c, 0x0006, 0x01, 0x00); if (ret) return ret; @@ -1482,8 +1639,8 @@ static int cs42l81_state_4_output_on(struct cs42l81 *c) ret = cs42l81_write(c, 0xc85f, 0x0f); if (ret) return ret; - /* RE state 4 base is 0x0E; glass A/B via c96f_final (try 0x1E). */ - ret = cs42l81_write(c, 0xc96f, (u8)(c96f_final & 0xff)); + /* Rail down. sub_D3280(4) writes 0x0E here, unconditionally. */ + ret = cs42l81_write(c, 0xc96f, 0x0e); if (ret) return ret; ret = cs42l81_write(c, 0x0223, 0x08); @@ -1492,23 +1649,11 @@ static int cs42l81_state_4_output_on(struct cs42l81 *c) ret = cs42l81_write(c, 0x0224, 0x09); if (ret) return ret; + /* State 1 puts 0x33 here; standby takes it to 0. */ ret = cs42l81_write(c, 0x0225, 0x00); if (ret) return ret; - /* - * Exact D3280(4) RE: 400330(64)/400330(65) then 229=0x41. - * Optional glass rail nudge (c96f_final=0x1e) keeps prior 219 lo3 dance. - */ - if ((c96f_final & 0xff) == 0x1e) { - ret = cs42l81_rmw(c, 0x0219, 0x07, 0x01); - if (ret) - return ret; - msleep(100); - ret = cs42l81_write(c, 0xc96f, 0x1e); - if (ret) - return ret; - } - /* D3280(4): 3FA0E0(553,64) before 400330 pair on 227. */ + /* D3280(4): 3FA0E0(553,64) before the 400330 pair on 0x0227. */ ret = cs42l81_write(c, 0x0229, 0x40); if (ret) return ret; @@ -1525,24 +1670,49 @@ static int cs42l81_state_4_output_on(struct cs42l81 *c) if (ret) return ret; - dev_info(&c->spi->dev, "D3280(4) / output_on complete\n"); + dev_info(&c->spi->dev, "D3280(4) standby complete\n"); return 0; } /* - * OSOS sub_D3280(a1==3) — headset detect / pre-output (not full play). + * OSOS sub_D3280(a1==3). + * + * Named for what it does rather than what it was once thought to be: it + * restores the codec clock and releases the freeze latch D3280(1) takes + * (0x0006 bit 6, 0x0007 bit 6, 0x0075 bit 7). The 0x007B/0x007C reads in + * the middle are stock and are left in place, but nothing here consults + * them or gates on them. */ -static int cs42l81_state_3_headset_detect(struct cs42l81 *c) +static int cs42_d3280_state3_unfreeze(struct cs42l81 *c) { u8 r74 = 0, r7b = 0, r7c = 0, r0f = 0, r2f = 0; int ret; + /* sub_41CBD8(9, 1): clock back on before the latch is released. */ + cs42_codec_clk(c, true); + + /* + * sub_D2EFC, guarded by MEMORY[0x892A028] so it runs exactly once. + * Whatever 0x9901 gates, stock wants it opened here and not again. + */ + if (!c->unlock_done) { + cs42l81_write(c, 0x9901, 0xa5); + cs42l81_write(c, 0x9901, 0x00); + c->unlock_done = true; + } + ret = cs42l81_rmw(c, 0x0007, 0x40, 0x00); if (ret) return ret; + /* Release of the freeze latch state 1 sets. Not optional. */ ret = cs42l81_rmw(c, 0x0006, 0x40, 0x00); if (ret) return ret; + c->mode38 = 0x28; /* MEMORY[0x892A038] = 40 */ + /* + * 0x220 bits 5 and 3. State 1 has already put mask 0x78 to 0x78, + * which subsumes this; stock writes it anyway and so do we. + */ ret = cs42l81_rmw(c, 0x0220, 0x28, 0x28); if (ret) return ret; @@ -1567,16 +1737,156 @@ static int cs42l81_state_3_headset_detect(struct cs42l81 *c) return 0; } -/* OSOS sub_D34C0 / 183138 rate programming. */ -static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) +/* + * The sample-rate group, properly dispatched — RetailOS sub_D34C0. + * + * sub_D34C0 is not a sequence, it is a three-way branch on a mode word the + * rest of the driver maintains (MEMORY[0x892A038], modelled here as + * c->mode38): + * + * mode38 & 0x08 and mode38 & 0x20 -> short path: 0x000F/0x012F only + * mode38 & 0x08 and not 0x20 -> long path: 0x0121/0x0122/0x0130/ + * 0x0131 and 0x0222/0x0223/0x0224 + * otherwise -> sub_183138(code, 1) + * + * This driver ran the 183138 body and then the long path's tail on top of + * it, unconditionally, as though D34C0 were one straight sequence. They are + * alternatives. The two use *different* register pairs for the same job -- + * 183138 programs 0x010B/0x010C, the long path programs 0x0223/0x0224 -- + * so running both wrote a rate into a block the current mode does not use, + * and, worse, the long path's closing `0x0220 mask 0x20 = 0x00` released a + * hold that 183138 had deliberately left set. + * + * Which branch is live follows from mode38, and mode38 follows from the + * sequence: D3280(3) sets it to 0x28, and sub_D2F64 recomputes it as + * `v3 & 0x28` -- zero for both mode 271 and mode 6. So after + * output_path_enable the third branch is the live one, which is why the + * 183138 body was the right code all along and the tail was not. + * + * mode38 is tracked rather than assumed so the branch keeps following the + * sequence if the sequence changes. + */ + +/* + * 0 = call sub_183138 directly (default, and what prepare needs); 1 = go + * through sub_D34C0's dispatch on mode38. See cs42l81_set_rate(). + */ +static int rate_path; +module_param(rate_path, int, 0644); +MODULE_PARM_DESC(rate_path, "0=183138 direct (default), 1=D34C0 dispatch"); + +/* sub_D34C0 short path: rate code into 0x000F/0x012F, bracketed. */ +static int cs42_d34c0_short(struct cs42l81 *c, u8 code) { - const struct n31_rate_cfg *r = n31_find_rate(rate); - u8 code; int ret; - if (!r) - return -EINVAL; - code = r->cs42_rate_code; + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000f, 0x0f, code); + if (ret) + return ret; + ret = cs42l81_write(c, 0x012f, (u8)(code | (code << 4))); + if (ret) + return ret; + return cs42l81_rmw(c, 0x000e, 0xc0, 0x40); +} + +/* + * sub_D34C0 long path. + * + * Mutes to code 0x40 (-90 dB) and raises the 0x0220 bit-5 hold, programs + * the rate inside a 0x000E bracket, then drops the hold and restores the + * gain. Stock's SRC test is a pair of config-flag lookups + * (sub_149E98(126)/(127)) crossed with the rate; with no access to that + * config the native case is taken to be code 12 exactly, as elsewhere in + * this driver. + */ +static int cs42_d34c0_long(struct cs42l81 *c, u8 code) +{ + bool src = code != 12; + int ret; + + ret = cs42l81_set_output_gain(c, 64); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x20, 0x20); + if (ret) + return ret; + usleep_range(1000, 1500); + + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x000f, 0x0f, code); + if (ret) + return ret; + ret = cs42l81_write(c, 0x012f, (u8)(code | (code << 4))); + if (ret) + return ret; + + if (src) { + ret = cs42l81_write(c, 0x0121, 0x08); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0122, 0x09); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0130, 0x0f, code); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0131, 0x01, 0x00); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0223, 0x04); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0224, 0x33); + } else { + ret = cs42l81_rmw(c, 0x0131, 0x01, 0x01); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0223, 0x08); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0224, 0x09); + } + if (ret) + return ret; + + ret = cs42l81_write(c, 0x0222, src ? 12 : code); + if (ret) + return ret; + + ret = cs42l81_rmw(c, 0x000e, 0xc0, 0x40); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x20, 0x00); + if (ret) + return ret; + usleep_range(1000, 1500); + + /* Stock restores the cached volume here; this driver's cache is + * user_vol, applied at the end of prepare. + */ + dev_info(&c->spi->dev, "D34C0 long: code=%u src=%d\n", code, src); + return 0; +} + +/* + * sub_183138(code, 1). + * + * Ends muted at -90 dB with the 0x0220 bit-5 hold raised, and that is not + * an oversight in the transcription: sub_D2F64 clears 0x0220 mask 0x28 as + * its second write, so output_path_enable is what releases the hold. The + * gain comes back with cs42l81_apply_user_vol() at the end of prepare. + * + * This is why the order in cs42_codec_prepare has to be set_rate before + * output_path_enable and not the other way round. + */ +static int cs42_183138_set_rate(struct cs42l81 *c, u8 code) +{ + int ret; ret = cs42l81_rmw(c, 0x000e, 0xc0, 0xc0); if (ret) @@ -1589,12 +1899,10 @@ static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) return ret; /* - * sub_183138 branches on rate code: - * code==12 (48 kHz): 0x10B=8, 0x10C=9, 0x131 bit0=1 - * else (e.g. 10=44.1): 0x121=8, 0x122=9, 0x130 lo=code, - * 0x131 bit0=0, 0x10B=4, 0x10C=0x33 - * Linux previously always took the 48 kHz arm while IIS ran - * 44.1 (CLKDIV 272) → ASP/SRC mismatch → pulsed noise. + * code == 12 (48 kHz) runs native on 0x010B/0x010C; every other rate + * goes through the SRC, which needs 0x0121/0x0122/0x0130 as well. + * Linux previously always took the 48 kHz arm while IIS ran 44.1 + * (CLKDIV 272) -- an ASP/SRC mismatch, and the pulsed noise with it. */ if (code == 12) { ret = cs42l81_write(c, 0x010b, 0x08); @@ -1631,11 +1939,62 @@ static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) if (ret) return ret; + /* Both of these were missing. See the comment above. */ + ret = cs42l81_set_output_gain(c, 64); + if (ret) + return ret; + return cs42l81_rmw(c, 0x0220, 0x20, 0x20); +} + +static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) +{ + const struct n31_rate_cfg *r = n31_find_rate(rate); + u8 code; + int ret; + + if (!r) + return -EINVAL; + code = r->cs42_rate_code; + + /* + * Prepare takes the 183138 branch, and does not consult mode38 to get + * there. + * + * mode38 shadows 0x0220 bits 5 and 3 -- D3280(3) sets both and writes + * 0x28 to the shadow in the same breath, sub_D2F64 clears both and + * writes v3 & 0x28. Those two bits read as an idle/standby pair: + * sub_D2F64 clears them for *both* mode 271 and mode 6, i.e. whenever + * a route exists at all, and only a1 == 0 (no route) puts them back. + * + * At the point prepare programs the rate, D3280(3) has just set them, + * so a faithful D34C0 dispatch would take the short branch -- which + * writes 0x000F and 0x012F and nothing else. That cannot be right at + * 44.1 kHz: the SRC lives in 0x0121/0x0122/0x0130/0x0131 and the short + * branch never touches it. + * + * The reading that fits is that D34C0 is the steady-state entry point + * (a track change, with a route already up) while initial route setup + * calls sub_183138 directly -- consistent with 183138 having its own + * a2 argument and its own 41F944 route gate. So prepare calls it + * directly, and the D34C0 dispatch is available on rate_path=1 for a + * device-side A/B rather than being settled here by assertion. + */ + if (rate_path == 0 || !(c->mode38 & 0x08)) + ret = cs42_183138_set_rate(c, code); + else if (c->mode38 & 0x20) + ret = cs42_d34c0_short(c, code); + else + ret = cs42_d34c0_long(c, code); + if (ret) + return ret; + c->rate = rate; dev_info(&c->spi->dev, - "CS42 set_rate %u code=%u 10B=%02x 10C=%02x 131bit0=%d\n", - rate, code, code == 12 ? 0x08 : 0x04, - code == 12 ? 0x09 : 0x33, code == 12 ? 1 : 0); + "CS42 set_rate %u code=%u mode38=0x%02x path=%s\n", + rate, code, c->mode38, + (rate_path && (c->mode38 & 0x08)) + ? ((c->mode38 & 0x20) ? "D34C0/short" : "D34C0/long") + : "183138"); return 0; } @@ -1646,8 +2005,8 @@ static int cs42l81_set_rate(struct cs42l81 *c, unsigned int rate) /* * Stage markers for codec prepare. * - * This path can hang the kernel, and there was nothing between "no headset - * reported" and roughly fifty register writes to say how far it got. Each + * This path can hang the kernel, and there was nothing between entry and + * roughly fifty register writes to say how far it got. Each * marker prints BEFORE its step, so the last line in the log names the * operation that never returned rather than the last one that succeeded. * That distinction is the point: a trailing "ok" tells you where you were @@ -1715,6 +2074,150 @@ static void cs42_mailbox_reads(struct cs42l81 *c) lvl_51f, lvl_520, v524, i); } +/* + * Keep the bootloader's analog power-up values through the play path. + * + * cs42_analog_power_up() sets 0x225=0x19, 0x220=0x50 within mask 0x78, and + * 0x006 bit 6, and it reports success: "analog power-up: 0x2F=0x80 + * ready=1". Then the register dump taken after a playback reads 0x225=0x00 + * and 0x220=0x78, and there is no sound at the jack. + * + * Two writes on the play path undo it. cs42l81_state_4_output_on() -- the + * function whose job is to turn the output on -- writes 0x225 = 0x00 as a + * whole byte, and the dump confirms it ran (223=08 224=09 225=00 is exactly + * its sequence). cs42_d3280_state3_unfreeze() clears 0x006 bit 6, which + * the power-up had just set and which the bootloader leaves set. + * + * The driver's own notes flag both as deviations: "0x225 mask 0xFF = 0x19 + * we write 0x00 here" and "0x220 mask 0x78 = 0x50 we use mask 0x28". + * + * This is a switch rather than an edit because the two sequences come from + * different reverse-engineering: the bootloader's sub_1310 and OSOS's + * sub_D3280(4). Both could be right at their own moment. It defaults on + * because the current behaviour produces no sound at all, so there is + * nothing to lose by trying the bootloader's values and something to learn + * either way. + */ + +/* + * RetailOS sub_41CBD8(sub_4F82F8(), on) — CLKCON+0x0C bit 15, active low. + * D3280(1) drops it, D3280(3) restores it, and the 0x0006/0x0007 bit-6 + * freeze latch exists to hold the analog block across that gap. The gate + * itself lives in the IIS driver, which owns the CLKCON mapping. + */ +static bool codec_clk_gate = true; +module_param(codec_clk_gate, bool, 0644); +MODULE_PARM_DESC(codec_clk_gate, + "cycle CLKCON+0x0C bit15 across D3280(1)/(3) as stock does"); + +static void cs42_codec_clk(struct cs42l81 *c, bool on) +{ + void (*gate)(bool); + + if (!codec_clk_gate) + return; + gate = (void (*)(bool))__symbol_get("s5l8740_codec_clk_gate"); + if (!gate) { + dev_warn_once(&c->spi->dev, + "s5l8740_codec_clk_gate absent — clock left running\n"); + return; + } + gate(on); + __symbol_put("s5l8740_codec_clk_gate"); +} + +/* + * OSOS sub_D3280(a1 == 1) -- the analog power-up the shipping firmware + * actually runs. + * + * cs42_analog_power_up() below is the *bootloader's* version, sub_1310, and + * this driver has been running that at prepare time on the reasoning that + * it is the sequence which produces the audible plop. It is -- at boot. + * OSOS then runs its own, and the two are not the same sequence: + * + * bootloader sub_1310 OSOS sub_D3280(1) + * 0x0227 rmw 0x7f = 0x40 wr = 0x40 (six-byte frame) + * 0x0225 rmw 0xff = 0x19 wr = 0x33 (six-byte frame) + * 0x0226 rmw 0xff = 0x19 not written + * 0x0228 rmw 0x7f = 0x40 not written + * 0x0229 not written wr = 0x40 (six-byte frame) + * 0x0075 not touched rmw 0x80 = 0x00 + * 0x0220 rmw 0x78 = 0x50 rmw 0x78 = 0x78 + * 0x0006 b0 set set + * 0x002F poll bit 7 poll bit 7 + * 0x0006 b6 set set + * 0x0007 b6 CLEARED SET + * + * The last row is the one that should not be shrugged at. The bootloader + * leaves 0x0007 bit 6 clear; OSOS leaves it set, and then clears it again + * in state 3. Running the bootloader's ending means state 3 clears a bit + * that was never set, and whatever that bit gates never went through its + * transition. + * + * 0x0229 and 0x0075 are simply absent from the bootloader path, and 0x0225 + * and 0x0220 differ in value rather than in kind. + * + * The three six-byte writes come out of cs42l81_write() automatically -- + * 0x0225, 0x0227 and 0x0229 are exactly the registers that dispatch to the + * wide frame, which is not a coincidence: they are the analog output + * registers, and this is the analog power-up. + * + * The poll stays bounded. Stock's is a bare do/while on 0x2F bit 7 with no + * escape, which is fine in an RTOS that owns the machine and is not fine + * here. + */ +static int cs42_d3280_state1_analog_on(struct cs42l81 *c) +{ + unsigned int i; + u8 v2f = 0; + bool ready = false; + int ret; + + ret = cs42l81_write(c, 0x0227, 0x40); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0225, 0x33); + if (ret) + return ret; + ret = cs42l81_write(c, 0x0229, 0x40); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0075, 0x80, 0x00); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0220, 0x78, 0x78); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0006, 0x01, 0x01); + if (ret) + return ret; + + for (i = 0; i < 50; i++) { + usleep_range(1000, 1500); + if (cs42l81_read(c, 0x002f, &v2f)) + break; + if (v2f & 0x80) { + ready = true; + break; + } + } + + ret = cs42l81_rmw(c, 0x0006, 0x40, 0x40); + if (ret) + return ret; + ret = cs42l81_rmw(c, 0x0007, 0x40, 0x40); + if (ret) + return ret; + + /* sub_41CBD8(9, 0): codec clock off, latch holds the analog block. */ + cs42_codec_clk(c, false); + + dev_info(&c->spi->dev, + "D3280(1) analog on: 0x2F=0x%02x ready=%d after %u polls\n", + v2f, ready, i); + return 0; +} + /* * Analog power-up, from the N31 bootloader (sub_1310 @ 0x1566). * @@ -1789,26 +2292,6 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) { u8 st = 0, r219 = 0; int ret; - int jack = -ENODEV; - int (*mikey_jack)(void); - - /* - * Same rule as cs42_retailos_play_start(): an absent headset must not - * fail the stream. This is the copy that actually matters, because - * hw_params lands here, so -ENODEV came straight back out of - * snd_soc_dai_hw_params and ASoC walked every advertised rate looking - * for one that would take -- all the way down to 8 kHz, failing each. - * That is the start/stop churn in the boot log. - */ - /* - * No jack detection here. This board does not use the codec's jack - * detect, and consulting it did nothing but harm: it gated the whole - * bring-up on a MikeyBus answer that is absent whenever UART2 is not - * up, returned -ENODEV out of hw_params, and left the PCM layer - * retrying at every advertised rate -- which is where the boot-time - * pinmux and reset storm came from. Whether something is plugged in - * is not the codec driver's business and never gates configuration. - */ CS42_STAGE(c, "d1830_audio_rails"); { @@ -1829,21 +2312,6 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) } } - CS42_STAGE(c, "mikeybus_jack_present"); - mikey_jack = (int (*)(void))__symbol_get("apple_mikeybus_jack_present"); - if (mikey_jack) { - jack = mikey_jack(); - __symbol_put("apple_mikeybus_jack_present"); - } - if (jack < 0) - dev_info(&c->spi->dev, - "MikeyBus unbound (uart2 disabled) — analog HP not gated\n"); - else if (jack == 0) - dev_warn(&c->spi->dev, - "MikeyBus open circuit — force_headset=1 to override\n"); - else - dev_info(&c->spi->dev, "MikeyBus jack present\n"); - { void (*ts_path)(struct device *); @@ -1858,10 +2326,6 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) } } - ret = cs42l81_state_3_headset_detect(c); - if (ret) - return ret; - if (!rate) rate = cs42_pick_rate(c, 0); /* @@ -1892,8 +2356,39 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) dump_stack(); } + c->graph_latched = false; + CS42_STAGE(c, "analog_power_up"); - cs42_analog_power_up(c); + if (osos_analog_on) + cs42_d3280_state1_analog_on(c); + else + cs42_analog_power_up(c); + + /* + * State 3 goes *after* state 1, which is not where this call used to + * sit. + * + * The two are a matched pair around the codec clock gate. State 1 + * ends by setting 0x0006 bit 6 and 0x0007 bit 6 and then dropping the + * clock (sub_41CBD8(clk, 0)); state 3 restores the clock and clears + * those same two bits. They are a freeze latch held across the gate + * transition, and stock never runs one without the other. + * + * This driver called state 3 first, up before the rate check, so it + * cleared a latch nothing had set and then state 1 set it -- with no + * one left to clear it. The analog block spent the whole of playback + * held. 0x0075 bit 7 inverted the same way: state 1 clears it, state 3 + * sets it, and in the old order we finished with it clear. + * + * With the bootloader power-up this was merely inert, because that + * sequence ends with 0x0007 bit 6 *cleared*. Against OSOS state 1 it + * is fatal, which is why the order matters now and did not appear to + * before. + */ + CS42_STAGE(c, "state_3_unfreeze"); + ret = cs42_d3280_state3_unfreeze(c); + if (ret) + return ret; CS42_STAGE(c, "mailbox_reads"); cs42_mailbox_reads(c); @@ -1908,17 +2403,82 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) if (ret) return ret; - CS42_STAGE(c, "state_4_output_on"); - ret = cs42l81_state_4_output_on(c); + /* + * Build the play graph here, not in the trigger. + * + * It is 80 SPI writes and a 100 ms settle -- measured, 118 ms. Run + * from the transport START callback that delay lands between the + * application asking for playback and the DMA being kicked, and the + * stream stops again about thirty milliseconds later. hw_params has + * no such constraint. + * + * cs42_retailos_play_start() then has only to unmute and re-arm + * 0x0401 bit 1, which is microseconds. + */ + CS42_STAGE(c, "play_graph"); + ret = cs42_570620_play_graph(c, 1); if (ret) return ret; + c->graph_latched = true; - /* Play-object companions (40C028/54F/220) — safe before graph latch. */ - cs42l81_rmw(c, 0x0075, 0x3f, 0x3c); + /* + * D3280(4) used to be called here, as the last step of prepare, under + * the name "state_4_output_on". It is a power-DOWN sequence and the + * name was wrong. + * + * sub_3C6244 settles it. That function is a pure dB-code converter -- + * sign-extend the 6-bit code, then map: -64 becomes -90, and anything + * below -50 becomes 2*code + 50. State 4 feeds it 64 and 65, i.e. + * codes 0x40 and 0x41, i.e. **-90 dB and -76 dB**, and caches those as + * the current left/right volume. Around that it clears 0x0006 bit 0 + * (the analog enable state 1 had just polled 0x002F for), drops the + * 2v5 rail to 0x0E, and writes 0x0225 = 0x00 over state 1's 0x33. + * + * So every prepare finished by muting to -76 dB, switching the analog + * block off and dropping the rail -- immediately after bringing all + * three up. + * + * It also clobbered the sample rate. 0x0223 = 0x08 / 0x0224 = 0x09 is + * the *native* pair out of sub_D34C0, the one used when no SRC is + * needed. Running that after set_rate meant every 44.1 kHz stream had + * its SRC pair (0x04/0x33) overwritten with the 48 kHz native values a + * few microseconds after being programmed. + * + * It belongs on the standby transition, and that is where it now is: + * cs42_d3280_state4_standby(), called from the stop path. + */ + /* + * 0x054F mask 0xF0 = 0 is attested -- sub_42A5D6(1359, 240, 0). The + * 0x0075 mask 0x3F = 0x3C that used to sit beside it is not: across + * the whole image 0x0075 is only ever written under mask 0x40 or mask + * 0x80, both of which D3280(1) and D3280(3) have already done by this + * point. Nothing writes its low six bits. Removed. + */ cs42l81_rmw(c, 0x054f, 0xf0, 0x00); - cs42l81_rmw(c, 0x0220, 0x28, 0x28); + /* + * 0x0220 mask 0x28 = 0x28 used to be re-applied here. D2F64(271), + * which runs a few lines earlier inside output_path_enable, clears + * exactly those two bits as its second write, so this put them + * straight back and left the register in a state no decoded sequence + * produces. It was not transcribed from anything; it is gone. + */ - cs42_hsdet_pulse(c); + /* + * cs42_hsdet_pulse() used to run here. Its comment credited it to + * "RE D3280(3)/audio_on", and it is from neither: sub_D3280(3) + * touches 0x000F, 0x0074, 0x0075, 0x007B, 0x007C, 0x0006, 0x0007 and + * 0x0220, and none of the registers that function wrote. Searching + * all 42,760 extracted functions, *nothing* in the image writes + * 0x0073, 0x0079 or 0x0009. The sequence was invented. + * + * That would be tolerable if it were inert, and it was not: it left + * 0x0009 -- MCLK control, on the CS42L42 map this part follows for + * the low pages -- rewritten to 0x80 rather than restored, on a codec + * whose clocking prepare has just finished setting up. And it sat + * twenty lines below this function's own comment explaining that this + * board does not use the codec's jack detect and that consulting it + * did nothing but harm. + */ cs42l81_read(c, 0x0227, &st); cs42l81_read(c, 0x0219, &r219); @@ -1930,7 +2490,6 @@ static int cs42_codec_prepare(struct cs42l81 *c, unsigned int rate) "codec_prepare 0x227=0x%02x 0x219=0x%02x rate=%u graph_mode=%d\n", st, r219, rate, graph_mode); c->codec_prepared = true; - cs42_jack_poll_start(c); /* audio_path_mode=0: legacy debug — graph folded into prepare. */ if (audio_path_mode == 0) { @@ -1994,14 +2553,28 @@ static int __maybe_unused cs42l81_set_mute(struct cs42l81 *c, int mute) * software -- that only ever worked on the PIO path, and the codec gain * applies to both paths anyway. */ +/* + * Gain, and nothing else. + * + * This used to force the mute state as well, on every call. Stock's volume + * path is sub_D2C98 to work out the code and sub_400330 to write it, and + * sub_400330 touches 0x0227 and the 2v5 rail -- not 0x0527, and certainly + * not 0x0401. Ours reached 0x0401 through cs42_play_unmute(), so setting + * the volume latched the graph's commit bit, and the call at the end of + * prepare did that before the graph had been built at all. + * + * Mute state is cs42l81_apply_mute(), called from the paths whose job it + * actually is. + */ static int cs42l81_apply_user_vol(struct cs42l81 *c) { - int ret; - - ret = cs42l81_set_output_gain(c, + return cs42l81_set_output_gain(c, cs42l81_db_to_code(cs42l81_vol_to_db(c->user_vol))); - if (ret) - return ret; +} + +/* Apply c->dai_mute to the analog mute, and re-latch the graph if playing. */ +static int cs42l81_apply_mute(struct cs42l81 *c) +{ if (c->dai_mute) return cs42_f141c_play_unmute(c, false); if (c->play_started) @@ -2239,7 +2812,7 @@ static ssize_t mute_store(struct device *dev, struct device_attribute *attr, return -EINVAL; mutex_lock(&c->lock); c->dai_mute = v ? 1 : 0; - ret = cs42l81_apply_user_vol(c); + ret = cs42l81_apply_mute(c); mutex_unlock(&c->lock); return ret ? ret : count; } @@ -2281,98 +2854,67 @@ static ssize_t rreg_store(struct device *dev, struct device_attribute *attr, static DEVICE_ATTR_WO(rreg); /* - * After IIS BCLK/LRCK run (RetailOS 26DDDE: 414FAE before sustained PCM). - * Re-run 183138 clock regs and poll 0x2F bit6 for ASP sync (see asp_bit6_is_los). - * LOS does not always self-recover — pulse 0x220 and retry clock prog. + * Read 0x002F and say what it holds. That is all. + * + * This used to be cs42l81_asp_lock(): five attempts, eighty polls each, + * waiting on 0x002F bit 6 for "ASP sync", re-running the rate programming + * between attempts, and returning -EAGAIN if the bit never moved. + * + * There is no such mechanism in the part. Across the entire OSOS image + * 0x002F is read exactly once -- the readiness poll inside sub_D3280(1), + * which tests bit 7 -- and bit 6 is never examined anywhere. The whole + * check was built on a bit stock never looks at, and the asp_bit6_is_los + * parameter existed because its polarity had never been established + * either: a guess with a switch on it. + * + * What stock does after the rate is programmed is start the I2S and play. + * There is no link-up handshake to wait for. + * + * The cost of the guess was not just wasted code. Worst case it spent + * ~2 seconds per playback start polling a meaningless bit, and once the + * retry path was corrected to re-program the rate properly it also ran + * four mute/unmute cycles on the way through. + * + * The read stays because it is one SPI transaction and the value is worth + * having in the log next to the clock registers. */ -static void cs42l81_asp_clock_pulse(struct cs42l81 *c) -{ - cs42l81_rmw(c, 0x0220, 0x20, 0x00); - udelay(50); - cs42l81_rmw(c, 0x0220, 0x20, 0x20); -} - -/* Re-apply full 183138 during ASP lock (not just 0x0E/0x0F/0x12F). */ -static void cs42l81_asp_program_rate(struct cs42l81 *c) -{ - cs42l81_set_rate(c, cs42_pick_rate(c, c->rate)); -} - -int cs42l81_asp_hold_light(void); - -static int cs42l81_asp_lock(struct cs42l81 *c) +static int cs42_asp_status(struct cs42l81 *c) { - unsigned int attempt, i; u8 r2f = 0, r0e = 0, r0f = 0, r08 = 0, r09 = 0; - for (attempt = 0; attempt < 5; attempt++) { - if (attempt) - cs42l81_asp_clock_pulse(c); - cs42l81_asp_program_rate(c); - for (i = 0; i < 80; i++) { - cs42l81_read(c, 0x002f, &r2f); - if (cs42l81_asp_synced(r2f)) - break; - usleep_range(500, 1000); - } - if (cs42l81_asp_synced(r2f)) - break; - } + cs42l81_read(c, 0x002f, &r2f); cs42l81_read(c, 0x0008, &r08); cs42l81_read(c, 0x0009, &r09); cs42l81_read(c, 0x000e, &r0e); cs42l81_read(c, 0x000f, &r0f); dev_info(&c->spi->dev, - "asp_lock 0x2F=0x%02x los=%d synced=%d 0x0E=0x%02x 0x0F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", - r2f, asp_bit6_is_los, cs42l81_asp_synced(r2f), r0e, r0f, r08, r09); - return cs42l81_asp_synced(r2f) ? 0 : -EAGAIN; + "asp status 0x2F=0x%02x 0x0E=0x%02x 0x0F=0x%02x 0x08=0x%02x 0x09=0x%02x\n", + r2f, r0e, r0f, r08, r09); + return 0; } /* * Mid-stream LOS recovery: one 183138 pulse + short poll (~10 ms). * dma_tone calls this when 0x2F bit6 asserts LOS mid-tone. */ +/* + * Mid-stream "loss of signal" recovery. There is no loss-of-signal bit -- + * see cs42_asp_status() -- so there is nothing to recover from and nothing + * to detect it with. Kept as an exported diagnostic so the tone generator's + * call site still links; it reads and logs and changes nothing. + */ +int cs42l81_asp_hold_light(void); + int cs42l81_asp_hold_light(void) { struct cs42l81 *c = cs42l81_dev; - unsigned int i; - u8 r2f = 0, before = 0; - int ret = -ENODEV; if (!c) return -ENODEV; mutex_lock(&c->lock); - cs42l81_read(c, 0x002f, &before); - if (cs42l81_asp_synced(before)) { - ret = 0; - goto out; - } - cs42l81_asp_program_rate(c); - for (i = 0; i < 24; i++) { - cs42l81_read(c, 0x002f, &r2f); - if (cs42l81_asp_synced(r2f)) - break; - usleep_range(400, 800); - } - if (cs42l81_asp_synced(r2f)) { - ret = 0; - } else { - cs42l81_asp_clock_pulse(c); - cs42l81_asp_program_rate(c); - for (i = 0; i < 16; i++) { - cs42l81_read(c, 0x002f, &r2f); - if (cs42l81_asp_synced(r2f)) - break; - usleep_range(400, 800); - } - ret = cs42l81_asp_synced(r2f) ? 0 : -EAGAIN; - } - dev_info(&c->spi->dev, - "asp_hold_light 0x2F 0x%02x->0x%02x ret=%d\n", - before, r2f, ret); -out: + cs42_asp_status(c); mutex_unlock(&c->lock); - return ret; + return 0; } EXPORT_SYMBOL_GPL(cs42l81_asp_hold_light); @@ -2479,12 +3021,11 @@ EXPORT_SYMBOL_GPL(cs42l81_pre_iis_start); int cs42l81_post_iis_start(void) { struct cs42l81 *c = cs42l81_dev; - int probe; if (!c) return -ENODEV; mutex_lock(&c->lock); - probe = cs42l81_asp_lock(c); + cs42_asp_status(c); /* * Checkpoint-010 / handoff: do not gate HP unmute on dai_mute. * ALSA mute_stream(1) on a prior close left dai_mute stuck, so @@ -2492,17 +3033,31 @@ int cs42l81_post_iis_start(void) * TXCOM=6 — silent jack with "perfect" digital telemetry. * asp_gate_unmute=0 (default): always force 0x527=0x60 / 0x401&3=2. */ - if (!asp_gate_unmute || !probe) { + { + /* + * Unmute and restore gain. Nothing else. + * + * This used to write 0x0229 = 0x41 and 0xC96F = 0x0E here, + * both lifted from D3280(4) -- the standby sequence. 0x0229 is + * written by sub_D3280 and by nothing else in the whole image, + * only ever as 0x40 or 0x41, and 0x41 is the value standby + * leaves; play was overwriting state 1's 0x40 with it. 0xC96F + * is the 2v5 backpower rail, whose only legitimate writers are + * sub_400330's -8 dB crossing logic and standby, and 0x0E is + * the rail-DOWN value -- so starting playback dropped the rail + * and bypassed the state machine that owns it. + * + * The gain does need restoring: sub_183138 ends the rate + * programming muted at -90 dB by design. + */ c->dai_mute = false; - cs42l81_write(c, 0x0229, 0x41); - cs42l81_write(c, 0xc96f, (u8)(c96f_final & 0xff)); cs42_play_unmute(c); cs42l81_apply_user_vol(c); } cs42l81_log_start_state(c, "post_iis"); cs42_log_final_state(c, "post_iis"); mutex_unlock(&c->lock); - return asp_gate_unmute ? probe : 0; + return 0; } EXPORT_SYMBOL_GPL(cs42l81_post_iis_start); @@ -2565,8 +3120,8 @@ static ssize_t probe_2f_store(struct device *dev, struct device_attribute *attr, mutex_lock(&c->lock); for (i = 0; i < n; i++) { cs42l81_read(c, 0x002f, &r2f); - dev_info(&c->spi->dev, "probe_2f[%u]=0x%02x synced=%d\n", - i, r2f, cs42l81_asp_synced(r2f)); + dev_info(&c->spi->dev, "probe_2f[%u]=0x%02x ready=%d\n", + i, r2f, !!(r2f & 0x80)); } mutex_unlock(&c->lock); return count; @@ -2581,11 +3136,14 @@ static ssize_t force_play_store(struct device *dev, struct device_attribute *att if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') return -EINVAL; mutex_lock(&c->lock); + /* + * Same rule as the real play path: unmute and set the gain. The + * 0x0229 and 0xC96F writes that used to be here are standby values + * out of D3280(4), and the raw 0x0527/0x0401 pair is what + * cs42_play_unmute() already does properly. + */ c->dai_mute = false; - cs42l81_write(c, 0x0229, 0x41); - cs42l81_write(c, 0xc96f, 0x1e); - cs42l81_write(c, 0x0527, 0x60); - cs42l81_rmw(c, 0x0401, 0x03, 0x02); + cs42_play_unmute(c); cs42l81_apply_user_vol(c); cs42l81_log_start_state(c, "force_play"); mutex_unlock(&c->lock); @@ -2602,7 +3160,7 @@ static ssize_t asp_lock_store(struct device *dev, struct device_attribute *attr, if (buf[0] != '1' && buf[0] != 'y' && buf[0] != 'Y') return -EINVAL; mutex_lock(&c->lock); - ret = cs42l81_asp_lock(c); + ret = cs42_asp_status(c); mutex_unlock(&c->lock); return ret ? ret : count; } @@ -2691,8 +3249,10 @@ static int cs42l81_dai_mute_stream(struct snd_soc_dai *dai, int mute, int stream /* Do not F141C-mute on ALSA mute(1). Short START/STOP bursts were * remuting HP 20-50ms after play_start. Real stop is 42D364 on IIS. */ - if (!mute) + if (!mute) { cs42l81_apply_user_vol(c); + cs42l81_apply_mute(c); + } mutex_unlock(&c->lock); dev_info_ratelimited(&c->spi->dev, "DAI mute=%d user_vol=%u%s\n", mute, c->user_vol, @@ -2787,6 +3347,8 @@ static void cs42l81_vol_workfn(struct work_struct *work) if (vol != prev || unmute) { c->user_vol = vol; cs42l81_apply_user_vol(c); + if (unmute) + cs42l81_apply_mute(c); } mutex_unlock(&c->lock); @@ -2922,7 +3484,7 @@ static int cs42l81_sw_put(struct snd_kcontrol *kcontrol, mutex_lock(&c->lock); changed = mute != c->dai_mute; c->dai_mute = mute; - cs42l81_apply_user_vol(c); + cs42l81_apply_mute(c); mutex_unlock(&c->lock); return changed; } @@ -2994,7 +3556,6 @@ static int cs42l81_probe(struct spi_device *spi) atomic_set(&c->vol_steps, 0); INIT_WORK(&c->vol_work, cs42l81_vol_workfn); INIT_DELAYED_WORK(&c->asp_post_work, cs42l81_asp_post_workfn); - INIT_DELAYED_WORK(&c->jack_work, cs42_jack_workfn); spi_set_drvdata(spi, c); mutex_lock(&c->lock); @@ -3049,7 +3610,6 @@ static void cs42l81_remove(struct spi_device *spi) } cancel_work_sync(&c->vol_work); cancel_delayed_work_sync(&c->asp_post_work); - cs42_jack_poll_stop(c); c->component = NULL; } if (cs42l81_dev == c) diff --git a/sound/soc/apple/n31-audio-rates.h b/sound/soc/apple/n31-audio-rates.h index d11ca552e9dab4..2485f1059b1047 100755 --- a/sound/soc/apple/n31-audio-rates.h +++ b/sound/soc/apple/n31-audio-rates.h @@ -15,13 +15,30 @@ #include /* - * Every rate the hardware actually has a divider for. Both the codec and - * the IIS DAI advertise exactly this set: they used to advertise only - * 44.1/48 while the table below carried nine entries, so 8/11.025/12/16/ - * 22.05/24/32 kHz streams were refused by ALSA despite the silicon - * supporting them. + * Advertise only the two rates RetailOS actually uses. + * + * The table below carries a divider for every rate the silicon can clock, + * and for a while all nine were advertised on the reasoning that refusing a + * stream the hardware could take was needlessly strict. The cost of that + * only became clear once the codec was being compared against stock: OSS + * opens /dev/dsp at 8 kHz by default, so every playback began by + * configuring the codec for 8 kHz -- rate code 1, CLKDIV 1500, and the SRC + * arm of sub_183138 -- before settling on the rate that was actually + * wanted. RetailOS never runs 8 kHz. Those probes were walking the part + * down a path stock has never taken, on every single open, while we were + * trying to work out why stock's path produced no sound. + * + * So the DAI advertises 44.1 and 48 kHz and nothing else, and anything + * lower is resampled up before it reaches the hardware. OSS emulation and + * plughw insert a rate plugin and do this transparently; an application + * opening hw: directly at a lower rate is now refused, which is the + * deliberate half of the trade. + * + * The full table stays: n31_resolve_rate() still uses it to pick the + * nearest supported rate, and the dividers are real. This changes only what + * ALSA is told the DAI will accept. */ -#define N31_RATE_MASK (SNDRV_PCM_RATE_8000_48000 | SNDRV_PCM_RATE_12000 | SNDRV_PCM_RATE_24000) +#define N31_RATE_MASK (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) #define N31_RATE_DEFAULT 44100u From 6496ae3c07c0cc42014478fd8f00b71914774091 Mon Sep 17 00:00:00 2001 From: andrew867 Date: Sat, 29 Aug 2026 23:44:31 -0230 Subject: [PATCH 31/31] n31: dts and gpio refresh from the working tree Carried along with the storage and audio work of this session; no behavioural change intended beyond what those commits describe. Co-Authored-By: Claude Opus 5 --- arch/arm/boot/dts/samsung/s5l8740-n31.dts | 2 +- drivers/gpio/gpio-s5l8740.c | 44 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/arch/arm/boot/dts/samsung/s5l8740-n31.dts b/arch/arm/boot/dts/samsung/s5l8740-n31.dts index 187325dab137ae..cc1ed6288d367a 100755 --- a/arch/arm/boot/dts/samsung/s5l8740-n31.dts +++ b/arch/arm/boot/dts/samsung/s5l8740-n31.dts @@ -24,7 +24,7 @@ chosen { /* U-Boot CONFIG_BOOTARGS overrides this. g_ether is built-in (0525:a4a2). */ - bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=0 clk_ignore_unused init=/init"; + bootargs = "console=tty0 fbcon=font:MINI4x6 earlyprintk nohlt panic=0 clk_ignore_unused init=/init netconsole=6666@192.168.7.2/,6666@192.168.7.1/"; stdout-path = "serial0"; /* * U-Boot ft_board_setup adds apple,n31-isys-addr / apple,n31-isys-size diff --git a/drivers/gpio/gpio-s5l8740.c b/drivers/gpio/gpio-s5l8740.c index 04bd74573a25c8..597a1c712ab1d3 100755 --- a/drivers/gpio/gpio-s5l8740.c +++ b/drivers/gpio/gpio-s5l8740.c @@ -41,6 +41,7 @@ #include #define S5L8740_GPIO_BANK_STRIDE 32 +#define S5L8740_GPIO_PCON_OFF 0x00 #define S5L8740_GPIO_DIN_OFF 0x04 /* sub_428F70 target: input/pull enable, one bit per pad. */ #define S5L8740_GPIO_INEN_OFF 0x0c @@ -528,6 +529,49 @@ void s5l8740_iis0_pad6_enable(unsigned int mode) } EXPORT_SYMBOL_GPL(s5l8740_iis0_pad6_enable); +/* + * Set one pad's function nibble and direction, and nothing else. + * + * Audio checkpoint-010 records the stock pad state while RetailOS plays: + * bank0 PCON 0x32112224 / DIR 0xFF and bank2 PCON 0x02230000 / DIR 0x70, + * which is GPIO6 func2, GPIO7 func3, GPIO20 func3, GPIO21 func2 and GPIO22 + * func2, all outputs. Linux sets 7 and 20 and leaves 6, 21 and 22 alone. + * + * That matters because everything upstream now matches the oracle exactly + * -- TXCON, TXCOM, CLKDIV 272, CLKCON +0x18 and +0x1C, IIS STATUS, the + * PL080 channel -- and the jack is still silent. Clocks and status can all + * be right while the serialiser's data pin is not muxed out of the SoC. + * + * A whole-word PCON write would take pins 0..5 with it, which is why this + * is per-pad read-modify-write. The doc is explicit that GPIO6 must not go + * through GPIOCMD, so the nibble is written directly. + */ +int s5l8740_gpio_set_pad(unsigned int gpio, unsigned int func, bool out) +{ + struct s5l8740_gpio *sg = s5l8740_n31; + void __iomem *bank; + unsigned int pin = gpio & 7; + u32 v; + + if (!sg || !sg->base || func > 15) + return -ENODEV; + bank = sg->base + (gpio >> 3) * S5L8740_GPIO_BANK_STRIDE; + + v = readl(bank + S5L8740_GPIO_PCON_OFF); + v &= ~(0xfu << (pin * 4)); + v |= (func & 0xf) << (pin * 4); + writel(v, bank + S5L8740_GPIO_PCON_OFF); + + v = readl(bank + S5L8740_GPIO_DIR_OFF); + if (out) + v |= BIT(pin); + else + v &= ~BIT(pin); + writel(v, bank + S5L8740_GPIO_DIR_OFF); + return 0; +} +EXPORT_SYMBOL_GPL(s5l8740_gpio_set_pad); + void s5l8740_gpio_log_iis0_pads(const char *tag) { struct s5l8740_gpio *sg = s5l8740_n31;